ESP32 与 STM32 嵌入式开发

本文系统讲解 ESP32 与 STM32 的嵌入式开发,回答 MCU 怎么选型、ESP-IDF 5.x 工程怎么组织、外设与低功耗模式怎么写、配网与 MQTT over TLS 怎么落地、STM32 用 HAL 还是 LL 等实战问题。覆盖 ESP32 家族、idf.py 命令、I2C 读 BME280 与 deep sleep 代码、STM32 时钟树与看门狗、供电与天线设计,附常见坑清单。

引言

MCU 是物联网的物理起点。云平台、时序数据库、设备影子这些上层设施做得再好,如果节点选错了芯片、布错了天线、写错了低功耗模式,现场就是一天一换电池、一周掉一次线。嵌入式开发的痛苦在于:很多决策在打样阶段就锁死了,后期只能在软件里绕。

这一层的工程难点集中在四件事。第一是无线共存:Wi-Fi、BLE、Thread 共用 2.4GHz 射频前端,收发切换、天线净空、共存仲裁都会影响吞吐和功耗。第二是功耗:从 Modem-sleep 的 20mA 到 Deep-sleep 的 10uA,量级差三个数量级,选错模式电池寿命就从十年变成三个月。第三是内存:ESP32 只有 520KB SRAM,一个 TLS 握手就可能吃掉几十 KB,栈开多大、堆怎么分都要提前算。第四是实时性:传感器采样、PWM 调光、通信协议栈同时跑,裸机轮询很快就不够用。

本文按「选型 → ESP32 家族 → ESP-IDF 工程 → 外设 → 低功耗 → 配网 → 安全连接 → STM32 → 硬件设计」的顺序展开,给出 I2C 读传感器与 Deep-sleep 两段可直接编译的代码,最后是权衡取舍与常见坑清单。任务调度与 RTOS 部分请配合 RTOS 与 FreeRTOS 任务调度 一起看。

目录

  1. MCU 选型维度
  2. ESP32 家族
  3. ESP-IDF 5.x 工程结构与 idf.py
  4. 外设编程
  5. 代码:I2C 读 BME280
  6. 低功耗模式与唤醒源
  7. 代码:Deep-sleep 配置
  8. Wi-Fi 配网方案
  9. MQTT over TLS 与证书管理
  10. STM32 家族与开发方式
  11. STM32 时钟树、DMA 与中断
  12. 看门狗、Flash 与 EEPROM 模拟
  13. 供电与硬件设计要点
  14. 权衡取舍
  15. 常见坑清单
  16. 小结

1. MCU 选型维度

选型不是比主频,而是把产品需求映射到八个维度上,然后看哪颗芯片先撞墙。

维度关注点典型取值
内核架构、DSP/FPU、是否带 NPUCortex-M0+ / M4F / M7 / Xtensa LX7 / RISC-V
主频峰值算力、能否跑协议栈24MHz 到 480MHz
RAM协议栈 + 缓冲 + 栈的总和8KB 到 1MB
Flash固件 + OTA 双分区 + 文件系统128KB 到 16MB
外设串口数量、ADC 精度、定时器见第 4 节
无线Wi-Fi / BLE / Thread / LoRa决定是否需要外挂模组
功耗运行电流、休眠电流、唤醒时间见第 6 节
封装与成本QFN 尺寸、引脚间距、BOM 价影响 PCB 层数与产线

经验法则:先按「是否需要无线」分流。需要 Wi-Fi 且开发周期紧,直接选 ESP32 系列;只要低功耗远距离,选 STM32L 加 LoRa 模组,参考 LoRa 与 NB-IoT 低功耗广域网 ;只要纯控制、无无线需求,STM32F0/G0 更便宜也更稳。

2. ESP32 家族

乐鑫把 ESP32 做成了一个家族,不同后缀的内核、射频、外设差别很大,选错会白改一版板子。

