☰
GD32F303CCT6官方例程实战:从固件库到外设开发与移植指南
2026/10/21 15:26:27 网站建设 项目流程

简介:GD32F303官方例程是一套面向嵌入式开发者的完整参考代码包,适用于正在学习或基于兆易创新Cortex-M4内核芯片做项目开发的工程师。压缩包共2000个文件,包含C源码、头文件、Keil与IAR工程配置,以及编译生成的axf/hex/map等文件,并附有说明文档,整体约79MB,目录按功能模块划分,便于查找。已有4230人下载学习,口碑良好。例程覆盖基本时钟与GPIO配置、ADC/DAC/SPI/I2C/UART外设驱动、定时器与PWM输出,并包含DMA、USB OTG、CAN、以太网等通信接口示例;针对实时应用还提供FreeRTOS移植和中断服务例程,以及LCD与FFT相关代码。开发者可直接基于Keil或IAR工程打开编译,通过现成代码理解寄存器操作与驱动编写思路,减少重复劳动,加快从评估到落地的开发进程。

1. GD32F303官方例程:一块CCT6芯片的全部起跑姿势

做项目时手里同时压着新唐和 ST 两块开发板的排期,A同学丢过来一个 GD32F303CCT6 的最小系统板,说先把官方例程跑通再谈移植。一开始我并没当回事——不过是点灯、串口打印这类外设 Demo,但真把压缩包解开、逐个工程打开后,我发现这份 F303 官方例程包的含金量远不止"能跑"这么简单。

它解决的是 GD32F303 系列开发里最绕的一段路:固件库函数怎么命名、时钟树怎么配、各外设从初始化到中断的完整调用链长什么样。对第一次接触国产 ARM 内核 MCU 的开发者来说,照着这份例程点灯、调串口、采 ADC,比对着数据手册硬啃省下至少一周时间。对已经熟悉 STM32 的熟手来说,这也是一份现成的"差异对照手册"——F303 的库函数名、外设寄存器布局和 ST 的 HAL 库并不完全一致,很多踩坑点都是"看起来一样,用起来天差地别"。这篇笔记就是我从解压到烧录、再到二次改造的真实过程记录,适合手上正好有 CCT6 芯片、想快速把外设跑起来的开发者,也适合准备把项目从其他 MCU 迁到 GD32 上的人。

2. 资源解剖:F303 固件库的目录结构与分层思路

2.1 CCT6 芯片规格:这块芯片能装下多大的工程

GD32F303CCT6 属于 GD32F303 增强型系列,Cortex-M4 内核,主频最高 120MHz,Flash 容量 256KB,SRAM 48KB,封装是 LQFP48。这个配置在电机控制、传感器采集、小型仪表这类场景里非常能打。相比同封装的其他国产 M4 芯片,F303 的优势在于内置了 3 个 12 位 ADC 和多个 16 位定时器,做多路模拟量采集时不用外挂 ADC 芯片,成本和布线难度都降了一截。

参数项数值
内核Cortex™-M4F,带 FPU,最高 120MHz
Flash256KB
SRAM48KB
封装LQFP48
定时器多个 16 位高级/通用定时器
ADC3 个 12 位逐次逼近型 ADC,支持 DMA
通信接口最多 5 组 USART,另有 I2C、SPI、CAN

拿到压缩包先别急着开 Keil,先确认你的芯片丝印是 CCT6 还是 RCT6——两者 Flash 容量不同(RCT6 是 256KB 但引脚更多),代码里如果按 RCT6 的引脚分配去初始化外设,LQFP48 上根本没有对应的引脚,编译能过、跑起来就是死的。这就是典型的手册没看清导致的翻车,后面我会在避坑章节专门展开。CCT6 的引脚数决定了它引出的串口、SPI、ADC 通道是有限的,例程里默认用的引脚分配基本就是这颗芯片的极限用法,直接照抄最稳。

2.2 目录结构拆解:官方例程到底给你准备了什么

解压后,顶层目录通常分为Firmware、Library、Project、Utilities四类。

