☰
STM32 NUCLEO-H743ZI板载ST-Link虚拟串口实现printf打印完整指南
2026/9/28 23:21:42 网站建设 项目流程

前几天调试一块NUCLEO-H743ZI板卡,想确认系统时钟和传感器数据是否正常,结果在桌面翻了个底朝天也没找到USB转TTL模块。后来才反应过来,板子上的ST-Link本身就带虚拟串口功能,根本不需要额外的USB转TTL,用STM32CubeIDE配置一个UART,再重定向printf就能直接打印出来。这篇东西就把我踩过的坑和完整操作分享出来,给刚拿到NUCLEO-H743ZI的朋友省点时间。文章里会讲清楚ST-Link虚拟串口的工作原理、CubeIDE工程配置、printf重定向的常见坑,以及一堆实测出来的排查经验。

1. 先搞清楚原理:板载ST-Link凭啥能当串口用

1.1 USB转TTL和ST-Link虚拟串口,到底差在哪

很多人第一次听说“不用USB转TTL”都会愣一下,ST-Link不是调试器吗?它和串口有什么关系?其实这个理解需要拆开看。

传统USB转TTL模块,比如CH340G、CP2102、FT232这类芯片,本质是把PC的USB数据包翻译成UART的TX/RX电平信号。PC端看到一个COM口,MCU端看到的是标准的串口引脚。你接线的时候要把模块的TX接MCU的RX,模块的RX接MCU的TX,再把GND接一起,这套操作相信大家都很熟了。

而NUCLEO板上的ST-Link不一样。ST-Link调试器除了做SWD下载和调试之外,它的USB接口还会额外枚举出一个“Virtual COM Port”设备,这就是ST官网常说的VCP功能。板载ST-Link内部自带一个UART控制器,这个UART的引脚在PCB上已经连到了目标MCU的某个USART引脚上。也就是说,PC通过USB把数据发给ST-Link,ST-Link再用UART电平转发给MCU,反过来MCU发出来的UART数据也被ST-Link接收并转成USB数据发给PC。

本质上,板载ST-Link和USB转TTL做的是同一件事:USB到UART的协议转换。区别只在于USB转TTL是外接模块,得自己飞线;而NUCLEO板在出厂设计时就把这条线在板内连好了。我们省掉的不是协议转换,而是外部的杜邦线和模块。

注意一个术语问题:大家常说的“TTL串口”其实指的是UART电平信号(3.3V CMOS逻辑),跟老式RS232那种±12V电平完全是两码事。NUCLEO板上ST-Link和目标MCU之间都是3.3V逻辑,所以能直接连。如果外部接了5V电平的设备,直接怼到MCU引脚上就危险了。

1.2 NUCLEO-H743ZI上ST-Link接到了哪个UART

这里有个特别容易踩的坑,必须先说清楚:NUCLEO-H743ZI的板载ST-Link VCP默认接到的是目标MCU的USART3,对应引脚是PD8(USART3_TX)和PD9(USART3_RX)。

很多网上的老教程讲的是NUCLEO-F103RB,那块板上的ST-Link VCP接的是USART2,引脚是PA2和PA3。如果你照着F1的教程去配置PA2、PA3,在H743上大概率是没有任何输出的,因为PA2和PA3在这块板上没接到ST-Link那边去。

数据流方向是这样的:MCU的PD8被配置为USART3_TX后,数据会送到ST-Link的UART接收脚;ST-Link再把数据通过USB发给PC端的COM口。所以本文只需要重点关心PD8这条发送路径就够了,但配置的时候PD9也必须正确映射为USART3_RX,不用它也得占着这个功能,否则CubeIDE里USART3的初始化会出问题。

如果你想自己确认板子上的连接,最权威的途径是ST官网下载NUCLEO-H743ZI的用户手册,就是UM2179,里面ST-Link部分会写明VCP默认接到哪组UART引脚。也可以打开CubeIDE的芯片视图,在USART3上看到引脚自动分配到了PD8和PD9。养成看原理图的习惯,比死记硬背哪个板子接哪个串口靠谱得多。

2. CubeIDE工程搭建:选板、配时钟、开USART3

2.1 新建工程,别选错开发板

