ESP32 FreeRTOS调用xQueueReceive触发LoadProhibited异常排查
我正在开发包含两个任务的ESP32 FreeRTOS应用,核心功能为接收外设发来的UART消息,通过MQTT协议转发至中心broker。
第一个任务负责读取Serial1端口输入,将接收内容处理为消息结构体后添加到FreeRTOS队列中,实现代码如下:
typedef struct { int length; char buffer[AZ_EL_MAX_MESSAGE_LENGTH]; } tag_message_t; void uart_read_task(void * pvParameters){ BaseType_t xStatus; tag_message_t tag_message; while(true) { while(Serial1.available()) { first_char = Serial1.read(); if (first_char == '+') // Indicates the beginning of a message { for(int i = 0; i < AZ_EL_MAX_MESSAGE_LENGTH; i++) { message_buffer[i] = Serial1.read(); if (message_buffer[i] == '\n') // End of message received { ESP_LOGV(TAG, "Message found: %s", message_buffer); strncpy(tag_message.buffer, message_buffer, i + 1); tag_message.length = i + 1; xStatus = xQueueSend(xMessagesToSendQueue, (void*) &tag_message, 0); if (xStatus != pdPASS) ESP_LOGW(TAG, "Failed to queue message."); break; } } } } vTaskDelay(pdMS_TO_TICKS(20)); // Wait the minimum BLE advertisement period for messages to come in, i.e. 20ms } }
主函数loop()本质上是第二个FreeRTOS任务,负责从上述队列中接收消息,通过MQTT发送至本地broker,实现代码如下:
void setup() { Serial.begin(115200); // Configure and start WiFi configure_network(); connect_network(); // Configure the MQTT connection configure_mqtt_client(); // Configure and create the inter-task queues xMessagesToSendQueue = xQueueCreate(100, sizeof(tag_message_t)); if (xMessagesToSendQueue == NULL) { ESP_LOGE(TAG, "Unable to create messaging queue. Will not create UART handling message queue."); delay(10000); esp_restart(); } else { ESP_LOGI(TAG, "Messaging queue generated"); configure_uart(); xTaskCreate(uart_read_task, "UART_Processing", 20000, NULL, 1, NULL); } } void loop() { const TickType_t xTicksToWait = pdMS_TO_TICKS(100); // milliseconds to wait tag_message_t received_message; if (network_connected) { connect_mqtt_client(); while(mqtt_client.connected()) { mqtt_client.loop(); // Process messages on the xMessagesToSendQueue if (xMessagesToSendQueue != NULL) { ESP_LOGI(TAG, "Processing message"); if (xQueueReceive(xMessagesToSendQueue, &received_message, xTicksToWait) == pdPASS) { ESP_LOGD(TAG, "Received message, transmitting."); if(!mqtt_client.publish("aoa", received_message.buffer, received_message.length)); ESP_LOGW(TAG, "Failed transmission."); } else { vTaskDelay(pdMS_TO_TICKS(50)); } } else { ESP_LOGE(TAG, "Messages queue is null."); } } } else { ESP_LOGE(TAG, "WARNING Device not connected to the network. Reconnecting."); connect_network(); } delay(5000); }
已完成的验证项:
- MQTT broker运行正常
- 设备可正常连接WiFi
- Serial1端口消息读取逻辑正常
故障现象:loop()中调用xQueueReceive()时每次都会触发LoadProhibited未处理异常。
问题根因与修复方案
LoadProhibited异常是CPU尝试访问未映射到地址空间的非法内存指针导致的,结合代码实现,按故障概率从高到低排序,问题点如下:
1. 结构体定义不一致导致队列内存越界(最高发必现问题)
如果tag_message_t结构体在uart_read_task所在编译单元、和setup()/loop()所在编译单元的定义不一致(常见于头文件声明不规范、重复定义结构体、修改结构体后未全量重编译工程),会导致两处代码计算出的sizeof(tag_message_t)数值不同。
这种情况下xQueueCreate()会按照偏小的结构体大小分配队列内存,xQueueSend()写入完整结构体时会直接越界,破坏队列内核对象的链表指针、元数据字段,xQueueReceive()访问这些被篡改的非法指针时就会触发异常。
- 验证方式:在
setup()创建队列后、uart_read_task发送消息前分别打印sizeof(tag_message_t),数值不一致即可确认问题。 - 修复方式:将
tag_message_t的typedef声明放在统一的公共头文件中,所有使用队列的代码文件都包含该头文件,执行全量重编译。
2. UART接收缓冲区越界写破坏队列句柄
你贴出的代码中未定义first_char和message_buffer变量,如果message_buffer的实际长度小于AZ_EL_MAX_MESSAGE_LENGTH,当接收到长度超过缓冲区的消息时,写入操作会越界覆盖相邻内存。如果全局队列句柄xMessagesToSendQueue刚好被分配在message_buffer的相邻内存位置,就会被篡改成为非NULL的非法地址,传入xQueueReceive()后直接触发异常。
- 修复方式:明确定义
char message_buffer[AZ_EL_MAX_MESSAGE_LENGTH],不要使用长度不匹配的全局/局部数组,可额外多分配1字节冗余空间避免溢出。
3. loop任务栈溢出导致接收缓冲区地址非法
ESP32 Arduino核心的loop()任务默认栈大小为8KB,如果AZ_EL_MAX_MESSAGE_LENGTH定义过大(比如超过7KB),栈上定义的tag_message_t received_message变量会直接撑爆栈空间,导致栈指针落在未映射内存区域,xQueueReceive()往该地址拷贝消息时触发异常。
- 验证方式:将
received_message改为全局变量/静态变量(分配在全局内存区而非栈上),如果异常消失即可确认。 - 修复方式:缩小消息缓冲区最大长度,或修改Arduino核心配置调大loop任务栈空间。
4. 任务栈配置不合理导致堆内存破坏
原生FreeRTOS的xTaskCreate()栈深度单位是4字节字而非字节,你传入的20000栈深度实际需要分配80KB栈空间,在开启WiFi、MQTT的场景下很容易出现堆内存不足。你没有检查xTaskCreate()的返回值,若任务创建失败会破坏堆结构,导致后续队列操作访问非法内存。
- 修复方式:将UART任务栈大小调整为8192即可满足串口读取任务需求,同时检查
xTaskCreate()返回值是否为pdPASS,确认任务创建成功。
其他非崩溃类逻辑bug
- MQTT发布判断逻辑错误:
if(!mqtt_client.publish("aoa", received_message.buffer, received_message.length));末尾多了一个分号,会导致无论发布成功与否都会打印发布失败的警告,需要删除该多余分号。 - UART读取逻辑缺少超时保护:检测到'+'起始符后如果一直收不到'\n'结束符,会持续读取串口返回的-1值填满缓冲区,建议增加读取超时判断,避免无效占用CPU。
内容的提问来源于stack exchange,提问作者EruditeEngineer

