☰
串口收发与简单协议:单片机入门实战指南
2026/10/19 3:28:17 网站建设 项目流程

简介:面向单片机初学者的中微CMS8S6990串口通信工程包,围绕带简单协议的串口收发完整实现,帮助刚入门的朋友理解帧格式定义、数据解析与应答机制,并快速迁移到自己的外设通信场景中。压缩包共144个文件,约2.05MB,以h头文件与c源码为核心,辅以uvproj工程配置、hex烧录文件、lst/obj编译中间文件及A51启动文件,既能直接阅读源码逻辑,也可用于工程编译与下载验证。资源内包含timer、uart、adc、epwm、acmp、isr等模块文件,基本覆盖CMS8S6990常用外设初始化与中断处理流程,适合作为学习串口协议封装和单片机工程结构的参考样例。目前已有1554人学习下载,对希望在真实工程中练习串口收发、排查协议粘包或超时问题的初学者较有参考价值。

1. 串口像寄快递:先搞懂串口收发到底在做什么

很多零基础的朋友第一次听到「串口收发」这四个字,第一反应是:这不就是两根线互相传数据吗,有什么好学的?真去上手才发现,光是让电脑和单片机说上话,就能折腾一下午。我带你换个角度想:串口其实就是「寄快递」——单片机把数据打包,通过一根线寄出去,另一个设备收到后再拆开。问题在于,快递寄出去容易,拆的时候才知道麻烦:包裹有没有丢件?有没有被人调包?盒子里的东西是不是你本来要的那件?「串口收发加简单协议」这件事,就是给这份快递贴上单号、封好封口、加上验货单。这篇入门指南就是给你一条从零开始的路:先明白串口是什么,再懂为什么必须加协议,最后亲手跑通一个带协议的收发例子。适合完全没接触过单片机、看到寄存器就发怵的初学者,跟着走一遍,你会发现它并不玄。

2. 为什么要加协议:裸串口到底哪里不行

先说结论:不加协议的串口通信,就像往人群里扔纸飞机——你扔出去了,但对面接到的可能根本不是你那架,或者纸飞机飞到一半散架了。这一章把「协议」这件事讲透,你后面写代码才有底。

2.1 裸串口的三个老大难:边界、错位、丢包

串口通信最底层的机制就是一根线,按约定好的速度(波特率)一个比特一个比特地往外送。硬件层面它只负责「把字节发出去」「把字节收进来」,它不负责回答三个关键问题:

第一个问题是边界。发端连续发了0x01 0x02 0x03三个字节,收端怎么知道这是一条完整消息,而不是三条消息?如果发端一口气发了 100 个字节,收端怎么知道「到这里该停了」?裸串口完全不管这件事,收端只会傻乎乎地收到一个又一个字节,你自己得想办法给数据画上「这个包到此为止」的记号。

第二个问题是错位。串口是异步通信,靠起始位和停止位来对齐字节边界。一旦线路上有干扰、或者波特率两边差了一点点,收端可能从错误的比特开始读,收到的就是一堆乱码。更常见的情况是收发双方在「同一帧数据」里定义不一致,比如收端以为第一个字节是命令,发端其实先发的是长度,两边驴唇不对马嘴。

第三个问题是丢包和篡改。电气干扰、接线不良、缓冲区满了,都可能让字节在路上丢失。如果传输的数据是「开灯」「关灯」这种指令,丢一个字节可能指令就错了;如果是温度、距离这种数值,错一个比特,数据就完全失真。裸串口无法告诉你「这一帧数据是完整的、没坏的」。

这三个问题不是理论问题,是实际调板子时一定会撞上的。解决它们的办法就叫做「协议」。

2.2 协议是什么:一封快递的寄件标准

把串口通信想象成寄快递。你寄东西的时候,不是光把东西扔进邮筒就完事——你要写收件人地址、贴面单、封箱、称重。这些动作组合起来就是「寄件协议」。收件人拿到包裹先看面单:是不是寄给我的?再看外包装有没有破损,签收前还可以称一下重量,对不上就可以拒收。

通信协议做的事情一模一样。发端在原始数据外面包一圈「快递面单信息」:用特定的字节标记包裹从哪开始(帧头)、这个包裹有多长(长度)、里面装的是什么东西(命令)、以及一个用于验证没坏的总和(校验)。收端按同样的规则拆包:先找到帧头、再读长度、收齐数据、验一下校验和,全对才认为这包数据是完整的。

