51单片机双机通信实战:UART串口通信与Proteus仿真工程详解

51单片机双机通信实战:UART串口通信与Proteus仿真工程详解 简介基于51单片机的双机通信项目演示甲机按键控制两机LED按全灭、LED1亮、LED2亮、全亮顺序循环点亮乙机按键则向甲机依次发送0-9数字并由甲机数码管实时显示覆盖了串口通信、按键扫描和数码管驱动等常见单片机应用场景。压缩包共51个文件包含Proteus仿真工程DSN/DBK、Keil源代码工程uvproj/C/hex、原理图SchDoc/PDF、流程图bmp、元件清单xlsx及功能介绍文档大小仅1.05MB目录结构清晰仿真、源码、图纸分层存放方便按需查阅。目前已有241人学习下载。借助仿真文件可无硬件直接运行验证双机交互效果结合原理图与流程图能快速理解通信协议和电路设计源代码附带详细注释和构建配置适合51单片机初学者进阶也可直接作为课程设计、毕业设计的参考方案。 解压这个工程后先别急着点开仿真.DSN。表面上是数码管、LED和按键在动实际上考验的是两片51之间的串口链路甲机按键切换LED状态乙机LED要同步变化乙机按键发数字甲机数码管要立刻显示。这两件事合起来就是一次完整的一主一从双机通信。很多51课设卡在“发过去的字节收不到”或者“收到的是乱码”本质是TX/RX交叉、波特率初值、中断处理三个环节没串起来。这个包里的Proteus仿真、原理图、流程图和两份Keil工程正好补上了这条链路。如果你是准备课程设计答辩或正从单机程序往通信程序过渡这份资源能当可复现的起点。下面按硬件连接、协议约定、代码实现和联调验证的顺序拆开讲。2. 51双机通信的硬件链路TX/RX交叉与LED、数码管的端口分配2.1 为什么两端必须TX/RX交叉并共地51单片机自带的UART工作在TTL电平发送端TXD输出逻辑0时约为0V逻辑1时接近电源电压。接收端RXD同样按这个电平判断。所以点对点数据传输只需要三条线甲机TXD接到乙机RXD甲机RXD接到乙机TXD两边的GND接在一起。这里最容易出错的是把TX/RX直连导致两边都在同一根线上发送接收端什么也收不到。从硬件拓扑看这就是一个双机串行网络的最简形态。协议层上通信是异步的双方各自以约定的波特率对电平变化采样不需要额外时钟线。也正因如此波特率一旦不一致接收端拿到的就是乱码。Proteus仿真里GND不接也能仿真成功因为仿真模型把地当作公共参考点做成实物时共地必须检查否则电平参考不一致数据就会漂移。包内的原理图Sheet1.SchDoc对应Altium Designer工程如果不想装AD直接看工程里导出的PDF版原理图也可以。画图时除了串口两条线还要注意电源引脚和复位引脚Proteus里的单片机模型默认不上电也能工作但实物按此接线则完全不行。2.2 端口映射与LED驱动方式包内源码里甲机和乙机的端口分配并不是随意来的。甲机P1接4个LEDP2接共阳数码管的段码P3.2接按键P3.0和P3.1分别作为RXD和TXD。乙机P1也接4个LEDP3.3接按键P3.0和P3.1与甲机交叉连接。这样分配的原因是P1口内部有上拉电阻做输出时拉低能力比拉高能力强LED适合做成低电平点亮。我按低电平点亮的思路定义状态映射0x0F是全灭0x0E是LED1亮0x0D是LED2亮0x03是全亮。四个状态对应两个LED的组合和题目描述完全一致。数码管如果选共阳段码表就是0xC0、0xF9、0xA4这些反码形式如果选共阴需要把段码取反。Proteus原工程里用的是共阳数码管打开原理图PDF能看到公共端接VCC、段码经电阻到P2。模块端口/接法关键参数4个LEDP1.0-P1.3接限流电阻到VCC330Ω~470Ω数码管段码P2.0-P2.7公共端接VCC段电阻220Ω甲机按键K1P3.2到GND加10nF电容或软件消抖乙机按键K2P3.3到GND同上晶振XTAL1/XTAL211.0592MHz两个30pF电容2.3 物料清单里容易被忽略的参数打开包里的元件清单.xlsx元件数量并不多但有几个参数会直接影响实物调试。晶振频率一定要选11.0592MHz不是为了玄学而是为了9600波特率下定时器初值TH10xFD没有累积误差。如果换成12MHz串口波特率误差约2.1%短帧可能侥幸能通但做双机通信建议直接用11.0592MHz。第二个容易被忽略的是复位电路。仿真里复位往往是软件初始状态但实物上电若不复位单片机会从随机PC地址开始跑。常见做法是10μF电解电容接RST到VCC10kΩ电阻接RST到GND。另外双机共地后如果甲机用独立电源供电乙机也用独立电源供电两个电源的地必须接在一起否则UART电平参考不同数据完全不可靠。3. 指令协议设计一个字节同时承载LED状态和数字数据3.1 帧格式约定这个工程的通信内容只有两种LED状态命令0xF0~0xF3和数字数据0x00~0x09。如果为每个状态定义一个独立命令接收端代码会变成一长串if else如果直接把数字0-9发过去接收端又分不清这个字节是数据还是命令。最简单的方法是保留字节的最高位作为类型标识。发送值含义接收方动作0x00-0x09乙机发送的数字甲机数码管显示对应数字0xF0LED全灭甲乙机LED全灭0xF1LED1亮甲乙机LED1点亮0xF2LED2亮甲乙机LED2点亮0xF3LED全亮甲乙机LED全亮甲机按下按钮时向乙机发送对应的0xF0~0xF3同时更新本机LED。乙机收到后根据“高4位是否为0xF”区分命令再取低两位设置LED状态。乙机按下按钮时把0-9循环发送甲机在串口中断里判断“小于10”就刷新数码管。这样双机复用同一条串口链路协议上却互不干扰。这里特意没有用ASCII码比如发送00x30而不是0x00。如果发送ASCII接收端要先减0x30才能还原数字还浪费了帧空间。课程设计时直接传二进制数字更直观Proteus虚拟终端里切到Hex模式也能看得很清楚。3.2 甲机代码按键状态机与串口发送甲机需要完成两件事按下K1时切换LED状态并发送命令串口中断接收乙机发来的数字并刷新数码管。main.c里可以这样组织#include reg51.h #define LED_PORT P1 #define SEG_PORT P2 sbit KEY1 P3^2; typedef unsigned char u8; typedef unsigned int u16; u8 code seg_code[10] { 0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90 }; u8 code led_state_map[4] { 0x0F, // 全灭 0x0E, // LED1亮 0x0D, // LED2亮 0x03 // 全亮 }; void delay_ms(u16 t) { u16 i, j; for (i 0; i t; i) for (j 0; j 123; j); } void UART_Init(void) { SCON 0x50; // 串口模式18位数据允许接收 TMOD 0x0F; // 清空定时器1控制位 TMOD | 0x20; // 定时器1工作于8位自动重装模式 TH1 0xFD; // 11.0592MHz下获得9600波特率 TL1 0xFD; TR1 1; ES 1; EA 1; } void UART_SendByte(u8 dat) { SBUF dat; while (TI 0); TI 0; } void main(void) { u8 state 0; LED_PORT led_state_map[0]; SEG_PORT seg_code[0]; UART_Init(); while (1) { if (KEY1 0) { delay_ms(10); if (KEY1 0) { while (KEY1 0); // 等待释放避免一次按压多次触发 state (state 1) % 4; LED_PORT led_state_map[state]; UART_SendByte(0xF0 | state); // 0xF0~0xF3 } } } } void UART_ISR(void) interrupt 4 { u8 rx; if (RI) { RI 0; rx SBUF; if (rx 10) { SEG_PORT seg_code[rx]; } } if (TI) { TI 0; // 发送完成中断必须清标志 } }这段代码里led_state_map数组的顺序直接对应“全灭、LED1亮、LED2亮、全亮”初始状态显示全灭。按键检测先延迟10ms消抖再等待按键释放这样每按一次只会切换一个状态。UART_SendByte是阻塞发送先写SBUF等TI置1后清零不会和中断接收互相干扰因为发送和接收使用不同的硬件缓冲路径。3.3 乙机代码循环发送数字与LED命令接收乙机的逻辑相反按键发送0-9串口中断接收LED命令并修改P1。因为乙机不显示数码管所以不需要段码表只需要LED映射表和发送函数#include reg51.h #define LED_PORT P1 sbit KEY2 P3^3; typedef unsigned char u8; typedef unsigned int u16; u8 code led_state_map[4] { 0x0F, 0x0E, 0x0D, 0x03 }; u8 send_num 0; void delay_ms(u16 t) { u16 i, j; for (i 0; i t; i) for (j 0; j 123; j); } void UART_Init(void) { SCON 0x50; TMOD 0x0F; TMOD | 0x20; TH1 0xFD; TL1 0xFD; TR1 1; ES 1; EA 1; } void UART_SendByte(u8 dat) { SBUF dat; while (TI 0); TI 0; } void main(void) { LED_PORT led_state_map[0]; UART_Init(); while (1) { if (KEY2 0) { delay_ms(10); if (KEY2 0) { while (KEY2 0); UART_SendByte(send_num); // 发送0~9 send_num (send_num 1) % 10; } } } } void UART_ISR(void) interrupt 4 { u8 rx; if (RI) { RI 0; rx SBUF; if ((rx 4) 0x0F) { LED_PORT led_state_map[rx 0x03]; } } if (TI) { TI 0; } }注意两个单片机的中断服务函数都同时清RI和TI。UART_ISR是串口中断向量4RI和TI共用这个入口如果只清RI不改TI发送完成标志一直置1中断会被反复触发程序就会卡在空转里。这是初学者最容易忽略的问题。3.4 为什么接收放在中断函数里而不是主循环如果主循环里直接查询RI代码会写成while (RI 0); rx SBUF;这在单机运行里没问题但双机通信时CPU在等待接收期间就无法扫描按键。比如乙机主循环正在等数字发送此时甲机发来LED命令乙机要等到当前循环空闲才处理响应就会变慢。把接收放到中断里等于让串口硬件具备异步通知能力。乙机按键发送数字和接收LED命令是两个独立事件一个在主循环里处理一个在中断里处理互不阻塞。51的串口接收缓冲只有1字节如果主循环长时间不做别的事下一帧到来时上一帧会被覆盖所以中断处理能最大程度降低丢帧概率。4. Proteus仿真与Keil联调加载hex、波特率初值、串口波形排查4.1 打开双机仿真工程并加载两份hex用Proteus打开仿真.DSN后能看到两个AT89C51芯片甲机旁边是数码管和LED乙机旁边是LED和按键。运行前要确认每颗芯片都加载了正确的固件双击甲机芯片在Program File里选择甲程序目录下的template.hex双击乙机芯片选择乙程序目录下的template.hex。如果改了源码需要先在Keil里重新编译再确认Output选项里的Create HEX File已勾选。这里有个细节包内两份Keil工程名都叫template但所在目录不同分别对应甲机和乙机。改代码时不要改错工程。仿真运行时甲机按下K1两边的LED应该按“全灭→LED1亮→LED2亮→全亮”循环乙机每按一次K2甲机数码管从0跳到1一直到9后再回到0。仿真不受烧录器限制这是Proteus最大的优势。但仿真不能替代实物验证因为仿真模型不会反映输出引脚驱动能力不足、按键抖动毛刺、电源纹波这些问题。4.2 波特率初值的计算方式串口模式1的波特率由定时器1提供计算公式为波特率 (2^SMOD / 32) × (fosc / (12 × (256 - TH1)))。SMOD默认为0fosc取11.0592MHz代入TH10xFD即253算出来为(1/32) × (11059200 / (12 × 3)) 9600。这也是为什么代码里TH1和TL1都写0xFD。晶振频率目标波特率TH1初值实际波特率误差11.0592MHz96000xFD0%12MHz96000xFD约2.1%12MHz48000xF9约0.16%如果手头只有12MHz晶振建议把波特率降到4800误差更小。双机通信时两端必须用一样的晶振频率和TH1初值否则接收端采到的位序会偏移。4.3 仿真中的常见故障排查现象可能原因排查方法甲机按键后LED不动端口接反或LED驱动方向不对检查P1是否高电平灭、低电平亮乙机LED不跟随甲机TX/RX没有交叉或hex加载错芯片核对TXD→RXD、RXD→TXD数码管显示乱码段码表与共阳/共阴不匹配共阳用0xC0系列共阴取反按键按一下跳多个状态没有消抖或没有等待释放增加delay_ms和while释放检测通信完全无响应波特率不一致或GND未共地统一晶振为11.0592MHz检查仿真GND有一个很隐蔽的坑是Proteus中如果晶振频率设置成12MHz而代码按11.0592MHz计算TH1仿真里显示几乎正常但用虚拟终端观察会发现字符错位。原因在于Proteus的串口模型对波特率误差比真实芯片更敏感所以仿真工程和Keil工程里都要把晶振频率改成11.0592MHz。4.4 用虚拟终端和示波器观察串口波形Proteus左侧工具栏里放一个Virtual Terminal把TXD引脚接上去运行后能看到甲机发送的数据。虚拟终端默认按ASCII显示所以0xF0这类十六进制字节会显示成乱码需要右键虚拟终端选择Hex Display模式。示波器接在TXD引脚上按下K1时会看到一串低电平起始位加8个数据位加停止位的波形每个位宽约104μs。观察波形时要确认起始位是下降沿因为UART空闲时是高电平发送字节先拉低一个位时间接收端靠这个下降沿对齐采样。如果示波器上看到两个连续下降沿之间没有明显高电平恢复说明数据位全为0或波特率不对需要回到波特率计算检查。5. 逻辑分析仪验证时序并把协议升级成带帧头的格式5.1 用逻辑分析仪抓一帧9600波特率的数据手头有逻辑分析仪时比示波器更好用。把通道接在甲机TXD采样率设为1MHz以上触发方式选下降沿波特率设为9600抓一次K1按下的事件。一帧数据包含1位起始位低电平、8位数据位最低位在前、1位停止位高电平总耗时约10个位时间约1.04ms。以发送0xF1为例二进制是11110001数据位从低位到高位依次是1、0、0、0、1、1、1、1。逻辑分析软件里应该能看到起始位低电平后第1个数据位是高第2到第4个数据位是低剩余是高。如果看到的波形相反说明发送端代码或电平极性有问题检查是否误用了取反后的段码表。5.2 把单字节协议升级为带帧头和校验的版本原工程一字节通信在课程设计范围内够用但批量传输时无法防止干扰。简单升级是先发0xAA帧头再发命令字节最后发校验和接收端只在帧头匹配后才解析数据。发送函数可以写成void UART_SendFrame(u8 cmd) { u8 sum cmd; SBUF 0xAA; while (!TI); TI 0; SBUF cmd; while (!TI); TI 0; SBUF sum; while (!TI); TI 0; }接收端判断第一个字节是0xAA后才进入帧解析状态第二个字节写入缓存第三个字节用于验算。这样的好处是数据里出现任意值都不会引起误动作因为接收方只在帧头对齐后才会更新状态。如果需要传输两字节以上的数据可以把校验和改为对整帧求和取低8位或者直接用CRC8代码复杂度增加不多但实用性更强。本文还有配套的精品资源点击获取