我要搞死看门狗(第一季)

先来水一波,习惯性的copy科普一下,下一集期待更新中:

WDT硬件设计

WDT原理

1. 原理

    在手机Soc Chip中,里面的AP跑着linux操作系统软件,而任何软件都可能存在各种问题,如果遇到了这些异常,软件可能陷入死循环,导致手机变成“砖头”,如果没有其他硬件辅助,那么只能断电(拔电池)然后重新开机才行。为了避免出现这种情况,芯片内部增加了一个看门狗模块,这个模块专门检测CPU运行状态,只要出现卡死就复位系统。

    WDT全称是watchdog timer,就是看门狗模块,看门狗其实就是一个可以在一定时间内被复位的计数器。当看门狗启动后,计数器开始自动计数,经过一定时间,如果计数没有被复位,计数器达到指定数值就会发出复位信号,很多设备包括CPU接到这个信号而复位重启(俗称“被狗咬”)。为了保证看门狗不发出复位信号,就需要在看门狗允许的时间间隔内对看门狗计数器清零(俗称“喂狗”),计数器重新计数。如果系统正常并保证按时“喂狗”,那么就相安无事。一旦程序故障卡死,没有“喂狗”,系统“被咬”复位。

2. 种类

    嵌入式系统中主要可以分为两种类型的看门狗:

  • Soc芯片内部集成WDT,这是Soc常用的设计。当然PC上可能用独立的看门狗芯片。
  • 软件模拟看门狗,只要有个timer就可以模拟。

Mediatek WDT硬件设计

1. 结构
    在现有Soc芯片中,只有1个看门狗硬件模块,这个模块被包含在一个叫RGU的模块里。在早期平台中,比如MT6577,MT6575除了RGU里的WDT外,每个CPU都有一个local WDT,可以保证每个CPU不卡死。而MT6577之后就只有RGU里的WDT了,如图:
     在只有一个WDT的情况下如何保证每个CPU不卡死呢?这里需要软件设计使它都可以喂狗才行。软件设计部分将在下一章节介绍。
     RGU全称是reset generation unit,在datasheet有如下框图:

    可以看到WDT在RGU里只是其中一个模块,还有其他模块可以产生复位信号,比如thermal,当温度过高会触发IRQ或直接复位,这也是一种硬件保护措施。复位信号经过Mixer后会分出2个信号,grst_b会复位芯片内部模块,还有1个信号通过芯片管脚WATCHDOG复位外部芯片。有时为了debug,也可以测量WATCHDOG pin脚(正常是高电平,有复位信号时低电平),看这次重启是否是WDT等触发的。

2. dual mode

    WDT功能,大家可以看datasheet里的寄存器介绍,比如包含了

  • WDT_MODE:功能开关
  • WDT_LENGTH:超时计数
  • WDT_RESTART:写该寄存器触发计数清0
  • WDT_STA:状态
  • WDT_INTAVEL:reset信号长度
  • WDT_SWRST:写该寄存器直接触发复位

    这里介绍一个dual mode功能,通常CPU发生卡死,我们需要知道卡死位置,分析原因,然后改进,而不希望直接复位。设计的方法是:WDT超时后先不发复位信号,而是先送出IRQ(如上面的框图),WDT IRQ通常配置成CPU的FIQ,如果CPU只是软件卡死(内核死锁,中断过多等),会响应该FIQ,然后我们在FIQ里收集异常信息和CPU寄存器信息,然后再走正常的panic流程。重启后我们就有信息分析此次WDT timeout的原因了。为了保证IRQ发出后,CPU不卡死,WDT再次计时,如果在panic流程又卡死了,就会由WDT发出复位信号复位。

    简单讲:WDT超时先发出IRQ,WDT重新计时,如果再次超时WDT直接发出复位信号。可以看到WDT超时后分2个阶段,第1阶段发出IRQ用于收集异常信息,第2阶段直接复位。通常我们在第1阶段就完成异常信息收集并通过WDT_SWRST寄存器完成复位,而不用等到第2阶段的。这种情况我们称为HWT(Hardware Watchdog Timeout)

    事实上任何事情总有意外,比如CPU因为PMIC提供的电压低,或硬件故障等原因导致WDT超时发出的IRQ在CPU端得不到响应,WDT只能通过第2阶段复位芯片。这种情况我们称为HW reboot。HW reboot通常和硬件有较大关系。比如:

  • 硬件故障,电压或频率异常
  • 总线卡死
WDT软件设计

