资讯详情

资讯详情

蓝桥杯单片机串口通信实战:从波特率配置到中断收发完整解析

串口通信这个模块在蓝桥杯单片机组的赛题里属于那种“不考反而奇怪”的考点。不管是省赛还是国赛串口几乎都会出现要么让单片机把采集到的温度、电压发给电脑要么让电脑通过串口助手发一条指令来控制板上的LED、数码管、蜂鸣器。CT107D是官方使用的竞赛平台串口相关的硬件和代码套路都很固定一旦把波特率、寄存器、中断这三个点吃透这类题基本就是送分。这篇内容写给自己动手备赛的同学尤其是那种“抄官方例程能点亮但换个波特率就懵、一进中断就乱”的初学者。我会从板子上的物理连线开始讲一直讲到能直接上赛场的完整代码框架最后把比赛现场常见的翻车点也帮你理一遍。只要跟着走一遍串口对你来说就不再是玄学。1. 赛题视角串口通信为什么绕不开1.1 高频考点背后的逻辑蓝桥杯的单片机赛题表面上考的是某个具体外设实际上考的是“单片机系统与外界的交互能力”。LED、数码管、按键这些东西是单向的要么输入要么输出但串口是双向的既能往外发数据也能收外部指令天然适合做综合题的“枢纽”。所以你在赛题里会看到串口和DS18B20温度采集、PCF8591电压采集、继电器控制、PWM调速等模块组合在一起串口负责把数据上报或者接收上位机的控制命令。从出题角度想串口题有个天然优势判分容易。评卷时不需要额外的仪器上位机发一条指令看板上现象对不对或者单片机主动发一串数据看内容正不正确。这就决定了串口题在省赛里经常出现在“第二道大题”里的某个小问而在国赛里往往作为整套系统的通信主干。1.2 串口的“一头多能”串口在赛题里通常承担三类任务。第一类是状态上报比如定时发送“TEMP25.6”或者按键按下时上报键值。第二类是远程控制比如电脑发0x01亮灯、0x02关灯单片机收到后执行。第三类是参数配置比如上位机下发一个数字作为PWM占空比或者设置定时器的初值。这三类都建立在同一个基础上可靠地收发单个字节并且能解析出完整的命令帧。很多选手备考时喜欢死记官方驱动里的初始化函数这没问题但换个芯片型号、换个晶振频率、换个波特率就容易“失灵”。真正需要理解的就三件事串口硬件怎么接的、波特率怎么算出来的、中断收发怎么配合。这三个点通透了串口题拿分是必然的。2. 硬件基础CT107D上的串口到底连在哪2.1 核心器件与信号链路CT107D的主控是STC15F2K60S2这是一颗增强型8051内核的单片机指令集兼容传统51但速度更快、资源更多。板载的串口转换芯片是CH340它负责把电脑USB口发出的信号转换成单片机认识的TTL电平信号。USB线插到开发板上电脑端会识别出一个虚拟COM口单片机端则通过P3.0RXD和P3.1TXD完成数据收发。信号流向很简单电脑发送数据时USB信号经过CH340变成TTL电平从CH340的TXD脚输出送到单片机的RXD引脚P3.0单片机往外发数据时P3.1TXD输出送到CH340的RXD脚再转成USB信号给电脑。注意这里是交叉连接这是串口通信的老规矩发送接接收接收接发送。2.2 板载跳线与连接细节CT107D板上有几个跳线帽需要留意。比如J13跳线控制LED的电源如果拔掉了LED操作会失效串口相关的跳线如果松动也会出现“电脑发了但单片机收不到”的诡异现象。上电之前先把所有跳线按默认位置插好尤其是涉及板载供电和数据通路的跳线别小看这一步比赛现场因为跳线松脱浪费半小时的人不在少数。另外一个认知需要纠正CT107D的串口通信走的是USB线不需要外接USB转TTL模块也不需要接RS232电平转换芯片。很多同学第一次看到串口题下意识去找“串口线”和DB9接口其实这块板子一根USB线全搞定。下载程序用的是这条线串口调试用的还是这条线比老式开发板方便很多。2.3 电脑端和单片机的握手电脑端使用串口助手时需要选择正确的COM口号。CH340的驱动装好之后在设备管理器里能看到“USB-SERIAL CH340”字样后面跟着COM编号。如果插上板子后没反应先检查驱动再换一根数据线——这里有个坑有些USB线只能充电不能传数据下载程序失败、串口无响应的原因可能就是线不对。串口助手的参数设置要和单片机端完全一致。默认情况下CT107D官方例程用的是9600波特率、8位数据、无校验、1位停止位也就是常说的8N1。不要开流控RTS和CTS都不要勾。只要波特率不一致发出来的数据必然乱码这个现象也是最常见的串口故障。3. 原理拆解波特率、寄存器与数据帧3.1 异步通信的数据帧长什么样串口通信是异步串行通信异步的意思是没有独立的时钟线收发双方靠约定好的波特率对齐节奏。每个字节发送时先发一个起始位低电平然后从低位到高位依次发8个数据位最后发一个停止位高电平。接收方在起始位的下降沿开始采样按波特率定时读取每个bit的电平拼出一个完整的字节。这就好比两个人约好“每秒说一个字”发话方按节奏吐字听话方按同样的节奏记字。只要节奏不一致听到的内容就会错位。所以波特率的精度直接决定通信是否可靠。这也是为什么很多工程板子喜欢用11.0592MHz晶振而不是12MHz——后者虽然便宜但算出来的波特率会带误差。3.2 波特率的本质与计算方法STC15F2K60S2的串口1工作在方式1时波特率由定时器1的溢出率决定。经典公式是波特率 (2^SMOD / 32) × 定时器1溢出率SMOD是PCON寄存器最高位通常设成0。定时器1工作在方式2也就是8位自动重装溢出率的计算公式为定时器1溢出率 定时器计数频率 / (256 - TH1)这里要分清楚STC15和传统51的区别。传统51的定时器固定12分频而STC15可以通过AUXR寄存器的bit6控制定时器1是否12分频。官方例程默认把AUXR的bit6清0让定时器1按12分频工作也即计数频率 晶振频率 / 12。以CT107D常见的11.0592MHz晶振、9600波特率为例。计数频率 11059200 / 12 921600 HzTH1取0xFD也就是253256 - 253 3所以溢出率 921600 / 3 307200 Hz波特率 307200 / 32 9600整个过程刚好整除误差为0。这就是官方例程里TH1和TL1都写0xFD的原因。如果换用12MHz晶振算9600结果会是 12000000 / 12 / 3 / 32 10416.67和9600差了大约8.5%这种误差在连续字节传输时必然乱码。如果需要115200波特率可以把AUXR的bit6置1让定时器1工作在1T模式。这时计数频率等于晶振频率同样取TH10xFD则115200 11059200 / (32 × (256 - 253)) 11059200 / 96 115200依然整除。所以在STC15上11.0592MHz晶振配TH10xFD12T模式是96001T模式是115200这个规律很好记。3.3 关键寄存器逐个说清楚串口1涉及的核心寄存器主要有SCON、SBUF、PCON、TMOD、TH1/TL1、AUXR、IE。逐个过一遍SCON是串口控制寄存器方式1的配置值是0x50。二进制0101 0000SM00、SM11表示方式18位UART波特率可变REN1表示允许接收。低两位在方式1里不用保持0即可。SBUF是串口缓冲寄存器物理上是两个写SBUF就是发送数据读SBUF就是接收数据。这个“读写地址相同但物理独立”的机制是51内核的特色理解成邮箱就行你投信和收信用的是同一个窗口但信件不会混。PCON寄存器里的SMOD位控制波特率加倍默认0不用改。TMOD控制定时器模式方式2对应高四位为0010也就是TMOD | 0x20同时注意不要影响定时器0的配置所以先TMOD 0x0F清掉高四位再设置。AUXR是STC15扩展的特殊功能寄存器。官方初始化代码里的AUXR 0xBF是把bit6清零让定时器1按12分频工作AUXR 0xFE是保证串口1的波特率发生器选择定时器1而不是定时器2。这个寄存器的存在是STC15和传统51配置串口时最大的区别很多人移植代码失败就是漏了这一步。IE寄存器控制总中断和串口中断。EA总开关ES是串口1中断允许位。初始化时还要记得ET10关闭定时器1的中断因为这里只需要定时器1提供波特率时钟不需要它产生中断。如果ET1忘记关而程序里又没有定时器1的中断服务函数单片机就会在溢出后跳到一个空的中断入口程序跑飞现象极其诡异。4. 代码落地初始化、收发与中断框架4.1 初始化函数模板下面这段代码是CT107D官方风格的串口1初始化适用于9600波特率、11.0592MHz晶振void uart_init(void) { SCON 0x50; // 串口1方式1允许接收 AUXR 0xBF; // 定时器1按12分频工作 AUXR 0xFE; // 串口1选择定时器1作为波特率发生器 TMOD 0x0F; // 清空定时器1的模式位 TMOD | 0x20; // 定时器1设为模式28位自动重装 TL1 0xFD; // 波特率初值 TH1 0xFD; // 自动重装值 ET1 0; // 禁止定时器1中断 TR1 1; // 启动定时器1 }如果想要115200波特率只需要把AUXR 0xBF改成AUXR | 0x40让定时器1工作在1T模式其余不变。这组参数我已经在板子上实测过配合串口助手收发数字、字符串都没问题。4.2 单字节发送与字符串发送发送一个字节很简单把数据放进SBUF然后等待TI标志置1即可。但这里有一个非常经典的坑如果开启了串口中断ES而且中断服务函数里对TI做了清0处理那么发送函数里while(!TI)会永远等不到TI置1因为TI刚置1就被中断服务函数清掉了。解决办法是发送期间临时关闭串口中断发送完再恢复void uart_send_byte(unsigned char dat) { ES 0; // 临时关闭串口中断 SBUF dat; // 写入发送缓冲 while (!TI); // 等待发送完成 TI 0; // 清发送完成标志 ES 1; // 恢复串口中断 } void uart_send_string(unsigned char *s) { while (*s) { uart_send_byte(*s); } }有些同学的代码里发送函数没有临时关闭ES结果主循环跑到发送字符串时卡死还以为是波特率错了。实际上就是中断把TI抢先清掉的问题。如果你没有开启串口中断或者中断里只清RI不清TI那发送函数可以不关ES但为了稳妥我建议始终保留这个临时关ES的操作。4.3 中断接收与环形缓冲区接收数据推荐用中断方式不要在while循环里轮询RI标志因为主循环往往还要处理数码管刷新、按键扫描这些任务轮询很容易丢字节。基本原理是串口收到一个字节后硬件自动把数据放进SBUF并触发中断在中断服务函数里把SBUF读出来存到缓冲区。缓冲区可以做成最简单的环形队列#define RX_BUF_SIZE 16 unsigned char rx_buf[RX_BUF_SIZE]; unsigned char rx_head 0; unsigned char rx_tail 0; void uart_isr() interrupt 4 { if (RI) { RI 0; rx_buf[rx_tail] SBUF; if (rx_tail RX_BUF_SIZE) { rx_tail 0; } } if (TI) { TI 0; } }rx_head表示主循环读到哪了rx_tail表示中断写到哪了。两者相等时缓冲区为空rx_tail追上rx_head时说明缓冲区满了但赛题里的命令帧一般只有几个字节16字节足够用。如果真想写严谨一点可以在满了之后丢弃旧数据或停止接收但对竞赛来说没必要。主循环里只需要做一件事只要缓冲区不为空就取出一个字节交给解析函数。void main(void) { uart_init(); EA 1; ES 1; uart_send_string(Uart OK\r\n); while (1) { while (rx_head ! rx_tail) { unsigned char d rx_buf[rx_head]; if (rx_head RX_BUF_SIZE) { rx_head 0; } parse_byte(d); } // 这里继续做数码管刷新、按键检测等任务 } }这个结构的好处是中断只负责快速收数据耗时较长的命令解析放在主循环里不会因为中断服务函数执行时间过长而丢下一帧数据。5. 赛题实战一条串口指令控制板载外设5.1 一个典型的综合任务怎么拆赛题里最常见的串口综合任务是电脑通过串口发送命令帧单片机解析后控制LED、蜂鸣器、数码管或者回传采集到的数据。我拆一个简化版示例任务要求上位机发送5字节帧第一字节0xAA作为帧头第二字节0x55作为同步字第三字节命令字第四字节参数第五字节为校验和。命令字0x01表示点亮第N个LED0x02表示全部熄灭0x03表示上报“TEMP25.6”字符串。拆题的时候先把两部分分开一部分是串口底层负责把字节收进来并还原出完整的帧另一部分是业务层根据命令字去操作外设。千万不要在中断里直接写业务逻辑否则命令解析、数据回传、LED操作全都挤在中断里代码混乱而且容易丢字节。5.2 帧协议与状态机解析帧协议就是收发双方约定好的数据格式。0xAA、0x55起定位作用校验和用来确认数据传输过程中没有出错。校验和用简单的异或就行校验和 0xAA ^ 0x55 ^ 命令字 ^ 参数。这种校验方式防不了复杂干扰但对竞赛场景完全够用。解析用状态机比用缓冲区攒一整包再判断要稳得多。状态机的好处是帧与帧之间的间隔时间随意哪怕命令一个字节一个字节地来也能正确拼出完整帧#define FRAME_HEAD1 0xAA #define FRAME_HEAD2 0x55 unsigned char rx_step 0; unsigned char rx_cmd 0; unsigned char rx_param 0; unsigned char rx_check 0; void parse_byte(unsigned char d) { switch (rx_step) { case 0: if (d FRAME_HEAD1) { rx_step 1; } break; case 1: if (d FRAME_HEAD2) { rx_step 2; } else if (d ! FRAME_HEAD1) { rx_step 0; } break; case 2: rx_cmd d; rx_step 3; break; case 3: rx_param d; rx_step 4; break; case 4: rx_check d; if ((FRAME_HEAD1 ^ FRAME_HEAD2 ^ rx_cmd ^ rx_param) rx_check) { process_cmd(rx_cmd, rx_param); } rx_step 0; break; default: rx_step 0; break; } }这个状态机里有一个细节值得注意在等待第二个帧头时如果收到的不是0x55要再判断一下是不是0xAA。这是因为数据流中间可能有残缺的帧如果收到一个0xAA之后紧跟着另一个0xAA而你直接把状态归零就会漏掉一组可能的帧头。5.3 主循环里的事件分发process_cmd实现具体的业务逻辑代码很直观void process_cmd(unsigned char cmd, unsigned char param) { switch (cmd) { case 0x01: if (param 1 param 8) { P2 (P2 0x1F) | 0x80; // 选通LED锁存器 P0 ~(0x01 (param - 1)); // 低电平点亮 P2 0x1F; // 关闭锁存器 } break; case 0x02: P2 (P2 0x1F) | 0x80; P0 0xFF; // 全灭 P2 0x1F; break; case 0x03: if (param 0) { uart_send_string(TEMP25.6\r\n); } break; default: uart_send_string(ERR\r\n); break; } }这里操作LED时打开锁存器、操作完立刻关闭是一个好习惯。CT107D的P0口同时连接多个外设的锁存器如果操作完不关闭P0上的残留数据可能会串到其他外设导致蜂鸣器乱响、数码管乱跳。整个流程跑起来之后的效果是串口助手发送AA 55 01 03 57校验和可以自己算板上第3个LED点亮发送AA 55 02 00 58所有LED熄灭发送AA 55 03 00 59串口助手收到TEMP25.6。这个示例基本覆盖了串口题的所有核心要素帧格式设计、状态机解析、外设联动、数据回传。6. 调试技巧与避坑经验6.1 把串口当成调试利器串口不光是赛题要求更是调试神器。程序跑飞了、变量值不对、外设没反应最直接的办法就是在关键位置加一句uart_send_string把状态打出来。比如初始化之后发一个“Uart OK”接收中断里收到字节后转发回串口就可以立刻判断底层收发是否正常。我在备赛时习惯把串口发送函数定义成一条“调试总线”在按键扫描、ADC采集、温度读取这些关键节点都埋上报点。比赛时如果某一项功能没实现先不急着查硬件看串口有没有输出能快速缩小问题范围。6.2 高频故障的排查顺序串口出问题时按下面的顺序排查能省很多时间现象大概率原因检查方法完全收不到COM口号不对、驱动没装、USB线只供电不传数据检查设备管理器换线重插收得到但乱码波特率不一致、晶振频率和代码不匹配确认9600 vs 115200确认11.0592MHz发不出去TXD/RXD接反、P3.1被占用用官方例程回发测试能发不能收SCON里REN没置1或ES没开或中断里没清RI检查SCON0x50检查IE寄存器发送卡死发送函数和TI中断冲突发送函数里临时关ES程序跑飞ET1没关定时器1中断没服务函数检查ET10帧解析不稳定帧头检测状态机没处理“连续帧头”按5.2的状态机写法这里面“乱码”还有一个容易被忽略的来源串口助手的数据位、停止位设置不对。如果上位机那边配成了7位数据而单片机是8位收到的内容就会错位。每次连接串口助手的参数都确认一遍不要想当然。6.3 赛前准备与现场节奏赛前一定要把自己的串口初始化函数、发送函数、接收缓冲区、状态机解析函数整理成一套独立模板放在工程里随时调用。不要到了赛场上才开始“回忆”官方例程怎么写。模板准备好之后找几道历年的串口真题把命令帧、回传数据、外设联动全部验证一遍重点是验证各种波特率下的收发稳定性。现场还有一个很小的细节每次重新下载程序后单片机是自动复位的串口助手不用重新打开但要注意发送数据前先确认串口助手的“打开串口”按钮是开启状态。另外比赛时如果题目要求串口上报数据记得在字符串末尾加上\r\n换行评卷系统或者串口助手会自动换行也方便人工观察输出。我在实际备赛过程中踩过最深的坑是中断里直接写命令解析导致的高速收帧丢字节。后来把中断改成只往环形缓冲区塞数据解析挪到主循环问题立刻消失。这个结构看似多了一道工序但对赛题这种连续多发几帧的场景非常有效。串口题真正难的地方从来不是背代码而是理解数据从电脑到引脚、从引脚到寄存器、从寄存器到中断、从中断到缓冲区、再从缓冲区到业务逻辑的完整链路。这串链路捋顺了串口就是整张卷子里最稳定拿分的那道题。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →