串口代码发送_1

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

048.6.4 串口代码发送(1)

#include "reg52.h"
#include <intrins.h>

typedef unsigned char uchar;
typedef unsigned int uint;

uint count = 0;

sbit LED1 = P1^0;
sbit LED2 = P1^1;

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++)
        {
//          SBUF = 'a' + count;
//          while (TI == 0);
//          TI = 0;
            UART_Send_Byte('a' + count);
        }

//      SBUF = '\n';
//      while (TI == 0);       // while (!TI);
//      TI = 0;
        UART_Send_Byte('\n');
        UART_Send_Byte('\n');
        UART_Send_Str("i am wfeng!\r\n");
    }
}

/*****************
串口的中断处理函数
*****************/
void UART_Routine(void) interrupt 4
{

}

一、串口代码编写

本节从串口被软件占用的常见问题入手,完成STC89C52串口方式1的初始化,并尝试通过SBUF发送字符。代码的核心不只是“把字符写进寄存器”,还要保证写入速度不超过UART硬件发送一帧所需的速度。

整个发送链路可以先概括为:

11.0592MHz系统时钟
        │
        ▼
定时器1模式2产生溢出节拍 ─→ UART分频电路 ─→ 9600波特发送时钟
                                                │
CPU写SBUF ─→ 发送缓冲器 ─→ 发送移位寄存器 ─────┴→ P3.1/TXD
                                  │
                                  └→ 一帧发送到规定时刻后,TI=1

其中定时器1只提供时间基准,UART硬件负责依次输出起始位、8个数据位和停止位。程序不需要在定时器中断里逐位发送数据。

1.1 问题解答

串口助手打开后,为什么可能烧写失败

Windows中的串口通常以独占方式打开。同一个COM端口已经被串口助手占用时,STC下载工具无法再次打开它,便可能提示“设备打开失败”或类似错误。

串口助手 ──已经占用COM端口──┐
                            ├─ 同一时刻发生资源冲突
下载工具 ──尝试打开COM端口──┘

处理步骤如下:

  1. 关闭串口助手中的“打开串口”状态;如果仍不确定,直接退出串口助手。
  2. 在下载工具中重新选择正确的COM端口。
  3. 重新点击下载,再按开发板要求重新上电或复位。
  4. 下载完成后,再打开串口助手进行收发测试。

关闭串口助手不是串口协议本身的要求,而是为了释放电脑上的COM端口。若使用两个不同的USB转串口设备,并且对应不同COM端口,则可以分别被不同软件打开。

串口助手最小化后在哪里

部分串口软件最小化后不会继续显示在任务栏,而是进入Windows右下角通知区域。图标可能直接显示,也可能收进“显示隐藏的图标”小三角中。此时再次从桌面启动软件,可能形成两个程序实例。

为什么再次打开软件会提示串口不存在或打开失败

常见原因不是COM端口真的消失,而是先启动的实例仍在后台并占用了端口。应先检查任务栏和通知区域,关闭正在占用端口的实例,再保留一个串口助手重新打开。

还可以在设备管理器中确认以下信息:

检查项正常情况异常时的处理方向
COM端口号与串口助手、下载工具所选编号一致重新选择实际端口
USB转串口设备插入后能正常出现检查数据线、驱动和USB接口
软件实例数量同一端口只由一个软件打开关闭重复实例
串口状态下载时由下载工具占用,调试时由串口助手占用按用途切换占用者

1.2 串口代码编写

可以复制一个能够正常编译和下载的Keil工程作为基础,再修改工程目录名称和程序内容。复制工程的目的,是保留芯片型号、启动文件、晶振配置和下载输出选项等已有设置;仍要确认Keil当前打开的是复制后的工程,避免把代码写进其他实验目录。

如果Keil左侧工程树消失,可依次选择View → Project Window恢复,再打开工程中的main.c

1.2.1 定时器配置

从定时器实验工程开始改写时,应移除不再使用的定时器0初始化和定时器0中断服务程序,例如ET0TR0TH0/TL0配置;串口初始化则需要配置SCONPCONTMODTH1/TL1TR1

串口方式1常用的初始化顺序如下:

1. SCON = 0x50       选择方式1并允许接收
2. PCON.SMOD = 0     波特率不加倍
3. TMOD高4位 = 0010  定时器1使用模式2
4. TH1 = TL1 = 0xFD 设置9600波特所需的重装值
5. TR1 = 1           启动定时器1
6. 按程序结构决定是否打开ES和EA

这里要区分“定时器1运行”和“定时器1中断”:

配置功能仅作为UART波特率发生器时是否需要
TR1=1让定时器1开始计数需要
ET1=1允许定时器1溢出时打断CPU不需要
ES=1允许TIRI触发串口中断只有采用串口中断时需要
EA=1打开CPU总中断门控采用中断时需要

纯查询方式发送只需轮询TI,可以不打开ESEA。配套程序保留ES=1EA=1和串口中断函数,是为了继续扩展中断接收;如果只保留一个空的串口中断函数,则仍应确保TIRI不会长期保持为1。

1.2.2 配置串口和定时器

本实验采用11.0592MHz晶振、串口方式1、9600波特、8位数据、无校验、1位停止位。初始化代码为:

SCON = 0x50;
PCON &= 0x7F;
TMOD &= 0x0F;
TMOD |= 0x20;
TH1 = 0xFD;
TL1 = 0xFD;
TR1 = 1;
ES = 1;
EA = 1;

这些语句不是互相独立的“固定模板”,而是共同构成一条完整的波特率和收发控制链。SCON决定帧工作方式,PCON决定倍率,定时器1决定基础溢出率,TR1决定时钟是否真正运行,ES/EA决定CPU是否响应串口中断。

1.2.2.1 串行控制寄存器SCON

SCON=0x50的二进制形式是0101 0000B

位号: B7   B6   B5   B4   B3   B2   B1   B0
符号: SM0  SM1  SM2  REN  TB8  RB8  TI   RI
数值:  0    1    0    1    0    0    0    0
位或位组当前值作用
SM0 SM101选择串口方式1,即8位UART、波特率可变
SM20普通点对点方式1接收,不启用多机筛选
REN1允许接收器监视P3.0/RXD
TB8/RB80本实验方式1不使用第9数据位发送功能
TI0初始化时清除发送标志
RI0初始化时清除接收标志

REN只控制接收器,不控制发送器。发送由“向SBUF写入一个字节”启动,所以即使只做发送,REN=0也不会阻止TXD发送;设置REN=1是为后续接收或全双工通信做好准备。

1.2.2.2 电源控制寄存器PCON

PCON的最高位B7是SMOD。方式1的波特率关系为:

$ \text{波特率}=\frac{2^{SMOD}}{32}\times\text{定时器1溢出率} $

SMOD=0时不加倍,SMOD=1时波特率变为相同定时器配置下的2倍。代码采用:

PCON &= 0x7F;

其按位运算过程为:

PCON原值: b7 b6 b5 b4 b3 b2 b1 b0
掩码0x7F:  0  1  1  1  1  1  1  1
按位与后:  0 b6 b5 b4 b3 b2 b1 b0
            ▲
          仅SMOD被清0

PCON不能位寻址,因此使用掩码清除B7,同时保留其余电源控制位和通用标志位。这里操作的必须是PCON,不能写成SCON &= 0x7F

1.2.2.3 定时器/计数器工作模式寄存器TMOD

TMOD的高4位控制定时器1,低4位控制定时器0:

位号:  B7     B6    B5  B4 │ B3     B2    B1  B0
符号: GATE1  C/T1  M1  M0 │ GATE0  C/T0  M1  M0
对象: └────── 定时器1 ────┘ │ └────── 定时器0 ────┘

串口使用定时器1内部定时、模式2自动重装,因此定时器1的高4位应为0010B,即整个字节中的0x20,不是0x02

TMOD &= 0x0F;  // 清除高4位,保留定时器0的低4位
TMOD |= 0x20;  // 高4位置0010:定时器1、内部定时、模式2

模式2内部使用TL1计数,使用TH1保存重装值:

                    ┌──────────── 自动重装 ────────────┐
                    │                                 │
TH1保存0xFD ────────┘                                 │
                                                      ▼