GD32F30x_Demo_Suites/ ├── Firmware/ │ ├── CMSIS/ # 内核相关,包括启动文件 │ └── GD32F30x_standard_peripheral/ │ ├── inc/ # 标准外设库头文件 │ └── src/ # 标准外设库源文件 ├── Library/ # 部分版本叫 Utilities,放公共驱动 ├── Project/ │ ├── 01_GPIO_Runing_LED/ # 每个例程独立成文件夹 │ ├── 02_USART_Printf/ │ ├── 03_ADC/ │ └── ... └── Utilities/ ├── board.h # 板级支持 └── debug.c # 调试辅助

这个结构是典型的"库函数 + 例程工程"双轨设计。Firmware下面是整个硬件的抽象层,启动文件、系统时钟初始化、外设驱动都在这里面;Project下每个子文件夹是一个独立的可编译工程,互相不影响。好处是你想验证某个外设,直接打开对应工程改两行配置就行,不需要动其他文件。

我第一次看这份目录时最大的困惑是:为什么每个例程都要复制一份Firmware的完整代码?后来才明白这是官方刻意为之——保证每个例程脱离整体目录也能独立编译。代价是占磁盘空间,但换来了极强的可移植性。项目里如果只用到两三个外设,完全可以把用不到的库文件全部删掉,只保留需要的.c/.h,链接时能省不少 Flash。

2.3 工程模板:三套 IDE 你怎么选

打开的Project目录会发现每个例程里都有MDK-ARM、IAR、GD32_IDE三套工程文件。这对应了三种开发环境:Keil MDK、IAR EWARM、GD32 官方自研的 IDE。我的建议是:个人学习用 Keil MDK,公司生产用 IAR,刚入坑的新手可以直接用 GD32 官方 IDE——它对国产芯片做了很多开箱即用的配置,不需要自己折腾芯片支持包。

实际踩坑点在这:Keil MDK 默认不识别 GD32 芯片型号,需要先安装对应的 Device Family Pack。这个包如果装不上,打开工程会报错"Device not found",原因通常是 Keil 的包管理器网络问题或安装路径有中文。解决办法是手动下载 Pack 文件双击安装,路径不要带中文和空格。IAR 则需要对芯片型号直接做选择,GD32 官方 IDE 一般已经预置。这一节的结论是:别一上来三个工程都试,先选定一个 IDE,把"编译 → 烧录 → 调试"的链路跑通,再去碰其他环境。

3. 核心代码拆解:从复位到外设工作的完整链路

3.1 启动文件和系统时钟:上电后第一行代码在哪

打开任一例程的 Keil 工程,在CMSIS目录下能看到两个关键文件:启动文件(汇编.s文件)和system_gd32f30x.c。启动文件负责建立中断向量表、初始化堆栈、调用SystemInit(),之后才进入main()。这颗芯片上电默认是内部 RC 振荡器运行,频率不高,真正把主频拉到 120MHz 靠的是SystemInit()里配置的外部高速晶振 PLL 倍频。

/* system_gd32f30x.c 中核心的时钟配置函数 */ void system_clock_120m_hxtal(void) { uint32_t timeout = 0U; uint32_t stab_flag = 0U; /* 启用外部高速晶振 HXTAL */ RCU_CTL |= RCU_CTL_HXTALEN; /* 等待晶振稳定,超时则回退 */ do { timeout++; stab_flag = (RCU_CTL & RCU_CTL_HXTALSTB); } while ((0U == stab_flag) && (timeout < 0x8000U)); /* 切换到外部晶振,并配置 PLL 倍频系数 */ if (0U != stab_flag) { RCU_CFG = RCU_CFG_PLLSEL_HXTAL | RCU_CFG_PLLDV_6_M4; RCU_CTL |= RCU_CTL_PLLEN; /* 等待 PLL 锁定 */ } }

这段代码的逻辑是:先使能外部晶振并等待它稳定,稳定后把 PLL 的时钟源切到外部晶振,通过RCU_CFG_PLLDV_6_M4这个宏设定倍频系数,最终得到 120MHz 主频。timeout变量是超时保护——如果外部晶振没焊好或起振失败,代码不会死等,而是继续往后走,这时候系统会以内部 RC 时钟运行,外设波特率、定时器周期全部失真。判断起振是否成功的标志就是RCU_CTL_HXTALSTB这一位是否被置起。

很多移植过来的开发者习惯直接抄 STM32 的时钟配置流程,但 GD32 的 RCU 寄存器位定义和 ST 的 RCC 并不一一对应,比如 PLL 倍频系数的编码方式就不同。我一般建议直接用例程里的system_clock_120m_hxtal(),别自己按 ST 的逻辑改。需要更低主频省电时,改RCU_CFG_PLLDV这个宏就行,但要同步确认不同倍频系数下 Flash 等待周期是否匹配,否则跑起来会有随机死机的玄学问题。

