ESP32和STM32F407如何通过串口实现双机通信

AI助手已提取文章相关产品:

ESP32 与 STM32F407 的串口双机通信实战指南

你有没有遇到过这样的场景:一个项目里,既要处理复杂的实时控制任务——比如电机驱动、高速 ADC 采集,又要实现 Wi-Fi 联网上传数据到云平台?这时候,单靠一块 MCU 往往捉襟见肘。CPU 资源被中断抢占得七零八落,Wi-Fi 协议栈一跑起来,实时性直接崩盘。

那怎么办?

聪明的工程师早就给出了答案: 分工协作

把“干活”的活儿交给擅长控制的 STM32F407,把“说话”的事儿甩给精通联网的 ESP32 —— 它们之间怎么“对话”?最简单、最可靠的方式之一就是: UART 串口通信

别看它老,但真香。没有复杂的协议栈,不需要额外芯片,两根线加一个共地,就能打通两个世界的任督二脉。今天我们就来深挖这个看似基础、实则关键的技术点:如何让 ESP32 和 STM32F407 通过串口稳定“对线”。


为什么是 UART?而不是 SPI 或 I²C?

你说,SPI 不是更快吗?I²C 不是能连多个设备吗?确实。但在跨平台互联这件事上,UART 反而成了最优解。

先说 SPI :虽然速率高,但它本质上是个主从结构。谁当主?谁当从?一旦角色固定,灵活性就差了。而且 ESP32 和 STM32 的 SPI 时序细节略有不同,稍不注意就会出现采样错位、极性相位不匹配的问题。更麻烦的是,SPI 引脚通常不能随意重映射,布线受限。

再看 I²C :支持多设备没错,但它对电平匹配和上拉电阻要求严格。ESP32 和 STM32 都是 3.3V 器件,理论上兼容,但实际使用中经常因为总线负载过大或干扰导致通信失败。再加上 I²C 是开漏输出,速率越高越容易出问题,远距离传输基本劝退。

UART 呢?

  • 点对点通信,天生适合双机交互;
  • 全双工,各自 TX/RX 互不干扰;
  • 协议极其简单,起始位 + 数据位 + 停止位,硬件自动搞定;
  • 几乎所有 MCU 都带至少两个 UART 接口;
  • 波特率标准化(9600、115200…),配置统一;
  • 调试方便,拿个 USB-TTL 模块一接,串口助手就能看到原始数据流。

所以,在“STM32 干活 + ESP32 上网”这种经典架构里,UART 成为了事实上的首选通信方式。

🛠️ 小贴士:如果你需要三台以上设备组网,可以考虑用 RS485 + Modbus 协议;但如果只是两台机器“私聊”,UART 绝对是最轻量、最高效的方案。


物理连接:别小看这两根线

我们先来看最基本的硬件连接:

STM32F407         ↔        ESP32
    TX   ------------------->   RX
    RX   <-------------------   TX
    GND  ====================   GND

看起来很简单吧?但很多初学者在这里翻车了。

常见坑点一:TX 接错了 RX!

这是新手最容易犯的错误。记住一句话: 你的嘴(TX)要对着别人的耳朵(RX)

  • STM32 的发送引脚(TX) → 接 ESP32 的接收引脚(RX)
  • STM32 的接收引脚(RX) ← 接 ESP32 的发送引脚(TX)

交叉连接!不是同名对接!

常见坑点二:忘了共地

你以为信号线接好了就行?NO!如果没有共享的地线(GND),电压参考系不一样,电平识别就会出错。哪怕两个板子都插在同一个 USB 插排上,也不能保证地是连通的。

✅ 正确做法:用一根足够粗的导线将两块开发板的 GND 引脚牢牢焊在一起,越短越好,避免形成地环路引入噪声。

常见坑点三:电平兼容性真的 OK 吗?

ESP32 和 STM32F407 都是 3.3V 逻辑器件,按理说可以直接互联。但要注意一点:某些 ESP32 模块的 IO 可能允许 5V 输入(如 NodeMCU 板载电平转换),而 STM32F407 的 IO 是 5V tolerant 吗?

查手册!
STM32F407 的多数 IO 支持 5V tolerant(具体看封装和引脚定义),但一旦启用内部上拉/下拉,可能会影响耐压能力。为保险起见,建议全程工作在 3.3V 系统,不要混入 5V 电源。

如果必须隔离(比如工业现场有强干扰),可以用光耦或者专用隔离模块(如 ADM2682E),但这会增加成本和复杂度。一般情况下,做好共地+短线连接即可。


软件实现:两边怎么“说话”

接下来进入代码环节。我们要确保两边使用相同的“语言规则”——也就是通信参数一致。

关键参数必须同步

