裸机与RTOS环境下的通信模型:轮询、中断、DMA、任务间通信
通信模型这玩意儿,说白了就是CPU怎么知道“有数据来了”。我早年做项目时,觉得只要功能跑通就行,管它什么模型。结果产品一上量,问题全出来了——丢数据、响应慢、CPU空转。嗯,今天咱们就把这四种模型掰开揉碎,看看它们到底差在哪。
轮询模型:最原始,也最“笨”
轮询就是CPU不停地问:“你好了没?你好了没?” 像个复读机。
核心逻辑:主循环里反复检查某个标志位或寄存器。
// 裸机轮询示例
while(1) {
if (UART->SR & (1 << 5)) { // 检查RXNE位
data = UART->DR;
process(data);
}
// 其他任务...
}
我在项目中遇到过一个问题:一个温控设备用了轮询读传感器,结果按键响应延迟了200ms。用户按下去,半秒后才反应。为什么?因为轮询把CPU全占满了。
| 特性 | 轮询 |
|---|---|
| 实时性 | 差,取决于轮询频率 |
| CPU占用 | 100%,一直在跑 |
| 代码复杂度 | 极低 |
| 适用场景 | 简单、低速、非实时 |
我曾经踩过的坑:轮询间隔设得太短,导致CPU连看门狗都喂不了。系统反复复位,查了两天才找到原因。
中断模型:让CPU“该干嘛干嘛”
中断就好比你在写代码,突然电话响了——你放下手头的事去接电话,接完再回来继续写。CPU也是这么干的。
// 中断服务函数
void USART1_IRQHandler(void) {
if (USART1->SR & (1 << 5)) {
data = USART1->DR;
// 注意:ISR里别干重活!
buffer[buf_idx++] = data;
}
}
我个人习惯,ISR里只做“搬数据”这件事。什么协议解析、状态机跳转,统统扔到主循环或任务里。你想想看,ISR里如果跑一个复杂的解析函数,其他中断就得排队等着——这叫“中断延迟”。
| 特性 | 中断 |
|---|---|
| 实时性 | 好,事件驱动 |
| CPU占用 | 低,无事可做时休眠 |
| 代码复杂度 | 中等,注意临界区 |
| 适用场景 | 大多数嵌入式系统 |
小技巧:中断优先级别乱设。我一般把定时器中断设最高,串口中断次之,外部中断再次之。优先级反转的问题,在裸机里同样存在。
DMA模型:真正的“硬件搬运工”
DMA就是CPU的“私人助理”。CPU说:“你把这块数据从外设搬到内存,搬完告诉我一声。”然后CPU就去干别的了。
// DMA配置示例(伪代码)
DMA_InitTypeDef dma;
dma.PeriphAddr = (uint32_t)&USART1->DR;
dma.MemAddr = (uint32_t)rx_buffer;
dma.Length = 256;
dma.Mode = DMA_MODE_CIRCULAR; // 循环模式
DMA_Init(DMA1_Channel5, &dma);
DMA_Cmd(DMA1_Channel5, ENABLE);
我记得第一次用DMA时,觉得这东西太神奇了——CPU完全不用管数据搬运,串口数据自己就进了内存。但要注意:DMA触发源要配对,不然搬的都是垃圾数据。
| 特性 | DMA |
|---|---|
| 实时性 | 极好,硬件级并行 |
| CPU占用 | 几乎为0 |
| 代码复杂度 | 高,配置繁琐 |
| 适用场景 | 大数据量、高速传输 |
我曾经犯过的错:DMA缓冲区设小了,又没有及时处理,导致数据覆盖。后来加了“半传输完成中断”和“传输完成中断”双保险,才解决。
RTOS任务间通信:消息队列、信号量、邮箱
到了RTOS环境,事情就变得有意思了。多个任务之间怎么传数据?直接读写全局变量?别闹,那是裸机的玩法。
消息队列
消息队列就像邮局。任务A把信投进去,任务B去取。信不会丢,也不会乱。
// FreeRTOS消息队列示例
QueueHandle_t xQueue;
// 发送任务
void vSenderTask(void *pvParameters) {
int32_t data = 100;
xQueueSend(xQueue, &data, 0);
}
// 接收任务
void vReceiverTask(void *pvParameters) {
int32_t received;
xQueueReceive(xQueue, &received, portMAX_DELAY);
process(received);
}
信号量
信号量就是个“令牌”。任务拿到令牌才能干活,没拿到就等着。我常用二值信号量做“事件通知”——比如按键按下,ISR给信号量,任务收到后处理。
邮箱
邮箱比消息队列轻量,但只能存一条消息。适合“最新值”场景,比如传感器数据。
| 通信方式 | 数据量 | 阻塞/非阻塞 | 典型用途 |
|---|---|---|---|
| 消息队列 | 多条,FIFO | 均可 | 数据流、命令传递 |
| 信号量 | 无数据,仅计数 | 阻塞 | 同步、资源管理 |
| 邮箱 | 单条,覆盖 | 非阻塞 | 状态、最新值 |
我的建议:能用消息队列就别用全局变量。消息队列自带互斥保护,不会出现“一个任务写到一半,另一个任务读了一半”的惨案。
四种模型对比总表
| 对比项 | 轮询 | 中断 | DMA | RTOS任务通信 |
|---|---|---|---|---|
| CPU利用率 | 极低 | 高 | 极高 | 高 |
| 实时性 | 差 | 好 | 极好 | 好(取决于调度) |
| 数据吞吐量 | 低 | 中 | 高 | 中 |
| 代码可维护性 | 差 | 中 | 中 | 好 |
| 调试难度 | 低 | 中 | 高 | 中 |
如何选择?我的经验法则
- 数据量小、不频繁:中断就够了。比如按键、报警信号。
- 数据量大、连续:必须上DMA。比如音频、高速ADC。
- 裸机简单项目:轮询+中断混搭。轮询做慢速任务,中断处理紧急事件。
- RTOS项目:消息队列是标配。ISR里只给信号量或往队列里扔数据,别干别的。
避坑指南:我曾经在一个项目里,ISR中调用了消息队列发送函数,结果队列满了,发送失败,数据丢了。后来改成“ISR只给信号量,任务里再发队列”,问题解决。
最后说一句:别迷信某一种模型。我见过有人非要用DMA读一个每秒变化一次的传感器,结果配置代码写了200行,还不如一个中断搞定。选模型,看需求,别炫技。