当前位置:首页 > 技术学院 > 技术前线
[导读]串口(UART/TTL/RS232/RS485)凭借接线简单、兼容性强、调试成本低的优势,一直是嵌入式设备、工业控制、智能家居设备中最常用的短距离通信方式。但串口本身是基于字节流的底层通信协议,只保证单个字节的传输,不处理数据帧的分组、校验、错误处理,如果没有一套合理的用户层协议,很容易出现粘包、丢包、错误数据无法识别等问题,轻则功能异常,重则导致设备误动作引发安全问题。

串口(UART/TTL/RS232/RS485)凭借接线简单、兼容性强、调试成本低的优势,一直是嵌入式设备、工业控制、智能家居设备中最常用的短距离通信方式。但串口本身是基于字节流的底层通信协议,只保证单个字节的传输,不处理数据帧的分组、校验、错误处理,如果没有一套合理的用户层协议,很容易出现粘包、丢包、错误数据无法识别等问题,轻则功能异常,重则导致设备误动作引发安全问题。

如何设计一套稳定可靠、易于实现、可扩展的用户层串口协议?本文结合实际开发经验,梳理协议设计的核心原则、常见的编帧技巧,并给出一套可直接复用的实现代码,帮助开发者快速搭建稳定的串口通信协议。

一、用户层串口协议的核心需求

设计协议之前,我们首先需要明确串口用户层协议需要解决哪些核心问题,这是设计的基础:

帧分界识别:如何从连续的字节流中分割出一帧完整的合法数据?解决“粘包”问题——多个连续发送的帧连在一起,接收端要能准确拆分出每一帧。

数据完整性校验:传输过程中受到电磁干扰,可能会出现比特翻转、字节丢失,如何识别出错误的脏数据,避免把错误数据当成合法数据处理。

功能寻址与扩展:如何区分不同类型的指令、不同地址的设备?方便后续新增功能的时候不需要重新修改底层协议框架,满足迭代需求。

异常处理与重传:丢包或者错误的时候,如何通知发送端重发,保证数据最终可达,满足工业场景下高可靠性需求。

满足这几个核心需求的协议,就是一套能用的协议,在此基础上再根据实际场景做裁剪和优化即可。

二、协议编制的核心技巧

1. 帧分界:四种常见成帧方式的优劣对比

帧分界识别是协议设计第一步,常用的成帧方式有四种,各有优劣,适用不同场景。

(1)固定长度帧

最简单的方式,约定每一帧的长度都是固定N字节,接收端收到N字节就认为是一帧完整数据。优点是实现极其简单,不需要判断分界,只需要计数即可;缺点也非常明显:如果实际数据长度小于约定长度,就会一直等待收不到完整帧,或者把下一个帧的字节拼到当前帧,导致整个流乱掉;如果数据长度变化大,会浪费带宽传输填充字节,利用率低。

适用场景:数据长度固定不变的场景,比如只传输固定格式的传感器数据,或者指令长度都一样的简单交互场景,不适合数据长度变化大的复杂场景。

(2)特殊字符分界(起止符)

最常用的成帧方式,约定一个特定的帧头(起始符)和帧尾(结束符),接收端只要识别到帧头就开始收集数据,直到识别到帧尾就认为一帧结束。比如常见的用0xAA作为帧头,0x55作为帧尾,或者用回车换行\r\n作为AT指令的帧尾,实现简单,带宽利用率高,适合数据长度变化大的场景。

但这种方式有一个核心问题:如果有效数据中出现了和帧尾相同的字符,会被误判为帧结束,导致帧被截断。解决这个问题需要做转义处理:当有效数据中出现帧头、帧尾、转义符本身的时候,在前面加一个转义符,接收端收到转义符后,把下一个字符当作普通数据处理,就能解决数据和分界符冲突的问题。

转义处理会增加一点点代码复杂度,但实现难度很低,是大多数中小规模串口项目的首选,兼顾实现难度和带宽利用率。

(3)长度字段成帧

把数据长度放在帧头的固定位置,接收端先收帧头,读出长度字段,然后再收指定长度的数据,收满之后就是完整一帧。这种方式不需要转义,不管有效数据是什么内容都不会影响分界,实现也比较简单,带宽利用率高。

