H41基于51单片机双机通讯proteus设计,keil程序,c语言,AT89C51
作者:单片机仿真   类别:51单片机仿真    日期:2026-11-05 14:02:33    阅读:12 次   消耗积分:0 分

H41 用两片 51 单片机做串口双机通讯:各自带 4×4 矩阵键盘和一位共阳数码管,三根线交叉相连,同一份程序烧两片,按哪个键对方立刻显示 0~F;顺带把 11.0592MHz 晶振与 9600 波特率的关系算清楚,附 Keil 程序、Proteus 仿真与设计报告。

H41基于51单片机双机 图1

01 设计任务

功能介绍:

使用两片 51 单片机做串口双机通讯,每片各带一套"键盘 + 数码管",要求如下:

1、双向通讯:两片单片机通过串口交叉连线(A 的 TXD 接 B 的 RXD,A 的 RXD 接 B 的 TXD,共地),谁都能发、谁都能收;

2、本机按键发送:4×4 矩阵键盘 16 个键对应 0~9、A~F,按下即把键值通过串口发给对方;

3、对方接收显示:收到数据后立刻在一位共阳数码管上显示出来(0~9、A~F);

4、同一份程序烧两片:主循环管发送、串口中断管接收,两边功能镜像对称,没有主机从机之分;

5、9600 波特率:串口方式 1、定时器方式 2 自动重装,TH1 = TL1 = 0xFD,在 11.0592MHz 晶振下精确 9600。

 特别注意:前面几十篇全是单片机的"独脚戏"——自己测、自己算、自己显示。这一篇是第一次让两台机器说话,而且只用了三根线(TX、RX、GND)。看起来简单,但有两个地方几乎人人都要栽一次:晶振频率(必须 11.0592MHz,12MHz 会乱码)和接线(TX/RX 必须交叉)。02、03 节把这两个坑讲透。


H41基于51单片机双机 图2

单片机双机通讯proteus仿真整机图

Keil工程代码界面

02 为什么晶振非得是 11.0592MHz

这是初学者最想不通的地方:明明 12MHz 是个整数、算定时器初值又方便,为什么串口通讯偏要用 11.0592 这么"怪"的频率?

因为串口对时间的准确性要求极高。先看方式 1 的波特率是怎么来的:

波特率由 2 的 SMOD 次方除以 32、再乘以定时器 1 的溢出率得到;而定时器 1 在方式 2(8 位自动重装)下,溢出率等于晶振频率除以 12 与「256 减 TH1」的乘积。

代入程序里的参数:TH1 = 0xFD = 253,所以 256 - 253 = 3,每次溢出只要 3 个机器周期。设 SMOD=0:

按这组参数代进去:溢出率是 307200,再除 32 正好落到 9600,一位不差。

换成 12MHz 呢?

同样代进 12MHz,溢出率是 333333.3,再除 32 得 10416.7,离 9600 差了 8.5%。

误差 8.5% 是什么概念? 串口接收方是在每一位的中间采样,容忍范围大约 ±5%。误差超了,采样点就会从"位中间"漂到"位的边上",收到的是上一半和下一半的混合——表现出来就是乱码、丢字节、显示成鬼字符。

11.0592 这个数字不是随便选的:11059200 = 9600 × 32 × 12 × 3,恰好能被常用波特率整除。它在 9600(÷3)、4800、19200、38400 下都能得出零误差的 TH1 值。所以凡是涉及串口通讯的 51 单片机系统,晶振一律 11.0592MHz——这条经验值得抄在本子上。

顺带说个自查方法:不确定自己的波特率对不对,就反算一遍——

反算的写法是把晶振频率除以 12、32 和波特率三者的乘积,再用 256 减掉它。

9600 波特率、11.0592MHz 代入:256 - (11059200/3686400) = 256 - 3 = 253 = 0xFD ✅

03 三行初始化,撑起整个通讯

Init_Uart() 里真正干活的只有三行:

串口设成方式 1 并允许接收,定时器 1 切到方式 2 自动重装,两个重装寄存器都装 0xFD,最后启动定时器、打开接收允许、开串口中断和总中断。

逐位拆开看 SCON = 0x50(二进制 0101 0000):

位名称值含义
SM0 SM1工作方式01方式 1:10 位异步收发(1 起始 + 8 数据 + 1 停止)
SM2多机通讯控制0不用多机通讯时置 0
REN接收允许1允许接收(不置 1 只能发不能收)
TB8/RB8第 9 位数据0方式 1 用不到
TI/RI收发中断标志0初始清零

