2、实时操作系统(RTOS)基础概念
好,咱们开始聊RTOS。说实话,我早年做工业控制时,一直用前后台系统。那时候觉得,不就一个主循环加几个中断嘛,够用了。直到有一次,一个多轴运动控制的项目把我折腾得够呛——几个中断抢资源,主循环里一个延时就把整个系统卡住了。嗯,从那以后,我才真正开始认真研究RTOS。
2.1 前后台系统 vs RTOS
先说说前后台系统。说白了,就是一个大循环(后台)加上中断服务(前台)。代码结构大概这样:
void main(void)
{
while(1)
{
// 后台:轮询各种标志
if(flag_adc) { process_adc(); }
if(flag_uart) { process_uart(); }
// 延时等待
delay_ms(10);
}
}
void ISR_ADC(void)
{
flag_adc = 1; // 前台:置个标志就跑
}
这种结构简单,但问题也明显。你想想看,如果process_adc()执行时间长了,process_uart()就得等着。我在一个温控项目中就吃过这个亏——ADC处理里加了个滤波算法,结果UART接收超时,数据丢了一串。
RTOS就不一样了。它把任务拆开,让每个任务看起来像在“独享”CPU。实际上,调度器在背后快速切换。我习惯这么比喻:前后台系统像只有一个窗口的银行,所有人排队;RTOS像多个窗口,每个窗口服务一个客户,窗口间快速轮换。
2.2 任务(Task)
任务,是RTOS里最基本的执行单元。每个任务都有自己的栈空间、优先级和状态。任务的状态一般有四种:
- 运行态:正在使用CPU
- 就绪态:啥都准备好了,等调度器分配CPU
- 阻塞态:在等某个事件(比如信号量、消息队列)
- 挂起态:被暂停了,需要别人唤醒
我见过不少新手犯一个错:把任务写得特别大,一个任务里塞了几百行代码。其实,任务应该短小精悍。我个人习惯,一个任务就是一个独立的功能模块,比如“读取传感器”、“处理通信协议”、“控制电机”。
void task_sensor(void *param)
{
while(1)
{
read_adc();
vTaskDelay(pdMS_TO_TICKS(100)); // 100ms采集一次
}
}
void task_control(void *param)
{
while(1)
{
// 等待传感器数据就绪
xSemaphoreTake(sem_data_ready, portMAX_DELAY);
calculate_pid();
set_motor();
}
}
2.3 调度器(Scheduler)
调度器是RTOS的心脏。它决定下一个该运行哪个任务。常见的调度策略有两种:
| 调度策略 | 特点 | 适用场景 |
|---|---|---|
| 优先级抢占式 | 高优先级任务随时可以打断低优先级任务 | 实时性要求高的工业控制 |
| 时间片轮转 | 同优先级任务轮流执行固定时间片 | 多个同等重要的任务 |
我建议在工业控制中优先使用优先级抢占式。为什么?因为紧急事件(比如急停信号)必须立刻响应。你想想看,如果电机过流了,还要等当前任务的时间片用完才处理,那设备可能就烧了。
不过要注意优先级反转的问题。我曾经在一个项目中,低优先级任务占着互斥锁,中优先级任务一直跑,高优先级任务等锁等到超时。解决办法?用优先级继承协议,或者干脆把临界区做短一点。
2.4 内核对象
内核对象是任务间通信和同步的桥梁。我重点说三个最常用的:
信号量(Semaphore)
信号量就是个计数器。二值信号量像一把钥匙,谁拿到谁干活。计数信号量像停车场空位,空位有几个就能停几辆车。
// 二值信号量:中断中释放,任务中等待
void ISR_Button(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(sem_button, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
void task_button(void *param)
{
while(1)
{
xSemaphoreTake(sem_button, portMAX_DELAY);
// 处理按键事件
}
}
消息队列(Message Queue)
消息队列用于传递数据。比如一个任务采集数据,另一个任务处理数据。队列可以缓冲多个消息,避免数据丢失。
typedef struct {
uint16_t adc_value;
uint32_t timestamp;
} sensor_data_t;
// 发送任务
void task_send(void *param)
{
sensor_data_t data;
while(1)
{
data.adc_value = read_adc();
data.timestamp = get_tick();
xQueueSend(queue_sensor, &data, 0);
vTaskDelay(10);
}
}
// 接收任务
void task_recv(void *param)
{
sensor_data_t data;
while(1)
{
if(xQueueReceive(queue_sensor, &data, pdMS_TO_TICKS(100)) == pdTRUE)
{
process_data(&data);
}
}
}
互斥锁(Mutex)
互斥锁解决的是资源互斥访问问题。比如两个任务都要写同一个串口,不加锁的话,数据就乱套了。
互斥锁和二进制信号量有啥区别?互斥锁有优先级继承机制,能防止优先级反转。所以,保护共享资源时,我建议用互斥锁,别用信号量。
void task_log(void *param)
{
while(1)
{
xSemaphoreTake(mutex_uart, portMAX_DELAY);
// 安全地使用串口
uart_send("Log data...\n");
xSemaphoreGive(mutex_uart);
vTaskDelay(1000);
}
}
2.5 实时性指标
做工业控制,实时性就是生命。三个核心指标你得搞清楚:
- 响应时间(Latency):从事件发生到任务开始处理的时间。我一般要求控制在1ms以内。
- 抖动(Jitter):响应时间的波动范围。抖动越小,系统越稳定。比如一个周期任务,每次执行间隔应该是10ms,但实际可能是9.8ms到10.2ms之间波动,这个波动就是抖动。
- 吞吐量(Throughput):单位时间内能处理的任务数量。说白了就是系统能扛多重的活。
我记得有个项目,客户要求响应时间小于500μs。我一开始用FreeRTOS默认配置,结果响应时间在800μs左右。后来我把中断优先级调高,把调度器Tick频率从100Hz提到1000Hz,又把一些不紧急的任务放到空闲钩子里,最终压到了400μs以内。嗯,调优的过程就像解谜,挺有意思的。
好了,这一章的基础概念就聊到这儿。下一章我们深入讲讲任务创建和管理的细节。记住一句话:RTOS不是万能药,但用好了,它能让你的系统脱胎换骨。