设计原理

    上一章节有讲Soc芯片内部只有1个WDT模块,但CPU有多颗,为了保证每个CPU卡死都可以顺利复位,就必须借助软件。WDT本质是个计数器并且支持dual mode,目前我们将超时时间(第1阶段超时)设定为30s,在发出IRQ之后超时时间(第2阶段超时)不可设定,各个平台可能不一样,一般在30s~60s之间。
    配置好后,还需要设计喂狗模块,这是重中之重。流程如下:
  • bit位代表CPU的喂狗状态:CPU核数每个平台都不太一样,我们用bit位代表1个CPU,比如4个CPU就用4个bit,我们用kick_bit变量表示。
  • 喂狗进程负责喂狗:在喂狗模块初始化时针对每个CPU创建的对应的内核实时进程(优先级最高)并绑定到对应的CPU上(也就是说这个进程只运行在指定CPU上,不会发生迁移),这些进程喂狗(其实就是将kick_bit对应的bit位置1而已,并没有真正喂狗)后进入睡眠,喂狗间隔是20s,如此循环反复。
  • 真正喂外部WDT狗:任何一个喂狗进程在喂狗之后,会检查kick_bit变量,如果该值等于cpus_kick_bit变量(这个变量的每个bit代表对应CPU online情况,如果CPU online,对应bit置1),表示所有CPU都已喂狗,这时才真正喂外部WDT狗,同时将kick_bit清0,进入下一轮喂狗流程,如此循环反复。

    用一张框图总结上面的流程(以4核CPU为例):

 这里的wdtk-x就是对应CPU的喂狗进程。以上的一套设计可以保证各个CPU卡死都可以通过看门狗复位。

    喂狗间隔是20s,而超时时间是30s,也就是说最长能容忍卡住的时间是10s(卡一小会还是可以的),超过这个时间,系统就会复位了。这里还有问题,由于喂狗进程之间没有同步,是否有可能存在刚开始一起喂狗,之后渐渐出现先后呢?误差肯定有的,但在任何30s时间里,喂狗进程都会喂1次狗的(因为喂狗间隔20s,每个CPU肯至少会喂1次狗)。而只要CPU卡死超过10s就复位了。

    多核CPU不可能一直都是online,系统会根据负载做hotplug,在某颗CPU power down/power up时,会更新cpus_kick_bit变量并将kick_bit变量清0,同时喂外部WDT狗。

    当只剩1个CPU时,并且该CPU要进入睡眠,此时kernel进入suspend状态,WDT模块当然也要关闭的,唤醒时再重启开启。那睡眠后的流程卡死怎么办?那已超出WDT管辖的范围了,需要用其他硬件保证,不在该文章讨论范围。

WDT驱动分布

    android系统中,启动分好几个阶段:preloader、lk、kernel、android。因此每个阶段都要配置WDT,才能保证不卡死。这里我们介绍kernel阶段的WDT驱动,其他阶段是类似的。

 
kernel WDT驱动

    这里我们没有用到kernel原生的WDT驱动,而是前面章节讲的那套机制。具体代码位置:

    alps/kernel-3.10/drivers/misc/mediatek/wdt/$platform/mtk_wdt.c。下面我们以L1.MP6的MT6752的wdt驱动做讲解。
 
初始化

    流程是:module_init(mtk_wdt_init) => mtk_wdt_init() => mtk_wdt_probe()。

    在mtk_wdt_probe()函数里,做了如下几件事:

  • 注册WDT IRQ处理函数wdt_fiq(),kernel本身是没有使用FIQ的,因此WDT FIQ发生时一定会被处理,除非FIQ被关闭或硬件故障了。由于64bit kernel默认支持ATF(ARM Trusted Firmware),FIQ为security interrupt,kernel是无法处理security interrupt的,因此FIQ会路由到ATF里处理。在后面的流程中会详细介绍。
  • 设定WDT超时时间30s。
  • 调用mtk_wdt_mode_config()初始化WDT,这里打开的dual mode。

    这样就完成WDT的初始化了,WDT初始化是比较早的,基本上3s内就完成了(kernel时间)。以下是72KK版本的uart log中搜索wdt结果:

睡眠/唤醒

    在系统进入睡眠时,WDT要被关闭,不过不是在WDT suspend里关闭WDT,因为suspend不是睡眠最后的步骤。这里会在sleep模块通过调用WDK的wd_suspend_notify()来间接调用mtk_wd_suspend()来关闭WDT的。同理唤醒时也是早于resume流程就被sleep模块通过WDK的resume_notify()调用mtk_wd_resume()重新启动WDT。

重启

    系统重启(比如adb reboot)是通过WDT来完成复位的,在驱动里提供了wdt_arch_reset()函数,这个函数通过写WDT_SWRST寄存器完成软件复位。基本上重启的log都长这样:

    红线以上是正常的log,写完WDT_SWRST寄存器基本就复位了,但有些平台并不是立即生效,需要过几百毫秒才完成复位,因此有可能看到红线以下的log,这是正常的。 

喂狗函数

    WDT启动后就开始计时了,如果没人喂狗就会触发FIQ,然后在硬复位。因此需要提供了mtk_wdt_restart()函数,调用一次就将计数清0。

    如果有驱动需要长时间关中断运行,比如开机时的TP固件升级,就需要在里面添加喂狗动作,防止HWT。

开关WDT

    有时为了调试,我们需要关闭看门狗,驱动里也提供了开启/关闭函数:mtk_wdt_enable()。如果你要关闭WDT,可以直接在代码里调用这个函数,那么WDT就永久关闭了。

    在WDK驱动里还提供了用户接口,可以很方便在adb shell里关闭/打开WDT。

结语

    WDT驱动比较简单,基本都是控制WDT硬件寄存器达到所需效果。重点还在于WDK驱动(喂狗模块)。

WDK驱动

WDK驱动

    wdk是喂狗模块,由于只有一个WDT,而需要保护多个CPU不卡死,根据前面设计的原理来设计wdk驱动,驱动具体位置:

    kernel-3.10/drivers/misc/mediatek/wdk,里面主要有wd_common_drv.c/wd_api.c
    wd_api.c负责对外接口,wd_common_drv.c负责喂狗工作。

对外接口

     get_wd_api ()可以获取到通用的wdk api结构体,里面提供了WDT和WDK所需的操作。使用方法如下(手动喂狗):
    下面介绍常用的几个api函数:
    