参数 必须一致? 推荐值
波特率 115200 (常用)
数据位 8
停止位 1
校验位 ❌(可选) 无(N)
流控

其中最重要的是 波特率 。假设 STM32 设置为 115200,而 ESP32 设成 9600,那收到的数据全是乱码,就像两个人用不同语速讲话,谁都听不懂谁。

为什么推荐 115200?
  • 够快:每秒传 11.5KB 数据,对于传感器上报、命令下发完全够用;
  • 兼容性好:几乎所有设备都支持;
  • 实测稳定:在普通杜邦线长度下(<30cm)误码率极低。

当然,如果你的线路很长或环境嘈杂,可以降到 9600 或 19200 提高容错率。


ESP32 端:Arduino 环境下的实现

ESP32 在 Arduino IDE 下编程非常友好,尤其对 UART 支持完善。

#include <HardwareSerial.h>

// 使用 UART2,避免占用默认串口(UART0)
HardwareSerial Serial2(2);

void setup() {
  // 初始化调试串口(用于打印日志)
  Serial.begin(115200);
  delay(1000);
  Serial.println("\n[ESP32] 启动中...");

  // 配置 UART2: RX=16, TX=17
  Serial2.begin(115200, SERIAL_8N1, 16, 17);
  Serial.println("[ESP32] UART2 已启动,等待 STM32 连接...");
}

void loop() {
  // 检查是否有来自 STM32 的数据
  if (Serial2.available()) {
    String msg = Serial2.readStringUntil('\n');  // 以换行为结束符
    Serial.println("📩 收到 STM32 数据: " + msg);

    // 解析并处理命令(示例)
    if (msg.startsWith("CMD:")) {
      handleCommand(msg);
    } else {
      // 默认回复 ACK
      Serial2.println("ACK:" + msg);
    }
  }

  delay(10);  // 小延时防 CPU 占满
}

void handleCommand(const String& cmd) {
  if (cmd == "CMD:START_MOTOR") {
    Serial.println("✅ 执行指令: 启动电机");
    Serial2.println("RESP:OK");
  } else if (cmd == "CMD:STOP_MOTOR") {
    Serial.println("🛑 执行指令: 停止电机");
    Serial2.println("RESP:OK");
  } else {
    Serial2.println("RESP:UNKNOWN_CMD");
  }
}

🔍 重点说明

  • 我们用了 HardwareSerial 类创建了一个独立的串口实例 Serial2 ,绑定到 GPIO16(RX)和 GPIO17(TX)。这样就不会影响 UART0 的下载和调试功能。
  • 使用 readStringUntil('\n') 是为了防止“粘包”。只要 STM32 每次发送完加个 \n ,ESP32 就能完整读取一帧。
  • 加了简单的命令解析机制,模拟真实应用场景。

💡 经验提示 :不要在 loop() 中频繁调用 delay() ,尤其是大数值。否则会影响实时响应。这里只用了 10ms,是为了防止 available() 查询太密集占用 CPU。


STM32F407 端:基于 HAL 库的配置与通信

相比 ESP32 的高级封装,STM32 更接近底层,但也更可控。

我们使用 STM32CubeMX 配合 HAL 库进行配置,这是目前主流开发方式。

Step 1:CubeMX 配置 USART2

  • 选择 USART2,模式设为 Asynchronous(异步串行)
  • 波特率:115200
  • 字长:8
  • 停止位:1
  • 校验:None
  • 硬件流控:Disable
  • 引脚分配:PA2 → TX,PA3 → RX(默认复用)

生成代码后,会在 main.c 中自动生成 MX_USART2_UART_Init() 函数。

Step 2:编写发送与接收逻辑

#include "main.h"
#include "usart.h"
#include "string.h"

UART_HandleTypeDef huart2;

// 接收缓冲区(全局变量)
uint8_t rx_byte = 0;           // 单字节接收缓存
char rx_buffer[64];            // 存储完整消息
int buffer_index = 0;

// 发送封装函数
void send_to_esp32(const char* str) {
  uint8_t len = strlen(str);
  uint8_t packet[128];
  sprintf((char*)packet, "%s\n", str);  // 添加换行符作为帧边界
  HAL_UART_Transmit(&huart2, packet, strlen((char*)packet), 100);
}

// 接收回调函数(中断触发)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
  if (huart == &huart2) {
    // 收到一个字节
    if (rx_byte == '\n' || rx_byte == '\r') {
      // 结束符到达,处理整条消息
      rx_buffer[buffer_index] = '\0';  // 补字符串结尾
      buffer_index = 0;                 // 重置索引

      // 回显测试
      send_to_esp32("ECHO:");
      send_to_esp32(rx_buffer);

      // 可在此处添加命令解析逻辑
    } else {
      // 普通字符存入缓冲区
      if (buffer_index < 63) {
        rx_buffer[buffer_index++] = rx_byte;
      }
    }

    // 重新开启下一次中断接收
    HAL_UART_Receive_IT(&huart2, &rx_byte, 1);
  }
}

