STM32 零基础可移植教程 07USART 串口打印从 CubeMX 配置到 printf 输出前面几篇我们已经做了 LED、蜂鸣器、按键轮询、按键消抖和外部中断。到这里板子已经能“看得见、听得到、按得动”。但嵌入式调试里还有一个非常重要的能力让板子把信息说出来。比如你想知道程序有没有跑到某个位置按键事件到底有没有触发ADC 采样值是多少I2C 设备地址有没有应答某个变量为什么变成了奇怪的值。这时候串口打印就很有用了。这一篇只做一个明确目标STM32 通过 USART 每秒打印一次 Hello 嵌入式小站并且把printf()重定向到串口。后面再写 ADC、I2C、SPI、CAN 时我们就可以直接打印调试信息。本篇目标最终现象串口助手每1秒显示一行 Hello 嵌入式小站, count1Hello 嵌入式小站, count2Hello 嵌入式小站, count3...本篇用到的外设USART GPIO Alternate Function本篇跑通标准Keil 编译通过程序能下载到开发板串口助手能收到 STM32 打印的文本printf()能正常输出能说清楚 TX、RX、GND 怎么接能说清楚波特率、数据位、停止位、校验位是什么意思。本篇只讲串口发送和printf()打印不讲串口接收、中断、DMA、不定长帧。那些后面单独讲。准备工作你需要准备|项目|说明|| — | — ||STM32 开发板|任意 STM32 开发板||下载器|ST-LINK/V2 或板载 ST-LINK||USB 转 TTL 模块|如果开发板没有板载 USB 串口需要外接||串口助手|任意常见串口工具都可以||原理图|确认 USART TX/RX 接到哪个引脚|如果你用的是 Nucleo、Discovery 或一些带 USB 串口芯片的开发板可能板子已经把某个 USART 接到了板载 USB 串口。如果你用的是 F103 最小系统板通常需要外接 USB 转 TTL 模块。硬件连接串口最容易接错的是 TX 和 RX。STM32 的TX是发送USB 转 TTL 的RX是接收所以要交叉连接|STM32|USB 转 TTL|| — | — ||USART_TX|RXD||USART_RX|TXD||GND|GND|如果本篇只做 STM32 打印理论上只接STM32 TX -USB 转 TTL RX GND -GND也能看到输出。但建议一开始就把 RX 也接好后面讲串口接收时可以继续用。注意电平STM32 GPIO 通常是3.3V TTL 电平 不要直接接 RS232 电平普通 USB 转 TTL 模块一般支持 3.3V 或 5V有些模块有跳帽或开关。给 STM32 接信号时优先选择 3.3V 电平。串口参数先看懂我们先用最常见配置115200, 8N1它的意思是|参数|值|说明|| — | — | — ||Baud Rate|115200|每秒传输的符号数||Word Length|8 Bits|每个数据字节 8 位||Parity|None|不使用校验位||Stop Bits|1|1 个停止位||Flow Control|None|不使用硬件流控|串口两端参数必须一致。STM32 配了 115200串口助手也要选 115200。STM32 配 8N1串口助手也要选 8N1。如果两边不一致常见现象就是乱码。CubeMX 配置步骤1. 复制上一篇工程建议从上一篇06_key_exti或一个干净基础工程复制一份改名为07_usart_printf这一篇不依赖按键可以只保留 LED也可以从基础工程开始。2. 选择 USART 实例常见 STM32F103 工程里经常使用USART1默认引脚通常是PA9 -USART1_TX PA10 -USART1_RX但不同芯片、不同开发板不一定一样。在 CubeMX 左侧找到Connectivity -USART1把 Mode 设置为Asynchronous3. 确认 TX/RX 引脚CubeMX 会自动把对应引脚设置成 USART 复用功能。比如PA9 -USART1_TX PA10 -USART1_RX你要做的是确认这两个引脚和你的开发板接线一致。如果开发板板载 USB 串口接的是 USART2那就不要硬用 USART1要改成 USART2。原则还是那句话原理图接到哪个 USART就配置哪个 USART4. 配置 USART 参数进入 USART 参数配置页面设置|配置项|推荐值|| — | — ||Baud Rate|115200||Word Length|8 Bits||Parity|None||Stop Bits|1||Data Direction|Receive and Transmit||Hardware Flow Control|None||Over Sampling|16 Samples|本篇只做发送但建议保留 Receive and Transmit后面讲接收可以继续用。5. 本篇暂时不打开 USART 中断这一篇只做阻塞发送HAL_UART_Transmit()所以暂时不需要打开 USART NVIC 中断。后面讲串口接收、中断接收、DMA 接收时我们再单独打开。6. 生成 Keil 工程配置完成后点击GENERATE CODE然后打开 Keil 工程先编译一次。Keil 工程生成和编译打开 Keil 后先编译Build / F7确认输出里没有错误0Error(s)然后看一下 CubeMX 是否生成了串口句柄。在此之前我们先解释一下串口句柄的概念你可以把串口想象成一扇门比如 COM1 是房门COM2 是窗户。句柄就是这扇门的把手或者更准确地说是操作系统发给你的一把“数字钥匙”。你拿到这把“钥匙”句柄就能对门做事情开门初始化串口、送东西出去发送数据、收东西进来接收数据、关门关闭串口。如果你没有这把钥匙操作系统就不允许你碰这扇门。在很多 CubeMX 工程里你会看到类似UART_HandleTypeDef huart1;如果你用的是 USART2可能是UART_HandleTypeDef huart2;在单片机中没有操作系统管理“句柄”这个概念但是CubeMX生成的结构体指针起到了完全相同的作用 他也是一个“钥匙”包含了串口的所有状态和配置信息。后面的代码默认用huart1。如果你的工程是huart2需要改一个宏。这个 huart1 变量准确的说是它的指针 huart1就扮演了“串口句柄”的角色。 后续所有串口操作都要带上它发送HAL_UART_Transmit(huart1, data, len,timeout);接收HAL_UART_Receive(huart1, buffer, len,timeout);完整代码这一篇新增两个文件Core/Inc/app_uart.h Core/Src/app_uart.c它们负责两件事封装字符串发送函数把 Keil 下常用的printf()输出重定向到 USART。1. 新建Core/Inc/app_uart.h在Core/Inc目录下新建app_uart.h写入下面代码#ifndef APP_UART_H#define APP_UART_H#include main.h#include stdint.hvoid App_UART_Init(void);void App_UART_Print(const char *text);void App_UART_PrintBytes(const uint8_t *data, uint16_t len);#endif2. 新建Core/Src/app_uart.c在Core/Src目录下新建app_uart.c写入下面代码#include app_uart.h#include stdio.h#include string.h/* * Default USART handle is huart1. * If your project uses USART2, define APP_UART_HANDLE as huart2. */#ifndef APP_UART_HANDLE#define APP_UART_HANDLE huart1#endifextern UART_HandleTypeDef APP_UART_HANDLE;void App_UART_Init(void){}void App_UART_Print(const char *text){if(textNULL){return;}HAL_UART_Transmit(APP_UART_HANDLE,(uint8_t *)text,(uint16_t)strlen(text), HAL_MAX_DELAY);}void App_UART_PrintBytes(const uint8_t *data, uint16_t len){if((dataNULL)||(len0u)){return;}HAL_UART_Transmit(APP_UART_HANDLE,(uint8_t *)data, len, HAL_MAX_DELAY);}int fputc(int ch, FILE *f){uint8_t data(uint8_t)ch;HAL_UART_Transmit(APP_UART_HANDLE,data, 1u, HAL_MAX_DELAY);returnch;}这里有一个可移植点#define APP_UART_HANDLE huart1如果你用的是 USART2就在app_uart.c里改成#define APP_UART_HANDLE huart2如果你用的是 USART3就改成#define APP_UART_HANDLE huart3fputc()是关键。Keil 工程里printf()最终会一个字符一个字符输出我们在fputc()里调用HAL_UART_Transmit()这样printf()的内容就会从 USART 发出去。3. 把app_uart.c加入 Keil 工程在 Keil 工程树里右键Application/User/Core选择Add Existing Files to GroupApplication/User/Core然后添加Core/Src/app_uart.c如果忘了这一步可能会报undefined symbol App_UART_Init undefined symbol App_UART_Print或者printf()没有按预期走你的重定向。main.c 调用方式1. Includes 区域找到/*USERCODE BEGIN Includes */ /*USERCODE END Includes */改成/*USERCODE BEGIN Includes */#include app_uart.h#include stdio.h/*USERCODE END Includes */注意printf()需要包含#include stdio.h2. 初始化区域确保 CubeMX 生成的串口初始化函数已经被调用。如果用 USART1通常是MX_USART1_UART_Init();如果用 USART2可能是MX_USART2_UART_Init();然后在USER CODE BEGIN 2里添加/*USERCODE BEGIN2*/ App_UART_Init();printf(USART printf test start\r\n);/*USERCODE END2*/App_UART_Init()现在是空函数保留它是为了以后扩展比如初始化环形缓冲区、日志等级、串口接收状态机。3. while 循环区域找到while(1){/*USERCODE END WHILE */ /*USERCODE BEGIN3*/ /*USERCODE END3*/}改成while(1){/*USERCODE END WHILE */ /*USERCODE BEGIN3*/ static uint32_t count0;count;printf(Hello STM32, count %lu\r\n, count);HAL_Delay(1000);/*USERCODE END3*/}这里的\r\n是换行。很多串口助手在 Windows 下更习惯\r\n如果只写\n有些工具可能只换行不回到行首看起来排版有点怪。Keil 里建议打开 MicroLIB在 Keil 里使用printf()时建议打开 MicroLIB。进入OptionsforTarget -Target勾选Use MicroLIB如果你不勾 MicroLIB有些工程也能工作但新手阶段容易遇到半主机、库函数重定向、链接配置这些额外问题。本系列先采用最容易跑通的方式。串口助手设置打开串口助手选择 USB 转 TTL 对应的 COM 口。参数设置为Baud Rate:115200Data Bits:8Stop Bits:1Parity: None Flow Control: None如果你不知道是哪个 COM 口可以打开 Windows 设备管理器看“端口 COM 和 LPT”。编译、下载和验证代码加完后先编译Build / F7没有错误后下载Download打开串口助手正常情况下你应该能看到USARTprintfteststart Hello STM32, count1Hello STM32, count2Hello STM32, count3如果 Keil 下载后程序没有自动运行但你按复位键后能看到串口输出那说明程序本身没问题还是之前说过的下载后自动复位运行问题。移植到其他板子的修改点这篇的移植点主要有 7 个。|要改的地方|为什么要改|在哪里改|| — | — | — ||USART 实例|不同板子板载串口可能接 USART1/2/3|CubeMX Connectivity||TX/RX 引脚|不同芯片复用引脚不同|CubeMX Pinout 和原理图||串口句柄|huart1、huart2、huart3不同|APP_UART_HANDLE||波特率|两端必须一致|CubeMX USART 参数和串口助手||时钟|波特率依赖串口时钟|CubeMX Clock Configuration||电平|STM32 是 TTL 电平不是 RS232 电平|硬件连接||GND|串口通信必须共地|开发板和 USB 转 TTL|换板子的推荐顺序看原理图确认板载 USB 串口接到哪个 USART如果外接 USB 转 TTL确认你接的是哪个 TX/RX 引脚CubeMX 配置对应 USART 为 Asynchronous设置 115200、8N1根据实际句柄修改APP_UART_HANDLE串口助手选择相同参数编译下载看是否能收到Hello STM32。常见问题排查1. 串口助手完全没输出优先检查|优先检查|具体方法|| — | — ||程序是否运行|下载后按复位键看是否开始输出||TX/RX 是否接反|STM32 TX 接 USB-TTL RX||GND 是否共地|STM32 GND 和 USB-TTL GND 必须相连||COM 口是否选对|设备管理器查看端口号||波特率是否一致|STM32 和串口助手都设为 115200||app_uart.c是否加入工程|Keil 工程树确认|2. 串口乱码常见原因波特率不一致串口助手数据位/停止位/校验位不一致系统时钟配置不对导致串口实际波特率偏差USB 转 TTL 电平不匹配GND 没接好或接触不良。先把两边都设成115200, 8N1如果还是乱码再回 CubeMX 看 Clock Configuration 有没有红色错误。3. 编译报huart1未定义说明你的工程里没有huart1。可能原因你配置的是 USART2所以句柄是huart2你配置的是 USART3所以句柄是huart3CubeMX 没有生成 USART 初始化代码app_uart.c里APP_UART_HANDLE没改。解决方法打开 CubeMX 生成的串口初始化文件或main.c找到实际句柄比如UART_HandleTypeDef huart2;然后把app_uart.c里的宏改成#define APP_UART_HANDLE huart24.printf()没输出但App_UART_Print()能输出说明串口发送本身是通的问题在printf()重定向。优先检查app_uart.c里是否有fputc()app_uart.c是否加入 Keil 工程main.c是否包含stdio.hKeil 是否勾选Use MicroLIB工程里是否有另一个fputc()或重定向函数冲突。5. 输出不换行建议使用printf(Hello STM32\r\n);不要只写printf(Hello STM32\n);有些串口助手对\n的显示不够友好\r\n更稳。6. 中文输出乱码新手阶段建议先用英文和数字输出。中文涉及源码编码、串口助手编码显示、终端字体等问题容易把简单问题复杂化。先确认英文Hello STM32能稳定输出再考虑中文。本篇小结这一篇我们完成了 STM32 串口打印的第一步。你现在至少应该知道串口 TX/RX 要交叉连接STM32 和串口助手参数必须一致常用参数是115200, 8N1CubeMX 里 USART 要选择 AsynchronousHAL_UART_Transmit()可以阻塞发送数据fputc()可以把 Keil 下的printf()重定向到串口换 USART1/2/3 时要改APP_UART_HANDLE串口没输出时先查运行、接线、COM 口、波特率和 GND。下一篇我们继续沿着串口往下走STM32 串口接收一个字节先把 RX 中断跑通。有了接收能力以后板子就不只是能“说话”还能听电脑发来的命令。