多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

STM32国产替代实战:GD32与CH32V103移植避坑指南

STM32国产替代实战:GD32与CH32V103移植避坑指南 1. 从一块“不听话”的板子说起去年这个时候我接手了一个小批量的工控采集板项目。需求不复杂几路模拟量输入两路串口输出外加一个CAN总线接口跑在-20℃到70℃的工业环境里。按照以前的习惯我直接选了STM32F103C8T6这颗“国民神针”画板、打样、写代码两周搞定。但项目方在评审会上提了一句“能不能评估一下国产替代的可行性成本能降一点是一点。”就是这句话让我开始了长达一年的国产芯片折腾之路。从GD32到CH32V103从RISC-V内核到各种外设兼容性坑我前后换了四家国产厂商的芯片打了七版PCB烧了不知道多少片。这一年里我既体验过“引脚全兼容、代码直接跑”的爽快也经历过“调试器连不上、串口乱码、CAN突然掉线”的深夜崩溃。这篇文章不吹不黑纯粹从一个一线嵌入式开发者的角度聊聊国产芯片到底能不能用、怎么用、哪里会踩坑。如果你正在做STM32的国产替代评估或者单纯想了解一下GD32、CH32V103这些芯片的真实表现这篇内容应该能帮你省下不少打样费和熬夜时间。我会把这一年里积累的选型逻辑、移植步骤、调试技巧和避坑经验全部摊开来讲尽量做到“你看完就能上手试”。2. 国产芯片选型的核心逻辑与方案对比2.1 为什么不是简单“换一颗芯片”的事很多人以为国产替代就是“把STM32的型号换成GD32代码重新编译一下就行”。我一开始也这么想结果第一版板子就翻车了。芯片替换涉及的不只是内核和外设还包括供电特性、时钟树结构、Flash等待周期、中断响应延迟、甚至PCB布局对电源纹波的要求。这些东西在数据手册里往往写得比较含蓄只有实际跑起来才能发现差异。举个例子STM32F103的Flash在72MHz主频下需要插入2个等待周期而GD32F103因为Flash工艺不同在108MHz下只需要1个等待周期。这意味着如果你直接把STM32的代码搬到GD32上不改时钟配置芯片虽然能跑但性能没有完全释放反过来如果你把GD32的代码搬到STM32上超频到108MHzSTM32大概率会跑飞。这种细节只有真正对比过两家手册、实际测过波形的人才会注意到。所以选型的第一步不是看参数表而是明确你的项目对哪些指标最敏感。是成本是供货周期是外设兼容性还是长期稳定性不同的优先级对应完全不同的选型策略。2.2 主流国产MCU的定位与适用场景这一年我重点用了三款芯片GD32F103、CH32V103和一款基于RISC-V内核的国产MCU。它们各自的定位差异很大我整理了一个对比表格方便你快速判断哪款更适合你的项目。对比维度GD32F103CH32V103RISC-V国产MCU内核架构ARM Cortex-M3RISC-V青稞V3ARISC-V主频108MHz80MHz144MHz引脚兼容性与STM32F103高度兼容部分兼容需重新设计开发工具Keil、IAR、GCCMounRiver Studio厂商定制IDE外设库与STM32标准库类似自研库自研库供货稳定性较好好一般适合场景快速替代STM32低成本新项目高性能新项目GD32的最大优势是“迁移成本低”。它的引脚定义、外设寄存器布局和STM32F103非常接近很多情况下甚至可以直接烧录STM32的二进制文件虽然不推荐这么做。如果你的项目已经基于STM32标准库开发完成时间紧、任务重GD32是最稳妥的选择。CH32V103走的是另一条路。它用的是RISC-V内核开发工具链和ARM完全不同但外设设计上参考了STM32的风格串口、SPI、I2C的用法逻辑很相似。它的优势在于成本极低而且供货一直很稳。适合新项目从零开始设计或者对成本极度敏感的量产产品。那款RISC-V国产MCU主频最高性能最强但生态最不成熟。它的开发环境需要单独配置调试器兼容性也一般适合有较强技术储备的团队去啃。如果你是一个人做项目或者团队里没有专门搞底层移植的人我建议先从GD32入手。2.3 选型时必须确认的五个硬指标不管选哪家下面这五个指标一定要在打样前确认清楚否则后面全是坑。第一供电电压范围。STM32F103是2.0V到3.6V但有些国产芯片最低只能到2.7V。如果你的板子上有3.3V的LDO问题不大但如果是电池供电电压会随着电量下降而降低这时候低压下的稳定性就至关重要。第二Flash和RAM的实际可用容量。有些芯片标称64KB Flash但实际可用的连续空间可能只有48KB剩下的被Bootloader或系统区占用了。这个信息在数据手册里往往藏得很深最好直接问FAE要一份内存映射图。第三中断向量表的偏移支持。如果你要做IAP升级或者Bootloader必须确认芯片是否支持重定位中断向量表。GD32和STM32一样可以通过SCB-VTOR寄存器来设置偏移但有些RISC-V芯片的中断控制器设计不同需要改link.ld文件里的地址映射。第四调试接口的兼容性。STM32用SWD接口GD32也支持SWD但CH32V103用的是两线调试接口虽然引脚兼容但协议不同J-Link不一定能直接用。买芯片的时候最好连官方调试器一起买省得后面折腾。第五CAN、USB这些复杂外设的IP来源。有些国产芯片的CAN控制器是自研的寄存器和STM32的bxCAN完全不同移植的时候需要重写驱动。如果你的项目重度依赖CAN通信这一点必须提前确认。3. 从STM32到GD32的移植实操与关键细节3.1 工程迁移的第一步时钟树重新配置我拿一个实际的STM32F103工程来演示移植过程。这个工程用了标准库功能包括串口收发、定时器中断和ADC采样。目标芯片是GD32F103C8T6引脚完全兼容PCB不用改。第一步是替换启动文件和链接脚本。GD32的启动文件startup_gd32f10x_md.s和STM32的startup_stm32f10x_md.s结构几乎一样但中断向量表的名称有细微差别。比如STM32的TIM2_IRQHandler在GD32里叫TIMER2_IRQHandler少了一个字母。如果你直接替换启动文件而不改中断函数名编译能过但中断永远进不去。第二步是调整系统时钟。STM32F103的标准库默认用8MHz外部晶振经过PLL倍频到72MHz。GD32F103支持更高的主频但它的PLL配置寄存器和STM32略有不同。下面是我实际使用的时钟初始化代码void SystemInit(void) { /* 使能外部高速晶振 */ RCC-CR | RCC_CR_HSEON; while(!(RCC-CR RCC_CR_HSERDY)); /* 配置PLLHSE 8MHz倍频到108MHz */ RCC-CFGR ~RCC_CFGR_PLLMULL; RCC-CFGR | RCC_CFGR_PLLMULL9; /* 8MHz * 9 72MHz */ RCC-CFGR | RCC_CFGR_PLLSRC; /* 使能PLL */ RCC-CR | RCC_CR_PLLON; while(!(RCC-CR RCC_CR_PLLRDY)); /* 配置Flash等待周期108MHz需要2个等待周期 */ FLASH-ACR ~FLASH_ACR_LATENCY; FLASH-ACR | FLASH_ACR_LATENCY_2; /* 切换系统时钟到PLL */ RCC-CFGR ~RCC_CFGR_SW; RCC-CFGR | RCC_CFGR_SW_PLL; while((RCC-CFGR RCC_CFGR_SWS) ! RCC_CFGR_SWS_PLL); }注意这里有个细节GD32的Flash等待周期设置和STM32不同。STM32F103在72MHz下需要2个等待周期而GD32在108MHz下也只需要2个。如果你把STM32的代码直接搬过来Flash等待周期设成了2但主频还是72MHz那没问题但如果你想跑到108MHz就必须确认等待周期设置正确否则取指会出错。3.2 外设驱动的差异点与修改方法时钟配好之后接下来是外设。串口、SPI、I2C这些基本外设的寄存器布局和STM32几乎一样标准库函数可以直接调用。但有几个地方需要特别注意。GPIO的驱动能力。GD32的GPIO输出驱动电流比STM32略小典型值在8mA左右而STM32可以到20mA。如果你用GPIO直接驱动LED问题不大但如果是驱动光耦或者MOS管可能需要加一级三极管缓冲。我在第一版板子上用PA8直接驱动一个光耦结果发现光耦导通不完全波形上升沿变缓。后来加了一个S8050三极管才解决。ADC的采样时间。GD32的ADC采样保持时间和STM32不同。STM32F103的ADC时钟是14MHz采样时间可配置为1.5到239.5个周期。GD32的ADC时钟最高可以到28MHz但采样时间配置寄存器的位定义有差异。如果你直接套用STM32的ADC配置代码采样值可能会偏小或者跳动。我的做法是先用一个已知电压比如3.3V的基准去校准调整采样时间直到读数稳定。CAN控制器的寄存器偏移。这是坑最深的地方。GD32的CAN控制器虽然功能上和STM32的bxCAN兼容但寄存器的基地址和位定义有细微差别。比如STM32的CAN_MCR寄存器地址偏移是0x00GD32也是0x00但里面的位定义不同。STM32的CAN_MCR_INRQ是第0位GD32也是第0位但CAN_MCR_TXFP的位置不一样。如果你直接用STM32的CAN驱动库初始化能过但发送和接收会异常。我的建议是CAN、USB、以太网这些复杂外设不要试图直接复用STM32的驱动老老实实用GD32官方提供的固件库重新写一遍。虽然麻烦一点但能避免很多玄学问题。3.3 调试器配置与下载注意事项GD32支持SWD调试J-Link和ST-Link都能用。但有一个细节GD32的SWD接口在芯片复位后默认是使能的但如果你在代码里禁用了JTAG比如为了释放PB3、PB4、PA15作为普通GPIO需要确保SWD功能没有被一起关掉。STM32的标准库里有这样一段代码/* 禁用JTAG保留SWD */ GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);这段代码在GD32上也能用但GD32的AFIO寄存器位定义和STM32略有不同。如果你发现下载一次之后再也连不上调试器了大概率是这段代码把SWD也关了。解决办法是在代码开头加一个延时或者用复位引脚强制复位然后快速下载。另外GD32的Flash编程算法和STM32不同。如果你用Keil需要安装GD32的Device Family Pack否则下载会报错。用J-Link的话需要在J-Link Commander里手动指定Flash算法或者更新J-Link驱动到最新版本。4. CH32V103与RISC-V内核的实战体验4.1 RISC-V开发环境的搭建过程CH32V103用的是RISC-V内核开发环境和ARM完全不同。它不能用Keil或者IAR需要用沁恒官方的MounRiver Studio。这个IDE基于Eclipse集成了GCC工具链和调试器驱动安装包大概500MB装完之后基本可以开箱即用。我第一次用的时候最大的不适应是中断处理。ARM的中断向量表是固定的每个中断源对应一个固定的入口地址。RISC-V的中断控制器设计更灵活但也更复杂。CH32V103的中断向量表需要在link.ld文件里手动配置而且中断服务函数的命名规则和ARM不一样。举个例子STM32的串口1中断函数叫USART1_IRQHandler在CH32V103里叫USART1_IRQHandler名字一样但函数声明需要加一个__attribute__((interrupt()))属性告诉编译器这是一个中断函数。如果不加这个属性编译器会按照普通函数处理中断触发时不会保存现场程序直接跑飞。void USART1_IRQHandler(void) __attribute__((interrupt())); void USART1_IRQHandler(void) { if(USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t data USART_ReceiveData(USART1); /* 处理接收数据 */ } }这个细节在官方例程里有体现但如果你习惯了STM32的写法很容易漏掉。4.2 link.ld文件的关键配置RISC-V的链接脚本比ARM的复杂一些因为需要显式指定内存布局和中断向量表的位置。CH32V103的link.ld文件里有几个关键段需要特别注意。ENTRY(_start) MEMORY { FLASH (rx) : ORIGIN 0x00000000, LENGTH 64K RAM (xrw) : ORIGIN 0x20000000, LENGTH 20K } SECTIONS { .init : { *(.init) } FLASH .vector : { *(.vector) } FLASH .text : { *(.text) *(.text.*) } FLASH .data : { *(.data) *(.data.*) } RAM ATFLASH .bss : { *(.bss) *(.bss.*) } RAM }注意.vector段必须放在Flash的最前面因为RISC-V的中断入口地址是固定的。如果你在.vector之前放了其他代码中断就会跳转到错误的地址。这一点和ARM的SCB-VTOR机制不同ARM可以通过寄存器动态调整向量表偏移RISC-V需要改链接脚本。4.3 外设库的差异与适配技巧CH32V103的外设库是沁恒自研的函数命名风格和STM32标准库很像但参数定义有差异。比如GPIO初始化STM32的写法是GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure);CH32V103的写法是GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure);看起来几乎一样对吧但CH32V103的GPIO_Mode_Out_PP定义的值和STM32不同而且它的GPIO寄存器是32位的STM32是16位的。如果你直接复制STM32的代码编译能过但GPIO可能不工作。我的做法是把STM32的驱动代码作为参考但每个外设都对照CH32V103的官方例程重新写一遍。5. 常见问题排查与避坑经验实录5.1 调试器连不上的几种情况和解决办法这一年里我遇到最多的就是调试器连不上。总结下来大概有这几种情况第一种芯片被锁了。如果你在代码里禁用了SWD或者JTAG又没有留出复位延时芯片上电后直接运行代码调试器就再也连不上了。解决办法是用复位引脚强制复位然后在芯片启动的瞬间点击下载。或者用J-Link的“Connect under reset”模式。第二种供电不足。有些国产芯片的调试接口对电压比较敏感如果板子上的3.3V LDO输出电流不够调试器可能识别不到芯片。我遇到过一块板子用USB供电时调试器正常但用外部电源供电时就连不上。后来发现是外部电源的纹波太大换了一个LDO就好了。第三种时钟配置错误。如果你把主频超得太高或者Flash等待周期设错了芯片可能跑飞调试器也连不上。这时候需要把BOOT0拉高让芯片从系统存储器启动然后用调试器擦除Flash。5.2 CAN通信突然断连的排查思路CAN通信在工控项目里很常见也是最容易出问题的地方。我遇到过几次CAN突然断连的情况排查下来大概有这几个原因第一终端电阻不匹配。CAN总线两端需要各接一个120Ω的终端电阻如果只接了一端或者电阻值不对通信距离一长就会出错。我用示波器量过终端电阻不匹配时CAN_H和CAN_L的差分波形会有明显的反射。第二波特率配置错误。GD32的CAN波特率计算和STM32略有不同因为它的APB1时钟频率可能不一样。STM32F103的APB1默认是36MHzGD32F103的APB1可以到54MHz。如果你直接套用STM32的波特率配置实际波特率会偏大或偏小。我的做法是用示波器量一个CAN帧的位时间然后反推波特率。第三中断优先级冲突。CAN接收中断如果被其他高优先级中断打断太久可能会导致接收溢出。GD32的中断优先级分组和STM32一样但NVIC的寄存器地址有差异。建议把CAN中断的优先级设高一点确保及时响应。5.3 常见问题速查表问题现象可能原因排查方法解决办法调试器连不上SWD被禁用用复位引脚强制复位修改代码保留SWD串口乱码时钟配置错误量串口波特率重新配置时钟树CAN断连终端电阻不匹配量差分波形两端各接120ΩADC读数跳动采样时间太短用基准电压校准增加采样时间Flash写入失败等待周期错误检查Flash_ACR寄存器按主频设置等待周期中断进不去中断函数名错误对比启动文件修改中断函数名GPIO不工作寄存器位定义不同对比数据手册重写GPIO驱动程序跑飞堆栈溢出检查RAM使用量增大堆栈或优化代码5.4 几个让我印象深刻的坑第一个坑是GD32的DMA。STM32的DMA通道和GD32的DMA通道映射关系不同。STM32F103的USART1_TX默认用DMA1_Channel4GD32F103的USART1_TX也是DMA1_Channel4但它的DMA请求映射表不一样。如果你直接套用STM32的DMA配置数据可能发不出去。我后来是查了GD32的参考手册重新配置了DMA请求映射才解决。第二个坑是CH32V103的USB。CH32V103有USB设备控制器但它的USB库和STM32的USB库完全不同。STM32的USB库是ST官方的CH32V103的USB库是沁恒自研的。如果你要做USB HID设备需要重新学习它的API。我花了两天时间才把USB键盘的例程跑通。第三个坑是RISC-V的中断嵌套。ARM的Cortex-M3支持硬件中断嵌套RISC-V需要软件配置。CH32V103的中断控制器支持嵌套但需要在中断函数里手动清除中断标志否则会一直触发。这个细节在官方文档里写得很简略我是通过调试器单步跟踪才发现的。6. 一年使用后的真实评价与建议6.1 国产芯片到底能不能用用了一年我的结论是能用但要看场景。如果你的项目是消费类电子产品对成本敏感对稳定性要求不是特别苛刻国产芯片完全可以胜任。GD32和CH32V103在批量出货的产品里已经用了很多可靠性是有验证的。但如果是工业控制、汽车电子、医疗设备这些对可靠性要求极高的领域我建议还是谨慎一点。不是说国产芯片不行而是这些领域对芯片的长期稳定性、温度特性、抗干扰能力有严格的要求需要大量的测试数据来支撑。国产芯片在这些方面的积累还不如国际大厂。6.2 给准备尝试国产芯片的开发者几条建议第一先做小批量验证。不要一上来就量产先打几块板子把主要功能跑通做高低温测试和长时间老化测试。我一般会跑72小时连续运行观察有没有死机、复位、通信异常。第二保留STM32的备份方案。PCB设计的时候尽量让国产芯片和STM32的引脚兼容这样万一国产芯片出问题可以直接换回STM32。GD32和STM32F103的引脚是完全兼容的这一点很方便。第三重视官方文档和FAE支持。国产芯片的文档质量参差不齐有些细节写得很模糊。遇到问题的时候直接找FAE要例程或者问技术细节比自己在网上搜快得多。第四不要迷信“完全兼容”。虽然GD32和STM32的引脚兼容但内部外设的差异还是存在的。移植的时候每个外设都要实际测试不能想当然。6.3 后续还可以尝试的方向如果你已经用熟了GD32和CH32V103下一步可以试试RISC-V的高性能芯片。现在国产RISC-V MCU的主频已经可以到144MHz甚至更高性能上不输ARM Cortex-M4。而且RISC-V的生态正在快速完善工具链和RTOS的支持也越来越好。另外可以关注一下国产芯片的无线连接方案。现在很多国产MCU集成了蓝牙、Wi-Fi或者LoRa做物联网终端很方便。我最近在试一款集成了BLE的国产MCU用它做了一个温湿度采集节点整体体验还不错。最后再分享一个小技巧如果你在移植过程中遇到奇怪的问题先不要怀疑芯片先检查自己的代码。我这一年里遇到的bug90%都是自己代码的问题只有10%是芯片本身的差异。把时钟配置、中断向量表、链接脚本这三样东西检查清楚大部分问题都能解决。
返回列表