型号内核主频SRAM射频适用场景
ESP32 经典双核 Xtensa LX6240MHz520KBWi-Fi 4 + BLE 4.2通用网关、需要大量 GPIO
ESP32-S3双核 LX7240MHz512KB + 可选 PSRAMWi-Fi 4 + BLE 5带摄像头、语音、AI 推理
ESP32-C3单核 RISC-V160MHz400KBWi-Fi 4 + BLE 5低成本单无线节点
ESP32-C6单核 RISC-V160MHz512KBWi-Fi 6 + BLE 5 + ThreadMatter、Thread 边界路由器

几个容易踩的差异点。ESP32 经典的双核可以一个核跑协议栈、一个核跑应用,xTaskCreatePinnedToCore 能指定亲和核。C3/C6 是单核 RISC-V,没有双核亲和的概念,但从 Xtensa 迁过来代码基本能编译过。S3 支持外部 PSRAM(常见 8MB Octal PSRAM),跑图像和模型时必须开,因为 512KB 内部 SRAM 装不下一帧 QVGA 的 RGB565 缓冲。C6 是唯一原生支持 Thread 和 Wi-Fi 6 的,做 Matter 设备只能选它。

天线形式也影响选型:模组分 PCB 板载天线、IPEX 外接天线、陶瓷天线三种。金属外壳内必须用 IPEX 外接,板载天线净空区至少要留 15mm x 8mm 且下方所有层掏空。

3. ESP-IDF 5.x 工程结构与 idf.py

ESP-IDF 5.x 用 CMake 构建,工程根目录下有一个 CMakeLists.txt 声明项目名,main/ 目录下有另一个 CMakeLists.txt 声明源文件。组件(component)是复用单元,components/ 下的每个子目录自动被扫描。

idf.py set-target esp32s3      # 设定目标芯片,会重新生成 sdkconfig
idf.py menuconfig              # 图形化配置,写入 sdkconfig
idf.py build                   # 编译,产出 build/*.bin
idf.py -p /dev/ttyUSB0 flash   # 烧录
idf.py -p /dev/ttyUSB0 monitor # 串口监视,Ctrl+] 退出
idf.py flash monitor           # 烧录后直接监视
idf.py size-components         # 查看各组件占用 Flash 大小
idf.py create-component mydrv  # 新建组件骨架

sdkconfig 是配置的真相来源,sdkconfig.defaults 用于把关键配置纳入版本控制。产线固件建议在 sdkconfig.defaults 里固定这几项:CONFIG_ESPTOOLPY_FLASHSIZE_8MB、CONFIG_PARTITION_TABLE_CUSTOM、CONFIG_FREERTOS_HZ=1000、CONFIG_ESP_TASK_WDT_TIMEOUT_S=10。构建系统会生成 sdkconfig.h 供代码里用 CONFIG_ 宏判断。

分区表决定了 OTA 能不能用。做固件升级至少要 factory 或双 ota_0/ota_1 加一个 otadata 分区,细节见 OTA 固件升级 。nvs 分区默认 24KB,存证书和配网信息往往不够,建议调到 64KB 以上。

4. 外设编程

ESP-IDF 5.x 把外设驱动统一成了 driver/gpio.h、driver/i2c_master.h、driver/spi_master.h、driver/uart.h、driver/ledc.h、esp_adc/adc_oneshot.h 这几套 API。5.0 之后 I2C 从老的 i2c.h 迁到新的 i2c_master.h,句柄式管理,老代码需要改。

外设关键点常用参数
GPIO上下拉、中断触发沿、驱动能力GPIO_PULLUP、GPIO_INTR_NEGEDGE
I2C主从模式、时钟频率、内部上拉100kHz 标准 / 400kHz 快速
SPI主机 DMA、CS 手动或自动10MHz 到 80MHz
UART波特率、FIFO 深度、中断或轮询115200 8N1
LEDC16 路通道、14 位分辨率、硬件渐变5kHz 调光
ADC12 位、需校准、衰减决定量程ADC_ATTEN_DB_12
定时器通用 64 位、硬件报警回调gptimer API

ADC 是坑最多的。ESP32 经典 ADC 出厂校准数据烧在 eFuse 里,必须用 adc_cali_create_scheme_curve_fitting 拿校准句柄,否则误差可达 10% 以上。ADC2 在 Wi-Fi 工作时被射频占用,两者不能同时用,所以采样通道尽量安排在 ADC1。输入电压超过量程要先分压,ADC_ATTEN_DB_12 大约对应 0 到 3.1V 满量程。

