串口代码发送_2

Tutorial: 电子电路 Category: 嵌入式 Published: 2026-08-29 08:09:36 Views: 0 Likes: 0 Comments: 0

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写入下一个字符时,前一个字符可能仍在发送。

TIRI都是串行控制寄存器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 = 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);

因为!TITI=0时为1,在TI=1时为0,所以两种写法效果相同。while(TI!=1);也能表达相同逻辑,但while(TI==0);通常更直观。

这个等待属于阻塞式查询。等待期间CPU不能处理主循环中的其他任务,代码简单但效率不高,适合入门实验和少量数据发送。实际项目还可以使用发送中断、环形缓冲区或DMA等方式减少阻塞;经典STC89C52 UART没有DMA,通常使用中断配合软件缓冲区。

为什么for循环需要大括号

C语言的forwhileif在没有大括号时,只控制紧随其后的第一条语句。要让“写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依次对应az。串口助手应重复显示:

abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz...

如果显示的字符仍然异常,应依次检查:

检查项本实验应采用的设置或现象
串口波特率9600
数据格式8位数据、无校验、1位停止位,即8N1
单片机晶振11.0592MHz
定时器1重装值TH1=TL1=0xFD,且使用12T计数条件
发送等待每个字节后都等待TI=1并清零
串口助手显示模式文本/ASCII显示,而不是只按HEX格式观察

编译并生成HEX文件后,应先关闭串口助手以释放COM端口,再由下载工具烧写。下载完成后重新打开串口助手观察输出。串口助手字体大小只影响显示效果,不改变接收到的字节内容;字符难以观察时,可以在软件的界面或主题设置中调大字体。

调试时还可以临时把循环上限设为20。这样只会发送at,可用于验证循环边界确实参与了输出控制;验证完成后再恢复为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_ByteUART_Routine
调用者由程序主动调用由硬件中断请求触发
执行条件程序运行到函数调用处(TI或RI) && ES && EA成立
当前用途查询TI并发送一个字节为后续串口中断收发预留入口
是否使用interrupt 4

配套代码采用查询方式等待TI,同时保留ES=1EA=1和空的中断函数作为后续实验基础。仅实现查询发送时,可以不启用ES;若已经启用串口中断,正式程序就应在中断函数内判断并及时清除TIRI,避免标志长期保持为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字节,而中文字符所占字节数取决于源码和终端采用的编码:

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

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

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

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

七、总结

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

  1. TISCON中的发送标志位。方式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,程序再通过串口中断读取和处理该字节。