GD32F103CBT6开发环境搭建:VSCode+GCC+OpenOCD实战指南
2026/9/19 9:23:52 网站建设 项目流程

1. 为什么这个组合值得搭:GD32F103CBT6 + VSCode + GCC

1.1 从STM32F103CBT6迁移过来的第一印象

GD32F103CBT6这块片子,大家第一反应通常是:这不就是个"国产STM32F103CBT6"吗?引脚兼容、外设相似、价格还便宜,很多人手里那批"Blue Pill"板子上的芯片其实就是它。但真正拿它做项目,或者只是搭个调试环境的时候,你会发现它和ST之间那种"说一样又不一样"的微妙关系,往往就藏在环境配置的第一公里。

这块芯片的核心规格是ARM Cortex-M3内核,最高主频108MHz,64KB Flash,20KB SRAM,LQFP48封装。对比ST的STM32F103CBT6,主频从72MHz拉到了108MHz,Flash和SRAM完全一致,大部分引脚和片上外设都能对上。功耗、价格、供货这三项,则是它能在大量量产项目里站住脚的原因。说句实在话,如果只是做一个中低复杂度嵌入式项目,GD32F103CBT6能覆盖的需求面非常广,从电机控制、传感器采集,到小尺寸HMI、通信网关,都能扛。

但问题来了:ST的官方工具链对GD32基本是"能识别但心里不服"的状态。你在Keil里装好ST-Link驱动,选择STM32F103CB这个型号,烧录大概率能跑,然而一旦涉及到Flash算法、芯片ID识别、时钟树的细节时,各种稀奇古怪的警告和暗坑就会出现。而IAR、STM32CubeIDE的体验也类似。所以我想表达一个核心观点:如果你决定用GD32,最好直接按GD32的方式去搭环境,而不是拿ST那套东西硬套。VSCode + GCC + OpenOCD这条路,恰恰是可控性最高、也最接近"原汁原味GD32"的方式。

1.2 Keil、IAR、VSCode三条路线怎么选

80%的人接触GD32环境,第一条路是Keil MDK。Keil胜在装完Pack就能点一下编译,调试窗口也很成熟,适合老工程师的肌肉记忆。缺点是:工程文件是uvprojx,和Git协作不友好,代码编辑体验停留在十年前,而且aarch32编译器版本和License问题也经常让人头疼。IAR则更贵,界面更冷门,用的人更少。

VSCode + GCC这条路呢?首先,它不花钱。ARM GNU Toolchain、OpenOCD、VSCode本体全是开源或免费的。其次,代码阅读、搜索、重构体验吊打Keil,特别是接手一个几千文件的老固件库时,VSCode的全局符号跳转和语义高亮能省大量时间。第三,构建脚本是CMakeLists.txt,可读、可审查、可自动生成,CI/CD也能直接复用。第四,调试完全可控,OpenOCD自己管连接、管Flash擦写、管GDB Server,出了任何问题都能一层层剥开看。

我用一个表格把三条路线放在一起对比,方便你按自己的习惯选:

维度Keil MDKIAR EWARMVSCode + GCC
上手门槛低,装好Pack就能跑中等,界面和操作都偏传统中高,要自己理解工具链关系
费用商业License,破解问题绕不开较贵全免费
编译速度一般较好取决于机器,Ninja通常很快
Flash烧录内置算法,省心内置算法,省心要理解OpenOCD的target配置
代码检索/编辑最强
Git/CI友好度
出问题可排查性黑盒多黑盒多全链路透明

我的建议是:如果你的项目已经用Keil维护多年、同事都是Keil党,没必要为了"炫技"换工具。但如果你是个人开发者、学生、或者团队打算认真搞一套现代化开发流程,那VSCode + GCC值得投入一两天时间配置。这套文章后面所有内容,都是按这条路线的实际操作去写的。

1.3 这套环境真正适合谁

先说结论,这套环境最适合三类人:

第一,从零开始学Cortex-M3但不想被付费IDE绑架的人。学生或者刚转嵌入式的新人,用这套环境能看清编译、链接、烧录、调试的每一个环节,而不是只会在IDE里点三角形按钮。

第二,做量产产品且需要严格版本控制的工程师。固件源码、链接脚本、启动文件、烧录配置全都可以文本化存进Git,换电脑、换人、出问题回滚都要方便得多。

