/** * ESP32 故障诊断、常见错误与问题排查指南的结构化数据。 * 涵盖 6 个技术类别,包含根因分析、最佳实践与临时凑合方案的对比, * 以及 Arduino C++、ESP-IDF(C)和 MicroPython 的代码示例。 * 代码示例中的注释为中文,串口日志字符串保持英文,以免终端显示乱码。 */ export const categoriasDiagnostico = [ { id: "alimentacao", nome: "供电与硬件", icone: "Zap", cor: "amber", descricao: "Wi-Fi/BLE 射频峰值引起的电压跌落、冲突的 启动配置引脚、电磁噪声以及 LDO 问题。", }, { id: "cpu-panics", nome: "CPU 异常与 Guru Meditation", icone: "Cpu", cor: "red", descricao: "空指针、内存访问违规(Load/StoreProhibited)、非法指令以及 ISR 中的缓存异常。", }, { id: "freertos", nome: "FreeRTOS 与多任务", icone: "Layers", cor: "purple", descricao: "任务看门狗(TWDT)、中断看门狗(IWDT)、栈溢出(Stack Overflow)以及调度器阻塞。", }, { id: "memoria", nome: "内存、堆与 PSRAM", icone: "Activity", cor: "blue", descricao: "堆碎片化、内存泄漏(Memory Leaks)、DMA 内存分配以及 PSRAM 的限制。", }, { id: "boot-flash", nome: "引导加载程序与烧录", icone: "Terminal", cor: "emerald", descricao: "串口通信失败(DTR/RTS)、SPI 闪存读取错误、时钟不匹配与分区问题。", }, { id: "conectividade", nome: "网络连接与协议", icone: "Wifi", cor: "cyan", descricao: "Wi-Fi Beacon 超时、缺少 SNTP 时钟导致的 TLS/HTTPS 握手失败,以及信号不稳定。", }, ]; export const sintomasRapidos = [ { id: "bootloop", rotulo: "🔄 反复重启(Bootloop)", categoria: "todos" }, { id: "wifi-crash", rotulo: "📶 开启 Wi-Fi/BLE 时死机", categoria: "alimentacao" }, { id: "upload-fail", rotulo: "🔌 烧录代码失败(Upload)", categoria: "boot-flash" }, { id: "random-freeze", rotulo: "⏱️ 运行数分钟/数小时后死机", categoria: "memoria" }, { id: "null-pointer", rotulo: "💥 Guru Meditation(崩溃)", categoria: "cpu-panics" }, { id: "wdt-reset", rotulo: "🐕 Watchdog Triggered", categoria: "freertos" }, { id: "tls-fail", rotulo: "🔒 HTTPS / SSL 失败", categoria: "conectividade" }, ]; export const errosCatalogo = [ { id: "brownout-reset", titulo: "Brownout Detector 复位(电压跌落)", categoria: "alimentacao", sintomas: ["bootloop", "wifi-crash"], gravidade: "高", logSerial: "Brownout detector was triggered\n\nrst:0xc (SW_CPU_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)", resumo: "ESP32 的供电电压跌到了安全工作阈值以下(约 2.8V ~ 2.43V)。", causaRaiz: "开启 Wi-Fi 或 BLE 射频时,功率放大级(PA)会在不到 1 毫秒内产生超过 500mA 的快速电流脉冲。如果电源、又长又细的 USB 线、面包板电源轨或 LDO 稳压器无法以低阻抗提供瞬时电流,芯片 3V3 引脚上的电压就会瞬间塌陷,触发内部的 Brownout 比较电路,以避免闪存数据损坏和不确定状态。", gambiarra: { titulo: "通过软件关闭 Brownout", descricao: "调用 WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0),在代码里让错误不再出现。", consequencia: "极其危险:ESP32 会在欠压下继续执行指令,损坏 SPI 闪存、锁死寄存器,并在现场出现无预警的不可预测行为。", }, solucaoEngenharia: { titulo: "合理设计硬件与去耦", passos: [ "在尽可能靠近 ESP32 的 3V3 和 GND 引脚处,并联一个 10µF 到 100µF 的低 ESR(Low-ESR)钽电容或陶瓷电容,以及一个 100nF 陶瓷电容。", "使用峰值电流不低于 600mA 至 1A、瞬态响应优秀的 LDO 稳压器(例如 AP2112K、ME6211、TPS73733 或可靠的 XC6206)。避免使用老款 AMS1117 的劣质仿品。", "当 Wi-Fi 以最大功率发射时,避免用很长的面包板电源轨加细杜邦线给 ESP32 供电。", ], }, codigoArduino: `// 不要关闭 Brownout!如果测试时需要降低峰值: #include void setup() { Serial.begin(115200); // 可选:把 Wi-Fi 最大发射功率从 20dBm 略降到 17dBm, // 可使电流峰值降低约 20% WiFi.setTxPower(WIFI_POWER_17dBm); WiFi.begin("SSID", "PASSWORD"); }`, codigoEspIdf: `// ESP-IDF 中的配置: #include "esp_wifi.h" #include "esp_log.h" void app_main(void) { // 在 menuconfig 中:Component config -> ESP32-specific -> Hardware Brownout Reset(始终保持开启) // 在有功率限制的环境中降低峰值: esp_wifi_set_max_tx_power(68); // 68 = 17dBm(单位为 0.25dBm) }`, codigoMicroPython: `# MicroPython 中带稳定等待的安全 Wi-Fi 连接: import network import time wlan = network.WLAN(network.STA_IF) wlan.active(True) # 等待供电稳定后再开始发射 time.sleep_ms(200) wlan.connect("SSID", "PASSWORD") tentativas = 0 while not wlan.isconnected() and tentativas < 30: time.sleep_ms(200) tentativas += 1 if wlan.isconnected(): print("Wi-Fi connected! IP:", wlan.ifconfig()[0]) else: print("[ERROR] Failed to connect to Wi-Fi")`, dicaPro: "如果你在设计自己的 PCB,请在模组下方铺设完整的地平面,并让电源走线宽度至少为 0.5mm 到 1mm,以承受电流浪涌而不产生阻性压降(IR drop)。", }, { id: "strapping-pins", titulo: "启动时 启动配置引脚冲突", categoria: "alimentacao", sintomas: ["bootloop", "upload-fail"], gravidade: "严重", logSerial: "rst:0x1 (POWERON_RESET),boot:0x12 (SPI_FAST_FLASH_BOOT)\nflash read err, 1000\nwaiting for download", resumo: "ESP32 无法从闪存启动固件,或意外进入下载模式,原因是某个 启动配置引脚在复位时被强制到了错误的逻辑电平。", causaRaiz: "在 EN/CHIP_PU 引脚拉高(上电复位)的瞬间,CPU 会对特定引脚(启动配置引脚)的状态采样,以配置闪存电压(1.8V 或 3.3V)、启动模式(UART 下载或 SPI 闪存)和时钟输出。如果你在这些引脚上接了带上拉/下拉的传感器、继电器或 LED,微控制器读到的就是无效逻辑电平,启动随之卡住。", gambiarra: { titulo: "在代码里加延时,或每次上电都手动拔线", descricao: "试图在 setup() 里用软件解决问题,或依赖每次重启前手动断开连线。", consequencia: "没用:启动配置引脚由硬件读取,此时 CPU 还没有加载你的代码或引导加载程序中的任何一行。", }, solucaoEngenharia: { titulo: "安全的 GPIO 分配与复位期间隔离", passos: [ "经典 ESP32:避免使用 GPIO 0(Boot)、GPIO 2(闪存启动时必须为 LOW 或悬空)、GPIO 12(决定闪存电压:复位时若被拉高,会损坏或卡死 3.3V 闪存)以及 GPIO 15(为 LOW 时屏蔽启动日志)。", "ESP32-C3:避免在复位时把 GPIO 8 或 GPIO 9 拉低。", "ESP32-S3:注意 GPIO 0、GPIO 45(VDD_SPI 电压)和 GPIO 46。", "如果必须把这些引脚用于外设,请用三极管或缓冲器隔离,直到 EN 信号完全稳定。", ], }, codigoArduino: `// 安全引脚分配建议(经典 ESP32): // 适合 I2C / SPI / 一般传感器的引脚: // SDA: GPIO 21 | SCL: GPIO 22 // SPI: MOSI 23 | MISO 19 | SCK 18 | CS 5 // 注意:GPIO 34、35、36 (VP)、39 (VN) 只能作输入(没有内部上拉)。 // 不要对它们使用 pinMode(..., OUTPUT),也不要用于 I2C/SPI。`, codigoEspIdf: `// 在 ESP-IDF 中,通过 eFuse 固定闪存电压设置(注意:不可逆): // 如果你使用经典 ESP32 且需要释放 GPIO12,可以烧写 3.3V 的 eFuse: // espefuse.py --port /dev/ttyUSB0 set_flash_voltage 3.3V // 这会永久关闭 GPIO12 上的硬件采样。`, codigoMicroPython: `# MicroPython 中正确的引脚配置: from machine import Pin # ✅ 可安全用于输出和带内部上拉按键的引脚: led = Pin(2, Pin.OUT) # GPIO 2 botao = Pin(4, Pin.IN, Pin.PULL_UP) # GPIO 4(安全) # ⚠️ 引脚 34、35、36、39 只能作输入,且没有内部上拉: sensor_analogico = Pin(36, Pin.IN) # 正确(若用作按键,需要外接电阻)`, dicaPro: "请始终记住:“仅输入”引脚(经典 ESP32 的 GPI 34 到 39)在芯片内没有上拉/下拉电路,也没有输出三极管。在这些引脚上接模拟传感器或按键,需要外接上拉电阻。", }, { id: "guru-load-prohibited", titulo: "Guru Meditation:LoadProhibited / StoreProhibited", categoria: "cpu-panics", sintomas: ["bootloop", "null-pointer"], gravidade: "严重", logSerial: "Guru Meditation Error: Core 0 panic'ed (LoadProhibited). Exception was unhandled.\nCore 0 register dump:\nPC : 0x400d14bc PS : 0x00060830 A0 : 0x800d1512 A1 : 0x3ffb1f20 \nEXCVADDR: 0x00000000", resumo: "CPU 试图读取(Load)或写入(Store)一个不可访问或无效的内存地址,通常是空地址 0x00000000。", causaRaiz: "这是典型的空指针解引用(Null Pointer Dereference)或野指针(Dangling Pointer)。EXCVADDR 寄存器给出了引发故障的地址:如果 EXCVADDR 为 0x00000000 或非常接近零(例如 0x00000008,即结构体成员的偏移),就说明你访问了一个未初始化、或 malloc/创建失败的对象/指针的成员。", gambiarra: { titulo: "随便加延时,或在循环里不处理就重启", descricao: "以为是时序问题,在代码里堆满 delay(),或者不查原因就屏蔽自动重启。", consequencia: "空指针条件再次出现时,系统在量产环境中仍会随机崩溃。", }, solucaoEngenharia: { titulo: "防御式校验与 Backtrace 解码", passos: [ "在访问返回值的成员之前,始终检查分配或查找类函数(malloc、WiFi.client()、JSON 解析、xQueueReceive)是否返回了 NULL。", "在你的 IDE(Arduino IDE / PlatformIO)中安装“Espressif Exception Decoder”,或使用命令 'xtensa-esp32-elf-addr2line -pfia -e firmware.elf 0x400d14bc',精确找到崩溃发生在 .c/.cpp 的哪一行。", "在 ESP-IDF 中运行 'idf.py monitor',它会实时自动解码栈回溯。", ], }, codigoArduino: `// 防御式编程: struct SensorData { float temperatura; float umidade; }; void processarSensor(SensorData* sensor) { // 防止 LoadProhibited 的必要检查: if (sensor == nullptr) { Serial.println("[ERROR] Null sensor pointer!"); return; } Serial.printf("Temp: %.1f C\\n", sensor->temperatura); }`, codigoEspIdf: `// 在 ESP-IDF 中使用 ESP_ERROR_CHECK 或显式断言: #include "esp_log.h" #include "esp_check.h" static const char* TAG = "SENSOR"; esp_err_t ler_dado(char* buffer, size_t max_len) { ESP_RETURN_ON_FALSE(buffer != NULL, ESP_ERR_INVALID_ARG, TAG, "Null buffer provided!"); // 安全的操作... return ESP_OK; }`, codigoMicroPython: `# 在 MicroPython 中防止 NoneType 和对象异常: import json def processar_sensor(dados_raw): # 防御式处理空 payload: if dados_raw is None: print("[ERROR] Null data received!") return None try: obj = json.loads(dados_raw) # 安全使用带默认值的 .get(),避免 KeyError temperatura = obj.get("temp", 0.0) return temperatura except (ValueError, AttributeError) as e: print("Error processing object:", e) return None`, dicaPro: "如果 EXCVADDR 类似 0x3fxxxxxx 或 0x40xxxxxx 但你的代码仍然出错,可能是数组越界访问(buffer overflow)覆盖了内存中的指针。", }, { id: "guru-iram-cache", titulo: "ISR 中的 Cache Error / InstructionFetchError", categoria: "cpu-panics", sintomas: ["bootloop", "null-pointer"], gravidade: "高", logSerial: "Guru Meditation Error: Core 0 panic'ed (Cache disabled but cached memory region accessed)\nGuru Meditation Error: Core 0 panic'ed (InstructionFetchError)", resumo: "中断服务例程(ISR)试图在 SPI 闪存因读写操作而被禁用时,执行闪存中的代码或读取其中的数据。", causaRaiz: "默认情况下,ESP32 的代码存放在外部闪存中,并通过内部缓存(Cache)按需加载给 CPU。当 ESP32 向闪存写入数据(如写入 NVS、LittleFS、SPIFFS 或 OTA 升级)时,控制器会暂时关闭指令缓存。如果此刻触发了硬件中断(定时器、GPIO 等),而 ISR 函数位于闪存中,CPU 就取不到下一条指令,立即陷入异常。", gambiarra: { titulo: "写闪存前关闭中断", descricao: "不加区分地用 noInterrupts() 包住所有文件写入操作。", consequencia: "会丢失高速传感器的脉冲,导致系统定时器不稳定,并可能引发看门狗溢出。", }, solucaoEngenharia: { titulo: "把 ISR 与常量放进 IRAM / DRAM", passos: [ "给每一个中断回调函数都加上 IRAM_ATTR 属性(或 IRAM_ATTR static void...)。", "确保 ISR 中访问的变量或字符串也位于 DRAM 中(在 ESP-IDF 中,避免在 ISR 里直接使用没有 DRAM_ATTR 标记的字符串字面量)。", "不要在 ISR 里调用外部库的重型函数(如 Serial.print 或大量浮点计算)。", ], }, codigoArduino: `// 正确放入 IRAM 的 ISR: volatile uint32_t contadorPulsos = 0; void IRAM_ATTR tratarInterrupcaoGPIO() { contadorPulsos++; } void setup() { pinMode(4, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(4), tratarInterrupcaoGPIO, FALLING); }`, codigoEspIdf: `// 在 ESP-IDF 中: #include "esp_attr.h" #include "driver/gpio.h" static volatile uint32_t pulsos = 0; // 显式放入内部 IRAM 的函数: static void IRAM_ATTR gpio_isr_handler(void* arg) { pulsos++; } void app_main(void) { gpio_install_isr_service(ESP_INTR_FLAG_IRAM); gpio_isr_handler_add(GPIO_NUM_4, gpio_isr_handler, NULL); }`, codigoMicroPython: `# MicroPython 中安全的中断(ISR)处理: import micropython from machine import Pin # 分配应急缓冲区,用于调试 ISR 内部的异常: micropython.alloc_emergency_exception_buf(100) contador_pulsos = 0 # 中断回调:不要分配内存(不要创建字符串,不要产生新的浮点数) def tratar_pulso(pin): global contador_pulsos contador_pulsos += 1 sensor = Pin(4, Pin.IN, Pin.PULL_UP) sensor.irq(trigger=Pin.IRQ_FALLING, handler=tratar_pulso)`, dicaPro: "如果在 ESP-IDF 中注册中断时传入了 ESP_INTR_FLAG_IRAM,那么该中断调用的任何函数(包括 FreeRTOS 函数)都必须支持 IRAM(例如 xQueueSendFromISR)。", }, { id: "freertos-twdt", titulo: "任务看门狗定时器(TWDT)超时", categoria: "freertos", sintomas: ["bootloop", "wdt-reset"], gravidade: "严重", logSerial: "E (10532) task_wdt: Task watchdog got triggered. The following tasks/users did not reset the watchdog in time:\nE (10532) task_wdt: - IDLE0 (CPU 0)\nTask watchdog got triggered. System will be reset.", resumo: "一个高优先级任务陷入阻塞式循环,没有让出 CPU 时间,使 IDLE 任务无法喂狗。", causaRaiz: "ESP32 上的 FreeRTOS 在每个 CPU 内核上都运行一个优先级为 0 的关键任务,名为 IDLE。任务看门狗会监测 IDLE 任务能否周期性运行(默认每 5 秒)。如果你创建了一个优先级 >= 1 的任务,其中有 while(1) 循环却没有任何阻塞调用(vTaskDelay、读队列、信号量),CPU 就会 100% 被占满,看门狗随即触发紧急复位。", gambiarra: { titulo: "在 menuconfig 里关闭任务看门狗,或把它调到 60 秒", descricao: "关掉 TWDT,让代码不再重启。", consequencia: "如果代码陷入死循环或死锁,微控制器会永远冻结,只能有人跑到现场拔电源。", }, solucaoEngenharia: { titulo: "事件驱动架构与让出 CPU(Yield)", passos: [ "在 FreeRTOS 任务中,不要使用空的忙等待循环 while(!condicao);。", "始终加入 vTaskDelay(pdMS_TO_TICKS(10)),或通过带超时的 xQueueReceive()、ulTaskNotifyTake() 或 xSemaphoreTake() 等待事件。", "如果必须持续进行繁重的数学运算,请用 esp_task_wdt_reset() 显式喂狗。", ], }, codigoArduino: `// Arduino 搭配 FreeRTOS 的正确写法: void minhaTarefa(void *pvParameters) { for (;;) { // 读取传感器 float valor = analogRead(36); // 把 CPU 让给 FreeRTOS 和 IDLE 任务: vTaskDelay(pdMS_TO_TICKS(100)); // 以非阻塞方式等待 100ms } }`, codigoEspIdf: `// ESP-IDF 中使用任务看门狗: #include "esp_task_wdt.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" void tarefa_processamento(void *pvParameters) { // 可选:把当前任务注册到 TWDT esp_task_wdt_add(NULL); while (1) { // 处理... // 如果这段处理很长,就喂狗: esp_task_wdt_reset(); vTaskDelay(pdMS_TO_TICKS(50)); } }`, codigoMicroPython: `# MicroPython 中使用 uasyncio 的看门狗与非阻塞循环: from machine import WDT import uasyncio as asyncio # 初始化硬件看门狗,超时 5 秒 wdt = WDT(timeout=5000) async def tarefa_sensor(): while True: # 每个正常周期都喂狗: wdt.feed() # 把控制权交还给协作式调度器: await asyncio.sleep_ms(100) async def main(): asyncio.create_task(tarefa_sensor()) while True: await asyncio.sleep(1) asyncio.run(main())`, dicaPro: "在 Arduino 中,delay(ms) 函数内部其实调用的是 FreeRTOS 的 vTaskDelay。但使用 delayMicroseconds() 的循环或纯 CPU 运算不会让出时间,持续数秒以上就可能触发 TWDT。", }, { id: "freertos-stack-overflow", titulo: "栈溢出(Stack Overflow)", categoria: "freertos", sintomas: ["bootloop", "null-pointer"], gravidade: "严重", logSerial: "Guru Meditation Error: Core 1 panic'ed (Unhandled debug exception).\nDebug exception reason: Stack canary watchpoint triggered (minha_tarefa)", resumo: "任务消耗的栈(Stack)内存超过了创建时分配的大小,破坏了 RTOS 的保护金丝雀(canary)。", causaRaiz: "FreeRTOS 中的每个任务都会得到一块固定的连续内存,用来存放局部变量、函数参数和调用的返回地址。如果你声明了大数组(如 char buffer[2048];)、实例化了较大的 JSON 缓冲区(如 DynamicJsonDocument doc(4096);),或在默认栈为 2048 字节的任务里做很深的递归调用,栈就会越界并覆盖系统的关键区域。", gambiarra: { titulo: "不加考虑地给所有任务都设 64KB 栈", descricao: "分配巨大的栈,免得操心溢出。", consequencia: "ESP32 的内部 SRAM(实际可用约 320KB)很快会耗尽,导致 Wi-Fi 初始化和堆分配失败。", }, solucaoEngenharia: { titulo: "有意识地确定大小,并用 HighWaterMark 监控", passos: [ "使用 uxTaskGetStackHighWaterMark() 函数测量栈的实际消耗。它返回自启动以来栈剩余空闲字节/字数的最小值。", "把体积大的缓冲区从栈移到动态堆(临时使用 malloc/free),如果会被重复使用,则在函数作用域外声明为 static。", "使用 TLS/HTTPS 安全连接或 SSL 的 MQTT 的任务,由于 MbedTLS 加密例程很复杂,至少需要 6KB 到 8KB 的栈。", ], }, codigoArduino: `// 在 Arduino 中监控栈: void tarefaSensor(void *pvParameters) { for (;;) { // 任务代码... // 检查栈的最小剩余空间(经典 FreeRTOS 以 Word 为单位,ESP32 以字节为单位) UBaseType_t espacoLivre = uxTaskGetStackHighWaterMark(NULL); Serial.printf("[STACK] Minimum free space: %d bytes\\n", (int)espacoLivre); vTaskDelay(pdMS_TO_TICKS(5000)); } } void setup() { Serial.begin(115200); // 合理地分配 4096 字节(4KB): xTaskCreatePinnedToCore(tarefaSensor, "tarefaSensor", 4096, NULL, 1, NULL, 1); }`, codigoEspIdf: `// 在 menuconfig 中启用栈的双重检查: // Component config -> FreeRTOS -> Set a end of stack check -> Option 2 (Check by Stack Canary) // 这样能在运行时数据被破坏之前就检测到溢出。`, codigoMicroPython: `# MicroPython 中监控内存与栈: import gc import micropython # 输出 Python 虚拟机的 RAM 与栈统计: micropython.mem_info() # 运行垃圾回收,释放孤立的内存块: gc.collect() print("Free heap:", gc.mem_free(), "bytes") print("Allocated heap:", gc.mem_alloc(), "bytes")`, dicaPro: "如果函数只是偶尔处理一次 JSON 或 HTTP 数据包,请只在处理期间把缓冲区分配在堆上,用完立刻释放,而不是撑大任务的常驻栈。", }, { id: "memoria-fragmentacao-heap", titulo: "堆碎片化与内存泄漏", categoria: "memoria", sintomas: ["random-freeze", "tls-fail"], gravidade: "高", logSerial: "E (45892) mbedtls: mbedtls_ssl_setup returned -0x7f00\nE (45892) esp-tls: create_ssl_handle failed\nE (45893) HTTP_CLIENT: Connection failed, out of memory", resumo: "微控制器分配内存失败,尽管报告的空闲内存总量看起来还很多。", causaRaiz: "当长时间发生成千上万次大小不一的分配与释放时(在 Arduino 中对 String 对象在循环里不断拼接文本时非常常见),堆就会碎片化。堆被分割成许多被占用块隔开的小空闲块。当 Wi-Fi 驱动或 TLS 库需要一块连续的 40KB 作为 SSL 加密缓冲区时,即便分散的空闲内存总共有 100KB,malloc 也会失败。", gambiarra: { titulo: "每隔 X 小时用 ESP.restart() 定期重启 ESP32", descricao: "安排每天重启一次,以清理碎片化的内存。", consequencia: "只是掩盖缺陷:在请求高峰时,设备会远早于计划的重启就崩溃。", }, solucaoEngenharia: { titulo: "避免动态字符串,使用预先分配的静态缓冲区", passos: [ "在 Arduino 中避免使用 String 类。使用 C 风格的字符缓冲区(char buffer[128];)和 snprintf() 这类安全函数。", "通过对照空闲内存总量(heap_caps_get_free_size)与最大连续空闲块(heap_caps_get_largest_free_block)来监控碎片化。", "在复杂的 ESP-IDF 固件中,启用堆内存追踪(Heap Memory Tracing,CONFIG_HEAP_TRACING),精确列出是哪些文件和行忘了调用 free()。", ], }, codigoArduino: `// 对比: // ❌ 避免(会造成严重的堆碎片化): // String json = "{\\"temp\\":"; // json += String(temperatura) + "}"; // ✅ 正确(不在堆上动态分配): char payload[64]; snprintf(payload, sizeof(payload), "{\\"temp\\":%.2f,\\"umid\\":%.2f}", temperatura, umidade); // 监控: Serial.printf("Free heap: %u | Largest block: %u\\n", ESP.getFreeHeap(), ESP.getMaxAllocHeap());`, codigoEspIdf: `// ESP-IDF 中的堆监控: #include "esp_heap_caps.h" #include "esp_log.h" void inspecionar_memoria(void) { size_t livre_total = heap_caps_get_free_size(MALLOC_CAP_8BIT); size_t maior_bloco = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT); ESP_LOGI("HEAP", "Total free: %d bytes | Largest contiguous block: %d bytes", livre_total, maior_bloco); }`, codigoMicroPython: `# MicroPython 中预防堆碎片化: import gc # 1. 启用自动回收 gc.enable() # 2. 预先分配 bytearray 并复用,而不是创建新字符串: buffer_leitura = bytearray(256) def ler_sensor_sem_alocacao(stream): # 复用同一块内存读取(零分配,Zero-Allocation): bytes_lidos = stream.readinto(buffer_leitura) return buffer_leitura[:bytes_lidos] # 3. 在建立重量级连接(如 HTTPS / TLS)之前触发 GC: gc.collect() print(f"Free heap: {gc.mem_free()} B | Allocated: {gc.mem_alloc()} B")`, dicaPro: "如果最大连续空闲块小于 35KB ~ 40KB,现代 HTTPS/TLS 连接会在证书握手期间失败。", }, { id: "memoria-dma-psram", titulo: "在外部内存(PSRAM)中分配 DMA 失败", categoria: "memoria", sintomas: ["bootloop", "random-freeze"], gravidade: "高", logSerial: "E (1204) spi_master: spi_bus_initialize: DMA channel not compatible with external RAM buffer\nESP_ERROR_CHECK failed: esp_err_t 0x102 (ESP_ERR_INVALID_ARG)", resumo: "固件试图把分配在外部 PSRAM 中的缓冲区交给带 DMA 的外设(SPI、I2S、LCD、SDMMC),而这些外设只能访问内部 SRAM。", causaRaiz: "在 ESP32 系列芯片中(尤其是采用标准总线的经典 ESP32、ESP32-S2 和 ESP32-S3),外设的 DMA 控制器没有直接访问通过 SPI/QSPI 连接的外部 PSRAM 的物理总线。DMA 要求收发缓冲区必须位于芯片的内部 SRAM 中。", gambiarra: { titulo: "关闭外设的 DMA", descricao: "把 SPI/I2S 总线配置为不用 DMA 的轮询模式。", consequencia: "传输速率大幅下降,向显示屏发送帧或进行音频流传输时 CPU 被 100% 占满。", }, solucaoEngenharia: { titulo: "用 MALLOC_CAP_DMA 和 MALLOC_CAP_INTERNAL 显式分配", passos: [ "始终使用 heap_caps_malloc(大小, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL) 函数分配专用于 DMA 的缓冲区。", "把体积大的非 DMA 数据(如解码后的图像、机器学习模型和待播放的音频)保留在 PSRAM 中,并按需把较小的块复制到内部的 DMA 缓冲区(乒乓缓冲,ping-pong buffering)。", ], }, codigoArduino: `// 在 Arduino ESP32 中分配 DMA 缓冲区: #include uint8_t* dmaBuffer = nullptr; void setup() { size_t tamanho = 4096; // 确保缓冲区位于支持 DMA 的内部 SRAM 中: dmaBuffer = (uint8_t*) heap_caps_malloc(tamanho, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); if (dmaBuffer == nullptr) { Serial.println("[ERROR] Failed to allocate DMA buffer in internal memory!"); } }`, codigoEspIdf: `// ESP-IDF: #include "esp_heap_caps.h" uint8_t* alocar_buffer_spi(size_t len) { return (uint8_t*) heap_caps_aligned_alloc(4, len, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); }`, codigoMicroPython: `# 在 MicroPython 中,检查可用的 PSRAM 内存: import gc import esp32 # 如果固件编译时带有 SPIRAM 支持(例如名称含 'SPIRAM' 的固件): print("Total memory accessible in MicroPython:", gc.mem_free() + gc.mem_alloc(), "bytes") # 对外设做快速操作时,使用连续的 bytearray: buffer_spi = bytearray(1024)`, dicaPro: "在 ESP32-P4 上,先进的 AXI 总线架构允许在某些外部区域使用 DMA,但对整个 ESP32/S2/S3/C3 系列,必须使用内部 SRAM 的规则是严格的。", }, { id: "boot-failed-connect", titulo: "Failed to connect to ESP32: Timed out waiting for packet header", categoria: "boot-flash", sintomas: ["upload-fail"], gravidade: "中", logSerial: "A fatal error occurred: Failed to connect to ESP32: Timed out waiting for packet header\nesptool.py v4.6\nConnecting...................", resumo: "烧录工具(esptool / Arduino IDE / PlatformIO)无法让微控制器进入 UART 引导加载模式。", causaRaiz: "开始烧录固件时,USB 转串口芯片(CH340、CP2102、FTDI)会通过 DTR 和 RTS 线把 GPIO 0 引脚拉低,同时让 EN/RST 引脚先拉低再拉高。在许多常见开发板上,EN 引脚的 RC(电阻电容)延时电路电容量不足,无法在 DTR 信号变化期间保持复位。", gambiarra: { titulo: "每次烧录都用手指按住 BOOT 按钮", descricao: "每次编译器生成完二进制文件后,都要按一下物理 BOOT/GPIO0 按钮。", consequencia: "不利于敏捷开发,对于封装好的或装在外壳内的板子则根本做不到。", }, solucaoEngenharia: { titulo: "在 EN 引脚加 10µF 电容,或检查数据线", passos: [ "在板子的 EN(RST)引脚和 GND 之间接一个 10µF(或 1µF 到 10µF)的电解电容或陶瓷电容,以稳定自动复位的上升斜坡。", "检查 USB 线的数据线(D+ 和 D-)是否正常。许多便宜的手机充电线只有 VBUS 和 GND 两根电源线。", "在 Linux 上,确认你的用户属于 'dialout' 组(sudo usermod -a -G dialout $USER),并且 'brltty' 服务没有占用串口。", ], }, codigoArduino: `// 用 esptool 测试串口通信的命令行提示: // python -m esptool --port /dev/ttyUSB0 chip_id // 如果返回了 MAC 地址和芯片型号,说明物理通信完全正常。`, codigoEspIdf: `// 在 ESP-IDF 中: // idf.py -p /dev/ttyUSB0 -b 460800 flash // 如果高速烧录失败,请把波特率降到 115200: // idf.py -p /dev/ttyUSB0 -b 115200 flash`, codigoMicroPython: `# 通过终端把官方 MicroPython 固件烧录到 ESP32: # 1. 擦除闪存: # esptool.py --port /dev/ttyUSB0 erase_flash # 2. 以稳妥的波特率写入二进制文件: # esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 460800 write_flash -z 0x1000 ESP32_GENERIC-*.bin`, dicaPro: "如果你使用的是带原生 USB(Direct USB-JTAG)的 ESP32-S3 或 C3 模组,请确认选择的是 CDC/JTAG 端口,而不是传统的 UART 端口。", }, { id: "tls-sntp-clock-fail", titulo: "时钟不同步导致 TLS/HTTPS 握手失败", categoria: "conectividade", sintomas: ["tls-fail", "random-freeze"], gravidade: "高", logSerial: "E (15420) esp-tls-mbedtls: mbedtls_ssl_handshake returned -0x2700\nE (15420) esp-tls: Failed to verify peer certificate!\nE (15421) esp-tls: Verification info: The certificate is not yet valid (or has expired)", resumo: "HTTPS 安全连接、AWS IoT、带 TLS 的 MQTT 或 Web API 失败,原因是 ESP32 的内部时钟停在默认日期 1970 年 1 月 1 日。", causaRaiz: "数字安全证书(X.509 SSL/TLS)有严格的有效期(Not Before 和 Not After)。由于 ESP32 默认没有外部 RTC 电池,上电时内部时钟总是从 Unix 纪元零点(1970 年)开始。MbedTLS 客户端校验服务器证书的日期时,会认为证书“尚未生效”(因为相对 1970 年,2026 年是未来),并立刻以错误 -0x2700(MBEDTLS_ERR_X509_CERT_VERIFY_FAILED)中止加密连接。", gambiarra: { titulo: "关闭 SSL 证书校验(Insecure / No-Verify)", descricao: "在 Arduino 中使用 client.setInsecure(),或在 ESP-IDF 中关闭 CA 检查。", consequencia: "开了一个非常严重的安全缺口:设备易受中间人攻击(MitM),攻击者可以截获密码和固件。", }, solucaoEngenharia: { titulo: "在 TLS 连接之前,必须通过 SNTP 同步日期和时间", passos: [ "在 Wi-Fi 获得 IP 之后,立即启动 SNTP 客户端,指向可靠的服务器(如 pool.ntp.org、time.google.com)。", "等待时钟状态变为已同步,再发起第一次 HTTPS/MQTT 连接。", "配置正确的时区(Timezone),让日志显示准确的时间。", ], }, codigoArduino: `// Arduino 中 HTTPS 之前先做 SNTP 同步: #include #include void sincronizarRelogio() { // 配置 NTP 服务器(pool.ntp.org 和 Google) configTime(8 * 3600, 0, "pool.ntp.org", "time.google.com"); Serial.print("Waiting for time sync"); time_t now = time(nullptr); // 2020 年的 Unix 时间是 1577836800。如果更小,说明仍停在 1970 年: while (now < 1577836800) { delay(500); Serial.print("."); now = time(nullptr); } Serial.println("\\n[OK] Time synchronized! TLS connections enabled."); }`, codigoEspIdf: `// ESP-IDF 中的 SNTP: #include "esp_sntp.h" #include "esp_log.h" void inicializar_sntp(void) { ESP_LOGI("SNTP", "Starting SNTP client..."); esp_sntp_setoperatingmode(SNTP_OPMODE_POLL); esp_sntp_setservername(0, "pool.ntp.org"); esp_sntp_init(); // 等待时钟同步... }`, codigoMicroPython: `# MicroPython 中的 SNTP 同步与 HTTPS 请求: import ntptime import time import urequests def sincronizar_e_requisitar(): try: print("Syncing clock via NTP...") ntptime.host = "pool.ntp.org" ntptime.settime() # 通过 NTP 更新 ESP32 的内部 RTC print("Synchronized UTC time:", time.localtime()) # 现在 TLS 握手可以正常校验证书,不会再出现 -0x2700 错误: resposta = urequests.get("https://api.github.com") print("HTTPS response:", resposta.status_code) resposta.close() except Exception as e: print("[TLS/NTP ERROR]:", e)`, dicaPro: "如果你的项目运行在无法访问外部互联网的环境(隔离的局域网),请在路由器上配置本地 NTP 服务器,或通过 I2C 添加 DS3231 之类的实体 RTC 模块。", }, ]; export const regrasDecodificadorLog = [ { padrao: /Brownout detector was triggered/i, titulo: "Brownout Detector(严重欠压)", tipo: "供电 / 硬件", explicacao: "ESP32 的 3V3 引脚电压跌到了安全水平以下(通常是 Wi-Fi/BLE 射频的电流峰值,或电源容量不足所致)。", acaoRecomendada: "在 VCC 处加一个 10µF 到 100µF 的低 ESR 电容,使用稳定的电源,并检查 LDO 稳压器。", }, { padrao: /LoadProhibited|StoreProhibited/i, titulo: "Guru Meditation:Load/StoreProhibited(空指针或无效指针)", tipo: "CPU 异常 / 异常", explicacao: "试图读取或写入内存中的无效地址(几乎总是 NULL 指针解引用或数组越界)。", acaoRecomendada: "用 Exception Decoder 或 'addr2line' 解码 Backtrace,找到确切的行。加入防御式检查 `if (ptr != NULL)`。", }, { padrao: /Cache disabled but cached memory region accessed|InstructionFetchError/i, titulo: "中断中的 Cache Error(ISR 不在 IRAM 中)", tipo: "CPU 异常 / 闪存", explicacao: "中断服务例程(ISR)试图从闪存运行,而此时闪存正忙于写入数据(NVS、LittleFS 或 OTA)。", acaoRecomendada: "给中断函数加上 `IRAM_ATTR` 属性,并确保它不会访问闪存中没有 DRAM_ATTR 的变量。", }, { padrao: /Task watchdog got triggered/i, titulo: "任务看门狗定时器(TWDT)超时", tipo: "FreeRTOS / 多任务", explicacao: "一个高优先级任务独占 CPU 而不让出时间(没有 vTaskDelay 或阻塞),使 IDLE 任务的看门狗无法喂狗。", acaoRecomendada: "在长循环中加入 `vTaskDelay(pdMS_TO_TICKS(10))`,或使用队列/信号量来代替持续的 `while(1)` 循环。", }, { padrao: /Stack canary watchpoint triggered/i, titulo: "栈溢出(任务栈溢出)", tipo: "FreeRTOS / 内存", explicacao: "任务超出了创建时分配的栈(Stack)内存量,破坏了保护金丝雀。", acaoRecomendada: "在 `xTaskCreate` 中增大栈的大小(例如从 2048 增加到 4096 字节),并把大数组移到堆或静态变量中。", }, { padrao: /Timed out waiting for packet header|Failed to connect to ESP32/i, titulo: "UART / 烧录连接失败", tipo: "引导加载程序 / 连接", explicacao: "电脑无法通过 DTR/RTS 让 ESP32 进入烧录模式,或 USB 线没有数据线。", acaoRecomendada: "在 EN 引脚和 GND 之间接一个 10µF 电容,换一根带数据线的 USB 线,并在连接时尝试按住 BOOT 按钮。", }, { padrao: /The certificate is not yet valid|-0x2700/i, titulo: "SSL/TLS 证书失败(时钟停在 1970 年)", tipo: "网络连接 / TLS", explicacao: "ESP32 试图在日期仍为 Unix 纪元零点(1970 年 1 月 1 日)的情况下验证 HTTPS/TLS 证书。", acaoRecomendada: "在发起安全的 HTTPS 请求之前,通过 SNTP 同步时间(Arduino 中用 `configTime`,MicroPython 中用 `ntptime.settime()`)。", }, { padrao: /rst:0x1 \(POWERON_RESET\),boot:0x[0-9a-fA-F]+/i, titulo: "上电复位 / 启动配置引脚", tipo: "硬件 / 启动", explicacao: "微控制器重启并报告了 启动配置引脚寄存器。如果之后卡住,可能是某个启动引脚处于错误的逻辑电平。", acaoRecomendada: "检查 GPIO 0、2、12、15(ESP32)或 8、9(C3)是否连着在启动时会强制出错误电平的电阻或外设。", }, ];