4. FreeRTOS移植实战(下):配置系统时钟、中断管理、临界区保护,验证移植成功
上一节我们把FreeRTOS的源码请进了工程,还搞定了内存管理。但说实话,那只是万里长征第一步。系统时钟怎么配?中断怎么管?临界区怎么保护?这些才是真正让RTOS跑起来的关键。
我个人习惯把这一节叫做「三座大山」。翻过去,你的FreeRTOS就活了。翻不过去,嗯…代码可能连编译都过不了。
4.1 系统时钟配置——给RTOS一颗跳动的心脏
FreeRTOS需要一个心跳,叫SysTick。它是个硬件定时器,每隔固定时间产生一次中断。这个中断就是任务调度的节拍器。
我在项目中遇到过最坑的事:SysTick配错了,任务调度时快时慢,查了两天才发现是时钟源选错了。你想想看,一个系统的心跳都不准,其他还能对吗?
4.1.1 SysTick配置要点
- 时钟源:一般选处理器时钟(HCLK/8或直接HCLK),具体看芯片手册
- 重装载值:决定Tick频率。公式:重装载值 = 时钟频率 / 期望的Tick频率
- 优先级:SysTick中断优先级要设成最低,避免影响其他关键中断
核心原则:SysTick中断频率通常设为1000Hz(1ms一个Tick)。太快了浪费CPU,太慢了调度响应迟钝。
看个实际配置代码,我以STM32为例:
// 配置SysTick为1ms中断
// 假设HCLK = 72MHz
void vSetupSysTick(void)
{
// 1. 关闭SysTick
SysTick->CTRL = 0;
// 2. 设置重装载值
// 72MHz / 1000 = 72000
SysTick->LOAD = 72000 - 1;
// 3. 清空当前计数值
SysTick->VAL = 0;
// 4. 配置时钟源为HCLK,使能中断,使能定时器
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk;
// 5. 设置SysTick中断优先级为最低(15)
NVIC_SetPriority(SysTick_IRQn, 15);
}
我的小技巧:重装载值减1是因为计数器从0开始计数到LOAD值,总共LOAD+1个周期。这个细节我当年栽过跟头,记牢了。
4.2 中断管理——别让中断把RTOS搞崩了
RTOS下的中断管理和裸机完全不同。裸机时你可以在中断里随便写,想写多少写多少。但在RTOS里,中断处理要守规矩。
为什么会这样?因为中断可能打断任务调度,还可能访问任务正在用的资源。搞不好就数据错乱,系统崩溃。
4.2.1 中断优先级分组
ARM Cortex-M内核支持中断优先级分组。说白了就是把优先级分成抢占优先级和子优先级两部分。
| 分组方式 | 抢占优先级位数 | 子优先级位数 | 说明 |
|---|---|---|---|
| NVIC_PriorityGroup_0 | 0 | 4 | 全部是子优先级,无抢占 |
| NVIC_PriorityGroup_1 | 1 | 3 | 2级抢占,8级子优先级 |
| NVIC_PriorityGroup_2 | 2 | 2 | 4级抢占,4级子优先级 |
| NVIC_PriorityGroup_3 | 3 | 1 | 8级抢占,2级子优先级 |
| NVIC_PriorityGroup_4 | 4 | 0 | 16级抢占,无子优先级 |
警告:FreeRTOS要求所有中断优先级必须使用抢占优先级。建议使用NVIC_PriorityGroup_4,即16级抢占优先级。我曾经见过有人用了分组2,结果中断嵌套乱套,查了三天才找到原因。
4.2.2 中断服务函数写法
在FreeRTOS里,中断服务函数要遵循一个原则:快进快出。不要在中断里做复杂处理,只做最必要的事,然后通过信号量或消息队列通知任务去处理。
// 正确的写法:中断里只发信号
void USART1_IRQHandler(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// 检查接收中断标志
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
// 读取数据
uint8_t data = USART_ReceiveData(USART1);
// 发送信号量通知任务
xSemaphoreGiveFromISR(xRxSemaphore, &xHigherPriorityTaskWoken);
}
// 如果需要任务切换
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
关键点:中断里必须使用带FromISR后缀的API函数。普通API不能在中断里调用,否则会死锁。这个坑我踩过,代码直接卡死在临界区里。
4.3 临界区保护——守护共享资源
临界区,说白了就是一段不能被打断的代码。比如你在修改一个全局变量,突然来了个中断也修改它,结果就乱套了。
FreeRTOS提供了两种保护方式:
4.3.1 任务级临界区
用在任务代码里,保护任务与任务之间的共享资源。
// 进入临界区
taskENTER_CRITICAL();
// 这里放需要保护的代码
// 比如修改全局变量
g_shared_counter++;
g_shared_flag = 1;
// 退出临界区
taskEXIT_CRITICAL();
4.3.2 中断级临界区
用在中断服务函数里,保护中断与任务之间的共享资源。
// 在中断中使用
UBaseType_t uxSavedInterruptStatus;
// 保存并关闭中断
uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();
// 保护代码
g_shared_buffer[index] = data;
index++;
// 恢复中断状态
taskEXIT_CRITICAL_FROM_ISR(uxSavedInterruptStatus);
我的经验:临界区要尽量短。别在里面做复杂计算或延时,否则会影响系统实时性。我曾经见过有人把整个数据处理都包在临界区里,结果系统响应慢得像蜗牛。
4.4 验证移植——让代码真正跑起来
配置都写好了,怎么知道移植成功了?我一般用三步验证法:
4.4.1 第一步:编译通过
先确保代码能编译通过,没有语法错误和链接错误。这一步过不了,后面都是白搭。
4.4.2 第二步:创建两个任务测试
// 任务1:LED闪烁
void vTask1(void *pvParameters)
{
while(1)
{
LED_Toggle();
vTaskDelay(pdMS_TO_TICKS(500));
}
}
// 任务2:串口打印
void vTask2(void *pvParameters)
{
while(1)
{
printf("RTOS is running!\r\n");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
// 主函数
int main(void)
{
// 硬件初始化
HAL_Init();
SystemClock_Config();
// 创建任务
xTaskCreate(vTask1, "Task1", 128, NULL, 1, NULL);
xTaskCreate(vTask2, "Task2", 128, NULL, 2, NULL);
// 启动调度器
vTaskStartScheduler();
// 正常情况下不会执行到这里
while(1);
}
4.4.3 第三步:观察现象
- LED是否以500ms间隔闪烁?
- 串口是否每秒打印一次信息?
- 两个任务是否同时运行?
如果都正常,恭喜你,移植成功了!
常见问题:如果任务不运行,先检查vTaskStartScheduler()是否执行到了。我遇到过好多次,都是因为前面某个初始化函数卡死了,根本没走到启动调度器那一步。
4.5 本章知识体系
下面这张图总结了本章的核心内容,帮你理清思路:
这张图把三个核心要素串起来了。系统时钟提供心跳,中断管理处理异步事件,临界区保护共享资源。三者配合,RTOS才能稳定运行。
嗯,到这里,FreeRTOS移植的核心内容就讲完了。配置时钟、管好中断、保护临界区,这三步走完,你的系统就有了RTOS的骨架。剩下的就是往里填业务逻辑了。
记住我的一句话:移植不是目的,稳定运行才是。多测试,多验证,别急着往下走。
公众号:蓝海资料掘金营,微信deep3321