☰
GD32E10x官方例程包实战指南:从STM32迁移到外设调试避坑
2026/10/14 18:34:39 网站建设 项目流程

简介:GD32E10x系列官方例程是兆易创新面向嵌入式开发者提供的示例代码集,基于ARM Cortex-M3内核,主频可达72MHz,适用于工业控制、消费电子、通信设备等场景,适合已具备单片机基础、希望快速上手国产MCU的工程师与高校学生。压缩包共935个文件,约11.16MB,以288个h头文件与261个c源文件为核心,配套37套uvproj/uvopt工程、36套IAR工程文件,以及若干启动汇编、链接脚本、Makefile与说明文档,覆盖GPIO、SPI、I2C、UART、ADC、DAC、定时器、CAN等外设驱动与中断处理示例。已有873人学习下载。通过阅读这些初始化代码、驱动与应用程序,读者可掌握系统时钟配置、外设初始化、中断服务及LED闪烁、串口通信、ADC采样等典型功能的实现思路,并借助Keil MDK或IAR环境直接编译调试,缩短项目开发周期,提升嵌入式系统开发能力。

1. 从一份 GD32E10x 官方例程包说起:它到底能帮你省下多少事

如果你手头正捏着一块 GD32E10x 系列的板子,却对着空白的工程模板发愁,那这份官方例程包大概率就是你最该先翻的东西。GD32E10x 是兆易创新基于 ARM Cortex-M3 内核做的通用 MCU,主频能跑到 120MHz,外设覆盖 GPIO、USART、SPI、I2C、ADC、DAC、定时器、CAN、USB 这一整套。官方例程包本质上就是把这些外设的驱动代码、初始化流程、中断处理、时钟配置全部拆成一个个独立可编译的工程,每个工程只干一件事,比如“用定时器输出 PWM”“用 USART 收发数据”“用 ADC 采电压”。对刚从 STM32 转过来的嵌入式硬件工程师来说,这套东西最大的价值不是代码写得多漂亮,而是它把 GD32 和 STM32 之间那些“看着一样、跑起来不一样”的坑,提前用可运行的代码给你标出来了。适合谁?适合手里有 GD32E10x 开发板、想快速验证硬件设计、或者需要从 STM32 迁移到 GD32 但不想重写底层驱动的从业者。新手能照着工程一步步跑通,熟手能直接扒里面的时钟树配置和中断向量表来改自己的项目。

2. 例程包的结构拆解:从目录到最小可编译单元

2.1 拿到包先看什么:目录层级与文件职责

一份典型的 GD32E10x 官方例程包,解压后通常长这样:根目录下会有Firmware、Template、Examples这几个文件夹。Firmware里放的是 CMSIS 内核文件、GD32E10x 标准外设库的.c和.h,还有启动文件startup_gd32e10x.s和链接脚本gd32e10x_flash.ld。Template是一个空壳工程,只配好了时钟和中断,适合拿来当自己项目的起点。Examples下面按外设分文件夹,比如GPIO、USART、TIMER、ADC、SPI、I2C,每个外设文件夹里再按功能分子目录,像GPIO/Running_led、USART/Printf、TIMER/PWM_output。每个子目录里至少有一个main.c、一个gd32e10x_it.c(中断服务函数)、一个工程文件(可能是 Keil 的.uvprojx或者 IAR 的.eww),以及一个readme.txt简单说明这个例程演示什么。

我一般拿到包先不急着编译,而是先看Firmware/GD32E10x_standard_peripheral/Include下面有哪些头文件,这决定了标准库支持哪些外设。然后翻Examples里跟手头硬件最接近的那个例程,比如板子上有个 LED 接在 PC13,那就直接找GPIO/Running_led,打开main.c看它怎么配时钟、怎么初始化 GPIO、怎么翻转电平。这一步能帮你快速判断这份例程包的版本和你的芯片型号是否对得上。

