当前位置: 首页 > news >正文

工业级串口通信设计

1. 工业级串口通信的特点

在工业控制、仪器仪表、传感器数据采集等场景中,串口(UART/RS232/RS485)依然是常用的通信方式。相比消费电子,工业串口通信有几个特点:

  1. 高可靠性要求
    数据丢失或错误可能导致设备停机、生产事故,因此必须有帧同步长度标识校验机制

  2. 多设备、多协议复用
    一个总线(比如 RS485)上可能挂多个设备,每个设备可能有不同的命令/数据类型,需要通过帧头(prefix)区分。

  3. 环境干扰大
    工厂现场存在电磁干扰,串口数据可能出现毛刺、丢字节、错码,因此接收逻辑必须有容错和快速同步能力。

  4. 实时性要求
    控制系统往往要求数据延迟可控,接收解析必须高效,不能阻塞主线程。


2. 帧格式设计

在基于 prefix 的串口协议中,帧结构通常如下(示例):

字段长度(字节)说明
STX1帧起始符,固定值如 0x55
Prefix1~2帧类型标识,区分不同命令或设备
Length1~2后续 Payload 的字节数
PayloadN实际数据内容
Checksum1~2校验和(累加和、CRC16、CRC32等)

设计理由

  • STX 用于快速定位帧的起点。
  • Prefix 用于区分不同的消息类型,方便上层分发处理。
  • Length 用于动态接收 payload,避免粘包/拆包问题。
  • Checksum 用于检测传输错误。

3. 接收流程(状态机思想)

工业级串口接收一般采用状态机来解析数据,避免混乱。流程如下:

  1. 空闲状态(Idle)

    • 不断读取串口字节,直到检测到 STX。
    • 一旦找到 STX,进入下一状态。
  2. 接收 Prefix

    • 读取 Prefix 字节(1~2字节)。
    • 保存 Prefix,进入接收 Length 状态。
  3. 接收 Length

    • 读取 Length 字节,解析出 payload 长度 N。
    • 进入接收 Payload 状态。
  4. 接收 Payload

    • 持续读取 N 个字节存入缓冲区。
    • 读完后进入接收 Checksum 状态。
  5. 接收 Checksum

    • 读取 Checksum 字节,并与本地计算值比较。
    • 校验正确:将帧数据(含 Prefix、Payload)提交给上层处理,回到 Idle。
    • 校验错误:丢弃本帧,回到 Idle,并记录错误日志。
  6. 异常处理

    • 若超时未收完一帧(例如 100ms 无新数据),复位状态机。
    • 若在接收过程中再次收到 STX,说明帧同步错误,立即复位并从新 STX 开始。

4. C++实现

下面给出一个工业级可用的串口帧接收类和发送类,包含状态机和缓冲区管理。

帧格式:

[STX] [Prefix] [Length] [Payload] [Checksum]
  • STX:固定 0x55
  • Prefix:帧类型标识(1字节)
  • Length:Payload 长度(1字节)
  • Payload:数据内容(长度可变)
  • Checksum:所有字节累加和的低8位

(1)接收器代码

