串口代码发送_2
049.6.5 串口代码发送(2)
/*
* 整体目的:初始化STC89C52的UART方式1,以9600波特可靠发送a~z、换行符和字符串。
* 整体设计:定时器1模式2提供波特率时基;UART_Send_Byte通过TI同步相邻字节;
* UART_Send_Str遍历以'\0'结尾的字符串并逐字节复用发送函数。
* 时钟依据:TH1/TL1取0xFD的条件是11.0592MHz晶振、定时器1为12T、SMOD=0。
* 硬件路径:写SBUF后,UART从P3.1/TXD输出数据,板载CH340N将TTL UART转换为USB串口。
* 课程上下文:本节由“连续写SBUF导致字符错乱”入手,重点掌握等待TI并由软件清TI;
* 源码为后续接收课程打开了串口中断,但当前中断函数仍是空的预留入口。
*/
#include "reg52.h" /* 引入8052特殊功能寄存器声明,使程序可以使用SCON、SBUF、TI等名称。 */
#include <intrins.h> /* 引入Keil C51内部函数声明,软件延时中的_nop_()由该头文件提供。 */
typedef unsigned char uchar; /* 为8位无符号字符型建立简写,单个UART数据字节使用该类型。 */
typedef unsigned int uint; /* 为C51的16位无符号整型建立简写,用作计数器和延时参数。 */
uint count = 0; /* 定义全局循环计数器,取0~25时分别生成ASCII字符'a'~'z'。 */
sbit LED1 = P1^0; /* 将LED1名称绑定到P1.0;这是模板遗留定义,本节发送流程没有使用它。 */
sbit LED2 = P1^1; /* 将LED2名称绑定到P1.1;本节没有修改该引脚,不影响串口发送。 */
/*
* 函数目的:按照参数执行毫秒级软件忙等待。
* 时间前提:计数值按11.0592MHz和当前Keil C51指令耗时生成;改变时钟或优化会产生误差。
* 当前状态:main没有调用该函数,因此每轮测试数据之间没有人为增加等待时间。
*/
void Delay_xms(uint xms) /* 定义软件延时函数,xms表示计划等待的毫秒数。 */
{ /* 延时函数体开始,执行期间CPU在前台忙等。 */
uchar data i, j; /* 定义两个8位循环计数器,并放入片内直接寻址data RAM。 */
while (xms) /* 每执行一轮约1ms的内层循环,就减少一次剩余毫秒数。 */
{ /* 一毫秒延时循环开始;xms为0时不会进入。 */
_nop_(); /* 执行空操作,不改变业务状态,仅补齐计算出的延时时间。 */
i = 2; /* 初始化外层8位计数器,控制j循环需要重复的轮数。 */
j = 199; /* 初始化内层计数器,其递减指令消耗构成主要延时。 */
do /* 使用后判断循环,保证循环体至少执行一次。 */
{ /* do循环体开始,此处没有业务,仅消耗CPU时间。 */
while (--j); /* 先把j减1,非0就继续空转;分号表示循环体为空。 */
} while (--i); /* j循环结束后把i减1,非0则借助8位回绕继续计数。 */
xms--; /* 一轮约1ms延时完成,将剩余毫秒数减1。 */
} /* xms非0时继续下一轮,否则退出忙等待。 */
} /* 延时函数结束并返回调用者。 */
/* 函数目的:封装一个字节的查询发送,防止CPU过快地连续写入发送缓冲器。 */
void UART_Send_Byte(uchar send_byte) /* send_byte保存本次需要发送的8位数据。 */
{ /* 单字节发送流程开始。 */
SBUF = send_byte; /* 写发送SBUF并启动UART硬件输出一帧数据。 */
while (TI == 0); /* TI尚未置位时原地等待;末尾分号表示空循环。 */
TI = 0; /* 硬件置位TI后由软件清0,为下一帧完成事件做准备。 */
} /* 当前字节发送到规定时刻,函数返回。 */
/* 函数目的:遍历以'\0'结尾的C字符串,并调用单字节函数依次发送所有正文字符。 */
void UART_Send_Str(uchar *send_str) /* send_str指向待发送字符串的首字符。 */
{ /* 字符串发送函数体开始。 */
while (*send_str != '\0') /* 解引用指针;当前字符不是NUL结束标志时继续发送。 */
{ /* 当前正文字符的发送分支开始。 */
UART_Send_Byte(*send_str++); /* 发送当前字符,随后把指针移动到下一个字符。 */
} /* 回到条件处检查新字符,遇到'\0'便结束。 */
} /* 字符串正文发送完毕,结束标志本身不会被发送。 */
/* 函数目的:初始化UART和定时器1,然后循环输出课程中的测试数据序列。 */
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; /* 保存每次TL1溢出后由硬件装入的重装值253。 */
TL1 = 0xFD; /* 让第一次计数也从253开始,与后续周期保持一致。 */
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',并可靠发送当前字符。 */
} /* 26个小写字母发送完毕。 */
/* 以下三行是已禁用的直接换行发送方案,实际代码统一调用UART_Send_Byte。 */
// SBUF = '\n';
// while (TI == 0); // while (!TI);
// TI = 0;
UART_Send_Byte('\n'); /* 发送LF(0x0A),让终端光标移动到下一行。 */
UART_Send_Byte('\n'); /* 再发一个LF,使相邻两轮输出之间空出一行。 */
UART_Send_Str("i am wfeng!\r\n"); /* 发送字符串正文及CR+LF,回车后从下一行行首继续。 */
} /* 本轮结束后立即开始下一轮,没有额外延时。 */
} /* 主函数语法边界;正常运行时不会从无限循环返回。 */
/*
* 函数目的:为串口中断号4预留服务函数入口。
* 当前限制:函数体不判断、不清除TI/RI,也不处理数据,不能视为完整中断收发逻辑。
*/
void UART_Routine(void) interrupt 4 /* Keil把该函数连接到串口中断向量地址0x0023。 */
{ /* 空中断函数体开始。 */
} /* 空中断函数结束;后续接收课程会补充RI处理。 */
一、任务资源图
上一阶段连续执行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置位做准备。 */
TI = 0; /* 将SCON.1清0;UART硬件只负责置位TI,不会在软件读到后自动清除。 */
如果不等待TI就连续写入SBUF,CPU的写入速度会远高于串口移位速度。9600波特、8N1时,一个字节至少占用:
$ T_{byte}=\frac{1+8+1}{9600}\approx1.0417\text{ms} $
经典8051发送硬件没有能保存整串字符的多字节FIFO,因此连续快速写入会丢失待发送字符,串口助手看到的内容就无法保持预期顺序。
三、发送一个字符
3.1 代码讲解与串口发送原理
查询方式发送一个字节的完整流程为:
/* 片段目的:按照“启动发送→等待完成标志→软件复位”的顺序可靠发送字符a。 */
SBUF = 'a'; /* 把ASCII码0x61写入发送SBUF,启动一帧UART传输。 */
while (TI == 0); /* TI仍为0时执行空循环,阻止程序过早写入下一个字节。 */
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 == 0);等价的简写查询方式。 */
while (!TI); /* TI为0时逻辑非结果为1并继续空转,TI变为1时条件为0而退出。 */
因为!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”三条语句都属于循环体,必须使用大括号:
/* 片段目的:利用ASCII编码连续性并结合TI查询,依次可靠发送a~z。 */
for (count = 0; count < 26; count++) /* count取0~25,共执行26轮。 */
{ /* 当前字母的一次发送开始。 */
SBUF = 'a' + count; /* 在'a'的编码上增加偏移量,生成并发送当前小写字母。 */
while (TI == 0); /* 等待当前帧完成,避免下一个循环过早改写SBUF。 */
TI = 0; /* 软件清除完成标志,准备检测下一字节。 */
} /* 当前字节完成后count递增,继续下一个字母。 */
若省略大括号,只有SBUF='a'+count;会循环26次,等待和清零只在循环结束后执行一次,仍会导致发送错乱。
3.2 例题1: 串口发送的代码实现与调试
发送a~z的代码为:
/* 例题实现:发送26个小写字母,每一轮都等待并清除本字节的TI。 */
for (count = 0; count < 26; count++) /* 初始化count=0;小于26时循环;每轮结束后加1。 */
{ /* for循环体开始。 */
SBUF = 'a' + count; /* 生成ASCII字符'a'到'z'并写入发送SBUF。 */
while (TI == 0); /* 当前帧未完成时阻塞等待。 */
TI = 0; /* 当前帧完成后清TI,使下一帧有独立的完成事件。 */
} /* 26轮结束后退出循环。 */
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循环之后再发送换行字符:
/* 片段目的:发送一个LF换行字符,并等待该字符的UART帧完成。 */
SBUF = '\n'; /* 将转义字符'\n'对应的ASCII码0x0A写入发送SBUF。 */
while (TI == 0); /* 等待UART硬件发送到置位TI的时刻。 */
TI = 0; /* 软件清TI,为后续字节发送做准备。 */
如果想留出一个空行,可以连续发送两个'\n',但每一个字节都必须拥有自己的等待和清零流程:
/* 片段目的:连续发送两个LF;每个字节都有各自的等待和清标志步骤。 */
SBUF = '\n'; /* 启动第一个LF(0x0A)字符的发送。 */
while (!TI); /* TI为0时等待,直到第一个LF发送到规定完成时刻。 */
TI = 0; /* 清除第一个LF产生的TI。 */
SBUF = '\n'; /* 第一个字节处理完后,再启动第二个LF的发送。 */
while (!TI); /* 等待第二个LF对应的TI置位。 */
TI = 0; /* 清除第二个LF的完成标志,结束本片段。 */
'\n'是换行符LF,ASCII值为0x0A;'\r'是回车符CR,ASCII值为0x0D。不同终端对换行的处理存在差异:
| 发送内容 | 可能的显示效果 |
|---|---|
"\n" | 移动到下一行,但不一定回到行首 |
"\r" | 回到当前行行首,但不下移 |
"\r\n" | 回到行首并进入下一行,串口文本输出中最通用 |
因此发送完整文本行时,通常推荐使用"\r\n"。字符常量使用单引号,例如'\n';字符串使用双引号,例如"\r\n"。
反复出现的三条发送语句适合封装成单字节发送函数,避免某个位置漏写等待或漏清TI。
五、封装发送字节函数
5.1 封装发送字节函数的代码实现
单字节发送函数定义如下:
/* 函数目的:把重复的查询发送三步封装为一个可传入任意8位数据的函数。 */
void UART_Send_Byte(uchar send_byte) /* 定义无返回值函数,send_byte是本次待发字节。 */
{ /* 函数体开始。 */
SBUF = send_byte; /* 把调用者传入的数据写入发送SBUF。 */
while (TI == 0); /* 阻塞等待UART硬件置位发送完成标志。 */
TI = 0; /* 软件清TI,为下次调用准备。 */
} /* 当前字节发送到规定时刻,函数返回。 */
函数包含四个组成部分:
| 组成 | 代码 | 含义 |
|---|---|---|
| 返回类型 | 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); /* 发送由当前count生成的一个小写字母。 */
UART_Send_Byte('\n'); /* 发送一个ASCII换行LF;函数内部负责等待并清TI。 */
这样,for循环可简化为:
/* 片段目的:用已封装的单字节函数简化a~z的可靠循环发送。 */
for (count = 0; count < 26; count++) /* 产生0~25这26个字符偏移量。 */
{ /* 当前字母的发送循环体开始。 */
UART_Send_Byte('a' + count); /* 生成当前字符,函数内部完成写SBUF、等TI和清TI。 */
} /* 当前字节完成后进入下一轮。 */
封装没有改变底层发送流程,只是把重复且必须成组出现的代码集中到一个函数中。以后如果发送策略变化,也只需统一调整这个函数。
5.2 串口的中断处理函数
UART_Send_Byte是普通阻塞函数,不是中断处理函数。真正的串口中断函数带有interrupt 4修饰:
/*
* 函数目的:为后续串口中断收发预留中断号4入口。
* 当前限制:空函数没有判断或清除TI、RI;若正式使用中断,必须补充对应处理。
*/
void UART_Routine(void) interrupt 4 /* 声明经典8051的串口中断服务函数。 */
{ /* 空函数体开始。 */
} /* 立即中断返回;本节核心发送逻辑采用查询方式。 */
两类函数的区别如下:
| 对比项 | UART_Send_Byte | UART_Routine |
|---|---|---|
| 调用者 | 由程序主动调用 | 由硬件中断请求触发 |
| 执行条件 | 程序运行到函数调用处 | (TI或RI) && ES && EA成立 |
| 当前用途 | 查询TI并发送一个字节 | 为后续串口中断收发预留入口 |
是否使用interrupt 4 | 否 | 是 |
配套代码采用查询方式等待TI,同时保留ES=1、EA=1和空的中断函数作为后续实验基础。仅实现查询发送时,可以不启用ES;若已经启用串口中断,正式程序就应在中断函数内判断并及时清除TI、RI,避免标志长期保持为1而反复产生中断请求。
函数调用时,实参会传给形参。例如:
/* 片段目的:演示调用封装函数发送一个确定的ASCII字符。 */
UART_Send_Byte('x'); /* 传入字符常量'x'(0x78),函数内部完成整套查询发送。 */
调用瞬间,字符'x'对应的字节值进入形参send_byte,函数再把它写入SBUF。把循环上限从26临时设为20,只会改变调用次数和传入内容,不会改变发送函数的内部工作方式。
单字节发送函数一次只处理一个字节。如果要发送一句文字,需要再封装字符串发送函数,让它重复调用UART_Send_Byte。
六、应用案例
6.1 发送一个字符串
完整程序通过下面的调用发送字符串:
/* 片段目的:调用字符串发送函数,输出正文并用CR+LF结束当前文本行。 */
UART_Send_Str("i am wfeng!\r\n"); /* 双引号生成以'\0'结尾的字符串,函数逐字节发送但不发送'\0'。 */
双引号中的内容是C字符串,编译器会在末尾自动添加'\0'结束符。其字节排列可以表示为:
地址递增方向 →
'i' ' ' 'a' 'm' ' ' 'w' 'f' 'e' 'n' 'g' '!' '\r' '\n' '\0'
│ │
└─ send_str最初指向这里 循环在这里停止
UART_Send_Str不是让UART一次发送整段字符串。UART每次仍只接收一个字节,字符串函数只是自动完成“取一个字节→调用单字节发送→指针后移→判断结束符”的循环。
使用字符串函数时要确保:
- 参数确实指向以
'\0'结尾的有效字符串。 - 字符串所在内存在整个发送期间保持有效。
- 串口助手和程序对非ASCII内容使用相同字符编码。
- 字符串很长时,要意识到该函数会一直阻塞到全部字节发送完成。
6.2 回到发送字符串函数定义
字符串发送函数如下:
/*
* 函数目的:从字符串首地址开始逐字节发送,直到遇到C字符串结束标志'\0'。
* 依赖关系:UART_Send_Byte负责每个字节的SBUF写入、TI等待和TI清除。
*/
void UART_Send_Str(uchar *send_str) /* 形参是指向8位字符的指针,接收字符串首地址。 */
{ /* 字符串发送函数体开始。 */
while (*send_str != '\0') /* 检查当前地址处的字符,不是NUL就继续循环。 */
{ /* 当前字符属于字符串正文。 */
UART_Send_Byte(*send_str++); /* 发送当前字符,再将指针移到下一个字符地址。 */
} /* 返回条件处继续判断,遇到NUL时退出。 */
} /* 字符串发送结束,NUL仅作边界而不发到串口。 */
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++); /* 后缀++优先结合指针,先解引用旧地址发送,再让send_str指向下一字符。 */
由于后缀++的优先级高于一元*,它等价于:
/* 片段目的:借助括号明确展示后缀自增先作用于指针、解引用使用旧指针值。 */
UART_Send_Byte(*(send_str++)); /* 与*send_str++等价:发送当前字符后再把指针向后移动一个元素。 */
执行顺序可以理解为:先取得当前指针,读取并发送它指向的字节,然后让send_str指向下一个字节。为了初学时更直观,也可以写成两句:
/* 片段目的:把复合表达式拆成两句,直观展示“先发送当前字符,再移动指针”。 */
UART_Send_Byte(*send_str); /* 解引用当前指针并发送其指向的一个字符,指针暂时不变。 */
send_str++; /* 指针前进一个uchar元素,下一轮将读取字符串中的下一个字符。 */
为什么必须判断'\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,程序再通过串口中断读取和处理该字节。