所以协议不是高深的理论,它就是通信双方提前商量好的一份核对清单。只要两边都按同一张清单打包和拆包,数据就算在半路被干扰,收端也能通过校验发现「这包是坏的」,宁可丢掉也不当成真数据用。

2.3 协议帧的四个零件:帧头、长度、校验、命令

一个最简单的协议帧通常由四类零件组成,我给初学者一个最常用的范式:

零件作用打个比方
帧头告诉收端「从这个字节开始算一帧」快递面单上的条形码起点
命令字说明这一帧想干什么面单上的「文件」或「易碎品」标记
数据长度说明后面跟多少字节的数据包裹上的重量
校验验证收到的内容有没有被改坏签收前检查外包装是否完好

帧头最常见的取值是像0xAA 0x55这种成对出现的固定字节,因为连续出现两个特殊字节的概率很低,收端不容易误判。数据长度告诉收端「接下来还要收 N 个字节才算完」,这是解决边界问题的关键。校验一般用累加和,就是把帧头、命令、长度、数据所有字节加一遍,取低 8 位发过去,收端自己也加一遍,两边不等就说明这一帧被改过了。

看到这里你可能已经明白了:所谓「串口收发加简单协议」,核心工作就是两件事——发端按规则组帧,收端按规则拆帧。接下来我们把这套东西落到一颗真实的单片机上。

3. 认识 CMS8S6990:零基础第一次面对这颗单片机

上一章讲了协议的思想,这一章带你认识实际的硬件载体。CMS8S6990 是一颗 8 位架构的单片机,这类芯片在入门阶段非常友好,因为它的外设简单、寄存器少,串口模块的逻辑也直白,正适合第一次接触「单片机 + 串口」的初学者。

3.1 一颗 8 位单片机,能干什么

CMS8S6990 这类 8 位 MCU,你可以把它想成一个功能单一但「叫得动」的小管家:它能读引脚电平、能控制引脚输出、能定时,还能通过串口和外面的设备通信。从学习串口这个目标出发,你要用到它的能力其实就三样:跑代码的 CPU、控制串口收发的外设、以及存放数据的寄存器。

寄存器是初学者最容易发怵的词,我说句实话:你不需要一个个背寄存器。入门阶段只需要知道,「串口发一个字节」这件事,本质就是往某个寄存器写一个值;「收到一个字节」就是串口模块帮你把收到的数据放到了某个寄存器里,你过去读一下就行。CMS8S6990 的串口外设帮你处理了最底层的波特率生成、起始位、停止位,你只需要关心上面那层「协议」怎么拆怎么组。

学这颗芯片的串口,我建议你先把它当成一个「能收发字节的黑匣子」:你写一个字节进去,它就从 TX 引脚送出去;RX 引脚收到字节,它就放在寄存器里等你取。协议逻辑、数据打包、校验计算全部是你用 C 代码在外面包的,芯片本身不关心这些。这种分层的思维,能让你在入门阶段少掉一大半头发。

3.2 从零起步要准备的四样东西

第一次上手之前,先确认手头的东西齐全,不然调线时少一根都抓狂。你需要准备:

一个 CMS8S6990 的开发板或者最小系统板,这是跑代码的地方。一个 USB 转 TTL 的小板,用来把单片机的串口信号转成电脑 USB 能认的信号。四根杜邦线,分别接电源、地、TX、RX。串口调试助手软件,装在你电脑上,用来和单片机对话。开发用的 IDE 与官方例程包,用来写代码、下载程序。这些东西都是做单片机入门通用的配置,第一次买的时候记得确认「USB 转 TTL 小板是不是 3.3V/5V 兼容」,CMS8S6990 的 IO 电平要看清,免得直接把芯片烧了。这算是入门最贵的踩坑位置。

3.3 第一个小时:先把串口助手里的数据调出来

拿到开发板之后,不要一上来就写协议,先做一次最小验证:让单片机上电后每隔一秒通过串口发一个0x01,然后用串口助手看能不能收到。这一步的意义是把「工具链、接线、串口配置」这三样打通,任何一个环节断掉,你后面写再多协议代码都白搭。

