串口代码发送_1

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 0 Likes: 0 Comments: 0

048.6.4 串口代码发送(1)

/*
 * 整体目的:初始化STC89C52的UART方式1,以9600波特循环发送a~z、两个换行符和一行字符串。
 * 整体设计:定时器1模式2提供波特率时基;UART_Send_Byte等待TI同步每个字节;
 *           UART_Send_Str在单字节函数之上逐字符发送以'\0'结尾的C字符串。
 * 时钟依据:TH1/TL1取0xFD的计算条件是11.0592MHz晶振、定时器1为12T、SMOD=0。
 * 硬件路径:写SBUF后,UART硬件从P3.1/TXD输出起始位、8位数据和停止位;
 *           开发板上的CH340N再把TTL UART转换为电脑可识别的USB串口数据。
 * 课程上下文:本节重点是查询TI的发送流程;源码同时打开了串口中断却保留空服务函数,
 *             实际只做查询发送时可不打开ES和EA,后续实现中断接收后再启用它们更清晰。
 */

#include "reg52.h"   /* 引入8052特殊功能寄存器声明,使程序可以使用P1、SCON、SBUF、TI等名称。 */
#include <intrins.h>  /* 引入Keil C51内部函数声明,下面的软件延时需要调用_nop_()。 */

typedef unsigned char uchar;  /* 为8位无符号字符型建立简写,串口的单个数据字节使用该类型。 */
typedef unsigned int uint;    /* 为C51的16位无符号整型建立简写,用作计数和毫秒参数。 */

uint count = 0;  /* 定义全局循环计数器,取值0~25时分别生成字符'a'~'z'。 */

sbit LED1 = P1^0;  /* 将LED1名称绑定到可位寻址的P1.0;本节发送程序中暂未使用该别名。 */
sbit LED2 = P1^1;  /* 将LED2名称绑定到P1.1;这是从实验模板保留的定义,不参与当前发送流程。 */

/*
 * 函数目的:按参数执行毫秒级忙等待,供需要限制循环发送频率的实验代码调用。
 * 时间前提:计数值按11.0592MHz和当前Keil C51生成的指令周期设计;时钟或优化变化会造成误差。
 * 当前状态:本节main没有调用该函数,因此字符串会在while循环中连续重复发送。
 */
void Delay_xms(uint xms)                  /* 定义软件延时函数,xms表示计划等待的毫秒数。 */
{                                        /* 延时函数体开始,等待期间CPU不能并行处理其他前台任务。 */
    uchar data i, j;                      /* 定义两个8位内层计数器,并要求编译器优先放入片内data RAM。 */

    while (xms)                           /* 每完成一轮约1ms的内层空转,就把剩余毫秒数减1。 */
    {                                     /* 一毫秒延时循环开始;xms为0时不会进入。 */
        _nop_();                           /* 执行一条空操作指令,用来补偿延时生成器计算的指令时间。 */
        i = 2;                             /* 初始化外层8位计数器,使下面的j循环重复指定轮数。 */
        j = 199;                           /* 初始化内层计数器;递减循环的指令耗时构成主要延时。 */
        do                                /* 采用后判断循环,保证内层计数过程至少执行一次。 */
        {                                 /* do循环体开始,此处只通过反复执行指令消耗CPU时间。 */
            while (--j);                  /* 先递减j,结果非0就继续空转;末尾分号表示循环体为空。 */
        } while (--i);                    /* 当前j循环结束后递减i,非0时借助8位回绕再次运行j循环。 */
        xms--;                             /* 一轮约1ms等待结束,将尚需等待的毫秒数减1。 */
    }                                     /* xms仍非0便开始下一轮,否则离开忙等待。 */
}                                         /* 延时函数结束并返回调用处。 */

/*
 * 函数目的:可靠地查询发送一个字节,避免CPU连续写SBUF而覆盖尚未处理完的数据。
 * 同步方法:写SBUF启动硬件发送,等待TI由硬件置1,再由软件清0供下一帧使用。
 */
void UART_Send_Byte(uchar send_byte)       /* 定义单字节发送函数,形参保存本次要交给UART的8位数据。 */
{                                         /* 单字节发送流程开始;调用期间CPU会阻塞等待发送进度。 */
    SBUF = send_byte;                      /* 写发送SBUF,UART随后自动输出起始位、数据位和停止位。 */
    while (TI == 0);                       /* TI未置1时原地查询;空循环末尾的分号表示没有循环体。 */
    TI = 0;                                /* 发送到规定时刻后软件清TI,为检测下一帧完成事件做准备。 */
}                                         /* 一个字节的查询发送结束,返回调用者。 */

