专栏说明:前面章节完成裸机轮询版本 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 物联网上云系列持续更新!