第三,被ST生态折磨过、想彻底按GD32真实身份开发的人。比如你发现GD32在108MHz下运行不正常,或者OpenOCD在擦除Flash时频繁报错,那大概率是因为工具链还在按STM32F103的模式在猜GD32的行为。自己搭环境,就能把这些不确定性逐一排除。

当然,如果你只打算点几下鼠标赶紧出一版DEMO,Keil可能确实更快。但请相信,磨刀不误砍柴工,把环境理透了,后面写外设驱动和调Bug的效率会成倍提升。

2. 环境准备从零开始:编译器、构建工具、调试器、插件

2.1 安装ARM GNU Toolchain并验证版本

不管你是Windows、Linux还是macOS,第一步都是装ARM交叉编译器。我建议直接去Arm官网下载arm-gnu-toolchain(现在叫Arm GNU Toolchain,以前的gcc-arm-none-eabi是旧名字),注意选择arm-none-eabi目标,而不是Linux主机本身用的GCC。

Windows用户建议下载.exe安装包,安装时有一个步骤是"Add path to environment variable",一定要勾上,否则后续VSCode和CMake都找不到编译器。Linux用户更简单,解压 tar.xz 到/opt下,然后把bin目录加进PATH即可。例如:

wget https://developer.arm.com/-/media/Files/downloads/gnu/13.2.rel1/binrel/arm-gnu-toolchain-13.2.rel1-x86_64-arm-none-eabi.tar.xz sudo tar -xf arm-gnu-toolchain-13.2.rel1-x86_64-arm-none-eabi.tar.xz -C /opt export PATH=/opt/arm-gnu-toolchain-13.2.rel1-x86_64-arm-none-eabi/bin:$PATH

装完之后,务必打开终端验证:

arm-none-eabi-gcc --version

如果能看到类似arm-none-eabi-gcc (Arm GNU Toolchain 13.2.Rel1)的输出,就说明编译器可用。这一步别跳过,后面CMake如果提示找不到编译器,回来先检查这里。

这里额外提一个版本选择的细节:我不是建议一定追最新版,但至少要用12.x以上。新版GCC对Cortex-M3这类老内核的优化、以及对新库函数的支持都更好,而且--specs=nano.specs这类half-host配置行为更稳定。

2.2 副作用最小的构建组合:CMake + Ninja

编译器有了,还需要一套构建系统。GD32官方固件库本身是不带CMake支持的,官方例程默认都是Keil/IAR工程,所以我们得自己写CMakeLists.txt,让工程结构清晰且可扩展。

为什么选CMake而不是直接写Makefile?因为CMake能自动处理源码文件集合、头文件路径、宏定义,还能结合Ninja实现增量编译,效率比Makefile高很多。Ninja的定位是"极速构建工具",它比GNU Make更快,尤其在大型工程里优势明显。

Windows安装Ninja,官网GitHub Releases下载zip解压后,把含ninja.exe的目录加入PATH即可。Linux直接用包管理器:

sudo apt install ninja-build cmake

验证:

cmake --version ninja --version

然后是最容易被忽略的一步:CMake需要一个"工具链文件"或者直接在CMakeLists里指明编译器前缀。我的习惯是在CMakeLists里直接设置,这样不依赖本机环境变量,换机器也能跑:

set(CMAKE_SYSTEM_NAME Generic) set(CMAKE_SYSTEM_PROCESSOR cortex-m3) set(CMAKE_C_COMPILER arm-none-eabi-gcc)

这里CMAKE_SYSTEM_NAME Generic告诉CMake这是一个裸机工程,不要去做主机系统检测,否则它可能会拿本机的GCC去做编译测试,结果全乱套。

2.3 调试器和OpenOCD驱动准备

调试器我推荐ST-Link V2,因为价格便宜、兼容性好、OpenOCD对它的支持也最成熟。完整的调试链路是:OpenOCD通过USB连接ST-Link,ST-Link通过SWD(或JTAG)连接GD32板子,然后GDB Client(VSCode的Cortex-Debug插件)连接OpenOCD提供的GDB Server端口。

Windows下装驱动是第一个坑。新版OpenOCD对ST-Link的访问,底层走的是libusb/WinUSB驱动,而ST官方驱动是它自己那套。如果你在命令行运行OpenOCD时报Error: libusb_claim_interface() failed,基本就是驱动冲突。

