stm32 freertos development

    STM32 与 FreeRTOS:怎样找到硬故障、死机和随机复位的原因

    STM32 加 FreeRTOS 崩溃排查指南:读硬故障寄存器、中断优先级陷阱、栈和堆溢出、DMA 与内存放置的错误、优先级反转,以及能记录复位原因的看门狗设计。

    ·XTELL 工程团队

    故障是信息,前提是你把它抓住

    设备随机复位、跑一天就卡死、或者在传输中途停住,是固件里最昂贵的一类问题,主要是因为证据会随着复位一起消失。第一条规矩就是别再丢掉它。在 STM32 这样的 Cortex-M 芯片上,每一次严重的崩溃都会经过故障处理函数,处理器已经保存了能解释崩溃的状态。要做的,就是在任何东西把它覆盖之前,把这个状态读出来。

    第 1 步:在硬故障时捕获寄存器

    默认的硬故障处理函数是一个空循环,什么信息也给不了你。把它换成一个处理函数:先找出故障时使用的栈,再把它交给一个能读取保存的寄存器和故障状态寄存器的函数。

    __attribute__((naked)) void HardFault_Handler(void)
    {
        __asm volatile (
            "tst lr, #4\n"
            "ite eq\n"
            "mrseq r0, msp\n"
            "mrsne r0, psp\n"
            "b hard_fault_c\n");
    }
    
    void hard_fault_c(uint32_t *stack)
    {
        volatile uint32_t pc    = stack[6];   /* 出事的地方 */
        volatile uint32_t lr    = stack[5];   /* 谁调用的 */
        volatile uint32_t cfsr  = SCB->CFSR;  /* 故障类型 */
        volatile uint32_t hfsr  = SCB->HFSR;
        volatile uint32_t mmfar = SCB->MMFAR; /* 出错地址(若有效) */
        volatile uint32_t bfar  = SCB->BFAR;
        (void)pc; (void)lr; (void)cfsr; (void)hfsr; (void)mmfar; (void)bfar;
        for (;;) { }   /* 在这里打断点,或者保存这些值然后复位 */
    }

    程序计数器的值指向出错的那条指令,到映射文件或反汇编里去查。可配置故障状态寄存器会告诉你是未定义指令、未对齐访问、数据访问的总线错误,还是内存管理违规,地址寄存器则告诉你是哪个地址。对于现场的产品,把这些值写到复位后仍然保留的 RAM 区或闪存里,下次启动时上报:一行崩溃记录,胜过一周的猜测。

    第 2 步:中断优先级陷阱

    这是 STM32 上最常见的 FreeRTOS 故障,它造成的死机和硬故障,可能相隔好几天才出现一次。在 Cortex-M 上,优先级数字越小,优先级越高;FreeRTOS 定义了一个阈值 configMAX_SYSCALL_INTERRUPT_PRIORITY。调用任何 FreeRTOS 函数(哪怕是"FromISR"版本)的中断,其优先级数字必须大于等于这个阈值(也就是逻辑上优先级不高于它)。高于阈值的中断完全不能碰内核。

    • 把中断优先级分组设置成全部位用于抢占优先级(在 STM32 HAL 里就是 4 位分组),这是 FreeRTOS 期望的。
    • 检查每一个调用 xQueueSendFromISR、xSemaphoreGiveFromISR 等函数的外设中断:它的优先级必须不高于阈值。
    • 绝不在中断里调用 FreeRTOS 函数的非 ISR 版本。打开 configASSERT,内核就会替你检查优先级,并停在出错的那一行。

    第 3 步:栈溢出

    每个任务有自己的栈,而在 FreeRTOS 里,创建任务时传入的大小以字为单位,不是字节,这是栈小了四倍的经典原因。溢出的任务会破坏它下面的任何东西,崩溃发生在后来、发生在别处。

    • 在配置里打开栈溢出检查(方法 2 更彻底),并提供钩子函数,让它记录任务名。
    • 在真实的运行中,用 uxTaskGetStackHighWaterMark 读出每个任务的水位线,并留出余量。打印几行带格式的字符串、使用文件系统或调用较深的库代码,所需的栈远大于一个简单任务。
    • 带浮点单元的芯片上,使用浮点的任务要保存更多状态,栈也更大。
    • 主栈由中断和启动代码使用,它的大小在链接脚本里。那里的溢出看起来就像随机故障。

    第 4 步:堆耗尽与碎片

    如果任务、队列和缓冲区在运行时反复创建和删除,堆就会碎片化,最终某次分配失败,而且通常发生在你没有测试到的时刻。定义 malloc 失败钩子,随时间记录堆剩余的统计,凡是只创建一次的东西尽量用静态分配。长期存在的对象只在启动时动态创建,之后绝不再创建,可以消除这一整类问题。

    第 5 步:内存放置与 DMA

    有些 STM32 系列的内存区域,处理器可以用,DMA 却访问不到。例如 F4 系列的核心耦合 RAM,速度快、适合放栈和变量,但 DMA 不能访问;把缓冲区放在那里,传输会静默失败或触发故障。要查看链接脚本里栈和 DMA 缓冲区实际落在哪里。在带数据缓存的芯片上,DMA 缓冲区还必须按缓存行对齐,并在传输前后做清理或失效,否则处理器会看到旧数据。在我们的 STM32F407 工业控制项目里,FreeRTOS、通过 SDIO 访问 SD 卡上的 FAT 文件系统和外扩 SRAM 结合在一起,缓冲区放置和任务栈大小,决定了持续记录下的稳定性。

    第 6 步:共享数据、优先级反转和死锁

    • 中断与任务之间共享的数据需要保护,哪怕它"只是一个标志"。使用队列、任务通知或临界区,并把共享变量正确标注。
    • 保护由不同优先级任务共享的资源,要用互斥量而不是二值信号量,因为互斥量有优先级继承,信号量没有。否则持有资源的低优先级任务,可能在中优先级任务不停运行的整个期间,一直阻塞高优先级任务。
    • 两个任务以相反顺序获取两把锁,迟早会死锁。固定加锁顺序,或者只用一把锁。
    • 不要在临界区内阻塞,让中断处理函数尽量短:把工作推迟到任务里做。

    第 7 步:能说明复位原因的看门狗

    独立看门狗把死机变成复位;关键是让这次复位带有信息。不要在定时器中断里或某一个低优先级循环里喂狗,因为那只能证明定时器是好的。让每个重要任务通过标志或事件位来报到,只有所有任务都报到了,才由一个监督任务去喂狗。有任务没有报到时,在复位发生之前,记录下是哪个任务缺席。下次启动时,读复位原因寄存器(看门狗、掉电、软件、引脚),和保存的故障数据一起记录。

    第 8 步:跟踪与测试

    当原因是时序上的相互作用,而不是某一个坏指针时,你需要的是任务切换和中断的跟踪。许多 STM32 芯片通过调试口支持指令和事件跟踪,识别 RTOS 的跟踪工具能显示哪个任务在运行、哪个中断抢占了它,以及内核在等什么。让系统在高温下连续运行数天,并对通信和存储施加有意的压力,这样的硬件测试,能抓住短时间台架测试永远发现不了的故障。

    我们能帮什么

    我们编写并抢救 FreeRTOS 和裸机下的 STM32 固件,包括长期运行的工业产品和低功耗产品。请看我们的固件开发服务,以及嵌入式软件架构与 RTOS 设计指南。

    需要专业服务?

    联系我们获取定制化解决方案和报价