
1. 项目概述与核心目标上次我们成功把 RT-Thread Nano 内核搬到了梁山派 GD32F470 开发板上让这块高性能的 MCU 跑起了实时任务调度。但光有内核就像给电脑装好了操作系统却没法敲命令、看输出调试和交互起来非常别扭。官方 Nano 包的精髓之一就是提供了添加 UART 控制台和 FinSH 组件的能力这能让我们的开发板“开口说话”通过串口终端进行打印输出和命令交互。所以这次的目标很明确在已经移植好的 Nano 基础工程上打通串口控制台实现rt_kprintf打印功能为后续添加更强大的 FinSH 交互命令行打下坚实基础。整个流程的核心是让 RT-Thread 的系统输出 API主要是rt_kprintf与我们硬件上的具体串口比如 USART0绑定起来。这涉及到三个关键环节第一硬件串口外设的驱动初始化第二实现一个名为rt_hw_console_output的底层输出函数供系统调用第三正确配置工程将必要的库文件和驱动文件添加进来。听起来步骤不少但只要我们跟着硬件手册和 RT-Thread 的框架一步步来其实都是“体力活”关键在于理解每个操作背后的意图避免踩进配置的坑里。下面我就把在梁山派 GD32F470 上实现 UART 控制台的完整过程、背后的原理以及我调试时遇到的几个典型问题详细拆解一遍。2. 工程准备与文件结构梳理在开始动手修改代码之前我们必须先理清工程的文件结构。一个清晰的工程结构是后续顺利编译和调试的前提。我基于上一篇文章移植好的纯净 RT-Thread Nano 工程进行操作这个工程里主要包含 RT-Thread 内核源码、GD32F4xx 的设备支持包Device Support Pack以及用户应用程序目录。2.1 现有工程分析首先打开你的 Keil MDK 工程。在 Project 窗口你应该能看到类似这样的结构RT-Thread Nano组里面是 RT-Thread 内核的核心文件如cpuport.cCPU 移植层、board.c板级支持包、rtthread.h等。Device组包含 GD32F4xx 的启动文件startup_gd32f4xx.s、系统初始化文件system_gd32f4xx.c以及基本的头文件。User组这里放着我们的main.c和应用代码。Hardware组可能需要新建这个组计划存放我们板子特有的外设驱动比如串口、LED、按键等。目前它可能是空的。我们需要添加的文件主要来自两个地方一是梁山派官方 SDK 或示例代码中的串口驱动文件二是 GD32F4xx 标准外设库Standard Peripheral Library, SPL中与串口和中断相关的源文件。2.2 定位并添加关键驱动文件第一步找到梁山派配套资料里的串口驱动文件。通常它会被命名为类似bsp_usart.c和bsp_usart.h的文件。bsp意为“板级支持包”这类文件封装了针对特定开发板的硬件初始化代码。我们需要把bsp_usart.c添加到工程的Hardware组里。在 Keil 中操作右键点击Hardware组如果没有就新建一个选择 “Add Existing Files to Group ‘Hardware’...”然后导航到你的bsp_usart.c文件所在位置选中并添加。用同样的方法把bsp_usart.h所在的目录路径添加到工程的“包含路径”Include Paths中。这一步至关重要否则编译器在board.c里找不到bsp_usart.h这个头文件会报错。添加包含路径的位置在 “Options for Target” - “C/C” - “Include Paths”。第二步添加 GD32F4xx 标准外设库的必要文件。bsp_usart.c驱动底层调用了 GD32 官方库函数来操作寄存器因此我们需要把对应的库源文件也加入工程。主要是两个文件gd32f4xx_usart.c包含了所有 USART 外设的配置、发送、接收、中断等函数。gd32f4xx_misc.c包含了 NVIC嵌套向量中断控制器的配置函数比如设置中断优先级。找到你的 GD32F4xx 标准外设库的安装目录或随 SDK 提供的目录通常在Firmware/GD32F4xx_standard_peripheral/Source路径下。将上述两个.c文件添加到工程中我建议可以新建一个名为GD32F4xx_Peripheral_Drivers的组来管理它们这样工程结构更清晰。同样别忘了将这些.c文件对应的头文件目录通常是Firmware/GD32F4xx_standard_peripheral/Include也添加到工程的包含路径中。注意不同版本的 GD32 库或梁山派资料包文件路径和名称可能略有差异。关键是找到功能对应的文件。如果找不到确切的gd32f4xx_misc.c有时相关函数会集成在gd32f4xx_rcu.c时钟控制或一个通用的系统文件里需要根据编译错误提示灵活调整。完成文件添加后先尝试编译一下工程。此时如果只是缺少一些宏定义或函数声明导致的错误可以先忽略因为我们还没有进行关键的配置。但如果出现大量“未定义标识符”的错误很可能是包含路径没有设置正确请回头仔细检查。3. 核心配置与代码移植详解文件准备就绪后我们就进入了核心的配置阶段。这个过程就像是给系统搭建一个“输出管道”告诉 RT-Thread“请把所有要打印的字符都通过我指定的这个串口发出去。”3.1 启用控制台宏定义RT-Thread Nano 的功能通过一系列宏定义来裁剪和启用。控制台功能对应的宏是RT_USING_CONSOLE。我们需要在rtconfig.h文件中找到它并将其使能。rtconfig.h是 RT-Thread 的核心配置文件通常位于工程根目录或RT-Thread Nano组内。打开rtconfig.h使用查找功能定位RT_USING_CONSOLE。你可能会看到它被定义为 0 或者根本没有定义。将其修改为#define RT_USING_CONSOLE 1这个操作相当于打开了系统控制台功能的总开关。修改后立即编译你很可能会遇到错误提示rt_hw_console_output函数未定义。这是完全正常的也是我们预期的因为我们还没有实现这个函数。这个错误恰恰说明系统已经准备使用控制台正在寻找底层的硬件输出接口。3.2 移植并完善串口初始化函数uart_init接下来我们要在board.c文件中实现串口硬件初始化。board.c是板级支持包的核心它包含了硬件初始化的框架。RT-Thread Nano 已经为我们预留了uart_init()函数的空壳我们需要用梁山派串口的实际初始化代码来填充它。首先打开梁山派的bsp_usart.c文件找到串口初始化函数可能叫usart_init或usart_config。我们需要将其中的 GPIO 配置、USART 参数配置、中断配置等核心代码移植过来。但这里有个更高效的做法直接复用bsp_usart.c中已经写好的配置函数。观察bsp_usart.c通常会有一个专门配置 GPIO 复用和模式的函数比如usart_gpio_config()。我们打开board.c找到uart_init()函数它可能是一个空函数或者只有个框架。然后我们从bsp_usart.c里复制usart_gpio_config()函数体内的所有代码粘贴到board.c的uart_init()函数中。但是直接粘贴肯定会报错因为代码里用到的大量宏定义如BSP_USART_TX_RCU,BSP_USART,BSP_USART_IRQ等都定义在bsp_usart.h里。所以我们必须在board.c文件的开头通常在#include “rtthread.h”之后添加一行#include “bsp_usart.h”这样编译器就能认识这些宏了。这些宏在bsp_usart.h中定义了具体的引脚、端口、时钟和中断号例如#define BSP_USART USART0 #define BSP_USART_RCU RCU_USART0 #define BSP_USART_TX_PORT GPIOA #define BSP_USART_TX_PIN GPIO_PIN_9 #define BSP_USART_TX_RCU RCU_GPIOA #define BSP_USART_IRQ USART0_IRQn // ... 等等它们将抽象的“控制台串口”映射到了梁山派开发板上具体的 USART0 (PA9, PA10) 引脚。这就是板级支持的意义——实现硬件抽象。现在uart_init()函数里已经有了 GPIO 和 USART 的基本配置代码。我们还需要关注一个关键参数波特率。在复制的代码中波特率可能是一个变量band_rate或者一个固定的宏。为了将其设置为控制台常用的 115200我们需要找到设置波特率的那行代码通常是usart_baudrate_set(BSP_USART, band_rate);。将band_rate直接改为115200或者修改band_rate变量的值/宏定义确保最终设置的波特率是 115200。这个波特率需要与后续你电脑上串口终端软件的设置保持一致。3.3 实现底层输出函数rt_hw_console_output这是连接 RT-Thread 打印系统和硬件串口的关键桥梁。rt_kprintf函数内部最终会调用rt_hw_console_output(const char *str)我们需要在这个函数里将字符串str一个字符一个字符地通过串口发送出去。在board.c中找到rt_hw_console_output函数它可能也是一个空壳。我们需要实现其功能。一个健壮的实现需要考虑换行符的处理。在 C 语言中换行是\n但在串口通信中通常需要发送“回车”Carriage Return,\rASCII 0x0D和“换行”Line Feed,\nASCII 0x0A两个字符才能使终端光标回到行首并下移一行。许多终端软件如 Putty, SecureCRT的“自动换行”设置可以只处理\n但为了兼容性最好我们主动将\n转换为\r\n。因此rt_hw_console_output的标准实现逻辑如下获取传入字符串的长度。遍历字符串中的每一个字符。如果当前字符是\n则先发送一个\r回车符。发送当前字符。重复直到字符串结束。对应的代码实现如下void rt_hw_console_output(const char *str) { rt_size_t i 0; rt_size_t size 0; char a ‘\r’; // 回车符 if (str RT_NULL) return; // 增加健壮性判断 size rt_strlen(str); for (i 0; i size; i) { if (*(str i) ‘\n’) { // 遇到换行符先发送回车符 while(usart_flag_get(BSP_USART, USART_FLAG_TBE) RESET); // 等待发送缓冲区空 usart_send_data(BSP_USART, (uint8_t)a); } // 发送当前字符 while(usart_flag_get(BSP_USART, USART_FLAG_TBE) RESET); // 等待发送缓冲区空 usart_send_data(BSP_USART, (uint8_t)*(str i)); } }代码解析与注意事项rt_strlen是 RT-Thread 内部提供的字符串长度函数与标准 C 库的strlen功能相同。usart_flag_get(BSP_USART, USART_FLAG_TBE)是 GD32 库函数用于查询串口“发送缓冲区空”标志位。TBE即 Transmit Buffer Empty。这里是一个关键点在调用usart_send_data发送一个字节前必须等待这个标志位为SET或非RESET表示上一个数据已经从缓冲区移送到移位寄存器可以发送新数据了。如果不等待直接连续发送会导致数据覆盖丢失打印出乱码。这就是“阻塞式发送”虽然简单有效但在高频率打印时会影响系统实时性。对于控制台调试这种方式是可接受的。usart_send_data(BSP_USART, (uint8_t)*(str i))是实际的发送函数将字符写入串口的数据寄存器。我添加了if (str RT_NULL) return;这行这是一个良好的编程习惯防止传入空指针导致程序崩溃。将这段代码复制到board.c的rt_hw_console_output函数体中。现在再次编译工程应该就不会再有关于控制台的链接错误了。4. 连接测试与功能验证代码移植和配置完成后就到了激动人心的测试环节。我们需要验证rt_kprintf是否能通过串口正常输出。4.1 编写测试代码在main.c的main函数中或者在创建的第一个任务里添加测试打印语句。一个简单的例子如下int main(void) { // 系统初始化等操作... rt_kprintf(“\n\r”); // 先发送一组换行回车让终端显示清晰 rt_kprintf(“[UART Console Test] System Start.\n”); rt_kprintf(“Hello, LiangshanPi GD32F470 RT-Thread Nano!\n”); while (1) { rt_thread_mdelay(1000); // 延时1秒 rt_kprintf(“RT-Thread Nano is running, tick: %d\n”, rt_tick_get()); // 打印系统心跳 } }这段代码在系统启动后打印欢迎信息然后每隔一秒打印一次当前的系统心跳计数rt_tick_get()的返回值。这不仅能测试串口输出还能直观地看到系统是否在正常运行。4.2 硬件连接与终端设置将梁山派开发板通过 USB 线连接到电脑。梁山派板载的 USB 转串口芯片通常是 CH340 或 CP2102会将 USART0 的引脚PA9, PA10连接到 USB 口。因此你需要在电脑上识别出对应的串口号COMx 或 /dev/ttyUSBx。安装驱动如果电脑是第一次连接该开发板可能需要安装 CH340 或 CP2102 的 USB 转串口驱动。驱动通常在开发板资料包里可以找到。查看端口在 Windows 设备管理器的“端口COM 和 LPT”下查看新增的串口记住其 COM 编号如 COM3。在 Linux 或 macOS 下可以使用ls /dev/ttyUSB*或ls /dev/tty.SLAB*等命令查看。打开终端软件使用你喜欢的串口终端软件如 Putty、SecureCRT、MobaXterm 或者 Arduino IDE 的串口监视器。我个人习惯用 Putty 或 VS Code 的串口插件。配置终端参数端口选择你刚才查到的 COMx 或 /dev/ttyUSBx。波特率设置为115200必须与我们在uart_init中设置的完全一致。数据位8停止位1校验位None流控制None连接点击打开串口。4.3 现象分析与问题排查给开发板上电并复位程序。如果一切顺利你应该在串口终端软件里看到如下输出[UART Console Test] System Start. Hello, LiangshanPi GD32F470 RT-Thread Nano! RT-Thread Nano is running, tick: 1000 RT-Thread Nano is running, tick: 2000 RT-Thread Nano is running, tick: 3000 ...心跳计数每秒增加RT_TICK_PER_SECOND默认为 100即每秒打印一次数值增加 100。如果看不到任何输出或者输出是乱码请按照以下步骤排查检查硬件连接确保 USB 线连接牢固开发板供电正常电源指示灯亮。确认串口号和波特率这是最常见的问题。务必确认终端软件选择的串口号正确且波特率严格设置为 115200。检查代码中的波特率再次核对board.c中uart_init函数里usart_baudrate_set调用设置的波特率值。检查rt_hw_console_output实现重点检查usart_send_data函数调用是否正确。第一个参数必须是你的串口宏例如BSP_USART即USART0。检查usart_flag_get轮询等待的代码是否被正确包含。如果不等待发送完成就发送下一个字节可能导致数据丢失或硬件 FIFO 溢出。检查串口引脚映射确认bsp_usart.h中的宏定义是否确实映射到了 USART0 和正确的 GPIO 引脚PA9, PA10。可以对比梁山派原理图进行确认。检查系统时钟GD32F470 的系统时钟频率会影响串口波特率发生器的计算。确保你的system_gd32f4xx.c中的系统时钟配置如通过system_clock_200m_25m_hxtal()函数与工程实际运行频率一致。如果系统时钟配置错误实际波特率会和设定值产生偏差导致乱码。使用调试器单步调试如果条件允许使用 J-Link 或 DAP-Link 等调试器连接开发板在rt_hw_console_output函数和uart_init函数中设置断点单步执行观察程序是否执行到这些函数以及发送数据寄存器的值是否正确。检查中断冲突进阶如果你在工程中同时启用了其他中断如 SysTick 中断是默认开启的并且串口初始化中配置了中断如我们代码中的nvic_irq_enable和usart_interrupt_enable但还没有实现中断服务函数这可能会导致程序异常。对于目前只实现输出的控制台可以暂时注释掉uart_init中所有与中断使能相关的代码行即nvic_irq_enable和两个usart_interrupt_enable调用。因为我们目前只使用发送功能且是轮询等待发送完成不需要中断。5. 常见问题与深度优化思考在实现过程中除了上述基本的连通性问题还有一些更深层次或常见的疑惑点这里集中分享一下。5.1 为什么需要board.c和bsp_usart.c两个文件它们分工是什么这是一个关于软件分层的好问题。bsp_usart.c/h属于板级驱动层。它封装了“梁山派开发板上 USART0 硬件如何初始化”的具体细节。比如USART0 对应哪两个 GPIO 引脚PA9, PA10它们的时钟是哪个RCU_GPIOA, RCU_USART0复用功能是 AF7 等等。这部分代码高度依赖具体的硬件板卡。如果换一块板子即使也是 GD32F470但串口接到了不同的引脚上那么只需要修改bsp_usart.h中的宏定义和bsp_usart.c中的初始化序列而上层代码可以不动。board.c属于RT-Thread 板级支持包适配层。它实现了 RT-Thread 内核所要求的一系列标准硬件接口函数例如rt_hw_board_init,uart_init,rt_hw_console_output等。它的作用是将 RT-Thread 内核的抽象硬件需求“翻译”并“对接”到下层具体的板级驱动bsp_usart.c上。board.c中的uart_init调用了源自bsp_usart.c的配置逻辑rt_hw_console_output则调用 GD32 标准库的usart_send_data函数。这种分层设计提高了代码的复用性和可移植性。移植到新板子时我们主要工作是编写或修改bsp_xxx.c驱动以及调整board.c中的对接逻辑而 RT-Thread 内核源码完全无需改动。5.2 轮询发送 (while(usart_flag_get(...))) 会影响系统实时性吗如何优化会的。while循环等待发送完成是一种“忙等待”Busy Waiting在此期间 CPU 被完全占用无法响应其他任务或中断。对于调试信息的偶尔打印影响不大。但如果需要高频、大量地打印日志这种方式会严重降低系统的实时响应能力。优化方案是采用中断驱动发送或DMA发送。中断发送使能串口“发送完成中断”或“发送缓冲区空中断”。在rt_hw_console_output函数中不再等待而是将数据放入一个软件 FIFO环形缓冲区然后启动第一次发送并打开发送中断。在中断服务函数中检查 FIFO 中是否还有数据有则取出并写入数据寄存器没有则关闭发送中断。这样CPU 只在真正需要搬移数据到硬件寄存器时才被短暂中断大部分时间可以处理其他任务。DMA发送配置 DMA 通道将内存中的字符串数据块直接搬运到串口的数据寄存器。rt_hw_console_output函数只需启动 DMA 传输CPU 即可立即返回。DMA 在后台完成全部数据的发送对 CPU 占用率极低。这是最高效的方式但配置相对复杂。RT-Thread 的完整版非 Nano通常提供了基于设备框架的串口驱动已经实现了中断和 DMA 模式。在 Nano 上如果需要高性能输出可以参照其思路自行实现一个带缓冲区的中断或 DMA 驱动。5.3 编译时出现undefined symbol SystemCoreClock错误这个问题可能在添加了 GD32 标准外设库后出现。gd32f4xx_usart.c中的波特率设置函数usart_baudrate_set内部可能会使用一个全局变量SystemCoreClock来计算分频值。这个变量在system_gd32f4xx.c中定义和更新它代表了当前系统的核心时钟频率HCLK。解决方法确保system_gd32f4xx.c文件已经添加到你的工程中通常在Device组里。在board.c或main.c等会调用uart_init的文件中确保包含了gd32f4xx.h头文件该头文件会声明SystemCoreClock这个外部变量。检查你的系统时钟初始化函数如system_clock_200m_25m_hxtal()是否在main函数开始或rt_hw_board_init中正确调用以确保SystemCoreClock被正确赋值。5.4 想换用其他串口如 USART1作为控制台怎么办这充分体现了我们分层设计的好处。修改步骤非常清晰修改板级驱动在bsp_usart.h中修改所有相关的宏定义。例如将BSP_USART从USART0改为USART1将引脚宏BSP_USART_TX_PORT、BSP_USART_TX_PIN等改为 USART1 对应的引脚例如 PB6, PB7将时钟宏BSP_USART_RCU改为RCU_USART1将中断号BSP_USART_IRQ改为USART1_IRQn。检查硬件连接确保你的硬件线路如 USB 转串口模块实际连接到了新的引脚PB6, PB7上。重新编译由于board.c中的uart_init和rt_hw_console_output函数都是通过宏BSP_USART来引用串口的所以只要宏定义改了它们就会自动操作新的串口。无需修改board.c的代码。通过以上步骤我们不仅成功实现了 UART 控制台功能还深入理解了其背后的层次结构、工作原理和调试方法。这个控制台是后续添加 FinSH 交互命令行的基础。有了它我们就能像在电脑终端里一样实时查看 RT-Thread 内核的运行状态、变量值甚至动态执行一些函数嵌入式开发的调试体验将获得质的提升。