#include <cstdint>
#include <vector>
#include <functional>
#include <chrono>
#include <thread>
#include <iostream>// 帧结构定义
struct Frame {uint8_t prefix;std::vector<uint8_t> payload;
};// 环形缓冲区,用于串口数据缓存
template <size_t N>
class RingBuffer {
public:bool push(uint8_t data) {size_t next = (head + 1) % N; //计算下一位的位置if (next == tail) return false; // 满buffer[head] = data;head = next;return true;}bool pop(uint8_t& data) {if (head == tail) return false; // 空data = buffer[tail];tail = (tail + 1) % N;return true;}size_t size() const {return (head - tail + N) % N;}bool empty() const {return head == tail;}private:uint8_t buffer[N];size_t head = 0;size_t tail = 0;
};class SerialFrameReceiver {
public:using FrameHandler = std::function<void(const Frame&)>;SerialFrameReceiver(uint8_t prefix, size_t maxPayloadSize = 256): targetPrefix(prefix), maxPayload(maxPayloadSize) {reset();}// 喂入串口数据(由串口中断或读取线程调用)void feed(uint8_t byte) {rb.push(byte);}// 处理缓冲区中的数据void process() {uint8_t byte;while (rb.pop(byte)) {stateMachine(byte);}}void setFrameHandler(FrameHandler handler) { //handler是一个函数封装器,相当于std::function(函数),用来代指函数(函数对象functors),下文调用的时候把lambda函数传入setFrameHandler函数。this->handler = handler; //给lambda函数创建函数封装器,并将其赋值给成员变量handler}private://状态机设计enum class State {Idle,GotSTX,GotPrefix,GotLength,GettingPayload,GotChecksum};void reset() {state = State::Idle;currentFrame.prefix = 0;currentFrame.payload.clear();payloadLength = 0;received = 0;checksum = 0;lastTime = std::chrono::steady_clock::now();}void stateMachine(uint8_t byte) {switch (state) {case State::Idle:if (byte == STX) {state = State::GotSTX;checksum = byte;}break;case State::GotSTX:if (byte == targetPrefix) {currentFrame.prefix = byte;checksum += byte;state = State::GotPrefix;} else {reset(); // 前缀不匹配,复位}break;case State::GotPrefix:payloadLength = byte;if (payloadLength == 0 || payloadLength > maxPayload) {reset(); // 长度非法break;}checksum += byte;currentFrame.payload.reserve(payloadLength);state = State::GettingPayload;break;case State::GettingPayload:currentFrame.payload.push_back(byte);checksum += byte;received++;if (received == payloadLength) {state = State::GotChecksum;}break;case State::GotChecksum:if (byte == (checksum & 0xFF)) {if (handler) handler(currentFrame);//handler 是通过 setFrameHandler 保存的 lambda函数} else {std::cerr << "Checksum error\n";}reset();break;}}static constexpr uint8_t STX = 0x55;uint8_t targetPrefix;size_t maxPayload;RingBuffer<1024> rb;FrameHandler handler;State state = State::Idle;Frame currentFrame;size_t payloadLength = 0;size_t received = 0;uint8_t checksum = 0;std::chrono::steady_clock::time_point lastTime;
};// 示例:模拟串口数据接收
int main() {SerialFrameReceiver receiver(0xAA, 128);receiver.setFrameHandler([](const Frame& frame) {std::cout << "Received frame, prefix: 0x" << std::hex << static_cast<int>(frame.prefix) << std::dec;std::cout << ", payload size: " << frame.payload.size() << std::endl;});// 模拟一帧数据: STX(0x55) + Prefix(0xAA) + Length(0x03) + Payload(0x01 0x02 0x03) + Checksum(0xFF)uint8_t testFrame[] = {0x55, 0xAA, 0x03, 0x01, 0x02, 0x03, 0xFF};for (auto b : testFrame) {receiver.feed(b);}receiver.process();// 模拟错误帧uint8_t badFrame[] = {0x55, 0xAA, 0x02, 0x11, 0x22, 0x00}; // checksum wrongfor (auto b : badFrame) {receiver.feed(b);}receiver.process();return 0;
}

(2)发送器代码

#include <cstdint>
#include <vector>
#include <functional>
#include <iostream>struct Frame {uint8_t prefix;std::vector<uint8_t> payload;
};class SerialFrameSender {
public:using SendHandler = std::function<void(const uint8_t*, size_t)>;SerialFrameSender(SendHandler sender)//构造函数,实例化的时候要传入SendHandler类型的函数: sendHandler(sender) {}//用sender去初始化成员变量sendHandler// 将 Frame 打包并发送bool sendFrame(const Frame& frame) {if (frame.payload.size() > 255) {std::cerr << "Payload too large (max 255 bytes)\n";return false;}// 组装帧std::vector<uint8_t> buffer;buffer.reserve(4 + frame.payload.size()); // STX + Prefix + Length + Payload + Checksumuint8_t STX = 0x55;buffer.push_back(STX);buffer.push_back(frame.prefix);buffer.push_back(static_cast<uint8_t>(frame.payload.size()));for (uint8_t b : frame.payload) {buffer.push_back(b);}// 计算校验和uint8_t checksum = STX + frame.prefix + static_cast<uint8_t>(frame.payload.size());for (uint8_t b : frame.payload) {checksum += b;}buffer.push_back(checksum);// 调用发送回调(例如串口发送函数)if (sendHandler) {sendHandler(buffer.data(), buffer.size());return true;}return false;}private:SendHandler sendHandler;
};// 示例:模拟串口发送
int main() {// 模拟串口发送函数auto uartSend = [](const uint8_t* data, size_t len) {std::cout << "Sending " << len << " bytes: ";for (size_t i = 0; i < len; ++i) {printf("%02X ", data[i]);}std::cout << std::endl;};SerialFrameSender sender_test(uartSend);// 创建一帧数据Frame frame;frame.prefix = 0xAA;frame.payload = {0x01, 0x02, 0x03};// 发送帧sender.sendFrame(frame);return 0;
}

