拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
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上,超频到108MHz,STM32大概率会跑飞。这种细节,只有真正对比过两家手册、实际测过波形的人才会注意到。

所以选型的第一步,不是看参数表,而是明确你的项目对哪些指标最敏感。是成本?是供货周期?是外设兼容性?还是长期稳定性?不同的优先级,对应完全不同的选型策略。

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)); /* 配置PLL:HSE 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寄存器地址偏移是0x00,GD32也是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 AT>FLASH .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默认是36MHz,GD32F103的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_Channel4,GD32F103的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,用它做了一个温湿度采集节点,整体体验还不错。

最后再分享一个小技巧:如果你在移植过程中遇到奇怪的问题,先不要怀疑芯片,先检查自己的代码。我这一年里遇到的bug,90%都是自己代码的问题,只有10%是芯片本身的差异。把时钟配置、中断向量表、链接脚本这三样东西检查清楚,大部分问题都能解决。

返回列表