1. 从一块“不听话”的板子说起:我为什么要做这次长测
去年这个时候,我手头同时到了三块开发板:一块是经典的STM32F103C8T6最小系统板,一块是号称“Pin to Pin替代”的GD32F103,还有一块是当时刚拿到样片的CH32V103R8T6。三块板子摆在桌面上,引脚位置几乎一模一样,但芯片内核、主频、外设实现完全是三条不同的技术路线。我当时给自己定了一个很朴素的计划:用一年时间,把这三类芯片分别塞进真实的项目里跑,不跑分、不吹参数,就看它们在长时间运行、外设并发、中断密集、低功耗切换这些实际工况下到底表现如何。
这一年里,我用STM32做过带FreeRTOS的物联网网关,用GD32改过两轮差速小车的控制板,用CH32V103折腾过RISC-V工具链下的串口屏驱动。中间踩过的坑包括但不限于:GD32的vector table base offset配置不对导致APP跳转后直接HardFault、CH32V103的GPIO复用功能需要额外使能、STM32的ADC通道切换时DMA搬运错位。这些经历让我对“国产芯片真实水平”这个问题有了比较具体的答案,不是一句“能用”或者“不能用”能概括的。
这篇文章适合谁看?如果你正在做基于STM32的毕业设计、在选型阶段纠结要不要换国产替代、或者已经上手了GD32/CH32但被某个诡异问题卡住,那这篇内容应该能帮你省下不少查手册和逛论坛的时间。我会把这一年的实测数据、踩坑记录、以及最终稳定下来的配置方案都摊开讲,不藏私。
2. 三块芯片的底子:核心架构与选型逻辑拆解
2.1 内核差异决定了你的开发方式
STM32F103用的是ARM Cortex-M3内核,GD32F103同样是Cortex-M3,但两者在Flash访问机制上有本质区别。STM32的Flash控制器带预取缓冲,代码在Flash里执行时基本能跑到72MHz满速;GD32的Flash工艺不同,它靠的是零等待周期设计,主频拉到108MHz时依然不需要插入等待周期。这个差异直接导致了一个现象:同样的延时循环代码,在GD32上跑出来的实际延时比STM32短一截。我实测过for(i=0;i<1000;i++)这种空循环,GD32比STM32快大约30%,如果你从STM32迁移到GD32,所有软件延时函数都得重新校准。
CH32V103走的是另一条路,它用的是RISC-V内核,具体是沁恒自研的V3F架构,主频80MHz。RISC-V的好处是指令集精简、中断响应快,但坏处是工具链和调试生态跟ARM完全不是一回事。你没法直接用Keil MDK编译CH32V103的工程,得用MounRiver Studio或者自己搭GCC工具链。我一开始试图用VSCode加RISC-V GCC插件硬搭环境,折腾了两天才把printf重定向到串口调通,后来发现MounRiver Studio虽然界面丑,但开箱即用,省心得多。
2.2 外设实现上的“同名不同命”
三块芯片的引脚定义高度相似,但外设寄存器的行为差异很大。举个例子,STM32的GPIO_USART2_PARTIAL_REMAP这个功能,在GD32上对应的寄存器位和使能顺序就不一样。我在GD32上做串口重映射时,按照STM32的习惯先开AFIO时钟再配置重映射寄存器,结果串口死活不出数据。后来翻GD32的用户手册才发现,它的AFIO时钟使能位在RCU_APB2EN寄存器里的位置跟STM32不同,而且重映射配置需要在关闭USART使能的前提下进行。
再比如ADC。STM32的ADC切换通道时,如果开了DMA,需要先停止DMA再改通道,否则会出现数据错位。GD32的ADC在规则组通道切换上做了优化,但注入组通道的切换逻辑又跟STM32不一样。我在做电池电压采集时,用STM32的代码直接烧到GD32上,结果采到的电压值跳得厉害,后来发现是采样时间配置的寄存器位宽不同,GD32的采样周期档位比STM32少两档,需要重新计算。
2.3 选型时我实际考虑的几个维度
| 维度 | STM32F103 | GD32F103 | CH32V103 |
|---|---|---|---|
| 内核 | Cortex-M3 | Cortex-M3 | RISC-V V3F |
| 主频 | 72MHz | 108MHz | 80MHz |
| 开发环境 | Keil/IAR/VSCode | Keil/IAR/VSCode | MounRiver/GCC |
| 库函数兼容性 | 标准库/HAL | 高度兼容STM32 | 自研库 |
| 供货稳定性 | 波动较大 | 相对稳定 | 稳定 |
| 单价(参考) | 中高 | 中 | 低 |
| 适合场景 | 生态成熟项目 | 性能敏感替代 | 成本敏感新项目 |
这个表格是我自己根据实际采购和测试整理的,不是抄的选型手册。你如果只是做基于STM32的智能台灯这种小项目,STM32的生态最省心;如果要跑FreeRTOS物联网网关并且对成本敏感,GD32的性价比很突出;如果是全新项目、团队愿意学新工具链,CH32V103的RISC-V路线值得考虑。
3. 真实项目里的表现:从网关到小车的实测记录
3.1 STM32跑FreeRTOS网关:稳定但要注意内存
我用STM32F103RCT6搭了一个物联网网关,跑FreeRTOS,任务包括:串口屏刷新、LWIP协议栈、MQTT上报、ADC采集。系统跑了三个月没重启,但中间遇到过两次printf卡死的情况。排查后发现是串口发送没有加互斥锁,两个任务同时调用printf导致DMA发送冲突。后来我给串口输出加了一个二值信号量,问题再没出现过。
STM32的HAL库用起来方便,但它的HAL_Delay在FreeRTOS下会阻塞整个任务调度,必须换成osDelay。另外,STM32的DWT(数据观察点单元)可以用来做微秒级延时,比定时器方便,但需要手动使能DWT_CTRL寄存器。我在调试超声波测距时就用DWT做回波脉宽测量,精度比HAL_Delay高得多。
3.2 GD32改两轮差速小车:性能有惊喜,坑也集中
GD32F103的108MHz主频在跑两轮差速小车的PID控制时优势明显。同样的控制周期,STM32上只能跑到1kHz,GD32能跑到2kHz,电机响应更跟手。但GD32的CAN通信有个坑:它的CAN控制器在总线负载高时,如果发送邮箱满了,会直接丢帧而不是等待。我在做STM32控制伺服电机485通信时,用GD32替代STM32做CAN节点,结果偶尔丢指令。后来在应用层加了重发机制才解决。
另一个坑是APP程序跳转。GD32的vector table base offset配置跟STM32不同,STM32用SCB->VTOR寄存器设置偏移,GD32虽然也支持,但需要在跳转前关闭所有中断并清除挂起标志,否则APP里一开中断就进HardFault。我当时的Bootloader代码改了三天才稳定。
3.3 CH32V103驱动串口屏:RISC-V的初体验
CH32V103我主要用来驱动一块ILI9341串口屏。RISC-V的中断控制器叫PFIC,配置方式和NVIC完全不同。我一开始按照STM32的NVIC_EnableIRQ去写,编译都过不了。后来查了沁恒的例程,发现需要用PFIC_EnableIRQ,而且中断优先级分组只有两档,比ARM的灵活度低。
但CH32V103有个好处:它的GPIO翻转速度很快,我用它做五线四相步进电机的脉冲输出,能轻松跑到100kHz,STM32F103在72MHz下跑同样的代码只能到60kHz左右。另外,CH32V103的Flash写入不需要像STM32那样先擦除整个页,它支持双字编程,写入效率更高。不过要注意,CH32V103的Flash擦写次数标称只有10万次,比STM32的100万次低一个数量级,频繁写参数的场景要加磨损均衡。
4. 工具链与开发环境:从Keil到VSCode的迁移实录
4.1 STM32的芯片包安装与VSCode环境搭建
STM32的芯片包安装是最成熟的。Keil MDK下直接用Pack Installer下载对应系列的DFP包就行。但如果你像我一样习惯用VSCode,搭建STM32开发环境需要几步:先装Cortex-Debug插件,再装STM32CubeMX生成Makefile工程,然后配置J-Link下载任务。我实测下来,VSCode加J-Link的下载速度比Keil快,而且代码补全更顺手。
注意:VSCode下用J-Link下载STM32时,需要在
launch.json里指定device和interface,否则会报“无法识别设备”。另外,STM32的ld链接文件需要根据芯片Flash大小手动调整,比如F103C8T6是64KB Flash,链接文件里的LENGTH要改成0x10000。
4.2 GD32的工程迁移:从STM32标准库到GD32固件库
GD32的固件库跟STM32标准库高度相似,但函数名有细微差别。比如STM32的GPIO_Init在GD32里叫gpio_init,参数结构体成员名也不同。我迁移一个STM32标准库新建工程到GD32时,直接替换启动文件和库文件,结果编译报了几十个错误。后来发现GD32的gd32f10x.h里对寄存器位的宏定义跟STM32的stm32f10x.h不一样,需要逐个替换。
更省事的方法是直接用GD32官方的GD32F10x_Firmware_Library,里面的例程覆盖了大部分外设。但要注意,GD32的USB库跟STM32的完全不同,如果你做STM32 USB串口相关的项目,迁移到GD32需要重写USB描述符和端点配置。
4.3 CH32V103的MounRiver Studio使用要点
MounRiver Studio基于Eclipse,界面跟Keil差别很大。新建工程时选CH32V103模板,它会自动生成启动文件和链接脚本。调试用WCH-Link,比J-Link便宜但速度慢一些。我实测下载一个20KB的固件,WCH-Link要3秒左右,J-Link只要1秒。
提示:CH32V103的
printf重定向需要实现_write函数,而且要在工程设置里勾选Use MicroLIB。另外,MounRiver Studio的代码补全偶尔会卡,建议关掉自动构建,手动点编译。
5. 常见问题与排查技巧实录
5.1 串口通信类问题
问题:STM32的printf输出乱码。排查思路:先确认波特率是否匹配,再检查SystemCoreClock是否正确。如果用了HSE但实际晶振是8MHz而代码里写的是12MHz,波特率就会偏。我遇到过用STM32 GBK转UTF8时,串口助手编码设错导致中文乱码,改成UTF-8就好了。
问题:GD32的GPIO_USART2_PARTIAL_REMAP配置后串口无输出。排查思路:确认AFIO时钟已开,重映射寄存器在USART使能前配置。GD32的RCU_APB2EN寄存器里AFIO使能位是RCU_AFEN,不是STM32的RCC_APB2ENR_AFIOEN。
5.2 中断与DMA类问题
问题:STM32的ADC切换通道后DMA数据错位。排查思路:在切换通道前先HAL_ADC_Stop_DMA,改完通道再重新启动。或者用注入组通道,注入组切换不需要停DMA。
问题:STM32的delay函数卡死。排查思路:检查SysTick中断优先级是否被其他中断抢占。如果用了FreeRTOS,HAL_Delay会卡死,必须换osDelay。
5.3 烧录与启动类问题
问题:GD32的APP程序跳转后HardFault。排查思路:跳转前关中断、清挂起、设VTOR、设MSP。GD32的VTOR寄存器地址跟STM32一样,但跳转前必须确保APP的向量表偏移正确。
问题:CH32V103用WCH-Link烧录失败。排查思路:检查目标板供电是否足够,WCH-Link的固件是否最新。有时候需要按住复位键再点下载。
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 串口乱码 | 波特率/时钟配置错误 | 检查晶振频率和分频系数 |
| ADC数据跳变 | 采样时间不足 | 增加采样周期档位 |
| CAN丢帧 | 发送邮箱满 | 应用层加重发机制 |
| APP跳转HardFault | 中断未关/VTOR未设 | 跳转前关中断并设VTOR |
| Flash写入失败 | 未擦除/对齐错误 | 按页擦除并按双字对齐 |
6. 一年使用后的个人体会与建议
这一年下来,我对这三类芯片的定位有了比较清晰的认识。STM32的生态最完善,遇到问题搜一下基本都有答案,适合基于STM32的毕业设计和需要快速出成果的项目。GD32的性能确实强,108MHz主频在两轮差速小车和步进电机控制上优势明显,但迁移时不能无脑替换,外设寄存器的差异需要逐项核对。CH32V103的RISC-V路线有特色,成本低、GPIO快,但工具链和调试体验还有提升空间,适合愿意折腾新平台的团队。
如果你正在选型,我的建议是:先看项目对生态的依赖程度,再看成本敏感度,最后看团队的学习意愿。不要因为“国产”两个字就盲目换,也不要因为“习惯”就死守一个平台。我踩过的那些坑,本质上都是用A芯片的经验去套B芯片导致的。把手册翻一遍,把例程跑一遍,大部分问题都能提前避免。
最后分享一个小技巧:不管用哪块芯片,先把串口调通。串口是调试的眼睛,串口通了,后面的事情都好办。我在CH32V103上折腾最久的就是串口,一旦printf能出字,后面的ADC、定时器、Flash都是顺水推舟。这个顺序你可以在任何新平台上复用。