串口接收代码

Tutorial: 电子电路 Category: 嵌入式 Published: 2026-08-29 08:09:36 Views: 0 Likes: 0 Comments: 0

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 ───┘

RISCON的B0位,是接收中断请求标志,不是独立寄存器。硬件负责置1,软件负责清0:

if (RI == 1)
{
    RI = 0;
    recv = SBUF;
}

配套程序先清RI再立即读取SBUF。在9600波特下,一帧约需1.04ms,而这两条指令的执行时间远短于下一帧到达时间,因此能够正常工作。也常见先读取SBUF再清RI的写法;关键是进入中断后立即保存数据并及时清除标志,不能让RI长期保持为1。

ES=1EA=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中的分支使用0x010x09,因此串口助手应选择HEX方式发送。例如在HEX输入框发送01,单片机收到的才是数值0x01

串口助手操作实际发送字节能否匹配case 0x1
HEX方式输入010x01
文本方式输入字符10x31不能
HEX方式输入310x31不能

字符'1'的ASCII码是0x31,与数值1不同。如果希望同时支持文本命令,可以增加case '1':,或者在接收后把ASCII数字转换为数值:

if ((recv >= '0') && (recv <= '9'))
{
    recv = recv - '0';
}

使用switch解析命令

switch(recv)根据接收字节跳转到相应case。每个分支末尾使用break,防止继续执行后面的分支:

命令字节执行操作控制电平
0x01点亮LED1P1.0=0
0x02点亮LED2P1.1=0
0x03点亮LED3P1.2=0
0x04点亮LED4P1.3=0
0x05熄灭4个LEDP1.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 S9012PNP三极管,作为高边开关并提供电流放大
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发送。依次发送0109,观察LED、蜂鸣器和继电器是否与命令表一致。若某个外设无响应,应按“收到的字节值→switch分支→端口定义→实际连线→低电平有效电路”的顺序排查。