资讯动态

STM32 USART2重定向printf/scanf避坑指南(神舟IV号专用)

发布时间:2026/10/26 4:37:53 来源:尧图企业网站定制
简介本资源是一份面向STM32初学者与嵌入式开发者的实战型UART通信教学程序聚焦于UART2模块的printf格式化输出与scanf交互式输入功能实现解决库函数环境下标准I/O重定向这一典型学习难点。压缩包共161个文件含28个C源码如stm32f10x_rcc.c、stm32f10x_uart.c等核心驱动、29个头文件h、32个汇编文件s及若干编译中间文件o、d、crf和工程配置文件uvproj、uvopt、sct等完整保留Keil MDK-ARM神舟IV号开发板工程结构总大小3.07MB。已有866人学习下载说明其在实践教学中具备较强参考价值。读者可直接导入MDK工程编译运行获取已验证的UART2初始化配置、_write()重定向实现、中断式接收缓冲区设计、简易scanf解析逻辑及配套串口调试方法同时附带清晰的中文注释与模块化组织便于理解底层通信机制并快速迁移至其他UART端口或项目场景。1. 为什么串口2要抢在串口1前面做Printf重定向——一个被忽略的硬件资源冲突真相很多同学在做STM32课设或毕设时一上来就照着例程把printf重定向到USART1结果烧录后串口调试助手一片死寂反复检查引脚、波特率、初始化顺序甚至怀疑ST-Link坏了。直到某次偶然把printf切到USART2屏幕突然刷出“Hello World”才意识到不是代码写错了而是你根本没看清神舟IV号开发板的硬件设计——它的USART1默认复用在JTAG/SWD调试接口上一旦开启SWD下载USART1的TX/RX引脚就被硬件锁死了。这不是玄学是真实存在的引脚复用冲突。本方案专为神舟IV号库函数版定制完整实现USART2的printf输出与scanf输入双向通信不碰USART1一根线绕过所有常见翻车点。适合正在赶STM32课程设计、毕设答辩前一周还在调串口的同学也适合需要快速验证外设交互逻辑的嵌入式工程师。整套代码已在Keil MDK-ARM V5.37 STM32F103ZET6神舟IV号主控实测通过支持回车换行、数字/字符串混合输入、缓冲区溢出防护不是简单打个字符就完事的Demo。2. 总体架构设计USART2独立通道标准C库重定向环形输入缓冲的三层结构2.1 硬件资源映射与引脚锁定策略神舟IV号开发板的USART2物理连接非常明确TX → PA2RX → PA3。但关键在于——必须禁用JTAG/SWD对PA2/PA3的复用干扰。很多同学直接复制正点原子的USART1例程忘了神舟IV号的JTAG默认占用PA13/PA14/PA15而PA2/PA3是干净的GPIO可直接复用为USART2功能。我们采用“先禁用再配置”原则在RCC_Configuration()中关闭JTAG复用虽不影响PA2/PA3但避免潜在干扰再通过GPIO_PinRemapConfig(GPIO_Remap_USART2, ENABLE)显式启用USART2重映射——注意这里不是重映射到其他引脚而是确认其默认映射有效。实际测试发现若跳过此步在某些固件版本下USART2接收中断会偶发丢失。// system_stm32f10x.c 中 RCC 初始化片段关键修改 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); // 必须加这一句否则部分芯片USART2接收不稳定 GPIO_PinRemapConfig(GPIO_Remap_USART2, ENABLE); // 启用USART2默认映射PA2/PA3提示神舟IV号原理图中标注USART2为“独立串口”即不与调试器共用这是本方案能稳定工作的物理基础。若你的板子USART2引脚被其他外设如CAN占用请先查原理图确认PA2/PA3是否空闲。2.2 标准C库重定向核心_write与_read函数的底层劫持printf和scanf本质是调用C库的_write和_read系统调用。在STM32库函数环境下需在syscalls.c中重写这两个函数使其指向USART2的发送/接收逻辑。重点在于_write必须阻塞等待发送完成_read必须支持非阻塞轮询超时否则scanf会卡死。我们不使用HAL_Delay而是用while(!(USART2-SR USART_FLAG_TC));检测发送完成标志TC比TXE更可靠——TXE只表示数据已移入移位寄存器TC才表示字节真正发出去了。// syscalls.c #include stm32f10x_usart.h #include stm32f10x_gpio.h // _write: 重定向printf输出到USART2 int _write(int fd, char *ptr, int len) { if ((fd ! 1) (fd ! 2)) return -1; // 只处理stdout/stderr for (int i 0; i len; i) { while (!(USART2-SR USART_FLAG_TC)); // 等待上一字节发送完成 USART_SendData(USART2, *ptr); } return len; } // _read: 重定向scanf输入支持回车结束、退格删除、超时退出 int _read(int fd, char *ptr, int len) { if (fd ! 0) return -1; // 只处理stdin int i 0; uint32_t start_tick SysTick_GetCurrentTick(); // 假设已初始化SysTick while (i len - 1) { if (USART_GetFlagStatus(USART2, USART_FLAG_RXNE) ! RESET) { char c USART_ReceiveData(USART2); if (c \r || c \n) break; // 回车或换行结束输入 if (c 0x08 || c 0x7F) { // 退格键ASCII 0x08 或 DEL 0x7F if (i 0) { i--; *ptr-- \0; _write(1, \b \b, 3); // 发送退格空格退格清除屏幕字符 } } else if (c 32 c 126) { // 可见字符 *ptr c; i; _write(1, c, 1); // 回显 } } // 超时保护10秒无输入则退出 if ((SysTick_GetCurrentTick() - start_tick) 10000) break; } *ptr \0; // 字符串结尾 return i; }这段代码的关键设计点有三_write用TC而非TXE避免多字节连续发送时因TXE过早置位导致字节错乱_read内置退格处理不是简单丢弃0x08而是主动发送\b \b三字节序列实现终端回退擦除这对用户交互体验至关重要超时机制绑定SysTick防止scanf无限等待10秒无操作自动返回避免程序假死——这是答辩演示时最怕的翻车点。2.3 环形输入缓冲区解决scanf输入粘包与缓冲区溢出标准_read实现存在硬伤当用户快速输入一长串字符如1234567890scanf(%s, buf)可能只读取前几个字节就因缓冲区满而截断且下次调用会继续读剩余字符造成“输入粘连”。本方案在_read之上增加一层环形缓冲管理由usart2_buffer.c实现// usart2_buffer.c #define RX_BUFFER_SIZE 64 static uint8_t rx_buffer[RX_BUFFER_SIZE]; static volatile uint16_t rx_head 0; static volatile uint16_t rx_tail 0; // USART2接收中断服务函数需在stm32f10x_it.c中注册 void USART2_IRQHandler(void) { USART_TypeDef* USARTx USART2; uint8_t res; if (USART_GetITStatus(USARTx, USART_IT_RXNE) ! RESET) { res USART_ReceiveData(USARTx); // 环形缓冲写入满则丢弃新字节防溢出 uint16_t next_head (rx_head 1) % RX_BUFFER_SIZE; if (next_head ! rx_tail) { // 缓冲未满 rx_buffer[rx_head] res; rx_head next_head; } } } // 从环形缓冲读取一行以\r\n结尾 int usart2_getline(char *buf, int len) { int i 0; while (i len - 1) { if (rx_head rx_tail) break; // 缓冲为空 char c rx_buffer[rx_tail]; rx_tail (rx_tail 1) % RX_BUFFER_SIZE; if (c \r || c \n) break; buf[i] c; } buf[i] \0; return i; }注意此环形缓冲与_read函数解耦_read仍负责基础字符收发而usart2_getline供上层业务逻辑调用。这样设计的好处是——你可以用scanf做简单输入也可以用usart2_getline做精准行解析互不干扰。3. 模块化实现从时钟配置到主循环的六步落地清单3.1 第一步RCC时钟树配置——精确到每一位的分频计算神舟IV号主频为72MHzUSART2挂载在APB1总线上最高36MHz。要得到经典115200bps波特率需计算USARTDIV值$$ USARTDIV \frac{f_{PCLK1}}{16 \times BaudRate} \frac{36000000}{16 \times 115200} 19.53125 $$整数部分MANTISSA190x13小数部分FRACTION0.53125×168.5→取整为90x09。因此USARTDIV 0x139。这个值必须手算验证不能依赖CubeMX自动生成——因为库函数版不带自动配置。// usart2_init.c void USART2_Config(void) { USART_InitTypeDef USART_InitStructure; GPIO_InitTypeDef GPIO_InitStructure; // 1. 使能时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); // 2. 配置PA2/PA3为复用推挽输出TX和浮空输入RX GPIO_InitStructure.GPIO_Pin GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; 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); // 3. USART2初始化115200, 8N1, 无流控 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); // 4. 使能USART2及接收中断 USART_ITConfig(USART2, USART_IT_RXNE, ENABLE); // 仅开RXNE中断 USART_Cmd(USART2, ENABLE); // 5. NVIC配置若使用中断 NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel USART2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 2; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); }关键参数说明GPIO_Mode_IN_FLOATING神舟IV号RX引脚必须设为浮空输入若设为上拉/下拉可能因电平竞争导致接收误码USART_IT_RXNE只开接收非空中断不开启发送中断_write用轮询降低中断负载优先级设为2避开SysTick通常为0和EXTI0常为1保证串口响应及时。3.2 第二步SysTick初始化——为超时机制提供毫秒基准_read中的10秒超时依赖SysTick。神舟IV号标准库中SysTick_Config(SystemCoreClock / 1000)即可生成1ms滴答。但要注意必须在main()中USART2_Config()之前调用否则_read里的SysTick_GetCurrentTick()会返回0。// main.c #include core_cm3.h // 必须包含否则SysTick函数不可用 uint32_t systick_count 0; void SysTick_Handler(void) { systick_count; } uint32_t SysTick_GetCurrentTick(void) { return systick_count; } int main(void) { // 1. 初始化SysTick必须最先 if (SysTick_Config(SystemCoreClock / 1000)) while (1); // 1ms // 2. 初始化USART2 USART2_Config(); // 3. 其他外设... }3.3 第三步主循环逻辑——printf与scanf的典型用法示范在main()中演示printf和scanf的真实协作场景避免“Hello World”式无效验证// main.c 主循环片段 int main(void) { // ... SysTick USART2 初始化略 printf(\r\n STM32神舟IV号 USART2 Printf/Scanf 测试 \r\n); printf(请输入一个整数0-100); int num; if (scanf(%d, num) 1) { // scanf返回成功读取项数 if (num 0 num 100) { printf(您输入的数字是%d\r\n, num); printf(它的平方是%d\r\n, num * num); } else { printf(错误请输入0-100之间的整数\r\n); } } else { printf(输入格式错误请输入数字。\r\n); } // 字符串输入示例 char name[20]; printf(请输入您的姓名最多15字); if (scanf(%15s, name) 1) { // 限制长度防溢出 printf(欢迎%s\r\n, name); } while (1) { // 保持运行可在此添加其他任务 } }这段代码展示了三个工程级实践scanf返回值校验必须检查1否则用户输字母时num值未定义数值范围检查嵌入式环境无异常处理越界值必须手动防护字符串长度限制%15s强制截断避免name数组溢出——这是内存破坏类Bug的高发区。3.4 第四步Keil工程配置——五个必须勾选的关键选项库函数版在Keil中需手动配置否则printf链接失败配置项正确值说明Use MicroLIB✅ 勾选启用精简版C库否则printf无法链接One ELF Section per Function✅ 勾选减小代码体积避免函数交叉引用问题Read-Only Position Independent❌ 不勾选神舟IV号Flash执行无需PIEUse Default Library❌ 不勾选必须用MicroLIB否则_write/_read不生效Library Configuration--library_typemicrolib在Misc Controls中添加双重保险提示若编译报错undefined symbol __FILE或__stdout一定是MicroLIB未启用。此时需清理全部中间文件Objects/ Listings/后重新编译。3.5 第五步串口调试助手设置——神舟IV号专用参数在XCOM、SSCOM等工具中必须按以下参数设置否则看到乱码参数值为什么波特率115200与代码中USART_BaudRate严格一致数据位8USART_WordLength_8b停止位1USART_StopBits_1校验位无USART_Parity_No流控无USART_HardwareFlowControl_None换行模式CRLFprintf中用\r\n调试助手需识别CR换行注意神舟IV号USB转串口芯片为CH340驱动必须安装V3.5以上版本旧版驱动在Win11下可能导致波特率漂移。3.6 第六步烧录与验证——三步确认法第一步LED指示在printf前加GPIO_SetBits(GPIOC, GPIO_Pin_13);神舟IV号PC13接LED若LED亮起但无串口输出说明USART2初始化失败重点查RCC和GPIO时钟第二步单字节发送注释掉printf改用USART_SendData(USART2, A); while(!(USART2-SRUSART_FLAG_TC));若串口收到A说明硬件链路正常问题在_write第三步中断触发验证在USART2_IRQHandler开头加GPIO_ToggleBits(GPIOC, GPIO_Pin_13);若按键时LED闪烁证明中断已响应_read逻辑可进一步调试。4. 常见问题排查五个让90%同学当场崩溃的翻车点4.1 现象printf输出乱码但波特率确认是115200原因USART2时钟源错误。神舟IV号APB1总线默认为36MHz但若RCC_CFGR中PPRE1被误设为RCC_HCLK_Div4即9MHz则USARTDIV计算值错误实际波特率偏差超10%。解决检查system_stm32f10x.c中RCC-CFGR | (uint32_t)RCC_CFGR_PPRE1_DIV2;APB1HCLK/236MHz确保未被其他代码覆盖。4.2 现象scanf始终不返回串口助手光标一直闪烁原因_read函数中未正确处理回车符。Windows串口助手默认发送\r\n而代码只判断\r或\n若先收到\r则立即退出\n残留在缓冲区下次scanf读到\n直接返回空字符串。解决在_read的字符接收循环中增加对\r\n组合的过滤if (c \r) { // 等待下一个字符若是\n则丢弃否则当作普通字符 if (USART_GetFlagStatus(USART2, USART_FLAG_RXNE) ! RESET) { uint8_t next USART_ReceiveData(USART2); if (next ! \n) USART_SendData(USART2, next); // 错误组合回传给PC } break; // 以\r为结束符 }4.3 现象输入中文或特殊符号时程序死机原因scanf(%s)遇到非ASCII字符如中文UTF-8编码的0xE4 0xB8 0xAD会持续读取直到缓冲区溢出最终踩坏栈。库函数版无编码检测能力。解决永远不要用scanf接收未知长度的非ASCII输入。改为usart2_getline(buf, sizeof(buf))并在业务层用strlen(buf)判断字节数若3则提示“请勿输入中文”。4.4 现象多次调用scanf后后续printf输出变慢或丢失原因环形缓冲区rx_buffer未清空残留的\r\n或垃圾数据被_read误读导致scanf内部状态机混乱。scanf不是原子操作它依赖底层_read的语义一致性。解决在每次scanf调用前强制清空环形缓冲// 清空USART2环形缓冲 void usart2_flush_rx(void) { rx_head rx_tail 0; } // 调用位置 usart2_flush_rx(); scanf(%d, num);4.5 现象Keil编译通过但下载后串口无任何反应J-Link识别正常原因神舟IV号Boot引脚BOOT0/BOOT1配置错误。若BOOT01、BOOT10芯片从系统存储器启动ISP模式此时USART1被占用但USART2仍可用——然而多数同学会误以为“下载成功程序运行”其实程序根本没跑。解决确认BOOT00、BOOT1x任意然后按RST键复位。用万用表量PA2电压空闲时应为3.3V发送时有下降沿脉冲可快速验证。5. 答辩验收指南如何用三分钟让导师信服这不是抄的5.1 答辩现场必演的三个动作缺一不可动作操作步骤导师关注点为什么有效动作1现场改波特率在usart2_init.c中将115200改为9600重新编译下载用XCOM切换至9600并输入数字导师会看你是否理解波特率是软硬件协同参数而非固定值抄作业者只会用默认值改了就崩你能改且稳证明吃透了USARTDIV计算逻辑动作2故意输错再纠正输入abc→ 显示“输入格式错误” → 按退格删掉abc→ 输入42→ 显示“平方是1764”导师会看退格键是否真能擦除屏幕字符错误处理是否友好90%的Demo没有退格逻辑靠关软件重开你实现了终端级交互说明重写了_read动作3拔掉USB线再插上断开CH340供电等待5秒重插 → 程序自动恢复通信无需复位导师会看USART2是否启用了接收超时和错误标志清除抄来的代码往往不处理ORE溢出错误标志断线重连后RXNE被锁死必须复位芯片提示这三个动作要在答辩PPT的“演示环节”单独一页列出用手机录屏提前练习确保一次成功。导师最反感“我试试…啊不行可能是线松了”要的是“我设计时就考虑了这个场景”。5.2 答辩PPT技术页必备的四张图硬件连接拓扑图手绘风格标出PA2→CH340-TX、PA3→CH340-RX、GND共地旁边注明“避开JTAG复用引脚”USART2初始化流程图用方框箭头表示“使能时钟→配置GPIO→计算USARTDIV→初始化USART→开启中断”在“计算USARTDIV”处手写36000000/(16×115200)19.53125→0x139_write/_read函数对比表 | 函数 | 阻塞方式 | 关键标志位 | 超时机制 | 退格处理 | |------|----------|------------|----------|----------| |_write| TC轮询 |USART_FLAG_TC| 无 | 无 | |_read| RXNE中断轮询 |USART_FLAG_RXNE| SysTick 10s |\b \b三字节擦除 |环形缓冲区示意图画一个64字节圆圈标出rx_head绿色、rx_tail红色、rx_buffer[64]用箭头表示写入/读取方向并注明“满时丢弃新字节永不溢出”。5.3 导师必问的三个问题及满分回答Q1为什么不用HAL库而坚持用库函数版A神舟IV号配套教材、实验指导书、历年考题均基于标准外设库HAL库的句柄抽象会掩盖寄存器操作细节。比如HAL_UART_Transmit内部调用UART_WaitOnFlagUntilTimeout而我们要求学生亲手写while(!(USART2-SRUSART_FLAG_TC))这才是嵌入式底层能力的训练目标。Q2scanf输入安全性怎么保障A做了三层防护第一层是_read中的len-1边界检查第二层是scanf格式化字符串的长度限制如%15s第三层是环形缓冲的“满丢弃”策略。这比单纯依赖scanf更可控——毕竟scanf在嵌入式里本就是危险函数。Q3如果用户狂按键盘系统会不会崩溃A不会。环形缓冲区大小64字节是根据神舟IV号RAM余量20KB和典型输入长度20字计算的安全值_read中每字符处理不超过50条指令最坏情况10秒超时强制退出且所有全局变量rx_head/rx_tail均为volatile中断安全。我用信号发生器模拟100Hz随机字符注入连续运行2小时无异常。从那以后我每次做串口项目都强制走一遍“改波特率→输错再纠→拔插USB”三动作不是为了应付答辩而是因为这三步真的能暴露90%的底层隐患。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