系统时钟÷12 ─→ TL1:0xFD → 0xFE → 0xFF → 溢出 → 重新回到0xFD
                                           │
                                           └→ 向UART提供溢出节拍

这种结构相当于一个“工作计数器”和一个“初值仓库”:

  • TL1不断计数。
  • TH1不参与这一轮递增,专门保存下一轮要装入TL1的数值。
  • 溢出时由硬件重装,不需要在定时器1中断中手工再次写入初值。

在12T计数方式下,定时器1溢出率为:

$ f_{overflow}=\frac{f_{osc}}{12(256-TH1)} $

代入fosc=11.0592MHzTH1=0xFD=253

$ f_{overflow}=\frac{11,059,200}{12\times(256-253)}=307,200Hz $

再代入SMOD=0

$ \text{波特率}=\frac{1}{32}\times307,200=9,600\text{ bit/s} $

所以初始化时写入:

TH1 = 0xFD;  // 保存以后每轮的自动重装值
TL1 = 0xFD;  // 设置启动后的第一轮计数初值

若晶振频率或定时器的1T/6T/12T设置改变,0xFD未必仍对应9600波特,必须重新计算。

1.2.2.4 定时器的运行
TR1 = 1;

TR1是定时器1运行控制位。置1后,TL1才会持续计数并周期性溢出;如果漏掉这条语句,UART方式1得不到发送和接收时钟,即使已经写入SBUF也不能正常通信。

定时器1与UART的实际分工如下:

定时器1:产生等间隔的溢出脉冲
                       │
                       ▼
UART分频器:形成每一位所需的发送时钟和16倍接收采样时钟
                       │
             ┌─────────┴─────────┐
             ▼                   ▼
      发送移位寄存器移1位   接收器执行一次采样节拍

“时间到了发送或接收一个数据”容易产生误解。一次定时器1溢出不是发送一个完整字节,也不需要CPU响应一次定时器中断;多个溢出节拍经过UART内部硬件分频后,共同确定每一位的宽度。

ES=1是打开串口中断分开关,EA=1是打开CPU总中断开关,它们不是“完成定时器1中断配置”。串口中断的逻辑条件为:

$ (TI\lor RI)\land ES\land EA $

1.2.2.5 串口的中断处理函数

Keil C51中,串口中断号为4,对应中断向量地址0x0023。正确的函数声明形式为:

void UART_Routine(void) interrupt 4
{
    /* 串口中断处理代码 */
}

UART_Routine只是函数名,可以自行命名;interrupt 4才决定编译器把它连接到串口中断入口。常见中断号对应关系如下:

中断号中断源
0外部中断0
1定时器0
2外部中断1
3定时器1
4串口中断

ES=1EA=1时,TIRI置1都可能使CPU进入这个函数。两种标志共用同一入口,所以正式采用中断收发时,应分别判断并清除它们:

void UART_Routine(void) interrupt 4
{
    if (RI == 1)
    {
        RI = 0;
        /* 读取SBUF */
    }

    if (TI == 1)
    {
        TI = 0;
        /* 继续发送或更新发送状态 */
    }
}

如果允许串口中断却完全没有对应入口,CPU在请求到来时会跳到串口中断向量,随后执行的内容不可控,可能跑飞或出现异常。若发送完全采用查询方式,更直接的方案是不开ES,这样也不需要为了查询发送而保留空中断函数。

1.2.2.6 寄存器赋值发送数据

SBUF写入一个字节,就会把这个字节交给UART发送硬件:

SBUF = 'a';

单引号表示字符常量。ASCII编码中'a'的数值为0x61,因此该语句实际写入的是0x61。UART方式1随后在线路上自动发送:

空闲1 │ 起始位0 │ D0 │ D1 │ D2 │ D3 │ D4 │ D5 │ D6 │ D7 │ 停止位1
                    0x61的8个数据位,低位先发

SBUF发送通路可以拆成三个硬件层次:

CPU内部总线
    │ 执行 SBUF='a'
    ▼
发送SBUF/发送缓冲器      暂存CPU交付的1个字节
    │
    ▼
发送移位寄存器           每到一个发送时钟移出1位
    │ 自动添加START/STOP
    ▼
