news 2026/9/2 7:30:40

GD32F103串口通信实战:从时钟配置到中断收发全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
GD32F103串口通信实战:从时钟配置到中断收发全解析

简介:本资源是面向嵌入式初学者与GD32单片机开发者的串口通信入门实践例程,基于国产GD32F103C8T6芯片(兼容STM32F103系列),聚焦USART1基础收发功能实现——通过PA9(TX)与PA10(RX)与PC端调试软件交互,实现“接收即回传”的实时响应逻辑,适用于裸机驱动学习、外设验证及调试接口搭建等典型场景。压缩包共82个文件,含29个头文件(.h,定义寄存器、函数接口与配置宏)、27个源文件(.c,涵盖系统初始化、GPIO/USART/RCC驱动、中断服务及主循环逻辑)、4个汇编启动文件(.s)、2个可执行hex固件及Keil MDK工程配置文件(.uvprojx/.uvoptx),结构完整,符合标准CMSIS+GD32外设库(v3.0.0)开发范式。目前已有412人学习下载,配套readme.txt说明清晰,工程可直接编译烧录运行,无需额外配置,是掌握GD32串口底层驱动与中断处理流程的实用参考模板。

1. 项目概述:为什么从GD32F103的串口开始?

如果你刚拿到一块GD32F103C8T6的开发板,或者从STM32转过来想试试国产替代,第一个要跑的例程大概率就是串口收发。这几乎成了嵌入式开发的“Hello World”。但别小看这个简单的串口,它背后牵扯到时钟树配置、GPIO复用、中断管理、数据缓冲等一系列核心概念,搞通了它,单片机开发的大门才算真正推开。

GD32F103C8T6作为一款与STM32F103C8T6高度兼容的国产ARM Cortex-M3内核单片机,以其出色的性价比和供货稳定性,在消费电子、工业控制等领域应用广泛。它的串口(USART)功能强大,支持同步/异步通信、多处理器模式、硬件流控等。我们今天的任务,就是写一个最基础、最实用的异步全双工收发例程,让你不仅能收到数据,还能稳定地发回去,并且理解每一步配置的用意。

这个例程的目标很明确:实现PC通过USB转串口工具发送任意数据给GD32,GD32收到后原样回传,同时在调试助手界面上显示。我们会采用中断方式接收数据,查询方式发送,这是最常用也最易于理解的组合。过程中,我会把那些数据手册里一笔带过、但实际调试中坑最多的细节都掰开讲清楚。

2. 工程搭建与核心外设初始化解析

2.1 开发环境与工程创建要点

工欲善其事,必先利其器。对于GD32,主流的选择是Keil MDK-ARM(我后面简称Keil)和GCC+Makefile/VSCode。考虑到大多数初学者的使用习惯,这里以Keil为例。首先,你需要从兆易创新官网下载GD32F10x系列的Device Family Pack(DFP)支持包,安装后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配置流程如下:

  1. 使能外部高速晶振(HXTAL):rcu_osc_on(RCU_HXTAL);
  2. 等待晶振稳定:while(SUCCESS != rcu_osc_stab_wait(RCU_HXTAL));
  3. 配置AHB、APB2、APB1分频器。APB1总线时钟最高36MHz,串口1挂在APB2上(最高108MHz),串口2/3挂在APB1上。
  4. 配置PLL:选择HXTAL作为PLL时钟源,进行倍频。例如,8MHz * 27 = 216MHz,然后经过2分频得到108MHz的PLL输出。
  5. 选择PLL作为系统时钟源。

为什么这么麻烦?因为串口波特率发生器USART_BRR寄存器的值,是根据给定的波特率和当前串口所在的APB总线时钟频率计算出来的。如果系统时钟源不稳定或者频率不准,计算出的分频系数就会有误差,导致通信乱码。使用外部晶振+PLL,能获得稳定且精确的时钟源,这是通信可靠性的第一道保障。

注意:GD32F103的PLL配置范围与STM32F103略有不同,务必参考GD32的数据手册和库函数说明,直接套用STM32的代码可能会导致时钟配置错误,系统无法运行。

2.3 GPIO复用配置:让引脚变身串口功能

GD32F103C8T6的串口1(USART0)默认复用引脚是PA9(TX)和PA10(RX)。我们需要把这两个普通的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上,假设我们系统时钟是108MHz,APB2不分频,那么f_APB2 = 108MHz。计算108000000 / 115200 = 937.5