2.2 最小可编译单元:一个 GPIO 例程的完整构成

拿GPIO/Running_led举例,一个能跑起来的最小工程包含这些文件:

  • startup_gd32e10x.s:启动文件,负责设置堆栈指针、初始化中断向量表、调用SystemInit和main。
  • system_gd32e10x.c:系统时钟初始化,把芯片从内部 8MHz RC 时钟切换到外部晶振并倍频到 120MHz。
  • gd32e10x.h:寄存器定义和标准库函数声明。
  • main.c:用户代码,配置 GPIO 为推挽输出,然后在死循环里翻转电平。
  • gd32e10x_it.c:中断服务函数,空工程里通常是空的,但必须存在,因为启动文件里引用了中断向量。
  • gd32e10x_libopt.h:标准库的裁剪开关,通过宏定义决定编译哪些外设驱动,能减小代码体积。

编译这个工程,你至少需要把Firmware/CMSIS/GD/GD32E10x/Source和Firmware/GD32E10x_standard_peripheral/Source下的.c文件全部加入工程,头文件路径也要指到对应的Include目录。很多人第一次编译报一堆undefined reference,八成就是库文件没加全或者路径没配对。

2.3 从 STM32 迁移过来的人,第一件事该看哪里

STM32 和 GD32 虽然都是 Cortex-M3,外设寄存器地址和位定义却不一样。比如 STM32F103 的 GPIO 输出速度配置寄存器是GPIOx_CRL和GPIOx_CRH,GD32E10x 虽然也有类似寄存器,但复位后的默认值和时钟使能顺序有差别。官方例程里system_gd32e10x.c的SystemInit函数就是第一个要看的地方,它决定了芯片跑多快、外部晶振多少兆、PLL 怎么倍频。第二个要看的是gd32e10x_it.c里的中断向量名,STM32 叫USART1_IRQHandler,GD32 可能叫USART0_IRQHandler,名字对不上编译直接报错。第三个是标准库函数的命名习惯,STM32 标准库是GPIO_Init,GD32 标准库是gpio_init,参数结构体成员名也有细微差别。把这些对清楚,迁移就完成了一半。

3. 把例程跑起来:环境搭建与第一个工程编译

3.1 工具链选择:Keil、IAR 还是 GCC

官方例程包通常同时提供 Keil MDK 和 IAR EWARM 的工程文件,有些版本还会给 Makefile 支持 GCC。如果你习惯 Keil,直接双击Examples/GPIO/Running_led/Project/Running_led.uvprojx就能打开。但要注意,Keil 需要装 GD32E10x 的器件支持包,否则打开工程会提示找不到器件。这个支持包一般从官网下载,安装后在 Keil 的Pack Installer里能看到GigaDevice::GD32E10x系列。IAR 用户同理,需要对应的器件描述文件。GCC 用户则要自己写 Makefile 或者用官方的Makefile模板,把arm-none-eabi-gcc的路径配好。

我一般推荐新手先用 Keil,因为例程包里的 Keil 工程配置最全,下载算法、调试器设置、宏定义都配好了,省得自己折腾。熟手如果要做 CI 或者跨平台开发,可以拿 GCC 版本改,但要注意链接脚本里的内存地址和芯片实际 Flash/RAM 大小必须一致。

3.2 编译前必须检查的三个配置项

打开工程后,先别急着点编译,检查这三个地方:

第一,Options for Target里的Device是否选对了具体型号,比如GD32E103C8还是GD32E103RB,Flash 和 RAM 大小不一样,选错了下载会失败。

第二,C/C++选项卡里的Define框,通常会有GD32E10X、USE_STDPERIPH_DRIVER这类宏,如果缺了USE_STDPERIPH_DRIVER,标准库的gd32e10x_libopt.h就不会被包含,编译会报找不到函数声明。

