FreeRTOS基础回顾:任务创建与管理、队列与信号量、软件定时器、中断管理

各位同学,咱们今天来聊聊FreeRTOS的几个核心基础模块。说实话,这部分内容我每次讲课时都觉得特别重要——因为不管你后面要做什么复杂的嵌入式项目,任务、队列、信号量、定时器、中断管理这五样东西,你几乎天天都要跟它们打交道。

我个人习惯把FreeRTOS比作一个「小型操作系统工厂」。任务就是工厂里的工人,队列和信号量是工人之间的沟通工具,定时器是闹钟,中断管理则是紧急事件处理通道。好,咱们一个一个来看。

一、任务创建与管理

任务,说白了就是一段独立运行的函数。在FreeRTOS里,每个任务都有自己的栈空间和优先级。我刚开始用FreeRTOS时,总觉得任务越多越好,结果有一次把任务数开到了30多个,系统直接崩了。后来才明白——任务不是越多越好,够用就行。

创建任务用 xTaskCreate() 函数。它的原型长这样:

BaseType_t xTaskCreate(
    TaskFunction_t pvTaskCode,      // 任务函数指针
    const char * const pcName,      // 任务名称(调试用)
    configSTACK_DEPTH_TYPE usStackDepth, // 栈深度(单位是字,不是字节)
    void *pvParameters,             // 传给任务的参数
    UBaseType_t uxPriority,         // 优先级(0最低,configMAX_PRIORITIES-1最高)
    TaskHandle_t *pxCreatedTask     // 任务句柄(可传NULL)
);

举个例子,创建一个LED闪烁任务:

void vLEDTask(void *pvParameters) {
    while(1) {
        GPIO_ToggleBits(GPIOA, GPIO_Pin_5);
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

TaskHandle_t xLEDHandle = NULL;
xTaskCreate(vLEDTask, "LED", 128, NULL, 1, &xLEDHandle);

小提示:栈深度给多少合适?我一般这样估算:任务里局部变量总大小 + 函数调用深度×每个调用帧大小 + 中断嵌套深度×中断帧大小。保守点,给个256字(1024字节)起步,跑起来后用 uxTaskGetStackHighWaterMark() 看看实际用了多少,再往下调。

任务管理还包括挂起、恢复、删除。挂起用 vTaskSuspend(),恢复用 vTaskResume()。删除任务用 vTaskDelete(),但要注意——删除后记得把任务句柄置NULL,不然容易野指针。

二、队列与信号量

队列和信号量,是任务间通信的两大法宝。队列用来传数据,信号量用来传「信号」。

队列(Queue)

队列是个先进先出的缓冲区。创建队列用 xQueueCreate(),发送用 xQueueSend(),接收用 xQueueReceive()。我在项目中遇到过一个问题:两个任务同时往一个队列里写数据,结果数据乱了。后来发现是队列长度设得太小,数据来不及被读走就覆盖了。嗯,这里要注意——队列长度一定要大于峰值数据量。

QueueHandle_t xDataQueue;
xDataQueue = xQueueCreate(10, sizeof(uint32_t));

// 发送任务
void vSenderTask(void *pvParameters) {
    uint32_t data = 0;
    while(1) {
        xQueueSend(xDataQueue, &data, portMAX_DELAY);
        data++;
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

// 接收任务
void vReceiverTask(void *pvParameters) {
    uint32_t received;
    while(1) {
        if(xQueueReceive(xDataQueue, &received, pdMS_TO_TICKS(1000)) == pdTRUE) {
            // 处理数据
        }
    }
}

信号量(Semaphore)

信号量分两种:二值信号量和计数信号量。二值信号量像一把钥匙,谁拿到谁干活。计数信号量像停车场的空位计数器,每来一辆车减1,每走一辆加1。

创建二值信号量用 xSemaphoreCreateBinary(),获取用 xSemaphoreTake(),释放用 xSemaphoreGive()。我习惯用二值信号量来做「任务同步」——比如一个任务等另一个任务处理完数据再继续。

避坑指南:我曾经在中断服务函数里直接调用了 xSemaphoreGive(),结果系统死锁了。后来才想起来——中断里必须用 xSemaphoreGiveFromISR() 这个带ISR后缀的版本。切记,切记!

三、软件定时器

软件定时器,说白了就是闹钟。你设定一个时间,到了时间它就响(执行回调函数)。FreeRTOS的软件定时器有两种模式:单次模式和周期模式。

创建定时器用 xTimerCreate()

TimerHandle_t xMyTimer;
xMyTimer = xTimerCreate(
    "MyTimer",              // 名字
    pdMS_TO_TICKS(1000),    // 周期(1000ms)
    pdTRUE,                 // pdTRUE=周期模式,pdFALSE=单次模式
    (void *)0,              // 定时器ID(可用来区分多个定时器)
    vTimerCallback          // 回调函数
);

void vTimerCallback(TimerHandle_t xTimer) {
    // 定时器到期后执行这里
    uint32_t id = (uint32_t)pvTimerGetTimerID(xTimer);
    // 处理...
}

// 启动定时器
xTimerStart(xMyTimer, 0);

你想想看,软件定时器跟硬件定时器有什么区别?硬件定时器精度高、响应快,但数量有限。软件定时器数量可以很多(取决于内存),但精度受任务调度影响。我一般这样用:需要高精度定时用硬件定时器,需要多个定时任务用软件定时器。

嗯,这里要注意——软件定时器的回调函数是在定时器服务任务中执行的,不要在回调里做阻塞操作(比如 vTaskDelay()),不然会影响其他定时器。

四、中断管理

中断管理是FreeRTOS里最需要小心的地方。为什么?因为中断跟任务调度是两套不同的机制,搞不好就出问题。

FreeRTOS的中断管理有几个关键点:

  • 中断优先级分组:FreeRTOS要求中断优先级必须分组为抢占优先级(4位)和子优先级(0位),也就是 NVIC_PriorityGroup_4。这个在 FreeRTOSConfig.h 里配置。
  • 最大系统调用优先级configMAX_SYSCALL_INTERRUPT_PRIORITY 这个宏定义了哪些中断可以调用FreeRTOS的API。优先级高于这个值的中断,不能调用任何FreeRTOS函数。
  • 中断安全API:所有在中断里调用的FreeRTOS函数,都必须用带 FromISR 后缀的版本。比如 xQueueSendFromISR()xSemaphoreGiveFromISR()

举个例子,在UART接收中断里通知任务:

void USART1_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    uint8_t data;

    if(USART_GetITStatus(USART1, USART_IT_RXNE)) {
        data = USART_ReceiveData(USART1);
        xQueueSendFromISR(xUARTQueue, &data, &xHigherPriorityTaskWoken);
    }

    // 如果xHigherPriorityTaskWoken为pdTRUE,说明有更高优先级的任务被唤醒
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

核心要点:中断服务函数要短小精悍。我见过有人把整个数据处理逻辑都写在中断里,结果系统响应越来越慢。正确的做法是——中断里只做最必要的事(比如接收数据、置标志位),把耗时操作交给任务去处理。

说到中断,我还想提一个常见的坑:中断嵌套。FreeRTOS默认支持中断嵌套,但如果你在中断里调用了 portYIELD_FROM_ISR(),系统会在中断退出后立即进行任务切换。这时候如果还有更高优先级的中断在等待,可能会出问题。我的建议是——尽量保持中断优先级简单,不要搞太复杂的嵌套。

好了,FreeRTOS的这几个基础模块就回顾到这里。说白了,任务、队列、信号量、定时器、中断管理,这五样东西你掌握了,FreeRTOS的基本功就算扎实了。下一节课,咱们就要把这些知识用到CANopenNode的整合实战中去了。

有什么问题,欢迎随时交流。