BRR寄存器是一个16位的寄存器,高4位用于小数部分,低12位用于整数部分。所以937.5的整数部分937放在低12位,小数部分0.5需要转换:0.5 * 16 = 8,放在高4位。最终BRR = (8<<12) | 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)方式

  1. 使能发送完成中断:usart_interrupt_enable(USART0, USART_INT_TC);
  2. 在中断服务函数中,检查TC标志,如果置位,表示一个字节或一帧数据已完全发出,此时可以发送下一个字节或清除忙标志。
  3. 主循环中,只需将数据放入发送缓冲区并启动发送,无需等待。

DMA方式: 对于大量连续数据的发送(如发送一帧图像数据),DMA是最佳选择。它可以自动将内存中的数据搬运到串口数据寄存器(TDR),完全解放CPU。

  1. 配置DMA通道,源地址是内存中的数组,目标地址是USART0_TDATA寄存器。
  2. 设置传输数据量。
  3. 使能USART的DMA发送请求。
  4. 启动DMA传输,传输完成后会产生DMA中断。

对于初学者例程,阻塞式发送最简单直观。但在实际项目中,尤其是需要处理多个任务时,非阻塞式或DMA方式是必须掌握的技能。

5. 调试技巧与典型问题排查实录

5.1 硬件连接与软件调试工具使用

首先确保硬件连接正确:

  • GD32 TX(PA9) --->USB转串口模块 RX
  • GD32 RX(PA10) --->USB转串口模块 TX
  • 两地共地:GD32的GND ---> USB转串口模块的GND

踩过的坑:TX接TX,RX接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_configusart_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、中断、缓冲区、协议这些嵌入式通信的基石。下次当你需要调试传感器、连接蓝牙模块、与上位机通信时,这套流程和排查思路都能直接套用。记住,调试硬件就是和物理世界对话,耐心和逻辑比代码本身更重要。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/2 7:29:23

开学季到了:制服熊孩子 数学题、字帖、英语、简笔画、拼音

数学题、字帖、英语、简笔画、拼音 小学生快乐神器 这款软件是我利用业余时间&#xff0c;专门为我家五岁的大儿子亲手打造的。做它的初衷很简单——陪他学习、陪他成长。分享出来&#xff0c;也是希望能帮到同样身为家长的朋友们&#xff0c;少走些弯路&#xff0c;不用再满世…

作者头像 李华
网站建设 2026/9/2 7:29:10

CVPR 2026 | DIQ:只用1%精选微调数据,让医学推理更高效,媲美全量数据

引言 大语言模型在医疗推理中展现出巨大潜力&#xff0c;但现有微调方法往往依赖海量标注数据&#xff0c;不仅计算成本高昂&#xff0c;且大量冗余、低质量的样本反而会稀释模型的临床推理能力。如何在有限数据下实现高效、精准的医学推理&#xff0c;成为亟待解决的核心难题。…

作者头像 李华
网站建设 2026/9/2 7:28:36

基于正则化方法的电阻率反演与成像系统:MATLAB实现与工程实践

简介&#xff1a;本资源是一个面向地球物理勘探科研人员与高年级本科生的MATLAB电阻率反演教学与实践工具&#xff0c;聚焦于解决电阻率成像中因数据不足与噪声干扰导致的反演不适定问题。系统采用Tikhonov等正则化策略&#xff0c;融合先验约束与优化求解&#xff0c;实现从地…

作者头像 李华
网站建设 2026/9/2 7:28:25

WorkBuddy 创建任务:从入门到实战的完整指南

1. 引言WorkBuddy 是一款面向团队协作与自动化的工作流管理工具&#xff0c;它通过「任务」这一核心概念&#xff0c;把零散的工作项、审批流程和自动化动作串联起来。无论是日常待办、跨部门协作&#xff0c;还是定时触发的自动化任务&#xff0c;WorkBuddy 都提供了统一且可扩…

作者头像 李华
网站建设 2026/9/2 7:27:03

icode身份凭证配置问题排查指南:从概念到实战解决方案

最近在开发过程中&#xff0c;很多同学都遇到了与 icode 相关的各种报错和配置问题&#xff0c;尤其是在集成第三方服务或进行身份验证时。这类问题往往表现为连接失败、认证无效或参数错误&#xff0c;错误信息又比较模糊&#xff0c;排查起来相当耗时。本文将系统性地梳理 …

作者头像 李华
网站建设 2026/9/2 7:22:53

基于Matlab/Simulink的四旋翼无人机控制仿真:从PID整定到动力学建模实战

简介&#xff1a;本资源是一套面向控制理论学习者、无人机方向本科生及入门级科研人员的四旋翼无人机Matlab控制仿真系统&#xff0c;聚焦飞行力学建模、姿态与轨迹跟踪控制算法设计与闭环验证等核心问题。压缩包共25个文件&#xff0c;含21个MATLAB脚本&#xff08;如runsim主…

作者头像 李华