(3)发送流程

  1. 检查长度
    因为 Length 字段是 1 字节,所以 payload 最大长度是 255。

  2. 组装帧头

    • 写入 STX(0x55)
    • 写入 Prefix(帧类型)
    • 写入 Length(payload 长度)
  3. 写入 payload

  4. 计算校验和
    从 STX 开始,把所有字节累加,取低 8 位。

  5. 调用发送回调
    用户需要传入一个实际的发送函数(比如 write() 到串口)。


(4) 运行示例

假设我们发送:

prefix = 0xAA
payload = {0x01, 0x02, 0x03}

组装后的帧为:

0x55 0xAA 0x03 0x01 0x02 0x03 0xFF
  • 0x55 是 STX
  • 0xAA 是 Prefix
  • 0x03 是 payload 长度
  • 0x01 0x02 0x03 是 payload
  • 0xFF 是校验和(累加和 = 0x1FF → 低8位 0xFF)

运行输出:

Sending 7 bytes: 55 AA 03 01 02 03 FF

(5) 扩展建议

  • 支持不同校验算法(如 CRC16/CRC32)
  • 支持批量发送(多个帧一次性发送)
  • 增加发送队列(在多线程环境下安全)
  • 支持可变长度的 Length 字段(比如 2 字节表示更大长度)

5. 代码特点与工业适配建议

  1. 状态机驱动
    避免了复杂的 if-else 嵌套,逻辑清晰,容易调试和扩展。

  2. 环形缓冲区
    串口数据可能是中断方式接收,用环形缓冲可以解耦数据接收和解析。

  3. 可配置 prefix
    支持多协议,同一串口可以接收不同类型帧。

  4. 校验机制
    当前是简单累加和,工业中可替换为 CRC16/CRC32 提高抗干扰性。

  5. 超时处理
    可以在 process() 中加入超时判断,防止卡死在某个状态。


6. 测试与调试

  • 正常帧测试:发送符合格式的数据,验证能否正确解析。
  • 错误帧测试:修改 payload 或 checksum,验证是否能丢弃。
  • 边界测试:payload 长度为 0、最大长度、STX 出现在 payload 中间等情况。
  • 性能测试:在高波特率(例如 115200、921600)下连续发送数据,确保无丢帧。

工业级串口接收流程必须具备:

  • 可靠的帧同步(STX + Prefix)
  • 动态长度解析(Length字段)
  • 数据校验(Checksum/CRC)
  • 异常处理与快速恢复
  • 高效缓冲与低延迟


最后给出一套liunx系统上C++实现串口发送和接收的实用代码

[STX] [Prefix] [Length] [Payload] [Checksum]
  • STX0x55
  • Prefix:帧类型(1字节)
  • Length:Payload 长度(1字节)
  • Payload:数据内容
  • Checksum:累加和低8位

发送

#include <cstdint>
#include <vector>
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <cstring>
#include <stdexcept>struct Frame {uint8_t prefix;std::vector<uint8_t> payload;
};class LinuxSerialFrameSender {
public:// 打开串口并配置LinuxSerialFrameSender(const std::string& port = "/dev/ttyUSB0", speed_t baud = B115200) {fd = open(port.c_str(), O_WRONLY | O_NOCTTY);//open函数是C接口,不支持std::stirng类型,需要用c_str()转为C风格字符串if (fd < 0) {throw std::runtime_error("无法打开串口设备: " + port);}struct termios tio;memset(&tio, 0, sizeof(tio));// 配置串口tio.c_cflag = baud | CS8 | CLOCAL | CREAD; // 波特率 | 8数据位 | 本地连接 | 允许接收tio.c_iflag = IGNPAR;                      // 忽略奇偶校验错误tio.c_oflag = 0;tio.c_lflag = 0;tio.c_cc[VTIME] = 0;                       // 读取超时tio.c_cc[VMIN]  = 1;                       // 最小读取字符数// 清理串口缓存并应用配置tcflush(fd, TCIFLUSH);if (tcsetattr(fd, TCSANOW, &tio) != 0) {close(fd);throw std::runtime_error("无法配置串口");}std::cout << "串口 " << port << " 已打开,波特率 " << baud << std::endl;}~LinuxSerialFrameSender() {if (fd >= 0) {close(fd);std::cout << "串口已关闭" << std::endl;}}// 发送一帧数据bool sendFrame(const Frame& frame) {if (frame.payload.size() > 255) {std::cerr << "Payload 超过最大长度 255\n";return false;}std::vector<uint8_t> buffer;buffer.reserve(4 + frame.payload.size()); // STX(1) + Prefix(1) + Length(1) + Payload(n) + Checksum(1)uint8_t STX = 0x55;buffer.push_back(STX);buffer.push_back(frame.prefix);buffer.push_back(static_cast<uint8_t>(frame.payload.size()));for (uint8_t b : frame.payload) {buffer.push_back(b);}// 计算校验和uint8_t checksum = STX + frame.prefix + static_cast<uint8_t>(frame.payload.size());for (uint8_t b : frame.payload) {checksum += b;}buffer.push_back(checksum);// 发送数据ssize_t n = write(fd, buffer.data(), buffer.size());if (n < 0) {perror("write");return false;}std::cout << "已发送 " << n << " 字节: ";for (size_t i = 0; i < buffer.size(); ++i) {printf("%02X ", buffer[i]);}std::cout << std::endl;return true;}private:int fd = -1; // 串口文件描述符
};int main() {try {LinuxSerialFrameSender sender("/dev/ttyUSB0", B115200);Frame frame;frame.prefix = 0xAA;frame.payload = {0x01, 0x02, 0x03};sender.sendFrame(frame);} catch (const std::exception& e) {std::cerr << "错误: " << e.what() << std::endl;return -1;}return 0;
}