具体做法是:先用官方例程里的串口发送 Demo,或者按数据手册初始化串口,配置好波特率(比如 9600),主循环里调用发送函数。编译下载后,打开串口助手,选对 USB 转 TTL 对应的串口号,波特率设成和代码里一致,点打开。如果你能在接收区看到周期的01,恭喜,硬件链路通了。看到乱码,优先怀疑波特率;完全看不到,优先查 TX/RX 是否接反、板子是否共地。

这一步做完,你对「串口收发」就算有了手感:代码里写一个字节,电脑上就看到一个字节。接下来才轮到协议登场——因为你现在发的是一个孤零零的字节,还谈不上「一帧数据」。

4. 第一个能跑的例子:一套最简单的串口协议

这一章直接上代码。我们会定一个特别简单的协议帧格式,然后分别实现接收端的状态机解析和发送端的组帧函数。代码是通用的 C 语言风格示意,你把它套到 CMS8S6990 的工程里,替换掉底层的发送和中断入口函数即可。

4.1 先定协议:帧格式一页纸

我建议初学者用下面这个帧格式,够简单、够直观、也够验证所有关键知识点:

字节序内容说明
第 1 字节0xAA帧头 1
第 2 字节0x55帧头 2
第 3 字节CMD命令字,比如 0x01 表示读温度
第 4 字节LEN数据区长度,单位是字节
第 5 至 4+LEN 字节DATA真正要传的数据
最后 1 字节SUM前面所有字节的累加和,取低 8 位

举例:发送命令 0x01、数据是 0x10 0x20 两个字节,那么完整帧就是AA 55 01 02 10 20 72。校验和 0x72 怎么来的?0xAA + 0x55 + 0x01 + 0x02 + 0x10 + 0x20 = 0x172,取低 8 位就是 0x72。

这里有个小细节值得初学者注意:LEN 只统计 DATA 的字节数,不含帧头、命令、校验自己。这样收端才好算「还要再收几个字节」,不会把自己绕进去。协议定好后,收发两端都必须严格按这张表执行,这就是前面说的「提前商量好的清单」。

4.2 接收端代码:状态机一点一点啃

接收端最怕的是「一口气给你一长串字节,你不知道从哪下手」。破法就是状态机:每收到一个字节,就根据当前状态决定「怎么处理这个字节」,然后切到下一个状态。下面是核心逻辑的示意代码,在 CMS8S6990 的串口接收中断里调用on_uart_byte,每来一个字节进一次。

// 协议帧: AA 55 CMD LEN DATA[LEN] SUM #define FRAME_HEAD1 0xAA #define FRAME_HEAD2 0x55 #define MAX_DATA_LEN 32 static unsigned char recv_step = 0; // 当前解析到哪一步 static unsigned char recv_cmd = 0; // 命令字 static unsigned char recv_len = 0; // 数据长度 static unsigned char recv_data[MAX_DATA_LEN]; static unsigned char recv_cnt = 0; // 当前已收的数据字节数 static unsigned char recv_sum = 0; // 边收边累加的校验和 void on_uart_byte(unsigned char ch) // 每收到一个字节调用一次 { switch(recv_step) { case 0: // 等帧头1: 期望收到 0xAA if(ch == FRAME_HEAD1) { recv_step = 1; // 对上, 进入下一状态 } // 没对上就留在 0, 继续等 break; case 1: // 等帧头2: 期望收到 0x55 if(ch == FRAME_HEAD2) { recv_step = 2; } else if(ch != FRAME_HEAD1) { recv_step = 0; // 不是 0x55 也不是 0xAA, 重新等 } break; case 2: // 收命令字 recv_cmd = ch; recv_cnt = 0; // 数据计数器清零 recv_sum = FRAME_HEAD1 + FRAME_HEAD2 + ch; // 先把前面字节累加 recv_step = 3; break; case 3: // 收数据长度 recv_len = ch; recv_sum += ch; if(recv_len > MAX_DATA_LEN) { recv_step = 0; // 长度超过缓冲区, 整帧作废 } else if(recv_len == 0) { recv_step = 5; // 没有数据, 直接去收校验 } else { recv_step = 4; } break; case 4: // 收数据区 recv_data[recv_cnt] = ch; recv_sum += ch; recv_cnt++; if(recv_cnt >= recv_len) { recv_step = 5; // 数据收齐, 去收校验 } break; case 5: // 收校验 if(ch == (recv_sum & 0xFF)) { // 校验通过, 一帧完整数据拿到手 process_frame(recv_cmd, recv_data, recv_len); } recv_step = 0; // 无论对错, 重新等下一帧 break; } }