第三,Debug选项卡里的调试器选对,比如 ST-Link 还是 J-Link,然后点Settings确认能识别到芯片 ID。如果识别不到,检查接线:SWDIO、SWCLK、GND、VCC 四根线必须接对,有些板子还要把 BOOT0 拉低。

/* main.c 里典型的 GPIO 初始化代码,以 Running_led 为例 */ #include "gd32e10x.h" #include "systick.h" void gpio_config(void) { /* 使能 GPIOC 时钟,GD32 的标准库函数是 rcu_periph_clock_enable */ rcu_periph_clock_enable(RCU_GPIOC); /* 配置 PC13 为推挽输出,速度 50MHz */ gpio_init(GPIOC, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_13); } int main(void) { /* 配置系统滴答定时器,1ms 中断一次 */ systick_config(); gpio_config(); while(1) { /* 翻转 PC13 电平 */ gpio_bit_write(GPIOC, GPIO_PIN_13, (bit_status)(1 - gpio_output_bit_get(GPIOC, GPIO_PIN_13))); /* 延时 500ms */ delay_1ms(500); } }

这段代码里rcu_periph_clock_enable对应 STM32 的RCC_APB2PeriphClockCmd,gpio_init对应GPIO_Init,参数顺序和枚举名都变了。GPIO_MODE_OUT_PP是推挽输出,GPIO_OSPEED_50MHZ是输出速度。gpio_bit_write和gpio_output_bit_get是 GD32 标准库提供的位操作函数,比直接读改写寄存器安全。systick_config和delay_1ms通常在systick.c里实现,例程包里会带。

3.3 下载与调试:识别不到芯片怎么办

编译通过后点下载,如果 Keil 报No target connected或者Flash Download failed,按这个顺序排查:先确认调试器驱动装了没有,设备管理器里有没有识别到 ST-Link 或 J-Link;再确认工程Debug设置里的调试器型号选对;然后检查板子供电,有些 GD32 板子 USB 供电和外部供电只能二选一,同时接可能烧坏;最后看复位电路,GD32 的 NRST 引脚如果被电容拉得太狠,调试器可能无法复位芯片,可以试着把复位方式改成Connect under Reset。还有一个玄学问题:有些 Keil 版本对 GD32 的 Flash 算法支持不好,需要手动添加GD32E10x_Flash.FLM到Flash Download的编程算法列表里,这个文件通常在器件支持包的Flash目录下。

4. 外设例程逐个拆:USART、定时器、ADC 的实操要点

4.1 USART 收发与 printf 重定向

Examples/USART/Printf这个例程演示了怎么把printf重定向到串口,方便调试。核心是重写fputc函数:

/* 重定向 printf 到 USART0 */ int fputc(int ch, FILE *f) { /* 等待发送数据寄存器空 */ while(usart_flag_get(USART0, USART_FLAG_TBE) == RESET); /* 把字符写入数据寄存器 */ usart_data_transmit(USART0, (uint8_t)ch); return ch; }

usart_flag_get查的是USART_FLAG_TBE发送缓冲区空标志,usart_data_transmit把数据塞进USART_DATA寄存器。串口初始化部分要配波特率、字长、停止位、校验位,GD32 的usart_baudrate_set函数会根据当前时钟自动算分频值,但前提是rcu_periph_clock_enable(RCU_USART0)已经调用,而且 USART0 挂的 APB 时钟频率跟system_gd32e10x.c里配的一致。常见坑是波特率算出来偏差太大导致乱码,这时候要检查外部晶振频率是不是跟system_gd32e10x.c里的HXTAL_VALUE宏对得上,比如板子焊的是 8MHz 晶振,宏定义写的是 12MHz,那串口肯定乱。

4.2 定时器 PWM 输出:频率与占空比怎么算

Examples/TIMER/PWM_output演示了用 TIMER2 的通道 0 输出 PWM。关键参数是预分频器prescaler和自动重装载值period。PWM 频率 = 定时器时钟 / ((prescaler + 1) * (period + 1))。假设定时器时钟是 120MHz,想要 1kHz PWM,那(prescaler + 1) * (period + 1)应该等于 120000。可以设prescaler = 119,period = 999。占空比由timer_channel_output_pulse_value_config函数设置,比如设成 500,占空比就是 500/1000 = 50%。

