简介:这是一份基于51单片机与Proteus仿真的双机通信设计资料,包含仿真图、源代码与讲解视频,面向单片机初、中级学习者及课程设计人群,解决双机数据交互、串口通信、中断与PWM输出等典型问题。资源共38个文件,压缩包11.68MB,主要包含Keil工程文件、Proteus仿真工程、C源码与hex固件、电路截图与说明文档,另有exe格式的讲解视频,方便对照源码与仿真环境逐步复现。设计实现了单片机1发送正弦波采样值,通过按键选择频率,单片机2接收后用1602LCD显示字节数并回送应答,同时以PWM方式经有源RC滤波输出波形,虚拟示波器可观测结果;单片机1收到应答后数码管提示通信成功。目前已有185人学习浏览,资料结构清晰,文件夹按仿真工程、源程序、文档与视频分类,适合作为课程设计、毕业设计或竞赛培训的参考方案,也可基于现有代码扩展更多自定义通信功能。
1. 双机通信的Proteus仿真:从串口中断到波形还原
做单片机通信实验,最头疼的就是手边没有第二块板子。用Proteus仿真双机通信,好处是波形、时序、收发状态全都在同一屏里看得到。这套设计里,单片机1通过串口把正弦波采样值发给单片机2,单片机2用1602LCD显示收到的字节数,同时在P1.0用PWM方式还原波形,再接RC滤波给虚拟示波器显示。整条链路覆盖了UART收发、定时器T0/T1的两种不同用法、查表法生成波形、PWM占空比控制,以及双机握手协议。
这套设计的核心不在于“点灯”,而在于两个单片机之间如何约定通信格式、如何用中断保证实时性、如何把数字采样值变成模拟波形。适合正在学51单片机串口通信、或者准备课程设计的人群。下面按发送端、接收端、仿真验证三个层面拆开讲。
2. 串口通信基础与T1波特率发生器配置
2.1 为什么选工作模式1,而不是模式0或模式2
51单片机的串口有四种工作模式。模式0是同步移位寄存器,只能做8位数据的移位输入输出,不能用于双机异步通信。模式2和模式3都是9位UART,多出来的第9位可以做多机通信的地址帧标志,但对这个项目来说并不需要。模式1是8位UART,一帧10位:1位起始位、8位数据位、1位停止位,帧格式最干净,波特率由T1的溢出率决定,换算逻辑最直观。
这个设计要求“T1作为波特率发生器,使用工作模式1”,所以SCON寄存器要配置成0101 0000,也就是SM0=0、SM1=1、REN=1。REN必须置1,否则接收端根本收不到数据。
T1工作在模式2,也就是8位自动重装。之所以不用模式1(16位计数),是因为串口通信的波特率要求定时器溢出周期尽可能精确稳定,而模式1溢出后需要软件重新赋初值,赋值指令会引入几个机器周期的误差。模式2由硬件自动重装TH1到TL1,溢出周期完全确定。
2.2 波特率计算公式与初值表
模式1的波特率计算公式:
波特率 = (2^SMOD / 32) × (T1溢出率) T1溢出率 = 系统时钟频率 / (12 × (256 - TH1))合并简化后:
波特率 = (2^SMOD / 32) × (晶振频率 / (12 × (256 - TH1)))系统时钟频率这里指晶振频率除以12后的机器周期频率。SMOD是PCON寄存器的第7位,默认值为0。SMOD=0时波特率不分频倍增,SMOD=1时波特率翻倍。
常用晶振和波特率的TH1初值对照如下:
| 晶振频率 | 目标波特率 | SMOD | TH1初值 | 实际波特率 | 误差 |
|---|---|---|---|---|---|
| 11.0592MHz | 9600 | 0 | 0xFD | 9600 | 0% |
| 11.0592MHz | 4800 | 0 | 0xFA | 4800 | 0% |
| 12MHz | 9600 | 1 | 0xF9 | 8928 | 7.5% |
| 12MHz | 4800 | 0 | 0xF3 | 4807 | 0.15% |
注意12MHz晶振算9600波特率的误差。12MHz无法整除出精确的9600,误差会到7.5%,这个误差在短帧数据传输时问题不大,但连续大量收发时可能偶尔触发校验错误。Proteus仿真里如果波形出现偶发乱码,先把波特率换成4800试一下,往往就能定位到误差问题。
2.3 串口初始化代码
// UART初始化:模式1,8位数据,波特率9600(11.0592MHz晶振) void UART_Init(void) { SCON = 0x50; // 0101 0000:模式1,REN=1允许接收 TMOD &= 0x0F; // 只修改T1相关位,保留T0配置 TMOD |= 0x20; // T1工作模式2:8位自动重装 TH1 = 0xFD; // 波特率9600初值(11.0592MHz) TL1 = 0xFD; // 重装值 PCON = 0x00; // SMOD=0,波特率不倍增 ES = 1; // 使能串口中断 EA = 1; // 开总中断 TR1 = 1; // 启动T1 }这段初始化代码的顺序是有讲究的。先配置SCON确定串口工作方式,再配置TMOD确定T1的工作模式,然后设TH1和TL1的初值。如果先把TR1置1再赋值TH1,定时器会先按一个不确定的初值跑几个机器周期,这期间如果恰好有串口中断请求,帧头就丢了。ES和EA最后开,是因为串口中断标志RI和TI在上电时不可控,等寄存器全部配置完成后再开中断,可以避免误入中断服务函数。
3. 发送端实现:正弦波采样表、按键选频与应答显示
3.1 正弦波采样值怎么生成
发送端要发送“一个周期的正弦波采样值”。实际实现是预先算好一张正弦波采样值表,存到数组中,然后在主循环里循环发送。注意,这里的关键点是用查表法生成正弦波,而不是实时计算sin函数。51单片机做浮点sin运算要消耗几十个机器周期,实时算根本跟不上串口发送速率的节奏,而且代码体积和运行时间都不划算。
// 正弦波16点采样表,取值0~255,正好对应PWM占空比 // 计算公式:value = 127 + 127 * sin(2 * PI * i / 16) unsigned char code sin_table[16] = { 127, 176, 221, 249, 255, 249, 221, 176, 127, 78, 33, 5, 0, 5, 33, 78 };为什么采样值范围取0到255?因为接收端要用PWM方式输出波形,PWM的占空比寄存器通常就是8位,采样值直接映射到占空比,不需要再额外做量程变换。如果采样值范围取0到100,也可以,但后面每次赋值都要先乘2.55再做整数变换,多了一步运算。0到255直接赋值最简单。
采样点数量选16而不是32或64,是因为这个项目里采样值需要通过串口逐个字节发送,采样点越多,完整波形周期越长,按键切换频率时的响应就越迟钝。16点配合按键切换延时,波形视觉上足够平滑,数据量也适中。
3.2 按键选频用T0计数中断
设计要求“通过按键选择频率,该按键使用定时器T0计数方式中断实现”。这里用的不是查询按键电平,而是把按键接到T0的外部计数引脚(P3.4),让T0工作在计数模式。每按一次按键,相当于在P3.4引脚上产生一个下降沿,T0计数寄存器加1。当计数值溢出时触发T0中断,在中断服务函数里修改频率参数。
void Timer0_Init(void) { TMOD &= 0xF0; // 只修改T0相关位,保留T1配置 TMOD |= 0x05; // T0工作模式1:16位计数器,外部计数 TH0 = 0xFF; // 初值设为FFFB,接近溢出 TL0 = 0xFB; // 每计5个脉冲溢出一次 ET0 = 1; // 开启T0中断 EA = 1; // 开总中断 TR0 = 1; // 启动T0 } // 频率档位表,按键切换时依次循环 unsigned char code freq_table[4] = {1, 2, 4, 8}; unsigned char freq_index = 0; void Timer0_ISR(void) interrupt 1 { TH0 = 0xFF; // 重装初值,保证下次仍旧5个脉冲溢出 TL0 = 0xFB; freq_index++; // 切换频率档位 if (freq_index >= 4) freq_index = 0; }为什么要用“计数模式”而不是“定时器中断+按键扫描”?因为按键扫描需要主循环不断轮询引脚电平,一旦主循环里有串口发送的延时等待,按键响应就不及时。计数模式完全由硬件完成脉冲计数,不需要CPU干预,按5次触发一次中断,抖动也不会导致误触发。这个思路在处理低速外部事件时非常实用。
频率切换后的实际发送间隔,由主循环里的延时函数决定。比如基础延时是2ms,频率档位为1时每个采样点间隔2ms,完整周期32ms,约31Hz;档位为4时每个采样点间隔0.5ms,完整周期8ms,约125Hz。具体数值可按需要调整。
3.3 发送主循环与应答数码管
// 数码管段码表:共阴极,0~F unsigned char code seg_table[10] = { 0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F }; void main(void) { unsigned char i; UART_Init(); Timer0_Init(); while (1) { for (i = 0; i < 16; i++) { SBUF = sin_table[i]; // 发送一个采样值 while (TI == 0); // 等待发送完成 TI = 0; // 清发送中断标志 delay_ms(freq_table[freq_index]); // 根据当前频率延时 } if (ack_flag == 1) { P2 = seg_table[1]; // 数码管显示"1",表示应答成功 ack_flag = 0; } } }TI标志位是发送完成标志。发送一个字符前要先确认上次发送已完成,不能直接把SBUF连续赋值。while (TI == 0)是查询方式,也可以用中断方式在串口中断服务函数里置位标志,但查询方式在这个场景下更简洁,因为发送端的主循环本身就是按周期发送,不需要同时处理其他并发任务。
应答信号的接收是在串口中断里完成的。
void UART_ISR(void) interrupt 4 { unsigned char rx_data; if (RI) { rx_data = SBUF; // 读取数据 RI = 0; // 清接收标志 if (rx_data == 0xAA) { ack_flag = 1; // 设置应答标志,主循环里显示 } } }串口中断服务函数里只做最简单的置标志操作,不直接操作数码管、不调用延时函数。中断服务函数越短越好,这是51单片机中断编程的第一原则。
4. 接收端实现:1602LCD显示计数与PWM波形输出
4.1 接收端完整代码结构与串口中断
接收端的核心任务是三件事:接收串口数据、统计字节数、在LCD上实时显示;收到数据后回送应答信号;用定时器T0在P1.0引脚上输出PWM波形,波形占空比跟随收到的采样值变化。
unsigned char rx_counter = 0; // 收到的字节计数 unsigned char pwm_duty = 127; // 当前PWM占空比 void UART_ISR(void) interrupt 4 { if (RI) { pwm_duty = SBUF; // 读取采样值,同时作为PWM占空比 RI = 0; rx_counter++; // 字节计数加1 LCD_Show_Counter(rx_counter); // 显示接收到的字节数 SBUF = 0xAA; // 回送应答信号 while (TI == 0); TI = 0; } }这里有一个细节:回送应答信号放在串口中断服务函数里,意味着每收到一个字节就要回复一个应答。这会产生频繁的双向通信,但在这个场景下是合理的,因为发送端是按固定周期发数据的,接收端回复的0xAA是固定握手信号。
LCD_Show_Counter函数需要把二进制计数值转换成十进制数字,一位一位显示到LCD上:
void LCD_Show_Counter(unsigned int cnt) { unsigned char temp[3]; temp[0] = cnt / 100; // 百位 temp[1] = (cnt % 100) / 10; // 十位 temp[2] = cnt % 10; // 个位 LCD_Write_Char(0x80, temp[0] + '0'); // 第一行第一位 LCD_Write_Char(0x81, temp[1] + '0'); LCD_Write_Char(0x82, temp[2] + '0'); }4.2 1602LCD四线驱动与关键命令
1602LCD有两种连接方式:8线并口和4线并口。8线方式一次传输8位数据,占用P0口全部引脚;4线方式分两次传输高4位和低4位,只占4个引脚。这个设计里LCD要和P1.0的PWM输出、T0计数等外设共存,引脚资源紧张,用4线驱动更合理。
sbit RS = P2^0; // 寄存器选择:0=指令,1=数据 sbit RW = P2^1; // 读写选择:0=写,1=读 sbit EN = P2^2; // 使能信号 // 1602写命令 void LCD_Write_Cmd(unsigned char cmd) { P0 = cmd & 0xF0; // 先传高4位 RS = 0; RW = 0; EN = 1; delay_short(); EN = 0; P0 = (cmd << 4) & 0xF0; // 再传低4位 EN = 1; delay_short(); EN = 0; delay_long(); } // 1602写数据 void LCD_Write_Data(unsigned char dat) { P0 = dat & 0xF0; RS = 1; RW = 0; EN = 1; delay_short(); EN = 0; P0 = (dat << 4) & 0xF0; EN = 1; delay_short(); EN = 0; delay_long(); }使用4线模式时,初始化命令序列和8线模式不同。LCD在上电后默认是8线模式,必须先通过三次0x03指令切换到4线模式,然后再发送功能设置命令0x28(4线,2行,5x8点阵)。
void LCD_Init(void) { delay_ms(15); LCD_Write_Cmd(0x03); // 第一次发0x03 delay_ms(5); LCD_Write_Cmd(0x03); // 第二次 delay_ms(5); LCD_Write_Cmd(0x03); // 第三次 delay_ms(1); LCD_Write_Cmd(0x02); // 正式切换到4线模式 LCD_Write_Cmd(0x28); // 功能设置:4线、2行、5x8 LCD_Write_Cmd(0x08); // 关闭显示 LCD_Write_Cmd(0x01); // 清屏 LCD_Write_Cmd(0x06); // 光标右移 LCD_Write_Cmd(0x0C); // 开启显示,不显示光标 }每次调用LCD_Write_Cmd(0x03)时,实际上只用了P0口的高4位(P0.4到P0.7)与LCD的DB4到DB7连接。连续三次发0x03,是让LCD无论如何都能对齐到8位模式的指令接收状态,这是切换4线模式的标准做法,少一次都不行。
4.3 PWM方式输出正弦波与RC滤波器参数计算
接收端P1.0输出PWM波形。用定时器T0中断控制PWM周期和占空比:
void Timer0_Init(void) { TMOD &= 0xF0; TMOD |= 0x01; // T0工作模式1:16位定时器 TH0 = 0xFF; TL0 = 0x9C; // 100us定时初值(11.0592MHz) ET0 = 1; EA = 1; TR0 = 1; } void Timer0_ISR(void) interrupt 1 { static unsigned char pwm_count = 0; TH0 = 0xFF; TL0 = 0x9C; pwm_count++; if (pwm_count <= pwm_duty) P1_0 = 1; // 占空比时间内输出高电平 else P1_0 = 0; if (pwm_count >= 255) pwm_count = 0; }PWM的周期是255 × 100us = 25.5ms,约39Hz的PWM载波频率。PWM的频率越高,滤波后的波形越平滑。如果想提高载波频率,可以把100us定时改为50us,同时把采样值映射范围从255改为对应值,但注意中断频率越高,CPU在中断里花的时间占比就越大,会挤压主循环处理LCD刷新的时间。
RC低通滤波器的截止频率:
fc = 1 / (2 × π × R × C)设计目标是让载波频率被衰减,而正弦波信号频率通过。PWM载波39Hz,正弦波信号最高125Hz左右,这两个频率比较接近,一级RC滤波效果有限。常见做法是取R=10kΩ,C=1uF,此时fc约15.9Hz,对39Hz载波有约8dB的衰减,波形上能看到比较明显的纹波。想更平滑,就用两阶RC滤波:
第一阶:R1=4.7kΩ,C1=10uF 第二阶:R2=10kΩ,C2=1uF两阶滤波后,纹波明显减小,示波器上看到的正弦波形更干净。注意Proteus里虚拟示波器直接接在RC滤波器输出端,不要再接入其他负载,否则影响截止频率。
4.4 两边T0的不同配置对比
这个设计把T0在两端用出了完全不同的效果。发送端的T0是计数器模式(方式1,外部脉冲计数),接收端的T0是定时器模式(方式1,内部时钟计时)。两者初值和中断服务函数完全不一样,放在同一篇文章里对比看,非常有助于理解定时器/计数器模式切换的本质区别。
// 发送端T0:计数器,按键输入 TMOD |= 0x05; // 0000 0101,T0方式1计数模式 // 接收端T0:定时器,PWM输出 TMOD |= 0x01; // 0000 0001,T0方式1定时模式C/T(Counter/Timer)位决定了T0是对外部脉冲计数还是对内部机器周期计数。一个引脚P3.4,一个外部信号,T0的输入源完全不同。这个区别在Proteus仿真里看得特别清楚,发送端按按键时能看到TH0/TL0在硬件层面变化,接收端则是TH0/TL0按固定节奏自增。
5. 双机通信的Proteus仿真接线与波形验证技巧
5.1 电路连接要点
Proteus中搭建这个电路,关键连接关系如下:
| 连接项 | 发送端 | 连接 | 接收端 |
|---|---|---|---|
| UART TXD | P3.1 | —— 交叉连接 —— | P3.0 (RXD) |
| UART RXD | P3.0 | —— 交叉连接 —— | P3.1 (TXD) |
| 按键输入 | P3.4 | —— | 接外部按健至GND |
| PWM输出 | — | — | P1.0接RC滤波器 |
| 示波器 | — | RC滤波器输出 | 虚拟示波器CH1 |
两个单片机的TXD、RXD必须交叉连接。同名端相连是仿真里最容易犯的错误。GND必须共地,Proteus仿真里虽然不接GND也能跑,但真实硬件上不共地会导致通信失败。
按键接P3.4到GND之间,按下时产生下降沿脉冲,T0计数一次。TTL电平的UART信号,在Proteus里不需要MAX232做电平转换,因为两个单片机直接互联,逻辑电平一致。如果接PC串口,才需要MAX232。
5.2 COMPIM模块的用法说明
Proteus中如果想把这个仿真接到真实串口调试工具上调试,可以使用COMPIM组件。双击COMPIM打开属性面板,Physical port选择实际串口号(如COM1),Baud rate设为9600,Data bits为8,Parity为NONE,Stop bits为1,Flow control选none。COMPIM的TXD连接发送端P3.1,RXD连接接收端P3.0,同样交叉。
COMPIM的用处在于可以把Proteus仿真里的串口数据转发到真实串口,方便用PC上的串口助手观察数据流。仿真里跑通了,再下载到真实单片机,通信协议不需要改动。
5.3 常见现象与排查方式
现象一:示波器没有波形,但LCD在计数。说明接收端收到数据并正常处理,问题出在P1.0的PWM输出或RC滤波电路。检查P1.0在Proteus里有没有加电压探针,RC滤波器的电阻电容值有没有接反。
现象二:LCD显示乱码或者完全空白。先看发送端初始化是否正确,P0口上有没有加上拉电阻排(RESPACK-8)。51单片机P0口是开漏输出,没上拉的话高电平驱动能力不足,LCD收不到有效数据。
现象三:数码管一直没有显示“1”。说明接收端的应答信号没到发送端,或者发送端串口中断没收到。检查两个单片机之间P3.0和P3.1是否交叉正确,接收端的SBUF = 0xAA是否确实执行。
现象四:波形存在明显台阶感。正弦波采样点只有16个,RC滤波后波形本身就是阶梯状的,这是正常现象。加大采样表到32点会改善不少,但会占更多存储空间,发送周期也会变长。
5.4 双机通信的扩展验证方式
在Proteus仿真跑通之后,可以再做两件事验证通信可靠性。
第一,人为改一个错误波特率,比如把发送端TH1从0xFD改成0xFA,此时发送端实际波特率约为4800,接收端仍按9600接收,可以看到LCD上的字节计数开始跳动,收到的数据不再是有效正弦波值,PWM输出波形变成乱码。这个实验能直观看到波特率不匹配的后果。
第二,在接收端串口中断里加一个校验逻辑,检查收到的采样值是否在合理范围(0到255),超出则丢弃。真实硬件通信中,干扰可能让某个字节出错,丢弃非法数据能避免PWM输出毛刺。Proteus仿真里串口几乎不会出错,但真实线缆和电源噪声会让这个问题变得很现实。
本文还有配套的精品资源,点击获取