解决方法是下载Zadig工具,把ST-Link的驱动切换为WinUSB。具体操作:ST-Link插上电脑,打开Zadig,菜单选择Options -> List All Devices,在列表里找到ST-LINK相关设备,把驱动换成WinUSB,点击Replace Driver。换完之后ST-Link在设备管理器里会显示为WinUSB设备。

CMSIS-DAP调试器用户也基本是同样思路。OpenOCD支持interface/cmsis-dap.cfg,如果发现连接不上,检查CMSIS-DAP的HID接口是否被系统其他程序占用,或者在OpenOCD命令里加一条:

openocd -f interface/cmsis-dap.cfg -f target/gd32f1x.cfg -c "cmsis_dap port 0"

cmsis_dap port 0是强制使用第一个可用的CMSIS-DAP端口,有时候多设备混插时OpenOCD会选错,这个参数能救急。

OpenOCD本身建议直接用最新版本,0.12.0或更高。原因后面第4章细说。

2.4 VSCode插件清单与必配项

VSCode本身是个编辑器,真正让它变成嵌入式IDE的是插件。我会装这几款:

  • C/C++(ms-vscode.cpptools):提供代码补全、语法高亮、IntelliSense。
  • CMake Tools(ms-vscode.cmake-tools):可以直接识别CMakeLists.txt,点按钮配置、构建、调试。
  • Cortex-Debug(marus25.cortex-debug):这是VSCode调试ARM芯片的核心插件,支持OpenOCD、J-Link、pyOCD等多种servertype,比C/C++插件自带的launch调试要强大得多。
  • Serial Monitor(或者叫串口监视器插件):用来在VSCode里直接看串口输出,省得再开一个串口助手。
  • Cortex-Debug: Device Support Pack:如果打开SVD文件报错,可能需要这个支持包,但一般新版Cortex-Debug已经内置。

装完之后,在VSCode的settings.json里至少配置这几项:

{ "cmake.configureOnOpen": true, "cmake.buildDirectory": "${workspaceFolder}/build", "cortex-debug.armToolchainPath": "你的arm-none-eabi-gcc所在目录", "editor.formatOnSave": false }

cortex-debug.armToolchainPath一定要指向包含arm-none-eabi-gdb的目录。有些版本Cortex-Debug找不到GDB会直接报错,这个变量是保底方案。

3. 工程骨架搭建:从官方固件库到第一个能编译的ELF

3.1 官方固件库目录里该拿什么文件

搭建工程的第一步,是获取GD32F10x的官方固件库。我建议去GigaDevice官网下载GD32F10x_Firmware_Library_V3.x.x,国内很多镜像站也有,注意别下载成GD32F30x或GD32F4xx的库。

解压后的目录里,我们实际用到的内容有这些:

Firmware/ ├── CMSIS/ │ ├── ARM/ │ │ └── startup_gd32f10x_md.s │ ├── GD/ │ │ └── GD32F10x/ │ │ ├── Include/ │ │ │ ├── gd32f10x.h │ │ │ ├── system_gd32f10x.h │ │ │ └── ... │ │ └── Source/ │ │ └── system_gd32f10x.c │ └── Include/ │ └── core_cm3.h └── GD32F10x_standard_peripheral/ ├── Include/ │ ├── gd32f10x_gpio.h │ ├── gd32f10x_rcu.h │ ├── gd32f10x_usart.h │ ├── gd32f10x_syscfg.h │ └── ... └── Source/ ├── gd32f10x_gpio.c ├── gd32f10x_rcu.c ├── gd32f10x_usart.c └── ...

把这堆文件复制到你的工程目录下,建议保持官方目录结构,方便以后对照官方例程。我自己习惯建一个Libraries目录放固件库,User目录放main.c和自定义驱动,ld目录放链接脚本,vscode相关的配置放.vscode目录。

3.2 启动文件与宏定义必须一一对应

启动文件(startup file)是芯片上电后最先执行的汇编代码,负责初始化栈指针、调用SystemInit、再跳转到main。GD32固件库里按Flash容量大小提供了多个启动文件:

  • startup_gd32f10x_ld.s:低密度,Flash ≤ 32KB
  • startup_gd32f10x_md.s:中密度,Flash ≤ 128KB
  • startup_gd32f10x_hd.s:高密度,Flash 256KB以上
  • 还有xlcl等变体