/*
 * 函数目的:把以'\0'结尾的C字符串拆成单个字节,并复用UART_Send_Byte依次发送。
 * 指针规则:每发送当前字符后指针自增,直到它指向字符串结尾的NUL标志。
 */
void UART_Send_Str(uchar *send_str)        /* 接收指向字符串首字符的指针;函数不会发送结尾的'\0'。 */
{                                         /* 字符串发送函数体开始。 */
    while (*send_str != '\0')              /* 解引用指针检查当前字符,只要不是结束标志就继续循环。 */
    {                                     /* 当前字符有效,进入本轮逐字节发送。 */
        UART_Send_Byte(*send_str++);       /* 先发送指针当前所指字符,再把指针移动到下一个字符。 */
    }                                     /* 回到条件处检查新字符,遇到'\0'时结束。 */
}                                         /* 整个字符串发送完成,返回调用处。 */

/*
 * 函数目的:完成UART及其波特率发生器初始化,并永久循环输出测试字符。
 * 数据序列:每轮发送abcdefghijklmnopqrstuvwxyz、LF、LF、"i am wfeng!"、CR、LF。
 */
void main()                                /* 定义Keil C51程序入口,启动代码初始化运行环境后进入这里。 */
{                                         /* 主函数体开始。 */
    SCON = 0x50;                           /* 选择UART方式1、允许接收,并将TI和RI初始化为0。 */
    PCON &= 0x7F;                          /* 仅清除SMOD,选择波特率不加倍并保留PCON其他位。 */
    TMOD &= 0x0F;                          /* 清除定时器1对应的高4位,同时保留定时器0的低4位配置。 */
    TMOD |= 0x20;                          /* 把定时器1设为内部定时、模式2自动重装。 */
    TH1 = 0xFD;                            /* 保存定时器1的自动重装值253,对应每3个计数脉冲溢出一次。 */
    TL1 = 0xFD;                            /* 设置第一次计数的起点,使首轮与以后各轮周期一致。 */
    TR1 = 1;                               /* 启动定时器1,开始为UART持续提供9600波特所需时基。 */
    ES = 1;                                /* 打开串口中断分开关;只有TI或RI置位时才会提出请求。 */
    EA = 1;                                /* 打开CPU总中断;当前空ISR仅为后续接收课程预留。 */

    while (1)                              /* 单片机持续运行,无操作系统可返回,因此用无限循环重复发送。 */
    {                                     /* 一轮完整测试数据发送开始。 */
        for (count = 0; count < 26; count++)  /* 让count依次取0~25,共循环26次。 */
        {                                  /* 当前小写字母的发送分支开始。 */
            /* 以下三行是已禁用的直接查询写法,保留用来与封装函数的等价步骤对照。 */
//          SBUF = 'a' + count;
//          while (TI == 0);
//          TI = 0;
            UART_Send_Byte('a' + count);   /* 利用ASCII连续性生成'a'~'z',并逐字节等待TI。 */
        }                                  /* 26个小写字母发送结束。 */

        /* 以下三行是已禁用的直接发送换行写法,实际代码改用UART_Send_Byte复用同步逻辑。 */
//      SBUF = '\n';
//      while (TI == 0);       // while (!TI);
//      TI = 0;
        UART_Send_Byte('\n');               /* 发送ASCII换行LF(0x0A),让终端移动到下一行。 */
        UART_Send_Byte('\n');               /* 再发送一个LF,因此多数终端会额外空出一行。 */
        UART_Send_Str("i am wfeng!\r\n");   /* 发送正文以及CR+LF,使下一轮显示从新行行首开始。 */
    }                                      /* 本轮结束后立即回到while顶部;这里没有额外发送间隔。 */
}                                          /* 主函数语法边界;无限循环使程序正常运行时不会返回。 */

/*
 * 函数目的:为后续串口中断接收保留中断号4的服务函数入口。
 * 当前限制:函数体没有判断或清除RI/TI,也没有处理数据,不能视为完整的中断收发实现。
 */
void UART_Routine(void) interrupt 4        /* 声明串口中断服务程序,Keil将其连接到向量地址0x0023。 */
{                                          /* 空的中断函数体开始;后续课程会在这里加入标志判断。 */

}                                          /* 空服务函数结束;实际启用中断时必须确保请求标志能被及时清除。 */

