资讯动态

GD32F103CBT6开发环境搭建:VSCode+GCC+OpenOCD实战指南

发布时间:2026/9/19 9:23:54 来源:尧图企业网站定制
1. 为什么这个组合值得搭GD32F103CBT6 VSCode GCC1.1 从STM32F103CBT6迁移过来的第一印象GD32F103CBT6这块片子大家第一反应通常是这不就是个国产STM32F103CBT6吗引脚兼容、外设相似、价格还便宜很多人手里那批Blue Pill板子上的芯片其实就是它。但真正拿它做项目或者只是搭个调试环境的时候你会发现它和ST之间那种说一样又不一样的微妙关系往往就藏在环境配置的第一公里。这块芯片的核心规格是ARM Cortex-M3内核最高主频108MHz64KB Flash20KB SRAMLQFP48封装。对比ST的STM32F103CBT6主频从72MHz拉到了108MHzFlash和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的行为。自己搭环境就能把这些不确定性逐一排除。当然如果你只打算点几下鼠标赶紧出一版DEMOKeil可能确实更快。但请相信磨刀不误砍柴工把环境理透了后面写外设驱动和调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这类老内核的优化、以及对新库函数的支持都更好而且--specsnano.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-LinkST-Link通过SWD或JTAG连接GD32板子然后GDB ClientVSCode的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 0cmsis_dap port 0是强制使用第一个可用的CMSIS-DAP端口有时候多设备混插时OpenOCD会选错这个参数能救急。OpenOCD本身建议直接用最新版本0.12.0或更高。原因后面第4章细说。2.4 VSCode插件清单与必配项VSCode本身是个编辑器真正让它变成嵌入式IDE的是插件。我会装这几款C/Cms-vscode.cpptools提供代码补全、语法高亮、IntelliSense。CMake Toolsms-vscode.cmake-tools可以直接识别CMakeLists.txt点按钮配置、构建、调试。Cortex-Debugmarus25.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. 工程骨架搭建从官方固件库到第一个能编译的ELF3.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 ≤ 32KBstartup_gd32f10x_md.s中密度Flash ≤ 128KBstartup_gd32f10x_hd.s高密度Flash 256KB以上还有xl、cl等变体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( -mcpucortex-m3 -mthumb -Wall -ffunction-sections -fdata-sections ) add_link_options( -mcpucortex-m3 -mthumb -T ${LINKER_SCRIPT} -Wl,--gc-sections --specsnano.specs -Wl,-Mapoutput.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固件库文件不多编译速度影响很小。第三--specsnano.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.cfgopenocd -f interface/stlink.cfg -f target/gd32f1x.cfg这个文件里针对GD32的Flash控制器驱动、芯片ID、复位时序都做了适配。0.12.0以上的OpenOCD基本都内置了gd32f1x.cfg所以第2章我说一定要用新版。如果你的OpenOCD版本太老没有gd32f1x.cfg也可以在启动时手动覆盖CPUTAPIDopenocd -f interface/stlink.cfg -f target/stm32f1x.cfg -c set CPUTAPID 0x3ba004770x3ba00477是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后targetOpenOCD会按顺序加载。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不需要OpenOCDCortex-Debug原生支持J-Link。4.3 第一次烧录与调试的操作路径配置完成后把板子和ST-Link按SWD方式接好SWDIO接PA13SWCLK接PA14GND共地。很多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晶振想跑到108MHzGD32F103CBT6能到108MHz就在main开头调用int main(void) { system_clock_108M_hxtal(); // 下面再初始化外设 }如果你想用内部高速时钟IRC8MGD32还提供了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_set和gpio_bit_reset反过来。注意GD32库的函数名前缀是gpio_bit_set不是STM32的GPIO_SetBits。用习惯了ST库写GD32代码时最容易犯的错就是把ST库的函数名带进来。编译报错倒还好最怕的是宏定义恰好相同、函数行为却不完全一致的情况。5.3 UART0重定向printf配置与底层输出串口打印是嵌入式调试的必备手段。GD32F103CBT6的USART0对应STM32的USART1引脚是PA9TX和PA10RX。初始化代码#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重定向。因为我们在链接时用了--specsnano.specsprintf底层不会自动调用默认串口外设需要自己提供_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_Configvolatile 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. 踩过的坑全记录GD32VSCode最容易翻车的6个细节6.1 宏定义错配外设地址全乱我把GD32F10X_MD写错过一次当时用的是GD32F10X_HD因为觉得64KB加20KB SRAM比C8T6强应该算高密度。结果工程编译通过但USART0的寄存器读写完全不正常发送数据永远是0xFF。原因很简单固件库里的gd32f10x_usart.h、gd32f10x_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引脚重映射成了普通GPIOOpenOCD就连不上。处理办法按住复位键在OpenOCD启动的同一瞬间松开让芯片处于复位状态时连接然后用reset halt把芯片停住再烧录一个正常的固件进去。这个技巧能解决大部分连不上芯片的尴尬。6.4 printf没输出先查_nano.specs很多人在VSCode里跑通编译后printf打印不出东西第一反应是串口线接错了。其实--specsnano.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 100100kHz是非常保守的速率虽然慢但抗干扰能力强很多。调试阶段用慢速连接等程序稳定后再调高到1MHz甚至4MHz这是在低成本硬件条件下一个很实用的折中方案。最后再分享一个小技巧调试GD32F103CBT6时我喜欢在工程里放一个编译日期和Git提交号的宏printf启动时先打印出来。比如printf(build: %s %s, git: %s\r\n, __DATE__, __TIME__, GIT_COMMIT);这样每次固件烧进板子第一屏就能确认烧的到底是哪个版本排查明明改了代码怎么现象没变这种问题时会省很多时间。这套VSCode环境搭好之后前期的配置成本会在调试和版本管理上成倍赚回来。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价