本章面向STM32零基础新手,基于STM32F103标准库开发,从USART串口单字节发送的核心原理出发,逐步扩展实现16位数据、数组、字符串发送功能,并讲解C标准库printf/scanf的重定向方法。你可以把USART串口理解为STM32的"有线电话"------芯片通过它和电脑、传感器等外部设备"说话"(发送数据)或"听对方说话"(接收数据),本章核心就是教会STM32如何"说更长的句子、说格式化的话"。
前置基础(新手必看)
1. 核心概念
-
通用同步/异步收发器(Universal Synchronous/Asynchronous Receiver/Transmitter, USART):STM32用于串行通信的外设,支持异步通信(如和电脑串口助手通信),是嵌入式中最常用的通信方式之一。
-
发送移位寄存器 :USART负责发送数据的核心硬件,单次只能装下8位(1字节)数据------就像快递员的小包裹箱,一次只能装1个8cm见方的包裹,要发更大的包裹,只能拆成小块分次发。
-
TXE标志位(Transmit Data Register Empty):"包裹箱空了"的提示------表示发送数据寄存器已空,可以放入下一个字节的数据。
-
TC标志位(Transmission Complete):"所有包裹都送完了"的提示------表示最后一个字节已完全移出移位寄存器,整组数据发送完成。
2. 芯片架构关联
STM32F103基于ARM Cortex-M3内核,USART外设挂载在APB1/APB2总线上(USART1在APB2,USART2/3在APB1),其发送逻辑由硬件寄存器控制:我们通过操作寄存器(或标准库封装的函数),告诉硬件"要发什么数据",硬件会自动完成串行移位发送,同时通过标志位反馈"发送状态"。
核心前提:单字节发送函数(所有进阶功能的基础)
USART移位寄存器单次仅能发送8位数据,所有多字节发送功能,都需要基于单字节发送函数循环/分批次实现。
原理
通过库函数USART_SendData将字节写入发送寄存器,然后循环等待TXE标志位为"空",确保当前字节已进入移位寄存器,再进行下一次发送。
代码实例(可直接编译)
// 串口发送单字节函数
// pUSARTx: 串口外设(USART1/USART2/USART3等),本质是寄存器结构体指针
// ch: 待发送的8位数据(uint8_t对应C语言的无符号字符型,占1字节)
void Usart_SendByte(USART_TypeDef * pUSARTx, uint8_t ch)
{
// 知识点:USART_TypeDef是STM32标准库封装的串口寄存器结构体,pUSARTx是指向该结构体的指针
// 把待发送字节写入串口数据寄存器
USART_SendData(pUSARTx, ch);
// 等待TXE标志位为1(寄存器空),RESET表示"未置位"(0),SET表示"已置位"(1)
// 知识点:while循环是阻塞式等待------直到条件不满足才退出,确保数据真正送入硬件
while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);
}
关键点解析
-
USART_TypeDef * pUSARTx:C语言指针的典型应用------通过指针传递不同串口外设(如USART1、USART2),让函数支持多串口复用,无需为每个串口写重复代码。 -
阻塞式等待:新手入门阶段优先保证数据发送的可靠性,阻塞式等待是最简单的方式(后续进阶可改用中断/DMA)。
1. 16位(半字)数据发送函数
概念与原理
16位数据(半字,uint16_t,占2字节)无法单次发送,需拆分为高8位 和低8位两个字节,分两次调用单字节发送函数,就像把16cm的包裹拆成8cm×2的两个小包裹,先寄大的一半(高8位),再寄小的一半(低8位)。
配置/实现步骤
-
提取16位数据的高8位:用
& 0xFF00屏蔽低8位,再右移8位; -
提取16位数据的低8位:用
& 0x00FF屏蔽高8位; -
依次调用单字节函数发送高低8位。
代码实例
// 串口发送16位半字函数
void Usart_SendHalfWord(USART_TypeDef * pUSARTx, uint16_t ch)
{
uint8_t temp_h, temp_l;
// 知识点:位运算------&是按位与,>>是右移,嵌入式中常用位运算拆分/组合数据
// 提取高8位:0xFF00是16进制掩码,屏蔽低8位后右移8位,得到纯高8位
temp_h = (ch & 0xFF00) >> 8;
// 提取低8位:0x00FF屏蔽高8位,直接得到低8位
temp_l = ch & 0x00FF;
// 先发送高8位
Usart_SendByte(pUSARTx, temp_h);
// 后发送低8位
Usart_SendByte(pUSARTx, temp_l);
}
实验验证(新手必做)
-
主函数调用示例:
int main(void)
{
// 串口初始化(需提前实现,配置115200波特率、8位数据位、1位停止位、无校验)
USART_Config();
// 发送16位数据0xFF56
Usart_SendHalfWord(DEBUG_USARTx, 0xFF56);
while(1); // 死循环,防止程序退出
} -
现象说明:
-
串口调试助手勾选「十六进制显示」:接收到
FF 56(对应拆分的高低8位); -
不勾选十六进制:显示乱码(因为0xFF、0x56不是可打印ASCII字符),属于正常现象。
-
关键点解析
-
位运算:嵌入式开发中最常用的操作之一,用于拆分/组合数据、配置寄存器位,比算术运算更高效(硬件直接支持)。
-
数据类型:
uint16_t(无符号16位整数)、uint8_t(无符号8位整数)是嵌入式标准类型(定义在stdint.h),比int/char更明确,避免不同编译器的位数差异。
2. 8位数据数组批量发送函数
概念与原理
数组是连续存储的多个8位数据,就像一整箱8cm的小包裹,通过for循环逐个取出包裹,调用单字节函数发送;全部发完后,需等待TC标志位,确保最后一个"包裹"真正送到对方手里。
配置/实现步骤
-
传入数组首地址和元素个数;
-
循环遍历数组,逐个发送元素;
-
等待TC标志位,确认整组数据发送完成。
代码实例
// 串口发送8位数组函数
// array: 数组首地址(C语言中数组名本质是首元素指针)
// number: 数组元素个数(最大255,因为uint8_t范围0~255)
void Usart_SendArray(USART_TypeDef * pUSARTx, uint8_t *array, uint8_t number)
{
uint8_t i;
// 知识点:for循环遍历数组,嵌入式中常用遍历方式
for(i = 0; i < number; i++)
{
// 数组元素访问:*(array + i) 等价于 array[i]
Usart_SendByte(pUSARTx, array[i]);
}
// 等待整组数据发送完成(TC标志位),区别于单字节的TXE
while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TC) == RESET);
}
实验验证
-
主函数调用示例:
int main(void)
{
// 定义并初始化数组,10个8位数据
uint8_t send_array[10] = {1,2,3,4,5,6,7,8,9,10};
USART_Config(); // 串口初始化
// 发送数组:串口1、数组首地址、10个元素
Usart_SendArray(USART1, send_array, 10);
while(1);
} -
调试要点:
-
勾选「十六进制显示」:接收到
01 02 03 04 05 06 07 08 09 0A; -
不勾选:无可见字符(1~10是不可打印ASCII码),并非代码错误;
-
若数组元素为
'a'(97)、'b'(98),不勾选可显示ab。
-
关键点解析
-
数组与指针:
uint8_t *array接收数组首地址,array[i]等价于*(array + i),嵌入式中常通过指针操作硬件寄存器/数组,节省内存。 -
TC vs TXE:单字节发送等TXE(寄存器空),整组发送等TC(全部发完),混淆会导致最后一个字节发送不完整。
3. 字符串发送函数
概念与原理
C语言中字符串是以'\0'(空字符,ASCII码0)为结束标志的字符数组,就像一串有"终止符"的包裹,我们只需循环发送字符,直到遇到'\0'停止,无需提前知道字符串长度。
配置/实现步骤
-
传入字符串首地址;
-
用
do-while循环逐个发送字符; -
循环结束后等待TC标志位;
-
处理常见问题(如换行、变量初始化)。
代码实例
// 串口发送字符串函数
void Usart_SendString(USART_TypeDef * pUSARTx, char *str)
{
// 知识点:变量必须显式初始化!未初始化的i是随机值,会导致数组越界
uint8_t i = 0;
// do-while循环:至少执行一次(避免空字符串)
do
{
Usart_SendByte(pUSARTx, *(str + i));
i++;
} while(*(str + i) != '\0'); // 直到遇到结束符'\0'
// 等待字符串全部发送完成
while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TC) == RESET);
}
实验验证与常见问题
-
主函数调用示例:
int main(void)
{
USART_Config();
// 字符串末尾加\r\n(回车+换行),解决数据粘连问题
Usart_SendString(USART1, "STM32串口字符串测试\r\n");
Usart_SendString(USART1, "零基础也能学会!\r\n");
while(1);
} -
常见问题解决:
-
问题1:串口无输出→循环变量
i未初始化(随机值导致访问越界)→必须uint8_t i = 0;; -
问题2:字符串无换行→在字符串末尾加
\r\n(如"测试\r\n"); -
问题3:乱码→串口波特率/数据位配置不匹配(如初始化是115200,助手设为9600)。
-
关键点解析
-
字符串结束符:
'\0'是C语言字符串的核心标志,缺失会导致循环"跑飞"(访问内存中无关数据),嵌入式中内存越界可能导致程序崩溃。 -
do-while循环:区别于while循环,先执行后判断,确保空字符串也会进入循环(但发送0字节),更适配字符串发送场景。
4. C标准库输入输出函数重定向
STM32默认无法使用printf/scanf(这些函数默认向"电脑屏幕/键盘"读写),需重定向其底层函数,将读写逻辑绑定到串口------相当于把printf的"输出屏幕"改成"串口",scanf的"输入键盘"改成"串口"。
4.1 printf/putchar重定向
原理
printf/putchar底层都会调用fputc函数(标准库函数,负责输出单个字符),只需重写fputc,将字符输出逻辑替换为串口单字节发送,即可让printf通过串口打印。
配置步骤
-
代码实现(添加到串口驱动.c文件):
// 必须包含标准库头文件,否则无法识别FILE、fputc
#include "stdio.h"// 知识点:函数重写------自定义fputc覆盖标准库默认实现
// ch: 待输出的字符;f: 文件指针(printf默认忽略,仅兼容标准库格式)
int fputc(int ch, FILE *f)
{
// 将int型ch转为uint8_t(仅保留低8位,符合串口发送要求)
USART_SendData(DEBUG_USARTx, (uint8_t) ch);
// 阻塞等待TXE标志位
while (USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_TXE) == RESET);
return (ch); // 返回字符,兼容标准库调用逻辑
} -
Keil MDK工程配置(关键!):
-
点击
Options for Target→Target→ 勾选Use MicroLIB(微库); -
微库是精简版C标准库,适配嵌入式场景,不勾选则重定向失效。
-
实验验证
#include "stdio.h" // 使用printf必须包含
int main(void)
{
uint16_t num = 1234;
float temp = 25.68f;
USART_Config();
// 知识点:printf格式化输出------嵌入式中常用作调试信息打印
printf("STM32 printf重定向测试\r\n");
printf("数字:%d,十六进制:0x%X\r\n", num, num);
printf("温度:%.2f℃\r\n", temp);
// putchar同步生效,发送单个字符
putchar('!');
while(1);
}
现象:串口助手(取消十六进制显示)显示:
STM32 printf重定向测试
数字:1234,十六进制:0x4D2
温度:25.68℃
!
4.2 scanf/getchar重定向
原理
scanf/getchar底层调用fgetc函数(负责读取单个字符),重写fgetc,将字符读取逻辑替换为串口接收,即可通过串口输入数据。
代码实现
#include "stdio.h"
// 重写fgetc,绑定串口接收
int fgetc(FILE *f)
{
// 阻塞等待串口接收数据(RXNE标志位:接收寄存器非空)
while (USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_RXNE) == RESET);
// 读取接收寄存器数据并返回
return (int)USART_ReceiveData(DEBUG_USARTx);
}
实验验证
#include "stdio.h"
int main(void)
{
char ch;
int num;
USART_Config();
printf("请输入一个字符:");
// getchar读取串口输入的字符
ch = getchar();
printf("你输入的字符是:%c\r\n", ch);
printf("请输入一个数字:");
// scanf格式化读取串口输入的数字
scanf("%d", &num);
printf("你输入的数字是:%d\r\n", num);
while(1);
}
注:该函数是阻塞式接收 ------程序会卡在
while处,直到串口接收到数据,适合简单的指令交互场景。
关键点解析
-
函数重写:嵌入式中常用的技巧,通过重写标准库/底层函数,适配硬件场景,无需修改上层代码(如直接用
printf)。 -
MicroLIB:Keil专为嵌入式优化的C标准库,体积小、适配裸机开发,默认标准库不支持重定向
fputc/fgetc。
5. 关键调试与兼容性
5.1 串口调试助手配置规则(新手必记)
| 发送数据类型 | 串口助手显示配置 | 示例现象 |
|---|---|---|
| 16位数据、数组(原始数值) | 勾选「十六进制显示」 | 发送0xFF56 → 显示FF 56 |
| 字符串、printf格式化输出 | 取消「十六进制显示」 | 发送"测试" → 显示测试 |
5.2 常见避坑点
-
循环变量未初始化→数组/字符串访问越界→串口无输出;
-
TXE/TC标志位混淆→最后一个字节发送不完整;
-
未勾选MicroLIB→printf重定向失效;
-
字符串无
'\0'→发送乱码/程序崩溃; -
串口初始化参数(波特率、数据位)与助手不匹配→乱码。
5.3 跨平台兼容性
-
跨串口:函数通过
pUSARTx形参指定串口,只需修改初始化代码,即可从USART1移植到USART2/3; -
跨芯片:核心逻辑兼容Cortex-M3/M4/M7内核的STM32(如F4/F7系列),仅需调整库函数名(如HAL库改为
HAL_UART_Transmit)。
小结
-
USART串口单次仅能发送8位数据,多字节发送需基于单字节函数循环/拆分实现;
-
16位数据拆分为高低8位发送,数组通过for循环遍历发送,字符串通过
'\0'判断结束; -
printf/scanf重定向的核心是重写fputc/fgetc,并配置MicroLIB; -
嵌入式开发中,位运算、指针、变量初始化、标志位判断是核心基础,需熟练掌握。
思考
-
阻塞式发送/接收会占用CPU资源,如何通过中断实现非阻塞的串口发送/接收?
-
除了标准库,STM32HAL库中如何实现串口多字节发送和printf重定向?
-
若要发送浮点型数据(如3.1415),如何基于现有函数实现?
-
串口接收数据时,如何避免因数据丢失导致的程序异常?(提示:缓冲区)
建议查阅STM32F103官方参考手册(RM0008)的USART章节,进一步理解寄存器工作原理和标志位时序。