方式 2 的定时器为什么必须自动重装? 波特率是由定时器溢出率决定的,如果定时器每次溢出后不自动重装,就需要软件在中断里重装初值——软件重装会有几个机器周期的延迟,累积起来波特率就漂了。方式 2 是硬件自动重装,溢出周期严格恒定,所以串口波特率发生器一律用方式 2。这也是为什么 TMOD |= 0x20 用的是 |=(不影响已经在用的定时器 0)。

接线必须交叉。 串口的命名是站在自己角度说的:TXD 是"我发出去",RXD 是"我收进来"。所以我发的要进你的收,你的发要进我的收:

A 机的发送脚 P3.1 接 B 机的接收脚 P3.0,A 机的接收脚 P3.0 接 B 机的发送脚 P3.1,另外用一根地线把两机的地连起来。

图上两台单片机的 P3.0/P3.1 就是这样对拉的两根长线。最容易犯的错是 TX 接 TX、RX 接 RX——那样两边都在"说话"、没人"听",仿真里表现就是按了键对方毫无反应。

04 发送:一个阻塞的 TX_data

发送函数只有四行,但是整个通讯的可靠基础:

把要发的字节写进发送缓冲,硬件自己开始一位一位往外移;接着原地等这一帧发完的标志,标志到了就清掉、准备下一帧。

关键在 while(!TI)。TI 是"发完一帧"的硬件标志:写 SBUF 只是把数据放进缓冲,硬件要一位一位地移出去(9600 波特率下 10 位约 1.04ms),移完了才置 TI。

这个等待为什么必要?如果不等待、连续往 SBUF 里写,后一个字节会把前一个覆盖掉(SBUF 只有一个字节的缓冲)。所以发送必须"发完一个、确认一个"。

代价是阻塞约 1ms。对这份程序来说完全可以接受:主循环里本来就有 Delay_1ms(10),按下按键后卡 1ms 谁都感觉不到。但在需要同时做别的事的场合(比如边通讯边测温),就该改成中断发送 + 环形缓冲——把要发的数据排队,TI 中断来了就发下一个。

TI=0 必须手动清,这是 51 的规矩:硬件置位、软件清零。忘了清,下一帧的 while(!TI) 会立刻通过(标志还是上一次的),数据就丢了。

05 接收:中断里一进一出

中断进来先判 RI,确认是接收引起的之后,把 SBUF 里的字节搬进一个临时变量,再拿它当索引去段码表取那一项,直接写 P0。

为什么接收必须用中断? 因为数据什么时候来是不确定的。如果主循环里轮询 if(RI),正在执行 Delay_1ms(10) 的那 10ms 里来的数据就可能被下一帧覆盖(RI 只有一个标志、SBUF 只有一个字节)。中断能保证"数据一到立刻搬走"。

中断里只做最少的事。 这段代码只做了三件事:清标志、读数据、送显示——不延时、不运算、不刷屏。这是中断服务程序的铁律:越短越好。一旦在中断里加个 Delay_1ms,主循环的按键响应就会明显发滞。

注意这里只判了 RI 没判 TI:因为发送用的是阻塞式 while(!TI),发送完成标志在主函数里就处理掉了;如果以后改成中断发送,这个 if 就得分两支(if(RI) 收、if(TI) 发)。

顺带一个隐患:P0 = table[temp] 里的 temp 直接当下标用,没有做范围判断。如果收到的数据大于 15(比如干扰导致的错帧),就会读到段码表外面去,显示一个随机字符。规范做法是先 if(temp < 16) 再显示。单字节通讯没有帧头、没有校验,属于"能跑但不健壮",做课程设计够用,做产品必须加协议。

06 矩阵键盘:逐行拉低找列

发送的内容来自 4×4 矩阵键盘(接 P1),扫描思路是逐行拉低、读列判断:

循环四次,每次只把其中一行拉低,写进键盘口再读回来比对;两者不一致就说明这一行有键按下了,随即取出高四位的列信息、结束扫描。

~(1<<i) 依次产生 1110、1101、1011、0111 四种行扫描码。哪一行被按下,那一行就会被按键短接到某一列,读回来高 4 位就不是全 1——低 4 位是我们驱动的行、高 4 位是读回来的列,所以 & 0xf0 取出列信息。

拿到"行号 i + 列编码"之后查表确定键值:

行号定下来后,再看列编码落在四个分支的哪一个,每个分支直接写死一个键值。

