RT-Thread 移植教程 基于GD32F4XX
1. 移植所涉及到的文件
1.1 Cortex-M 芯片内核移植代码:
- context_rvds.s
- cpuport.c
1.2 Kernel 文件包括:
- clock.c
- components.c
- device.c
- idle.c
- ipc.c
- irq.c
- kservice.c
- mem.c
- mempool.c
- object.c
- scheduler.c
- thread.c
- timer.c
1.3 配置文件:
- board.c
- rtconfig.h
2. 文件解读
Cortex-M 芯片内核移植代码:
2.1 context_rvds.s
RT-Thread RTOS中用于上下文切换和中断处理的汇编实
好的,我们来逐段解读 context_rvds.S
这个文件。这是 RT-Thread 实时操作系统(RTOS)针对 ARM Cortex-M 系列处理器(特别是 M3/M4/M7)编写的上下文切换核心汇编代码。它负责在任务(线程)之间进行切换,以及处理中断的开关和硬件错误。
文件核心功能:
- 中断控制: 提供全局中断的禁用和启用函数。
- 任务上下文切换:
- 主动切换: 当前任务主动让出 CPU 时调用。
- 中断中切换: 在中断服务程序(ISR)中请求任务切换。
- 首次切换: 系统启动时,从无任务状态切换到第一个任务。
- PendSV 异常处理: 实际执行上下文保存和恢复的地方,利用 PendSV 的可延迟特性实现高效切换。
- 硬件错误处理: 增强的 HardFault 处理程序,用于收集错误现场信息。
代码解读:
-
常量定义 (
EQU
)SCB_VTOR EQU 0xE000ED08 ; 向量表偏移寄存器地址 NVIC_INT_CTRL EQU 0xE000ED04 ; 中断控制状态寄存器 (ICSR),用于触发 PendSV NVIC_SYSPRI2 EQU 0xE000ED20 ; 系统优先级寄存器 2,用于设置 PendSV/SysTick 优先级 NVIC_PENDSV_PRI EQU 0xFFFF0000 ; 设置 PendSV 和 SysTick 为最低优先级 (0xFF) NVIC_PENDSVSET EQU 0x10000000 ; 设置 PendSV 挂起位 (PENDSVSET) 的值
- 定义了操作 NVIC (Nested Vectored Interrupt Controller) 和 SCB (System Control Block) 相关寄存器所需的地址和位掩码。
-
区域和属性 (
AREA
,THUMB
,REQUIRE8
,PRESERVE8
)AREA |.text|, CODE, READONLY, ALIGN=2 THUMB REQUIRE8 PRESERVE8
AREA
: 定义一个名为.text
的代码区域,属性为只读 (READONLY
),按 4 字节对齐 (ALIGN=2
)。THUMB
: 指示后续代码使用 Thumb/Thumb-2 指令集。REQUIRE8
/PRESERVE8
: 指示栈需要 8 字节对齐(符合 AAPCS 标准),并且该函数本身保持栈的 8 字节对齐。
-
导入符号 (
IMPORT
)IMPORT rt_thread_switch_interrupt_flag IMPORT rt_interrupt_from_thread IMPORT rt_interrupt_to_thread IMPORT rt_hw_hard_fault_exception
- 声明这些符号(全局变量/函数)是在其他 C 语言文件中定义的,本汇编文件需要引用它们。
rt_thread_switch_interrupt_flag
: 一个标志变量,通常为 0 或 1,表示是否需要执行上下文切换。rt_interrupt_from_thread
: 指向即将被切换出去的任务的控制块(通常包含其栈顶指针)。rt_interrupt_to_thread
: 指向即将被切换进来的任务的控制块(通常包含其栈顶指针)。rt_hw_hard_fault_exception
: C 语言编写的硬件错误处理函数。
-
rt_hw_interrupt_disable
(禁用中断)rt_hw_interrupt_disable PROCEXPORT rt_hw_interrupt_disable ; 导出此函数供外部调用MRS r0, PRIMASK ; 将 PRIMASK 寄存器的当前值读入 r0CPSID I ; 设置 PRIMASK=1, 禁用所有可屏蔽中断BX LR ; 返回,返回值 r0 是进入函数时的中断状态 ENDP
- 功能: 禁用所有可屏蔽中断,并返回之前的中断状态。
- 流程:
- 读取
PRIMASK
寄存器的值到r0
(保存旧状态)。 - 执行
CPSID I
指令设置PRIMASK=1
,禁用中断。 - 返回,
r0
包含了调用前的中断状态(0 表示中断开启,1 表示中断关闭)。
- 读取
-
rt_hw_interrupt_enable
(启用中断)rt_hw_interrupt_enable PROCEXPORT rt_hw_interrupt_enable ; 导出此函数供外部调用MSR PRIMASK, r0 ; 将 r0 的值写入 PRIMASK 寄存器BX LR ; 返回 ENDP
- 功能: 根据传入的参数
level
(通常由rt_hw_interrupt_disable
返回)恢复中断状态。 - 流程:
- 将传入的参数
r0
写入PRIMASK
寄存器。 - 返回。如果
r0
是 0,则启用中断;如果是 1,则保持中断禁用(虽然通常传入的是之前保存的状态)。
- 将传入的参数
- 功能: 根据传入的参数
-
rt_hw_context_switch
/rt_hw_context_switch_interrupt
(请求上下文切换)rt_hw_context_switch_interrupt ; 在中断中调用的切换入口点EXPORT rt_hw_context_switch_interrupt rt_hw_context_switch ; 在任务中调用的切换入口点EXPORT rt_hw_context_switch; 检查切换标志是否已设置 (防止重复设置)LDR r2, =rt_thread_switch_interrupt_flagLDR r3, [r2] ; 加载标志值到 r3CMP r3, #1 ; 比较标志是否为 1BEQ _reswitch ; 如果已经是 1,跳过设置标志,直接设置目标线程; 设置切换标志为 1MOV r3, #1STR r3, [r2] ; rt_thread_switch_interrupt_flag = 1; 保存"从哪个线程切换"的指针LDR r2, =rt_interrupt_from_threadSTR r0, [r2] ; rt_interrupt_from_thread = r0 (from) _reswitch; 保存"切换到哪个线程"的指针LDR r2, =rt_interrupt_to_threadSTR r1, [r2] ; rt_interrupt_to_thread = r1 (to); 触发 PendSV 异常 (实际切换将在 PendSV 处理程序中发生)LDR r0, =NVIC_INT_CTRL ; 加载 ICSR 地址LDR r1, =NVIC_PENDSVSET ; 加载 PENDSVSET 位掩码STR r1, [r0] ; 设置 PENDSVSET 位,挂起 PendSV 异常BX LR ; 返回 ENDP
- 功能: 请求进行一次任务切换。
rt_hw_context_switch_interrupt
是专门在中断服务程序 (ISR) 中调用的入口点,但两者共享大部分代码。 - 参数:
r0
: 指向要切换出去的线程控制块(通常包含其当前栈顶指针)。r1
: 指向要切换进来的线程控制块(通常包含其保存的栈顶指针)。
- 流程:
- 检查全局切换标志
rt_thread_switch_interrupt_flag
。 - 如果标志为 0(未设置):
- 设置标志为 1。
- 将
r0
(from
)保存到全局变量rt_interrupt_from_thread
。
- 将
r1
(to
)保存到全局变量rt_interrupt_to_thread
。 - 向 NVIC 的 ICSR 寄存器的
PENDSVSET
位写入 1,挂起 PendSV 异常。这是关键步骤! 实际的寄存器保存和恢复工作将由 PendSV 异常处理程序(PendSV_Handler
)在稍后(通常是在退出所有中断后)执行。这种设计使得在中断内部请求切换不会立即发生,避免了在中断嵌套复杂时进行上下文切换的风险,提高了系统的实时性和可靠性。 - 函数返回。此时 CPU 继续执行当前代码(任务或中断服务程序),直到中断处理完毕,PendSV 异常被处理时才会真正切换任务。
- 检查全局切换标志
- 功能: 请求进行一次任务切换。
-
PendSV_Handler
(实际执行上下文切换)PendSV_Handler PROCEXPORT PendSV_Handler ; 导出 PendSV 异常处理程序; === 保护阶段 ===MRS r2, PRIMASK ; 保存当前中断状态 (PRIMASK) 到 r2CPSID I ; 在切换过程中禁用中断 (防止被更高优先级中断打断切换); 检查切换标志LDR r0, =rt_thread_switch_interrupt_flagLDR r1, [r0] ; 加载标志值到 r1CBZ r1, pendsv_exit ; 如果标志为 0 (未设置),直接退出 PendSV (异常被错误触发); 清除切换标志 (表示切换即将处理)MOV r1, #0x00STR r1, [r0] ; rt_thread_switch_interrupt_flag = 0; === 保存旧任务上下文 ===LDR r0, =rt_interrupt_from_thread ; 获取"from"线程指针地址LDR r1, [r0] ; 加载"from"线程指针本身到 r1CBZ r1, switch_to_thread ; 如果指针是 NULL (第一次切换或特殊切换),跳过保存; 保存非自动保存的寄存器 (r4-r11) 和 FPU 寄存器 (如果需要)MRS r1, psp ; 获取当前任务的栈指针 (PSP) 到 r1; [FPU 处理 - 可选] 检查 EXC_RETURN[4] (FPCA 位)IF {FPU} != "SoftVFP" ; 如果使用硬件 FPUTST lr, #0x10 ; 测试 LR (EXC_RETURN) 的 bit4VSTMFDEQ r1!, {d8 - d15} ; 如果 bit4=0 (FPU 上下文活跃),保存 FPU 寄存器 s16-s31 (d8-d15)ENDIF; 保存通用寄存器 r4-r11STMFD r1!, {r4 - r11} ; 将 r4-r11 压入当前任务栈 (PSP 指向的栈),并更新 PSP; [FPU 处理 - 可选] 保存 FPU 活跃标志IF {FPU} != "SoftVFP"MOV r4, #0x00 ; 初始化标志为 0 (FPU 未使用)TST lr, #0x10 ; 再次检查 EXC_RETURN[4]MOVEQ r4, #0x01 ; 如果 bit4=0 (FPU 活跃),设置标志为 1STMFD r1!, {r4} ; 将 FPU 活跃标志压入栈ENDIF; 更新"from"线程的栈指针LDR r0, [r0] ; 再次加载"from"线程指针到 r0 (STR 需要目标地址)STR r1, [r0] ; 将更新后的 PSP (保存了上下文后的栈顶) 存入"from"线程控制块 switch_to_thread; === 恢复新任务上下文 ===LDR r1, =rt_interrupt_to_thread ; 获取"to"线程指针地址LDR r1, [r1] ; 加载"to"线程指针本身到 r1LDR r1, [r1] ; 加载"to"线程保存的栈顶指针到 r1 (指向它之前保存的上下文); [FPU 处理 - 可选] 弹出 FPU 活跃标志IF {FPU} != "SoftVFP"LDMFD r1!, {r3} ; 从新任务栈弹出 FPU 活跃标志到 r3ENDIF; 恢复通用寄存器 r4-r11LDMFD r1!, {r4 - r11} ; 从新任务栈弹出 r4-r11; [FPU 处理 - 可选] 恢复 FPU 寄存器 (如果需要) 并调整 EXC_RETURNIF {FPU} != "SoftVFP"CMP r3, #0 ; 检查弹出的 FPU 标志 (r3)VLDMFDNE r1!, {d8 - d15} ; 如果标志 !=0 (FPU 需要恢复),弹出 FPU 寄存器 s16-s31; 根据标志更新 EXC_RETURN (LR) 以指示 FPU 状态ORR lr, lr, #0x10 ; 设置 EXC_RETURN[4]=1 (FPCA=1, 表示下次异常进入时 FPU 上下文不自动保存)CMP r3, #0 ; 再次检查标志BICNE lr, lr, #0x10 ; 如果标志 !=0 (FPU 需要活跃),清除 EXC_RETURN[4]=0 (FPCA=0)ENDIF; 更新新任务的栈指针 (PSP)MSR psp, r1 ; 将更新后的栈顶 (弹出上下文后的位置) 设置回 PSP pendsv_exit; === 清理和返回 ===MSR PRIMASK, r2 ; 恢复进入 PendSV 时的中断状态 (PRIMASK)ORR lr, lr, #0x04 ; 确保 EXC_RETURN 的 bit2=1 (返回后使用 PSP)BX lr ; 异常返回。CPU 从新任务的栈(PSP)恢复 r0-r3, r12, LR, PC, xPSR ENDP
- 功能: 这是上下文切换的核心。当 PendSV 异常被触发后,CPU 会跳转到这里执行。它负责:
- 保存当前运行任务(被切换出去的任务)的上下文(寄存器)到其栈中。
- 从即将运行任务(被切换进来的任务)的栈中恢复其上下文(寄存器)。
- 切换任务栈指针(PSP)。
- 关键点:
- 中断保护: 进入后立即保存
PRIMASK
并禁用中断 (CPSID I
),确保切换过程原子化。 - 标志检查: 检查
rt_thread_switch_interrupt_flag
确保切换是合法的请求。 - 首次切换处理: 如果
rt_interrupt_from_thread
为 NULL(通常是系统启动后的第一次切换),则跳过保存旧任务上下文的步骤(因为没有旧任务)。 - PSP 的使用: 任务模式使用 Process Stack Pointer (
PSP
)。MRS psp, r1
和MSR psp, r1
用于读写 PSP。 - 寄存器保存/恢复: 保存和恢复的是在发生中断/异常时不会被 CPU 自动压栈的寄存器,即
r4-r11
。r0-r3
,r12
,LR
,PC
,xPSR
会在进入和退出异常时由 CPU 自动处理。 - FPU 支持 (Lazy Stacking): 代码包含条件编译块 (
IF {FPU} != "SoftVFP"
) 来处理硬件浮点单元 (FPU)。这是 Cortex-M4F/M7 等支持浮点的芯片的关键优化:- 保存: 检查
EXC_RETURN[4]
(FPCA 位)。如果为 0,表示当前任务使用了 FPU 寄存器 (s16-s31
),需要将它们 (d8-d15
) 保存到任务栈。同时保存一个标志位记录是否保存了 FPU。 - 恢复: 从新任务栈弹出标志位。如果标志指示该任务使用了 FPU,则恢复
s16-s31
(d8-d15
)。根据标志位修改EXC_RETURN[4]
(FPCA),告诉 CPU 在下次异常进入时是否需要自动保存 FPU 寄存器 (s0-s15
和FPSCR
)。ORR lr, #0x10
设置 FPCA=1 (不自动保存),BICNE lr, #0x10
清除 FPCA=0 (需要自动保存)。
- 保存: 检查
- 栈帧结构: 任务栈顶保存的内容顺序通常是 (从高地址到低地址):
FPU Flag? (可选)
,r11
,r10
,r9
,r8
,r7
,r6
,r5
,r4
,FPU Regs d8-d15? (可选)
。psp
指向的位置在保存后是r4
的保存位置之下。 - 异常返回:
ORR lr, #0x04
确保EXC_RETURN
的 bit2 为 1,表示异常返回后使用PSP
作为栈指针,并返回到任务模式(或可嵌套中断的 Handler 模式)。BX lr
触发异常返回机制,CPU 自动从新任务的栈 (PSP
) 中弹出r0-r3
,r12
,LR
(任务代码的LR
,通常无意义),PC
(任务恢复点),xPSR
。
- 中断保护: 进入后立即保存
- 功能: 这是上下文切换的核心。当 PendSV 异常被触发后,CPU 会跳转到这里执行。它负责:
-
rt_hw_context_switch_to
(首次任务切换)rt_hw_context_switch_to PROCEXPORT rt_hw_context_switch_to; 设置目标线程 ("to")LDR r1, =rt_interrupt_to_threadSTR r0, [r1] ; rt_interrupt_to_thread = r0 (to); [FPU 处理 - 可选] 初始化 FPU 控制 (Cortex-M4F/M7)IF {FPU} != "SoftVFP"; 清除 CONTROL.FPCA (指示初始任务没有活跃的 FPU 上下文)MRS r2, CONTROL ; 读取 CONTROL 寄存器BIC r2, #0x04 ; 清除 bit2 (FPCA)MSR CONTROL, r2 ; 写回 CONTROLENDIF; 清除"from"线程指针 (表示没有旧任务)LDR r1, =rt_interrupt_from_threadMOV r0, #0x0STR r0, [r1] ; rt_interrupt_from_thread = NULL; 设置切换标志LDR r1, =rt_thread_switch_interrupt_flagMOV r0, #1STR r0, [r1] ; rt_thread_switch_interrupt_flag = 1; 设置 PendSV 和 SysTick 的异常优先级为最低LDR r0, =NVIC_SYSPRI2 ; 系统优先级寄存器 2 地址LDR r1, =NVIC_PENDSV_PRI ; 最低优先级掩码 (0xFFFF0000)LDR.W r2, [r0, #0x00] ; 读取当前值ORR r1, r1, r2 ; 合并新优先级 (PendSV/SysTick 位置为 0xFF)STR r1, [r0] ; 写回寄存器; 触发 PendSV 异常LDR r0, =NVIC_INT_CTRL ; ICSR 地址LDR r1, =NVIC_PENDSVSET ; PENDSVSET 掩码STR r1, [r0] ; 挂起 PendSV; === 初始化主栈指针 (MSP) ===LDR r0, =SCB_VTOR ; 向量表偏移寄存器地址LDR r0, [r0] ; 读取向量表起始地址LDR r0, [r0] ; 读取向量表第一个项 (主栈指针初始值 MSP)MSR msp, r0 ; 设置 MSP 为向量表定义的初始值; === 启用中断 ===CPSIE F ; 启用可配置故障 (如 HardFault, MemManage)CPSIE I ; 启用所有可屏蔽中断; === 永不返回 === ENDP
- 功能: 在操作系统启动阶段,从没有任务运行的状态(或特权模式)切换到第一个用户任务。这是启动调度器后调用的关键函数。
- 参数:
r0
: 指向要运行的第⼀个任务的控制块。 - 流程:
- 设置
rt_interrupt_to_thread
为目标任务。 - (FPU) 初始化
CONTROL.FPCA
位为 0,表示初始任务没有活跃的 FPU 上下文。 - 设置
rt_interrupt_from_thread
为 NULL (表示没有前一个任务需要保存)。 - 设置切换标志
rt_thread_switch_interrupt_flag
为 1。 - 配置 PendSV 和 SysTick 异常的优先级为最低(
0xFF
),确保它们不会抢占其他中断。 - 手动触发 PendSV 异常 (
STR NVIC_PENDSVSET, [NVIC_INT_CTRL]
)。 - 关键初始化: 从向量表 (
SCB->VTOR
) 加载初始主栈指针 (MSP
) 的值并设置 MSP。这是确保系统拥有有效栈的关键步骤。 - 全局启用中断 (
CPSIE I
) 和可配置故障 (CPSIE F
)。 - 函数在此结束,但永不返回。因为 PendSV 已经被触发,CPU 会立即(或在退出当前执行流后)进入
PendSV_Handler
。在PendSV_Handler
中,由于rt_interrupt_from_thread
是 NULL,它会跳过保存旧上下文,直接加载第一个任务的上下文,并通过异常返回到第一个任务开始执行。
- 设置
-
rt_hw_interrupt_thread_switch
(兼容性函数)rt_hw_interrupt_thread_switch PROCEXPORT rt_hw_interrupt_thread_switchBX lr ; 直接返回,不做任何事 ENDP
- 这是一个空函数,仅用于兼容旧版本代码。现代 RT-Thread 使用
rt_hw_context_switch_interrupt
。
- 这是一个空函数,仅用于兼容旧版本代码。现代 RT-Thread 使用
-
HardFault_Handler
(硬件错误处理)EXPORT HardFault_Handler HardFault_Handler PROC; 确定错误发生时使用的栈指针 (MSP 还是 PSP?)TST lr, #0x04 ; 检查 EXC_RETURN[2] (SPSEL)ITE EQ ; IF-THEN-ELSE 块MRSEQ r0, msp ; 如果 EXC_RETURN[2]=0 (使用 MSP), 将 MSP 读入 r0MRSNE r0, psp ; 如果 EXC_RETURN[2]=1 (使用 PSP), 将 PSP 读入 r0; 在错误栈帧上模拟保存 r4-r11 (为了统一上下文结构)STMFD r0!, {r4 - r11} ; 将 r4-r11 压入错误栈帧 (r0 指向的栈); [FPU 处理 - 可选] 压入一个占位符 (dummy) 代替 FPU 标志IF {FPU} != "SoftVFP"STMFD r0!, {lr} ; 压入一个占位符 (dummy) 代替 FPU 标志ENDIF; 保存 EXC_RETURN 值STMFD r0!, {lr} ; 将 LR (此时的 EXC_RETURN) 压入栈; 更新对应的栈指针 (MSP 或 PSP)TST lr, #0x04 ; 再次检查 EXC_RETURN[2]ITE EQMSREQ msp, r0 ; 如果之前用 MSP, 更新 MSP 为修改后的值MSRNE psp, r0 ; 如果之前用 PSP, 更新 PSP 为修改后的值; 调用 C 语言错误处理函数PUSH {lr} ; 保存 LR (可能包含 EXC_RETURN? 这里保存的是进入 HardFault 时的 LR)BL rt_hw_hard_fault_exception ; 调用处理函数,传入的 r0 是构造的栈帧指针POP {lr} ; 恢复 LR; 异常返回 (虽然通常不会再返回)ORR lr, lr, #0x04 ; 确保 EXC_RETURN[2]=1 (返回时使用 PSP, 虽然通常不返回)BX lr ; 尝试返回 ENDP
- 功能: 当发生硬件错误(如访问非法内存、执行非法指令等)时,CPU 会跳转到这里。它的主要目的是收集错误发生时的现场信息(寄存器、栈内容),并传递给 C 语言处理函数
rt_hw_hard_fault_exception
进行错误诊断、打印或系统复位。 - 流程:
- 确定栈帧位置: 检查
EXC_RETURN[2]
确定错误发生时 CPU 使用的是主栈 (MSP
) 还是进程栈 (PSP
)。将对应的栈指针加载到r0
。 - 构造统一上下文帧: CPU 在进入 HardFault 时已经自动压入了
r0-r3
,r12
,LR
,PC
,xPSR
。为了匹配 RT-Thread 任务上下文的结构(包含r4-r11
和可能的 FPU 标志),这段代码手动将r4-r11
压入错误栈帧。如果需要 FPU 支持,它压入一个占位符 (lr
的当前值,无实际意义) 代替 FPU 标志位。最后,将进入 HardFault 时的LR
(即EXC_RETURN
值) 也压入栈。这样,r0
指向的栈顶就构成了一个模拟的、完整的任务上下文帧。 - 更新栈指针: 将修改后的栈指针值(指向新构造的上下文帧顶部)写回对应的
MSP
或PSP
。 - 调用 C 处理函数: 调用
rt_hw_hard_fault_exception
,并将构造好的栈帧指针 (r0
) 作为参数传入。这个 C 函数可以解析这个栈帧,获取所有寄存器的值、返回地址 (PC
) 等关键信息用于调试。 - 尝试返回: 修改
EXC_RETURN
并尝试返回。但在实际发生 HardFault 后,系统通常处于严重错误状态,这个返回很可能不会成功,或者 C 函数内部已经决定复位系统。
- 确定栈帧位置: 检查
- 功能: 当发生硬件错误(如访问非法内存、执行非法指令等)时,CPU 会跳转到这里。它的主要目的是收集错误发生时的现场信息(寄存器、栈内容),并传递给 C 语言处理函数
总结:
context_rvds.S
是 RT-Thread 在 ARM Cortex-M 平台上实现多任务的核心。它利用 Cortex-M 的特性(特别是 PendSV 异常和双栈指针 MSP/PSP)高效、安全地完成任务切换:
- 开关中断: 提供基础的原子操作保护。
- 请求切换:
rt_hw_context_switch(_interrupt)
设置标志并触发 PendSV,实现延迟切换。 - 执行切换:
PendSV_Handler
在特权模式下,安全地保存旧任务寄存器到其栈 (PSP
),从新任务栈恢复寄存器,并切换PSP
。它巧妙地处理了硬件 FPU 的懒加载 (Lazy Stacking
)。 - 首次切换:
rt_hw_context_switch_to
初始化关键数据、设置 PendSV 优先级、加载初始 MSP、触发 PendSV 并最终跳转到第一个任务。 - 错误处理:
HardFault_Handler
收集错误现场信息供调试。
这份代码是 RTOS 内核中与硬件架构紧密耦合的关键部分,其设计和实现直接影响系统的实时性、可靠性和效率。
2.2 cpuport.c
负责任务栈的初始化(为第一次切换准备上下文)和高级异常处理(错误诊断、报告)。
好的,我们来详细解读 cpuport.c
这个文件。这是 RT-Thread 实时操作系统(RTOS)针对 ARM Cortex-M 系列处理器的 CPU 移植层核心代码。它主要完成以下关键任务:
- 任务栈初始化: 为每个新创建的任务初始化其栈空间,使其准备好被调度执行。
- 异常处理:
- 提供硬件错误(HardFault)的详细诊断信息。
- 允许注册自定义的异常钩子函数。
- CPU 控制: 提供 CPU 复位功能。
- 位操作辅助: 提供快速查找最低有效位(FFS)的函数(用于调度器等)。
代码解读:
-
FPU 检测 (
USE_FPU
)#if ... (复杂的编译器宏检测) ... #define USE_FPU 1 #else #define USE_FPU 0 #endif
- 这段代码根据编译器和目标平台配置,自动检测是否使用了硬件浮点单元(FPU)。
USE_FPU
的值会影响后续栈帧结构定义和初始化。
- 这段代码根据编译器和目标平台配置,自动检测是否使用了硬件浮点单元(FPU)。
-
全局变量
rt_uint32_t rt_interrupt_from_thread; rt_uint32_t rt_interrupt_to_thread; rt_uint32_t rt_thread_switch_interrupt_flag; static rt_err_t (*rt_exception_hook)(void *context) = RT_NULL;
rt_interrupt_from_thread
: 指向即将被切换出去的任务控制块(通常包含其栈顶指针)。rt_interrupt_to_thread
: 指向即将被切换进来的任务控制块(通常包含其栈顶指针)。rt_thread_switch_interrupt_flag
: 上下文切换请求标志(0 表示无请求,1 表示有请求)。rt_exception_hook
: 指向用户自定义的异常处理钩子函数的指针。默认为NULL
。
-
栈帧结构定义 (
struct exception_stack_frame
,struct stack_frame
,struct exception_stack_frame_fpu
,struct stack_frame_fpu
)exception_stack_frame
: 定义了 CPU 在发生异常(如中断、SysTick、PendSV)时自动压入栈的寄存器。这是 Cortex-M 架构的标准行为。struct exception_stack_frame {rt_uint32_t r0;rt_uint32_t r1;rt_uint32_t r2;rt_uint32_t r3;rt_uint32_t r12;rt_uint32_t lr; // 发生异常时的 LR (EXC_RETURN)rt_uint32_t pc; // 发生异常时的指令地址rt_uint32_t psr; // 程序状态寄存器 };
stack_frame
: 定义了在任务上下文切换时(主要在PendSV_Handler
中),RT-Thread 手动保存和恢复的额外寄存器 (r4-r11
),以及一个可选的 FPU 标志位 (flag
)。这个结构包含一个exception_stack_frame
成员,模拟了异常发生时 CPU 自动压栈的部分。任务第一次被调度时,栈顶就是这样一个完整的stack_frame
。struct stack_frame { #if USE_FPUrt_uint32_t flag; // 指示该任务是否使用了 FPU 寄存器 (s16-s31) #endif /* USE_FPU *//* r4 ~ r11 register */rt_uint32_t r4;... // r5-r10rt_uint32_t r11;struct exception_stack_frame exception_stack_frame; // CPU自动压栈的部分 };
exception_stack_frame_fpu
和stack_frame_fpu
: 这些是当USE_FPU
为 1 时使用的扩展结构。它们在标准的异常栈帧和任务栈帧基础上,增加了保存 FPU 寄存器 (S0-S15
,FPSCR
) 和扩展 FPU 寄存器 (S16-S31
) 的空间。exception_stack_frame_fpu
对应 CPU 自动压栈的 FPU 部分(如果 FPCA=0),stack_frame_fpu
对应手动保存的 FPU 部分 (S16-S31
) 和标志位。
-
核心函数
rt_hw_stack_init
(任务栈初始化)rt_uint8_t *rt_hw_stack_init(void *tentry, void *parameter, rt_uint8_t *stack_addr, void *texit) {struct stack_frame *stack_frame;rt_uint8_t *stk;unsigned long i;// 1. 计算栈顶位置 (向下生长栈)stk = stack_addr + sizeof(rt_uint32_t); // 通常跳过栈底的一个字 (可选)stk = (rt_uint8_t *)RT_ALIGN_DOWN((rt_uint32_t)stk, 8); // 8字节对齐 (AAPCS)stk -= sizeof(struct stack_frame); // 为 stack_frame 结构预留空间stack_frame = (struct stack_frame *)stk; // stack_frame 指向预留空间的起始// 2. 用 0xdeadbeef 填充整个 stack_frame (调试用,检测栈溢出)for (i = 0; i < sizeof(struct stack_frame) / sizeof(rt_uint32_t); i++) {((rt_uint32_t *)stack_frame)[i] = 0xdeadbeef;}// 3. 初始化模拟的“异常栈帧”部分 (任务第一次运行的初始上下文)stack_frame->exception_stack_frame.r0 = (unsigned long)parameter; // 任务入口参数stack_frame->exception_stack_frame.r1 = 0; // r1-r3, r12 初始化为 0stack_frame->exception_stack_frame.r2 = 0;stack_frame->exception_stack_frame.r3 = 0;stack_frame->exception_stack_frame.r12 = 0;stack_frame->exception_stack_frame.lr = (unsigned long)texit; // 任务退出函数地址stack_frame->exception_stack_frame.pc = (unsigned long)tentry; // 任务入口函数地址stack_frame->exception_stack_frame.psr = 0x01000000L; // Thumb 状态, 默认优先级// 4. 初始化 FPU 标志 (如果使用 FPU) #if USE_FPUstack_frame->flag = 0; // 初始化为 0 (未使用 FPU s16-s31) #endif /* USE_FPU */// 5. 返回初始化后的栈顶指针 (指向 stack_frame 结构开始处)return stk; }
- 功能: 为即将创建的任务初始化其栈空间,使其在第一次被调度时能够正确开始执行。
- 参数:
tentry
: 任务入口函数指针。parameter
: 传递给任务入口函数的参数。stack_addr
: 分配给该任务的栈空间的起始地址(通常是栈空间的最高地址,因为栈是向下生长的)。texit
: 任务退出函数指针(当任务函数tentry
返回时调用的函数,通常用于资源清理)。
- 流程:
- 计算栈顶: 从
stack_addr
(栈底)开始,跳过可能的一个字(可选),然后进行 8 字节对齐。最后减去struct stack_frame
的大小,为初始上下文预留空间。这个计算后的stk
就是任务第一次运行时 PSP 应该指向的位置。 - 填充调试值: 用
0xdeadbeef
填充整个stack_frame
区域。这有助于在调试时检测栈溢出(未使用的栈空间保持0xdeadbeef
,如果被覆盖了就能发现)。 - 初始化 CPU 自动保存部分: 设置
exception_stack_frame
成员:r0
: 任务参数 (parameter
)。r1
,r2
,r3
,r12
: 初始化为 0。lr
: 设置为任务退出函数texit
的地址。当任务函数tentry
执行完毕返回时,会跳转到这里。pc
: 设置为任务入口函数tentry
的地址。这是任务第一次被调度时开始执行的地方。psr
: 设置为0x01000000
。最低字节 (EPSR) 的 T 位为 1,表示 Thumb 状态;其他位通常表示默认优先级。
- 初始化 FPU 标志: 如果使用 FPU,将
flag
初始化为 0,表示该任务尚未使用过s16-s31
FPU 寄存器(采用 Lazy Stacking 优化)。 - 返回栈顶指针: 返回指向初始化好的
stack_frame
结构开始的指针。这个指针会被保存在任务控制块 (TCB) 中,当任务第一次被调度时,PendSV_Handler
会把它加载到 PSP。
- 计算栈顶: 从
-
异常钩子安装
rt_hw_exception_install
void rt_hw_exception_install(rt_err_t (*exception_handle)(void *context)) {rt_exception_hook = exception_handle; }
- 功能: 允许用户注册一个自定义的异常处理函数 (
exception_handle
)。当发生异常(如 HardFault)时,RT-Thread 的内核处理程序会先调用这个钩子函数。 - 参数:
exception_handle
: 用户自定义的异常处理函数指针。该函数接收一个void *context
参数(指向异常发生时的寄存器上下文),并返回rt_err_t
(如果返回RT_EOK
,内核将不再进行默认的异常处理)。
- 功能: 允许用户注册一个自定义的异常处理函数 (
-
硬件错误诊断 (
rt_hw_hard_fault_exception
)- 功能: 这是 RT-Thread 默认的硬件错误 (HardFault) 处理函数。当发生无法恢复的硬件错误时,CPU 会跳转到
HardFault_Handler
(通常在汇编文件如context_rvds.S
中定义),该处理程序收集错误现场信息后调用此 C 函数进行详细诊断和报告。 - 参数:
exception_info
: 指向一个结构体,包含错误发生时的exc_return
值和保存的寄存器上下文 (stack_frame
)。 - 流程:
- 调用钩子函数: 如果用户通过
rt_hw_exception_install
注册了钩子函数,则首先调用它。如果钩子函数返回RT_EOK
,则此函数直接返回(用户已处理错误)。 - 打印寄存器状态: 如果钩子函数不存在或未处理,则打印发生错误时所有关键寄存器的值 (
psr
,r0-r12
,lr
,pc
)。 - 判断错误位置: 检查
exc_return
的 bit2 (SPSEL):- 如果为 1,表示错误发生在线程模式。打印当前线程名称,并调用
list_thread()
(如果启用 Finsh)显示所有线程状态。 - 如果为 0,表示错误发生在Handler模式(中断服务程序内部)。打印相应信息。
- 如果为 1,表示错误发生在线程模式。打印当前线程名称,并调用
- 检查 FPU 状态: 检查
exc_return
的 bit4 (FPCA)。如果为 0,表示 FPU 上下文是活跃的(FPU 寄存器可能包含有效数据),打印提示信息。 - 详细错误跟踪: 如果启用了 Finsh (
RT_USING_FINSH
),调用hard_fault_track()
函数。该函数读取 Cortex-M 的 System Control Block (SCB) 中的各种故障状态寄存器 (CFSR, HFSR, MMAR, BFAR),解析具体的错误原因(内存访问错误、总线错误、用法错误等)并打印出来。 - 死循环: 最后进入一个无限循环 (
while (1);
),系统挂起。用户需要复位设备。
- 调用钩子函数: 如果用户通过
- 功能: 这是 RT-Thread 默认的硬件错误 (HardFault) 处理函数。当发生无法恢复的硬件错误时,CPU 会跳转到
-
错误跟踪辅助函数 (
usage_fault_track
,bus_fault_track
,mem_manage_fault_track
,hard_fault_track
)- 这些函数(仅在
RT_USING_FINSH
时编译)负责解析 SCB 寄存器中的错误状态位,提供人类可读的错误原因描述。例如:usage_fault_track()
: 解析 UsageFault 状态寄存器 (UFSR),报告未定义指令、非法状态、非法加载/存储、除零等错误。bus_fault_track()
: 解析 BusFault 状态寄存器 (BFSR),报告指令预取错误、精确/非精确数据访问错误、栈错误等,并可能打印错误地址 (BFAR)。mem_manage_fault_track()
: 解析 MemManage 状态寄存器 (MFSR),报告指令/数据访问违规、栈错误等,并可能打印错误地址 (MMAR)。hard_fault_track()
: 解析 HardFault 状态寄存器 (HFSR),判断是向量表读取失败、由其他错误升级而来,还是调试事件触发,并调用上述具体错误跟踪函数。
- 这些函数(仅在
-
CPU 控制函数
void rt_hw_cpu_shutdown(void) {rt_kprintf("shutdown...\n");RT_ASSERT(0); // 通常需要根据具体硬件实现 } RT_WEAK void rt_hw_cpu_reset(void) {SCB_AIRCR = SCB_RESET_VALUE; // 写 AIRCR 寄存器触发复位 }
rt_hw_cpu_shutdown()
: 关机函数。这里只是一个占位符实现,打印信息并触发断言。实际使用时需要根据目标硬件平台实现具体的关机操作(如进入低功耗模式、切断电源等)。rt_hw_cpu_reset()
: 复位函数。使用RT_WEAK
定义为弱符号,允许用户在其他文件中覆盖此实现。默认实现是向 SCB->AIRCR 寄存器写入特定的值 (0x05FA0004
) 来触发 CPU 复位。
-
FFS 函数 (
__rt_ffs
)#ifdef RT_USING_CPU_FFS ... // 根据不同编译器 (ARMCC, Clang, IAR, GCC) 实现 __rt_ffs #endif
- 功能:
Find First Set
。给定一个整数value
,返回其二进制表示中最低有效位(Least Significant Bit, LSB)为 1 的位置索引(从 1 开始计数)。如果value
为 0,则返回 0。 - 实现: 针对不同的编译器(ARMCC/Keil, Clang, IAR, GCC)提供了最优化的汇编或内置函数实现。核心思想是利用 Cortex-M 的
RBIT
(位反转)和CLZ
(前导零计数)指令高效计算。 - 用途: 在调度器、位图操作等需要快速查找最低位 1 的场景中非常高效。
- 功能:
总结:
cpuport.c
文件是 RT-Thread 在 ARM Cortex-M 平台上实现多任务的核心 CPU 移植层代码,主要完成以下工作:
- 任务栈初始化 (
rt_hw_stack_init
): 为新任务构建初始运行上下文,模拟一次“异常返回”后的状态,使任务第一次被调度时能正确开始执行。 - 异常处理框架:
- 提供默认的
HardFault
详细诊断功能,打印寄存器、分析错误原因。 - 允许用户通过
rt_hw_exception_install
注册自定义异常处理钩子。
- 提供默认的
- CPU 控制: 提供复位 (
rt_hw_cpu_reset
) 和关机 (rt_hw_cpu_shutdown
- 需定制) 的接口。 - 位操作辅助 (
__rt_ffs
): 提供高效的查找最低有效位 1 的函数,用于内核调度等。
这个文件与之前分析的汇编文件 context_rvds.S
紧密协作:
context_rvds.S
负责底层的上下文切换机制(寄存器保存/恢复、PendSV 处理)。cpuport.c
负责任务栈的初始化(为第一次切换准备上下文)和高级异常处理(错误诊断、报告)。
它们共同构成了 RT-Thread 在 Cortex-M 处理器上运行多任务的基础。