GD32F103CBT6的Flash是64KB,所以选startup_gd32f10x_md.s。启动文件的选择,必须和编译宏定义一致。在编译时我们要给整个工程定义一个宏:GD32F10X_MD,告诉固件库"这是一颗中密度的GD32F10x芯片"。固件库里很多外设驱动依赖这个宏来决定是否启用某些寄存器配置,比如USART0的引脚重映射、EXTI的线数量等。

如果你用的是CubeMX生成STM32F103CBT6的启动文件,再直接编译GD32固件库,大概率能编过,但运行时会出现外设行为不稳定的情况。我强烈建议:既然用了GD32的库,启动文件也一定要换成GD32的md版本。

3.3 链接脚本解析:64KB Flash和20KB SRAM的地址编排

链接脚本(.ld文件)决定了代码、常量、全局变量在芯片地址空间里的排布。GD32F103CBT6的内存映射和STM32F103CBT6基本一样:Flash从0x08000000开始,SRAM从0x20000000开始。

一个可用的链接脚本长这样:

MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K } _estack = ORIGIN(RAM) + LENGTH(RAM); SECTIONS { .text : { KEEP(*(.isr_vector)) *(.text*) *(.rodata*) _etext = .; } > FLASH .data : { _sdata = .; *(.data*) _edata = .; } > RAM AT > FLASH .bss : { _sbss = .; *(.bss*) *(COMMON) _ebss = .; } > RAM }

这里_estack是栈顶地址,Cortex-M3的栈是向下增长的,所以栈顶放在RAM的最高地址0x20005000.data段有一个AT > FLASH,这是关键:全局变量的初始值必须存在Flash里,上电后由启动文件把它们从Flash搬运到RAM。.bss段则全部清零。

KEEP(*(.isr_vector))的意思是保留中断向量表,链接器垃圾回收时不能把它优化掉。启动文件里那个向量表符号,如果被--gc-sections误删,芯片上电直接跑飞。

3.4 CMakeLists.txt完整实现与逐段说明

现在把上面的思路写成完整的CMakeLists.txt。这算是我压箱底的通用模板,改改源文件列表就能用到其他GD32工程:

