串口代码发送_2

Tutorial: 电子电路 Category: 嵌入式 Published: 2026-10-08 01:26:37 Views: 1 Likes: 0 Comments: 0

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
                                      │    │
                                      │    └─ 接收中断请求标志
                                      └────── 发送中断请求标志

本节的核心任务可以归纳为三步:

  1. 每次向SBUF写入一个字节后,等待硬件把TI置1。
  2. 由软件把TI清0,为下一次发送状态判断做好准备。
  3. 把单字节发送流程封装成函数,再利用指针逐字节发送字符串。
待发送字节 ─→ 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_ByteUART_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字节,而中文字符所占字节数取决于源码和终端采用的编码:

内容常见编码一个字符占用的字节数
英文AASCII、GBK、UTF-81
中文王GBK2
中文王UTF-83

因此“字符串由多个字节组成”是正确的,但“每个中文字符都是一个字节”并不正确。指针每次递增1会逐字节发送多字节编码;只要单片机程序中的字节编码与串口助手的解码设置一致,中文才会正常显示,否则会出现乱码。教学工程使用纯ASCII字符串,可以避开这一编码差异。

字符串发送的完整数据流如下:

字符串首地址
    │
    ▼
读取当前字节 ─→ 是否为'\0'? ──是──→ 结束
    │                    
    └────否──→ UART_Send_Byte ─→ 等待TI ─→ 指针+1 ─→ 回到判断

七、总结

串口查询发送需要掌握以下主线:

  1. TI是SCON中的发送标志位。方式1发送到停止位开始时由硬件置1,并由软件清0。
  2. 单字节发送必须遵循“写SBUF→等待TI=1→清TI”的顺序,否则连续字符可能丢失或错乱。
  3. while(TI==0);与while(!TI);都是阻塞等待;末尾分号表示空循环。
  4. UART_Send_Byte把三条发送语句封装起来,便于循环、换行和字符串发送复用。
  5. UART_Send_Str通过指针逐字节遍历字符串,在'\0'处结束;*send_str++表示发送当前字节后再移动指针。
  6. 字符使用单引号,字符串使用双引号;UART传输的是字节,中文能否正常显示取决于双方字符编码是否一致。
  7. 普通发送函数与interrupt 4串口中断函数不是同一个概念;查询发送可以不启用串口中断。

完成发送流程后,下一步是接收数据:UART接收硬件把一帧数据装入接收SBUF并置位RI,程序再通过串口中断读取和处理该字节。