初始化

    流程1: arch_initcall ( init_wk_check_bit ) =>  init_wk_check_bit () =>  wk_cpu_update_bit_flag ()。完成cpus_kick_bit、kick_bit这2个重要变量初始化。cpus_kick_bit表示需喂狗CPU位图,kick_bit表示已喂狗CPU位图,每个bit对应CPU。
    流程2: late_initcall ( init_wk ) =>  init_wk () =>  mtk_rgu_sysfs ()、 start_kicker_thread_with_default_setting ()、 wk_proc_init ()、 register_cpu_notifier (& cpu_nfb )。
  • 通过mtk_rgu_sysfs()创建了sysfs文件系统节点。
  • 通过start_kicker()创建per CPU的喂狗进程wdtk-x,这些进程都是系统中优先级最高的进程,执行的核心函数是kwdt_thread(),里面实现了前面提到的喂狗框架。
  • 通过wk_proc_init()创建了proc文件系统节点/proc/wdk。我们可以通过它来设定超时时间、喂狗时间、打开/关闭WDT。格式如下(如关闭WDT):echo 0 20 30 0 0 > /proc/wdk
    • 参数1:目前没用到,可以忽略,直接写0。
    • 参数2:喂狗间隔,默认20,单位秒。
    • 参数3:超时时间,默认30,单位秒。
    • 参数4:目前没用到,可以忽略。
    • 参数5:打开/关闭WDT。
  • 根据当前online CPU状况更新cpus_kick_bit并注册CPU hotplug回调函数,因为CPU会根据负载上电/下电,因此需要及时更新cpus_kick_bit、kick_bit。

    需要注意的是:

  • WDT初始化和WDK初始化是处于kernel初始化的不同阶段,WDT通过module_init()完成,而WDK通过late_initcall(),中间隔着各种驱动,因此如果有些驱动执行时间太长,会导致开机30s HWT的问题,这个在后面章节提到。
  • KK1.MP11以前的版本启动喂狗进程时,会直接调用到kwdt_thread(),里面会设置一遍kick_bit,最后等于cpus_kick_bit然后喂WDT,但实际上第1次kick并没有累加起来,查看log:

  • 看到所有wdk进程起来后,都会设置kick_bit(就是local_bit),但是没有累加起来,无法等于0xf,也就无法喂WDT,真正喂狗是在20s之后,从时间轴来看:

  • 这是为什么呢?原因是start_kicker()里没创建完进程,就会调用wk_cpu_update_bit_flag()将kick_bit清0,因此无法累加。KK1.MP11及之后的版本就没有再调用wk_cpu_update_bit_flag(),因此wdk初始化完就会完成第1次喂狗。

喂狗

    kwdt_thread()完成喂狗动作,里面就是一个循环:

  • 如果需要更新超时时间,则在这里调用wdt函数更新。
  • 更新对应CPU的kick_bit,如果等于cpus_kick_bit则清除kick_bit并喂WDT。
  • 显示UTC时间,主要用于kernel和android时间同步,这条log非常重要,如果要看android log当时对应的kernel log,就可以通过这条log找到大致的位置。由于WDK是每20s跑一次,因此这条log也是每20s输出一次,如下:[   25.876493]<1>[thread:187][RT:25867046616] 2015-01-01 00:10:38.35106 UTC; android time 2015-01-01 00:10:38.35106
  • 睡眠20s

    各个CPU的kwdt_thread()没有同步,因此各自喂狗的时间不定,但只要喂狗间隔不超过30s就没有事。

CPU hotplug

    由于有注册CPU hotplug回调函数,因此在某颗CPU上电时,会将cpus_kick_bit对应的bit置位并同时喂狗。下电时,会清除cpus_kick_bit对应bit位并同时喂狗。

超时触发流程

    如果喂狗进程没有及时喂狗,WDT就会超时触发FIQ,而分析解决HWT的问题很依赖WDT FIQ能输出什么信息。因此必须要熟悉WDT FIQ流程。 32bit/64bit kernel处理FIQ方式不一样,因此要分开讲解。
32bit kernel FIQ触发流程
 
    在WDT驱动里有注册了WDT FIQ中断处理函数 wdt_fiq (),当WDT发出中断后,kernel流程如下:
  • __vectors_start + 0x1C:这是异常向量表FIQ入口,所有FIQ中断都会导向这里。
  • fiq_glue ():FIQ入口直接调用了这个函数,此时处于FIQ mode,用独立的栈,只要还在这个独立的栈里就无法调用printk输出log,原因是printk使用了current(),会从栈底取出thread_info结构体,而该独立的栈咩有thread_info结构体。
  • fiq_isr ():这个函数更新了fiq_step。后面会继续讲fiq_step内容。
  • wdt_fiq ():到了WDT注册的中断函数了,这里还是无法使用printk,因此只能调用 aee_wdt_printf ()打印到buffer里。这里印出了最重要的信息kick和check(对应的kick_bit和cpus_kick_bit),比如:kick=0x00000007,check=0x0000000f。得到kick和check就知道是哪些CPU没有喂狗了,接下来重点关注没喂狗的CPU调用栈等信息。
  • aee_wdt_fiq_info ():这个函数首先将栈切换为当前进程栈,就可以用printk了。另外当前CPU的寄存器打印到per CPU buffer里。最后只允许一个CPU继续往下走,其他CPU直接死循环等待重启了。
  • aee_wdt_irq_info ():这个函数将输出重要log
    • 喂狗,防止超时。
    • 停止其他CPU。
    • 将per CPU buffer输出到last_kmsg。注意:只输出到last_kmsg,kernel log是看不到的。last_kmsg里的log如下:

