环境

  • MCU:STM32F407IGT6
  • IDE:IAR EWARM V8.32
  • RTOS:uC/OS-III V3.04.04
  • HAL库:STM32Cube FW_F4 V1.28.3

问题描述

在将 Micrium uC/OS-III 移植到 STM32F407 HAL 库项目中时,系统启动后立即进入 HardFault_Handler。通过调试器单步执行发现,HardFault 发生在 OSSched() 函数中 CPU_INT_EN() 之后,也就是第一次任务上下文切换的地方。

1
2
3
4
5
6
void OSSched(void)
{
// ... 省略前面的代码 ...
OS_TASK_SW(); // 触发 PendSV 进行上下文切换
CPU_INT_EN(); // ← 执行到这里后进入 HardFault
}

根因分析

问题出在 FPU(浮点单元)的使能时序 上。

IAR 工程配置了 FPU

在 IAR 工程的 General Options → FPU 中,选择了 FPUv4-SP(单精度浮点),这意味着编译器会在适当的地方生成 FPU 指令。

但 startup 文件没有使能 FPU

Cortex-M4 复位后,CP10 和 CP11 协处理器(即 FPU)默认是 禁用 的。必须在使用任何 FPU 指令之前,通过写 CPACR(Coprocessor Access Control Register)寄存器来使能它。

标准的 HAL 库 startup 文件(由 STM32CubeMX 生成)不包含 FPU 使能代码——这是设计上的预期行为,因为 HAL 本身不需要 FPU。

UCOS3 上下文切换会访问 FPU 寄存器

uC/OS-III 的 Cortex-M4 移植层在任务切换钩子 OSTaskSwHook 中包含了 FPU 寄存器保存/恢复的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
void OSTaskSwHook(void)
{
#if (OS_CPU_ARM_FP_EN == DEF_ENABLED)
if ((OSTCBCurPtr->Opt & OS_OPT_TASK_SAVE_FP) != (OS_OPT)0) {
OS_CPU_FP_Reg_Push(OSTCBCurPtr->StkPtr); // 保存 S0-S31
}
if ((OSTCBHighRdyPtr->Opt & OS_OPT_TASK_SAVE_FP) != (OS_OPT)0) {
OS_CPU_FP_Reg_Pop(OSTCBHighRdyPtr->StkPtr); // 恢复 S0-S31
}
#endif
// ...
}

当 IAR 工程启用了 FPU 时,__ARMVFP__ 宏被定义,OS_CPU_ARM_FP_EN 变为 DEF_ENABLED。此时 os_cpu_a.asm 中的 OS_CPU_PendSVHandler 会尝试使用 VMRSVSTMDB 等 FPU 指令。

如果 CPACR 没有使能 FPU,执行这些指令就会触发 UsageFault,进而升级为 HardFault。

解决方案

startup_stm32f407xx.sReset_Handler 中,调用 SystemInit 之前加入 FPU 使能代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Reset_Handler

;; 使能 FPU(CP10 和 CP11 协处理器)
LDR R0, =0xE000ED88 ; CPACR 寄存器地址
LDR R1, [R0] ; 读取 CPACR
ORR R1, R1, #(0xF << 20) ; 设置 bits 20-23,使能 CP10 和 CP11
STR R1, [R0] ; 写回 CPACR
DSB ; 数据同步屏障
;; 禁用 FPU 上下文自动保存
LDR R0, =0xE000EF34 ; FPCCR 寄存器地址
LDR R1, [R0]
BIC R1, R1, #0xC0000000 ; 清除 ASPEN 和 LSPEN 位
STR R1, [R0]
ISB ; 指令同步屏障

LDR R0, =SystemInit
BLX R0
LDR R0, =__iar_program_start
BX R0

关键寄存器说明

寄存器 地址 作用
CPACR 0xE000ED88 协处理器访问控制,bits[23:20] 控制 CP10/CP11(FPU)
FPCCR 0xE000EF34 浮点上下文控制,ASPEN(bit31) 和 LSPEN(bit30) 控制自动保存
  • ASPEN(Automatic State Preservation Enable):为1时,异常入口/退出自动保存/恢复 FPU 状态
  • LSPEN(Lazy State Preservation Enable):为1时,延迟保存 FPU 状态直到真正使用

uC/OS-III 需要自己管理 FPU 上下文(在 OSTaskSwHook 中),所以必须禁用硬件自动保存,避免冲突。

等价的 Keil 版本

如果使用 Keil MDK,代码如下(语法不同但功能相同):

1
2
3
4
5
6
7
8
9
10
11
12
13
IF {FPU} != "SoftVFP"
LDR.W R0, =0xE000ED88
LDR R1, [R0]
ORR R1, R1, #(0xF << 20)
STR R1, [R0]
DSB

LDR.W R0, =0xE000EF34
LDR R1, [R0]
AND R1, R1, #(0x3FFFFFFF)
STR R1, [R0]
ISB
ENDIF

UCOS3 + HAL 库集成要点

记录一下完整的集成过程中踩过的坑:

1. 中断时序问题

HAL_Init() 会立即配置 SysTick 并使能其中断。如果 UCOS3 还没初始化(OSInit() 未调用),SysTick 中断触发后会访问未初始化的 UCOS3 变量,导致 HardFault。

解决:在 main() 最开始调用 __disable_irq(),让 OSStart()OSStartHighRdy() 在准备好后通过 CPSIE I 使能中断。

2. PendSV_Handler 重定向

UCOS3 的上下文切换通过 PendSV 异常实现,处理函数是 OS_CPU_PendSVHandler(在 os_cpu_a.asm 中定义)。需要在 startup 文件中将 PendSV_Handler 重定向到它:

1
2
3
4
5
6
EXTERN  OS_CPU_PendSVHandler
PUBWEAK PendSV_Handler
SECTION .text:CODE:REORDER:NOROOT(1)
PendSV_Handler
LDR R0, =OS_CPU_PendSVHandler
BX R0

注意:不能用 C 函数包装 PendSV_Handler 来调用 OS_CPU_PendSVHandler,因为 PendSV 处理函数修改栈指针后通过异常返回机制退出,C 包装函数会破坏这个流程。

3. SysTick_Handler

HAL 和 UCOS3 共用 SysTick。在 stm32f4xx_it.c 中定义:

1
2
3
4
5
void SysTick_Handler(void)
{
HAL_IncTick(); // HAL 超时计数
OS_CPU_SysTickHandler(); // UCOS3 tick
}

4. CPU_TS_TmrRd

UCOS3 的时间戳功能需要 CPU_TS_TmrRd() 函数。使用 Cortex-M4 的 DWT 周期计数器实现:

1
2
3
4
CPU_TS_TMR CPU_TS_TmrRd(void)
{
return ((CPU_TS_TMR)DWT->CYCCNT);
}

初始化时需要使能 DWT:

1
2
3
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;