裸机与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模型 RTOS任务通信 各模型核心特性 特点 CPU持续查询 实时性差 代码简单 特点 事件驱动 实时性好 需注意临界区 特点 硬件搬运 CPU零占用 配置复杂 特点 消息/信号量 任务间解耦 可维护性好 简单低速场景 大多数嵌入式系统 大数据量高速传输 复杂多任务系统 选择建议:从简单到复杂,从裸机到RTOS,逐步升级 没有最好的模型,只有最合适的模型

最后说一句:别迷信某一种模型。我见过有人非要用DMA读一个每秒变化一次的传感器,结果配置代码写了200行,还不如一个中断搞定。选模型,看需求,别炫技。