int main(void) {
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART2_UART_Init();

  // 开启串口中断接收(关键!)
  HAL_UART_Receive_IT(&huart2, &rx_byte, 1);

  // 发送启动问候
  send_to_esp32("Hello ESP32! This is STM32F407");

  while (1) {
    // 主循环定期上报状态
    send_to_esp32("STATUS:RUNNING,TEMP=25.3,HUM=60");

    HAL_Delay(2000);  // 每 2 秒发一次
  }
}

🎯 核心要点解析

  • HAL_UART_Receive_IT() 启用中断接收模式,每收到一个字节就触发一次中断,极大降低 CPU 占用。
  • 在回调函数 HAL_UART_RxCpltCallback 中判断是否收到 \n ,以此界定一帧数据的结束。
  • 使用环形缓冲思想暂存字符,直到完整帧接收完毕再处理。
  • 每次处理完都要重新调用 HAL_UART_Receive_IT() ,否则后续无法继续接收!

⚠️ 注意陷阱:有些开发者习惯用轮询方式写 while(HAL_UART_Receive() != HAL_OK); ,这会导致程序卡死。一定要用中断或 DMA 方式才能保证系统响应性。


通信协议设计:让数据更有意义

光通上了还不够,还得“说得清楚”。

想象一下,如果 STM32 直接扔一堆原始字节过去,ESP32 怎么知道哪些是温度、哪些是指令?所以必须制定一套 通信协议

方案一:简单文本协议(推荐初学者)

优点:人类可读,调试方便。

格式示例:

DATA:{"temp":25.3,"hum":60}\n
CMD:START_MOTOR\n
RESP:OK\n

解析方式:字符串匹配 + JSON 解析(可用 ArduinoJson 库)。

适用场景:低频数据上报、远程控制类应用。

方案二:二进制协议(高效紧凑)

优点:体积小、解析快、抗干扰强。

定义结构体:

typedef struct {
  uint8_t header;      // 起始符,如 0xAA
  uint8_t cmd_id;      // 命令类型
  float temp;
  float hum;
  uint16_t crc;        // 校验和
} __attribute__((packed)) sensor_data_t;

然后整体发送:

sensor_data_t data = {0xAA, 0x01, 25.3f, 60.0f, 0};
data.crc = calculate_crc((uint8_t*)&data, sizeof(data)-2);
HAL_UART_Transmit(&huart2, (uint8_t*)&data, sizeof(data), 100);

ESP32 端同样用 struct 对应解析即可。

⚠️ 注意内存对齐问题!加上 __attribute__((packed)) 防止编译器插入填充字节。

加点健壮性:加入 CRC 校验

即使波特率匹配、线路良好,电磁干扰仍可能导致个别比特翻转。加个 CRC 校验能大幅提升可靠性。

推荐使用 CRC-16/CCITT-FALSE:

uint16_t crc16(uint8_t *data, int len) {
  uint16_t crc = 0xFFFF;
  for (int i = 0; i < len; ++i) {
    crc ^= data[i] << 8;
    for (int j = 0; j < 8; ++j) {
      if (crc & 0x8000)
        crc = (crc << 1) ^ 0x1021;
      else
        crc <<= 1;
    }
  }
  return crc;
}

接收方计算 CRC 并比对,不一致则丢弃该帧,并可要求重传。


实战中常见的那些“灵异事件”

你以为写完代码烧进去就万事大吉?Too young.

下面这些“玄学问题”,每一个我都亲手 debug 过……

🚨 问题 1:串口助手能看到数据,但另一方收不到?

检查是否开启了中断接收!

常见于 STM32 端忘记调用 HAL_UART_Receive_IT() ,导致只收第一个字节,后面全丢。

解决方法:确保在初始化后立即开启中断接收,并在每次回调后重新注册。

🚨 问题 2:偶尔收到乱码或字符缺失?

可能是波特率误差太大。

STM32 的 USART 波特率由 PCLK 分频而来。F407 的 APB1 外设时钟是 42MHz(PCLK1),而 USART2 属于 APB1 总线。

计算公式:

Baudrate = PCLK / (16 * USARTDIV)

查表可知,42MHz 下 115200 波特率的实际分频值为 22.96875,存在一定误差(约 0.8%),仍在可接受范围内(<2%)。

但如果主频配置错误(比如误设为 72MHz),误差可能飙升至 10% 以上,必然出错。

