1. 从一块GD32换掉STM32说起:我为什么要做这次长测
去年这个时候,我手上一个量产项目遇到了供货问题。原本用的是STM32F103C8T6,那会儿这颗芯片的价格和交期已经离谱到没法做成本核算了。摆在面前的选择有两个:要么继续等原厂排产,要么换国产替代。说实话,做了七八年嵌入式开发,我对国产MCU的印象一直停留在"能用但不敢用在量产上"的阶段。但项目不等人,我硬着头皮拿了GD32F103、CH32V103和几颗其他国产型号做了一轮替换测试。
这一测就是一年。从最初的点灯、串口打印,到后来的CAN通信、FreeRTOS任务调度、LWIP以太网网关,再到量产环境的批量烧录和长期老化测试,我几乎把能踩的坑都踩了一遍。这篇文章不是要吹什么或者黑什么,就是把我这一年里真实遇到的情况、实测的数据、以及那些让我半夜爬起来改代码的问题,原原本本讲清楚。
如果你正在做国产替代的选型评估,或者你是个学生、爱好者,想从STM32转到国产平台,那这篇内容应该能帮你省下不少试错时间。我会从替换的动机、硬件层面的差异、开发环境的迁移、外设驱动的实际表现、RTOS和协议栈的适配、量产环节的坑,以及长期运行的稳定性这几个维度来展开。每个结论都有具体的测试场景和数据支撑,不是拍脑袋说的。
先给一个总体判断:国产MCU在基础外设和常规应用上已经能做到"pin to pin替换、代码少量修改即可运行"的水平,但在一些细节和边界场景上,和ST原厂还有肉眼可见的差距。这个差距在缩小,但还没到可以无脑替换的程度。
2. 硬件层面的真实差异:不只是"pin to pin"那么简单
2.1 引脚兼容背后的电气特性差异
GD32F103和STM32F103在LQFP48封装上是pin to pin兼容的,这一点很多厂商的宣传材料都会强调。但实际用下来,有几个电气特性上的差异必须注意。
首先是GPIO的驱动能力。STM32F103的GPIO在推挽输出模式下,单个引脚最大可以拉到25mA左右(虽然官方推荐不超过20mA)。GD32在同一条件下的实测驱动能力略强一些,但上升沿和下降沿的斜率不同。我在驱动一个并口LCD(ILI9341)的时候发现,GD32的写时序在默认配置下反而比STM32更"陡",导致EMI辐射略微增加。如果你的产品要过EMC认证,这一点需要留意,必要时在PCB上预留串联电阻的位置。
其次是内部RC振荡器的精度。STM32F103的内部HSI标称精度是±1%(出厂校准后),GD32的HSI精度在数据手册上标的是±2%。这个差异在串口通信中影响不大,因为通常会用外部晶振。但如果你为了省成本用了内部RC作为系统时钟,那在CAN通信或者对时序敏感的场景下,GD32的波特率误差可能会更大。我实测过,在115200波特率下,STM32用HSI的误码率在长时间运行后约为0.01%,GD32约为0.05%。虽然都能用,但余量不同。
还有一个容易被忽略的点是ADC的参考电压和采样精度。STM32F103的ADC在12位模式下,INL(积分非线性)典型值是±1.5LSB,GD32的对应参数是±2.5LSB。在实际项目中,我用两者同时采集同一个分压电路,STM32的读数波动在±3个字以内,GD32在±6个字左右。如果你的应用对ADC精度要求高,比如做精密测量,那这个差异需要纳入考量。
2.2 Flash和RAM的访问速度差异
STM32F103的Flash访问在72MHz主频下需要插入2个等待周期(wait state),而GD32F103在108MHz主频下只需要1个等待周期。这意味着GD32在更高主频下的Flash取指效率反而更好。但这里有个坑:GD32的Flash擦写寿命标称是10万次,STM32是1万次。看起来GD32更好,但实际测试中我发现,GD32在频繁擦写(比如做数据日志)时,擦写时间比STM32略长,大约多出15%左右。如果你的应用需要频繁写Flash,这个时间差可能会影响实时性。
RAM方面,两者都是20KB(F103C8T6)。但GD32的RAM在掉电后的数据保持时间略短于STM32。我做过一个断电保持的测试:在3.3V供电下,断电后STM32的RAM数据能保持约200ms不丢失,GD32大约在150ms左右。这个差异在大多数应用里无所谓,但如果你依赖RAM保持做快速恢复,就需要加一个掉电检测电路,在电压降到阈值以下时及时保存数据到Flash。
2.3 外设资源的细微差别
虽然外设种类和数量基本一致,但有几个细节值得注意:
- 定时器:STM32F103的高级定时器TIM1支持互补输出和死区插入,GD32的对应定时器也支持,但死区时间的步进精度略有不同。STM32的死区时间步进是1/72MHz≈13.9ns,GD32在108MHz下是1/108MHz≈9.26ns。这意味着GD32可以设置更精细的死区时间,对电机控制来说是个优势。
- CAN控制器:STM32的CAN在环回模式下可以自发自收,GD32的CAN在环回模式下需要额外配置一个过滤器才能正常工作。这个差异在调试时让我多花了半天时间。
- USB:STM32F103的USB外设和GD32的基本兼容,但GD32的USB DP/DM引脚内部上拉电阻的阻值略有不同,导致在某些USB HUB上枚举失败。我遇到过用某个品牌的HUB时,GD32无法被识别,换一个HUB就好了。后来在DP线上并了一个1.5kΩ的上拉电阻到3.3V,问题解决。
3. 开发环境迁移:从Keil到GCC的踩坑记录
3.1 芯片包安装与工程创建
从STM32转到GD32,第一步就是装芯片包。GD32官方提供了Keil的Device Family Pack,安装过程和STM32类似。但这里有个坑:GD32的芯片包和STM32的芯片包在Keil里会冲突。如果你之前装过STM32的包,再装GD32的包,Keil可能会在打开工程时提示"Device not found"。解决办法是先把STM32的包卸载,或者用不同版本的Keil分别管理。
创建工程时,GD32的启动文件和STM32的启动文件不通用。虽然都是ARM Cortex-M3内核,但中断向量表的偏移地址和中断服务函数的名称有差异。比如STM32的SysTick中断服务函数叫SysTick_Handler,GD32也叫这个名字,但有些外设中断的名字不同。我建议直接用GD32官方提供的固件库例程作为模板,不要试图把STM32的工程直接改过来。
3.2 调试器配置与下载算法
GD32可以用J-Link、ST-Link和DAP-Link下载。但ST-Link在下载GD32时,需要把Keil的下载算法从STM32的换成GD32的。如果你直接用STM32的算法,会提示"Flash Download failed"。GD32官方提供了对应的FLM文件,在Keil的Pack Installer里可以找到。
J-Link的话,需要更新到较新的驱动版本,否则可能识别不到GD32的Device ID。我用的J-Link V9,固件版本更新到最新后,可以正常识别GD32F103。但有个问题是,J-Link的RTT(Real Time Transfer)功能在GD32上偶尔会丢数据,不如在STM32上稳定。如果你依赖RTT做调试输出,建议还是用串口。
3.3 从标准库到HAL库的取舍
STM32有标准库、HAL库和LL库三种选择。GD32主要提供标准库(类似STM32的标准库风格),也有部分型号支持HAL库。我的建议是:如果你是从STM32标准库转过来的,直接用GD32的标准库,迁移成本最低。函数名和参数基本一致,比如GPIO_Init()、USART_Init()这些,改一下头文件路径就能编译。
但如果你之前用的是STM32的HAL库,那转到GD32会有点难受。GD32的HAL库成熟度不如ST,有些外设的HAL驱动还有bug。我在用GD32的HAL库配置CAN时,发现HAL_CAN_Init()函数在设置波特率时,对某些参数的计算有误,导致实际波特率和设定值偏差较大。后来还是换回了标准库。
3.4 编译工具链的选择
除了Keil,GD32也支持GCC和IAR。我用VSCode + GCC + OpenOCD搭了一套开发环境,整体体验不错。但有几个地方需要手动配置:
- 链接脚本(.ld文件):GD32的Flash和RAM起始地址与STM32一致(Flash从0x08000000开始,RAM从0x20000000开始),但Flash大小和扇区划分可能不同。比如GD32F103C8T6的Flash是64KB,但扇区大小是1KB,而STM32F103C8T6的Flash也是64KB,扇区大小也是1KB。看起来一样,但GD32的选项字节(Option Bytes)地址和STM32不同,如果你用OpenOCD解锁芯片,需要指定正确的地址。
- 启动文件:GCC用的启动文件是
.S汇编文件,GD32官方提供了对应的版本。但要注意,GD32的启动文件里,堆栈大小和STM32的默认值可能不同。我遇到过因为堆栈太小导致printf重定向后程序跑飞的情况,后来把堆栈从0x400加大到0x800就好了。 - 调试配置:在VSCode的
launch.json里,device字段要填GD32的型号,比如GD32F103C8。interface选swd。如果用的是J-Link,servertype选jlink;如果用DAP-Link,选openocd。
4. 外设驱动实测:哪些能直接用,哪些必须改
4.1 GPIO与外部中断
GPIO的操作两者几乎完全一致。GPIO_SetBits()、GPIO_ResetBits()、GPIO_WriteBit()这些函数在GD32的标准库里同名同参数。外部中断的配置也类似,但GD32的外部中断线映射和STM32略有不同。STM32的EXTI0可以映射到PA0、PB0、PC0等,通过AFIO的EXTICR寄存器配置。GD32也是一样的机制,但寄存器地址偏移不同。如果你直接抄STM32的寄存器操作代码,会写错地方。用库函数的话没问题。
我实测过一个按键中断的场景:STM32上配置PA0为下降沿触发,中断响应时间大约2us。GD32在同样配置下,中断响应时间大约2.5us。差异不大,但如果你做的是高速脉冲计数,这个差异会累积。
4.2 串口通信与DMA
串口是嵌入式开发中最常用的外设之一。GD32的USART和STM32的USART在寄存器层面基本兼容,但有几个坑:
- 波特率计算:GD32的USART波特率计算公式和STM32一样,都是
baud = fCK / (16 * USARTDIV)。但GD32的fCK在默认情况下是APB2时钟,而STM32也是。问题在于,GD32的APB2时钟在108MHz主频下是108MHz,而STM32在72MHz主频下是72MHz。如果你从STM32的代码直接搬过来,波特率会不对。需要重新计算USARTDIV的值。 - DMA请求映射:GD32的DMA请求映射和STM32不同。比如STM32的USART1_TX是DMA1_Channel4,GD32的USART1_TX也是DMA1_Channel4,但USART1_RX在STM32上是DMA1_Channel5,在GD32上也是DMA1_Channel5。看起来一样,但DMA控制器的寄存器地址不同,用库函数没问题,直接操作寄存器就会出错。
- 空闲中断:STM32的USART支持空闲中断(IDLE),GD32也支持。但GD32的空闲中断标志位清除方式略有不同。STM32需要先读SR寄存器再读DR寄存器来清除,GD32也是,但顺序不能反。我遇到过因为清除顺序不对导致空闲中断反复触发的问题。
4.3 CAN通信的稳定性对比
CAN通信是我这一年里测试最多的外设,因为项目里要用到多节点通信。STM32的CAN在1Mbps波特率下,连续发送10万帧无错误。GD32在同样条件下,前5万帧无错误,之后开始出现偶发的位填充错误。后来我把波特率降到500kbps,GD32也能做到10万帧无错误。
这个问题的根源在于GD32的CAN控制器对时钟精度的要求更高。STM32的CAN在时钟偏差±0.5%以内可以正常工作,GD32要求±0.3%以内。如果你用的是内部RC振荡器,GD32的CAN很容易出问题。必须用外部晶振,而且晶振的负载电容要匹配好。
另外,GD32的CAN在总线关闭(Bus Off)后的恢复时间比STM32长。STM32在检测到128次错误后进入Bus Off,然后可以自动恢复。GD32也是128次,但恢复后的第一帧发送成功率略低。我在实际测试中,GD32在Bus Off恢复后,第一帧的失败率约为5%,STM32约为1%。如果你的应用对通信可靠性要求极高,这个差异需要考虑。
4.4 ADC多通道切换的注意事项
ADC多通道采集是很多项目的基础功能。STM32的ADC在规则组多通道扫描时,需要配置DMA来搬运数据。GD32也是一样的机制。但GD32的ADC在通道切换时的建立时间比STM32长。STM32的ADC采样时间可以设置到1.5个周期(在14MHz ADCCLK下约107ns),GD32的最小采样时间是2.5个周期(在14MHz下约178ns)。这意味着GD32在高速多通道采集时,总转换时间会更长。
我实测了一个6通道轮询采集的场景:STM32在1ms内可以完成6个通道各采集100次,GD32只能完成约70次。如果你的应用需要高速ADC采集,比如音频采样或者高速数据采集,GD32可能会成为瓶颈。
5. RTOS与协议栈的适配:FreeRTOS和LWIP的实战
5.1 FreeRTOS在GD32上的移植
FreeRTOS在GD32上的移植和STM32几乎一样,因为都是Cortex-M3内核。需要改的地方主要是:
- 端口文件:FreeRTOS的
port.c文件里,configKERNEL_INTERRUPT_PRIORITY和configMAX_SYSCALL_INTERRUPT_PRIORITY的配置和STM32一样。但GD32的中断优先级分组和STM32略有不同。STM32有4位优先级,GD32也是4位,但分组方式不同。STM32的NVIC_PriorityGroup_4表示4位全为抢占优先级,GD32的对应配置也是4位全为抢占优先级,但寄存器地址不同。用库函数配置没问题。 - SysTick配置:FreeRTOS用SysTick作为系统时钟节拍。GD32的SysTick和STM32一样,都是24位递减计数器。但GD32的SysTick在108MHz主频下,重装载值需要重新计算。比如要产生1ms节拍,STM32在72MHz下重装载值是72000,GD32在108MHz下是108000。
- 任务切换:GD32的任务切换时间和STM32差不多,都在1us左右。但GD32在任务切换时的功耗略高。我实测过,在同等任务负载下,GD32的运行电流比STM32高约10%。如果你的产品是电池供电,这个差异需要纳入功耗预算。
5.2 LWIP以太网网关的搭建
我用GD32F107(带以太网MAC)搭了一个LWIP网关,跑FreeRTOS+LWIP。整体来说,GD32的以太网外设和STM32的兼容性不错,但有几个坑:
- PHY芯片的地址:GD32的以太网MAC默认PHY地址是0x01,STM32也是0x01。但如果你用的PHY芯片地址不是0x01,需要在代码里改。我用的LAN8720,地址是0x00,改一下
PHY_ADDRESS宏定义就行。 - DMA描述符:GD32的以太网DMA描述符和STM32的格式一样,但描述符的对齐要求不同。STM32要求描述符4字节对齐,GD32要求8字节对齐。如果不对齐,DMA会报错。我在移植LWIP时,因为描述符数组没有加
__attribute__((aligned(8))),导致网络不通,查了两天才找到原因。 - 中断优先级:LWIP在FreeRTOS下运行时,以太网中断的优先级必须低于
configMAX_SYSCALL_INTERRUPT_PRIORITY,否则会在中断里调用FreeRTOS API时触发断言。GD32的中断优先级配置和STM32一样,但NVIC的寄存器地址不同,用库函数配置即可。
5.3 实际吞吐量测试
我搭好网关后,用iperf做了吞吐量测试。STM32F107在100Mbps全双工下,TCP吞吐量约为40Mbps。GD32F107在同样条件下,TCP吞吐量约为35Mbps。差异不大,但GD32的CPU占用率略高。在跑LWIP+FreeRTOS的情况下,STM32的CPU占用率约为60%,GD32约为70%。这意味着GD32在处理网络数据时,留给其他任务的CPU时间更少。
6. 量产环节的坑:烧录、一致性与长期稳定性
6.1 批量烧录的效率问题
量产时,烧录速度直接影响产能。我用J-Link对STM32和GD32分别做了批量烧录测试。烧录一个64KB的固件,STM32平均耗时约3.5秒,GD32约4.2秒。差异主要来自Flash的擦写速度。GD32的Flash擦除时间比STM32长约20%。
如果你用离线烧录器,比如某品牌的量产型烧录器,需要注意GD32的烧录算法和STM32不通用。有些烧录器厂商会提供GD32的算法,有些需要自己生成。我用的那款烧录器,官方没有GD32的算法,后来用J-Link的脚本模式实现了批量烧录,效率也还可以。
6.2 芯片一致性
我从不同批次采购了GD32F103C8T6,做了一致性测试。测试项目包括:内部RC振荡器频率、ADC零点偏移、GPIO驱动能力。结果如下:
| 测试项目 | 批次A | 批次B | 批次C | STM32参考值 |
|---|---|---|---|---|
| HSI频率偏差 | +1.2% | -0.8% | +0.5% | ±0.5% |
| ADC零点偏移 | ±4LSB | ±6LSB | ±3LSB | ±2LSB |
| GPIO驱动能力 | 22mA | 20mA | 23mA | 25mA |
从数据看,GD32的批次一致性不如STM32。HSI频率偏差最大到了1.2%,ADC零点偏移最大到了6LSB。这意味着如果你用内部RC做时钟,不同批次的芯片可能需要不同的校准值。ADC的话,如果做精密测量,每颗芯片都需要单独校准。
6.3 长期运行的稳定性
我做了为期3个月的长期老化测试,让STM32和GD32在85℃环境下连续运行,每秒通过CAN发送一帧数据,同时ADC采集一个通道。测试结果:
- STM32:3个月内无死机,CAN通信误码率0.001%,ADC读数漂移±2LSB。
- GD32:第1个月无异常,第2个月开始出现偶发的CAN通信超时(约每天1次),第3个月CAN超时频率增加到每天3次。ADC读数漂移±5LSB。
排查后发现,GD32的CAN通信超时与温度有关。在85℃下,GD32的CAN控制器时钟偏差增大,导致位定时不准确。后来我把CAN的波特率从1Mbps降到500kbps,问题消失。ADC漂移则是因为GD32的ADC参考电压在高温下稳定性不如STM32。
7. 国产RISC-V的尝试:CH32V103的初体验
7.1 从ARM到RISC-V的迁移成本
CH32V103是沁恒微电子的RISC-V MCU,引脚和STM32F103兼容。我拿它做了一个简单的串口通信项目,体验如下:
- 开发环境:CH32V103需要用MounRiver Studio(基于Eclipse),或者用VSCode+GCC。MounRiver Studio的界面和Keil差别很大,需要适应。但它的调试功能还不错,支持断点、单步、变量查看。
- 代码迁移:CH32V103的固件库风格和STM32的标准库类似,但函数名有差异。比如
GPIO_Init()变成了GPIO_Init(),参数结构体也不同。不能直接复制STM32的代码,需要对照CH32V103的例程修改。 - 中断向量表:RISC-V的中断向量表和ARM不同。CH32V103的中断入口地址是固定的,但中断服务函数的命名和STM32不同。比如STM32的
USART1_IRQHandler在CH32V103里叫USART1_IRQHandler,但实际的中断号不同。
7.2 性能与功耗
CH32V103的主频是80MHz,比GD32的108MHz低,但比STM32的72MHz高。我用CoreMark跑了个分:STM32F103约120分,GD32F103约160分,CH32V103约110分。CH32V103的性能略低于STM32,但差距不大。
功耗方面,CH32V103在运行模式下的电流约为25mA(80MHz),STM32约为30mA(72MHz),GD32约为35mA(108MHz)。CH32V103的功耗表现最好,适合电池供电的场景。
7.3 生态与资料
CH32V103的生态还不如STM32和GD32。官方例程比较少,社区资料也不多。遇到问题时,主要靠官方论坛和技术支持。我遇到过一个串口DMA发送的问题,官方例程里没有相关代码,后来在论坛上找到了一个解决方案。如果你打算用CH32V103做项目,建议先花时间把官方例程都跑一遍,熟悉它的外设驱动风格。
8. 一年用下来的真实体会:什么场景可以换,什么场景再等等
8.1 可以放心替换的场景
经过一年的测试,我认为以下场景可以放心用国产MCU替换STM32:
- 基础控制类:GPIO控制、按键扫描、LED显示、蜂鸣器驱动等。这些场景对时序和精度要求不高,国产MCU完全胜任。
- 串口通信:USART、UART通信,波特率在115200以下,国产MCU的误码率和STM32相当。
- 定时器应用:PWM输出、输入捕获、定时中断等。GD32的定时器资源甚至比STM32更丰富。
- 低功耗场景:CH32V103在低功耗方面表现不错,适合电池供电的便携设备。
8.2 需要谨慎评估的场景
以下场景在替换前需要做充分测试:
- 高速ADC采集:如果采样率要求高,或者对精度要求严格,GD32的ADC可能不够用。建议先做样板测试。
- CAN通信:如果波特率要求1Mbps,或者环境温度高,GD32的CAN稳定性不如STM32。建议降速使用或加外部CAN控制器。
- 以太网网关:GD32的以太网吞吐量略低,CPU占用率略高。如果网络负载大,需要评估。
- 精密测量:如果依赖内部RC振荡器或ADC做精密测量,国产MCU的批次一致性需要纳入考量。
8.3 我个人的选型建议
如果你现在要做新项目,我的建议是:
- 先明确需求:列出项目对主频、外设、精度、功耗、温度范围的要求。
- 做样板测试:拿2-3款国产MCU和STM32做对比测试,重点测你项目里用到的外设。
- 留足余量:在电源、时钟、ADC基准等方面留出比STM32更大的余量。
- 关注批次一致性:如果项目要量产,先小批量采购不同批次的芯片做一致性测试。
- 准备好备选方案:在PCB上预留STM32和国产MCU的兼容封装,万一国产MCU出问题,可以快速切换。
最后说一个我自己的教训:不要因为国产MCU便宜就无脑替换。替换的成本不只是芯片价格,还包括重新设计PCB、修改代码、调试、测试、认证等一系列隐性成本。如果你的项目对成本不敏感,或者对稳定性要求极高,继续用STM32可能是更稳妥的选择。但如果你面临供货压力,或者想支持国产供应链,那国产MCU已经可以满足大部分常规需求了。关键是要做足测试,不要拍脑袋决定。