接收

#include <cstdint>
#include <vector>
#include <functional>
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <cstring>
#include <stdexcept>struct Frame {uint8_t prefix;std::vector<uint8_t> payload;
};class LinuxSerialFrameReceiver {
public:using FrameHandler = std::function<void(const Frame&)>;// 打开串口并配置LinuxSerialFrameReceiver(const std::string& port = "/dev/ttyUSB0", speed_t baud = B115200) {fd = open(port.c_str(), O_RDONLY | O_NOCTTY);if (fd < 0) {throw std::runtime_error("无法打开串口设备: " + port);}struct termios tio;memset(&tio, 0, sizeof(tio));// 配置串口tio.c_cflag = baud | CS8 | CLOCAL | CREAD; // 波特率 | 8数据位 | 本地连接 | 允许接收tio.c_iflag = IGNPAR;                      // 忽略奇偶校验错误tio.c_oflag = 0;tio.c_lflag = 0;tio.c_cc[VTIME] = 0;                       // 读取超时tio.c_cc[VMIN]  = 1;                       // 最小读取字符数// 清理串口缓存并应用配置tcflush(fd, TCIFLUSH);if (tcsetattr(fd, TCSANOW, &tio) != 0) {close(fd);throw std::runtime_error("无法配置串口");}std::cout << "串口 " << port << " 已打开,波特率 " << baud << std::endl;}~LinuxSerialFrameReceiver() {if (fd >= 0) {close(fd);std::cout << "串口已关闭" << std::endl;}}// 设置帧处理回调函数void setFrameHandler(FrameHandler handler) {this->handler = handler;}// 读取并解析数据void readLoop() {uint8_t byte;ssize_t n;while ((n = read(fd, &byte, 1)) == 1) {stateMachine(byte);}if (n < 0) {perror("read");}}private:enum class State {Idle,GotSTX,GotPrefix,GotLength,GettingPayload,GotChecksum};void reset() {state = State::Idle;currentFrame.prefix = 0;currentFrame.payload.clear();payloadLength = 0;received = 0;checksum = 0;}void stateMachine(uint8_t byte) {switch (state) {case State::Idle:if (byte == STX) {state = State::GotSTX;checksum = byte;}break;case State::GotSTX:currentFrame.prefix = byte;checksum += byte;state = State::GotPrefix;break;case State::GotPrefix:payloadLength = byte;if (payloadLength == 0 || payloadLength > maxPayload) {reset();break;}checksum += byte;currentFrame.payload.reserve(payloadLength);state = State::GettingPayload;break;case State::GettingPayload:currentFrame.payload.push_back(byte);checksum += byte;received++;if (received == payloadLength) {state = State::GotChecksum;}break;case State::GotChecksum:if (byte == (checksum & 0xFF)) {if (handler) handler(currentFrame);} else {std::cerr << "校验和错误\n";}reset();break;}}static constexpr uint8_t STX = 0x55;static constexpr size_t maxPayload = 255;int fd = -1;FrameHandler handler;State state = State::Idle;Frame currentFrame;size_t payloadLength = 0;size_t received = 0;uint8_t checksum = 0;
};int main() {try {LinuxSerialFrameReceiver receiver("/dev/ttyUSB0", B115200);receiver.setFrameHandler([](const Frame& frame) {std::cout << "收到帧, prefix: 0x" << std::hex << static_cast<int>(frame.prefix) << std::dec;std::cout << ", payload 长度: " << frame.payload.size() << std::endl;std::cout << "Payload: ";for (uint8_t b : frame.payload) {printf("%02X ", b);}std::cout << std::endl;});std::cout << "开始接收数据...\n";receiver.readLoop();} catch (const std::exception& e) {std::cerr << "错误: " << e.what() << std::endl;return -1;}return 0;
}