行号 0~3、列编码 0xb0/0xd0/0xe0/0x70 一共 16 种组合,正好对应 16 个键。这种"行列组合查表"和 H28 的列扫描、H37 的行反转本质相同,只是扫描方向反了——H28 先定列再定行,这里先定行再定列。

静态标志为 0 表示这次按下还没处理过,置 1 后再查表发键值;标志已经是 1 说明手还按着,直接把键值置成无效值跳过;四行扫完都没发现按键,说明手松开了,标志复位。

按住不放只发一次,避免对方屏幕疯狂刷同一个数字。这类"只在按键的边沿产生动作"的思路,和 H39 中断里的 temp_check 是一回事——按键是事件,不是电平。

主循环很简单,就是"读键 → 有键就发 → 歇 10ms":

while(1) 里 Get_Key() 的返回值存进 key_num,0xff 表示没键按下就什么都不做,有键才调 TX_data 送出去;每轮循环末尾统一走一次 10ms 延时。

那个 Delay_1ms(10) 兼了两个职:降低扫描频率(省 CPU)、给按键留出抖动时间。

07 排错清单

1、对方收到乱码/毫无反应:第一条永远是查晶振——必须 11.0592MHz,用 12MHz 时 9600 波特率误差 8.5%,必然乱码。仿真里双击晶振元件看频率属性;

2、两边都按键、两边都没反应:查 TX/RX 有没有接反。交叉接:A 的 TXD(3.1) → B 的 RXD(3.0)。TX 接 TX 就是"两个人都对着说、没人听";另外 GND 必须共地,只接两根信号线不共地也不行;

3、能显示但只有一边能收到:某一根的线没连上(Proteus 里线看着交叉了,实际没接到引脚上,拖动一下确认有连接点);

4、显示值总比按键值大/小一位:键值映射表与自己的键盘接线不符。table 段码表和 temp_num 键值表要跟实际行列接线对齐,换键盘就得重排这张表;

5、按住一个键,对方只显示一次:正常,key_down_flag 防连发。想连续发就把这个标志去掉(但会刷屏);

6、数码管显示是"横杠"或怪字符:段码表共阳/共阴用反了。这份 table 是 0xc0 开头的共阳表(0 亮),如果换成共阴数码管要改用 0x3f 开头那套(H42 就是共阴表,可以直接对照);

7、接收偶尔丢一帧:单字节裸通讯本身没有校验,干扰或时序紧的时候会丢。课程设计可接受;要稳就加帧头 + 校验和(比如 0xAA + 数据 + 校验),接收端按状态机收包。

08 总结

双机通讯这篇的核心就三句:

· 晶振决定成败:11.0592MHz 是串口的"标准嗓门",9600 波特率才能分出准确的每一位;

· 接线必须交叉:TXD 对 RXD,加上共地,三根线就能让两台机器对话;

· 发用阻塞、收用中断:发送等 TI 保证不覆盖,接收靠中断保证不丢帧。

把这三条吃透,串口就不再神秘。下一篇 H42 换了个玩法——不用矩阵键盘,改成一个按键循环递增发送,接收端除了数码管还加了两个 LED 做状态指示,可以对照着看"同一套串口骨架,怎么变出不同的应用"。

欢迎关注,持续更新!


09 文件日期修改

如果你想把文件修改为最新的日期,又不想新建工程,那么你可以参考下面的文章。

"如何不打开程序的情况下更改proteus和keil程序的日期?"

工具和仿真文件在一起,见下面的下载链接。


10 资料下载

全套资料包含 Keil 工程(C 语言源码 + hex 文件)、Proteus 仿真工程、设计报告、程序流程图、讲解视频,以及编译截图和仿真截图。下载后可直接打开运行,也可用于课程设计、毕业设计参考。

点我下载全套资料 →

也可以关注公众号「单片机仿真」,回复 H41 获取下载入口。公众号内还有 A / T / H 全系列 400 多个单片机仿真实例,回复「目录」可查看完整清单。

同类设计合集:H 系列单片机仿真合集目录(含设计报告、视频讲解、流程图);全部系列合集总目录

版权所有,转载本站文章请注明出处:proteus单片机
上一篇:H40基于51单片机微波炉控制器proteus设计,keil程序,c语言,AT89C51
下一篇:H41基于51单片机双机通讯proteus设计,keil程序,c语言,AT89C51
${comment['nickname']}   ${comment['createtime']}
  
       
${comment.content}
${reply.nickname} 回复 ${comment.nickname}    ${reply.createtime}
     
  
回复内容:${reply.content}