STM32CubeIDE新建工程时,尽量用Board Selector选开发板,而不是直接选芯片。File -> New -> STM32 Project,在Board Selector页面输入NUCLEO-H743ZI,选中后点Next。选板子的好处是CubeIDE会把板载HSE晶振、默认时钟树、LED引脚这些基础配置一起生成好,省掉很多手工活。

第一次创建工程时,CubeIDE可能会提示下载H743对应的固件包,这个取决于你的IDE版本和网络情况,稍等一会就行。工程名自己起一个,比如nucleo_h743_uart_printf。建好之后进入的是图形化配置界面,也就是.ioc文件的编辑视图。

有个小提醒:如果你不是用开发板模板,而是手动选STM32H743ZIT6芯片,建完工程后记得先把PD8和PD9附近的其他功能清掉,比如某些LCD数字接口、FMC复用,避免引脚冲突。不过按开发板模板创建的话,一般不会有这个问题。

2.2 时钟树配置:先保证USART3的时钟源正确

打开.ioc文件下面的Clock Configuration页签,会看到CubeIDE自动生成的时钟树。H743这颗芯片最高可以跑到480MHz,开发板模板默认会把SYSCLK配置到480MHz,HSE用板载晶振。

很多人在这一步容易忽略USART3的时钟来源。USART3挂在APB1总线上,H7系列里就是D2域的APB1。在时钟树页面往下找,能看到USART3对应的时钟源选择下拉框,正常情况下应该选择PCLK1。如果你改成别的时钟源,比如PLL2Q、PLL3Q之类,波特率会乱掉,打印出来就是满屏乱码。

APB1本身的分频也要注意。H7的时钟拓扑和F1、F4不一样,F4时代很多人习惯把APB1设成42MHz或者84MHz,但H7的D2域APB1在默认配置下可能是120MHz或类似的高频率。HAL库会自动根据USART3时钟频率去计算波特率分频系数,只要你保证时钟树是CubeIDE自动生成的,并且USART3选了PCLK1,115200这种常规波特率就不会有问题。

如果你实在不想折腾,直接保持时钟树默认不动,只改USART3的时钟源,这是最快也最稳的方案。别去网上抄那些“H7超频”配置,串口打印场景根本不需要那些操作。

2.3 配置USART3为异步串口

Pinout & Connectivity页面,在左侧外设列表里展开Connectivity,找到USART3,选中后把Mode改成Asynchronous。底下Parameter Settings里把波特率设为115200,数据位8,停止位1,校验无,硬件流控关闭。这些都是最常规的串口参数。

如果有需要,可以打开NVIC Settings里的USART3 global interrupt。本文只做发送,用轮询方式就够了,所以中断可以先不开。保存.ioc文件后,CubeIDE会自动生成MX_USART3_UART_Init函数,并且声明一个UART_HandleTypeDef类型的huart3全局句柄。

引脚方面,CubeIDE会自动把USART3的TX映射到PD8,RX映射到PD9。你可以在System Core -> GPIO里看到这两个引脚被标成串口复用模式。有的网友分享的.ioc文件里,USART3的Mode可能是“Asynchronous with alternative pins”,或者“Single Wire Half-Duplex”,这两种配置都会让行为变得很奇怪。看到这类设置,直接改回普通的Asynchronous最省心。

配置完之后,回到main.c,在USER CODE BEGIN PFP之后或者在main函数里的MX_USART3_UART_Init调用之后,就能开始写串口相关代码了。

3. printf重定向:从“能打印”到“好打印”

3.1 直接调用HAL发送函数有什么问题

USART3初始化好之后,你当然可以直接用HAL_UART_Transmit发送数据。比如这样:

HAL_UART_Transmit(&huart3, (uint8_t*)"Hello\r\n", 8, HAL_MAX_DELAY);

这行代码能跑,但每次都要自己数清楚字符串有几个字符,写起来非常痛苦。想打印一个整数变量,还得先用sprintf拼到缓冲区里,然后再调用发送函数。项目一复杂,满屏都是这种拼接代码,看着就头疼。

printf是C标准库的格式化输出函数,能自动处理%d、%f、%x这些占位符。但嵌入式环境里,printf默认的“输出对象”是标准输出stdout,并不自动连接到串口。所以我们得做一个操作,叫“重定向”:把printf底层要调用的输出函数,从默认的“无意义”实现,改成往huart3里发数据。