/* 配置 TIMER2 通道 0 输出 1kHz PWM,占空比 50% */ timer_parameter_struct timer_initpara; timer_oc_parameter_struct timer_ocintpara; rcu_periph_clock_enable(RCU_TIMER2); timer_deinit(TIMER2); timer_initpara.prescaler = 119; timer_initpara.alignedmode = TIMER_COUNTER_EDGE; timer_initpara.counterdirection = TIMER_COUNTER_UP; timer_initpara.period = 999; timer_initpara.clockdivision = TIMER_CKDIV_DIV1; timer_init(TIMER2, &timer_initpara); timer_ocintpara.ocpolarity = TIMER_OC_POLARITY_HIGH; timer_ocintpara.outputstate = TIMER_CCX_ENABLE; timer_channel_output_config(TIMER2, TIMER_CH_0, &timer_ocintpara); timer_channel_output_pulse_value_config(TIMER2, TIMER_CH_0, 500); timer_channel_output_mode_config(TIMER2, TIMER_CH_0, TIMER_OC_MODE_PWM0); timer_channel_output_shadow_config(TIMER2, TIMER_CH_0, TIMER_OC_SHADOW_DISABLE); timer_auto_reload_shadow_enable(TIMER2); timer_enable(TIMER2);

注意timer_channel_output_shadow_config如果设成TIMER_OC_SHADOW_DISABLE,改占空比会立即生效;设成ENABLE则要等下一个更新事件才生效。调试电机驱动时这个区别很关键,立即生效可能导致波形抖动。

4.3 ADC 规则通道采样与 DMA 搬运

Examples/ADC/ADC_DMA演示了 ADC0 规则通道配合 DMA 自动搬运数据。配置步骤:先使能 ADC0 和 DMA0 时钟,配 DMA 源地址为ADC_RDATA(ADC0),目标地址为用户数组,传输长度设成通道数,然后配 ADC 为规则通道、扫描模式、连续转换,最后使能 ADC 的 DMA 请求。常见坑是 ADC 时钟不能超过 14MHz,GD32E10x 的 ADC 时钟来自 APB2 分频,如果系统跑 120MHz,APB2 也是 120MHz,那 ADC 预分频至少设成 8 分频才能降到 15MHz 以下。另一个坑是 DMA 传输完成中断里要清标志,否则会反复进中断。

5. 避坑与排查:GD32E10x 例程里最容易翻车的五个地方

5.1 现象:编译报错undefined reference to SystemInit

原因:启动文件startup_gd32e10x.s里调用了SystemInit,但system_gd32e10x.c没加入工程,或者头文件路径没包含Firmware/CMSIS/GD/GD32E10x/Include。解决:把system_gd32e10x.c加到工程源文件列表,并在C/C++的Include Paths里加上 CMSIS 和标准库的Include目录。

5.2 现象:串口打印乱码,波特率明明设对了

原因:外部晶振频率和system_gd32e10x.c里的HXTAL_VALUE宏不一致。比如板子焊的是 8MHz 晶振,宏定义却是 12MHz,系统时钟实际跑不到 120MHz,串口分频值算错。解决:打开system_gd32e10x.c,找到#define HXTAL_VALUE那一行,改成板子实际晶振频率,重新编译。

5.3 现象:PWM 输出频率不对,示波器测出来差很多

原因:定时器时钟源搞错了。GD32E10x 的 TIMER0 挂 APB2,TIMER1/2/3 挂 APB1,如果 APB1 分频系数不是 1,定时器时钟会是 APB1 时钟的 2 倍。比如系统 120MHz,APB1 分频 2 得 60MHz,但定时器实际时钟是 120MHz。解决:查rcu_clock_freq_get(CK_APB1)和rcu_clock_freq_get(CK_TIMER1)的实际值,按真实时钟算 prescaler 和 period。

