深入分析看门狗框架(1)

深入分析看门狗框架

1 WDT原理

 1. 原理

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

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

2. 种类

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

  • Soc芯片内部集成WDT,这是Soc常用的设计。当然PC上可能用独立的看门狗芯片。

  • 软件模拟看门狗,只要有个timer就可以模拟。


2 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通常和硬件有较大关系。比如:

  • 硬件故障,电压或频率异常

  • 总线卡死

3  设计原理

上一章节有讲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管辖的范围了,需要用其他硬件保证,不在该文章讨论范围。


4 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驱动(喂狗模块)。

5  WDT驱动

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位并同时喂狗。

6  超时触发流程

如果喂狗进程没有及时喂狗,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了,因此这个信息尤为重要。

    • 调用mt_aee_dump_sched_traces()输出中断信息。这个信息只有eng版本才有。这个有什么用呢?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基础知识支撑才行,因为任何模块都不是独立存在,在后面的案例分析就会有更深刻的体会了。


你可能感兴趣的:(Android,linux驱动)