3.2 GPIO 输出:最简点灯代码背后是两组配置函数

官方例程里点灯是最经典的第一课,F303 的 GPIO 初始化和 STM32 标准库有点神似但名字全变,第一次看容易懵:

/* main.c 中 GPIO 初始化 */ void gpio_config(void) { /* 使能 GPIOA 端口的时钟 */ rcu_periph_clock_enable(RCU_GPIOA); /* 设置 PA7 为推挽输出模式,50MHz */ gpio_mode_set(GPIOA, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_PIN_7); gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_7); /* 初始状态熄灭 */ gpio_bit_reset(GPIOA, GPIO_PIN_7); }
/* 主循环里翻转 LED */ while (1) { gpio_bit_set(GPIOA, GPIO_PIN_7); /* 拉高,LED 灭(板上电路而定)*/ delay_1ms(500); gpio_bit_reset(GPIOA, GPIO_PIN_7); /* 拉低,LED 亮 */ delay_1ms(500); }

关键点在于gpio_mode_set设置了端口的模式为输出(GPIO_MODE_OUTPUT),而gpio_output_options_set决定了输出的电气特性——推挽(GPIO_OTYPE_PP)还是开漏、翻转速率 50MHz。这两个函数分开设计是 GD32 库的明显特征,好处是配置更精细,坏处是少调一个函数就会出怪问题。

常见误区:在 STM32 标准库里一个GPIO_InitStructure结构体就全搞定了,但这里如果你只调用了gpio_mode_set而忘了gpio_output_options_set,引脚会保持默认的开漏状态,推 LED 能力不足,表现为"灯能亮但亮度很低"或者干脆点不亮。这种问题不是硬件故障,纯粹是库用少了。我排查这类问题时的习惯是先用万用表量引脚电平,如果电平正常(0/3.3V 都能切)但 LED 亮度异常,就回头看输出模式配置。

3.3 USART 发送:printf 重定向里藏着 retarget 文件

项目里几乎所有调试信息都要走串口打印。官方例程的 USART 工程里,除了初始化串口外,还多了一个retarget.c文件——它把 C 库的printf输出重定向到底层串口发送函数:

/* retarget.c 核心实现 */ int fputc(int ch, FILE *f) { /* 等待发送缓冲区空闲 */ while (RESET == usart_flag_get(USART0, USART_FLAG_TBE)); /* 写入数据寄存器 */ usart_data_transmit(USART0, (uint8_t)ch); return ch; }
/* USART0 初始化片段 */ void usart_config(void) { rcu_periph_clock_enable(RCU_USART0); usart_deinit(USART0); usart_baudrate_set(USART0, 115200U); usart_word_length_set(USART0, USART_WL_8BIT); usart_transmit_config(USART0, USART_TRANSMIT_ENABLE); }

fputc函数里的usart_flag_get(USART0, USART_FLAG_TBE)是在等发送数据寄存器为空,TBE位被置位后表示可以写下一个字节。如果不加这个等待,连续调用printf时数据会发生覆盖,串口助手收到的就是乱码。另一个容易被忽略的是串口引脚的复用映射——F303 的 USART0 可以映射到多组引脚,默认例程用哪一组,你实际接线就得接哪一组,否则数据永远发不出来。

踩过一个很现实的坑:printf在 Keil MDK 的微库(MicroLIB)模式下才能正常重定向,如果建工程时没有勾选 MicroLIB,代码编译能过、串口毫无输出。而如果你用的是 GCC 工具链,要处理的是_write而不是fputc。这也是为什么我强烈建议先用官方工程跑通串口,之后再自己建工程——官方工程里这些编译器相关的坑都被填好了,新工程一旦报"无法解析的外部符号"就知道是哪的问题了。

3.4 ADC 多通道采集:软件触发与 DMA 搬运一条龙

F303 的 ADC 是本人最喜欢的部分。官方例程里有单通道采集和多通道扫描两种,多通道例程展示了完整的"配置 → 触发 → 读取"流程。核心代码如下:

