Files
ESPDocs/lib/data/zh-CN/diagnostico.js
T

745 lines
36 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 <WiFi.h>
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 <esp_heap_caps.h>
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 <WiFi.h>
#include <time.h>
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)是否连着在启动时会强制出错误电平的电阻或外设。",
},
];