H42 用两片 51 单片机做数码管双机通讯:各带一个按键、两位数码管和两个 LED,串口交叉连线,每按一次把本地计数值发给对方(0~15 循环),对方收到即显示并用两个 LED 表示数值低两位;与 H41 共用同一套串口骨架,附 Keil 程序、Proteus 仿真与设计报告。

01 设计任务
功能介绍:
使用两片 51 单片机做串口双机通讯,本机按键触发、对方显示,要求如下:
1、双向数据传递:两片单片机串口交叉连线(TX 对 RX、RX 对 TX、共地),同一份程序烧两片,谁都能发谁都能收;
2、按键循环发送:本机只有一个按键,每按一次把本地计数值发出去,然后计数值加一(0~15 循环)——对方看到的是"你按到第几下";
3、接收端双重指示:数码管显示收到的数值,同时两个 LED 按数值的低两位显示四种组合(00 / 01 / 10 / 11);
4、共阴段码:table 从 0x3F 起(共阴,1 亮),与上一篇 H41 的 0xC0 起(共阳,0 亮)正好相反;
5、9600 波特率:串口方式 1、定时器方式 2 自动重装,TH1 = TL1 = 0xFD,晶振 11.0592MHz。
特别注意:上一篇 H41 用的是 4×4 矩阵键盘,按哪个键就发哪个键值——发的是"按键编码";这一篇换成一个按键,发的是"本地计数值":
键值不为 0 时先把当前计数值送出去,紧接着让计数值自增,加过 15 就回到 0。
别小看这个改动,两者的语义完全不同:发按键是"我按了什么",发计数是"我数到几了"。后者的接收端看到的是对方的一个状态,而不是一次事件——这就是遥测(telemetry)的雏形。理解了这一点,串口就不只是"传个字符",而是"两台机器同步状态"。