void adc_config(void) { /* 使能 GPIOC 和 ADC1 时钟 */ rcu_periph_clock_enable(RCU_GPIOC); rcu_periph_clock_enable(RCU_ADC1); /* 配置 ADC 输入引脚为模拟模式 */ gpio_mode_set(GPIOC, GPIO_MODE_ANALOG, GPIO_PUPD_NONE, GPIO_PIN_0); gpio_mode_set(GPIOC, GPIO_MODE_ANALOG, GPIO_PUPD_NONE, GPIO_PIN_1); /* ADC 时钟分频,保证 ADC 时钟不超过 14MHz */ adc_clock_config(ADC_ADCCK_PCLK2_DIV6); /* 设置规则通道组长度,这里采样 2 个通道 */ adc_channel_length_config(ADC1, ADC_REGULAR_CHANNEL, 2); /* 配置通道 10(PC0)和通道 11(PC1),采样顺序和采样时间 */ adc_regular_channel_config(ADC1, 0, ADC_CHANNEL_10, ADC_SAMPLETIME_55POINT5); adc_regular_channel_config(ADC1, 1, ADC_CHANNEL_11, ADC_SAMPLETIME_55POINT5); /* 使能 ADC1 并开始软件触发 */ adc_enable(ADC1); adc_software_trigger_enable(ADC1, ADC_REGULAR_CHANNEL); }

配置思路很清晰:先开时钟,再把引脚设成模拟模式,接着配 ADC 时钟分频系数ADC_ADCCK_PCLK2_DIV6,这个参数直接决定 ADC 的采样频率上限。adc_channel_length_config告诉 ADC 一次转换要扫几个通道,adc_regular_channel_config逐个指定这些通道的采样时间和顺序。最后adc_software_trigger_enable启动软件触发——触发之后,需要等待转换完成标志,再从数据寄存器里把结果读出来。

读取结果时有个容易错的细节:连续模式下第二次触发前,必须先清掉上一次的 EOC(转换结束)标志,否则读到的永远是第一组旧数据。项目里如果你要实时监控两路电压,我一般会开 DMA,让 ADC 转换完直接搬内存,避免 CPU 频繁进出中断。

/* 多通道采集并读取,非 DMA 方式 */ void read_adc_all_channel(void) { uint16_t value[2] = {0}; /* 等待规则通道转换完成 */ while (RESET == adc_flag_get(ADC1, ADC_FLAG_EOC)); value[0] = adc_regular_data_read(ADC1); /* 再触发一次采第二通道 */ adc_software_trigger_enable(ADC1, ADC_REGULAR_CHANNEL); while (RESET == adc_flag_get(ADC1, ADC_FLAG_EOC)); value[1] = adc_regular_data_read(ADC1); }

这段代码展示了最朴素的读取方式:触发一次采一个值。它的缺点很明显,两次转换之间有 CPU 空等。但在例程验证阶段,这种直白的方式其实比 DMA 更好排查问题——如果读出来的数值恒为 0,先量引脚电压,再看引脚是否配置成模拟模式;如果数值跳变很大,优先检查采样时间是不是太短或 GND 参考有没有虚接。把非 DMA 版本跑通后再换 DMA,避免两个变量同时引入时很难定位是逻辑还是内存的问题。

4. 环境搭建与运行:把官方例程烧进 CCT6 的完整过程

4.1 硬件准备:除了核心板还需要什么

手里是 GD32F303CCT6 最小系统板的话,通常板载一个 LED 和串口芯片,引出所有 GPIO。接线前先查清楚板子上的 LED 接在哪个引脚——官方例程默认可能是 PA7,但某些第三方板子会改到 PB0 或者 PC13,直接烧录官方代码会发现灯不亮。我习惯先看原理图,再对照例程源码,两边不一致就改源码里的引脚宏,而不是去焊板子。

烧录器方面,CCT6 支持 SWD 和 JTAG 两种接口,SWD 只需要 SWDIO、SWCLK、GND 三根线,速度足够日常调试。GD32 官方 IDE 和 Keil 都能识别 ST-Link、J-Link、DAP-Link 常见的 CMSIS-DAP 调试器。最省事的是借用板载调试器——很多开发板把 DAP-Link 直接做在板子上,一根 USB 线供电兼下载,完全不用额外接烧录器,这对新手的门槛是最低的。