一、串口代码编写

本节从串口被软件占用的常见问题入手,完成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中断服务程序,例如ET0、TR0、TH0/TL0配置;串口初始化则需要配置SCON、PCON、TMOD、TH1/TL1和TR1。

串口方式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允许TI或RI触发串口中断只有采用串口中断时需要
EA=1打开CPU总中断门控采用中断时需要

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

1.2.2 配置串口和定时器

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

/*
 * 片段目的:集中展示UART方式1、9600波特及串口中断门控的完整初始化顺序。
 * 使用前提:11.0592MHz晶振、定时器1为12T模式;只做查询发送时ES和EA并非必需。
 */
SCON = 0x50;   /* 选择方式1、允许接收,并清除初始化时的TI和RI。 */
PCON &= 0x7F;  /* 清SMOD选择波特率不加倍,同时保留PCON其余位。 */
TMOD &= 0x0F;  /* 清除控制定时器1的高4位,保留定时器0低4位。 */
TMOD |= 0x20;  /* 设置定时器1为内部定时、模式2自动重装。 */
TH1 = 0xFD;    /* 写入以后每次溢出时使用的自动重装值。 */
TL1 = 0xFD;    /* 写入启动后第一次计数使用的初值。 */
TR1 = 1;       /* 启动定时器1,让UART获得连续波特率时基。 */
ES = 1;        /* 打开串口中断分开关,使TI或RI能够请求中断。 */
EA = 1;        /* 打开CPU总中断开关,允许已开放的中断源被响应。 */

这些语句不是互相独立的“固定模板”,而是共同构成一条完整的波特率和收发控制链。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最高位SMOD,不改动同一寄存器中的其他功能位。 */
PCON &= 0x7F;  /* 0x7F的B7为0,按位与后SMOD=0,方式1波特率采用1倍系数。 */

其按位运算过程为:

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中属于定时器1的高4位。 */
TMOD &= 0x0F;  /* 与0000 1111B按位与:清除高4位,保留定时器0的低4位。 */
TMOD |= 0x20;  /* 与0010 0000B按位或:定时器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.0592MHz、TH1=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} $

所以初始化时写入:

/*
 * 片段目的:同时设置模式2的重装寄存器和当前计数器,保证第一次与后续溢出周期一致。
 * 数值前提:0xFD对应11.0592MHz、12T、SMOD=0条件下的9600波特。
 */
TH1 = 0xFD;  /* TH1保存以后每轮溢出后由硬件复制到TL1的自动重装值。 */
TL1 = 0xFD;  /* TL1保存启动后的第一轮计数初值,并从0xFD开始向上计数。 */

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

1.2.2.4 定时器的运行
/* 片段目的:在方式和初值配置完成后,正式启动定时器1的波特率时基。 */
TR1 = 1;  /* 将TCON.6置1,使定时器1开始计数;这不等于打开定时器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。正确的函数声明形式为:

/*
 * 片段目的:说明Keil C51中串口中断服务程序的基本声明形式。
 * 使用前提:函数体必须根据实际方案处理并清除RI和/或TI,下面的占位注释不是完整实现。
 */
void UART_Routine(void) interrupt 4  /* interrupt 4指定经典8051串口中断号,对应向量0x0023。 */
{                                    /* 中断函数体开始,硬件跳转到此处后由软件识别请求来源。 */
    /* 在这里分别检查RI和TI,处理数据并清除实际置位的标志。 */
}                                    /* 中断处理结束,编译器生成中断返回序列。 */

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

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

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

/*
 * 函数目的:在TI和RI共用的串口中断入口中,分别识别接收完成与发送完成事件。
 * 结构原因:使用两个独立if,两个标志同时为1时也能在一次中断中全部处理。
 */
void UART_Routine(void) interrupt 4  /* 声明串口中断号4的服务函数。 */
{                                    /* 中断处理开始;进入时并不知道是TI还是RI触发。 */
    if (RI == 1)                     /* 检查UART是否已经接收完一个有效数据帧。 */
    {                                /* 接收事件分支开始。 */
        RI = 0;                      /* 软件清除接收标志,为下一帧形成新的接收事件做准备。 */
        /* 在这里及时读取SBUF,并把字节保存到变量或接收缓冲区。 */
    }                                /* 接收事件处理结束,但仍需继续检查发送标志。 */

    if (TI == 1)                     /* 检查UART是否已把当前发送帧推进到规定完成时刻。 */
    {                                /* 发送事件分支开始。 */
        TI = 0;                      /* 软件清除发送标志,撤销本次请求并准备检测下一帧。 */
        /* 在这里按发送队列状态写入下一字节,或者只更新发送完成状态。 */
    }                                /* 发送事件处理结束。 */
}                                    /* 两类标志均检查完毕,退出中断服务程序。 */

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