cpu 0 preempt=1, softirq=0, hardirq=0 
cpu 0 r0->r10 fp ip sp lr pc cpsr orig_r0
00000000 00000000 00007530 00000001 c0940af8 00000000 c0940af8 c0c46a60 
c08ce0fc c0c26000 00000000 c0c27f3c c0c27f40 c0c27f30 c0344a1c c03444fc 
20010093 20010013 
cpu 0 stack [c0c27f30 c0c27fb0]
00000000 c0c27f40 c0344a1c c03444f0 c0c27f6c c0c27f50 c0344a4c c03449fc 
c0344a24 c0c26000 c0c26000 c0cd6ea8 c0c27f7c c0c27f70 c000ffdc c0344a30 
c0c27fa4 c0c27f80 c0091920 c000ffc4 c0c26000 c0cd67c6 c0c26000 c0c26000 
c08c9ba8 c16ed680 c0c27fbc c0c27fa8 c08b20f8 c00917ec 00000000 c0c46d54

cpu 0 backtrace : c0344a1c, c0344a1c,

cpu 1 preempt=0, softirq=0, hardirq=0 
cpu 1 r0->r10 fp ip sp lr pc cpsr orig_r0
......

    • 如果后面流程卡住了,那么能参考的信息只有last_kmsg了,因此这个信息尤为重要,里面包含CPU寄存器和调用栈。
    • 如果没有卡住,那么在kernel log也可以看到这些信息,类似如下:

[  203.439400]CPU 1 FIQ: Watchdog time out
preempt=1, softirq=0, hardirq=0
pc : ffffffc00012ed10, lr : ffffffc00012ecd8, pstate : 0000000060000145
sp : ffffffc003143c60
......
[  203.460206]Backtrace : 
[  203.460493]<0>-(0)[143:kworker/u8:4]ffffffc00012ecd8, ffffffc00012ec24, 
[  203.461328]<0>-(0)[143:kworker/u8:4]ffffffc0000ca238, ffffffc0000c1a00, 
[  203.462162]<0>-(0)[143:kworker/u8:4]ffffffc00008447c, [  203.462800]<0>-(0)[143:kworker/u8:4]
[  203.462800]<0>==========================================

      • 我们重点看CPU Backtrace部分,这些都是函数地址,需要将地址转换为函数名和所在文件、行号。则需要用到arm-linux-android-addr2line(用于32位)/aarch64-linux-android-addr2line(用于64位,详情请查看quick start => Native Exception问题深入解析 => 进阶篇: coredump分析 => GNU tools)和vmlinux(必须是当时一起编译出来的,如果后面有更新过,addr2line得出的结果可能错误)。比如:

            ./prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.9/bin/aarch64-linux-android-addr2line -Cfe vmlinux 0xffffffc00014e68c => ring_buffer_unlock_commit()

            全部转换后就可以看到完整的调用栈了,有助于我们分析哪里卡住。

    • 调用mt_aee_dump_sched_traces()输出中断信息。这个信息只有eng版本才打开(user版本打开方法请参考[FAQ15102]如何调试IRQ引起的HWT?)。这个有什么用呢?HWT有一个原因是IRQ太过频繁导致,因此IRQ信息能直接看出原因
      • 当前CPU中断信息,可以看到上次IRQ/SoftIRQ/tasklet/hrtimer/SoftTimer信息,如果HWT时调用栈在IRQ里,还可以看到该IRQ的信息,可以判断执行是否太久
      • IRQ Status会印出所有中断在Dur时间段里触发次数,如果太多就有问题了(比如>1000次每秒)。

    • 将aee wdt buffer打印到last_kmsg,之前是per CPU buffer,这次是aee wdt buffer:
kick=0x00000007,check=0x0000000f
Qwdt at [292641.063701]

,这里才能从last_kmsg看到kick/check信息。

  • BUG (),之后就是正常的die/panic流程了,所以kernel log总能看到PC is at aee_wdt_irq_info。看到这个信息就可以判断是HWT了。
64bit kernel FIQ触发流程
 
    64bit kernel有ATF(ARM Trusted Firmware),FIQ是security interrupt,必须路由到secure world处理,之后才会返回kernel完成剩下的工作。

    建议了解下ATF相关内容再来看下面的知识点。

    ATF初始化时就会注册WDT FIQ处理函数,该函数保存per-CPU寄存器信息并返回kernel处理。
    仅有ATF的流程比较简单,由于在ATF时,屏蔽所有中断,因此WDT FIQ只能在EL1 kernel时发生,如下:
    如果存在TEE OS则更复杂些,WDT FIQ可能在EL1 kernel或S-EL1 TEE OS触发,最后都会路由到S-EL1 OS处理,然后再返回ATF,最后到EL1 kernel,如下:
 
    除了底层流程不一样外,kernel流程是差不多的,aee_wdt_atf_info()等效于aee_wdt_irq_info()。因此可以参考上面的内容。
 
FIQ step

    kernel发生异常或HWT,当时的系统是不稳定的,无法保证panic或上面的WDT FIQ流程能顺利走完。因此我们使用fiq step标记走到了哪里,如上面的WDT fiq流程里,基本上每走几步就通过aee_rr_rec_fiq_step()记录特定的数字。在db解开后的SYS_REBOOT_REASON、SYS_LAST_KMSG和__exp_main.txt都会有fiq step以供查看,如下:

WDT status: 2 fiq step: 41  exception type: 1
    详情可参考:[FAQ14332]SYS_LAST_KMSG里的hw_status和fiq step的含义。
    一般kernel异常,fiq step都不为0,为0表示CPU没有走到这些步骤当中,可能是没异常或CPU卡死跑不了代码。
 
