裸机程序写到最后,主循环里往往塞满了按键扫描、串口解析、LED 闪烁、电机控制。这些模块对时间的要求完全不同, 互相阻塞在所难免。RTOS 的价值就在于:把”什么时候该执行谁”这件事交给调度器,而不是靠程序员在主循环里手工排布。
RTOS 不是让你写得更快,而是让你把每个功能写成独立任务,各自用阻塞的方式等待自己的事件。
1. 任务与调度器
在 FreeRTOS 里,任务就是一个永不返回的函数,通常写成 for (;;) 死循环。 每个任务有独立的栈空间和优先级,调度器在它们之间切换。
void vSensorTask(void *pvParameters)
{
for (;;) {
float value = read_sensor();
// 阻塞式发送:队列满时任务自动挂起,不占用 CPU
xQueueSend(g_sensorQueue, &value, portMAX_DELAY);
// 阻塞 100ms,让出 CPU 给其他任务
vTaskDelay(pdMS_TO_TICKS(100));
}
}
int main(void)
{
xTaskCreate(vSensorTask, "sensor", 256, NULL, 3, NULL);
xTaskCreate(vControlTask, "ctrl", 256, NULL, 2, NULL);
vTaskStartScheduler(); // 一般不会返回
for (;;);
}
1.1 优先级与抢占
FreeRTOS 默认使用抢占式调度:只要出现一个比当前任务优先级更高的就绪任务,就立刻发生上下文切换。 同优先级任务则按时间片轮转(需要开启 configUSE_TIME_SLICING)。
- 数值越大优先级越高,这一点和某些 RTOS 相反,写代码时务必确认
- 优先级要分层设计:中断触发的实时处理 > 控制回路 > 通信 > 日志与显示
- 不要贪多:任务优先级相同时调度器要轮转,任务太碎反而增加切换开销
2. 队列:任务间通信
任务之间最不该做的就是共享全局变量——因为没有任何机制保证读写是原子的。 队列是 FreeRTOS 推荐的通信方式:它自带拷贝、自带阻塞,天然线程安全。
QueueHandle_t g_sensorQueue;
// 创建:最多缓存 8 个 float
g_sensorQueue = xQueueCreate(8, sizeof(float));
// 接收方:portMAX_DELAY 表示一直阻塞直到有数据
void vControlTask(void *pv)
{
float v;
for (;;) {
if (xQueueReceive(g_sensorQueue, &v, portMAX_DELAY) == pdTRUE) {
update_control(v);
}
}
}
队列传递的是数据的副本而非指针,所以发送完就可以立即修改原缓冲区,不存在悬垂指针问题。 代价是大结构体会带来拷贝开销,此时可以改为传指针,但必须保证所指内存生命周期覆盖整个消费过程。
3. 信号量与互斥锁
两者都基于队列实现,但用途与语义不同,混用会埋下隐患:
| 机制 | 用途 | 是否支持优先级继承 | 典型 API |
|---|---|---|---|
| 二值信号量 | 任务与中断同步 | 否 | xSemaphoreGive / Take |
| 计数信号量 | 管理多个相同资源 | 否 | xSemaphoreCreateCounting |
| 互斥锁 | 保护共享资源 | 是 | xSemaphoreCreateMutex |
// 保护共享资源:必须用 Mutex,才有优先级继承
SemaphoreHandle_t g_spiMutex = xSemaphoreCreateMutex();
void write_flash(uint8_t *buf, size_t n)
{
if (xSemaphoreTake(g_spiMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
spi_transfer(buf, n);
xSemaphoreGive(g_spiMutex);
}
}
// 中断里同步任务:只能用 FromISR 版本
SemaphoreHandle_t g_rxSem = xSemaphoreCreateBinary();
void USART1_IRQHandler(void)
{
BaseType_t hpw = pdFALSE;
xSemaphoreGiveFromISR(g_rxSem, &hpw);
portYIELD_FROM_ISR(hpw); // 若唤醒了更高优先级任务则立即切换
}
4. 三个经典陷阱
- 优先级翻转:低优先级任务持锁时被中优先级任务抢占,导致高优先级任务被迫等待。用互斥锁的优先级继承机制可以缓解,二值信号量则不会
- 死锁:两个任务以相反顺序获取两把锁,互相等待。统一加锁顺序、或用超时返回兜底,都能避免
- 在中断里调用非 ISR 版本 API:
xQueueSend这类函数会尝试阻塞,在中断上下文中是致命错误,必须用FromISR系列
还有一个隐藏陷阱:xSemaphoreTake 超时后仍去访问共享资源,或者忘记 xSemaphoreGive 就提前 return。 建议把加解锁写成固定配对的小函数,而不是散落在业务分支里。
5. 总结
用 RTOS 的正确姿势是:每个功能一个任务,任务之间只通过队列和信号量通信,绝不直接共享全局变量。 优先级按实时性分层,共享资源用互斥锁保护,中断里只做最轻的唤醒动作。
做到这几点,代码会从”一堆 if 判断的超级循环”变成”若干职责清晰的任务”, 调试时也更容易定位到底是谁在阻塞谁。
如果这篇文章对你有帮助,请我喝杯茶吧