3. 代码说明

3.1 串口初始化

  • open() 打开串口设备,O_RDONLY 只读模式,O_WRONLY只写模式,O_NOCTTY不要把这个设备当成控制终端
  • termios 配置串口参数:
    • 波特率(默认 115200)
    • 8数据位、无校验、1停止位(8N1)
    • 忽略奇偶校验错误
    • 最小读取 1 字节

3.2 帧解析状态机

  • Idle:等待 STX (0x55)
  • GotSTX:收到帧头,等待 Prefix
  • GotPrefix:收到帧类型,等待 Length
  • GettingPayload:循环接收 payload 数据
  • GotChecksum:验证校验和,如果正确调用回调

3.3 回调机制

  • 用户通过 setFrameHandler() 设置帧处理函数
  • 解析成功时调用回调,把完整的 Frame 对象传出去

4. 编译与运行

编译:

g++ serial_receiver.cpp -o serial_receiver

运行(需要串口权限):

sudo ./serial_receiver

如果权限不足:

sudo usermod -aG dialout $USER

然后注销重新登录。


5. 测试方法

  1. 用 USB 转串口模块连接两台电脑(或一台电脑两个串口)
  2. 一个终端运行:
    ./serial_sender
    
  3. 另一个终端运行:
    ./serial_receiver
    
  4. 你会看到 serial_sender 发送帧,serial_receiver 收到并打印帧内容

  • 这个版本直接在 Linux 下从串口读取字节
  • 内置状态机解析帧,和发送器的帧格式完全匹配
  • 使用回调机制通知上层处理解析到的帧
  • 支持配置串口名和波特率
http://www.dtcms.com/a/490008.html

相关文章:

  • 盐山网站建设广西网上办事大厅
  • 郑州高端网站制作团队大连本地网
  • Linux网络的应用层自定义协议
  • leetcode 2598 执行操作后的最大MEX
  • FFmpeg 基本API avio_read函数内部调用流程分析
  • 【计算机网络】HTTP协议核心知识梳理
  • 基于 MediaMTX 的微信小程序 Web 实时音视频实操方案
  • 《UDP网络编程完全指南:从套接字到高并发聊天室实战》
  • 关于 云服务器WindowsServer2016双击无法运行可执行程序 的解决方法
  • LeetCode每日一题——在区间范围内统计奇数数目
  • Linux内核架构浅谈43-Linux slab分配器:小内存块分配与内核对象缓存机制
  • 最好的免费发布网站wordpress 文章二维码
  • Spring Boot 3零基础教程,Spring Boot 日志格式,笔记18
  • mybatis-plus分页插件使用
  • 福建住房和城乡建设网站网站做提示框
  • 李宏毅机器学习笔记24
  • Leetcode每日一练--28
  • Vue Router 路由元信息(meta)详解
  • 列表标签之无序标签(本文为个人学习笔记,内容整理自哔哩哔哩UP主【非学者勿扰】的公开课程。 > 所有知识点归属原作者,仅作非商业用途分享)
  • sk13.【scikit-learn基础】-- 自定义模型与功能
  • (Spring)Spring Boot 中 @Valid 与全局异常处理器的联系详解
  • 数据库数据类型,数据值类型,字符串类型,日期类型详解
  • 怎么写网站规划方案买链接做网站 利润高吗
  • SAP MM物料主数据锁定及解锁接口分享
  • [FSCalendar] 可定制的iOS日历组件 | docs | Interface Builder
  • 中兴B860AV5.1-M2/B860AV5.2M_安卓9_S905L3SB_支持外置WIFI_线刷固件包
  • AI 模型部署体系全景:从 PyTorch 到 RKNN 的嵌入式类比解析
  • 全球汽车紧固件产业进入关键转型期,中国供应链加速融入世界市场
  • 17网站一起做网店下载自动发卡网站建设
  • PHP 类型比较