串口代码发送_1
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端口──┘
处理步骤如下:
- 关闭串口助手中的“打开串口”状态;如果仍不确定,直接退出串口助手。
- 在下载工具中重新选择正确的COM端口。
- 重新点击下载,再按开发板要求重新上电或复位。
- 下载完成后,再打开串口助手进行收发测试。
关闭串口助手不是串口协议本身的要求,而是为了释放电脑上的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 SM1 | 01 | 选择串口方式1,即8位UART、波特率可变 |
SM2 | 0 | 普通点对点方式1接收,不启用多机筛选 |
REN | 1 | 允许接收器监视P3.0/RXD |
TB8/RB8 | 0 | 本实验方式1不使用第9数据位发送功能 |
TI | 0 | 初始化时清除发送标志 |
RI | 0 | 初始化时清除接收标志 |
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' + 0 | 0x61 | a |
| 1 | 'a' + 1 | 0x62 | b |
| 2 | 'a' + 2 | 0x63 | c |
| … | … | … | … |
| 25 | 'a' + 25 | 0x7A | z |
这里的循环边界必须是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' | 换行LF | 0x0A | 光标移动到下一行 |
'\r' | 回车CR | 0x0D | 光标回到本行开头 |
'\0' | 字符串结束符NUL | 0x00 | 标记C字符串结束,不作为正文发送 |
不同串口终端对换行的处理可能不同,发送文本行时常使用"\r\n"。最终发送流程应遵循同一原则:每向SBUF交付一个字节,都要用TI确认硬件已经推进到可以处理下一字节的时刻,再清除标志并继续发送。