✅ 建议:使用 STM32CubeMX 自动生成时钟树,避免手动算错。

🚨 问题 3:数据“粘在一起”分不清?

这就是典型的 粘包问题

例如 STM32 连续发送两条消息:

STATUS:OK
STATUS:OK

ESP32 可能在一次 available() 中读到 "STATUS:OKSTATUS:OK" ,无法分割。

解决方案:
- 发送端每条消息以 \n 结尾;
- 接收端使用 readStringUntil('\n')
- 或者改用固定长度包头(如前两位表示长度)。

🚨 问题 4:长时间运行后通信中断?

很可能是 缓冲区溢出 中断优先级冲突

比如 STM32 正在执行高优先级中断(如 PWM 更新),导致 UART 接收中断迟迟得不到响应,FIFO 溢出,触发 ORE (Overrun Error)。

查看状态寄存器:

if (__HAL_UART_GET_FLAG(&huart2, UART_FLAG_ORE)) {
  __HAL_UART_CLEAR_OREFLAG(&huart2);
  // 记录错误次数
}

优化策略:
- 提升 UART 中断优先级;
- 使用 DMA 替代中断接收(大数据量推荐);
- 增加超时检测机制,发现长期无通信则重启串口。


进阶玩法:DMA + 环形缓冲提升性能

当你需要传输大量数据(比如波形采样、图像片段),中断方式就不够用了。

这时该请出 DMA(Direct Memory Access) 登场。

STM32 上如何用 DMA 接收 UART 数据?

#define RX_BUFFER_SIZE 256
uint8_t dma_rx_buffer[RX_BUFFER_SIZE];

// 初始化时启用 DMA 接收
HAL_UART_Receive_DMA(&huart2, dma_rx_buffer, RX_BUFFER_SIZE);

// 当半数缓冲区填满时触发回调
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart) {
  if (huart == &huart2) {
    // 处理前半部分数据 [0 ~ 127]
    process_uart_data(&dma_rx_buffer[0], 128);
  }
}

// 当整个缓冲区填满时触发
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
  if (huart == &huart2) {
    // 处理后半部分数据 [128 ~ 255]
    process_uart_data(&dma_rx_buffer[128], 128);
  }
}

这种方式实现了“零 CPU 干预”的数据接收,特别适合高速连续数据流。

配合双缓冲机制,几乎不会丢失任何字节。

ESP32 端也有类似的 Ring Buffer 机制(在 ESP-IDF 中),可以实现对等高性能通信。


如何调试?别只会 printf

当通信出问题时,你怎么排查?

方法 1:串口助手监听(最直观)

将 USB-TTL 模块接到 TX 线上(注意:只能接一根!不能同时接两根造成短路),打开串口助手观察原始数据流。

看看有没有预期的消息发出,格式是否正确,是否有乱码。

⚠️ 切记:监听时不要让两个 MCU 同时向同一根线发送数据,否则会冲突!

方法 2:逻辑分析仪抓波形(专业级)

推荐 Saleae 或国产低成本型号。

直接夹上 TX 和 GND,设置对应波特率,就能看到真实的电平变化。

你可以验证:
- 起始位是否正常?
- 数据位是否符合 LSB 顺序?
- 停止位宽度是否足够?
- 是否存在毛刺或抖动?

这是我解决“偶发丢帧”问题的终极武器。

方法 3:LED 打拍子(土法炼钢)

在关键节点加 LED 指示灯:

// 每次收到数据闪烁一次
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
HAL_Delay(50);
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);

虽然原始,但在没有调试工具的情况下,能快速判断程序是否卡住、中断是否触发。


最后的思考:UART 过时了吗?

有人可能会说:“现在都 2025 年了,还用 UART?应该上 CAN、Ethernet、甚至 PCIe!”

话虽如此,但工程的本质是 解决问题的成本与效率平衡

UART 没有被淘汰,恰恰因为它够简单、够稳定、够通用。

在一个产品从原型到量产的过程中,UART 往往承担着至关重要的角色:

  • 调试通道(打印日志)
  • 升级接口(ISP 下载)
  • 外设通信(GPS、指纹模块、显示屏)
  • 双 MCU 协作中枢

它是嵌入式系统的“普通话”,不管你用什么方言(RTOS、裸机、Linux),都能靠它交流。

所以,别轻视 UART。掌握它,等于掌握了一种底层思维: 化繁为简,直击本质


现在你手里的杜邦线,已经不只是两根导线了。它是两个世界的桥梁,是控制与连接的交汇点。

下次当你把 STM32 和 ESP32 用串口连起来的时候,不妨想想:这两个芯片正在悄悄地说些什么?

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

您可能感兴趣的与本文相关内容

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值