ESP32整合HTTP请求、ArduinoJson解析与DMD32显示功能后,启用定时器触发系统崩溃问题排查
问题背景
我最近在做一个ESP32项目,要同时实现两个功能:从HTTP接口拉取JSON数据并用ArduinoJson解析,以及用DMD32库驱动P10显示屏展示数据。单独跑每个功能都正常,但把它们合在一起后,ESP32直接就崩溃重启了。
错误日志
串口输出的崩溃信息如下:
Start an alarm. assert failed: xQueueSemaphoreTake queue.c:1554 (!( ( xTaskGetSchedulerState() == ( ( BaseType_t ) 0 ) ) && ( xTicksToWait != 0 ) )) Rebooting... ets Jul 29 2019 12:21:46 rst:0xc (SW_CPU_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT) configsip: 0, SPIWP:0xee clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00 mode:DIO, clock div:1 load:0x3fff0030,len:1324 ho 0 tail 12 room 4 load:0x40078000,len:13508 load:0x40080400,len:3604 entry 0x400805f0
问题定位
我通过逐行注释代码排查,发现崩溃是在执行timerAlarmEnable(timer);(对应代码第67行)时触发的——只要启用这个定时器,系统立刻就会崩溃重启。
完整代码
#include <DMD32.h> #include "fonts/SystemFont5x7.h" #include "fonts/Arial_black_16.h" //---------------------------------------- // 初始化DMD显示屏 #define DISPLAYS_ACROSS 2 #define DISPLAYS_DOWN 1 DMD dmd(DISPLAYS_ACROSS, DISPLAYS_DOWN); // 硬件定时器配置 hw_timer_t * timer = NULL; // 定时器中断处理函数:用于触发显示屏刷新 void IRAM_ATTR triggerScan() { dmd.scanDisplayBySPI(); } #include <WiFi.h> #include <HTTPClient.h> #include <ArduinoJson.h> const char* ssid = "JacqHousey2"; const char* password = "XXXX"; void setup() { Serial.begin(115200); delay(4000); Serial.println(); delay(500); Serial.println(); Serial.println("获取CPU时钟频率"); uint8_t cpuClock = ESP.getCpuFreqMHz(); delay(500); Serial.println(); Serial.println("初始化定时器"); // 使用第0个硬件定时器,分频系数为CPU时钟频率(得到1us的计时单位) timer = timerBegin(0, cpuClock, true); delay(500); Serial.println(); Serial.println("绑定中断处理函数"); timerAttachInterrupt(timer, &triggerScan, true); delay(500); Serial.println(); Serial.println("设置定时器触发周期"); // 每300us触发一次中断,循环触发 timerAlarmWrite(timer, 300, true); delay(500); // 执行此行后系统崩溃 Serial.println(); Serial.println("启动定时器"); timerAlarmEnable(timer); delay(500); // 连接WiFi WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(1000); Serial.println("连接WiFi中..."); } Serial.println("WiFi连接成功"); } void loop() { if ((WiFi.status() == WL_CONNECTED)) { WiFiClient client; HTTPClient http; http.useHTTP10(true); http.begin(client, "http://bankethical.au/getlatestnames.php"); http.GET(); // 解析JSON响应 DynamicJsonDocument doc(2048); deserializeJson(doc, http.getStream()); int numdonors = doc.size(); // 解析第一条捐赠数据 JsonArray root_0 = doc[0]; const char* root_0_0 = root_0[0]; // "11" const char* root_0_1 = root_0[1]; // "2025-10-18 23:53:39.561412" const char* root_0_2 = root_0[2]; // "NEW" const char* root_0_3 = root_0[3]; // "USD" const char* root_0_4 = root_0[4]; // "100" const char* root_0_5 = root_0[5]; // "100" const char* root_0_6 = root_0[6]; // "John" const char* root_0_7 = root_0[7]; // "Doe" const char* root_0_8 = root_0[8]; // "johndoeemail@hotmail.com" const char* root_0_9 = root_0[9]; // "387.61" // 解析第二条捐赠数据 JsonArray root_1 = doc[1]; const char* root_1_0 = root_1[0]; // "12" const char* root_1_1 = root_1[1]; // "2025-10-18 23:56:27.002691" const char* root_1_2 = root_1[2]; // "NEW" const char* root_1_3 = root_1[3]; // "USD" const char* root_1_4 = root_1[4]; // "100" const char* root_1_5 = root_1[5]; // "100" const char* root_1_6 = root_1[6]; // "John" const char* root_1_7 = root_1[7]; // "Doe" const char* root_1_8 = root_1[8]; // "johndoeemail@hotmail.com" const char* root_1_9 = root_1[9]; // "487.61" // 解析第三条捐赠数据(原代码截断,保留原样) JsonArray root_2 = doc[2]; const char* root_2_0 = root_2[0]; // "13" const char* root_2_1 = root_2[1]; // "2025-10-18 23:" } }
问题分析
从错误日志里的assert failed: xQueueSemaphoreTake可以看出,崩溃的核心原因是在中断上下文里调用了需要阻塞等待队列信号量的操作。
具体来说,你在硬件定时器的中断处理函数triggerScan()里直接调用了dmd.scanDisplayBySPI(),而这个DMD32库的函数内部可能使用了xQueueSemaphoreTake并传入了非0的等待时间。但ESP32的FreeRTOS规则里,中断上下文是绝对不能执行阻塞等待操作的——因为中断是抢占式的,调度器在中断里不会工作,一旦调用带等待的队列操作,就会触发断言检查,直接重启系统。
另外,你的代码里是先启用定时器,再连接WiFi,此时系统的一些调度资源可能还没完全初始化,也可能加剧了问题。
解决方案建议
1. 把扫描操作移出中断上下文(最有效的解决方法)
不要在中断里直接调用dmd.scanDisplayBySPI(),而是在中断里只设置一个标志位,然后在主循环(loop)里检查标志位并执行扫描。这样中断里的操作极轻量,不会触发调度相关的断言。
修改步骤:
- 定义一个全局的volatile标志位(确保中断和主任务都能正确访问):
volatile bool needRefreshDisplay = false;
- 修改中断处理函数:
void IRAM_ATTR triggerScan() { needRefreshDisplay = true; }
- 在loop函数开头添加扫描逻辑:
void loop() { // 处理显示屏刷新 if (needRefreshDisplay) { needRefreshDisplay = false; dmd.scanDisplayBySPI(); } // 原来的HTTP请求和解析代码... if ((WiFi.status() == WL_CONNECTED)) { // 此处保留你原有的HTTP请求、JSON解析逻辑 } }
2. 调整初始化顺序
把WiFi连接的代码移到定时器启用之前,确保系统网络资源初始化完成后再启动定时器:
void setup() { Serial.begin(115200); delay(4000); Serial.println(); delay(500); // 先连接WiFi Serial.println("连接WiFi中..."); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(1000); Serial.println("连接WiFi中..."); } Serial.println("WiFi连接成功"); // 再初始化定时器 Serial.println(); Serial.println("获取CPU时钟频率"); uint8_t cpuClock = ESP.getCpuFreqMHz(); delay(500); Serial.println(); Serial.println("初始化定时器"); timer = timerBegin(0, cpuClock, true); delay(500); Serial.println(); Serial.println("绑定中断处理函数"); timerAttachInterrupt(timer, &triggerScan, true); delay(500); Serial.println(); Serial.println("设置定时器触发周期"); timerAlarmWrite(timer, 300, true); delay(500); Serial.println(); Serial.println("启动定时器"); timerAlarmEnable(timer); delay(500); }
3. 检查DMD32库的实现细节
如果上面的方法还是有问题,可以查看DMD32库的scanDisplayBySPI()函数源码,确认它内部是否确实使用了带阻塞的队列操作。如果是,也可以考虑修改库的代码,把阻塞操作改成非阻塞的,或者用其他方式实现显示屏刷新。
总结
这个问题的核心是中断上下文不能执行阻塞式的系统调用,只要把显示屏的刷新操作从中断里移到主任务(loop)里,就能解决这个崩溃问题。调整初始化顺序是辅助手段,能让系统资源准备更充分,但最根本的还是中断操作的轻量化处理。