缺点是如果长度字段在传输中出错,比如变成了一个很大的数值,接收端会一直等待收数据,把后续所有数据都当成当前帧的内容,导致整个通信流乱掉,需要额外加同步机制恢复。

实际开发中,常把长度字段和帧头结合使用:先收帧头(2-4字节固定魔法值),验证帧头正确后再读长度字段,这样可以大幅降低错读长度的概率,兼顾可靠性和实现难度,是复杂项目的首选方案。

(4)时间分片成帧

利用串口传输的间隙:两个帧之间发送间隔超过一定时间(比如几个字节的传输时间),就认为是一帧结束。这种方式不需要修改数据内容,对透明传输友好,但依赖超时判断,受波特率和系统调度影响很大,比如系统进入中断屏蔽,可能会误把同一帧分成多个帧,稳定性差,一般只用来做调试日志,不用于正式的指令交互。

四种方式对比总结:

成帧方式实现难度带宽利用率可靠性适用场景

固定长度极低低一般固定长度简单交互

起止符+转义低高较高中小规模可变长项目

帧头+长度中高高复杂可变长项目

时间分片低高差调试日志输出

2. 校验:选择合适的校验算法平衡性能和可靠性

校验是识别错误数据的核心,常用的校验方式有几种,根据错误概率和性能要求选择:

累加和校验(Checksum):把所有数据字节累加,取低8位或者低16位作为校验值,实现极其简单,一个循环就能完成,8位校验能识别大约99%的单比特错误,适合干扰不大的消费电子场景,缺点是多比特错误的漏检率偏高。

异或校验:和累加和类似,实现同样简单,性能差不多,漏检率和累加和接近,选择哪一种看个人习惯。

CRC校验:循环冗余校验,是工业场景最常用的校验方式,CRC16能识别几乎所有单比特和大多数多比特错误,漏检率远低于累加和,性能也非常好,一个字节一个字节计算只需要几个时钟周期,资源消耗很小,对于嵌入式MCU来说完全没有压力。如果是对可靠性要求高的工业场景,优先选CRC16(常用CCITT或者MODBUS标准)。

MD5/SHA校验:适合大文件传输,安全性和准确性极高,但计算量大,资源占用高,普通串口指令交互不需要这么强的校验,一般只用在固件升级这类大文件传输场景。

实际开发中,一般小项目用累加和足够,工业场景优先用CRC16,兼顾性能和可靠性,不要过度设计用复杂校验,徒增代码复杂度。

3. 地址与功能域设计:兼顾扩展性兼容性

协议需要区分不同设备和不同指令,一般我们会设计两个基本字段:

地址域:如果是一主多从的RS485总线,需要给每个从设备分配唯一地址,地址域占1字节就能支持256个设备,足够大多数场景,如果需要更多设备可以扩展为2字节。

功能码域:区分不同的指令类型,比如0x01是读传感器数据,0x02是写控制指令,占1字节支持256种指令,足够大多数项目用,后续新增指令只需要新增功能码即可,不需要修改协议框架。

为了兼容性,最好保留扩展位:比如功能码最高位标识是否是扩展功能,方便后续功能不够用的时候平滑扩展,不需要推翻原有设计。

4. 异常处理与重传:提升可靠性的关键

稳定的协议需要处理异常:当接收端收到错误帧(校验错、长度错),或者丢包的时候,需要有重传机制保证数据可达。常用的方案是:

发送端发送指令后启动定时器,等待接收端的ACK应答,如果超时没有收到ACK,就重发一次,重发3次还是失败就上报错误。

接收端收到校验正确的帧后,回复ACK确认,如果校验错误直接丢弃,不回复,等待发送端重发。

对于不需要保证百分百到达的场景(比如周期上报传感器数据,丢一帧不影响),可以去掉重传机制,简化代码,节省带宽。

三、一套通用协议的具体实现

我们结合上面的技巧,设计一套常用的“帧头+长度+地址+功能码+数据+CRC16校验”的可变长协议,适合大多数一主多从串口通信场景,代码可以直接复用。

1. 协议帧格式定义

我们的帧格式如下,总长度最小是7字节,最大支持255字节数据,满足绝大多数嵌入式场景:

字段字节长度说明

帧头11固定0xAA,标识帧起始

帧头21固定0x55,双字节帧头降低错同步概率

长度1整个帧的总长度(从地址到校验,单位字节)

地址1从设备地址

功能码1指令功能码