这段代码的骨架不难:状态 0 和状态 1 是等帧头,状态 2 收命令,状态 3 收长度,状态 4 收数据,状态 5 收校验。看着长,其实每个 case 只干一件事。关键参数有三个:recv_step是当前解析位置,recv_len是从帧里读出的长度,recv_sum是边收边算的累加和。

很多初学者第一次看这段代码会问:为什么不用数组把一整帧先存下来再统一解析?用状态机的好处是实时性强,来一个字节处理一个字节,不用等整帧到齐才开始动手,单片机缓冲区小也能扛住。CMS8S6990 的串口接收中断进来一个字节就调一次这个函数,天然匹配这种处理方式。校验通过后调用process_frame,它就是你自己的业务逻辑,比如判断recv_cmd是 0x01 就去读温度传感器。

4.3 发送端代码:组帧其实更简单

发送端比接收端好写得多,因为主动权在你手里,不用猜「下一个字节是什么」,只需按顺序把帧头、命令、长度、数据、校验一个个发出去即可。下面是组帧发送的示意代码:

void send_frame(unsigned char cmd, unsigned char *data, unsigned char len) { unsigned char sum = 0; unsigned char i; uart_send_byte(0xAA); // 帧头1 uart_send_byte(0x55); // 帧头2 uart_send_byte(cmd); // 命令字 uart_send_byte(len); // 数据长度 sum = 0xAA + 0x55 + cmd + len; // 校验从帧头开始累加 for(i = 0; i < len; i++) { uart_send_byte(data[i]); // 逐个发送数据字节 sum += data[i]; } uart_send_byte(sum & 0xFF); // 累加和取低8位, 作为校验字节 }

调用方式很直接。比如你想上报两个字节的传感器数据:

unsigned char sensor_data[2]; sensor_data[0] = 0x10; sensor_data[1] = 0x20; send_frame(0x01, sensor_data, 2);

发送端代码没什么坑,唯一需要注意的是校验和的计算范围和收端保持一致,发端从帧头开始加,收端也从帧头开始加,两边对齐就不会报错。

4.4 参数怎么调、失败看哪里

跑第一个例子时,参数集中在三处:波特率、帧头取值、缓冲区长度。波特率两边必须一致,CMS8S6990 的波特率是通过寄存器配置的,官方例程里一般有现成配置,初学者直接用默认值就行,别自己改,除非你搞清楚了时钟源。帧头用0xAA 0x55就好,别用0x00或0xFF这种容易被干扰、也容易出现连续相同值的字节。缓冲区长度MAX_DATA_LEN要大于你预计的最大数据长度,否则长度超限就被丢弃,表现出来就是「凑齐了长度结果整帧作废」。

调不通的时候,按顺序排查:第一步看串口助手有没有数据,没有就是接线或端口问题;第二步看数据是不是乱码,是就检查波特率;第三步看你发的帧是不是带校验,如果发的是裸数据,收端解析当然乱套。串口助手里尽量用 HEX 显示,别用字符显示,字符显示会把 0xAA 显示成乱码,让你误以为程序错了,实际上字节是对的。

5. 串口协议新手避坑指南:我替你踩过的五个坑

这一章直接给结论,每条都是初学者高频撞上的「翻车现场」,按「现象 → 原因 → 解决」写清楚,你遇到同类问题能少走两小时弯路。

5.1 现象:收到的全是乱码

如果你在串口助手里看到一串乱七八糟的符号,第一怀疑对象是波特率。发端配置成了 9600,收端开的是 115200,两边速度不一致,收端自然在错误的时刻采样,读出来的比特全是乱的。解决方法是把收发两端波特率设为一致,CMS8S6990 这边用官方例程的配置,串口助手那边下拉框选同一个值。还要确认一下文本模式和 HEX 模式,HEX 模式下 0xAA 会显示成AA,文本模式下可能显示成奇怪符号,那不一定是乱码。

5.2 现象:TX 和 RX 接反,完全没反应

很多初学者第一次接线,想当然地把单片机的 TX 接到 USB 转 TTL 的 TX 上,结果完全不通。原因很简单:串口通信是交叉接线的,一方的发送必须接另一方的接收。正确接法是单片机的 TX 接 USB 转 TTL 的 RX,单片机的 RX 接 USB 转 TTL 的 TX。另外别忘了共地——USB 转 TTL 的 GND 和开发板的 GND 必须连在一起,否则信号没有参考电平,也会表现为完全没反应。这两条是串口调不通的最大元凶。

5.3 现象:数据对,但校验老失败

跟着上面的代码写,帧头帧尾都对,数据也读出来了,就是process_frame不被调用,DEBUG 一看校验不通过。原因多半是你中途改了协议格式,比如增加了一个版本号字节,但校验和的计算范围还按老算法。解决方法是重新核对一下校验和的累加范围,记住一个原则:发端加几个字节,收端就加几个字节,一个都不能多一个都不能少。还有一种可能,帧头0xAA在数据区里也出现了,状态机把这个0xAA误认为新帧头,把后面的数据拆错了,这就是下一节要讲的问题。

5.4 现象:数据里出现帧头值,导致整帧被拆散

你传输的数据是传感器数值,取值范围刚好包含0xAA,收端在等数据时看到0xAA,可它还在状态 4(收数据),不会误判。真正的问题是「一帧刚解析完,下一帧的帧头还没等到,此时数据里来了一个0xAA」这种情况吗?其实状态机在第 0 步只会把0xAA当作可能的帧头,等第二个字节不是0x55就会重新等待,所以成对帧头本身已经降低了误判概率。但如果你用单个字节做帧头,误判就很容易发生。解决方法是坚持使用成对帧头,或者在数据长度较短时把帧头换成更少出现的值。这个问题属于「知道原因就不可怕」的类型。

5.5 现象:程序烧进去后第一次能跑,断电重启就失灵

这个问题看起来玄学,其实多半是烧录后没有重新上电,或者串口助手在单片机复位瞬间收到了半截数据,把缓冲区状态搞乱了。CMS8S6990 上电瞬间 RX 引脚可能处于不确定电平,串口模块可能误收一个字节,把状态机推到了状态 2 或状态 3,之后真正的新帧进来就被错误解析。解决方法是:状态机里加一个超时机制,超过比如 50ms 没收到下一个字节就强制回到状态 0,相当于给整帧解析加了一个「后悔药」。入门阶段可以先意识有这个问题,等跑通基础收发后再回来加超时。

6. 下一步学什么:从跑通到真正会用

到这里,你已经亲手跑通了一套带协议的串口收发,这是个实打实的里程碑。但「跑通」和「会用」之间还差几步,这一章给你一条循序渐进的路。

短期目标是把代码从「能跑」读到「能改」。你自己写的那几段代码,试着改几个地方:把最大数据长度从 32 改到 64,看看收端会不会出问题;把帧头改成0x5A 0xA5,验证收端逻辑;给协议加一种新的命令类型,比如 0x02 表示「写参数」。这些改动训练的是你修改协议的能力,比再抄十个例程都有用。改坏了不要怕,正好把上一章的排查方法练一遍。

中期目标给协议做加法。当前这套累加和校验能发现字节错误,但发现不了「字节顺序颠倒了」的问题。你可以研究 CRC 校验,它的检错能力比累加和强得多。还有转义帧的设计:当数据里出现和帧头相同的字节时,在发送端插入一个转义符,收端看到转义符就知道下一个字节是普通数据。这些概念初看有点绕,但都是真实通信里常用的办法,值得一个个试过去。

长期目标是用工具把串口数据「可视化」。串口助手只能看原始字节,数据稍微多一点人就盯不过来了。你可以试着写一个简单的上位机,用串口助手加文本日志重定向到文件,再做边收边校验的统计工具。当你把收发两端的数据画成曲线、看到校验失败次数的实时变化,你对这套协议的理解会上升一个台阶。到了这个阶段,CMS8S6990 的完整数据手册就有必要认真翻开读了,串口外设的寄存器、中断标志位,每一行都会变得亲切。

我自己第一次调串口协议时,卡在最简单的“为什么串口助手收到的是乱码”上面整整一个晚上,后来才发现是波特率差了一个零。从那以后我每次配置串口都强制走一遍五连查:波特率、TX/RX 交叉、共地、HEX 显示、校验和范围。这个习惯让我后来调各种模块都少吃了很多亏,希望这个五连查清单也能帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询