还有一个看是小事、实际很要命的点:供电电压。GD32F303 是 3.3V 供电,如果用 5V 供电直接烧芯片。大多数开发板板载了 3.3V 稳压芯片,你只需要确认跳线帽有没有正确选择供电来源。我曾经在一块板子上看到 5V 和 3.3V 两个丝印,默认跳线在 5V 位置,没注意就直接上电,跑程序一切正常,直到某次手碰到芯片发现烫得不行才意识到供电错了。从那以后,每次拿到新板子第一件事就是量 VDD 引脚对地电压。

4.2 编译烧录三步走:以 Keil 为例

第一步是在 Keil 的 Pack Installer 里装好 GD32F30x 对应的 Device Family Pack,装不上就手动下载离线包安装。第二步是打开例程工程,在 Options for Target 里确认 Device 已自动识别为 GD32F303CC,同时检查 Debug 选项卡里选择正确的烧录器。第三步是编译下载,烧录地址保持默认的 0x08000000 不动。流程看起来简单,但每一步都有对应的坑:

打开工程后第一次编译往往不会通过,最经典的错误是缺芯片头文件gd32f30x.h,原因是工程的头文件路径是相对路径,工程文件被移动位置后就找不到了。解决方法是把整个Project目录和Firmware保持原始相对位置,或者自己到 C/C++ 选项卡里把 Include Paths 重新指定一遍。

烧录时如果报 "RDDI-DAP Error",先检查调试器与板子的接线,特别是 SWDIO/SWCLK 有没有接反,然后确认板子是否已经上电。但我遇到过最无语的情况是接反了 SWDIO 和 SWCLK 后,还以为是线的问题换了好几根线,最后才发现是杜邦线端口的丝印看不清。给新手的建议是:先量通断,再查逻辑,最后怀疑调试器。

跑通之后验证的方式很简单:串口助手打开对应串口,波特率 115200,如果例程里是printf打印,你会看到一串串打印信息。看不到输出的情况只有两种:例程里根本没调 printf,或者串口接线不对。这里也建议先跑串口例程而不是点灯例程,因为串口打印能直接看出代码在哪个环节卡住了,LED 只能告诉你"程序活着"。

4.3 常见问题排查:六条真实踩坑记录

现象一:点灯例程下载后灯不亮,程序疑似没跑。

原因分析:最常见的是启动文件选错。GD32F303 是 M4 内核,但工程里如果误用了 M3 的启动文件(有些模版是从其他芯片拷贝过来的),程序也能编译下载,但中断向量表错位,复位后 PC 跳飞。这种故障没有任何报错,LED 当然不亮。

解决办法:打开启动文件,确认开头是类似于startup_gd32f30x_cl.s的文件名,特征是带有 M4 相关指令。如果工程里混入了 STM32F103 的启动文件,果断替换成 GD32 对应的。

现象二:串口输出乱码,且波特率调多少都是乱的。

原因分析:系统时钟没有真正跑到 120MHz。典型场景是外部晶振未焊接或虚焊,SystemInit()在等待 HXTAL 稳定时超时,系统退回内部 RC 振荡器运行。此时外设时钟全部按内部时钟计算,实际波特率和配置值严重不符,乱码不可避免。

解决办法:短接或补焊外部晶振,或者改代码让系统以内部 RC 时钟为参考(但要把__SYSTEM_CLOCK宏改成内部时钟频率并重新计算波特率)。验证办法是加一个延时点灯程序,如果灯闪烁频率明显偏慢,说明系统时钟偏低。

现象三:程序在assert_param里死循环。

原因分析:GD32 官方库默认开启参数检查,assert_param(IS_GPIO_PIN(GPIO_PIN_ALL))这类校验函数会在参数非法时进入assert_failed死循环。这是库保护机制在起作用,通常意味着你传了一个该外设根本不支持的参数——比如 GPIO 引脚号大于 15,或者 ADC 通道号超出范围。

解决办法:临时调试可把assert_param关掉(gd32f30x.h里有宏定义),但如果关掉后现象更诡异,说明参数确实传错了,回头逐项核对引脚和外设编号。这个宏建议发布版本里关闭,能省一点 Flash 和判断开销。

现象四:ADC 读到的值永远是 4095 或 0。

原因分析:前者通常是输入引脚悬空或接到了高电平,ADC 转换就在满量程;后者是引脚没有配置为模拟模式,内部仍然以推挽输出模式工作。还有个隐蔽点是参考电压引脚未接好。

