串口代码发送_1
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端口──┘
处理步骤如下:
- 关闭串口助手中的“打开串口”状态;如果仍不确定,直接退出串口助手。
- 在下载工具中重新选择正确的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位停止位。初始化代码为:
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 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 &= 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.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} $
所以初始化时写入:
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=1、EA=1时,TI或RI置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' + 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:
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' | 换行LF | 0x0A | 光标移动到下一行 |
'\r' | 回车CR | 0x0D | 光标回到本行开头 |
'\0' | 字符串结束符NUL | 0x00 | 标记C字符串结束,不作为正文发送 |
不同串口终端对换行的处理可能不同,发送文本行时常使用"\r\n"。最终发送流程应遵循同一原则:每向SBUF交付一个字节,都要用TI确认硬件已经推进到可以处理下一字节的时刻,再清除标志并继续发送。