简介本资源是面向嵌入式初学者与GD32单片机开发者的串口通信入门实践例程基于国产GD32F103C8T6芯片兼容STM32F103系列聚焦USART1基础收发功能实现——通过PA9TX与PA10RX与PC端调试软件交互实现“接收即回传”的实时响应逻辑适用于裸机驱动学习、外设验证及调试接口搭建等典型场景。压缩包共82个文件含29个头文件.h定义寄存器、函数接口与配置宏、27个源文件.c涵盖系统初始化、GPIO/USART/RCC驱动、中断服务及主循环逻辑、4个汇编启动文件.s、2个可执行hex固件及Keil MDK工程配置文件.uvprojx/.uvoptx结构完整符合标准CMSISGD32外设库v3.0.0开发范式。目前已有412人学习下载配套readme.txt说明清晰工程可直接编译烧录运行无需额外配置是掌握GD32串口底层驱动与中断处理流程的实用参考模板。1. 项目概述为什么从GD32F103的串口开始如果你刚拿到一块GD32F103C8T6的开发板或者从STM32转过来想试试国产替代第一个要跑的例程大概率就是串口收发。这几乎成了嵌入式开发的“Hello World”。但别小看这个简单的串口它背后牵扯到时钟树配置、GPIO复用、中断管理、数据缓冲等一系列核心概念搞通了它单片机开发的大门才算真正推开。GD32F103C8T6作为一款与STM32F103C8T6高度兼容的国产ARM Cortex-M3内核单片机以其出色的性价比和供货稳定性在消费电子、工业控制等领域应用广泛。它的串口USART功能强大支持同步/异步通信、多处理器模式、硬件流控等。我们今天的任务就是写一个最基础、最实用的异步全双工收发例程让你不仅能收到数据还能稳定地发回去并且理解每一步配置的用意。这个例程的目标很明确实现PC通过USB转串口工具发送任意数据给GD32GD32收到后原样回传同时在调试助手界面上显示。我们会采用中断方式接收数据查询方式发送这是最常用也最易于理解的组合。过程中我会把那些数据手册里一笔带过、但实际调试中坑最多的细节都掰开讲清楚。2. 工程搭建与核心外设初始化解析2.1 开发环境与工程创建要点工欲善其事必先利其器。对于GD32主流的选择是Keil MDK-ARM我后面简称Keil和GCCMakefile/VSCode。考虑到大多数初学者的使用习惯这里以Keil为例。首先你需要从兆易创新官网下载GD32F10x系列的Device Family PackDFP支持包安装后Keil的芯片列表里就会出现GD32的选项。新建工程时选择芯片型号“GD32F103C8T6”。这里有个关键点务必勾选“Copy System Startup Code to Project Folder”。GD32的启动文件startup_gd32f10x_hd.s和STM32的虽然相似但在时钟初始化部分有差异使用GD32专用的启动文件能避免一些诡异的时钟问题。接着需要把GD32标准外设库Firmware Library中的核心文件添加到工程。至少需要这几个分组CMSIS: 包含内核相关的头文件和启动文件。Firmware/StdPeriph_Driver/src: 外设驱动源文件我们至少需要gd32f10x_gpio.c,gd32f10x_rcu.c,gd32f10x_usart.c。Firmware/StdPeriph_Driver/inc: 对应的头文件目录。User: 存放我们的main.c,gd32f10x_it.c中断服务函数文件等。在魔术棒Options for Target设置里“C/C”选项卡的“Include Paths”一定要把上述inc目录和CMSIS目录添加进去。Define一栏通常需要填写GD32F10X_HD因为C8T6属于高密度产品线。这些步骤看似繁琐但一次配好后续开发就顺畅了。2.2 时钟系统配置一切稳定运行的基础GD32F103的时钟树比51单片机复杂得多也是性能优化的关键。系统时钟SYSCLK最高可达108MHz来源可以是内部8MHz RC振荡器IRC8M、外部晶振HXTAL或者PLL倍频。为了获得精确的串口波特率强烈推荐使用外部晶振。我们的板子上通常有一颗8MHz的晶振。初始化时钟的第一步是在system_gd32f10x.c文件或直接在main函数开头里调用rcu_clock_config函数进行配置。一个典型的108MHz配置流程如下使能外部高速晶振HXTALrcu_osc_on(RCU_HXTAL);等待晶振稳定while(SUCCESS ! rcu_osc_stab_wait(RCU_HXTAL));配置AHB、APB2、APB1分频器。APB1总线时钟最高36MHz串口1挂在APB2上最高108MHz串口2/3挂在APB1上。配置PLL选择HXTAL作为PLL时钟源进行倍频。例如8MHz * 27 216MHz然后经过2分频得到108MHz的PLL输出。选择PLL作为系统时钟源。为什么这么麻烦因为串口波特率发生器USART_BRR寄存器的值是根据给定的波特率和当前串口所在的APB总线时钟频率计算出来的。如果系统时钟源不稳定或者频率不准计算出的分频系数就会有误差导致通信乱码。使用外部晶振PLL能获得稳定且精确的时钟源这是通信可靠性的第一道保障。注意GD32F103的PLL配置范围与STM32F103略有不同务必参考GD32的数据手册和库函数说明直接套用STM32的代码可能会导致时钟配置错误系统无法运行。2.3 GPIO复用配置让引脚变身串口功能GD32F103C8T6的串口1USART0默认复用引脚是PA9TX和PA10RX。我们需要把这两个普通的GPIO引脚配置成复用推挽输出TX和浮空输入RX模式。void gpio_config(void) { // 开启GPIOA时钟 rcu_periph_clock_enable(RCU_GPIOA); // 配置PA9为复用推挽输出 (USART0 TX) gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_9); // 配置PA10为浮空输入 (USART0 RX) gpio_init(GPIOA, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_10); }这里有几个细节为什么要使能GPIO时钟这是ARM Cortex-M系列芯片的特点所有外设时钟默认是关闭的以节能使用前必须手动打开。GPIO速度设置为50MHz。对于串口这种低速外设其实10MHz也够用但设为50MHz也无妨。对于TX引脚输出速度会影响信号边沿的陡峭程度在高速或长线传输时有一定影响。为什么RX是浮空输入因为RX引脚是接收端由外部设备驱动。浮空输入模式下引脚电平完全由外部信号决定内部既不拉高也不拉低。如果板子上没有外部上拉电阻在空闲状态下RX引脚可能处于不稳定状态此时可以考虑使用上拉输入模式GPIO_MODE_IPU通过内部电阻拉到高电平避免干扰。3. 串口驱动深度配置与中断管理3.1 串口参数化配置与波特率计算内幕配置完引脚接下来就是核心的串口初始化。我们需要配置波特率、数据位、停止位、校验位和硬件流控等。这里我们使用最常见的8位数据位、1位停止位、无校验位。void usart_config(void) { // 开启USART0时钟 rcu_periph_clock_enable(RCU_USART0); // 初始化USART参数 usart_deinit(USART0); usart_baudrate_set(USART0, 115200U); usart_word_length_set(USART0, USART_WL_8BIT); usart_stop_bit_set(USART0, USART_STB_1BIT); usart_parity_config(USART0, USART_PM_NONE); usart_hardware_flow_control_set(USART0, USART_RTS_DISABLE, USART_CTS_DISABLE); usart_receive_config(USART0, USART_RECEIVE_ENABLE); usart_transmit_config(USART0, USART_TRANSMIT_ENABLE); // 使能USART usart_enable(USART0); }重点看usart_baudrate_set(USART0, 115200U)。这个库函数内部帮我们计算并设置了USART_BRR寄存器。这个寄存器的值决定了波特率分频系数。计算公式是BRR (f_APBx) / baudrate其中f_APBx是串口所在APB总线的时钟频率。对于USART0它在APB2上假设我们系统时钟是108MHzAPB2不分频那么f_APB2 108MHz。计算108000000 / 115200 937.5。BRR寄存器是一个16位的寄存器高4位用于小数部分低12位用于整数部分。所以937.5的整数部分937放在低12位小数部分0.5需要转换0.5 * 16 8放在高4位。最终BRR (812) | 937 0x8 3A9。库函数usart_baudrate_set就是帮你完成了这个计算和赋值。理解这个过程很重要当通信波特率出现偏差时你可以手动计算并核对一下这个寄存器的值是否正确。3.2 中断配置与优先级管理实战我们希望单片机在收到数据时能立即响应而不是不停地去查询状态寄存器这就需要使用中断。串口接收中断是必须开启的。void nvic_config(void) { // 配置USART0全局中断 nvic_irq_enable(USART0_IRQn, 0, 0); // 抢占优先级0子优先级0 } // 在usart_config函数中使能中断后添加 usart_interrupt_enable(USART0, USART_INT_RBNE); // 使能接收缓冲区非空中断USART_INT_RBNE中断是当接收数据寄存器RDR非空即收到一个字节的数据时触发。这是最常用的接收中断方式。为什么不使用“接收完成”中断因为“接收完成”中断在某些情况下可能不如“非空”中断及时。配置中断时还有一个关键步骤是编写中断服务函数ISR。这个函数必须放在gd32f10x_it.c文件中并且函数名要严格按照向量表定义来写对于USART0就是USART0_IRQHandler。// 在gd32f10x_it.c中 uint8_t usart_rx_buffer[256]; // 接收缓冲区 uint16_t usart_rx_index 0; // 缓冲区写入索引 void USART0_IRQHandler(void) { if(RESET ! usart_interrupt_flag_get(USART0, USART_INT_FLAG_RBNE)) { // 读取接收到的数据该操作会自动清除RBNE中断标志位 usart_rx_buffer[usart_rx_index] usart_data_receive(USART0); // 简单的缓冲区溢出保护 if(usart_rx_index 256) { usart_rx_index 0; } // 可以在这里设置一个标志位通知主循环处理数据 // usart_rx_flag 1; } // 理论上还应处理其他中断标志如发送完成、错误中断等但此例程简化处理 }在中断服务函数里有黄金法则快进快出。不要在里面做复杂的运算或调用可能阻塞的函数如printf。我们的做法通常是读取数据 - 存入缓冲区 - 更新索引或设置标志位 - 清除中断标志 - 立即退出。具体的数据处理如协议解析、回传放到主循环中根据标志位去执行。实操心得中断优先级nvic_irq_enable的参数抢占优先级和子优先级在简单系统中可以都设为0。但在复杂系统中如果串口通信实时性要求高需要给它分配一个较高的抢占优先级数字越小优先级越高防止被其他低优先级中断长时间阻塞导致数据丢失。4. 主循环逻辑与数据收发实现4.1 主函数框架与数据回传机制外设和中断都配置好后主函数的任务就清晰了初始化然后在一个死循环里检查是否有数据需要处理并回传。#include gd32f10x.h #include stdio.h // 可选用于printf重定向 // 声明外部缓冲区实际在it.c中定义 extern uint8_t usart_rx_buffer[256]; extern uint16_t usart_rx_index; volatile uint8_t usart_rx_flag 0; // 接收完成标志在中断中置位 int main(void) { // 系统时钟、GPIO、串口、中断初始化 system_clock_config(); // 假设此函数配置了108MHz时钟 gpio_config(); usart_config(); nvic_config(); // 可选printf重定向到串口方便调试 // usart_printf_redirect(USART0); while(1) { // 方式1轮询标志位推荐 if(usart_rx_flag) { usart_rx_flag 0; // 处理接收缓冲区中的数据 for(uint16_t i 0; i usart_rx_index; i) { // 回传数据 usart_data_transmit(USART0, usart_rx_buffer[i]); // 等待上一个字节发送完成 while(RESET usart_flag_get(USART0, USART_FLAG_TBE)); } usart_rx_index 0; // 处理完清空索引 } // 方式2也可以直接轮询发送简单但低效 // if(usart_flag_get(USART0, USART_FLAG_RBNE) ! RESET) { // uint8_t data usart_data_receive(USART0); // usart_data_transmit(USART0, data); // while(RESET usart_flag_get(USART0, USART_FLAG_TBE)); // } // 其他后台任务... } }在主循环中我们采用“中断接收主循环查询发送”的模式。中断负责快速“抢下”数据存好主循环检测到usart_rx_flag标志后再将缓冲区里的数据一个一个取出来通过usart_data_transmit函数发送出去。每发送一个字节都要通过while循环等待USART_FLAG_TBE发送缓冲区空标志置位确保上一个字节已从移位寄存器移出才能发送下一个否则数据会覆盖丢失。4.2 发送的阻塞与非阻塞策略选择上面的代码使用的是阻塞式发送即while循环等待发送完成。这在单任务或者发送数据量不大的简单应用中没问题但会占用CPU时间。更高效的做法是采用非阻塞式发送利用发送完成中断TC或DMA。发送完成中断TC方式使能发送完成中断usart_interrupt_enable(USART0, USART_INT_TC);在中断服务函数中检查TC标志如果置位表示一个字节或一帧数据已完全发出此时可以发送下一个字节或清除忙标志。主循环中只需将数据放入发送缓冲区并启动发送无需等待。DMA方式 对于大量连续数据的发送如发送一帧图像数据DMA是最佳选择。它可以自动将内存中的数据搬运到串口数据寄存器TDR完全解放CPU。配置DMA通道源地址是内存中的数组目标地址是USART0_TDATA寄存器。设置传输数据量。使能USART的DMA发送请求。启动DMA传输传输完成后会产生DMA中断。对于初学者例程阻塞式发送最简单直观。但在实际项目中尤其是需要处理多个任务时非阻塞式或DMA方式是必须掌握的技能。5. 调试技巧与典型问题排查实录5.1 硬件连接与软件调试工具使用首先确保硬件连接正确GD32 TX(PA9) ---USB转串口模块 RXGD32 RX(PA10) ---USB转串口模块 TX两地共地GD32的GND --- USB转串口模块的GND踩过的坑TX接TXRX接RX是新手最常犯的错误结果就是两边都收不到数据。记住“交叉互联”原则。另外如果使用CH340、CP2102这类USB转串口模块电脑端需要安装对应的驱动程序设备管理器中出现正确的COM口后才算连接成功。软件端推荐使用功能丰富的串口调试助手如SSCOM、XCOM、AccessPort等。关键设置选择正确的COM口。波特率设置为115200与代码一致。数据位8停止位1校验位无流控制无。勾选“十六进制显示”可以方便查看原始字节数据。5.2 常见问题速查与解决方案即使代码看起来没问题第一次调通串口也常常会遇到各种问题。下面这个表格是我总结的常见故障现象、可能原因和排查步骤现象可能原因排查步骤完全无收发1. 电源未接通或电压不对。2. 晶振未起振系统没运行。3. 串口线接错TX/RX反接。4. USB转串口模块损坏或驱动未安装。5. 代码中串口或GPIO时钟未使能。1. 测电压确认单片机供电正常3.3V。2. 用示波器测晶振引脚OSC_IN/OUT看是否有波形。最简单的办法写一个让LED闪烁的程序先确认单片机最小系统是否工作。3. 交换TX和RX线序试试。4. 换一个模块或检查设备管理器。5. 检查代码确认rcu_periph_clock_enable已调用。能发不能收或能收不能发1. 单向的线接错或虚焊。2. 代码中只使能了发送或接收。3. 中断配置错误仅影响接收。4. 对方设备流控设置不对。1. 重点检查单向不通的那根线。2. 检查usart_receive_config和usart_transmit_config。3. 检查NVIC和USART中断是否使能中断服务函数名是否正确。4. 确保调试助手和代码都关闭了硬件流控RTS/CTS。收发乱码1.波特率不匹配最常见。2. 时钟源配置错误系统时钟频率不对。3. 外部晶振频率与代码中PLL配置不符。4. 电源噪声大信号质量差。1. 核对代码和调试助手的波特率是否精确一致。尝试降低波特率如9600测试。2. 检查system_clock_config函数确认最终系统时钟频率。用示波器测量一个GPIO翻转的频率来反推系统时钟。3. 确认板载晶振是8MHz还是12MHz修改PLL倍频系数。4. 在TX/RX线上串联一个22-100欧姆的电阻或在信号线对地加10-50pF电容滤除高频噪声。只能接收一次或几次1. 接收中断标志未清除。2. 接收缓冲区溢出后未正确处理。3. 中断服务函数执行时间过长导致丢失后续中断。1. 在中断中读取usart_data_receive会自动清除RBNE标志确保你用了这个函数。2. 检查缓冲区大小和索引管理逻辑防止写穿。3. 优化中断服务函数只做最必要的操作存数据、设标志。发送数据丢失最后几个字节1. 在发送循环后立即复位或进入低功耗模式。2. 未等待最后一字节发送完成就关闭了串口或单片机。1. 发送完所有数据后增加一个等待TC发送完成标志的循环。2. 确保主循环持续运行不要在发送后立即执行__WFI()等休眠指令。5.3 进阶调试逻辑分析仪与printf重定向当问题比较复杂时串口调试助手的信息可能不够。这时可以请出硬件调试利器——逻辑分析仪或者带逻辑分析仪功能的示波器。用它同时抓取TX和RX引脚上的波形可以直观地看到波形是否符合UART协议起始位低电平、8位数据、停止位高电平。波特率是否准确测量一个位的时间宽度倒数即为实际波特率。数据内容是否正确。另一个极其有用的调试方法是printf重定向。将标准C库的printf函数输出重定向到串口就可以像在PC上编程一样随时打印变量值、程序状态等信息。// 在main.c中实现fputc函数重定向到USART0 #include stdio.h int fputc(int ch, FILE *f) { usart_data_transmit(USART0, (uint8_t)ch); while(RESET usart_flag_get(USART0, USART_FLAG_TBE)); return ch; }实现后在代码中就可以直接使用printf(System Clock: %d Hz\r\n, SystemCoreClock);来输出了。注意使用printf会显著增加代码体积并且不是线程安全的在中断中调用要特别小心通常不建议这么做。6. 从例程到项目稳定性与扩展性优化一个能跑通的例程只是起点要用于实际项目还需要考虑更多。首先数据缓冲区的管理需要加强。我们例程中用的简单线性缓冲区有溢出风险。更稳健的做法是使用环形缓冲区FIFO。创建两个索引读索引和写索引。当写索引追上读索引时表示缓冲区满当读索引追上写索引时表示缓冲区空。中断服务函数只负责向环形缓冲区写主循环从中读。这样可以高效、安全地处理数据流。其次要考虑通信协议。裸串口数据流是没有边界的你发送“123”对方可能一次收到“123”也可能分三次收到“1”、“2”、“3”。因此需要定义简单的应用层协议例如帧头长度数据校验例如0xAA 0x55 [长度L] [数据...] [校验和]。接收方只有检测到正确的帧头和长度并且校验和通过才认为收到一帧有效数据。特定结束符例如以\r\n作为一帧的结束。这在AT指令等场景中很常见。最后错误处理机制必不可少。使能串口的错误中断如噪声错误、帧错误、溢出错误在中断服务函数中检测并清除这些错误标志记录错误日志必要时进行复位或重连操作。一个健壮的通信模块必须能应对线路干扰等异常情况。通过这个GD32F103C8T6的串口收发例程我们不仅实现了功能更深入理解了时钟、GPIO、中断、缓冲区、协议这些嵌入式通信的基石。下次当你需要调试传感器、连接蓝牙模块、与上位机通信时这套流程和排查思路都能直接套用。记住调试硬件就是和物理世界对话耐心和逻辑比代码本身更重要。本文还有配套的精品资源点击获取