LEDC 做 PWM 调光时,频率和分辨率是此消彼长的:LEDC 时钟源 80MHz,分辨率 = log2(80MHz / 频率),5kHz 下最多 14 位,40kHz 下只剩 11 位。做 WS2812 这类需要 800kHz 精确时序的,LEDC 反而不好用,要用 RMT 外设。

5. 代码:I2C 读 BME280

下面这段用新版 I2C 主机 API 读 BME280 的温度和气压寄存器,走 400kHz。BME280 的 0xF7 起是 8 字节的 press/ temp/ hum 数据,气压是 20 位、温度是 20 位,需要拼装并做补偿计算。

#include "driver/i2c_master.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#define BME280_ADDR     0x76        // SDO 接地时为 0x76
#define REG_DATA        0xF7        // 数据起始寄存器
#define REG_CTRL_MEAS   0xF4        // 过采样与模式控制

static const char *TAG = "bme280";

static i2c_master_bus_handle_t s_bus;
static i2c_master_dev_handle_t s_dev;

static esp_err_t bme280_init(void)
{
    i2c_master_bus_config_t bus_cfg = {
        .i2c_port = I2C_NUM_0,
        .sda_io_num = GPIO_NUM_8,
        .scl_io_num = GPIO_NUM_9,
        .clk_source = I2C_CLK_SRC_DEFAULT,
        .glitch_ignore_cnt = 7,
        .flags.enable_internal_pullup = true,   // 板上有 4.7k 外部上拉时关掉
    };
    ESP_ERROR_CHECK(i2c_new_master_bus(&bus_cfg, &s_bus));

    i2c_device_config_t dev_cfg = {
        .dev_addr_length = I2C_ADDR_BIT_LEN_7,
        .device_address = BME280_ADDR,
        .scl_speed_hz = 400000,
    };
    ESP_ERROR_CHECK(i2c_master_bus_add_device(s_bus, &dev_cfg, &s_dev));

    // 温度与气压 2x 过采样,进入 normal 模式,t_standby 62.5ms
    uint8_t cfg[2] = { REG_CTRL_MEAS, 0x57 };
    return i2c_master_transmit(s_dev, cfg, sizeof(cfg), 100);
}

static esp_err_t bme280_read_raw(int32_t *raw_temp, int32_t *raw_press)
{
    uint8_t reg = REG_DATA;
    uint8_t buf[8] = {0};

    esp_err_t err = i2c_master_transmit_receive(s_dev, &reg, 1, buf, sizeof(buf), 100);
    if (err != ESP_OK) {
        return err;
    }

    // 气压 20 位:buf[0] 高位, buf[1] 中位, buf[2] 高 4 位有效
    *raw_press = ((int32_t)buf[0] << 12) | ((int32_t)buf[1] << 4) | (buf[2] >> 4);
    // 温度 20 位:buf[3] 高位, buf[4] 中位, buf[5] 高 4 位有效
    *raw_temp = ((int32_t)buf[3] << 12) | ((int32_t)buf[4] << 4) | (buf[5] >> 4);
    return ESP_OK;
}

