先来水一波,习惯性的copy科普一下,下一集期待更新中:
WDT硬件设计
1. 原理
在手机Soc Chip中,里面的AP跑着linux操作系统软件,而任何软件都可能存在各种问题,如果遇到了这些异常,软件可能陷入死循环,导致手机变成“砖头”,如果没有其他硬件辅助,那么只能断电(拔电池)然后重新开机才行。为了避免出现这种情况,芯片内部增加了一个看门狗模块,这个模块专门检测CPU运行状态,只要出现卡死就复位系统。
WDT全称是watchdog timer,就是看门狗模块,看门狗其实就是一个可以在一定时间内被复位的计数器。当看门狗启动后,计数器开始自动计数,经过一定时间,如果计数没有被复位,计数器达到指定数值就会发出复位信号,很多设备包括CPU接到这个信号而复位重启(俗称“被狗咬”)。为了保证看门狗不发出复位信号,就需要在看门狗允许的时间间隔内对看门狗计数器清零(俗称“喂狗”),计数器重新计数。如果系统正常并保证按时“喂狗”,那么就相安无事。一旦程序故障卡死,没有“喂狗”,系统“被咬”复位。
2. 种类
嵌入式系统中主要可以分为两种类型的看门狗:
可以看到WDT在RGU里只是其中一个模块,还有其他模块可以产生复位信号,比如thermal,当温度过高会触发IRQ或直接复位,这也是一种硬件保护措施。复位信号经过Mixer后会分出2个信号,grst_b会复位芯片内部模块,还有1个信号通过芯片管脚WATCHDOG复位外部芯片。有时为了debug,也可以测量WATCHDOG pin脚(正常是高电平,有复位信号时低电平),看这次重启是否是WDT等触发的。
2. dual mode
WDT功能,大家可以看datasheet里的寄存器介绍,比如包含了
这里介绍一个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通常和硬件有较大关系。比如:
用一张框图总结上面的流程(以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管辖的范围了,需要用其他硬件保证,不在该文章讨论范围。
android系统中,启动分好几个阶段:preloader、lk、kernel、android。因此每个阶段都要配置WDT,才能保证不卡死。这里我们介绍kernel阶段的WDT驱动,其他阶段是类似的。
这里我们没有用到kernel原生的WDT驱动,而是前面章节讲的那套机制。具体代码位置:
流程是:module_init(mtk_wdt_init) => mtk_wdt_init() => mtk_wdt_probe()。
在mtk_wdt_probe()函数里,做了如下几件事:
这样就完成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是喂狗模块,由于只有一个WDT,而需要保护多个CPU不卡死,根据前面设计的原理来设计wdk驱动,驱动具体位置:
对外接口
初始化
需要注意的是:
喂狗
kwdt_thread()完成喂狗动作,里面就是一个循环:
各个CPU的kwdt_thread()没有同步,因此各自喂狗的时间不定,但只要喂狗间隔不超过30s就没有事。
CPU hotplug
由于有注册CPU hotplug回调函数,因此在某颗CPU上电时,会将cpus_kick_bit对应的bit置位并同时喂狗。下电时,会清除cpus_kick_bit对应bit位并同时喂狗。
cpu 0 preempt=1, softirq=0, hardirq=0 cpu 0 backtrace : c0344a1c, c0344a1c, cpu 1 preempt=0, softirq=0, hardirq=0 |
[ 203.439400]CPU 1 FIQ: Watchdog time out |
我们重点看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()
全部转换后就可以看到完整的调用栈了,有助于我们分析哪里卡住。
kick=0x00000007,check=0x0000000f Qwdt at [292641.063701] |
,这里才能从last_kmsg看到kick/check信息。
建议了解下ATF相关内容再来看下面的知识点。
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以供查看,如下:
请结合代码理清一遍流程,知道哪条log从哪个函数打印出来。如果需要加log调试,也知道该加在哪个位置。
学完WDT/WDK相关内容并不代表就可以独立解决HWT/HW REBOOT的问题了,还需要很多kernel基础知识支撑才行,因为任何模块都不是独立存在,在后面的案例分析就会有更深刻的体会了。
开机30s超时
kernel初始化流程:start_kernel() => rest_init(),这个过程都是0号进程完成,在rest_init()就创建了1号进程执行kernel_init(),接着=> kernel_init_freeable() => do_basic_setup() => do_initcalls()。这个过程是单线程的,都是1号线程完成。
初始化时间长的问题,可以通过uart log检索[1:swapper/0]关键字分析就行了。后面有详细例子讲解。
分析所需材料:
kernel运行过程中30s超时
此时WDK已初始化好,而喂狗进程都是优先级最高的实时进程,其他任何进程都无法卡到喂狗进程。那谁可以卡到喂狗进程呢?换句话说,谁可以影响到系统调度?答案有:
分析方法:
分析所需材料
HW reboot原因
WDT第1阶段超时将触发FIQ,如果CPU没有响应或CPU响应了但没有在第2阶段超时时间内完成重启,就好导致第2阶段超时发出复位信号复位整个系统,生成的db类型就是HW reboot。
可以看到HW reboot定义很简单,这样导致了许多问题都导向了HW reboot,而处理问题的方法都不一样。那有哪些异常会导致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值有特别的含义:
还有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来自于哪一层了:
通过last PC可以知道在最后时刻,各个CPU都在干什么。一般会检查PC所在函数:
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 ...... |
这些信息显示了系统当时的状态,比如是否进入sodi,当时dvfs档位状态等等。因为系统的复杂性,这些信息也算是提供一些参考吧。
CPU hot plug
还有一个重要的信息就是hot plug信息,每个CPU都有,不过CPU0比较特殊,因为不会发生hot plug(在新平台上,CPU0也会发生hot plug),因此有3个参数,其他CPU都只有1个参数:
CPU 0 |
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开关等,这样调试起来才比较顺手。
案例分析
基本信息
分析过程
抓取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 |
[ 22.661560] (2)[1:swapper/0][wdk]bind thread[82] to cpu[0] |
[ 22.698744] (0)[1:swapper/0]BOOTPROF: 22698.741231:Kernel_init_done |
这些函数都是在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] ...... [ 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! [ 9.782636] (0)[1:swapper/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] [ 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] [ 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,也无法找到问题点。