结语

    请结合代码理清一遍流程,知道哪条log从哪个函数打印出来。如果需要加log调试,也知道该加在哪个位置。

    学完WDT/WDK相关内容并不代表就可以独立解决HWT/HW REBOOT的问题了,还需要很多kernel基础知识支撑才行,因为任何模块都不是独立存在,在后面的案例分析就会有更深刻的体会了。

分析方法

HWT

    WDT第1阶段超时将触发FIQ,此时系统收到FIQ并处理,最后调用BUG()走正常的kernel exception流程,生成的db类型就是HWT:hardware watchdog timeout。
    根据HWT发生的时间点可以分为2类。

开机30s超时

    kernel初始化流程:start_kernel() => rest_init(),这个过程都是0号进程完成,在rest_init()就创建了1号进程执行kernel_init(),接着=> kernel_init_freeable() => do_basic_setup() => do_initcalls()。这个过程是单线程的,都是1号线程完成。

    WDT/WDK驱动都在里面初始化,而WDT通常很早就完成初始化,WDK驱动较为靠后,这中间夹着其他驱动,如果其他驱动因为异常而卡较久时间,就有可能到30s都没有喂狗而超时。那原因和解决方法有哪些呢?主要有:
  • 驱动初始化时间太长。比如TP需要升级固件,简单的解决方法是手动喂狗。
  • 没有配置好或HW故障,打印出很多error log。特别是I2C,出现异常会连续try几次,如果发的cmd较多,会严重拉长初始化时间。还有其他模块如MSDC异常等。因此应该修复这类问题。
  • 其他HW故障或代码逻辑卡死。比较复杂,需要具体问题具体分析了。

    初始化时间长的问题,可以通过uart log检索[1:swapper/0]关键字分析就行了。后面有详细例子讲解。

    分析所需材料:

  • db、vmlinux和uart log
  • db在sd卡下的mtklog/aee_exp,或/data/aee_exp目录下
  • vmlinux在codebase里的out/target/product/$proj/obj/kernel_obj/vmlinux

kernel运行过程中30s超时

    此时WDK已初始化好,而喂狗进程都是优先级最高的实时进程,其他任何进程都无法卡到喂狗进程。那谁可以卡到喂狗进程呢?换句话说,谁可以影响到系统调度?答案有:

  • 长时间关闭中断。这个直接无法调度了,还可以细分为:
    • 关中断的CPU死锁
    • 关中断后陷入循环
  • 中断频繁触发。则导致CPU超载,进程基本没有机会被执行到。
  • 长时间关抢断。
  • 其他HW故障,如总线卡死或HW不稳定。

    分析方法

  • 获取kick/check bit得知哪些CPU没喂狗,查看last_kmsg/kernel log看这些CPU的调用栈(《超时触发流程》有讲如何转换CPU Backtrace),看是否卡在锁里。
  • 如果怀疑中断过多,则需要eng版本复现抓取db,看isr monitor显示的各个中断触发的频率,判断是哪个中断引起,再找对应的驱动解决。
  • 如果发现不符合逻辑引起的卡死,可能是HW不稳定,需要按其他方式处理。

    分析所需材料

    • db、vmlinux,必要时需要uart log或mobile log。

 

  • db在sd卡下的mtklog/aee_exp,或/data/aee_exp目录下
  • vmlinux在codebase里的out/target/product/$proj/obj/kernel_obj/vmlinux

HW REBOOT

HW reboot原因

    WDT第1阶段超时将触发FIQ,如果CPU没有响应或CPU响应了但没有在第2阶段超时时间内完成重启,就好导致第2阶段超时发出复位信号复位整个系统,生成的db类型就是HW reboot。

    可以看到HW reboot定义很简单,这样导致了许多问题都导向了HW reboot,而处理问题的方法都不一样。那有哪些异常会导致HW reboot?有如下:

  • 多个异常先后发生,这种情况,我们只关注第1个异常就够了,之后的异常可能因为系统已不稳定发生的异常。比如发生KE,在panic flow卡住引起HWT,HWT flow又卡住直接HW reboot了。这种情况,我们只关注KE而不是HW reboot。
  • ATF发生panic,ATF层是无法处理任何中断(包括WDT FIQ),里面发生了panic之后都是死循环等待HW reboot。这个不能按HW reboot方式处理,需要看atf log分析解决问题。
  • preloader/lk发生panic,和ATF一样,发生HW reboot。但不能按HW reboot的方式处理,这种情况应该看uart log(preloader/lk部分),分析解决问题。
  • HW故障,CPU无法正常执行。这个就是纯粹的HW reboot了,也是我们后面要关注的。

HW reboot调试信息

    基于HW reboot产生原因的复杂性。我们要学会如何区分呢?首先我们要了解哪些信息可以给我们参考。HW reboot没走任何软件流程,是直接复位的,无法知道CPU处于什么状态,因为重启后什么都丢了,不像kernel panic或HWT有详尽的CPU寄存器和调用栈参考。其实HW reboot还是有些硬件模块记录了当时的情况。

wdt status/fiq step

    在preloader初始化WDT前保留了WDT_STA寄存器的值,并转化为wdt status(或者叫hw_status),这个值保存在SYS_REBOOT_REASON/SYS_LAST_KMSG/__exp_main.txt,和fiq step一起。详情可参考:[FAQ14332]SYS_LAST_KMSG里的hw_status和fiq step的含义。

    wdt status提供了重启的原因,1或5是HW reboot,2是SW reboot等。fiq step的值为0或非0提供了是否走了kernel panic或HWT的流程。如果fiq step不为0,即使wdt status为1或5,我们都不会当成HW reboot来分析。这种情况的原因是panic或HWT流程卡住导致HW reboot了,我们需要参考last kmsg,按普通的kernel panic或HWT分析,找问题的原因。

    如果wdt status为1或5且fiq step为0,那么这个就是HW reboot了。如何调试呢?好像这些信息无法帮我们找到问题。我们继续看还有哪些调试信息。

