1、问题现象
项目使用恩智浦的 i.MX RT1050MCU,跑的rt_thread操作系统, 在高低温测试中,高温没问题, -40℃低温环境运行时,偶发系统重启现象(几个小时出现一次),通过日志得知是看门狗重启。(MCU复位原因可通过寄存器获取,下图截取自i.MX RT1050手册)
2、问题原因
2.1、看门狗的时钟异常
将问题反馈给MCU的FAE,得到答复是——看门狗的时钟是内部rc,受温度影响很大,试试把复位时间拉长。
程序目前看门狗复位时间是14S,喂狗任务的执行周期是0.5S,温度影响再大,也不太可能有这么离谱,但还是要测下看门狗时钟受温度影响的程度究竟有多大。
只需在喂狗前打印看门狗计数值,对比常温和低温环境时的计数值即可。代码如下。
void feed_dog_task(void *param)
{
...... //看门狗初始化
while(1)
{
RTWDOG_Refresh(RTWDOG);
rt_thread_mdelay(500);
rt_kprintf("watchdog CNT:%d", RTWDOG->CNT);//喂狗前打印计数值
}
}
可以看到,温度虽有影响,但问题不大,计数值只快了2%。
此项目是基于之前项目更换了flash,其他硬件没变,但程序启动时会将程序copy到sdram运行,所以排除flash,硬件这边暂时没得找了,那就找找软件。
2.2、有线程卡住,导致无法喂狗
这种情况可能是软件在死等某个外设状态就绪,由于低温环境等不来想要的状态,导致线程卡住,无法喂狗,逻辑如下。
do{
state = read_state();
}while(state != OK)
如果真是这么回事,要怎么定位到这段代码呢?首先是不能让软件复位,得把看门狗关闭,自己模拟个无复位的看门狗,用于检测任务卡住情况(倒是有一个看门狗中断,复位前会进入中断,尝试过在中断里关闭看门狗,没搞定);其次检测到任务卡住时,打印所有就绪任务的栈,从每个栈里查看任务运行的位置,找到在死等某个状态的任务。
2.2.1、模拟看门狗
在 SysTick_Handler(1ms执行一次) 里累加一个变量,在喂狗任务里清0此变量,如果变量累加时间超过14S,则说明有任务卡住了,则输出一个提示信息,代码逻辑如下。
int _1ms_cnt_test = 0;
void SysTick_Handler(void)
{
static int flag = 0;
...
_1ms_cnt_test++;
if((_1ms_cnt_test > 14000)&&(flag == 0))
{
flag = 1; //只提示一次
rt_kprintf("task blocked \r\n");
}
...
}
void feed_dog_task(void *param)
{
...... //看门狗初始化
while(1)
{
//RTWDOG_Refresh(RTWDOG);
_1ms_cnt_test = 0;
rt_thread_mdelay(500);
}
}
2.2.2、添加打印所有就绪任务栈数据的命令
任务卡住就不会主动让出CPU,因此只会处于就绪或者运行状态,不可能是挂起状态,在使用命令行打印时,Shell任务处于运行状态,那卡住任务则处于就绪状态,先打印所有就绪任务栈数据,再分析和定位。
rt_thread可以通过MSH_CMD_EXPORT宏添加命令,但执行命令行的Shell任务优先级默认比较低,先将其设为最高,确保有任务卡住时仍然能执行。打印命令代码如下。
void print_thread_stack_frame(struct rt_thread *thread)
{
rt_kprintf("Thread:%s\r\n",thread->name);
//打印任务栈中的前50个数据
for(int i = 0; i < 50; i++)
{
rt_kprintf("0x");
rt_kprintf("%08x "((int *)(thread->sp))[i]);
rt_kprintf("\r\n");
}
rt_kprintf("***********************************\r\n\n"):
}
void print_all_ready_threads_stack_frame(void)
{
struct rt_thread *thread;
struct rt_list_node *node;
int priority;
rt_enter_critical();
//遍历所有优先级
for (priority = 0; priority < RT_THREAD_PRIORITY_MAX; priority++)
{
//遍历当前优先级的线程链表
for (node = rt_thread_priority_table[priority].next;
node != &rt_thread_priority_table[priority];
node = node->next)
{
thread = rt_list_entry(node, struct rt_thread, tlist);
print_thread_stack_frame(thread);
}
}
rt_exit_critical();
}
MSH_CMD_EXPORT(print_all_ready_threads_stack_frame, Print all ready threads register info);
2.2.3、复现问题
运行几个小时后,打印了"task blocked",说明问题复现,此时通过命令行先看看线程状态,发现AAA_task任务的优先级由原来的20提高到了5,再看互斥量,发现此任务占用了smi_mutex互斥量导致有两个任务阻塞从而触发了优先级翻转机制。说明极有可能卡住的任务就是AAA_task,通过上述添加的打印栈信息命令获取到AAA_task的栈帧,再确认AAA_task任务是否在死等某个状态。
msh />list_thread
thread pri status sp stack size max used left tick error
---------------- --- ------- ---------- ---------- ------ ---------- ---
AAA_task 5 ready 0x000000e8 0x00001000 10% 0x00000027 000
...
...
...
msh />list_mutex
mutex owner hold suspend thread
---------------- -------- ---- --------------
smi_mutex AAA_task 0001 2
...
...
...
msh />print_all_threads_stack_frame
Thread: AAA_task
0xdeadbeef
0xdeadbeef
0xdeadbeef
0xdeadbeef
0xdeadbeef
0xdeadbeef
0xdeadbeef
0xdeadbeef
0x0000034e
0x00000a08
0x00000000
0x00000000
0x00000005
0x80054dab
0x80091140
0x21000200
...
...
...
2.2.4、确认卡住任务
RTOS切换任务会保存现场,这个现场就是内核中的寄存器,栈中前8个0xdeadbeef是r4-r11,后8个对应栈帧,返回地址(0x80091140)是一个延时函数,LR(0x80054dab)是一个do while的死等(汇编里是0x80054daa是因为bit0表示要选择什么指令集,我们cortex-m内核的都是thumb-2,所以bit0==1),此处代码与想象中情况基本一致,将此任务执行周期由原来的1s提升至100ms,复现时间由原来的几个小时提升至十多分钟,至此,已定位到问题代码。
2.2.5、分析原因
这段代码逻辑就有问题, value的最高位是一个状态位,为1时,表示对应动作还没执行完,为0时才应该退出循环,而代码恰恰相反。在低温环境中,可能会导致对应动作加快,在while里判断时状态已经为0了,因此卡在了这里。
3、解决方案
代码改为对应动作执行完才退出循环,重新低温测试,问题不再复现,至此问题解决。