专栏说明:前面章节完成裸机轮询版本 MQTT 协议栈,裸机状态机适合简单单片机项目。但是真实量产项目,设备同时要做传感器采集、屏幕刷新、按键处理、4G 模组 AT 指令解析、日志打印。裸机大轮询容易出现阻塞、延时耦合,一处 delay 阻塞整个 MQTT 轮询,导致心跳超时、设备掉线。
本章讲解基于 FreeRTOS 构建高稳定 MQTT 工程,任务划分、消息队列解耦、临界区保护、信号量处理,同时给出工程代码框架,规避 RTOS 下 MQTT 常见死锁、竞争访问、栈溢出坑点。
0 裸机轮询的局限性
裸机版本采用while(1)大循环调用mqtt_client_poll(),有几个致命问题:
- 如果业务代码有阻塞延时,MQTT 轮询得不到调度,PINGREQ 无法及时发出,服务端判定心跳超时踢设备下线。
- 接收报文、发送报文、传感器业务全部耦合在同一个循环,业务耗时直接影响通信实时性。
- 4G 模组 AT 指令等待响应会阻塞整个协议栈运行。
FreeRTOS 下最佳实践:把 MQTT 通信独立成单独任务,和业务任务完全解耦,业务任务通过队列向 MQTT 任务下发发布、订阅请求。MQTT 收到云端消息,通过队列转发给业务任务。
1 整体架构设计
任务划分
| 任务 | 优先级 | 栈大小 | 职责 |
|---|---|---|---|
| mqtt_task | 高 | 1024‑2048 word | MQTT 协议轮询、报文收发、心跳、重连、QoS 重传;不做业务处理 |
| app_task | 中 | 1024 word | 传感器采集、业务逻辑、本地控制;产生需要发布的数据;处理云端下发指令 |
| at_uart_task (可选) | 高 | 1024 word | 4G/Wi‑Fi 模组 AT 驱动,串口接收解析,和 mqtt_task 通过队列交互 |
优先级原则:MQTT 任务优先级高于普通业务任务,保证协议栈可以及时得到 CPU 调度,避免心跳超时;但不能抢占串口硬件中断。
核心通信组件
- 发送请求队列 mqtt_tx_queue:业务任务往队列投递发布、订阅请求,由 MQTT 任务内部完成报文编码发送。业务任务不能直接调用 tcp_send。
- 接收消息队列 mqtt_rx_queue:MQTT 任务解析 PUBLISH 报文之后,把主题 + 载荷封装成消息结构体投递到此队列,app 任务阻塞读取处理云端下发消息。
- 禁止业务任务直接操作
MqttClient_t结构体,全部通过队列交互,消除多任务竞争访问。
2 消息队列结构体定义
/* MQTT下发到业务的消息,云端推送PUBLISH报文封装 */ typedef struct { char topic[128]; uint8_t payload[256]; uint16_t payloadLen; uint8_t qos; }MqttRxMsg_t; /* 业务发给MQTT任务的请求类型 */ typedef enum { MQTT_REQ_PUBLISH, MQTT_REQ_SUBSCRIBE, MQTT_REQ_DISCONNECT }MqttReqType_e; /* 业务向MQTT任务发送请求结构体 */ typedef struct { MqttReqType_e reqType; char topic[128]; uint8_t payload[256]; uint16_t payloadLen; uint8_t qos; }MqttTxReq_t;队列创建(main 函数或者硬件初始化完成后)
QueueHandle_t mqtt_tx_queue; QueueHandle_t mqtt_rx_queue; void mqtt_rtos_queue_init(void) { /* 业务→MQTT任务,最多缓存8条待发送请求 */ mqtt_tx_queue = xQueueCreate(8, sizeof(MqttTxReq_t)); /* MQTT任务→业务任务,最多缓存8条云端下发消息 */ mqtt_rx_queue = xQueueCreate(8, sizeof(MqttRxMsg_t)); }3 MQTT 任务主体代码
注意:所有
MqttClient_t变量必须在 mqtt_task 任务栈之外,全局或者 static,不要放在任务局部栈,防止栈溢出。
static MqttClient_t g_mqttClient; static MqttQosManager_t g_qosMgr; static MqttSubManager_t g_subMgr; void mqtt_task(void *pvParameters) { MqttTxReq_t txReq; MqttRxMsg_t rxMsg; uint32_t tick_now; /* 初始化协议栈 */ mqtt_client_init(&g_mqttClient, &g_qosMgr); mqtt_sub_mgr_init(&g_subMgr); /* 配置遗嘱、设备ID、用户名密码,此处省略 */ for(;;) { tick_now = xTaskGetTickCount(); /* 1.处理业务层下发的发送请求,队列非阻塞读取 */ if(xQueueReceive(mqtt_tx_queue, &txReq, 0) == pdPASS) { switch(txReq.reqType) { case MQTT_REQ_PUBLISH: { /* 在MQTT任务上下文里面完成报文编码和发送 */ mqtt_client_publish(&g_mqttClient, txReq.topic, txReq.payload, txReq.payloadLen, txReq.qos); break; } case MQTT_REQ_SUBSCRIBE: { mqtt_client_subscribe(&g_mqttClient, txReq.topic, txReq.qos); break; } case MQTT_REQ_DISCONNECT: { mqtt_send_disconnect(&g_mqttClient); break; } default:break; } } /* 2.MQTT主状态机轮询:接收、解析报文、心跳、重连、QoS重传 */ mqtt_client_poll(&g_mqttClient, tick_now); /* 3.解析到云端PUBLISH消息回调,投递到业务队列 */ if(mqtt_has_new_publish(&g_mqttClient)) { /* 将解析出来的主题、载荷拷贝到rxMsg */ memcpy(&rxMsg.topic, g_mqttClient.lastRecvTopic,sizeof(rxMsg.topic)); memcpy(rxMsg.payload,g_mqttClient.lastRecvPayload,g_mqttClient.lastRecvLen); rxMsg.payloadLen = g_mqttClient.lastRecvLen; rxMsg.qos = g_mqttClient.lastRecvQos; /* 投递给业务任务,等待最多10ms;队列满直接丢弃消息,防止阻塞MQTT任务 */ xQueueSend(mqtt_rx_queue, &rxMsg, pdMS_TO_TICKS(10)); } /* 任务延时,不要写死太长;建议5‑20ms,兼顾CPU占用和实时性 */ vTaskDelay(pdMS_TO_TICKS(10)); } }4 业务任务如何调用 MQTT(app_task 示例)
业务任务禁止直接操作 g_mqttClient 全局结构体,全部压入 tx 队列。
void app_task(void *pvParameters) { MqttTxReq_t txReq; MqttRxMsg_t rxMsg; for(;;) { /* 模拟传感器采集 */ float temp = read_sensor_temp(); /* 业务需要上报传感器,组装请求压入队列 */ memset(&txReq,0,sizeof(txReq)); txReq.reqType = MQTT_REQ_PUBLISH; strcpy(txReq.topic,"device/sensor001/temp"); sprintf((char*)txReq.payload,"{\"temp\":%.2f}",temp); txReq.payloadLen = strlen((char*)txReq.payload); txReq.qos = 1; xQueueSend(mqtt_tx_queue,&txReq,0); /* 阻塞等待云端下发指令,最多等待500ms */ if(xQueueReceive(mqtt_rx_queue,&rxMsg,pdMS_TO_TICKS(500)) == pdPASS) { printf("收到主题:%s\r\n",rxMsg.topic); printf("payload:%s\r\n",rxMsg.payload); /* 在这里解析JSON指令,执行设备控制逻辑 */ } vTaskDelay(pdMS_TO_TICKS(500)); } }5 RTOS 环境关键保护点(量产高频踩坑)
5.1 临界区保护场景
如果串口接收中断往 MQTT 接收环形缓冲区写入数据,环形缓冲区读写需要临界区。
/*中断里面写入环形buffer时,读端任务侧读取加临界区*/ taskENTER_CRITICAL(); ring_buffer_read(&client->rxRingBuf,buf,len); taskEXIT_CRITICAL();不要在临界区内调用任何会阻塞的 RTOS API,不能 vTaskDelay,不能 xQueueReceive。临界区执行时间要尽可能短。
5.2 栈溢出防范
- MQTT 任务栈大小不能过小,报文拷贝、字符串处理占用栈空间,建议 1024word 起步;开启 FreeRTOS 栈检测
configCHECK_FOR_STACK_OVERFLOW。 - 大数组不要定义在任务局部栈,放到全局 static。
5.3 队列满处理策略
- mqtt_tx_queue 队列满:业务上报数据选择丢弃最新或者丢弃旧数据,绝对不能阻塞等待队列有空位。如果业务任务阻塞在队列,业务卡死;如果 MQTT 任务阻塞,心跳直接超时掉线。
- mqtt_rx_queue 队列满:直接丢弃云端下发消息,业务层需要能够容忍,重要指令使用 QoS1,Broker 会重发。
5.4 不要在回调中执行阻塞操作
MQTT 报文解析回调里面,禁止vTaskDelay、xQueueSend阻塞等待、printf 长时间打印。回调上下文属于 mqtt_task,阻塞会直接影响协议状态机。
5.5 重连时候队列清理策略
网络断开重连时:
- mqtt_rx_queue 可以直接清空,旧的已经过期消息丢弃;
- mqtt_tx_queue 业务待发送消息:业务决定是否保留;如果是采集数据可以清空;如果是报警事件需要保存,重连成功后继续发送。
/*清空队列工具函数*/ void mqtt_queue_reset(QueueHandle_t q) { while(xQueueReceive(q,NULL,0) == pdPASS); }6 任务创建示例
int main(void) { hardware_init(); mqtt_rtos_queue_init(); xTaskCreate(mqtt_task,"mqtt_task",1536,NULL,3,NULL); xTaskCreate(app_task,"app_task",1024,NULL,2,NULL); vTaskStartScheduler(); while(1); }7 RTOS 架构常见故障排查清单
- MQTT 频繁掉线:MQTT 任务优先级太低,被业务任务抢占得不到调度;或者任务延时设置过大。
- 系统死锁:队列阻塞调用放在 MQTT 任务上下文。
- 硬件 HardFault:任务栈溢出;局部定义超大数组;没有临界区,环形缓冲区读写竞争破坏数据。
- 偶尔收不到云端消息:rx 队列满把消息丢弃,调大队列深度,优化业务处理速度。
- 重连之后消息错乱:断开连接没有清空旧队列残留消息。
本章总结
本章完成 FreeRTOS 下量产 MQTT 工程架构。核心思想就是协议栈独立任务,队列做任务解耦,业务不直接操作协议栈结构体,规避 RTOS 多任务竞争、阻塞导致掉线。裸机轮询适合简单 Demo,商用产品强烈建议采用本章架构。
下一章预告:第 9 章 MQTT+JSON 数据上报、指令解析、协议封装实战
💖 点赞 + 收藏 + 关注,MQTT 物联网上云系列持续更新!