数据N有效数据,长度 = 长度 - 4(地址、功能码占2,校验占2)

CRC16低字节1CRC16校验值低8位

CRC16高字节1CRC16校验值高8位

这个设计结合了帧头+长度的优点,双字节帧头大大降低了随机数据匹配到帧头的概率,长度字段明确标识了帧的总长度,不需要转义,任何数据内容都能传输,可靠性高,实现也简单。

2. 接收状态机实现

串口接收是逐字节中断接收的,我们用状态机来处理接收流程,状态分为四个:

typedef enum {

UART_STATE_WAIT_HEAD1, // 等待第一个帧头

UART_STATE_WAIT_HEAD2, // 等待第二个帧头

UART_STATE_WAIT_LENGTH, // 等待长度字段

UART_STATE_RECV_DATA, // 接收剩余数据

} uart_recv_state_t;

中断里逐字节接收,状态机跳转逻辑:

初始状态是等待第一个帧头,收到0xAA就跳转到等待第二个帧头,否则继续等待。

等待第二个帧头,收到0x55就跳转到等待长度,否则退回到等待第一个帧头重新匹配。

收到长度字段,保存长度,检查长度是否在合法范围(最小4字节,最大255字节),合法就跳转到接收数据,否则退回到等待第一个帧头。

接收数据,直到收完长度+2(CRC两个字节)个字节,一帧接收完成,触发接收完成回调,交给上层处理,然后退回到等待第一个帧头,准备接收下一帧。

状态机的好处是不管链路怎么乱,只要下一个正确的帧头过来,就能重新同步,不会一直乱下去,自动从错误中恢复,可靠性很高。

3. CRC16校验的实现

我们采用工业常用的MODBUS CRC16标准,实现代码非常简洁,资源占用很小:

uint16_t crc16_modbus(uint8_t *data, uint16_t len)

{

uint16_t crc = 0xFFFF;

for (uint16_t i = 0; i < len; i++) {

crc^= data[i];

for (uint8_t j = 0; j < 8; j++) {

if (crc & 0x01) {

crc >>= 1;

crc^= 0xA001;

} else {

crc >>= 1;

}

}

}

return crc;

}

接收完成后,计算从地址到数据的CRC16,和帧尾的CRC16比较,一致就是合法帧,不一致直接丢弃,处理非常简单。

4. 发送流程实现

发送流程比接收更简单,按照协议格式组包:

先填充两个帧头0xAA 0x55。

计算总长度:长度 = 1(地址) + 1(功能码) + N(数据长度) + 2(CRC),填充长度字段。

填充地址、功能码、数据。

计算地址到数据的CRC16,填充低字节和高字节。

把整个包通过串口发送出去,如果需要应答,启动超时定时器等待ACK即可。

四、工程开发中的优化技巧

1. 接收缓冲区设计

不要在串口中断里处理整帧数据,只把字节放到环形缓冲区,然后在主线程或者任务里处理状态机,避免中断阻塞太长时间,影响其他中断响应,尤其是波特率较高的时候,这个优化非常重要。环形缓冲区可以用RT-Thread或者RTOS自带的实现,自己实现也只需要几十行代码。

2. 错同步恢复优化

如果接收过程中出错,状态机直接退回到等待帧头状态,不需要清空整个缓冲区,下一个帧头到来会自动同步,这种设计让协议非常健壮,哪怕传输中丢了几个字节,只要下一个帧正确,就能很快恢复,不会影响后续通信。

3. 一主多从总线的地址过滤

RS485总线场景下,每个从设备收到帧后,先对比地址域,如果地址不是自己的地址,也不是广播地址,直接丢弃,不需要处理,符合总线设计,实现简单。

4. 固件升级大帧分片传输

如果需要用串口传输固件这种大数据,把固件分成多个固定大小的分片,每个分片按照上面的协议帧传输,每个分片带分片序号,接收端收到所有分片后拼接成完整固件,校验整个固件的MD5,就能实现可靠的固件升级,不需要重新设计大帧协议,复用原有框架即可。

五、常见坑点规避

字节序问题:多字节字段(比如长度、CRC、数据中的16位/32位变量)要约定好字节序,一般默认用小端序,和ARM架构一致,避免发送和接收解析字节序不对,导致数据错误。