3.2 CubeIDE下最稳的两种重定向写法

STM32CubeIDE使用的是GCC编译器工具链,C库是Newlib,和Keil的ARMCC环境不一样。网上很多教程是在Keil里改fputc,照搬到CubeIDE里可能不生效。这里我给你两个实测可用的方法。

方法A,重写_write函数,这是我最推荐的做法。在main.c的USER CODE BEGIN 4和USER CODE END 4之间,加上这么一段:

#include <unistd.h> int _write(int file, char *ptr, int len) { HAL_UART_Transmit(&huart3, (uint8_t*)ptr, len, HAL_MAX_DELAY); return len; }

Newlib库在调用printf时,最终会通过系统调用接口找到_write函数。你只要提供了一个有效实现,printf的所有输出就会自动进入UART。这个方法在CubeIDE下非常稳定,我测试过多个CubeIDE版本都能正常工作。

方法B,重写fputc,代码如下:

int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart3, (uint8_t*)&ch, 1, HAL_MAX_DELAY); return ch; }

这招在Keil里是标配,但在CubeIDE里不一定有效,取决于编译链接时用的C库和链接选项。所以我的建议是:两者最好都写进去,不影响,而且能互相兜底。

还有一件事要提醒,如果编译时你用了nano.specs这种精简C库选项,建议同时加上nosys.specs,不然有些版本会报semihosting相关的链接错误。在Project Properties -> C/C++ Build -> Settings -> Tool Settings -> MCU GCC Linker -> Miscellaneous里可以看到这些选项。

3.3 打印细节:换行、浮点、初始化顺序

先说换行。很多老手在串口助手看到自己的输出变成阶梯状,就是字符串用了\n而不是\r\n。\n只是换行,\r才是光标准备回到行首。建议以后写串口字符串时固定用\r\n。你甚至可以封装一个宏,比如:

#define LOG_INFO(...) printf("[INFO] " __VA_ARGS__);

然后调用的时候在末尾自己加\r\n,或者干脆在宏里做统一处理,具体看偏好。

再说浮点打印。printf里的%f在默认GCC链接配置下可能会报undefined reference to _printf_float这种错误。解决办法是在链接器选项里加上--u _printf_float,或者编译时把printf的浮点支持打开。如果你只是打印整数和字符串,不需要理会这个坑。

最后是初始化顺序。printf重定向必须在MX_USART3_UART_Init执行之后再使用。main函数里初始化顺序一般是:MX_GPIO_Init、MX_USART3_UART_Init,然后才到USER CODE 2。如果你在某个外设初始化回调函数里提前调用printf,而此时huart3还没有初始化,轻则没输出,重则硬故障死机。我建议在main函数的USER CODE 2区域先打一行调试信息确认串口通了,再往业务代码里加打印。

4. 烧录、打开串口、看输出(完整实操记录)

4.1 编译和下载

在CubeIDE里点击工具栏上的锤子图标,或者按Ctrl+B编译工程,底部Console会显示编译进度,最后出现Finished才算成功。如果代码里有语法或链接错误,双击错误信息可以直接跳到出错的代码位置。

编译通过后,点击Run按钮或者按F11进入Debug模式。第一次运行需要创建调试配置,在Debug Configuration对话框里确认调试器是ST-LINK,接口默认SWD就行。如果你电脑上同时插了多个ST-Link设备,Debug Probe的Serial Number下拉框里要选对具体那一个,不然会下载到另一块板子上。

下载完成后,如果是Debug模式,程序会停在main函数的入口,需要在工具栏点Resume图标或者按F8,程序才会真正跑起来。如果只是Run模式,程序会直接运行。

4.2 找到ST-Link虚拟出来的COM口

在Windows下右键“此电脑”,选择“管理”,打开“设备管理器”,展开“端口(COM和LPT)”,能看到一个叫“STMicroelectronics STLink Virtual COM Port (COMx)”的设备,这个就是板载ST-Link虚拟出来的串口。

如果同时插了好几块ST-Link板子,COM口可能有好几个。最简单的区分办法是拔掉USB线看设备管理器里哪个COM口消失了,再插上确认。也可以看设备属性里的硬件ID,ST-Link虚拟串口的VID一般是0483,PID是5740。你要是用过STM32的USB设备,对这个VID应该很熟悉。