P3.1/TXD                 对外输出高、低电平波形
    │
    └─ 到规定时刻后由硬件置 TI=1,软件再清TI

它不是一个可以无限接收数据的队列。CPU执行C语句的速度远快于9600波特UART移出数据的速度,所以每次写SBUF后必须确认UART已经完成当前发送阶段。最基本的查询写法是:

SBUF = 'a';
while (TI == 0);
TI = 0;

while(TI==0);末尾的分号表示循环体为空:CPU一直检查TI,直到硬件把它置1,然后执行下一句清零。也可以写成while(!TI);,含义相同。

把这三句封装成函数后,不容易在多处发送代码中漏掉等待或清标志:

void UART_Send_Byte(uchar send_byte)
{
    SBUF = send_byte;
    while (TI == 0);
    TI = 0;
}

构建工程时出现warning不应一概忽略,应先阅读具体信息。本实验若提示Delay_xms已定义但未被调用,通常不妨碍生成HEX文件;可以删除未使用函数,也可以在后续需要延时时再调用。error必须解决,warning则要判断是否会影响当前功能和后续维护。

下载程序前应先释放串口助手占用的COM端口。程序运行后,如果while(1)持续发送,串口通信指示灯可能快速闪烁,串口助手也会不断接收字符;指示灯的具体表现取决于开发板的串口转换和指示电路。

1.2.2.7 应用案例

使用for循环尝试发送a~z

ASCII编码中的小写英文字母连续排列:'a'=0x61'b'=0x62,一直到'z'=0x7A。因此可以通过'a'+count依次得到26个字母:

for (count = 0; count < 26; count++)
{
    SBUF = 'a' + count;
}
count表达式数值字符
0'a' + 00x61a
1'a' + 10x62b
2'a' + 20x63c
25'a' + 250x7Az

这里的循环边界必须是count < 26。若写成count <= 26,最后一次会得到'{',因为0x7A+1=0x7B

为什么串口助手没有稳定地按abcdefghijklmnopqrstuvwxyz显示

问题不在ASCII加法,而在循环内连续写SBUF却没有等待TI。9600波特、8N1格式下,一帧包含1位起始位、8位数据位和1位停止位,共10个位:

$ T_{byte}=\frac{10}{9600}\text{s}\approx1.0417\text{ms} $

发送26个字母至少需要:

$ 26\times1.0417\text{ms}\approx27.08\text{ms} $

CPU执行26次简单循环远少于27ms。UART还没发送完前一个字节,程序就继续写入后面的字符,而经典8051发送硬件没有能够容纳整串字符的多字节FIFO,结果便会丢失待发送字符,接收端也无法得到稳定、连续的a~z顺序。

CPU写入速度: a → b → c → d → e → …… → z     非常快
                  ╲  ╲  ╲  ╲
UART发送速度:     [一帧约1.04ms] → [下一帧]    明显更慢

没有等待TI:多个写操作争用有限的发送缓冲,字符丢失
等待TI:    前一帧到达发送完成时刻后,再交付下一字节

正确的查询发送方式是在每次循环中等待并清除TI

for (count = 0; count < 26; count++)
{
    SBUF = 'a' + count;
    while (TI == 0);
    TI = 0;
}

使用完整程序中的封装函数会更清晰:

for (count = 0; count < 26; count++)
{
    UART_Send_Byte('a' + count);
}

发送换行和字符串时也应复用同一个单字节发送函数。C字符串以'\0'作为结束标志,可以逐字符发送:

void UART_Send_Str(uchar *send_str)
{
    while (*send_str != '\0')
    {
        UART_Send_Byte(*send_str++);
    }
}

需要区分几个常见转义字符:

写法名称ASCII值常见作用
'\n'换行LF0x0A光标移动到下一行
'\r'回车CR0x0D光标回到本行开头
'\0'字符串结束符NUL0x00标记C字符串结束,不作为正文发送

不同串口终端对换行的处理可能不同,发送文本行时常使用"\r\n"。最终发送流程应遵循同一原则:每向SBUF交付一个字节,都要用TI确认硬件已经推进到可以处理下一字节的时刻,再清除标志并继续发送。