1. 从一块开发板说起:为什么我要写这篇国产芯片的真实评价
去年这个时候,我手里同时摆着三块板子:一块是经典的STM32F103C8T6最小系统板,一块是GD32F103C8T6,还有一块是CH32V103的开发板。当时接了个小批量的工业采集项目,客户对成本敏感,但又要求供货稳定。我原本想都没想就选了STM32,结果采购那边告诉我,交期要12周起步,价格还比年初涨了不少。那一刻我才真正意识到,是时候认真评估一下国产替代方案了。
这一年里,我用GD32做过数据采集网关,用CH32V做过带USB的设备,也帮朋友调过几块基于RISC-V内核的国产芯片。踩过的坑不少,但收获的经验更实在。这篇文章不是厂商软文,也不是无脑吹或者无脑黑,就是一个一线嵌入式开发者用了一年之后的客观评价。我会从实际项目出发,聊清楚国产芯片到底能不能用、在哪些场景下好用、哪些地方还需要注意,以及从STM32迁移过来时那些让人头疼的细节。
如果你正在做选型,或者被STM32的交期和价格搞得焦头烂额,又或者你只是好奇国产芯片到底发展到什么水平了,这篇文章应该能给你一些参考。我会尽量把每个结论背后的原因讲清楚,把踩过的坑和绕过的弯路都摊开来说,让你少走一些我走过的冤枉路。
2. 国产芯片的真实水平:先给一个整体判断
2.1 能用,而且有些场景下很好用
先说结论:国产芯片在Cortex-M3/M4这个级别的通用MCU上,已经做到了“能用且好用”的水平。GD32、CH32、APM32、AT32这几家,我都直接或间接用过。它们在引脚兼容性、外设丰富度、开发工具链成熟度上,已经能满足大部分中低端应用的需求。我那个工业采集网关项目,最终就是用GD32F303做的,跑了FreeRTOS加LWIP协议栈,连续运行了三个月没出问题。
但“能用”和“完全替代”之间还有距离。这个距离不在芯片本身,而在生态细节上。比如某些STM32上跑得好好的代码,移植到GD32上就会遇到时钟配置差异、Flash等待周期不同、中断向量表偏移设置不一样等问题。这些问题不大,但如果你不知道,就会卡住半天甚至几天。
2.2 不同厂商的定位差异很明显
我整理了一个简单的对比表格,基于我实际使用和测试的感受:
| 厂商 | 代表型号 | 内核 | 主要优势 | 主要注意点 |
|---|---|---|---|---|
| GD32 | GD32F103/F303 | Cortex-M3/M4 | 引脚兼容STM32,主频更高,供货稳定 | 时钟树和Flash等待周期与STM32有差异 |
| CH32 | CH32V103/CH32F103 | RISC-V/Cortex-M3 | 自研内核,USB外设强,价格低 | RISC-V工具链与ARM不同,生态资料较少 |
| APM32 | APM32F103 | Cortex-M3 | 兼容性好,工业级温度范围 | 部分外设寄存器细节与STM32不同 |
| AT32 | AT32F403A | Cortex-M4 | 主频高,内存大,性价比突出 | 库函数风格与ST有差异,需要适应 |
这个表格不是绝对的,因为每家都有多个系列,定位也不同。但整体来看,GD32在兼容性上走得最远,CH32在自研内核和USB方面有特色,APM32和AT32则在特定参数上有优势。
2.3 一个关键判断:不是“替代”,而是“重新选型”
我一开始也抱着“找个STM32的替代品”的心态去用国产芯片,后来发现这个思路有问题。正确的做法是把它当成一个全新的选型对象,重新评估它的外设能力、时钟结构、功耗特性和开发工具。比如GD32F303的主频能跑到120MHz,比STM32F103的72MHz高不少,但它的Flash等待周期设置不一样,如果你直接照搬STM32的代码,可能跑起来没问题,但在高低温环境下就可能出现取指错误。
所以我的建议是:不要想着“无缝替换”,而是把它当成一个新产品来评估。该看的数据手册要看,该做的测试要做,该调的参数要调。这样反而能发挥出国产芯片的真正优势。
3. 从STM32迁移到GD32:那些你必须知道的细节
3.1 时钟配置的差异与调整方法
STM32F103的时钟树大家都很熟悉:外部晶振经过PLL倍频到72MHz,AHB、APB1、APB2各有分频系数。GD32F103的时钟树结构类似,但PLL倍频参数和Flash等待周期的对应关系不一样。STM32在48MHz以下Flash是0等待周期,48-72MHz是1等待周期。GD32因为主频可以跑到108MHz甚至120MHz,Flash等待周期的设置需要更细致。
我实际测试下来,GD32F103在108MHz主频下,Flash需要设置为2等待周期才能稳定运行。如果你从STM32的72MHz代码直接改过来,只改了PLL倍频参数而没改Flash等待周期,短期可能看不出问题,但长时间运行或者温度变化时就会出现异常。这个坑我在一个批量项目上踩过,后来在初始化代码里加了明确的等待周期设置才解决。
具体操作上,GD32的标准库函数fmc_wscnt_set()就是用来设置等待周期的。你需要根据系统主频和供电电压查数据手册里的表格来确定具体值。我一般会留一个余量,比如108MHz时设2等待周期,而不是设1。
3.2 中断向量表偏移的设置技巧
STM32的IAP升级或者Bootloader+App架构中,中断向量表偏移是通过SCB->VTOR寄存器设置的。GD32同样支持这个寄存器,但有个细节需要注意:GD32的启动文件里,向量表默认是放在Flash起始地址的,如果你在App程序里没有正确设置VTOR,中断就会跳到Bootloader的向量表去,导致程序跑飞。
我在做GD32的App程序时,习惯在SystemInit()之后、main()之前就设置好VTOR。代码大概是这样:
#define APP_START_ADDR 0x08004000 SCB->VTOR = APP_START_ADDR;同时,链接脚本(.ld文件)里也要把Flash起始地址改成对应的偏移。STM32的ld文件大家都很熟悉,GD32的ld文件结构类似,但要注意GD32的Flash页大小可能和STM32不同。比如GD32F103的Flash页大小是1KB或2KB,具体看型号,这会影响你擦除和写入的粒度。
3.3 GPIO和复用功能的细微差别
GD32的GPIO寄存器布局和STM32基本一致,但复用功能重映射的配置方式有差异。比如STM32的USART2部分重映射是通过AFIO_MAPR寄存器的某几位来设置的,GD32也有类似的寄存器,但位定义可能不同。我在调GD32的USART2部分重映射时,查了手册才发现它的重映射位和STM32不在同一个位置。
另外,GD32的GPIO输出速度配置和STM32也有细微差别。STM32有2MHz、10MHz、50MHz三档,GD32的档位划分不同,具体要看型号的数据手册。这些细节在移植时如果不注意,可能会导致通信不稳定或者信号完整性变差。
3.4 标准库与HAL库的选择建议
STM32有标准库、HAL库、LL库多种选择,GD32主要提供标准库,也有部分型号支持HAL风格的库。我的建议是:如果你从STM32标准库迁移,用GD32的标准库最顺手,函数命名和结构体定义都很接近。如果你习惯HAL库,可能需要适应一下,或者找第三方的HAL适配层。
CH32的情况更特殊,它的RISC-V系列用的是自己的库,和ARM的库风格不同。但CH32F103这种Cortex-M3的型号,库函数风格又接近STM32标准库。所以选型时要看清楚具体型号的内核和库支持情况。
4. 实操过程:用GD32搭建一个FreeRTOS物联网网关
4.1 硬件选型与电路设计要点
这个项目的需求是:采集4路RS485传感器数据,通过以太网上传到服务器,同时支持本地SD卡存储。我选了GD32F303RCT6作为主控,因为它有足够的UART、SPI和以太网外设,主频120MHz也够用。
电路设计上有几个点需要注意。第一是电源部分,GD32的供电范围和STM32基本一致,但内部LDO的纹波抑制比可能不同,我在VDD引脚旁边加了10uF和100nF的电容组合,实测下来电源纹波控制在30mV以内。第二是晶振电路,GD32的外部晶振匹配电容需要根据实际晶振的负载电容来调整,我一开始用了STM32上常用的20pF,结果起振时间偏长,后来改成12pF就正常了。
第三是复位电路,GD32的复位引脚内部有上拉,但为了可靠性,我还是外加了10K上拉和100nF电容。第四是BOOT引脚,GD32的BOOT0和BOOT1配置方式和STM32一样,但如果你要用内置Bootloader,要注意GD32的Bootloader命令集和STM32有差异。
4.2 开发环境搭建与芯片包安装
我用的开发环境是VSCode加PlatformIO,也试过Keil和IAR。PlatformIO的好处是跨平台,而且芯片包管理方便。安装GD32的芯片包时,需要在platformio.ini里指定正确的平台和框架:
[env:gd32f303rct6] platform = ststm32 board = gd32f303rct6 framework = arduino注意这里platform还是用的ststm32,因为GD32的PlatformIO支持是社区维护的,底层还是基于STM32的平台。如果你用Keil,需要去GD32官网下载对应的Device Family Pack,安装后才能在Keil里选到GD32的型号。
J-Link下载方面,GD32和STM32的SWD接口完全兼容,直接用J-Link的SWD模式就能下载和调试。但有个细节:GD32的Flash算法在J-Link的软件里可能没有预置,你需要手动添加或者用GD32提供的Flash算法文件。我一般用J-Link Commander手动加载算法,或者用GD32的官方下载工具。
4.3 FreeRTOS移植与任务划分
FreeRTOS在GD32上的移植和STM32几乎一样,因为都是Cortex-M内核,移植层代码可以直接复用。我用的FreeRTOS版本是10.4.3,移植时主要改了两个地方:一是portmacro.h里的时钟频率定义,二是FreeRTOSConfig.h里的configCPU_CLOCK_HZ要改成GD32的实际主频。
任务划分上,我分了四个任务:传感器采集任务、以太网通信任务、SD卡存储任务、系统监控任务。传感器采集任务优先级最高,每100ms运行一次;以太网通信任务优先级中等,用信号量和采集任务同步;SD卡存储任务优先级最低,在系统空闲时写入数据;系统监控任务负责看门狗喂狗和状态指示。
这里有个坑:GD32的FreeRTOS移植中,vPortSVCHandler和xPortPendSVHandler的汇编代码和STM32完全一样,但如果你用了GD32的标准库启动文件,中断向量表里的SVC和PendSV中断名称可能不同,需要手动对应上。我一开始就是启动文件里的中断服务函数名和FreeRTOS要求的不一致,导致任务切换失败,后来在启动文件里加了宏定义才解决。
4.4 LWIP协议栈的适配与网络调试
LWIP在GD32上的适配主要是网络接口驱动。GD32F303自带以太网MAC,我外接了一个PHY芯片。驱动部分需要实现ethernetif_init()、low_level_output()和low_level_input()这几个函数。GD32的标准库里提供了以太网驱动的例程,但那个例程是基于裸机的,移植到LWIP时需要改成中断接收加信号量通知的方式。
网络调试时,我建议先用ping测试基本连通性,再用UDP测试数据传输,最后上TCP。我遇到的一个问题是:GD32的以太网DMA描述符对齐要求比STM32更严格,如果描述符地址没有32字节对齐,DMA就会报错。后来我在定义描述符数组时加了__attribute__((aligned(32)))才解决。
另一个问题是PHY芯片的地址配置。GD32的以太网MAC支持通过MDIO接口扫描PHY地址,但有些PHY芯片的地址是硬件固定的,需要根据实际电路来设置。我用的PHY地址是0x01,在驱动里把PHY_ADDRESS改成对应的值就能正常通信了。
5. 常见问题与排查技巧实录
5.1 程序下载后不运行或跑飞
这是迁移过程中最常见的问题。原因通常有三个:一是中断向量表偏移没设置对,二是Flash等待周期配置错误,三是启动文件的堆栈大小不够。
排查方法:先用调试器单步运行,看程序能不能走到main()。如果卡在启动文件里,检查堆栈大小和Flash等待周期。如果能走到main()但一开中断就跑飞,检查VTOR设置。我遇到过一次,App程序里忘了设VTOR,结果中断一来就跳到Bootloader的向量表去了,现象就是程序随机重启。
5.2 串口通信乱码或丢数据
串口问题通常和时钟配置有关。GD32的USART时钟源和STM32一样,都是来自APB总线,但分频系数的计算方式可能不同。如果你从STM32的代码直接改过来,波特率寄存器(BRR)的值需要重新计算。
计算公式是:BRR = 系统时钟 / 波特率。比如系统时钟120MHz,波特率115200,BRR = 120000000 / 115200 ≈ 1042。但GD32的BRR寄存器是16位的,整数部分和小数部分的分配和STM32一样,高12位是整数,低4位是小数。我一般用标准库的usart_baudrate_set()函数来设置,避免手动计算出错。
丢数据的问题通常是中断优先级配置不当或者DMA配置错误。GD32的中断优先级分组和STM32一样,但优先级数值的定义可能不同。我建议用标准库的nvic_irq_enable()函数来配置,不要直接操作寄存器。
5.3 ADC采样值跳动大或不准确
GD32的ADC和STM32的ADC在结构上类似,但采样时间和参考电压的配置有差异。STM32的ADC采样时间可以设1.5、7.5、13.5、28.5、41.5、55.5、71.5、239.5个周期,GD32的档位划分不同,具体看数据手册。
我遇到过一次ADC采样值跳动大的问题,后来发现是采样时间设得太短,信号还没稳定就采样了。把采样时间从1.5周期改成55.5周期后,跳动明显减小。另外,GD32的ADC参考电压如果用的是VDDA,要确保VDDA的纹波足够小,否则采样值会随电源波动。
5.4 CAN通信突然连不上
CAN通信问题通常和波特率配置、终端电阻、收发器有关。GD32的CAN控制器和STM32的CAN控制器在寄存器层面有差异,尤其是位时序寄存器的定义。STM32的CAN位时序寄存器(CAN_BTR)里,BRP、TS1、TS2的位定义和GD32不完全一样。
我建议用GD32标准库的can_parameter_struct结构体来配置,不要直接写寄存器。另外,CAN总线的终端电阻必须是120欧姆,两端各一个。我遇到过因为终端电阻没接导致通信不稳定的情况,后来在板子上加了跳线选择终端电阻就方便多了。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 程序下载后不运行 | VTOR未设置、Flash等待周期错误 | 单步调试看能否到main | 设置VTOR、调整等待周期 |
| 串口乱码 | 波特率计算错误、时钟配置错误 | 用示波器测波特率 | 重新计算BRR、检查时钟树 |
| ADC跳动大 | 采样时间太短、参考电压不稳 | 改采样时间测试 | 增加采样时间、加滤波电容 |
| CAN连不上 | 位时序配置错误、终端电阻缺失 | 测CANH/CANL波形 | 用库函数配置、加120欧姆电阻 |
| 以太网不通 | DMA描述符未对齐、PHY地址错误 | ping测试、查PHY寄存器 | 32字节对齐、改PHY地址 |
| FreeRTOS任务切换失败 | 中断向量名不匹配 | 查启动文件中断名 | 加宏定义对应 |
6. 国产芯片的生态现状与个人使用建议
6.1 工具链和资料获取的便利性
GD32和CH32的官方资料现在都比较齐全了,数据手册、参考手册、标准库例程都能在官网下载到。但相比STM32,第三方教程和社区问答的数量还是少一些。我遇到问题时,通常先查官方手册,再去一些嵌入式论坛搜,实在不行就自己用调试器抓寄存器。
CH32的RISC-V系列资料相对少一些,但官方提供了MounRiver Studio这个IDE,集成了编译器、调试器和库函数,上手还算方便。如果你之前没用过RISC-V,可能需要花点时间适应它的指令集和工具链。
6.2 供货和价格的实际感受
这一年里,GD32的供货一直比较稳定,价格虽然有波动,但整体比STM32有优势。我那个工业网关项目,用GD32F303比用STM32F103每片能省下不少成本,而且交期从12周缩短到了4周左右。CH32的价格更低,但供货稳定性稍差一些,小批量采购时可能需要多问几家代理商。
6.3 我的个人使用建议
如果你现在正在选型,我的建议是:先明确你的需求。如果是简单的控制类应用,对成本敏感,GD32或CH32都是不错的选择。如果是复杂的物联网网关,需要跑协议栈和RTOS,GD32F303或AT32F403A更合适。如果是USB设备类应用,CH32的USB外设做得不错。
迁移时,不要想着“无缝替换”,而是把它当成一个新平台来评估。该看的文档要看,该做的测试要做。我一般会先点个灯,再调串口,然后调定时器和中断,最后上RTOS和协议栈。这样一步步来,遇到问题也容易定位。
最后再分享一个小技巧:在GD32上跑FreeRTOS时,如果遇到任务切换异常,先检查FreeRTOSConfig.h里的configTOTAL_HEAP_SIZE是否够用。我遇到过因为堆太小导致任务创建失败的情况,把堆从10KB加到20KB就正常了。这个坑在STM32上不太常见,因为STM32的RAM通常更大,但GD32的RAM配置可能不同,需要根据实际型号调整。