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的整合实战中去了。
有什么问题,欢迎随时交流。