STM32F103 学习笔记-21-串口通信(第4节)—串口发送和接收代码讲解(下)
本章面向STM32零基础新手基于STM32F103标准库开发从USART串口单字节发送的核心原理出发逐步扩展实现16位数据、数组、字符串发送功能并讲解C标准库printf/scanf的重定向方法。你可以把USART串口理解为STM32的“有线电话”——芯片通过它和电脑、传感器等外部设备“说话”发送数据或“听对方说话”接收数据本章核心就是教会STM32如何“说更长的句子、说格式化的话”。前置基础新手必看1. 核心概念通用同步/异步收发器Universal Synchronous/Asynchronous Receiver/Transmitter, USARTSTM32用于串行通信的外设支持异步通信如和电脑串口助手通信是嵌入式中最常用的通信方式之一。发送移位寄存器USART负责发送数据的核心硬件单次只能装下8位1字节数据——就像快递员的小包裹箱一次只能装1个8cm见方的包裹要发更大的包裹只能拆成小块分次发。TXE标志位Transmit Data Register Empty“包裹箱空了”的提示——表示发送数据寄存器已空可以放入下一个字节的数据。TC标志位Transmission Complete“所有包裹都送完了”的提示——表示最后一个字节已完全移出移位寄存器整组数据发送完成。2. 芯片架构关联STM32F103基于ARM Cortex-M3内核USART外设挂载在APB1/APB2总线上USART1在APB2USART2/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表示“未置位”0SET表示“已置位”1 // 知识点while循环是阻塞式等待——直到条件不满足才退出确保数据真正送入硬件 while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) RESET); }关键点解析USART_TypeDef * pUSARTxC语言指针的典型应用——通过指针传递不同串口外设如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码并非代码错误若数组元素为a97、b98不勾选可显示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。MicroLIBKeil专为嵌入式优化的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章节进一步理解寄存器工作原理和标志位时序。