last pc

    从82/92及之后的平台,WDT发出复位信号的同时锁存了online CPU的PC/FP/SP。在重启后记录在db的SYS_REBOOT_REASON里,格式如下:

[LAST PC] CORE_0 PC = 0xc022c3ac(buffer_size_add_atomic + 0x1c), FP = 0xc0c278dc, SP = 0xc0c278c8
[LAST PC] CORE_1 PC = 0xc036b1ac(aee_smp_send_stop + 0x4), FP = 0xdd9fbee4, SP = 0xdd9fbeb8
[LAST PC] CORE_2 PC = 0xc036b1ac(aee_smp_send_stop + 0x4), FP = 0x25, SP = 0xdcf4bff8
[LAST PC] CORE_3 PC = 0x0( + 0x4), FP = 0x0, SP = 0x0

    有些平台有特别的设定,比如MT6795的PC值有特别的含义:

  • 0xa0a0a0a0a0a0a0a0:PC为空,表示没抓到last PC。
  • 0xb0b0b0b0b0b0b0b0:抓取last PC失败。

    还有PC所指地址代表的含义大家也要清楚才行,以前平台的AP是扁平结构,PC值所在位置要吗在user space:0~0xBF000000,要吗在kernel space:0xBF000000~0xFFFFFFFF,后面引入64位使问题变复杂了。在aarch64下,PC值可能值NS EL0 user space,也可以是NS EL1 kernel space,或者SEC EL3 ATF及SEC EL1 TEE OS的。因此就需要了解整个框架,才能进一步判断PC来自于哪一层了:

  • 64bit kernel space:0xFFFFFF8000000000~0xFFFFFFFFFFFFFFFF。其中0xFFFFFFC000080000开始就是kernel代码起始地址。
  • 64bit user space:0~0x7fffffffff,所有app都跑在这里。
  • ATF space:不同平台有不同的layout。
    • MT6752/MT6580/MT6795/MT6797的ATF代码段位于0x111000-0x12C400
    • MT6735/MT6753/MT6735M位于0x43001000-0x43020000
    • MT6755位于0x101000-0x120000

    通过last PC可以知道在最后时刻,各个CPU都在干什么。一般会检查PC所在函数:

  • 是否是个死循环。看之前是否关闭了FIQ,如果是,则需要检查代码。
  • 正在读写寄存器。读写已关闭clock/power的模块的寄存器会引起bus hang,引起HW reboot。这时要检查读写了哪个模块的寄存器,如果是chip内部模块,直接找Mediatek分析。
  • 不在任何space里。那可能跑飞了,多半是HW不稳定引起。需要测量CPU vproc/vsram/Vm等重要电压,看异常发生前是否发生过drop等异常现象。
  • 在正常函数里。但没有死循环或其他可疑的地方,这种也可能是HW不稳定,和上面的case一样测量重要电压。

system tracker

    上面有提到,如果是读写了已关闭clock/power的寄存器会引起bus hang,最后HW reboot。如何调试这种情况呢?在MT6595及之后引入了system tracker,这个硬件模块会监控bus情况,如果卡死会送一个abort信号给CPU,CPU会进入kernel panic流程。但有时这个bus hang会引起CPU bus卡死,导致CPU也动不了而变成HW reboot。不过bus hang信息还保留在system tracker模块中,重启后会被打包到HW reboot db里。

    如果db里存在SYSTRACKER_DUMP文件,那一定是发生过bus hang了,该文件内容如下:

read entry = 0, valid = 0x0, tid = 0x0, read id = 0x0, address = 0x0, data_size = 0x0, burst_length = 0x0
read entry = 1, valid = 0x0, tid = 0x0, read id = 0x0, address = 0x0, data_size = 0x0, burst_length = 0x0
read entry = 2, valid = 0x0, tid = 0x0, read id = 0x0, address = 0x0, data_size = 0x0, burst_length = 0x0
read entry = 3, valid = 0x0, tid = 0x0, read id = 0x0, address = 0x0, data_size = 0x0, burst_length = 0x0
read entry = 4, valid = 0x0, tid = 0x0, read id = 0x0, address = 0x0, data_size = 0x0, burst_length = 0x0
read entry = 5, valid = 0x0, tid = 0x0, read id = 0x0, address = 0x0, data_size = 0x0, burst_length = 0x0
read entry = 6, valid = 0x0, tid = 0x0, read id = 0x0, address = 0x0, data_size = 0x0, burst_length = 0x0
read entry = 7, valid = 0x0, tid = 0x7, read id = 0x203, address = 0x11230014, data_size = 0x2, burst_length = 0x0
write entry = 0, valid = 0x0, tid = 0x0, write id = 0x0, address = 0x0, data_size = 0x0, burst_length = 0x0
write entry = 1, valid = 0x0, tid = 0x0, write id = 0x0, address = 0x0, data_size = 0x0, burst_length = 0x0
write entry = 2, valid = 0x0, tid = 0x0, write id = 0x0, address = 0x0, data_size = 0x0, burst_length = 0x0
write entry = 3, valid = 0x0, tid = 0x0, write id = 0x0, address = 0x0, data_size = 0x0, burst_length = 0x0
write entry = 4, valid = 0x0, tid = 0x0, write id = 0x0, address = 0x0, data_size = 0x0, burst_length = 0x0
write entry = 5, valid = 0x0, tid = 0x0, write id = 0x0, address = 0x0, data_size = 0x0, burst_length = 0x0
write entry = 6, valid = 0x0, tid = 0x0, write id = 0x0, address = 0x0, data_size = 0x0, burst_length = 0x0
write entry = 7, valid = 0x0, tid = 0x0, write id = 0x0, address = 0x0, data_size = 0x0, burst_length = 0x0

    上面记录的地址是物理地址,需要查找datasheet看是哪个模块的寄存器。然后检查代码,看看last pc所在函数是否有可能当时的power/clock被关闭了。

