本章面向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章节,进一步理解寄存器工作原理和标志位时序。