解决办法:先用量表量引脚电压是否在 0~3.3V 之间,正常则检查gpio_mode_set(GPIOx, GPIO_MODE_ANALOG, ...)是否被正确执行。排除这两项再看采样时间和通道选择。

现象五: Keil 编译报错说找不到芯片型号。

原因分析:当前 Keil 的 Device 列表里没有 GD32 条目,装了 Pack 没用。通常是 Pack 版本和 Keil 版本不兼容,或者 Pack 安装路径含有中文字符导致索引失败。

解决办法:卸载现有 Pack,重新以管理员身份运行 Pack 安装包,安装到默认路径。若还不行,检查 Keil 是否用的老版本,老版本可能需要升级或用官方 IDE。

现象六:程序下载后全速跑正常,单步调试时外设不工作。

原因分析:比如 ADC 或者串口在某些调试模式下有独立时钟门控,单步暂停时间过长导致看门狗超时复位,或者调试状态下外设中断被挂起。

解决办法:调试时关闭看门狗,或者在初始化代码里对调试外设做容忍处理。这个现象不是必现,但对电机控制这类工程影响很大,建议一开始就把调试时关狗的逻辑加上。

5. 二次开发改造:从例程走向自己项目的三个关键动作

5.1 把点灯例程改成自定义 GPIO 驱动

官方例程把引脚都硬编码在main.c里,项目里如果多块板子引脚不同,每次改源码非常痛苦。我一般把 GPIO 配置抽成模块化驱动,入口参数是引脚号和模式枚举,底层再调到官方库函数:

/* 自定义 gpio_driver.c 的部分封装 */ #include "gd32f30x.h" typedef struct { rcu_periph_enum clk; /* 外设时钟 */ uint32_t port; /* GPIOA ~ GPIOF */ uint32_t pin; /* GPIO_PIN_0 ~ GPIO_PIN_15 */ } gpio_t; void user_gpio_init(gpio_t *gpio, uint32_t mode, uint32_t speed) { rcu_periph_clock_enable(gpio->clk); gpio_mode_set(gpio->port, mode, GPIO_PUPD_NONE, gpio->pin); if (mode == GPIO_MODE_OUTPUT) { gpio_output_options_set(gpio->port, GPIO_OTYPE_PP, speed, gpio->pin); } } void user_gpio_write(gpio_t *gpio, uint8_t val) { if (val) { gpio_bit_set(gpio->port, gpio->pin); } else { gpio_bit_reset(gpio->port, gpio->pin); } }

这个封装有两个好处:一是换引脚时只需改结构体gpio_t里的port和pin,驱动函数不用动;二是多种外设(LED、继电器、蜂鸣器)共用一套初始化接口,代码量明显变少。GPIO_MODE_OUTPUT模式下的gpio_output_options_set参数里,speed要根据实际负载调整——驱动继电器这种慢速设备用GPIO_OSPEED_2MHZ就够了,没必要每次都拉到 50MHz,高频翻转容易引入电磁干扰。

5.2 用定时器中断改造出 1ms 系统心跳

实际项目里所有需要周期性执行的任务(按键消抖、LED 闪烁、传感器轮询)都依赖一个稳定的时基。官方例程点灯用的是软件延时阻塞方式,项目里绝不能这么干。改造方法是用一个通用定时器产生 1ms 中断,在中断服务函数里对系统节拍变量累加,主循环查这个节拍做非阻塞延时:

/* 定时器1初始化:1ms 中断 */ void systick_timer_init(void) { rcu_periph_clock_enable(RCU_TIMER1); /* 120MHz 主频,预分频 120,得到 1MHz 计数时钟 */ timer_prescaler_config(TIMER1, 119, TIMER_PSC_RELOAD_NOW); timer_autoreload_value_config(TIMER1, 999); /* 1MHz / 1000 = 1kHz */ /* 允许更新中断,使能定时器 */ timer_interrupt_enable(TIMER1, TIMER_INT_UP); nvic_irq_enable(TIMER1_IRQn, 0, 1); timer_enable(TIMER1); } volatile uint32_t g_tick_ms = 0; void TIMER1_IRQHandler(void) { if (SET == timer_interrupt_flag_get(TIMER1, TIMER_INT_FLAG_UP)) { timer_interrupt_flag_clear(TIMER1, TIMER_INT_FLAG_UP); g_tick_ms++; } } void delay_nonblock(uint32_t ms) { uint32_t start = g_tick_ms; while ((g_tick_ms - start) < ms); /* 无符号差值,安全处理回绕 */ }