有朋友在Windows 10和Windows 11下插上板子后发现只有“STLink”设备,没有“Virtual COM Port”,十有八九是驱动问题。可以先去ST官网搜索STSW-LINK007驱动包,装上后再看设备管理器。Win10/11系统多数情况下插上就能识别,遇到未知设备再手动指定驱动路径。

4.3 串口助手设置与实测输出

串口助手的选择很自由,市面上的常见串口调试工具都能用。关键是要把参数设置对:波特率115200,数据位8,停止位1,无校验。

打开串口后,按一下板子上的RESET按键让程序复位,正常情况下就能看到打印内容。我测试时跑的一个简单程序,输出大概是这样的:

System Clock: 480 MHz UART3 printf test starts... Hello from NUCLEO-H743ZI Count: 0 Count: 1 Count: 2

如果你看到的不是类似内容,先不要怀疑串口助手,按第5部分的排查顺序检查。

这里有个细节值得说一下:串口助手里的“DTR”和“RTS”选项,有些工具默认是勾选的。对大多数NUCLEO板来说,DTR/RTS不会影响ST-Link虚拟串口,但如果你用的USB转TTL模块支持自动复位电路,DTR通常会触发板子复位。如果在ST-Link虚拟串口上碰到“打开串口后程序重启”的怪事,试着把DTR/RTS勾选去掉。

4.4 为什么可以“边下载边打印”

用传统USB转TTL模块时,调试完想重新下载程序,得先把串口助手软件关掉,否则COM口被占用,下载工具可能连不上。板载ST-Link虚拟串口没这个烦恼,因为ST-Link的USB接口同时枚举了三个设备角色:调试器、虚拟串口、大容量存储接口。下载程序时USB数据走的是调试端点,虚拟串口端点可以继续工作。

实测表现是:程序正在循环打印的过程中,点击CubeIDE下载按钮,串口打印会短暂停顿,然后随着新程序运行重新恢复输出,COM口全程不会从系统里消失。这点在实际调试中非常舒服,尤其你一边看串口日志一边改代码的时候,省去了反复开关串口助手的操作。

5. 常见问题与排查速查表

5.1 串口助手能看到COM口,但没任何打印

这是最常遇到的问题。先按复位键,看板载LED有没有闪动,确认程序确实在跑。如果程序没跑,先解决下载和运行问题。

程序确定在跑了,还是没打印,那就用排除法。把_write里的发送目标临时改成直接发送固定字符串,类似:

HAL_UART_Transmit(&huart3, (uint8_t*)"abc\r\n", 5, HAL_MAX_DELAY);

如果能收到abc,说明串口通路没问题,问题出在printf重定向;如果连abc都收不到,那就要往前查USART3初始化、引脚配置、串口助手参数。

还要检查一个点:main.c里调用的初始化顺序。如果printf放在MX_USART3_UART_Init之前,程序可能在执行到那一步时就卡死或者异常复位了。串口无输出的同时,按下复位键后LED也不规律闪动,多半就是这个原因。

5.2 打印乱码

乱码问题基本都出在波特率不匹配或者波特率计算用的时钟源不对。如果输出像是“阥?趰?6??”这种完全看不懂的字符,先把波特率改成9600试试,看乱码规律是否变化。如果9600下字符变成可读的,说明实际波特率比115200低,问题出在USART3的时钟配置。

回到Clock Configuration,确认USART3的时钟源是PCLK1,并且APB1的总线频率是CubeIDE自动生成的那个值,不要手动乱改。另外,HAL库默认过采样率是16,不要轻易改成8,除非你知道自己在干什么。

还有一个被我忽略过的坑:如果外部接了USB转TTL模块同时连接到了同一组UART引脚,两边信号冲突,也可能导致乱码。本文场景是纯板载,没有这个顾虑,但如果你外接了其他模块,记得断开。

5.3 printf没输出,但HAL_UART_Transmit有输出

这说明串口硬件通路完全正常,问题出在重定向。先确认你写的是_write还是fputc,位置是否在USER CODE BEGIN 4和USER CODE END 4之间,因为CubeIDE重新生成代码时,USER CODE区之外的自定义代码会被覆盖掉。

