第三章 内核移植准备:获取RTOS源码、分析芯片架构、启动文件与链接脚本修改、时钟与定时器配置
好,咱们进入第三章。说实话,这一章是整个移植工作的地基。地基没打好,后面跑起来全是坑。我个人习惯是,拿到一个新项目,先别急着写代码,把这几样东西摸透了再说。
3.1 获取RTOS源码:别下错版本
RTOS源码去哪找?这问题看似简单,但我见过太多人栽跟头。我建议直接从官方仓库拉,比如FreeRTOS就去GitHub的官方仓库,RT-Thread去Gitee的官方镜像。
这里有个坑:别用第三方打包的“一键集成版”。我曾经图省事,下了个某论坛的“优化版”,结果里面改了调度器的核心宏定义,排查了整整两天才找到问题。嗯,从那以后我只用官方源码。
下载后,目录结构你得看懂。以FreeRTOS为例,核心就两个文件夹:
Source/— 内核源码,所有架构通用的部分Source/portable/— 移植层,按编译器、架构分目录
你想想看,我们做牵引控制,MCU不是Cortex-M就是RISC-V。所以重点看portable/GCC/ARM_CM4F/或者portable/GCC/RISC-V/。其他目录,暂时不用管。
3.2 分析芯片架构:Cortex-M vs RISC-V
说白了,移植RTOS就是在跟芯片的“寄存器”和“异常处理”打交道。不同架构,差别很大。
3.2.1 Cortex-M系列
Cortex-M天生为RTOS优化过。它有啥好处?
- 双堆栈指针:MSP(主堆栈)和PSP(进程堆栈)。内核跑MSP,任务跑PSP,切换时不用手动改SP。
- 硬件压栈:发生异常时,CPU自动把R0-R3、R12、LR、PC、xPSR压栈。你想想看,这省了多少代码?
- PendSV异常:专门用来做上下文切换。优先级设最低,等所有中断处理完了再切。
我在项目中遇到过一个问题:某款国产Cortex-M3芯片,硬件压栈时居然不压xPSR的某些位。查了勘误表才知道是芯片bug。所以,别盲目相信“Cortex-M都一样”,每个型号的勘误表都得看。
3.2.2 RISC-V系列
RISC-V就灵活多了,但也更“裸”。它没有硬件压栈,没有双堆栈指针。你得自己实现。
关键寄存器就这几个:
| 寄存器 | 作用 |
|---|---|
| mstatus | 机器状态寄存器,控制全局中断、特权级 |
| mepc | 异常返回地址,相当于Cortex-M的LR |
| mcause | 异常原因,判断是中断还是trap |
| mtvec | 中断向量表基地址 |
移植时,核心就是写汇编。我建议你先写一个简单的“任务切换”函数,只保存/恢复PC和SP,跑通了再加其他寄存器。
3.3 启动文件与链接脚本修改
启动文件,说白了就是芯片上电后执行的第一段代码。链接脚本,就是告诉编译器“代码放哪,数据放哪”。
3.3.1 启动文件修改
RTOS需要你修改启动文件里的“堆栈初始化”和“中断向量表”。
以Cortex-M为例,启动文件里通常有:
; 原来的启动文件
__initial_sp ; 栈顶地址
Reset_Handler ; 复位入口
NMI_Handler ; NMI中断
HardFault_Handler
; ... 其他中断
你需要做的:
- 把PendSV_Handler和SysTick_Handler指向RTOS的函数。比如FreeRTOS里,PendSV_Handler要改成vPortSVCHandler,SysTick_Handler要改成xPortSysTickHandler。
- 堆栈大小要够。牵引控制算法里,中断嵌套很深。我一般设8KB起步,调试时再调优。
我曾经犯过一个错:忘了改PendSV的向量,结果任务切换时直接进HardFault。查了半天才发现是向量表没更新。嗯,这种低级错误,犯一次就记住了。
3.3.2 链接脚本修改
链接脚本里,重点看这几个段:
| 段名 | 内容 | 注意事项 |
|---|---|---|
| .text | 代码段 | 一般放在Flash,不用改 |
| .data | 已初始化全局变量 | 启动时从Flash拷贝到RAM |
| .bss | 未初始化全局变量 | 启动时清零 |
| .heap | 堆区 | RTOS的动态内存分配用 |
| .stack | 栈区 | 中断和主程序用 |
移植RTOS时,堆区大小要显式指定。FreeRTOS的configTOTAL_HEAP_SIZE就是从这里来的。我习惯把堆放在RAM的末尾,栈放在堆下面。这样如果堆溢出,会先踩到栈,容易排查。
3.4 时钟与定时器配置
RTOS的“心跳”就是定时器。没有它,任务调度、延时、超时统统没法用。
3.4.1 系统时钟配置
牵引控制单元里,主频一般很高,几百兆赫兹常见。但RTOS的时钟节拍不需要那么快。我一般设1kHz,也就是1ms一个tick。
配置步骤:
- 配置PLL:把外部晶振倍频到目标主频。比如8MHz晶振,倍频到400MHz。
- 配置总线时钟:AHB、APB1、APB2的分频系数。注意,定时器挂在哪条总线上,决定了它的时钟源。
- 配置定时器:选择SysTick(Cortex-M)或者通用定时器(RISC-V)。
举个例子,Cortex-M的SysTick配置:
// 假设主频400MHz,AHB分频1,SysTick时钟源为AHB/8
// 目标:1ms中断一次
// 重装载值 = (400000000 / 8) / 1000 = 50000
SysTick->LOAD = 50000 - 1; // 重装载值
SysTick->VAL = 0; // 清空计数器
SysTick->CTRL = 0x07; // 使能、使能中断、使用AHB/8时钟源
这里有个细节:重装载值减1。因为计数器是从LOAD减到0,一共LOAD+1个周期。我刚开始做时忘了减1,结果时间总差一点点。虽然只差一个周期,但累积多了,调度就不准了。
3.4.2 定时器中断优先级
RTOS的定时器中断,优先级怎么设?
我个人建议:设成最低优先级。为什么?因为定时器中断里只做“标记tick到达”这件事,具体任务切换交给PendSV。如果优先级太高,会打断其他中断,影响实时性。
在牵引控制里,电流环中断优先级很高,可能几十微秒一次。如果SysTick抢了它的优先级,电流环就会抖动。嗯,这问题我在现场遇到过,电机嗡嗡响,查了半天才发现是优先级配反了。
3.5 小结
这一章的内容,说白了就是“把地基打牢”。源码别下错,架构要摸透,启动文件和链接脚本要改对,时钟要配准。每一步都不难,但每一步都容易出问题。
我个人的经验是:每改一个地方,就编译一次,下载一次,跑一个最简单的点灯程序。这样如果出问题,能立刻定位到是哪一步改错了。别等全改完了再调试,那时候问题堆在一起,神仙也难查。
下一章,咱们开始写真正的移植代码。到时候,这些准备工作就会派上用场了。