Low-Power功能标记

    在SYS_REBOOT_REASON不只有wdt status、fiq step和last pc,还有一堆如下内容的信息:

mcdi_wfi: 0x0
mcdi_r15: 0x0
SPM Suspend debug = 0x124000
deepidle: 0x0
sodi: 0x0
spm_suspend: 0x0
clk_data: 0x0
clk_data: 0x0
clk_data: 0x0
clk_data: 0x0
clk_data: 0x0
clk_data: 0x0
clk_data: 0x0
clk_data: 0x0
cpu_dvfs_vproc_big: 0x0
cpu_dvfs_vproc_little: 0x0

......

    这些信息显示了系统当时的状态,比如是否进入sodi,当时dvfs档位状态等等。因为系统的复杂性,这些信息也算是提供一些参考吧。

CPU hot plug

    还有一个重要的信息就是hot plug信息,每个CPU都有,不过CPU0比较特殊,因为不会发生hot plug(在新平台上,CPU0也会发生hot plug),因此有3个参数,其他CPU都只有1个参数:

CPU 0
irq: enter(0, 0) quit(0, 0)
hotplug: 2, 47, 0x0
cpu_dormant: 0x0
CPU 1
irq: enter(0, 0) quit(0, 0)
hotplug: 15, 
cpu_dormant: 0x0
CPU 2
irq: enter(0, 0) quit(0, 0)
hotplug: 15, 
cpu_dormant: 0x0
CPU 3
irq: enter(0, 0) quit(0, 0)
hotplug: 54, 
cpu_dormant: 0x0

    CPU0的的参数含义如下:

CPU事件定义在kernel-3.10/include/linux/cpu.h里(2表示CPU_ONLINE):

#define CPU_ONLINE 0x0002 /* CPU (unsigned)v is up */
#define CPU_UP_PREPARE 0x0003 /* CPU (unsigned)v coming up */
#define CPU_UP_CANCELED 0x0004 /* CPU (unsigned)v NOT coming up */
#define CPU_DOWN_PREPARE 0x0005 /* CPU (unsigned)v going down */
#define CPU_DOWN_FAILED 0x0006 /* CPU (unsigned)v NOT going down */
#define CPU_DEAD 0x0007 /* CPU (unsigned)v dead */
#define CPU_DYING 0x0008 /* CPU (unsigned)v not running any task, not handling interrupts, soon dead. Called on the dying cpu, interrupts are already disabled. Must not sleep, must not fail */
#define CPU_POST_DEAD 0x0009 /* CPU (unsigned)v dead, cpu_hotplug lock is dropped */
#define CPU_STARTING 0x000A /* CPU (unsigned)v soon running. Called on the new cpu, just before enabling interrupts. Must not sleep, must not fail */

    其他CPU参数含义如下(比如54表示CPU已经下电了):

 

last kmsg

    最后的参考资料是last kmsg,看看是否存在异常。如果没有看到异常,就看最后log停在哪里。猜测和哪个模块相关,然后进一步调试。

分析方法

    有了以上的调试信息,我们就可以开始分析了。纯粹的HW reboot一般和HW相关性较大。结合以上信息还不足以定位,通常还需要更多信息,比如复现次数等,大致分析如下:

复现概率 可能的问题 调试方向
仅1次 SW/HW 软件分析
多次,lastpc固定 SW 软件分析
多次,随机 HW
1. 信息调查
2. 软件分析
3. HW验证

    仅1次复现,如果以上的调试信息看不出问题点就只能后面在关注是否再次发生了。

    可复现的话,需要关注复现路径,发生问题的时间点,结合多次复现的db统一分析,看是否有相似性。

    随机问题,基本导向HW故障,还需要进一步做信息调查

排查项 结果 说明
PDN仿真 必须通过 非常重要,是HW稳定性的保证
硬件模块 所有的物料必须是Mediatek QVL验证过的 如MCP/晶振…
ETT 
(EMI Timing Tuning)
必须通过 非常重要,保证memory工作稳定性
3D Stress Test 必须通过10小时测试 保证系统稳定性
Driver Only版本验证 正常: 可能是SW问题
异常: 可能是HW问题
 

    如果软件分析没有任何进展,则需做HW验证

排查项 说明
模块加热 验证是否SMT焊接不良
测量Vproc/Vmem等重要电压 查看波形是否有异常drop等现象
CPU/Memory降频 验证layout是否不良
CPU交叉 验证CPU是否不良
MCP交叉 验证MCP是否不良

结语

    HW reboot是一个复杂的问题,需要积累经验及扩大知识面。还要积累各种调试手段,比如DVFS开关,hotplug开关等,这样调试起来才比较顺手。

案例分析

HWT-kernel初始化时间长(1)

    我们来看一个设备异常导致30s HWT的例子。

基本信息

    问题:关机状态下插USB不能进入充电模式并且手机会重启
    版本:ALPS.JB2.MP.V1.9
    平台:MT6589

分析过程

    抓取uart log,如下:

