串口接收代码
050.6.6 串口接收代码
#include "reg52.h"
#include <intrins.h>
typedef unsigned char uchar;
typedef unsigned int uint;
uint count = 0;
uchar recv;
sbit LED1 = P1^0;
sbit LED2 = P1^1;
sbit LED3 = P1^2;
sbit LED4 = P1^3;
sbit BEEP = P1^6;
sbit JDQ1 = P2^0;
void Delay_xms(uint xms) //@11.0592MHz
{
uchar data i, j;
while (xms)
{
_nop_();
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
xms--;
}
}
void UART_Send_Byte(uchar send_byte)
{
SBUF = send_byte;
while (TI == 0);
TI = 0;
}
void UART_Send_Str(uchar *send_str)
{
while (*send_str != '\0')
{
UART_Send_Byte(*send_str++);
}
}
void main()
{
SCON = 0x50;
PCON &= 0x7F;
TMOD &= 0x0F;
TMOD |= 0x20;
TH1 = 0xFD;
TL1 = 0xFD;
TR1 = 1;
ES = 1;
EA = 1; // 总中断打开
while (1)
{
for (count = 0; count < 26; count++)
{
UART_Send_Byte('a' + count);
}
UART_Send_Byte('\n');
UART_Send_Str("i am wfeng!\r\n");
Delay_xms(1000);
}
}
/*****************
串口的中断处理函数
*****************/
void UART_Routine(void) interrupt 4
{
if (1 == RI)
{
RI = 0;
recv = SBUF;
switch (recv)
{
case 0x1:
LED1 = 0;
break;
case 0x2:
LED2 = 0;
break;
case 0x3:
LED3 = 0;
break;
case 0x4:
LED4 = 0;
break;
case 0x5:
P1 |= 0x0F;
break;
case 0x6:
BEEP = 0;
break;
case 0x7:
BEEP = 1;
break;
case 0x8:
JDQ1 = 0;
break;
case 0x9:
JDQ1 = 1;
break;
default:
break;
}
}
}
一、串口数据的接收
本节在串口发送程序的基础上增加接收功能:电脑通过串口助手发送一个命令字节,STC89C52在串口中断中读取该字节,再根据数值控制LED、蜂鸣器和外接继电器模块。
完整数据链路如下:
串口助手选择HEX发送命令
│
▼
USB数据 ─→ CH340N ─→ P3.0/RXD ─→ UART接收器 ─→ 接收SBUF
│
├→ 硬件置RI=1
▼
ES=1且EA=1,进入中断号4
│
▼
recv=SBUF,switch解析命令
│
┌──────────────────────────┼─────────────────────┐
▼ ▼ ▼
P1.0~P1.3 P1.6 P2.0
控制LED 控制蜂鸣器 控制继电器模块
串口接收和发送共用名为SBUF的特殊功能寄存器地址,但芯片内部实际上是两个不同的缓冲器:
CPU写SBUF ─→ 发送缓冲器 ─→ 发送移位寄存器 ─→ P3.1/TXD
CPU读SBUF ←─ 接收缓冲器 ←─ 接收移位寄存器 ←─ P3.0/RXD
因此SBUF=data表示发送,data=SBUF表示读取已经接收的数据。读写方向由硬件地址译码自动区分。
1.1 中断处理函数
接收中断从哪里产生
串口方式1的接收器先在RXD上检测空闲高电平到起始低电平的跳变,再用定时器1提供的16倍采样节拍对数据位进行采样。接收一帧有效数据后,硬件把8位数据装入接收SBUF,并把SCON.RI置1。
RXD波形:空闲1 │ START0 │ D0 │ D1 │ D2 │ D3 │ D4 │ D5 │ D6 │ D7 │ STOP1
│ │
└─ 检测起始跳变 在停止位中部完成判定
│
数据装入接收SBUF,RI=1 ───┘
RI是SCON的B0位,是接收中断请求标志,不是独立寄存器。硬件负责置1,软件负责清0:
if (RI == 1)
{
RI = 0;
recv = SBUF;
}
配套程序先清RI再立即读取SBUF。在9600波特下,一帧约需1.04ms,而这两条指令的执行时间远短于下一帧到达时间,因此能够正常工作。也常见先读取SBUF再清RI的写法;关键是进入中断后立即保存数据并及时清除标志,不能让RI长期保持为1。
ES=1和EA=1构成串口中断的两级允许开关:
$ \text{进入串口中断的条件}=(RI\lor TI)\land ES\land EA $
串口发送标志TI和接收标志RI共用中断号4、向量地址0x0023。所以进入UART_Routine后必须判断标志来源,不能认为每次进入都一定是接收完成。当前代码只在RI=1时执行接收分支;发送部分采用查询TI并在主程序中清零。
更完整的中断框架可以写成:
void UART_Routine(void) interrupt 4
{
if (RI == 1)
{
RI = 0;
recv = SBUF;
/* 处理接收数据 */
}
if (TI == 1)
{
TI = 0;
/* 中断发送模式下处理后续字节 */
}
}
如果发送仍完全采用UART_Send_Byte中的查询方式,TI由发送函数清除即可;但正式工程要避免让TI在已开启ES的情况下长期为1,否则CPU可能反复响应同一个串口中断请求。
读取数据并保存到变量
uchar recv;
recv用于保存一个8位接收字节。recv=SBUF执行后,即使接收硬件继续工作,程序也可以使用recv进行判断。经典8051串口通常没有多字节接收FIFO,如果中断处理不及时且RI没有清除,后续帧可能无法装入接收SBUF而丢失。
中断服务程序应尽量短,不适合在里面执行长延时、等待按键或阻塞发送大量字符串。当前switch只进行寄存器赋值,执行时间很短,适合作为中断内的简单控制;复杂任务可以在中断中只保存命令并设置标志,再交给主循环处理。
HEX发送与文本发送不是同一个字节
switch中的分支使用0x01到0x09,因此串口助手应选择HEX方式发送。例如在HEX输入框发送01,单片机收到的才是数值0x01。
| 串口助手操作 | 实际发送字节 | 能否匹配case 0x1 |
|---|---|---|
HEX方式输入01 | 0x01 | 能 |
文本方式输入字符1 | 0x31 | 不能 |
HEX方式输入31 | 0x31 | 不能 |
字符'1'的ASCII码是0x31,与数值1不同。如果希望同时支持文本命令,可以增加case '1':,或者在接收后把ASCII数字转换为数值:
if ((recv >= '0') && (recv <= '9'))
{
recv = recv - '0';
}
使用switch解析命令
switch(recv)根据接收字节跳转到相应case。每个分支末尾使用break,防止继续执行后面的分支:
| 命令字节 | 执行操作 | 控制电平 |
|---|---|---|
0x01 | 点亮LED1 | P1.0=0 |
0x02 | 点亮LED2 | P1.1=0 |
0x03 | 点亮LED3 | P1.2=0 |
0x04 | 点亮LED4 | P1.3=0 |
0x05 | 熄灭4个LED | P1.0~P1.3=1 |
0x06 | 蜂鸣器响 | P1.6=0 |
0x07 | 蜂鸣器停止 | P1.6=1 |
0x08 | 继电器模块吸合 | P2.0=0 |
0x09 | 继电器模块释放 | P2.0=1 |
| 其他值 | 不执行动作 | 进入default |
Keil的View → Templates Window可以打开代码模板窗口,再插入switch基本结构。模板只提供语法骨架,switch表达式、各个case值和实际控制语句仍需按功能填写。
LED引脚与低电平点亮原理
程序把4个板载LED定义在P1.0~P1.3:
sbit LED1 = P1^0;
sbit LED2 = P1^1;
sbit LED3 = P1^2;
sbit LED4 = P1^3;
开发板上的每一路LED均从+5V经LED和10kΩ限流电阻连接到单片机端口,因此单片机输出低电平时形成电流通路:
+5V ─→ LED ─→ 10kΩ限流电阻 ─→ P1.x输出低电平 ─→ 芯片内部下拉 ─→ GND
│
└─ 有电流通过,LED点亮
P1.x输出高电平:电阻两端电压很小,几乎无电流,LED熄灭
所以代码采用LED1=0点亮,而不是LED1=1。限流电阻用于限制LED电流,避免LED和单片机端口过流;电阻越大,电流越小、亮度通常越低。
命令0x05使用:
P1 |= 0x0F;
0x0F的二进制为0000 1111B,按位或会把P1寄存器的低4位全部置1,同时保持高4位当前值:
P1原值: b7 b6 b5 b4 b3 b2 b1 b0
0x0F: 0 0 0 0 1 1 1 1
按位或: b7 b6 b5 b4 1 1 1 1
└P1.0~P1.3全部输出高电平,4灯熄灭
它设置的是低4位B0~B3,不是单独设置“第4位”B4。与逐句执行LED1=LED2=LED3=LED4=1相比,按位操作更紧凑,并且不会改变P1.4~P1.7。
蜂鸣器电路组成与低电平有效原理
开发板蜂鸣器并不是由P1.6直接供电,而是通过S9012 PNP三极管控制。电路包含:
| 元件 | 连接与作用 |
|---|---|
Q1 S9012 | PNP三极管,作为高边开关并提供电流放大 |
R3 1kΩ | 串联在P1.6与三极管基极之间,限制基极电流 |
BELL1 | 板载蜂鸣器,导通供电后发声 |
C6 100nF | 连接在+5V和GND之间,抑制蜂鸣器开关造成的高频电源扰动 |
+5V
│
E ┌───┴───┐
P1.6 ── R3 1kΩ ───── B│ Q1 │ S9012 PNP高边开关
└───┬───┘
C
│
BELL1
│
GND
C6 100nF
+5V ──||── GND
当P1.6=1时,三极管基极与发射极都接近+5V,基极-发射极电压不足,Q1截止,蜂鸣器无供电而停止。设置P1.6=0后,基极被R3拉低,PNP三极管导通,电流路径为:
+5V → Q1发射极 → Q1集电极 → BELL1 → GND
因此:
BEEP = 0; // Q1导通,蜂鸣器响
BEEP = 1; // Q1截止,蜂鸣器停止
使用三极管的原因是蜂鸣器工作电流可能超过单片机IO适合直接承担的电流。IO只需提供较小的基极控制电流,就能控制较大的蜂鸣器电流,也减少蜂鸣器对芯片引脚的影响。
继电器模块的三根控制线
本实验使用外接继电器模块,控制端连接:
STC89C52/开发板 继电器模块控制端
P2.0 ───────────────────────→ IN(控制信号)
+5V ───────────────────────→ VCC(模块供电)
GND ───────────────────────→ GND(共同参考地)
sbit JDQ1=P2^0;必须与实际杜邦线连接一致。若把信号线接到P2.4,软件也必须定义为P2^4;软件写的是哪个端口位,硬件信号线就必须接到哪个引脚。
视频使用的模块表现为低电平触发:
JDQ1 = 0; // 输入有效,继电器吸合
JDQ1 = 1; // 输入无效,继电器释放
低电平触发继电器模块的典型内部结构如下,具体元件型号和是否带光耦以模块实物为准:
控制侧
P2.0/IN ─→ 输入限流电阻 ─→ 三极管或光耦驱动 ─→ 继电器线圈 ─→ +5V
│
线圈两端并联续流二极管
吸收断电时的反向感应电压
触点侧
公共端COM ──机械触点── 常开端NO
└─机械触点── 常闭端NC
各部分作用如下:
| 部分 | 作用 |
|---|---|
| 输入电阻 | 限制控制端电流,保护IO和驱动器件 |
| 三极管/光耦 | 用小控制电流驱动较大的线圈电流;光耦型模块还能提供一定信号隔离 |
| 继电器线圈 | 通电后产生磁场,拉动机械触点 |
| 续流二极管 | 在线圈断电时为感应电流提供通路,抑制高压尖峰 |
COM/NO/NC | 构成与单片机控制侧分开的被控回路触点 |
控制端GND必须与开发板GND建立共同参考,否则模块无法可靠判断P2.0是高还是低。继电器触点侧可能用于切换更高电压,但入门实验应只操作安全低电压负载;市电接线必须由具备资质和安全条件的人员完成,不能在通电状态下触摸或临时插拔。
完整接收控制流程
电脑发送HEX命令01~09
│
▼
RXD收到一帧 ─→ UART硬件装入SBUF并置RI
│
▼
CPU进入interrupt 4
│
▼
判断RI=1 ─→ 软件清RI ─→ recv=SBUF
│
▼
switch(recv)
├─ 01~04:拉低对应LED引脚
├─ 05:P1低4位置1,熄灭全部LED
├─ 06/07:拉低/拉高P1.6,控制蜂鸣器
├─ 08/09:拉低/拉高P2.0,控制继电器
└─ 其他值:default,不执行动作
编译前应确认变量名为RI而不是相似的误拼写,并检查每个sbit定义与原理图、杜邦线位置一致。Delay_xms(1000)放在主循环中用于降低主动发送数据的频率,也使此前“函数定义但未调用”的warning消失;它不能放进串口中断服务程序,否则会长时间阻塞其他中断和接收任务。
完成代码后,先关闭串口助手并烧写6.3串口接收工程,再打开串口助手,选择9600、8N1和HEX发送。依次发送01到09,观察LED、蜂鸣器和继电器是否与命令表一致。若某个外设无响应,应按“收到的字节值→switch分支→端口定义→实际连线→低电平有效电路”的顺序排查。