简介神舟四号开发板上的STM32串口通信程序基于标准库函数实现了UART2的Printf输出和scanf输入面向正在学习串口通信、希望掌握标准库重定向以及中断接收处理机制的嵌入式开发者。程序压缩包共一百六十一个文件包括汇编源文件、头文件、C源文件等程序源码以及编译生成的HEX、AXF、MAP等工程文件同时附带说明文档和清理脚本整体体积约三点零七兆字节。目前已有八百六十四人学习下载。程序亲测可用通过将标准输入输出重定向到第二个串口并利用接收中断完成数据缓冲与解析完整演示了从引脚配置、串口初始化、波特率与校验位设置到数据收发、错误处理以及调试工具配合使用的全过程支持全双工通信可灵活配置常用串口参数。这套资源还提供详细的讲解文档和可复用的工程模板适合对照神舟四号开发板动手验证也有助于读者在此基础上扩展实现更复杂的串口通信功能。 做STM32串口调试的时候我见过太多人在printf重定向这一步卡到怀疑人生——网上代码拷下来烧进去串口助手就是不出字好不容易printf能用了scanf又直接卡死。这篇东西基于神舟IV号STM32F103ZET6库函数版实测通过把UART2的printf输出和scanf输入完整走一遍从原理到代码到串口助手设置全讲清楚。适合刚学STM32、正在做串口交互或者准备写命令行菜单的读者参考保证你照着敲完就能跑。1. 先搞明白为什么默认的printf和scanf在STM32上纯粹是“哑巴”很多初学者第一次碰这个需求时都会在main里顺手写一句printf(Hello World\r\n)然后发现串口助手上一片寂静于是开始怀疑是串口没初始化还是硬件坏了。其实根子出在stdio库和单片机之间的“断连”上。1.1 标准C库的输入输出到底走的是哪条路在PC上写C程序时printf会把字符通过一个叫stdout的流送到屏幕scanf从stdin流里读键盘输入。这两条流的底层是操作系统帮你把“文件描述符”和键盘、显示器关联好了。可STM32上压根没有操作系统管这摊事标准库里的printf内部最终会调用fputc往外吐字节scanf内部调用fgetc从外部取字节——问题是这两个函数默认不知道你的串口在哪。所以“重定向”的本质就是我们自己实现fputc和fgetc让printf输出的每个字符都走串口发送寄存器让scanf需要的每个字符都从串口接收寄存器拿。听起来简单但里面有个巨大的暗坑就是半主机模式。1.2 半主机模式程序莫名卡死的头号嫌疑犯ARM的C库在调试环境下默认会把fputc、fgetc这类底层函数交给“半主机模式”去处理也就是通过调试器把输入输出重定向到开发电脑的IDE终端上。你如果在开发板上直接跑这个逻辑程序执行到printf时会跳到一个BKPT 0xAB断点指令上等调试器响应没有调试器连接它就死等表现就是运行卡死、现象诡异。对付半主机有两条路一是在Keil里勾选Use MicroLIB微库默认关闭半主机是绝大多数人走的捷径二是自己实现_sys_exit等半主机相关函数把依赖彻底掐断。我的结论是做串口控制台、命令行交互这种场景直接用微库最省心坑最少。2. 神舟IV号上USART2的硬件连接与初始化配置标题既然点名了UART串口2神舟IV号这块板子默认就是把板载CH340 USB转串口接到了USART2上。所以第一步不是写代码而是确认硬件通路不然后面一切白搭。2.1 引脚和跳线帽最容易忽略的物理链路USART2对应的引脚是PA2(TX)和PA3(RX)。神舟IV号板载的CH340芯片与STM32之间一般有跳线帽或拨码开关控制连接位置你要确保跳线帽短接在USART2这一侧。不同批次的丝印可能标的是COM2或者UART2操作之前看一眼板子背面的原理图最稳妥。另外要注意CH340那边是TTL电平直接连STM32没问题但别把电脑串口线乱插到RS232接口上电平不匹配会烧芯片。用USB线连板子上的USB转串口口就行电脑会识别出一个COM口设备管理器里看到CH340或者对应驱动正常就算通路没问题。2.2 时钟树决定串口能不能跑起来STM32F103ZET6里USART2是挂在APB1总线上的而GPIOA挂在APB2上。这意味着初始化时要分清是哪条总线GPIOA时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE)USART2时钟RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE)很多人的USART2怎么都调不通就是把USART2的时钟用APB2开结果寄存器写进去根本没反应。APB1的最高频率是36MHzUSART2挂在APB1上所以你计算波特率分频时用的总线时钟是36MHz不像USART1挂在APB2上是72MHz。库函数封装好了这些细节但你不理解时钟树写到其他型号或者自己算分频时就容易出乱子。2.3 GPIO复用配置和串口参数PA2和PA3要配置成复用推挽输出和浮空输入GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); // TX PA2 GPIO_InitStructure.GPIO_Pin GPIO_Pin_2; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_Init(GPIOA, GPIO_InitStructure); // RX PA3 GPIO_InitStructure.GPIO_Pin GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); USART_InitTypeDef USART_InitStructure; USART_InitStructure.USART_BaudRate 115200; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_RX | USART_Mode_TX; USART_Init(USART2, USART_InitStructure); USART_Cmd(USART2, ENABLE);通信参数就定为115200、8位数据、1位停止位、无校验、无流控这是串口助手最通用的设置也是我实测最稳的组合。注意USART_Cmd这一步别漏漏了你前面全白写。3. 重定向fputc让printf开口的两条路线printf能不能输出拼的就是fputc写得够不够干脆。3.1 微库路线Keil里打个勾代码最简单用微库的前提是你在Keil的Options for Target里Target页的Code Generation分组下勾选了Use MicroLIB。然后实现一个fputcint fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) RESET); USART_SendData(USART2, (uint8_t)ch); return ch; }逻辑只有三步等上次发送完、把字符丢进发送寄存器、返回原值。TXE标志位表示发送数据寄存器空只有空了才能写入下一个字节否则会把上一个还没发完的数据覆盖掉。这个等待是关键不加的话快速连续printf可能出现丢字、乱码。3.2 标准库路线关掉半主机才敢上如果你就是不想用微库想在标准库环境下跑那就必须实现几个半主机相关的函数否则程序跑进printf直接HardFault。标准的做法是#pragma import(__use_no_semihosting) struct __FILE { int handle; }; FILE __stdout; void _sys_exit(int x) { x x; } void _ttywrch(int ch) { ch ch; } int fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) RESET); USART_SendData(USART2, (uint8_t)ch); return ch; }这一套东西里__use_no_semihosting是核心告诉链接器我不要半主机支持遇到相关调用你自己消化掉。相比之下微库路线省去了这些繁琐定义但微库也有一些限制比如对某些C99特性的支持弱一点在STM32这种资源敏感的场景下完全够用。我的经验是用微库把精力花在功能上别跟底层较劲。3.3 printf的换行问题为什么你的输出总是顶格重定向完成后第一句测试建议写printf(Hello STM32\r\n)注意后面带的是\r\n不是裸的\n。串口助手上换行需要回车\r把光标拉回行首再换行\n。只发\n的话很多串口助手显示时不会自动回到行首输出会变成一行叠着往右顶看起来就像没有换行一样。这个细节能避免你反复怀疑代码有问题。4. 重定向fgetcscanf能正常工作的分水岭printf搞定了以为scanf照葫芦画瓢就行天真。scanf比printf多了一层“输入解析”和“回显”的逻辑坑更集中在这里。4.1 fgetc的实现读一个字符顺手回显int fgetc(FILE *f) { uint8_t ch; while (USART_GetFlagStatus(USART2, USART_FLAG_RXNE) RESET); ch (uint8_t)USART_ReceiveData(USART2); while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) RESET); USART_SendData(USART2, ch); return (int)ch; }和fputc反着来等RXNE标志位置位也就是串口收到了一个字节然后从接收寄存器取出来再把它原样发送回去。这个“回显”不是闲得没事干是因为串口的输入输出是独立的物理通道你不主动发送回去串口助手上就看不到自己敲了什么输入体验极其别扭。回显做好之后串口助手上的表现就是你发送什么它就原样显示什么跟本地终端一模一样。4.2 scanf的阻塞等待和“发送新行”设置scanf是阻塞式的它会一直等在fgetc里直到拿到满足格式要求的字符。比如scanf(%d, num)会先跳过空白字符然后收集连续的数字字符碰到非数字字符才停下来。这里有个常见的翻车现场串口助手发送“123”过去scanf拿到1、2、3之后就一直在等第4个字符因为%s、%d这类格式需要“结束标记”才能返回。解决办法是串口助手里勾上“发送新行”让它在数据末尾自动补\r\nscanf拿到回车就知道输入结束了。不同串口助手这个选项名字不一样XCOM里叫“发送新行”sscom里叫“加回车换行”本质都是同一个东西。4.3 %c和%s的特殊情况回车、缓冲、缓冲区溢出用scanf(%c, ch)接收单个字符时回车符\r也可能被当成一个字符存进去。如果你发送的结尾带了\r\n那scanf读完目标字符后回车符还留在接收FIFO里下一个scanf会直接读到这个残留的\r表现为跳过输入、表现错乱。处理办法是在调用%c之前自己清空接收缓存或者在fgetc里主动忽略掉\r\n把回车换行滤掉if (ch \r || ch \n) ch 0;但这么干会影响scanf对数字输入的结束判断需要根据实际场景取舍。我在命令行菜单里一般不用%c全部用字符串解析的方式干净利落。%s还有缓冲区溢出的问题scanf(%s, buf)不管你buf多大它都往死里写。安全写法是限制宽度char buf[32]; scanf(%31s, buf);这样最多读入31个字符留一个位置给结束符。单片机资源本来就紧张这块地方值得多花两分钟写严谨。5. 完整可运行代码从初始化到命令行交互一气呵成把上面这些组合起来我给出一份亲测通过的完整示例。开发环境是Keil MDK标准外设库勾选MicroLIB芯片为STM32F103ZET6串口用USART2。#include stm32f10x.h #include stdio.h #include string.h void USART2_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_2; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); USART_InitStructure.USART_BaudRate 115200; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_RX | USART_Mode_TX; USART_Init(USART2, USART_InitStructure); USART_Cmd(USART2, ENABLE); } int fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) RESET); USART_SendData(USART2, (uint8_t)ch); return ch; } int fgetc(FILE *f) { uint8_t ch; while (USART_GetFlagStatus(USART2, USART_FLAG_RXNE) RESET); ch (uint8_t)USART_ReceiveData(USART2); while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) RESET); USART_SendData(USART2, ch); return (int)ch; } int main(void) { int num; char name[32]; USART2_Init(); printf(STM32 UART2 printf/scanf demo\r\n); printf(请输入一个整数: ); scanf(%d, num); printf(\r\n你输入的整数是: %d\r\n, num); printf(请输入你的名字(最多31个字符): ); scanf(%31s, name); printf(\r\n你好, %s!\r\n, name); while (1) { } }注意scanf(%d, num)之后我在printf前面加了\r\n是因为scanf在读数字时\r会被当成结束字符消费掉但\n很可能残留在FIFO里如果不先换一行接下来的printf输出会跟输入内容挤在同一行。实测中发现这个细节能避免很多看着像格式混乱的问题。串口助手这边这么设置波特率1152008N1打开串口发送区勾上“发送新行”。操作顺序是先给板子通电打开串口助手然后按一下板子上的复位键让程序从头跑一遍这样你能完整看到printf的输出序列和scanf的等待状态。6. 踩坑实录从卡死到中文乱码的病根排查这份代码能跑通不代表你不会在扩展时踩坑。把我在神舟IV号上实际遇到的高频问题列一下按出现的可能性排序供你排查时对照。现象根因解决printf后程序死循环没有勾选MicroLIB半主机模式未关闭勾选Use MicroLIB或实现__use_no_semihosting相关函数串口助手无任何输出USART2时钟错用APB2改成RCC_APB1PeriphClockCmd输出乱码或中文乱码波特率不匹配 / Keil源文件编码与串口助手编码不一致统一115200源文件用UTF-8编码串口助手对应设为UTF-8scanf卡住不返回串口助手未勾“发送新行”勾选发送新行让数据以\r\n结尾输入字符重复显示两次fgetc里做了回显串口助手又开了本地回显关掉串口助手中的本地回显选项scanf读数字读到一半不对发送的数据里混入了不是数字的字符确认发送内容避免中文输入法或多余空格6.1 中文乱码这个老大难STM32串口输出中文乱码九成是编码不一致造成的。Keil MDK老版本默认源码是ANSI编码在简体中文Windows下就是GBK而很多串口助手默认按UTF-8解码或者反过来源码是UTF-8串口助手按GBK解码都会乱码。处理思路很直接让源文件编码和串口助手解码保持一致。我的习惯是Keil里把源文件保存成UTF-8格式Edit - Configuration - Encoding选UTF-8串口助手用SSCOM或者Vofa这类支持UTF-8的工具。还有一个土办法就是printf里的中文字符串全部用英文彻底绕开编码问题。6.2 从USART2换到其他串口的改法如果你用的是别的开发板想把代码移植到USART1只需要改三处一是RCC时钟USART1挂在APB2上要改成RCC_APB2PeriphClockCmd二是GPIO引脚USART1的TX是PA9、RX是PA10三是函数里所有USART2改成USART1。其他逻辑不用动重定向代码是跟着串口句柄走的。6.3 中断接收和scanf的取舍串口项目玩深了之后你会发现scanf这种阻塞式输入在带操作系统或者带中断处理的工程里并不好用。我在状态机或者复杂交互的固件里更常用USART接收中断环形缓冲区配合解析函数来替代scanf。但初学者从scanf入手理解串口输入的概念是完全正确的路径它把“从物理层拿字节”和“格式解析”两个层次解耦得很清楚弄懂scanf的阻塞机制后续理解DMA加空闲中断的接收方案会轻松得多。最后再分享一个实际调试的小技巧在写交互程序前先把printf输出一个系统启动时间戳或者一段固定测试文字确认输出链路完全稳定之后再往上面叠加scanf逻辑。串口调试本质上是减少变量一次只验证一个环节出问题定位起来才快。这套代码我在神舟IV号上裸机跑通过你换个F103系列的板子只要引脚和时钟对应改好同样能跑。本文还有配套的精品资源点击获取