5.4 现象:ADC 采样值跳动很大,或者一直是 0

原因:ADC 时钟超频,或者 GPIO 没配成模拟输入模式。GD32E10x 的 ADC 时钟上限是 14MHz,超过后采样值不可信。另外 ADC 通道对应的 GPIO 必须配成GPIO_MODE_AIN,如果配成浮空输入,采样值会飘。解决:在rcu_adc_clock_config里把 ADC 预分频设大一点,确保 ADC 时钟低于 14MHz;检查 GPIO 初始化代码,把对应引脚设成模拟输入。

5.5 现象:中断进不去,或者进了中断就死循环

原因:中断向量名写错,或者中断标志没清。GD32 的中断向量名和 STM32 不完全一样,比如 USART0 的中断函数是USART0_IRQHandler,不是USART1_IRQHandler。另外在中断服务函数里必须调用对应的xxx_interrupt_flag_clear清标志,否则中断会反复触发。解决:对照startup_gd32e10x.s里的向量表确认函数名,在中断处理末尾加清标志语句。

6. 进阶用法:把例程改造成自己的项目模板

6.1 从 Template 工程开始裁剪

官方例程包里的Template工程是最干净的起点,只配了系统时钟和 SysTick,没有多余外设。我一般把它复制一份,重命名为项目名,然后按需往里加标准库源文件。比如项目要用 USART 和定时器,就只把gd32e10x_usart.c和gd32e10x_timer.c加入工程,其他外设的.c文件不加,这样编译出来的固件体积小,也减少变量冲突。gd32e10x_libopt.h里对应的宏也要打开,比如#define GD32E10X_USART和#define GD32E10X_TIMER,否则头文件里函数声明会被条件编译掉。

6.2 用宏定义做硬件版本管理

实际项目里板子可能改版,LED 引脚从 PC13 换到 PA1,串口从 USART0 换到 USART1。我习惯在main.h里用宏定义把硬件相关参数集中管理:

/* main.h 硬件配置宏 */ #define LED_GPIO_PORT GPIOC #define LED_GPIO_PIN GPIO_PIN_13 #define LED_RCU_CLOCK RCU_GPIOC #define DEBUG_USART USART0 #define DEBUG_USART_RCU RCU_USART0 #define DEBUG_USART_GPIO GPIOA #define DEBUG_USART_TX_PIN GPIO_PIN_9 #define DEBUG_USART_RX_PIN GPIO_PIN_10

这样换板子时只改main.h,不用满工程搜GPIOC和USART0。标准库函数调用时把宏传进去就行,比如gpio_init(LED_GPIO_PORT, ...)。这个习惯是从一次血泪经验里养成的:当时项目从 GD32E103 换到 GD32E103 的另一个封装,LED 引脚变了,结果忘了改一处gpio_bit_write里的端口参数,调试了半天才发现。

6.3 验证方法:用示波器和逻辑分析仪确认时序

例程跑起来只是第一步,要确认时序对不对,还得上仪器。PWM 输出用示波器看频率和占空比,串口用逻辑分析仪抓波形解码,SPI 和 I2C 用逻辑分析仪看时钟极性和相位。我一般会在关键代码前后翻转一个空闲 GPIO,然后用示波器测这个 GPIO 的高电平持续时间,就能算出函数执行时间。比如在 ADC 采样前后翻转 PA0,测到高电平 2us,说明采样加处理花了 2us。这个方法比用调试器单步更接近真实运行状态,也更容易发现中断延迟问题。

从那以后我每次拿到新的 MCU 例程包,都强制走一遍“编译→下载→示波器测波形→改宏定义换引脚→再测”的流程,确认整条链路都通了,才往里面加业务代码。希望帮到你。

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

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

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

立即咨询