cmake_minimum_required(VERSION 3.20) project(GD32F103CBT6_Demo C ASM) set(CMAKE_SYSTEM_NAME Generic) set(CMAKE_SYSTEM_PROCESSOR cortex-m3) set(TOOLCHAIN_PREFIX arm-none-eabi-) set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}gcc) set(CMAKE_ASM_COMPILER ${TOOLCHAIN_PREFIX}gcc) set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) set(LINKER_SCRIPT ${CMAKE_SOURCE_DIR}/ld/gd32f10x_flash.ld) add_compile_definitions( GD32F10X_MD USE_STDPERIPH_DRIVER ) add_compile_options( -mcpu=cortex-m3 -mthumb -Wall -ffunction-sections -fdata-sections ) add_link_options( -mcpu=cortex-m3 -mthumb -T ${LINKER_SCRIPT} -Wl,--gc-sections --specs=nano.specs -Wl,-Map=output.map ) file(GLOB_RECURSE CORE_SOURCES ${CMAKE_SOURCE_DIR}/Libraries/CMSIS/*.c ${CMAKE_SOURCE_DIR}/Libraries/CMSIS/ARM/*.s ) file(GLOB_RECURSE PERIPH_SOURCES ${CMAKE_SOURCE_DIR}/Libraries/GD32F10x_standard_peripheral/Source/*.c ) add_executable(${PROJECT_NAME}.elf ${CORE_SOURCES} ${PERIPH_SOURCES} ${CMAKE_SOURCE_DIR}/User/main.c) target_include_directories(${PROJECT_NAME}.elf PRIVATE ${CMAKE_SOURCE_DIR}/Libraries/CMSIS/Include ${CMAKE_SOURCE_DIR}/Libraries/CMSIS/GD/GD32F10x/Include ${CMAKE_SOURCE_DIR}/Libraries/GD32F10x_standard_peripheral/Include ${CMAKE_SOURCE_DIR}/User ) target_link_options(main PRIVATE ...)

几个容易出错的地方单独说:

第一,CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY非常重要。CMake在配置阶段会尝试编译一个测试程序,如果它不知道这是个交叉编译环境,会拿本机GCC去编译,必然报错。设成STATIC_LIBRARY后,CMake只生成一个静态库做验证,不再尝试链接成可执行文件,避开了裸机工程没有操作系统库的问题。

第二,file(GLOB_RECURSE ...)会把所有匹配的.c文件收集进来。优点是省事,缺点是如果你在源码目录里放了一个用不到的测试文件,它也会被编译进来。我一般会接受这个风险,因为GD32固件库文件不多,编译速度影响很小。

第三,--specs=nano.specs是C库的精简模式,大幅减小Flash占用。代价是printf的浮点数支持会被削减,而且必须自己实现_write函数才能让printf往串口输出。这个问题在第5章详细处理。

第四,链接脚本路径我放在ld目录下,用add_link_options里的-T指定。注意这里的路径格式是相对当前构建目录的,如果你构建目录不是build,链接脚本路径要相应调整。

3.5 VSCode下编译:tasks.json与settings.json

现在万事俱备,就差一个能在VSCode里点按钮就编译的任务。我建议用CMake Tools插件来管理构建:直接在VSCode底部状态栏点击CMake Tools相关的构建按钮,或者在命令面板里输入CMake: Build。它默认会用Ninja生成器,构建速度快。

如果你更习惯传统方式,可以配置任务:

{ "version": "2.0.0", "tasks": [ { "label": "build gd32 demo", "type": "shell", "command": "cmake --build build", "group": { "kind": "build", "isDefault": true }, "problemMatcher": ["$gcc"] } ] }

对应的settings.json里,还可以给C/C++插件配置IntelliSense的包含路径,让代码不再是满屏红波浪线:

{ "C_Cpp.default.includePath": [ "${workspaceFolder}/Libraries/CMSIS/Include", "${workspaceFolder}/Libraries/CMSIS/GD/GD32F10x/Include", "${workspaceFolder}/Libraries/GD32F10x_standard_peripheral/Include", "${workspaceFolder}/User" ], "C_Cpp.default.defines": [ "GD32F10X_MD", "USE_STDPERIPH_DRIVER" ], "C_Cpp.default.compilerPath": "C:/Arm GNU Toolchain/13.2.Rel1/bin/arm-none-eabi-gcc.exe" }

注意这里的defines必须和CMakeLists里保持一致,否则IntelliSense解析的头文件分支可能和编译时不一致,导致看起来没报错实际编译不过,或者反过来。

4. 烧录调试闭环:OpenOCD配置与Cortex-Debug实战

4.1 target/gd32f1x.cfg和stm32f1x.cfg怎么选

烧录调试这一步,是整个环境里最容易翻车的地方。核心工具是OpenOCD,它负责把GDB的调试命令翻译成SWD协议报文,同时管理Flash擦写。

OpenOCD启动时需要指定两个配置文件:一个描述调试器(interface),一个描述目标芯片(target)。以前大家用ST-Link调试GD32,习惯上会写:

openocd -f interface/stlink.cfg -f target/stm32f1x.cfg

这套配置在STM32F103上没问题,但用在GD32F103上就埋了雷。因为stm32f1x.cfg里带有STM32F103的芯片ID扫描逻辑,而GD32的ID Code和ST并不完全一致。老版本OpenOCD可能直接报TAP IDCODE does not match,新版本能扫到但也会打出一堆警告。

更正规的做法是使用OpenOCD自带的target/gd32f1x.cfg

openocd -f interface/stlink.cfg -f target/gd32f1x.cfg

这个文件里针对GD32的Flash控制器驱动、芯片ID、复位时序都做了适配。0.12.0以上的OpenOCD基本都内置了gd32f1x.cfg,所以第2章我说一定要用新版。

如果你的OpenOCD版本太老,没有gd32f1x.cfg,也可以在启动时手动覆盖CPUTAPID:

openocd -f interface/stlink.cfg -f target/stm32f1x.cfg -c "set CPUTAPID 0x3ba00477"

0x3ba00477是Cortex-M3内核的JTAG IDCODE,很多GD32F103的芯片都报告这个值。这个办法能绕过ID检查,但Flash操作依然按照STM32F103的模式去猜,存在擦除失败的风险,只建议临时应急。

4.2 launch.json配置:把调试器、固件和源码串起来

打开VSCode调试面板,点击创建launch.json,选择Cortex-Debug。一个可以直接用的配置如下:

{ "version": "0.2.0", "configurations": [ { "name": "GD32F103CBT6 OpenOCD Debug", "type": "cortex-debug", "request": "launch", "servertype": "openocd", "executable": "${workspaceFolder}/build/GD32F103CBT6_Demo.elf", "device": "GD32F103CB", "configFiles": [ "interface/stlink.cfg", "target/gd32f1x.cfg" ], "runToEntryPoint": "main", "svdFile": "${workspaceFolder}/libraries/GD32F10x.svd" } ] }

这里逐项解释:

  • executable指向编译出来的ELF文件。ELF格式同时包含机器码、符号表和源码行号信息,没有它GDB断点都不知道该怎么对应源码行。
  • configFiles里的顺序不能反,先interface后target,OpenOCD会按顺序加载。
  • runToEntryPoint设为main,断点会自动挂在main函数入口,省去每次手动从复位向量单步走。
  • svdFile是系统视图描述文件,能让调试窗口里看到外设寄存器表和每个位的含义。GD32F10x的SVD文件可以从GD32的Keil DFP包里解压出来,网上也有社区维护版本。没有它调试一样能用,但有了它看寄存器方便N倍。

如果你用的是CMSIS-DAP调试器,只要把configFiles改成:

"configFiles": [ "interface/cmsis-dap.cfg", "target/gd32f1x.cfg" ]

J-Link用户则可以直接用servertype": "jlink",不需要OpenOCD,Cortex-Debug原生支持J-Link。

4.3 第一次烧录与调试的操作路径

配置完成后,把板子和ST-Link按SWD方式接好:SWDIO接PA13,SWCLK接PA14,GND共地。很多GD32板子上的排针间距是2.54mm,标准杜邦线就能接。

然后按F5启动调试。正常情况下,VSCode下方会弹出OpenOCD的日志窗口,依次出现这些信息:

Info : STLINK V2J29S7 (API v2) VID:PID 0483:3748 Info : Target voltage: 3.30 V Info : clock speed 1000 kHz Info : starting gdb server for gd32f1x on port 3333 Info : Listening on port 3333 for gdb connections Info : JTAG/SWD tap: gd32f1x.cpu has IDCODE 0x3ba00477

看到IDCODE 0x3ba00477这一行,说明芯片已经被正确识别。之后OpenOCD会擦除Flash、写入固件,然后GDB连接上去,停在main函数里。

第一次调试浮空运行时,有几个排查技巧很实用。如果OpenOCD日志卡在TAP does not have valid IDCODE,多半是SWD接线接触不良,先检查GND线。如果提示cannot read chip id,则先确认是不是拿错了target配置文件,再确认ST-Link固件版本太旧需要升级。

调试过程中,可以自己在左边栏设置断点,在Watch里输入GPIO_OPD这类寄存器变量,配合SVD文件,能直接看到引脚电平状态,比用LED灯判断方便太多。

5. 跑起来才是硬道理:LED、串口与SysTick的完整工程

5.1 系统时钟初始化顺序,GD32和STM32第一个隐藏差异

很多从STM32迁移过来的朋友,写完GPIO初始化后发现程序不跑,或者跑了但时钟极慢,第一反应往往是"芯片坏了"。其实问题几乎都出在系统时钟初始化上。

STM32F103的SystemInit函数默认会把系统时钟配置到72MHz,只要外部晶振存在就能跑。但GD32F10x固件库里的SystemInit只做了向量表配置和预取缓冲使能,并没有帮我把PLL调到108MHz或者72MHz。这是官方固件库的刻意设计,把时钟初始化交给用户在main函数里显式调用。

GD32F10x固件库里提供了现成的时钟配置函数,通常在system_gd32f10x.c中。比如:

system_clock_72M_hxtal(); // 外部高速晶振,主频72MHz system_clock_108M_hxtal(); // 外部高速晶振,主频108MHz

如果你的板子上有8MHz晶振,想跑到108MHz(GD32F103CBT6能到108MHz),就在main开头调用:

int main(void) { system_clock_108M_hxtal(); // 下面再初始化外设 }

如果你想用内部高速时钟IRC8M,GD32还提供了system_clock_8M_irc8m()这类函数。但要提醒一句:内部RC振荡器精度不如外部晶振,做串口波特率要求高的场合就别用了。

我在实际项目里强烈建议:无论用什么芯片,main函数里通过一个明确的函数把系统时钟设好,再往下干活。这样时钟树清晰、可控,出问题定位也快。

5.2 GPIO输出:点亮一颗LED的代码

下面这份代码用了GD32标准外设库,风格和STM32 SPL很像,但函数名、宏名有一些差别:

#include "gd32f10x.h" #include "gd32f10x_gpio.h" #include "gd32f10x_rcu.h" void led_init(void) { rcu_periph_clock_enable(RCU_GPIOA); gpio_init(GPIOA, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_1); gpio_bit_set(GPIOA, GPIO_PIN_1); } int main(void) { system_clock_108M_hxtal(); led_init(); while (1) { gpio_bit_reset(GPIOA, GPIO_PIN_1); delay_1ms(500); gpio_bit_set(GPIOA, GPIO_PIN_1); delay_1ms(500); } }

这里用PA1推挽输出驱动LED,如果你板子上的LED是低电平点亮,就把gpio_bit_setgpio_bit_reset反过来。

注意GD32库的函数名前缀是gpio_bit_set,不是STM32的GPIO_SetBits。用习惯了ST库,写GD32代码时最容易犯的错就是把ST库的函数名带进来。编译报错倒还好,最怕的是宏定义恰好相同、函数行为却不完全一致的情况。

5.3 UART0重定向printf:配置与底层输出

串口打印是嵌入式调试的必备手段。GD32F103CBT6的USART0对应STM32的USART1,引脚是PA9(TX)和PA10(RX)。初始化代码:

#include "gd32f10x_usart.h" void usart0_init(void) { rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_USART0); gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_9); gpio_init(GPIOA, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_10); 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_rts_config(USART0, USART_RTS_DISABLE); usart_hardware_flow_cts_config(USART0, USART_CTS_DISABLE); usart_transmit_config(USART0, USART_TRANSMIT_ENABLE); usart_receive_config(USART0, USART_RECEIVE_ENABLE); usart_enable(USART0); }

然后是printf重定向。因为我们在链接时用了--specs=nano.specs,printf底层不会自动调用默认串口外设,需要自己提供_write函数:

int _write(int fd, char *ptr, int len) { for (int i = 0; i < len; i++) { while (RESET == usart_flag_get(USART0, USART_FLAG_TBE)) { } usart_data_transmit(USART0, (uint8_t)ptr[i]); } return len; }

有了这个函数,printf("hello gd32\r\n")就会通过USART0把字符串发送出去。VSCode里装个串口监视器插件,选对COM口、115200波特率,就能实时看到输出。

5.4 SysTick闪烁主循环

上面的代码里用了delay_1ms(500),这个函数不存在于GD32官方库中,需要自己基于SysTick实现。SysTick是Cortex-M3内核自带的24位递减定时器,非常适合做毫秒延时。

最简洁的实现是直接调用CMSIS的SysTick_Config

volatile uint32_t systick_count = 0; void SysTick_Handler(void) { systick_count++; } void delay_1ms(uint32_t ms) { uint32_t start = systick_count; while ((systick_count - start) < ms) { } } int main(void) { system_clock_108M_hxtal(); SysTick_Config(SystemCoreClock / 1000); // 初始化外设... while (1) { // 业务逻辑 } }

SystemCoreClock是CMSIS里的全局变量,存放着当前系统时钟频率。SysTick_Config(SystemCoreClock / 1000)表示让SysTick每1ms触发一次中断。如果你的system_clock_108M_hxtal()执行成功,SystemCoreClock应该是108000000,那么重装载值就是108000。

这里有个细节:systick_count最好声明成volatile,因为它在中断里被修改,在主循环里被读取。不加volatile的话,编译器可能把它优化进寄存器缓存,导致延时永远不结束,或者直接跑飞。

6. 踩过的坑全记录:GD32+VSCode最容易翻车的6个细节

6.1 宏定义错配,外设地址全乱

我把GD32F10X_MD写错过一次,当时用的是GD32F10X_HD,因为觉得"64KB加20KB SRAM比C8T6强,应该算高密度"。结果工程编译通过,但USART0的寄存器读写完全不正常,发送数据永远是0xFF。

原因很简单,固件库里的gd32f10x_usart.hgd32f10x_gpio.h会根据宏定义去切换外设基地址和寄存器偏移。用错了密度宏,地址映射就串位了。所以一定要记住:GD32F103CBT6用GD32F10X_MD,启动文件用startup_gd32f10x_md.s,二者必须对齐

6.2 HXTAL启动问题导致烧进去没反应

第一次拿到一块GD32F103CBT6板子,烧进去程序却不工作,万用表量晶振引脚,发现外部8MHz晶振一端停振、一端输出电压异常。查了半天,发现board上晶振并没有焊错,问题出在system_clock_72M_hxtal()里的HXTAL等待超时逻辑。GD32的HXTAL稳定判断和ST有差异,在晶振起振较慢或负载电容匹配不佳时,库函数判定失败,但函数不返回错误,程序继续往下跑,结果系统时钟还停在IRC8M的4MHz上,外设全都乱套。

处理办法有两个:一是检查硬件晶振负载电容,GD32建议12pF到22pF,别图省事不焊;二是如果确认晶振没问题,可以在system_gd32f10x.c里把HXTAL超时时间调大一点,或者改用内部IRC8M先跑起来,对比区分硬件问题还是软件问题。

6.3 OpenOCD识别不到目标芯片

现象是OpenOCD启动后,日志停在这一行:

Info : clock speed 1000 kHz Error: JTAG-DP STICKY ERROR

大概率原因不是配置问题,而是SWD接线太长、接触不良,或者目标板没上电。先量SWDIO和SWCLK有没有3.3V,再确认ST-Link的3.3V输出和目标板供电是否共地。

另一个常见原因是目标芯片进入了低功耗模式或者复位拉死。如果芯片程序里把SWD引脚重映射成了普通GPIO,OpenOCD就连不上。处理办法:按住复位键,在OpenOCD启动的同一瞬间松开,让芯片处于复位状态时连接,然后用reset halt把芯片停住,再烧录一个正常的固件进去。这个技巧能解决大部分"连不上芯片"的尴尬。

6.4 printf没输出,先查_nano.specs

很多人在VSCode里跑通编译后,printf打印不出东西,第一反应是串口线接错了。其实--specs=nano.specs启用的是精简版newlib,它默认不初始化标准输入输出设备,也不会自动调用我们写的_write

如果你添加了_write函数还是没有输出,检查链接时是否真的生效。一个简单的验证办法是在_write开头加上一个无限循环,如果串口TX引脚电平被拉低或卡住,说明代码确实执行进去了;如果没有,说明链接器用的是另一个_write,比如newlib自带的_write_r。这时可以补一个_write_r,让它转调_write

另外,在代码里尽量用\r\n而不是只加\n,很多串口助手会把\n原样显示成换行,效果看着就像没输出。

6.5 编辑器红波浪线不等于编译错误

C/C++插件的IntelliSense使用了自己的一套includePath和defines配置。如果你的c_cpp_properties.json里没写GD32F10X_MD,编辑器会把固件库里很多条件编译代码标红,但这些代码实际编译时是经过正确宏分支的,GCC能正常通过。

反过来,如果编译器报错,IntelliSense却显示正常,也是因为宏定义不一致。遇到这种情况,先对比c_cpp_properties.json和CMakeLists.txt里的defines,别急着改代码。我通常的做法是:以编译器输出为准,编辑器报错只做参考。代码写完,直接在CMake Tools的构建输出里看有没有Error,这才是真正的编译结果。

6.6 低价调试器的连接稳定性

市面上的ST-Link V2克隆版、十几块钱的CMSIS-DAP,性能参差不齐。遇到"稳定运行几分钟后OpenOCD掉线"的情况,不要急着怀疑芯片,先给调试器换一根短一点的杜邦线,或者直接换USB线。SWD对线材长度和接触质量很敏感,理想情况下SWDIO和SWCLK排线长度不要超过10厘米。

如果实在要用长线,把OpenOCD的SWD时钟频率降低,比如:

openocd -f interface/stlink.cfg -f target/gd32f1x.cfg -c "adapter speed 100"

100kHz是非常保守的速率,虽然慢但抗干扰能力强很多。调试阶段用慢速连接,等程序稳定后再调高到1MHz甚至4MHz,这是在低成本硬件条件下一个很实用的折中方案。


最后再分享一个小技巧:调试GD32F103CBT6时,我喜欢在工程里放一个编译日期和Git提交号的宏,printf启动时先打印出来。比如:

printf("build: %s %s, git: %s\r\n", __DATE__, __TIME__, GIT_COMMIT);

这样每次固件烧进板子,第一屏就能确认烧的到底是哪个版本,排查"明明改了代码怎么现象没变"这种问题时会省很多时间。这套VSCode环境搭好之后,前期的配置成本会在调试和版本管理上成倍赚回来。

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

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

立即咨询