第四课:ESP32 串口通信接收数据帧并解析 —— 实现自定义传感器协议支持
在很多实际项目中,我们经常会遇到通过串口接收传感器或其他设备的数据,这些数据通常不是简单的 ASCII 字符串,而是 结构化的数据帧,包含起始位、地址、数据体、校验码、结束位等。
本课将以一个典型的二进制协议数据帧为例,带你一步步实现 ESP32 对其接收、验证与解析的完整逻辑。
📦 一、数据帧结构说明
假设传感器每次通过串口发送的数据帧如下所示:
[0x49] [ADDR] [LEN] [DATA …] [CHECKSUM] [0x4D]
0x49
: 固定起始码ADDR
: 设备地址LEN
: 数据体长度(不含起始/结束/校验等)DATA
: 数据体内容,长度由LEN
决定CHECKSUM
: 从ADDR
到DATA
所有字节的校验和0x4D
: 固定结束码
例如,一个代表地址为 0x01、发送 2 字节数据 0x0A, 0x0B
的完整数据帧如下:
0x49 0x01 0x02 0x0A 0x0B 0x13 0x4D
其中 0x13 = 0x01 + 0x02 + 0x0A + 0x0B
🔌 二、ESP32 硬件串口接线说明
ESP32 支持多个 UART,我们使用 UART1
来连接传感器:
信号说明 | ESP32 引脚 | 方向 |
---|---|---|
传感器 TX | GPIO18 (U1RXD) | ESP32 接收 |
传感器 RX | GPIO17 (U1TXD) | ESP32 发送 |
⚠️ 注意:传感器与 ESP32 的串口是“交叉”连接,TX 接 RX,RX 接 TX。
🧠 三、核心功能实现概述
- 初始化 UART1 串口:使用
HardwareSerial
初始化,指定 GPIO 引脚; - 接收字节流并存入缓冲区:判断何时收到完整的一帧;
- 数据帧合法性校验:
- 起始码是否为
0x49
- 结束码是否为
0x4D
- 校验和是否一致
- 起始码是否为
- 解析帧内容并打印:
- 地址字段
- 数据体长度和具体内容
💡 四、完整代码与说明
#include <HardwareSerial.h>
#define RX_PIN 18 // 传感器的 RX 引脚接 ESP32 的 GPIO18 (U1RXD)
#define TX_PIN 17 // 传感器的 TX 引脚接 ESP32 的 GPIO17 (U1TXD)
#define BAUD_RATE 115200 // 串口波特率
#define BUFFER_SIZE 256 // 接收缓冲区大小
HardwareSerial mySerial(1); // 使用 UART1
uint8_t buffer[BUFFER_SIZE]; // 定义接收缓冲区
int bufferIndex = 0; // 缓冲区索引
// 验证数据帧是否合法
bool validateFrame(uint8_t* frame, int len) {
if (len < 5) return false; // 数据帧太短
if (frame[0] != 0x49 || frame[len - 1] != 0x4D) { // 检查起始码和结束码
Serial.println("错误: 起始码或结束码不匹配!");
return false;
}
// 校验和计算
uint8_t checksum = 0;
for (int i = 1; i < len - 2; i++) { // 地址到数据体的校验和
checksum += frame[i];
}
if (checksum != frame[len - 2]) {
Serial.println("错误: 校验码不匹配!");
return false;
}
return true;
}
// 解析数据帧
void parseFrame(uint8_t* frame, int len) {
if (!validateFrame(frame, len)) {
Serial.println("无效数据帧,跳过解析!");
return;
}
uint8_t address = frame[1]; // 地址
uint8_t dataLength = frame[2]; // 数据体长度
uint8_t* data = &frame[3]; // 数据体起始地址
Serial.print("解析成功,地址 = ");
Serial.println(address, HEX);
Serial.print("数据体 = ");
for (int i = 0; i < dataLength; i++) {
Serial.print(data[i], HEX);
Serial.print(" ");
}
Serial.println();
}
void resetBuffer() {
bufferIndex = 0;
memset(buffer, 0, BUFFER_SIZE); // 清空缓冲区
}
void setup() {
Serial.begin(115200); // 初始化调试串口
mySerial.begin(BAUD_RATE, SERIAL_8N1, RX_PIN, TX_PIN); // 初始化传感器串口
Serial.println("开始与传感器通信...");
}
void loop() {
// 检查串口是否有数据可读
while (mySerial.available()) {
uint8_t byteReceived = mySerial.read();
// 防止缓冲区溢出
if (bufferIndex >= BUFFER_SIZE) {
Serial.println("警告: 缓冲区溢出,丢弃当前数据!");
resetBuffer();
continue;
}
// 存储接收到的字节到缓冲区
buffer[bufferIndex++] = byteReceived;
// 检测是否接收到结束码
if (byteReceived == 0x4D) {
Serial.println("接收到完整数据帧!");
Serial.print("数据帧原始内容: ");
for (int i = 0; i < bufferIndex; i++) {
Serial.print(buffer[i], HEX);
Serial.print(" ");
}
Serial.println();
// 如果数据帧以起始码开头且合法,解析之
if (buffer[0] == 0x49 && validateFrame(buffer, bufferIndex)) {
parseFrame(buffer, bufferIndex);
} else {
Serial.println("无效数据帧,跳过解析!");
}
// 重置缓冲区,准备接收下一帧
resetBuffer();
}
}
delay(50); // 避免占用过多 CPU
}
🔍 五、关键逻辑详解
1️⃣ 校验函数 validateFrame()
用于判断当前帧是否完整、合法,是防止误解析的核心步骤。检查三点:
- 长度是否够
- 起始码、结束码是否正确
- 校验和是否与接收到的匹配
if (frame[0] != 0x49 || frame[len - 1] != 0x4D) {...}
若前面字节不是 0x49 起始码,或者校验和错,则打印错误信息并重置缓冲区。
🧪 六、调试建议与常见问题
接收不到数据?
检查传感器波特率是否为 115200;
检查是否接线正确(TX 接 RX);
使用串口助手(如串口调试精灵)模拟发帧验证 ESP32 接收功能。
数据帧校验失败?
注意校验和计算范围是 [ADDR] ~ [DATA],不含起始码和校验字节本身;
数据体长度错误也会导致校验失败。
缓冲区溢出?
确保数据频率合理,或加大 BUFFER_SIZE;
加入溢出判断逻辑,避免数据覆盖或死循环。
🚀 七、应用拓展建议
将解析后的数据用 BLE、MQTT、HTTP 方式上传;
支持多种帧格式:可进一步封装解码器类;
引入状态机机制解析多段复杂帧协议;
将数据体保存为 CSV、JSON 结构并写入 SD 卡。
本课实战演示了 ESP32 如何与自定义协议的传感器进行串口通信并正确解析结构化数据帧,为后续实现复杂协议通讯或多传感器接入打下基础。下一课,我们将尝试将解析后的数据存入 SD 卡或通过 BLE 广播发送。
如果你喜欢这样的内容,记得点赞 👍 收藏 📁 留言 💬,支持原创 ESP32 教程持续更新!