缓冲区溢出:一定要检查长度字段的合法性,限制最大帧长度,避免长度字段出错变成很大的值,导致缓冲区溢出,冲垮内存,引发系统崩溃,我们上面的实现中,长度占1字节,最大长度就是255,缓冲区分配256+8就足够,不会溢出。

波特率不匹配:协议再好,硬件波特率不对也会出错,两端的波特率、数据位、停止位、校验位一定要完全一致,优先选8N1(8数据位,1停止位,无校验),这是业界通用默认配置,兼容性最好。

485总线的收发切换延迟:RS485半双工总线,发送完成后不要立刻切回接收,要等最后一个字节发送完成再切换,否则会丢最后几个字节,一般加个几毫秒的延时就能解决,或者用发送完成中断来切换。

总结

串口用户层协议设计不需要追求过度复杂,核心是解决“分帧、校验、错误处理”三个核心问题,根据场景选择合适的成帧和校验方式,用状态机处理接收,就能实现一套稳定可靠的协议。本文给出的“双帧头+长度+CRC16”的设计,兼顾了可靠性、实现难度和扩展性,适合绝大多数嵌入式串口通信场景,代码可以直接复用,开发者只需要根据自己的需求修改功能码定义就能使用,能帮开发者节省大量调试时间,快速搭建稳定的串口通信功能。

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
换一批
延伸阅读

串口丢包常被归咎于线材或干扰,但很多系统在实验室里安静放着也会少字节。嵌入式通信一旦让缓冲水位和硬件流控彼此脱节,接收链路就会出现一种很危险的错觉:两端都认为自己已经提醒过对方减速,可数据还是继续冲过来。

关键字: 嵌入式 串口 丢包

串口作为一种经典的异步通信接口,凭借结构简单、成本低廉、抗干扰能力强的优势,广泛应用于嵌入式设备、工业控制、智能硬件等场景,是实现设备间数据交互与控制的重要桥梁。引脚电平(高电平、低电平)是数字电路中最基础的信号表现形式...

关键字: 串口 通信接口 数字电路

在资源受限的嵌入式系统中,传统调试工具(如JTAG)往往成本高昂且占用引脚资源。本文介绍一种基于串口的低成本调试方案,通过自定义协议实现内存数据的实时监控,硬件成本可降低80%以上,特别适用于8/16位MCU开发场景。

关键字: 嵌入式 串口 内存数据

医学实验室中分析仪种类繁多 , 通信方式均不相同 ,无法整合到统一的流水线系统中应用 。为提高校验效率 , 统筹分析各家分析仪通信需求 ,设计基于串 口 的实验室流水线分析仪接口通信方案 ,打通分析仪并入流水线的通信障碍...

关键字: 医学实验室流水线 分析仪接口 通信方案设计 串口 异步通信

‌组态屏与串口屏的核心区别在于功能定位与通信协议‌:组态屏内置组态软件,可作为独立主机运行并支持多协议交互;串口屏则主要作为显示终端,遵循特定通信协议从外部设备获取数据。

关键字: 串口 通信协议‌

串口全称是串行接口(Serial Interface),串口通讯指仅用一对传输线就能将数据以比特位进行传输的一种通讯方式。尽管串口通讯必按字节传输的并行通信慢,但是串口可以在仅用两根线的情况下完成数据传输,大大降低了成本...

关键字: 串口 UART

随着电脑技术的发展,一些老的设备在新电脑上不能被使用,主要原因是不管是台式电脑,还是笔记本电脑,都很少有串口接口,也就是我们常说的COM口。好在这些设备都有USB接口,不妨通过接口转换的方式,使我们的设备在新电脑上重新被...

关键字: 串口 USB

串口:串口是一个泛称,UART、TTL、RS232、RS485都遵循类似的通信时序协议,因此都被通称为串口。串口通讯应用是工控人必须掌握的一个技能,几乎在每一个项目中都会用到,今天我们就来详细比较一下它们究竟有何区别。

关键字: 串口 协议

在嵌入式开发过程中,许多系统通常使用串口驱动来满足通信要求,但在实际应用中,使用SPI通信方式会更加高效和快捷。

关键字: 串口 驱动

串口WiFi模块作为新一代嵌入式WiFi模块,因其体积小、功耗低的特点,广泛应用于物联网、智能家居等领域。

关键字: 串口 WiFi模块 嵌入式
关闭