这段代码的技术点在于:timer_prescaler_config把 120MHz 预分频 120 倍后得到 1MHz 计数频率,timer_autoreload_value_config设为 999 得到每 1000 次计数产生一次上溢中断,也就是 1ms。g_tick_ms用无符号 32 位整数做差值延时,49 天才会回绕一次,普通项目根本不用担心取模问题。

nvic_irq_enable(TIMER1_IRQn, 0, 1)的两个数字是抢占优先级和子优先级,中断里只做累加,不做耗时操作,这样主循环哪怕被其他中断打乱,系统时基也不会漂移。改造之后主循环就能写成非阻塞状态机,每次循环检查当前时间和目标时间的差值,到点才执行对应动作。这个惯性思维对项目后期加功能至关重要——如果一开始就把时基做出来,后面加任何定时逻辑都是拼接积木。

5.3 把串口打印升级成 DMA 发送

printf是阻塞发送,每个字节都在usart_flag_get里等。项目里如果打印量大,CPU 会被串口拖住。把发送改成 DMA 后,只要配置一次,之后把要发送的数据往内存缓冲区一放,通知 DMA 搬运就行,CPU 不需要等 TBE 标志。

/* USART0 发送 DMA 通道配置 */ void usart_dma_send_config(void) { rcu_periph_clock_enable(RCU_DMA0); /* 配置 DMA0 通道3 从内存到外设,方向是 USART0 数据寄存器 */ dma_parameter_struct dma_init_param; dma_deinit(DMA0, DMA_CH3); dma_init_param.direction = DMA_PERIPHERAL_TO_MEMORY; /* 这里要改对 */ dma_init(DMA0, DMA_CH3, &dma_init_param); }

CPU 在启动 DMA 后可以去做其他任务,发送完成由 DMA 中断通知,或者干脆不管它,下次发送时检查上一个 DONE 标志。DMA 在 GD32 里是以外设通道形式复用的,USART0_TX 对应固定的 DMA 通道,配错通道号会产生"数据根本没进内存"的怪现象。这类配置细节都以官方例程里的 DMA 工程为基准,我不会靠记忆去抠通道映射表,直接用例程改,改参数时保留原有的通道编号。

6. 从例程到产品:向正式项目迁移的验证路径与整改清单

例程跑通只是起点,真正把代码搬进量产项目还要过一次"例程瘦身"和"健壮性加固"。我的习惯是先把官方例程里assert_param关闭,然后用编译器的 Map 文件查看 Flash 占用——点了几个外设但还占着 100KB 以上的空间,那一定是有没用到的库文件也被链接进来了。逐个删掉Firmware下无用的外设源文件,GD32F303CCT6 的 256KB Flash 就会很宽裕,后面加功能时完全不需要担心空间不够。

冷启动稳定性是第二个验证点。把 CCT6 板子上电,看串口是不是每次都稳定打印,拔掉电源再立即上电,重复 20 次,如果有一次打印乱码,大概率是复位时序或晶振起振时间不稳定,需要加大等待超时。这一步看似琐碎,但半年后现场设备出现上电死机问题时,你一定会感激当时做了这个压力测试。等环境温度变化大的项目,还建议在 -20℃ 和 60℃ 各摸底一次,MCU 晶振的起振特性受温度影响非常明显。

最后是调试接口的管理。产品化时把 SWD 口仍然留着,哪怕出厂时用读保护锁住,也建议在固件里保留一个仅能访问内存的调试入口,方便后续返厂定位问题。但注意烧录选项里不要把整个芯片读保护设置成最高级别,否则 IC 锁死后连升级程序都进不去,只能换芯片。这种"锁死 = 变砖"的教训我做项目第二年就经历过,从那以后我每批次固件发布前都强制走一遍完整清单:关断言、跑 20 次冷启动、量一遍空闲电流、确认调试口状态。这套习惯帮我挡掉了不少来自身后端到现场的麻烦。希望这篇从解压到改造的全流程笔记帮到你,祝你在 GD32F303 上少走弯路,顺利跑起自己的第一个外设。

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

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

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

立即咨询