1.2.2.6 寄存器赋值发送数据

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

/* 片段目的:向发送缓冲器交付ASCII字符a,从而启动一帧UART发送。 */
SBUF = 'a';  /* 字符常量'a'的ASCII值为0x61,写SBUF后由硬件从TXD逐位发出。 */

单引号表示字符常量。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前本帧已推进完成。
 * 初始前提:TI在开始发送前为0,且UART和定时器1已经正确初始化。
 */
SBUF = 'a';           /* 把ASCII码0x61写入发送SBUF,启动字符a的一帧传输。 */
while (TI == 0);      /* TI仍为0时保持空循环,等待UART硬件将其置1。 */
TI = 0;               /* 发送完成后由软件清TI,让下一帧可以产生新的完成标志。 */

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

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

/* 函数目的:把“写SBUF→等待TI→清TI”封装为可重复调用的单字节查询发送接口。 */
void UART_Send_Byte(uchar send_byte)  /* send_byte保存调用者要求发送的8位数据。 */
{                                    /* 单字节发送函数体开始。 */
    SBUF = send_byte;                /* 把形参写入发送SBUF,启动UART硬件传输。 */
    while (TI == 0);                 /* 阻塞等待TI置1;末尾分号表示循环体故意为空。 */
    TI = 0;                          /* 软件清除TI,为下一次函数调用准备新的完成事件。 */
}                                    /* 本字节已发送到规定时刻,函数返回。 */

构建工程时出现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个字母:

/*
 * 反例目的:展示只利用ASCII加法却不等待TI的错误批量发送方式。
 * 错误后果:CPU写入速度远快于9600波特的帧发送速度,后续字符可能丢失。
 */
for (count = 0; count < 26; count++)  /* 让count依次取0~25,计划生成26个小写字母。 */
{                                    /* 一次过快写入的循环体开始。 */
    SBUF = 'a' + count;              /* 直接写下一个字符而未等待TI,可能争用有限的发送缓冲。 */
}                                    /* 循环很快结束,但UART尚未来得及逐帧发完这些字符。 */
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:

/* 片段目的:在每次循环内完成一次查询发送,可靠地依次输出a~z。 */
for (count = 0; count < 26; count++)  /* count取0~25;边界小于26可避免多发字符'{'。 */
{                                    /* 当前字符的一次查询发送开始。 */
    SBUF = 'a' + count;              /* 利用ASCII连续编码生成当前小写字母并写入发送SBUF。 */
    while (TI == 0);                 /* 等待当前帧推进完成,期间不向SBUF写入下一个字符。 */
    TI = 0;                          /* 清除本帧完成标志,为下一次循环等待新的TI置位。 */
}                                    /* 当前字符发送结束,count递增后处理下一个字符。 */

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

/* 片段目的:复用单字节查询发送函数,以更清晰的形式可靠输出a~z。 */
for (count = 0; count < 26; count++)  /* 依次产生0~25这26个字符偏移量。 */
{                                    /* 当前字符发送循环体开始。 */
    UART_Send_Byte('a' + count);     /* 生成当前字母;函数内部完成写SBUF、等待TI和清TI。 */
}                                    /* 函数返回说明本字节已完成,再进入下一轮。 */

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

/*
 * 函数目的:沿字符指针遍历C字符串,并通过UART_Send_Byte可靠发送每个正文字符。
 * 终止规则:'\0'只用于标记字符串结束,函数检测到它后退出而不会把它发送出去。
 */
void UART_Send_Str(uchar *send_str)  /* send_str初始指向待发送字符串的第一个字符。 */
{                                   /* 字符串发送函数体开始。 */
    while (*send_str != '\0')        /* 解引用并判断当前字符是否为字符串结束标志NUL。 */
    {                               /* 当前字符不是NUL,进入本轮发送。 */
        UART_Send_Byte(*send_str++); /* 发送当前字符,然后把指针递增到下一个字符地址。 */
    }                               /* 返回条件处检查新位置,直到遇到'\0'。 */
}                                   /* 全部正文字符发送完成,函数返回。 */

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

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

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