02 骨架没变:三行初始化照旧
如果你看过上一篇 H41,这段代码会非常眼熟——Init_Uart() 和 TX_data() 一个字都没改:
初始化里把串口设成方式 1 并开接收,定时器 1 切方式 2 自动重装、两个重装寄存器都装 0xFD,最后打开总中断、启动定时器;发送函数把字节写进发送缓冲,等发完标志置起后把它清掉。
这说明一件很重要的事:串口通讯是"标准件"。波特率算法、寄存器配置、发送握手,在所有 51 项目里都一样,抄过去就能用。真正要动脑的是"发什么、收到之后干什么"——这部分才因项目而异。
也正因为是标准件,那几个坑(晶振必须 11.0592MHz、TX/RX 必须交叉、发送必须等 TI)在每一篇里都要重新叮嘱一遍。这里只快速复述结论,推导过程见 H41:
· TH1 = 0xFD = 253 → 256-253 = 3 个机器周期溢出一次 → 11059200/(12×3)/32 = 9600 精确无误;
· 换 12MHz 晶振 → 12000000/(12×3)/32 ≈ 10417,误差 8.5%,超出串口约 ±5% 的容差,必乱码;
· 接线:A_TXD(P3.1) → B_RXD(P3.0),A_RXD(P3.0) ← B_TXD(P3.1),GND 必须共地。
图上两台机器之间那两根横穿全图的长线,就是这一对交叉信号线。
03 发送端:一个按键,一个计数器
H42 的键盘部分极简,就一个按键接 P1.7:
检测到引脚跳低后先复检一次电平,确认仍是低才返回 1、并记下这一次已经处理过;引脚回到高电平才把这条记录清掉,其余情况一律返回 0。
flag 是"已经处理过这次按下"的标记,保证按住不放只触发一次、松开才能再触发。5ms 延时是消抖,else if(K1==1) flag=0 是"松手复位"。
为什么 H42 用 5ms、H41 用 10ms? 都是经验值,机械按键抖动一般持续 5~20ms。延时太短消不干净(会连发两次),太长按键变迟钝。5~10ms 都是安全区间。
主循环每轮先读键,有键时调用发送函数把计数值送出去,然后计数值加一并做 0~15 的回绕,末尾统一延时 10ms。
注意先发后加的顺序:mode 从 0 开始,第一次按键发出去的是 0,然后 mode 变 1;第二次按发 1……第 16 次按发 15 然后回到 0。所以对方屏幕上的序列就是 0,1,2,...,15,0,1,...。
如果写成"先加后发",对方看到的就是 1,2,...,15,0,1——从 1 开始。这种差一位的问题,调试时最容易怀疑"串口丢帧了",其实是自己写错了顺序。发送和自增的先后,决定了对方看到的第一帧是什么,改之前先想清楚。
还有一个细节:mode 上限 15 是因为接收端用 table[mode] 查段码表,表里只有 16 项。发送端的取值范围必须和接收端的查表范围对齐,这是两个程序之间的"隐形契约"——单字节裸通讯没有类型检查,发个 20 过去,对方就查到表外面去了。
04 接收端:数码管 + 两个 LED 的双重指示
接收还是标准的中断写法,但这次收到数据后干了两件事:
中断里确认 RI 置起后清掉标志,把 SBUF 的字节取到临时变量,然后分两路用它:一路送去驱动数码管,一路对 4 取余、按余数给两个 LED 引脚赋四种高低组合。
第 ① 件是数码管:P0 直接输出段码。这里的 table 是共阴表:
十六个元素依次对应 0 到 F 的段码,从 0x3f 起排到 0x71。
0x3F = 二进制 0011 1111,低 6 位全 1(a~f 段亮、g 段灭)= 数字 0。共阴数码管是"1 点亮",所以段码里 1 越多越亮。对比 H41 的 0xC0(1100 0000,a~f 全 0 = 全灭,g 段 1 = 亮)——那是共阳表,0 点亮。
同样的数字 0,两套表的段码完全互补(0x3F 与 0xC0 逐位取反)。所以换数码管必须换段码表,接错极性的典型表现是:显示的数字缺段、或者每个数字都显示成一个"8"缺一块。
第 ② 件是两个 LED 显示数值的低两位,temp % 4 得到 0~3 四种状态:
| temp % 4 | LED1 | LED2 | 二进制含义 |
| 0 | 亮(0) | 灭(1) | 00 |
| 1 | 灭(1) | 亮(0) | 01 |
| 2 | 亮(0) | 亮(0) | 10 |
| 3 | 灭(1) | 灭(1) | 11 |
两个 IO 表达 4 种状态,正好是 2 bit 的信息量——这就是"用最少的引脚表达最多状态"的经典做法。实际项目里,设备的运行/待机/故障/报警四态、或者通讯链路的"正常/重发/超时/断开"四态,都可以用两个灯的组合表示,不占屏幕、不用额外器件。
不过这里有个逻辑上的小别扭:LED 亮灭和二进制位是反的(t==0 时 LED1 亮,而 00 的两位都是 0)。原因是 LED 低电平点亮(LED1=0 才亮),所以"亮"对应"0"。想让灯的组合直观反映二进制,把判断里的值取反就行:
最低位用位与取出来直接赋给第一个 LED,次低位同样用位与取出、右移一位后赋给第二个 LED。
两行搞定 4 种分支,而且灯灭=0、灯亮=1,和二进制一致。能用位运算就别写 if 链——这是这份程序可以改进的地方。
05 一个值得注意的地方:两位数码管同显
老实说,这份程序的显示部分有个"硬件比软件多走了一步"的地方:图上是两位一体的数码管,但程序里只有 P0 = table[temp],没有任何位选(扫描)代码。
这意味着两件事:
1. 如果两位的位选线接的是固定电平(都选通),那两位会同时显示同一个数字(比如收到 5,屏上显示 55);
2. 想让两位显示不同内容(比如左边显示十位、右边显示个位),必须加动态扫描——按 H23(74HC138 位选 + 段码)、H26(5 根位选线)、H37(三极管位选)的写法,用定时器每几毫秒切一位。
看懂这一点,就明白"数码管显示"这个词在不同项目里含金量完全不同:一位数码管只要送段码,多位数码管就必须"送段码 + 选位 + 快速轮换"三件事配合。后面如果你要把这个设计升级成"显示两位数字",第一步不是改 table,而是先把位选扫描补上。
06 中断里该做什么、不该做什么
两篇通讯程序(H41、H42)的接收中断都只做了三件事:清标志、取数据、输出。H42 多了 LED 判断,但依然没有延时、没有循环、没有等待。
这是刻意守的纪律。中断服务程序里最忌讳两件事:
· 长时间阻塞:一个 Delay_1ms 就会让主循环的按键扫描变迟钝,还会影响串口接收下一帧的时机;
· 调用会"等"的函数:比如 TX_data 里的 while(!TI) 就不能放进接收中断——一旦在中断里发数据、又等发送完成,整个系统就卡在那 1ms 里了。
所以正确的分工是:中断只做"搬运",复杂处理(判断、计算、显示刷新)放到主循环。H42 里 LED 判断只有几行位运算,放中断里没问题;但如果收到数据后要查表、算 CRC、刷整屏 LCD,那就该在中断里只置一个"收到新数据"的标志,让主循环去处理。
顺带提醒一个这份程序没做、但产品里必须做的检查:
取到字节后先判它是否小于 16,小于才去查段码表显示,否则这一帧直接丢掉。
单字节裸通讯没有帧头、没有校验,一旦线路干扰或对方发了超范围的值,table[temp] 就会读到表外。课程设计可以不管,做成产品必须加。
07 排错清单
1、对方乱码 / 收到怪字符:先看晶振是不是 11.0592MHz。12MHz 下 9600 波特率误差 8.5%,串口必乱;Proteus 里双击晶振看属性;
2、完全没反应:查 TX/RX 是否交叉、是否共地。TX 接 TX 是新手第一坑;
3、显示的数字缺段 / 所有数字都像"8"少一块:段码表共阳/共阴用反了。这份是共阴表(0x3F 起),接共阳数码管要换成 0xC0 起那套(H41 的表可直接抄);
4、按一次键,对方跳了好几个数:flag 防连发失效(比如 static 丢了),或者 Delay_1ms(5) 消抖被去掉;
5、对方从 1 开始数、不是从 0 开始:发送和自增的顺序反了,见 03 节;
6、两位数码管显示两个一样的数字:正常——程序没有位选扫描,两位同时选通(见 05 节)。想显示不同内容要加动态扫描;
7、LED 组合看起来和二进制反着:LED 低电平点亮,t==0 时灯反而亮。用 LED1 = t & 0x01; 这种位运算写法更直观(见 04 节)。
08 总结
H41 和 H42 是同一套串口骨架的两个应用,放在一起看特别清楚:
| H41 双机通讯 | H42 数码管双机通讯 | |
| 发送源 | 4×4 矩阵键盘,16 键 | 单按键 + 本地计数器 |
| 发送内容 | 按键编码(一次事件) | 计数值(一个状态) |
| 接收表现 | 一位共阳数码管 | 两位数码管 + 两个 LED(低 2 位) |
| 段码表 | 共阳(0xC0 起) | 共阴(0x3F 起) |
| 初始化/发送/接收 | 完全相同 | 完全相同 |
通讯层是标准件,业务层才是差异——记住这句话,以后遇到"双机通讯"类的题目,代码框架可以直接搬,只需要改"发什么"和"收到后怎么表现"。
再往下走,串口能干的事还有很多:加帧头校验做可靠传输、一发多收(多机通讯用 SM2 和第 9 位)、跟电脑上位机通讯(串口助手看数据)。这些都是在今天这套三行初始化的基础上长出来的。
欢迎关注,持续更新!
09 文件日期修改
如果你想把文件修改为最新的日期,又不想新建工程,那么你可以参考下面的文章。
"如何不打开程序的情况下更改proteus和keil程序的日期?"
工具和仿真文件在一起,见下面的下载链接。
10 资料下载
全套资料包含 Keil 工程(C 语言源码 + hex 文件)、Proteus 仿真工程、设计报告、程序流程图、讲解视频,以及编译截图和仿真截图。下载后可直接打开运行,也可用于课程设计、毕业设计参考。
也可以关注公众号「单片机仿真」,回复 H42 获取下载入口。公众号内还有 A / T / H 全系列 400 多个单片机仿真实例,回复「目录」可查看完整清单。