[   30.881685] (0)[0:swapper/0]------------[ cut here ]------------
[   30.882424] (0)[0:swapper/0]Kernel BUG at c034a578 [verbose debug info unavailable]
[   30.883379] (0)[0:swapper/0]Internal error: Oops - BUG: 0 [#1] PREEMPT SMP ARM
[   30.884278] (0)[0:swapper/0]Modules linked in: devinfo bf08c000 pvrsrvkm bf000000
[   30.886092] (0)[0:swapper/0]CPU: 0    Not tainted  (3.4.5 #1)
[   30.886811] (0)[0:swapper/0]PC is at aee_wdt_irq_info+0x180/0x188
[   30.887565] (0)[0:swapper/0]LR is at aee_wdt_irq_info+0x180188

    看到粗体部分就知道是HWT了,而且kernel时间是30s,这是典型的开机30s WDT timeout的case。
    能在30.8s复位,说明0.8s时就将WDT初始化完了,那我们看下log,检查WDK初始化的时间:

[   22.661560] (2)[1:swapper/0][wdk]bind thread[82] to cpu[0]
[   22.662302] (0)[1:swapper/0][wdk]bind thread[83] to cpu[1]
[   22.663039] (2)[1:swapper/0][wdk]bind thread[84] to cpu[2]
[   22.663772] (0)[1:swapper/0][wdk]bind thread[85] to cpu[3]
[   22.664465] (0)[1:swapper/0][WDK] Initialize proc

    这个明显不对啊,正常情况下kernel 3s左右就完成初始化了,WDK理应也在3s左右,最多不超过10s,如果再晚就会发生30s HWT。而22s肯定会发生HWT,估计在前面某些驱动卡了很长时间了,因此检查初始化流程:
  • start_kernel() -> rest_init()在rest_init()创建2个进程跑:kernel_init()和kthreadd(),其中kernel_init()之后就加载init,也就是init进程了。
  • kernel_init() -> init_post() -> Kernel_init_done,这条log差不多在开机3s左右要印出来的,但是客户的log:

[   22.698744] (0)[1:swapper/0]BOOTPROF: 22698.741231:Kernel_init_done

  • 内核中很多驱动通过module_init(),late_init()等方法初始化,在内核初始化中在do_one_initcal()调用这些注册的函数的。
  • kernel_init() -> do_basic_setup() -> do_initcalls() -> do_initcall_level() -> do_initcall_level() -> do_one_initcal() -> xxx()

    这些函数都是在1号进程里执行(单进程),因此我们只要查看[1:swapper/0]的log(为何是1:swapper/0呢?因为init还没被加载,所以fork出来的名字和父进程0:swapper/0一样),就可以知道卡在哪里了:

[    1.671991] (2)[1:swapper/0]platform_device_usbacm_register done!
[    1.672388] (2)[1:swapper/0] hwmsen_create_attr
......
[    3.670777] (3)[1:swapper/0]i2c i2c-3: addr: 30, transfer timeout
[    3.688464] (3)[1:swapper/0][Gsensh error -1
[    3.690363] (3)[1:swapper/0]sensor_gsensor device!
......
[    3.886192] (3)[59:mtk charger_hv_][Power/Battery] [upmu_is_chr_det] Charger exist but USB is host
[    5.710786] (3)[1:swapper/0]i2c i2c-3: addr: 60, transfer timeout
[    7.730774] (3)[1:swapper/0]i2c i2c-3: addr: 60, transfer timeout
[    9.750998] (0)[1:swapper/0]i2c i2c-3: addr: 60, transfer timeout
[    9.779278] (0)[1:swapper/0]I2C_TxData retry over 3
[    9.779880] (0)[1:swapper/0]mmc3416x_device set TM cmd failed
[    9.780597] (0)[1:swapper/0]mmc3416x_i2c_probe: err = 0
[    9.781303] (0)[1:swapper/0]sensor_msensor device!
......
sensor_osensor device!
  alsps_probe +
[    9.782636] (0)[1:swapper/0]  i=0
[    9.783133] (0)[1:swapper/0]  !!!!!!!!
[   11.780995] (0)[1:swapper/0]i2c i2c-3: addr: 90, transfer timeout
[   11.798700] (0)[1:swapper/0] hwmsen_write_block 218 : send command error!!
[   11.789302] (0)[1:swapper/0]FIFO_ST]DEBUGSTAT 41
[   11.789325] (0)[1:swapper/0]EXT_CONF 8001
[   11.798700] (0)[1:swapper/0] hwmsen_write_block 218 : send command error!!
[   13.790782] (2)[1:swapper/0]i2c i2c-3: addr: 90, transfer timeout
[   15.800772] (3)[1:swapper/0]i2c i2c-3: addr: 90, transfer timeout
......
[   17.820783] (0)[1:swapper/0]i2c i2c-3: addr: 72, transfer timeout
[   19.901021] (2)[1:swapper/0]i2c i2c-3: addr: 72, transfer timeout
swapper/0][ALS/PS] tmd2772_ps_calibrate 2340 : tmd2772_read_dat
[   20.076029] (2)[59:mtk charger_hv_][Power/Battery] [upmu_is_chr_det] Charger exist but USB is host
[   21.910769] (3)[1:swapper/0]i2c i2c-3: addr: 72, transfer timeout

    可以看到很多设备初始化失败(存在i2c error)并且花了20多s的时间。

解决方法

    这些设备的I2C error全部都要解决,解决这些问题之后kernel就可以在3s内完成初始化,也就不会再发生30s HWT了。

结语

    解决这类问题,首先要对kernel初始化流程有所了解。否则即使知道HWT,也无法找到问题点。



你可能感兴趣的:(看门狗)