串口代码发送_2
049.6.5 串口代码发送(2)
#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
{
}
一、任务资源图
上一阶段连续执行SBUF='a'+count时,串口助手没有稳定显示abcdefghijklmnopqrstuvwxyz。根本原因不是串口没有初始化,而是CPU没有按照SCON中的发送标志TI与UART硬件同步:CPU写入下一个字符时,前一个字符可能仍在发送。
TI和RI都是串行控制寄存器SCON中的标志位,并不是两个独立寄存器:
SCON(地址0x98)
位号: B7 B6 B5 B4 B3 B2 B1 B0
符号: SM0 SM1 SM2 REN TB8 RB8 TI RI
│ │
│ └─ 接收中断请求标志
└────── 发送中断请求标志
本节的核心任务可以归纳为三步:
- 每次向
SBUF写入一个字节后,等待硬件把TI置1。 - 由软件把
TI清0,为下一次发送状态判断做好准备。 - 把单字节发送流程封装成函数,再利用指针逐字节发送字符串。
待发送字节 ─→ UART_Send_Byte ─→ SBUF ─→ UART硬件发送 ─→ TI=1
▲ │
└──────── 软件清TI后发送下一字节 ───────┘
这种“硬件置位、软件查询、软件清零”的交互方式,是学习外设状态标志的典型模式。程序不能只负责下达动作,还要读取外设反馈的状态。
二、串口发送原理
向SBUF写入数据只表示CPU把一个字节交给串口硬件。UART还要用波特率时钟把并行字节转换成串行帧,然后从P3.1/TXD逐位输出:
CPU内部总线
│ 写SBUF
▼
发送缓冲/装载逻辑 ─→ 发送移位寄存器 ─→ 起始位与停止位控制 ─→ P3.1/TXD
▲ │
│ ▼
定时器1提供波特率时基 外部串口线路
│
└──────── 发送到规定时刻,TI=1
方式1的一帧由1位起始位、8位数据位和1位停止位组成。数据位按照D0→D7低位先发:
空闲 │ START │ D0 │ D1 │ D2 │ D3 │ D4 │ D5 │ D6 │ D7 │ STOP │ 空闲
1 │ 0 │ 8个数据位 │ 1 │ 1
▲
STOP开始时TI=1
在串口方式1中,发送停止位开始时,硬件将TI置1。工程中可以把它理解为“当前帧已经推进到允许软件处理下一字节的时刻”。严格来说,TI并不是在整段停止位完全结束之后才置1,因此“TI=1表示最后一位电平已经彻底结束”并不精确。
SCON=0x50在初始化时令TI=0。一帧发送到规定时刻后,硬件只负责置1,不会自动清零;软件必须执行:
TI = 0;
如果不等待TI就连续写入SBUF,CPU的写入速度会远高于串口移位速度。9600波特、8N1时,一个字节至少占用:
$ T_{byte}=\frac{1+8+1}{9600}\approx1.0417\text{ms} $
经典8051发送硬件没有能保存整串字符的多字节FIFO,因此连续快速写入会丢失待发送字符,串口助手看到的内容就无法保持预期顺序。
三、发送一个字符
3.1 代码讲解与串口发送原理
查询方式发送一个字节的完整流程为:
SBUF = 'a';
while (TI == 0);
TI = 0;
逐句解释如下:
| 语句 | 作用 |
|---|---|
SBUF='a'; | 将ASCII字节0x61交给UART,并启动一帧发送 |
while(TI==0); | 当硬件尚未置位TI时原地等待 |
TI=0; | 软件清除发送标志,为下一次发送准备 |
while(TI==0);末尾的分号非常关键。它表示循环体是一个空语句:
TI=0:条件成立 → 继续空循环 → 继续检查TI
TI=1:条件不成立 → 跳出循环 → 执行TI=0
也可以写成:
while (!TI);
因为!TI在TI=0时为1,在TI=1时为0,所以两种写法效果相同。while(TI!=1);也能表达相同逻辑,但while(TI==0);通常更直观。
这个等待属于阻塞式查询。等待期间CPU不能处理主循环中的其他任务,代码简单但效率不高,适合入门实验和少量数据发送。实际项目还可以使用发送中断、环形缓冲区或DMA等方式减少阻塞;经典STC89C52 UART没有DMA,通常使用中断配合软件缓冲区。
为什么for循环需要大括号
C语言的for、while和if在没有大括号时,只控制紧随其后的第一条语句。要让“写SBUF、等待TI、清TI”三条语句都属于循环体,必须使用大括号:
for (count = 0; count < 26; count++)
{
SBUF = 'a' + count;
while (TI == 0);
TI = 0;
}
若省略大括号,只有SBUF='a'+count;会循环26次,等待和清零只在循环结束后执行一次,仍会导致发送错乱。
3.2 例题1: 串口发送的代码实现与调试
发送a~z的代码为:
for (count = 0; count < 26; count++)
{
SBUF = 'a' + count;
while (TI == 0);
TI = 0;
}
ASCII编码中的小写字母连续排列,所以'a'+0到'a'+25依次对应a到z。串口助手应重复显示:
abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz...
如果显示的字符仍然异常,应依次检查:
| 检查项 | 本实验应采用的设置或现象 |
|---|---|
| 串口波特率 | 9600 |
| 数据格式 | 8位数据、无校验、1位停止位,即8N1 |
| 单片机晶振 | 11.0592MHz |
| 定时器1重装值 | TH1=TL1=0xFD,且使用12T计数条件 |
| 发送等待 | 每个字节后都等待TI=1并清零 |
| 串口助手显示模式 | 文本/ASCII显示,而不是只按HEX格式观察 |
编译并生成HEX文件后,应先关闭串口助手以释放COM端口,再由下载工具烧写。下载完成后重新打开串口助手观察输出。串口助手字体大小只影响显示效果,不改变接收到的字节内容;字符难以观察时,可以在软件的界面或主题设置中调大字体。
调试时还可以临时把循环上限设为20。这样只会发送a到t,可用于验证循环边界确实参与了输出控制;验证完成后再恢复为26。
四、发送后加换行符
如果a~z连续重复而没有行分隔,内容不易观察。可以在for循环之后再发送换行字符:
SBUF = '\n';
while (TI == 0);
TI = 0;
如果想留出一个空行,可以连续发送两个'\n',但每一个字节都必须拥有自己的等待和清零流程:
SBUF = '\n';
while (!TI);
TI = 0;
SBUF = '\n';
while (!TI);
TI = 0;
'\n'是换行符LF,ASCII值为0x0A;'\r'是回车符CR,ASCII值为0x0D。不同终端对换行的处理存在差异:
| 发送内容 | 可能的显示效果 |
|---|---|
"\n" | 移动到下一行,但不一定回到行首 |
"\r" | 回到当前行行首,但不下移 |
"\r\n" | 回到行首并进入下一行,串口文本输出中最通用 |
因此发送完整文本行时,通常推荐使用"\r\n"。字符常量使用单引号,例如'\n';字符串使用双引号,例如"\r\n"。
反复出现的三条发送语句适合封装成单字节发送函数,避免某个位置漏写等待或漏清TI。
五、封装发送字节函数
5.1 封装发送字节函数的代码实现
单字节发送函数定义如下:
void UART_Send_Byte(uchar send_byte)
{
SBUF = send_byte;
while (TI == 0);
TI = 0;
}
函数包含四个组成部分:
| 组成 | 代码 | 含义 |
|---|---|---|
| 返回类型 | void | 函数不返回数值 |
| 函数名 | UART_Send_Byte | 表明其功能是UART发送一个字节 |
| 形参 | uchar send_byte | 接收调用者要发送的8位数据 |
| 函数体 | 三条发送语句 | 写入、等待、清标志 |
uchar是前面定义的unsigned char别名。它在此工程中占1字节,取值范围为0~255,正好可以承载UART一次发送的8位数据。
函数定义在main之前时,main可以直接调用;若把函数定义放到main之后,则应在前面添加函数声明,例如:
void UART_Send_Byte(uchar send_byte);
调用时只需传入要发送的字节:
UART_Send_Byte('a' + count);
UART_Send_Byte('\n');
这样,for循环可简化为:
for (count = 0; count < 26; count++)
{
UART_Send_Byte('a' + count);
}
封装没有改变底层发送流程,只是把重复且必须成组出现的代码集中到一个函数中。以后如果发送策略变化,也只需统一调整这个函数。
5.2 串口的中断处理函数
UART_Send_Byte是普通阻塞函数,不是中断处理函数。真正的串口中断函数带有interrupt 4修饰:
void UART_Routine(void) interrupt 4
{
}
两类函数的区别如下:
| 对比项 | UART_Send_Byte | UART_Routine |
|---|---|---|
| 调用者 | 由程序主动调用 | 由硬件中断请求触发 |
| 执行条件 | 程序运行到函数调用处 | (TI或RI) && ES && EA成立 |
| 当前用途 | 查询TI并发送一个字节 | 为后续串口中断收发预留入口 |
是否使用interrupt 4 | 否 | 是 |
配套代码采用查询方式等待TI,同时保留ES=1、EA=1和空的中断函数作为后续实验基础。仅实现查询发送时,可以不启用ES;若已经启用串口中断,正式程序就应在中断函数内判断并及时清除TI、RI,避免标志长期保持为1而反复产生中断请求。
函数调用时,实参会传给形参。例如:
UART_Send_Byte('x');
调用瞬间,字符'x'对应的字节值进入形参send_byte,函数再把它写入SBUF。把循环上限从26临时设为20,只会改变调用次数和传入内容,不会改变发送函数的内部工作方式。
单字节发送函数一次只处理一个字节。如果要发送一句文字,需要再封装字符串发送函数,让它重复调用UART_Send_Byte。
六、应用案例
6.1 发送一个字符串
完整程序通过下面的调用发送字符串:
UART_Send_Str("i am wfeng!\r\n");
双引号中的内容是C字符串,编译器会在末尾自动添加'\0'结束符。其字节排列可以表示为:
地址递增方向 →
'i' ' ' 'a' 'm' ' ' 'w' 'f' 'e' 'n' 'g' '!' '\r' '\n' '\0'
│ │
└─ send_str最初指向这里 循环在这里停止
UART_Send_Str不是让UART一次发送整段字符串。UART每次仍只接收一个字节,字符串函数只是自动完成“取一个字节→调用单字节发送→指针后移→判断结束符”的循环。
使用字符串函数时要确保:
- 参数确实指向以
'\0'结尾的有效字符串。 - 字符串所在内存在整个发送期间保持有效。
- 串口助手和程序对非ASCII内容使用相同字符编码。
- 字符串很长时,要意识到该函数会一直阻塞到全部字节发送完成。
6.2 回到发送字符串函数定义
字符串发送函数如下:
void UART_Send_Str(uchar *send_str)
{
while (*send_str != '\0')
{
UART_Send_Byte(*send_str++);
}
}
send_str是指向uchar的指针。调用函数时传入字符串首地址,指针最初指向第一个字节。
send_str
│
▼
┌──────┬──────┬──────┬──────┬──────┐
│ 'a' │ 'b' │ 'c' │ '\r' │ '\0' │
└──────┴──────┴──────┴──────┴──────┘
地址n n+1 n+2 n+3 n+4
第1轮:读取地址n的'a',发送后指针移到n+1
第2轮:读取地址n+1的'b',发送后指针移到n+2
第3轮:读取地址n+2的'c',发送后指针移到n+3
第4轮:读取地址n+3的'\r',发送后指针移到n+4
第5轮:发现'\0',退出循环,不发送结束符
循环条件*send_str!='\0'中的*是解引用运算符,表示读取指针当前指向的字节,而不是比较指针地址本身。若写成send_str!='\0',比较对象就错误了。
语句:
UART_Send_Byte(*send_str++);
由于后缀++的优先级高于一元*,它等价于:
UART_Send_Byte(*(send_str++));
执行顺序可以理解为:先取得当前指针,读取并发送它指向的字节,然后让send_str指向下一个字节。为了初学时更直观,也可以写成两句:
UART_Send_Byte(*send_str);
send_str++;
为什么必须判断'\0'
C字符串本身不记录长度,而是依靠末尾的零字节表示结束。如果循环遗漏'\0'判断,指针会继续越过字符串边界读取其他内存,可能发送乱码,甚至访问无效区域。'\0'只用于结束判断,不会被本函数发送。
字符、字符串与编码
字符常量使用单引号,例如'a';字符串使用双引号,例如"abc"。把字符串用单引号包围会产生错误或与预期不符。
UART只负责传输字节,不知道字节代表英文还是中文。ASCII字符通常占1字节,而中文字符所占字节数取决于源码和终端采用的编码:
| 内容 | 常见编码 | 一个字符占用的字节数 |
|---|---|---|
英文A | ASCII、GBK、UTF-8 | 1 |
中文王 | GBK | 2 |
中文王 | UTF-8 | 3 |
因此“字符串由多个字节组成”是正确的,但“每个中文字符都是一个字节”并不正确。指针每次递增1会逐字节发送多字节编码;只要单片机程序中的字节编码与串口助手的解码设置一致,中文才会正常显示,否则会出现乱码。教学工程使用纯ASCII字符串,可以避开这一编码差异。
字符串发送的完整数据流如下:
字符串首地址
│
▼
读取当前字节 ─→ 是否为'\0'? ──是──→ 结束
│
└────否──→ UART_Send_Byte ─→ 等待TI ─→ 指针+1 ─→ 回到判断
七、总结
串口查询发送需要掌握以下主线:
TI是SCON中的发送标志位。方式1发送到停止位开始时由硬件置1,并由软件清0。- 单字节发送必须遵循“写
SBUF→等待TI=1→清TI”的顺序,否则连续字符可能丢失或错乱。 while(TI==0);与while(!TI);都是阻塞等待;末尾分号表示空循环。UART_Send_Byte把三条发送语句封装起来,便于循环、换行和字符串发送复用。UART_Send_Str通过指针逐字节遍历字符串,在'\0'处结束;*send_str++表示发送当前字节后再移动指针。- 字符使用单引号,字符串使用双引号;UART传输的是字节,中文能否正常显示取决于双方字符编码是否一致。
- 普通发送函数与
interrupt 4串口中断函数不是同一个概念;查询发送可以不启用串口中断。
完成发送流程后,下一步是接收数据:UART接收硬件把一帧数据装入接收SBUF并置位RI,程序再通过串口中断读取和处理该字节。