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 本章知识体系

下面这张图总结了本章的核心内容,帮你理清思路:

FreeRTOS移植核心三要素 RTOS移植成功 系统时钟配置 SysTick定时器配置 时钟源选择(HCLK) 重装载值计算 中断管理 中断优先级分组 ISR编写规范 FromISR API使用 临界区保护 任务级临界区 中断级临界区 共享资源保护 验证:创建任务测试运行

这张图把三个核心要素串起来了。系统时钟提供心跳,中断管理处理异步事件,临界区保护共享资源。三者配合,RTOS才能稳定运行。

嗯,到这里,FreeRTOS移植的核心内容就讲完了。配置时钟、管好中断、保护临界区,这三步走完,你的系统就有了RTOS的骨架。剩下的就是往里填业务逻辑了。

记住我的一句话:移植不是目的,稳定运行才是。多测试,多验证,别急着往下走。


公众号:蓝海资料掘金营,微信deep3321