void app_main(void)
{
    ESP_ERROR_CHECK(bme280_init());
    vTaskDelay(pdMS_TO_TICKS(10));      // 等首次转换完成

    while (1) {
        int32_t rt = 0, rp = 0;
        if (bme280_read_raw(&rt, &rp) == ESP_OK) {
            ESP_LOGI(TAG, "raw temp=%ld raw press=%ld", (long)rt, (long)rp);
        } else {
            ESP_LOGW(TAG, "i2c read failed");   // NACK 通常是地址或上拉问题
        }
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

补偿算法要用 BME280 手册里的 t_fine 公式,并且需要先读 0x88 起的校准系数(26 字节),这里为了篇幅省略。注意 i2c_master_transmit_receive 内部会发一个重复起始位(Repeated Start),比「先写寄存器地址再单独读」两次传输更符合传感器时序要求。

6. 低功耗模式与唤醒源

ESP32 的低功耗模式按「关掉什么」分档,电流量级差别极大。

模式CPUWi-Fi/BT电流(典型)唤醒时间保持内容
Active运行可开80mA 到 240mA-全部
Modem-sleep运行关闭射频20mA 到 30mA微秒级全部,可收 DTIM 信标
Light-sleep暂停关闭0.8mA 到 1.5mA约 1msSRAM 保持,RTC 内存保持
Deep-sleep断电关闭10uA 到 20uA约 300ms仅 RTC 内存(8KB)
Hibernation断电关闭约 5uA约 300ms仅 RTC 内存,RTC 外设关

关键机制有三条。RTC 内存(RTC_DATA_ATTR 修饰的变量)在 Deep-sleep 中保留,可以跨休眠累计计数、存连接凭据,但只有约 8KB。唤醒源包括定时器(esp_sleep_enable_timer_wakeup)、GPIO(esp_sleep_enable_ext0/1_wakeup,EXT1 支持多引脚任意组合)、触摸、UART。EXT0 只支持 RTC_IO 引脚的单个电平触发,EXT1 支持多引脚但只能按「任意一个满足」唤醒。

ULP 协处理器是 ESP32 低功耗的核心卖点:主 CPU 断电时,ULP RISC-V(S2/S3/C6)或 ULP FSM 仍可运行,周期读 ADC、比较阈值,只在超过阈值时才唤醒主核。典型用法是土壤湿度节点每 10 分钟 ULP 采样一次,一天只唤醒主核两次上报,平均电流能压到 30uA 以下。

7. 代码:Deep-sleep 配置

下面这段配置定时唤醒加 GPIO 唤醒,用 RTC 内存累计唤醒次数,唤醒后判断原因。

#include "esp_sleep.h"
#include "esp_log.h"
#include "driver/rtc_io.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#define WAKE_GPIO   GPIO_NUM_4      // 必须落在 RTC_IO 范围内

// RTC 内存:Deep-sleep 期间保持,掉电或复位清零
RTC_DATA_ATTR static uint32_t s_boot_count;
RTC_DATA_ATTR static uint32_t s_sensor_seq;

static const char *TAG = "sleep";

static void do_sensor_work(void)
{
    s_sensor_seq++;
    ESP_LOGI(TAG, "boot #%lu, seq=%lu", (unsigned long)s_boot_count,
             (unsigned long)s_sensor_seq);
}

void app_main(void)
{
    s_boot_count++;

    esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
    switch (cause) {
    case ESP_SLEEP_WAKEUP_TIMER:
        ESP_LOGI(TAG, "wake by timer");
        break;
    case ESP_SLEEP_WAKEUP_EXT1:
        ESP_LOGI(TAG, "wake by gpio mask=0x%llx",
                 esp_sleep_get_ext1_wakeup_status());
        break;
    default:
        ESP_LOGI(TAG, "cold boot");     // 首次上电或复位
        break;
    }

    do_sensor_work();

    // 配置 GPIO 唤醒:低电平触发,用 EXT1 以便多引脚扩展
    rtc_gpio_pullup_en(WAKE_GPIO);
    rtc_gpio_pulldown_dis(WAKE_GPIO);
    esp_sleep_enable_ext1_wakeup(1ULL << WAKE_GPIO, ESP_EXT1_WAKEUP_ALL_LOW);

    // 定时唤醒 60 秒(微秒为单位,uint64)
    esp_sleep_enable_timer_wakeup(60ULL * 1000000ULL);

    ESP_LOGI(TAG, "enter deep sleep");
    esp_deep_sleep_start();     // 此函数不返回
}

三个细节。第一,进入 Deep-sleep 前不需要手动关闭 Wi-Fi,esp_deep_sleep_start 会处理,但如果是 Light-sleep 则必须 esp_wifi_stop(),否则射频不关电流下不来。第二,rtc_gpio_pullup_en 必须在 esp_sleep_enable_ext1_wakeup 之前配置,且唤醒后要 rtc_gpio_hold_dis 释放保持,否则引脚状态被锁死。第三,esp_deep_sleep_start 之后代码不再执行,需要先 fflush(stdout) 或等待日志刷完,否则最后一条日志会丢。

8. Wi-Fi 配网方案

出厂设备不知道用户家里的 Wi-Fi 密码,配网是量产必答题。三种主流方案:

方案原理优点缺点
SmartConfig手机 App 把 SSID/密码编码进 UDP 包长度,设备在混杂模式嗅探无需切热点,体验好依赖 App,部分路由器不兼容
SoftAP设备开热点,手机连上后 HTTP 提交凭据兼容性最好,可网页配需要手动切热点,iOS 会提示无网络
BLE provisioning通过 BLE GATT 传输凭据快,iOS 友好,可加密需要 BLE 协议栈,占 Flash

ESP-IDF 提供了统一的 wifi_provisioning 组件,三种方案共用一套 API 和 Protocomm 协议,切换只需改 scheme。配网成功后把凭据存进 NVS(nvs_set_str 写 wifi_config 命名空间),下次上电直接读。建议实现「配网失败回退」:连续 3 次连接失败后重新进入配网模式,否则设备变砖只能返修。

配网凭据涉及明文 Wi-Fi 密码,NVS 要开加密分区(CONFIG_NVS_ENCRYPTION),密钥存 eFuse,配合 Flash 加密一起用才有意义。

9. MQTT over TLS 与证书管理

设备连云必须加密,ESP-IDF 用 mbedTLS 实现 TLS,esp-tls 是上层封装。MQTT 客户端推荐 esp-mqtt(mqtt_client.h),配置项里 broker.address.uri 用 mqtts:// 前缀即走 TLS。

证书管理的三种做法:

  • 内嵌 CA:把根 CA 的 PEM 用 target_add_binary_data 编进固件,最省事但换 CA 要重新烧录。
  • 存 NVS:把证书字符串写进 NVS 分区,OTA 时可更新,推荐量产用。
  • 双向认证:设备侧也要带客户端证书和私钥,私钥存 NVS 并配合 NVS 加密,服务端才能校验设备身份。
#include "esp_mqtt_client.h"
#include "nvs.h"

static esp_mqtt_client_handle_t start_mqtt(const char *ca_pem,
                                           const char *client_crt,
                                           const char *client_key)
{
    esp_mqtt_client_config_t cfg = {
        .broker.address.uri = "mqtts://iot.example.com:8883",
        .broker.verification.certificate = ca_pem,
        .credentials.client_id = "node-0001",
        .credentials.authentication.certificate = client_crt,
        .credentials.authentication.key = client_key,
        .session.keepalive = 60,            // 秒,服务端 1.5 倍判定离线
        .network.reconnect_timeout_ms = 5000,
    };
    return esp_mqtt_client_init(&cfg);
}

TLS 握手的峰值内存约 40KB(mbedTLS 默认配置),握手时如果同时做 OTA 下载会 OOM,所以要在 menuconfig 里调 CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN 和动态缓冲。另外 ESP32 有硬件加速的 AES/SHA,CONFIG_MBEDTLS_HARDWARE_AES 打开后握手时间能从 1.2s 降到 400ms 左右。

10. STM32 家族与开发方式

STM32 按内核和应用定位分很多系列,选型时先看内核再看外设。

系列内核主频Flash特点
F0Cortex-M0+48MHz16 到 256KB最便宜,无 FPU
F1Cortex-M372MHz64KB 到 1MB经典款,资料最多
F4Cortex-M4F168MHz512KB 到 2MB带 FPU/DSP,主流高性能
G0Cortex-M0+64MHz32 到 512KB新一代低价,宽压 1.7 到 3.6V
L4Cortex-M4F80MHz128KB 到 1MB低功耗,带 LPUART
H7Cortex-M7480MHz1 到 2MB高性能,带以太网、双核版可选

开发方式有三条路。CubeMX 图形化生成初始化代码,配好时钟树和引脚后导出 IAR/Keil/Makefile 工程,适合快速起步。HAL 库抽象度高、可移植,但代码体积大、执行慢,一个 HAL_GPIO_WritePin 要经过多层函数调用。LL 库(Low Layer)直接操作寄存器,效率和体积都更好,适合对时序敏感的外设。寄存器直写最快但可读性差、换型号要重写,一般只在中断服务函数的最内层用。

实际项目常见组合是:CubeMX 生成时钟与外设初始化,业务代码用 HAL 快速开发,关键路径(如 SPI 刷屏、DMA 搬运)换成 LL 或寄存器操作。

11. STM32 时钟树、DMA 与中断

时钟树是 STM32 最容易配错的地方。以 F407 为例:外部 8MHz 晶振经 PLL 倍频到 168MHz 作为 SYSCLK,AHB 分频 1 得到 HCLK 168MHz,APB1 最高 42MHz、APB2 最高 84MHz。定时器时钟有个隐藏规则:如果 APB 预分频不为 1,定时器时钟是 APB 时钟的 2 倍。所以挂在 APB1 上的 TIM2 实际时钟是 84MHz 而不是 42MHz,算波特率和 PWM 周期时算错这个会差一倍。

DMA 用来搬数据不占 CPU:串口收发、ADC 多通道扫描、SPI 刷屏都应该上 DMA。配置要点是「外设地址固定、内存地址递增、传输方向、数据宽度、循环或单次」。串口空闲中断加 DMA 接收是收不定长帧的标准做法:DMA 循环接收,HAL_UARTEx_ReceiveToIdle_DMA 在总线空闲时触发回调,直接拿到本帧长度。

NVIC 中断优先级要按「实时性要求」排。Cortex-M 的优先级数值越小优先级越高,抢占优先级决定能否打断别人,子优先级只在同时挂起时决定顺序。经验排法:电机控制/编码器 > 通信接收 > 定时器节拍 > 按键/显示。共享资源的中断和任务之间要用临界区或互斥量保护,__disable_irq 会关掉所有中断,尽量用 BASEPRI 屏蔽部分优先级。

12. 看门狗、Flash 与 EEPROM 模拟

看门狗分两条。IWDG(独立看门狗)用独立的 LSI 低速时钟,主时钟挂了它也能工作,是最后一道防线,喂狗周期通常设 1 到 4 秒。WWDG(窗口看门狗)挂在 APB 上,有「窗口」概念,喂太早也会复位,能检测程序跑飞后的异常快速喂狗,但时钟依赖主时钟。产品固件建议两个都开:IWDG 兜底、WWDG 检测任务时序。

STM32 内部 Flash 的擦除粒度是扇区(F4 上 16KB 到 128KB 不等),写粒度是 双字或字。用它存参数要注意:擦除前必须先解锁(HAL_FLASH_Unlock),擦除期间 CPU 取指会停顿,所以不能把中断服务函数放在正在擦除的扇区里。频繁写入要用「日志式」或双扇区轮换,否则 1 万次擦写寿命很快就到。

EEPROM 模拟有两种。一是用 STM32 的 EEPROM_Emul 中间件,双页轮换来延长寿命,代价是占两个 Flash 页。二是外挂 I2C EEPROM(如 AT24C256),32KB 容量、100 万次寿命、成本几毛钱,需要频繁存配置时更划算。

13. 供电与硬件设计要点

电源设计决定了整机稳定性,尤其是带射频的板子。

  • LDO 还是 DCDC:Wi-Fi 发射瞬间电流能到 500mA 峰值,LDO 压差大时发热严重,电池供电优先用 DCDC(如 TPS63020),噪声敏感的 ADC 参考单独用 LDO 供。
  • 去耦电容:每个电源引脚配 100nF,芯片整体再配 10uF 和 100uF 各一颗。射频供电(VDD3P3_RF)要额外加 10nF 到 100pF 的高频旁路,靠近引脚放。
  • 天线净空:板载 PCB 天线下方所有层必须挖空,周围 15mm 内不放金属和电池,外壳不用金属材质。
  • ESD 与浪涌:对外接口(RS485、USB、天线)要加 TVS,工业现场还要考虑隔离。
  • 复位与调试:预留 SWD 四线(VCC/GND/SWDIO/SWCLK)测试点,NRST 加 100nF 滤波,否则现场干扰容易误复位。

功耗实测时要注意万用表测不准:平均电流用高精度表配库仑计,峰值电流用示波器加电流探头或分流电阻,因为 Wi-Fi 发射的峰值持续时间只有几百微秒,普通万用表根本采不到。

14. 权衡取舍

取舍点选 A选 B判据
无线方案ESP32 一体化STM32 + 外挂模组一体化省成本省 PCB,外挂模组更灵活可换
开发方式HAL 库LL/寄存器开发速度 vs 代码体积与时序精度
低功耗频繁 Deep-sleep常连 Modem-sleep电池供电选前者,市电供电选后者
安全双向 TLS 证书单向 TLS + 令牌合规要求选前者,成本敏感选后者
存储内部 Flash 模拟外挂 EEPROM写入频次低于 1 万次用内部,高频用外挂
调试串口日志JTAG/SWD 单步早期用 SWD,量产用日志加看门狗

一条实用原则:把「不可逆」的决策(芯片选型、天线形式、分区表)在打样前评审三遍,把「可逆」的决策(日志级别、任务优先级、MQTT 主题)留到软件里快速迭代。

15. 常见坑清单

  1. 现象:ESP32 偶发重启,日志显示 Brownout detector。原因:Wi-Fi 发射瞬间电流跌落超过阈值。规避:加大电源去耦,关闭 brownout 检测前先排查供电。
  2. 现象:ADC 读数跳变十几个 LSB。原因:没用 eFuse 校准,或采样通道落在被 Wi-Fi 占用的 ADC2。规避:用 adc_cali 校准并只走 ADC1。
  3. 现象:Deep-sleep 后 GPIO 唤醒不响应。原因:唤醒引脚不在 RTC_IO 范围内,或唤醒后没释放 rtc_gpio_hold。规避:查芯片手册的 RTC_IO 表,唤醒后调 rtc_gpio_hold_dis。
  4. 现象:TLS 握手失败报 -0x2700。原因:设备时间未同步,证书有效期校验不过。规避:先 SNTP 对时或用 esp_tls_set_global_ca_store 跳过时间校验(不推荐)。
  5. 现象:I2C 读传感器返回 NACK。原因:地址写错(0x76 与 0x77 取决于 SDO 电平),或内部上拉不足。规避:扫描 I2C 总线确认地址,长走线加外部 4.7k 上拉。
  6. 现象:STM32 定时器周期差一倍。原因:APB 预分频不为 1 时定时器时钟自动乘 2。规避:查参考手册时钟树的定时器分支,别用 APB 频率直接算。
  7. 现象:Flash 写入后程序跑飞。原因:擦除时中断向量表或 ISR 代码落在被擦扇区。规避:把 ISR 放到 RAM(__attribute__((section(".RamFunc"))))或避开擦除扇区。
  8. 现象:OTA 升级后设备起不来。原因:分区表没有 otadata,或新固件版本号回滚被拒。规避:分区表留足 OTA 分区,配 CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE。
  9. 现象:串口接收丢帧。原因:中断里逐字节处理,波特率高时来不及。规避:DMA 加空闲中断,一次拿整帧。
  10. 现象:板载天线信号差 20dB。原因:天线净空区被铺铜或电池遮挡。规避:天线下方全层挖空,电池远离天线,外壳用非金属。

16. 小结

MCU 层的工作可以概括成三句话:选型阶段把无线、功耗、内存三个约束想清楚;开发阶段用框架(ESP-IDF / CubeMX)把外设和构建标准化,把精力留给业务;量产阶段把低功耗、看门狗、OTA、安全连接这四件事做实,否则返修成本远高于开发成本。

ESP32 的优势是无线与协议栈开箱即用,代价是功耗和射频约束多;STM32 的优势是实时性、外设丰富、生态成熟,代价是无线要自己搭。两者经常组合使用:STM32 做实时控制与采集,ESP32 做网关和上云。

下一步建议继续读 RTOS 与 FreeRTOS 任务调度了解多任务怎么组织,读 OTA 固件升级把量产固件升级链路打通。外设驱动的分层写法可以对照 设备驱动基础 ;如果 MCU 代码是 C++ 写的,嵌入式场景里关于静态分配和中断安全的讨论同样适用。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「物联网」更多文章

  1. 工业物联网协议与网关
  2. 边缘 AI 推理
  3. 设备配网与批量运维