• 在CubeMX生成项目中,手动移植FreeRTOS Kernel V11.3.0 LTS,HardFault 启动崩溃问题日志


    HardFault 启动崩溃问题日志

    项目:F411_Cmake_Jlink_FreeRTOSTest
    芯片:STM32F411CE (Cortex-M4F)
    RTOS:FreeRTOS Kernel V11.3.0 LTS
    日期:2026-05-29


    1. 现象

    FreeRTOS 移植完成后首次编译烧录,程序立即卡死在 HardFault_Handler,无法进入预期的 LED 闪烁任务。

    寄存器状态

    • SP = 0x2001ffc4(进入 HardFault 后的 MSP 值)
    • STM32F411CE RAM 范围:0x20000000 ~ 0x2001FFFF(128 KB)
    • SP 距离 RAM 顶部(0x20020000)仅 60 字节,表明崩溃发生在启动极早期,栈几乎未被使用

    时间线

    编译成功 → 烧录 → 启动 → HardFault(未到达 LED 任务)
    

    1.1 SP 分析

    0x2001ffc4 是进入 HardFault_Handler 之后的 MSP 值。逆推:

    事件 SP 变化
    HardFault 进入时的 SP 0x2001ffc4
    异常帧(CPU 自动压栈 8 字):xPSR, PC, LR, R12, R3, R2, R1, R0 -32 bytes
    触发 HardFault 前一刻的 SP 0x2001ffe4

    0x2001ffe4 距离复位初始值(_estack = 0x20020000)仅 28 字节,完全符合启动阶段崩溃的特征。


    2. 根因分析

    2.1 FreeRTOS 首个任务启动流程

    main()                                    [Core/Src/main.c:91]
      └→ OSAL_START_SCHEDULER()               [User/Drivers/Inc/osal.h]
           └→ vTaskStartScheduler()            [tasks.c]
                └→ xPortStartScheduler()       [port.c:305]
                     └→ prvPortStartFirstTask() [port.c:278]
                          └→ svc 0             [port.c:295]  ← 触发 SVC 异常
    

    prvPortStartFirstTask()port.c:278-299)的核心汇编:

    ldr r0, =0xE000ED08    ; 取向量表地址
    ldr r0, [r0]           ; 取初始 SP
    ldr r0, [r0]
    msr msp, r0            ; 重置 MSP 到栈顶
    mov r0, #0
    msr control, r0        ; 清除 CONTROL(特权模式 + MSP)
    cpsie i                ; 开中断
    cpsie f
    dsb / isb
    svc 0                  ; ★ 触发 SVC,跳转到向量表 SVC_Handler
    

    2.2 问题定位:向量表符号冲突

    CubeMX 生成的 Core/Src/stm32f4xx_it.c 定义了强符号 SVC_HandlerPendSV_Handler

    // stm32f4xx_it.c — CubeMX 生成区域(非 USER CODE)
    void SVC_Handler(void)      { /* 空壳 */ }
    void PendSV_Handler(void)   { /* 空壳 */ }
    

    而 FreeRTOS 端口层 port.c 定义了真正的实现为 vPortSVCHandlerxPortPendSVHandler

    // port.c:260 — 带 __attribute__((naked)) 声明
    void vPortSVCHandler( void )
    {
        __asm volatile (
            "   ldr r3, =pxCurrentTCB           \n"
            "   ldr r1, [r3]                    \n"  // 取 TCB 地址
            "   ldr r0, [r1]                    \n"  // 取任务栈顶
            "   ldmia r0!, {r4-r11, r14}        \n"  // 从任务栈恢复寄存器
            "   msr psp, r0                     \n"  // 设置 PSP
            "   isb                             \n"
            "   mov r0, #0                      \n"
            "   msr basepri, r0                 \n"
            "   bx r14                          \n"  // 异常返回 → 启动首个任务
        );
    }
    
    // port.c:504 — 同样为 naked
    void xPortPendSVHandler( void )
    {
        __asm volatile (
            "   mrs r0, psp                     \n"  // 读 PSP(任务栈指针)
            // ... 上下文保存 / 恢复逻辑 ...
        );
    }
    

    启动文件 startup_stm32f411xe.s:257-264 以 WEAK 声明这些符号:

    .weak      SVC_Handler
    .thumb_set SVC_Handler,Default_Handler
    .weak      PendSV_Handler
    .thumb_set PendSV_Handler,Default_Handler
    .weak      SysTick_Handler
    .thumb_set SysTick_Handler,Default_Handler
    

    链接器优先级:强符号 > 弱符号

    stm32f4xx_it.c 的强定义覆盖了启动文件的弱定义 → 向量表指向 CubeMX 的空壳函数

    2.3 调用约定冲突

    CubeMX 的 SVC_Handler 是普通 C 函数(GCC 生成标准栈帧序言 push {r7, lr})。第一次尝试是在其中通过函数调用转发:

    // 最初的间接路由尝试 — 导致 HardFault
    void SVC_Handler(void)
    {
        vPortSVCHandler();  // ← 从普通 C ISR 调用 naked 函数
    }
    

    汇编结果-O0):

    SVC_Handler:
        push {r7, lr}           ; C 序言 — 修改了 MSP
        bl  vPortSVCHandler     ; 函数调用 — 再次压栈 LR
        pop {r7, pc}            ; 永远不会执行到
    

    vPortSVCHandler 是 naked 函数,设计为处理器的直接异常入口。从普通 C 函数调用它时:

    • C 函数序言已在 MSP 上压栈了额外寄存器
    • vPortSVCHandler 通过 bx r14 执行异常返回(不会返回到调用者)
    • 但 MSP 已被污染(有 C 序言留下的垃圾数据)
    • 更关键的是:xPortPendSVHandler首次 PendSV 上下文切换时从普通 C ISR 被调用,其内部的 vstmdb(FPU 存储)等指令依赖正确的异常栈帧,任何偏移都会导致上下文损坏 → HardFault

    2.4 排除的怀疑项

    假设 排查结果
    FPU 未使能 SystemInit() (system_stm32f4xx.c:170-172) 在 #if (__FPU_PRESENT && __FPU_USED) 下正确设置 SCB->CPACR。编译标志包含 -mfloat-abi=hard__FPU_USED=1。FPU 正常使能。
    时钟配置错误 SystemClock_Config() 正确配置 HSE+PLL=96MHz,与 configCPU_CLOCK_HZ 一致。
    堆栈溢出 任务栈 256 words (1KB),heap 15KB。启动阶段栈使用极少(SP 接近顶部),不存在溢出。
    configTICK_TYPE_WIDTH_IN_BITS 重复定义 首次编译时 configUSE_16_BIT_TICKSconfigTICK_TYPE_WIDTH_IN_BITS 同时存在(V11.3 不允许),已修正。

    3. 解决方案

    三部分协同,彻底消除符号冲突和调用约定问题。

    3.1 新增 User/Drivers/Src/port_isr.c

    /**< SVC 异常:直接分支到 FreeRTOS 启动 handler(naked,无栈帧) */
    __attribute__((naked)) void SVC_Handler(void)
    {
        __asm volatile("b vPortSVCHandler");
    }
    
    /**< PendSV 异常:直接分支到 FreeRTOS 上下文切换 handler(naked,无栈帧) */
    __attribute__((naked)) void PendSV_Handler(void)
    {
        __asm volatile("b xPortPendSVHandler");
    }
    

    设计要点

    • __attribute__((naked))无 C 栈帧序言/尾声,SP 不被修改
    • b(branch)而非 bl(branch-and-link):不压栈 LR,不产生返回预期
    • vPortSVCHandler / xPortPendSVHandler 自身处理异常返回(bx r14
    • 零额外指令开销,相当于直接向量表条目

    3.2 修改 Core/Src/stm32f4xx_it.c

    USER CODE BEGIN Includes 区域添加宏重命名,将 CubeMX 生成的函数定义改名:

    /* USER CODE BEGIN Includes */
    extern void xPortSysTickHandler(void);
    
    /**< 将 CubeMX 生成的 SVC/PendSV handler 重命名,释放符号给 FreeRTOS 直接路由 */
    #define SVC_Handler      Unused_Cube_SVC_Handler
    #define PendSV_Handler   Unused_Cube_PendSV_Handler
    /* USER CODE END Includes */
    

    效果:CubeMX 的函数定义 void SVC_Handler(void) 经预处理后变为 void Unused_Cube_SVC_Handler(void),不再占用 SVC_Handler 链接器符号。该符号重新解析为 port_isr.c 中的 strong naked wrapper。

    3.3 修改 FreeRTOSConfig.h

    #define configCHECK_HANDLER_INSTALLATION        0
    

    port.c:325-344xPortStartScheduler() 中检查向量表条目是否直接指向 vPortSVCHandler / xPortPendSVHandler

    #if ( configCHECK_HANDLER_INSTALLATION == 1 )
    {
        const portISR_t * const pxVectorTable = portSCB_VTOR_REG;
        configASSERT( pxVectorTable[ portVECTOR_INDEX_SVC ] == vPortSVCHandler );
        configASSERT( pxVectorTable[ portVECTOR_INDEX_PENDSV ] == xPortPendSVHandler );
    }
    #endif
    

    由于向量表现在指向 port_isr.c 的 naked wrapper(而非直接的 FreeRTOS handler),此检查会触发断言失败。设为 0 跳过检查。


    4. 中断路由架构(修复后)

                        ┌──────────────────────────────────────────┐
                        │         startup_stm32f411xe.s            │
                        │  (WEAK SVC_Handler → Default_Handler)   │
                        └──────────┬───────────────────────────────┘
                                   │ 被强符号覆盖
                  ┌────────────────┼────────────────┐
                  │                │                │
         ┌────────▼────────┐ ┌────▼──────────┐ ┌───▼──────────────┐
         │ port_isr.c      │ │stm32f4xx_it.c │ │stm32f4xx_it.c    │
         │ (NAKED wrapper) │ │(SysTick,原样) │ │(CubeMX 残留)     │
         └────────┬────────┘ └────┬──────────┘ └───┬──────────────┘
                  │               │                │
         ┌────────▼────────┐ ┌────▼──────────┐     │ 永远不被调用
         │ vPortSVCHandler │ │xPortSysTick   │     │
         │ (port.c:260)    │ │Handler        │     │
         └────────┬────────┘ │(port.c:560)   │     │
                  │          └────────────────┘     │
         ┌────────▼────────┐                        │
         │xPortPendSV      │                        │
         │Handler           │                        │
         │(port.c:504)      │                        │
         └──────────────────┘                        │
    

    路由表

    异常 向量表条目 实际执行
    SVC SVC_Handler (port_isr.c, naked) →b→ vPortSVCHandler (port.c:260)
    PendSV PendSV_Handler (port_isr.c, naked) →b→ xPortPendSVHandler (port.c:504)
    SysTick SysTick_Handler (stm32f4xx_it.c, 普通C) →调用→ xPortSysTickHandler (port.c:560)
    CubeMX 残留 Unused_Cube_SVC_Handler / Unused_Cube_PendSV_Handler 闲置,永不调用

    5. 验证

    检查项 结果
    编译(Unix Makefiles) 35 目标全部通过,零警告
    固件尺寸 Flash 17,212 B (3.28%), RAM 17,416 B (13.29%)
    运行时行为 PC13 LED 以 500ms 间隔正常闪烁
    FreeRTOS 心跳 1ms 周期 SysTick → xPortSysTickHandler 正常运行

    6. 教训与准则

    1. naked 函数不得通过 C 调用链间接调用。FreeRTOS CM4F 端口的 vPortSVCHandlerxPortPendSVHandler 必须作为向量表的直接入口(或通过同类 naked 函数零开销分支)。

    2. CubeMX 中断文件的符号冲突是移植 FreeRTOS 到 CubeMX 裸机项目时的标准陷阱。#define 重命名 + naked wrapper 模式是解决此问题的最小侵入方案(仅修改 USER CODE 区域)。

    3. configCHECK_HANDLER_INSTALLATION 的语义:仅当向量表直接指向 FreeRTOS 内部 handler 时设为 1。任何 wrapper/间接路由方案均需设为 0

    4. 排查 HardFault 时,SP 值是首要线索:接近 RAM 顶部 → 启动极早期崩溃 → 优先排查异常向量表路由和时钟/FPU 初始化。

  • 相关阅读:
    Linux驱动开发(十七)---树莓派PWM驱动
    云原生中间件RocketMQ(三)RocketMQ集群(多Master和多Master-Slave方式)部署实操
    angualr:CSS一个div内两个子元素的高度自适应
    BST二叉搜索树
    I2VGen-XL高清图像生成视频大模型
    macos重装系统 启动U盘制作方法 - createinstallmedia 命令使用方法总结
    Linux高性能服务器编程 学习笔记 第五章 Linux网络编程基础API
    【学习笔记】memcpy_s 函数与坑
    【ESP 保姆级教程】疯狂Node.js服务器篇 ——钉钉/微信/飞书报警从Arduino移植到NodeJs服务器实现
    程序猿怎么选赛道|规划
  • 原文地址:https://www.cnblogs.com/Alanla/p/20228731