如果写了_write还是不生效,看一下链接器设置。CubeIDE默认的链接选项可能不包含nano.specs,但有些教程会让你加,加了nano之后没有加nosys,也可能导致printf在运行时进入半主机模式停滞。建议链接选项里放这两组:

--specs=nano.specs --specs=nosys.specs

这样printf占用的资源更小,也会使用nosys提供的系统调用桩,配合你自己实现的_write就能工作。修改链接选项后需要重新编译整个工程,别只点增量编译。

5.4 板卡无法下载或提示ST-Link错误

如果下载时报“Error in ST-Link connection”之类的错误,先从物理连接开始排查。很多USB线看着一样,实际上只有充电功能,没有数据线芯,换一根手机标配的带数据传输的线试试。USB口也换一个试试,优先插主板后置口。

设备管理器里如果ST-Link那个设备带黄色感叹号,说明驱动异常,下载STSW-LINK007重新安装。如果下载时提示ST-Link固件版本不匹配,在CubeIDE的Window -> Preferences -> ST-Link里可以更新ST-Link固件,更新过程大概十几秒。

还有一种情况是芯片被写保护了,或者SWD引脚被代码复用成了其他功能,导致调试器连不上。这时候可以按住板上的复位键,点下载,同时松开复位键,利用这个时序暂时绕过锁死的引脚状态。这招在H7板卡上也管用。

5.5 常见问题速查表

现象检查优先级常见原因解决措施
完全无输出1串口助手参数不对设成115200,8N1
完全无输出2程序没正常工作观察LED,加固定字符串发送验证
完全无输出3USART3引脚配置不对确认PD8/PD9被复用为USART3
乱码1波特率不匹配切换9600/115200等常用值测试
乱码2USART3时钟源不正确Clock Configuration中选PCLK1
printf不输出,HAL发送正常1重定向失效实现_write函数
printf不输出,HAL发送正常2链接器缺少nosys加--specs=nosys.specs
下载报错1USB线只供电无数据换数据线
下载报错2ST-Link驱动异常安装STSW-LINK007
下载报错3SWD引脚被复用按住复位键点下载再松键

6. 串口打印只是起点:几个值得留心的扩展方向

6.1 从“发”到“收”:双向通信与简单命令交互

很多人在串口打印跑通之后就收手了,其实ST-Link虚拟串口是双向的。你可以用HAL_UART_Receive_IT配合回调函数,或者用HAL_UARTEx_ReceiveToIdle_IT做不定长接收,在PD9上接收PC发来的数据。这样就能实现最简单的命令行交互:输入help返回帮助菜单,输入version返回固件版本号,输入set xxx配置某个参数。

接收方向最需要注意的还是引脚占用问题。如果USART3的RX模式没有打开,或者PD9被其他外设占用了,接收自然不工作。CubeIDE配置里USART3 Mode选Asynchronous时,TX和RX都会自动分配好,如果你手动把某个引脚改成GPIO输出,串口接收就会被切断。

6.2 给日志加上时间戳和分级

裸printf在高频率日志下非常难排查问题,建议自己封装一层简单的日志模块。比如用枚举区分DEBUG、INFO、WARN、ERROR等级别,打印时带上时间戳。H743有DWT计数器和TRC,通过DWT->CYCCNT可以轻松做到us级计时,比来回查逻辑分析仪方便得多。

实际调试中,我习惯在关键函数入口打一行INFO日志,在异常分支打WARN,在错误返回处打ERROR。配合时间戳,就能快速定位哪一步耗时异常、哪个分支进了不该进的条件。串口打印不只是一个输出手段,它其实是你和芯片之间最快的一扇窗户。

6.3 拿到新板卡后的一个老建议

这次折腾下来,我最大的体会是:拿到任何一块NUCLEO板子,先花十分钟把用户手册里ST-Link部分看明白。VCP默认接哪个UART、哪个引脚可以直接复用、哪些跳线会切断ST-Link和目标的连接,这些信息都写在UM文档里。省下来的时间,绝对比啃文档花掉的十分钟多得多。

尤其是H7这种大封装、多复用引脚的高端芯片,引脚功能表很长,但真正调试时你大概率只会用固定的那一两组外设。提前确认好板载调试器和串口的默认连接,能让你把精力放在业务逻辑上,而不是在引脚配置和接线错误里反复折腾。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询