引言
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 任务调度 一起看。
目录
- MCU 选型维度
- ESP32 家族
- ESP-IDF 5.x 工程结构与 idf.py
- 外设编程
- 代码:I2C 读 BME280
- 低功耗模式与唤醒源
- 代码:Deep-sleep 配置
- Wi-Fi 配网方案
- MQTT over TLS 与证书管理
- STM32 家族与开发方式
- STM32 时钟树、DMA 与中断
- 看门狗、Flash 与 EEPROM 模拟
- 供电与硬件设计要点
- 权衡取舍
- 常见坑清单
- 小结
1. MCU 选型维度
选型不是比主频,而是把产品需求映射到八个维度上,然后看哪颗芯片先撞墙。
| 维度 | 关注点 | 典型取值 |
|---|---|---|
| 内核 | 架构、DSP/FPU、是否带 NPU | Cortex-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 LX6 | 240MHz | 520KB | Wi-Fi 4 + BLE 4.2 | 通用网关、需要大量 GPIO |
| ESP32-S3 | 双核 LX7 | 240MHz | 512KB + 可选 PSRAM | Wi-Fi 4 + BLE 5 | 带摄像头、语音、AI 推理 |
| ESP32-C3 | 单核 RISC-V | 160MHz | 400KB | Wi-Fi 4 + BLE 5 | 低成本单无线节点 |
| ESP32-C6 | 单核 RISC-V | 160MHz | 512KB | Wi-Fi 6 + BLE 5 + Thread | Matter、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 |
| LEDC | 16 路通道、14 位分辨率、硬件渐变 | 5kHz 调光 |
| ADC | 12 位、需校准、衰减决定量程 | 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, ®, 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 的低功耗模式按「关掉什么」分档,电流量级差别极大。
| 模式 | CPU | Wi-Fi/BT | 电流(典型) | 唤醒时间 | 保持内容 |
|---|---|---|---|---|---|
| Active | 运行 | 可开 | 80mA 到 240mA | - | 全部 |
| Modem-sleep | 运行 | 关闭射频 | 20mA 到 30mA | 微秒级 | 全部,可收 DTIM 信标 |
| Light-sleep | 暂停 | 关闭 | 0.8mA 到 1.5mA | 约 1ms | SRAM 保持,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 | 特点 |
|---|---|---|---|---|
| F0 | Cortex-M0+ | 48MHz | 16 到 256KB | 最便宜,无 FPU |
| F1 | Cortex-M3 | 72MHz | 64KB 到 1MB | 经典款,资料最多 |
| F4 | Cortex-M4F | 168MHz | 512KB 到 2MB | 带 FPU/DSP,主流高性能 |
| G0 | Cortex-M0+ | 64MHz | 32 到 512KB | 新一代低价,宽压 1.7 到 3.6V |
| L4 | Cortex-M4F | 80MHz | 128KB 到 1MB | 低功耗,带 LPUART |
| H7 | Cortex-M7 | 480MHz | 1 到 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. 常见坑清单
- 现象:ESP32 偶发重启,日志显示
Brownout detector。原因:Wi-Fi 发射瞬间电流跌落超过阈值。规避:加大电源去耦,关闭 brownout 检测前先排查供电。 - 现象:ADC 读数跳变十几个 LSB。原因:没用 eFuse 校准,或采样通道落在被 Wi-Fi 占用的 ADC2。规避:用
adc_cali校准并只走 ADC1。 - 现象:Deep-sleep 后 GPIO 唤醒不响应。原因:唤醒引脚不在 RTC_IO 范围内,或唤醒后没释放
rtc_gpio_hold。规避:查芯片手册的 RTC_IO 表,唤醒后调rtc_gpio_hold_dis。 - 现象:TLS 握手失败报
-0x2700。原因:设备时间未同步,证书有效期校验不过。规避:先 SNTP 对时或用esp_tls_set_global_ca_store跳过时间校验(不推荐)。 - 现象:I2C 读传感器返回 NACK。原因:地址写错(0x76 与 0x77 取决于 SDO 电平),或内部上拉不足。规避:扫描 I2C 总线确认地址,长走线加外部 4.7k 上拉。
- 现象:STM32 定时器周期差一倍。原因:APB 预分频不为 1 时定时器时钟自动乘 2。规避:查参考手册时钟树的定时器分支,别用 APB 频率直接算。
- 现象:Flash 写入后程序跑飞。原因:擦除时中断向量表或 ISR 代码落在被擦扇区。规避:把 ISR 放到 RAM(
__attribute__((section(".RamFunc"))))或避开擦除扇区。 - 现象:OTA 升级后设备起不来。原因:分区表没有
otadata,或新固件版本号回滚被拒。规避:分区表留足 OTA 分区,配CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE。 - 现象:串口接收丢帧。原因:中断里逐字节处理,波特率高时来不及。规避:DMA 加空闲中断,一次拿整帧。
- 现象:板载天线信号差 20dB。原因:天线净空区被铺铜或电池遮挡。规避:天线下方全层挖空,电池远离天线,外壳用非金属。
16. 小结
MCU 层的工作可以概括成三句话:选型阶段把无线、功耗、内存三个约束想清楚;开发阶段用框架(ESP-IDF / CubeMX)把外设和构建标准化,把精力留给业务;量产阶段把低功耗、看门狗、OTA、安全连接这四件事做实,否则返修成本远高于开发成本。
ESP32 的优势是无线与协议栈开箱即用,代价是功耗和射频约束多;STM32 的优势是实时性、外设丰富、生态成熟,代价是无线要自己搭。两者经常组合使用:STM32 做实时控制与采集,ESP32 做网关和上云。
下一步建议继续读 RTOS 与 FreeRTOS 任务调度了解多任务怎么组织,读 OTA 固件升级把量产固件升级链路打通。外设驱动的分层写法可以对照 设备驱动基础 ;如果 MCU 代码是 C++ 写的,嵌入式场景里关于静态分配和中断安全的讨论同样适用。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。