ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

国产MCU替代STM32一年实测:GD32与CH32V103的工程实践与踩坑记录

国产MCU替代STM32一年实测:GD32与CH32V103的工程实践与踩坑记录 1. 从一块GD32换掉STM32说起我为什么要做这个一年期实测2023年初我手上一个工业数据采集项目遇到了供货问题。原本用的是STM32F103C8T6那会儿这颗芯片的市场价格已经从十几块炒到了七八十而且交期动不动就二三十周。项目不能停客户催得紧我被迫开始认真审视国产替代方案。说实话在此之前我对国产MCU的态度是能用但不敢用在正式项目上——这个偏见持续了大概两三年直到我真的把GD32、CH32V103这些芯片一颗一颗焊到板子上跑完一个完整的春夏秋冬。这一年里我经手的板子大概有四十多块涉及GD32F103、GD32E230、CH32V103、华大HC32F460还有几款RISC-V内核的芯片。应用场景从简单的串口通信、定时器捕获到CAN总线组网、USB设备、ILI9341屏幕驱动、步进电机控制都有覆盖。踩过的坑包括GD32的DMA和STM32行为不一致、CH32V103的中断向量表配置方式完全不同、RISC-V的link.ld文件要自己从头写等等。这篇文章不是芯片厂商的软文也不是无脑吹或者无脑黑。我只想把这十二个月里真实遇到的情况、实测的数据、踩坑的排查过程原原本本讲清楚。如果你正在做国产替代选型或者手头有个项目在犹豫要不要换国产芯片这些经验应该能帮你少走不少弯路。文章会涉及STM32和GD32的对比、CH32V103的开发体验、RISC-V工具链的配置细节以及一些具体外设驱动上的差异。2. GD32替换STM32引脚兼容背后的真实差异2.1 硬件层面的几乎兼容到底差在哪很多人第一次接触GD32都是冲着Pin to Pin替换STM32这个卖点去的。我一开始也是这么想的——板子不用改固件重新编译一下就能跑。实际动手之后发现硬件层面确实基本兼容但有几个细节必须注意。首先是供电范围。STM32F103的官方工作电压是2.0V到3.6VGD32F103标称是2.6V到3.6V。这意味着如果你原来的板子用的是2.5V供电有些低功耗场景会这么设计换GD32之后可能直接不启动。我有一块老板子用的是2.5V LDO换上GD32之后死活不跑查了半天才发现是电压不够。其次是复位引脚的内置上拉。STM32的NRST引脚内部有弱上拉GD32也有但阻值不一样。如果你的复位电路设计得比较临界比如上拉电阻用了100K换芯片之后可能会出现偶发复位。我后来统一把复位上拉改成10K问题就消失了。再就是晶振起振时间。GD32的外部晶振起振时间比STM32略长特别是在用8MHz无源晶振的时候。如果你的启动代码里等待HSERDY的超时时间设得太短GD32可能会启动失败。我在实际项目里把超时从原来的0x0500改成了0xFFFF就再没出现过起振失败。2.2 固件移植时最容易被忽略的DMA差异硬件兼容只是第一步固件层面的坑才是真正花时间的。我遇到最典型的问题就是DMA。STM32的DMA在传输完成后如果不再需要可以直接关闭通道下次用的时候重新配置。GD32的DMA在关闭通道时如果当前还有未完成的传输行为跟STM32不一样——它可能会把剩余的数据丢掉也可能产生一次额外的传输完成中断。我在做串口DMA接收的时候就因为这个差异导致数据偶尔丢包。排查过程是这样的我先用逻辑分析仪抓了串口波形确认数据确实发出来了然后在DMA传输完成中断里打印计数器发现有时候中断触发了但计数器没到预期值。最后翻GD32的参考手册才看到DMA通道关闭时有个传输未完成即终止的行为说明。解决办法是在关闭DMA之前先等待传输完成标志或者用软件触发一次传输完成。另一个DMA的坑是通道优先级。STM32的DMA通道优先级是硬件固定的GD32虽然也标称有优先级但实际调度行为有差异。我在一个项目里同时用了ADC DMA和串口DMASTM32上跑得好好的GD32上串口偶尔会丢数据。后来把串口DMA的优先级调高问题解决。2.3 中断向量表偏移GD32的APP程序跳转陷阱做IAP升级或者BootloaderAPP架构的朋友一定会遇到向量表偏移的问题。STM32的标准做法是在APP的main函数开头调用NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x4000)把向量表偏移到APP的起始地址。GD32也有类似的函数但偏移的粒度不一样。STM32的向量表偏移可以是任意值只要对齐到0x200有些系列是0x100。GD32要求偏移量必须是0x400的整数倍。我第一次做GD32的IAP时APP起始地址设在了0x08004000偏移量0x4000这个没问题。但后来另一个项目APP起始地址是0x08002000偏移量0x2000也是0x400的倍数按理说没问题结果APP就是跑不起来。查了很久才发现GD32的向量表偏移寄存器在写入之后需要等待一段时间才能生效而且这个等待时间跟偏移量大小有关。我在跳转到APP之前加了一个简单的延时循环问题就解决了。这个细节在GD32的参考手册里写得很隐蔽不仔细看根本注意不到。提示做GD32的IAP时跳转前除了关中断、设MSP、设向量表建议再加一个几十微秒的延时确保向量表偏移生效。2.4 调试器兼容性与GD32 Cube的真实体验调试器方面ST-Link V2和V3都能正常调试GD32J-Link也没问题。但有一个细节GD32的调试接口在低功耗模式下会被关闭。如果你的程序进了Stop模式ST-Link可能会连不上芯片。解决办法是在调试时先不让程序进低功耗或者用GD32的调试保持功能在低功耗模式下保持调试接口供电。至于GD32 Cube说实话跟STM32 CubeMX的成熟度还有差距。GD32 Cube的代码生成器在配置复杂外设比如USB、CAN时生成的初始化代码偶尔会有小问题。我遇到过一次CAN的波特率配置Cube生成的参数算出来跟实际不符导致CAN通信连不上。后来手动改了CAN_BTR寄存器的值才正常。所以我的建议是GD32 Cube可以用来生成框架代码但关键外设的初始化参数一定要对照参考手册自己核算一遍。3. CH32V103与RISC-V另一条技术路线的上手实录3.1 从ARM到RISC-V工具链要换的不只是编译器CH32V103是沁恒的RISC-V芯片内核是青稞V3A标称主频48MHz有些型号能到144MHz。我第一次拿到这块芯片的时候第一反应是这不就是个STM32的替代品吗结果上手才发现从ARM切到RISC-V要换的东西比想象中多。首先是工具链。STM32用Keil或者IARCH32V103用的是MounRiver Studio基于GCC。MounRiver的界面跟Eclipse很像用惯了Keil的人需要适应一下。但真正麻烦的是链接脚本。ARM的工程里链接脚本.sct或者.ld通常是IDE自动生成的你基本不用管。RISC-V的工程里link.ld文件需要你自己维护特别是当你需要把某些函数放到RAM里执行或者需要精确控制段地址的时候。我一开始没在意link.ld直接用MounRiver生成的默认脚本结果程序编译出来大小不对有些函数被链接到了错误的地址。后来花了一个下午研究RISC-V的链接脚本语法才把各个段的位置理顺。这里给一个我实际用的link.ld片段MEMORY { FLASH (rx) : ORIGIN 0x00000000, LENGTH 64K RAM (xrw) : ORIGIN 0x20000000, LENGTH 20K } SECTIONS { .text : { *(.vector_table) *(.text) *(.text.*) } FLASH .data : { *(.data) *(.data.*) } RAM ATFLASH .bss : { *(.bss) *(.bss.*) } RAM }这个脚本的关键在于.vector_table段必须放在最前面因为RISC-V的中断向量表默认在0x00000000。如果你把其他代码放在前面中断就会跳错地方。3.2 CH32V103的中断配置跟ARM完全不同的思路ARM Cortex-M的中断配置很直观使能中断、设置优先级、写中断服务函数名。RISC-V的中断配置是另一套逻辑。CH32V103用的是PFIC可编程快速中断控制器中断使能分两级全局使能和通道使能。而且中断服务函数的写法跟ARM不一样需要用特定的属性声明。我第一次写CH32V103的串口中断时按照ARM的习惯写了void USART1_IRQHandler(void)结果编译能过但中断死活不进。后来查了沁恒的例程才发现中断函数要这样写__attribute__((interrupt(WCH-Interrupt-fast))) void USART1_IRQHandler(void) { // 中断处理代码 }这个__attribute__((interrupt(WCH-Interrupt-fast)))是关键没有它编译器不会把函数放到中断向量表里。而且WCH-Interrupt-fast表示使用硬件压栈中断响应更快但要求中断函数里不能调用太复杂的东西。另一个差异是中断嵌套。ARM的中断嵌套是硬件自动管理的RISC-V需要软件参与。CH32V103支持中断嵌套但需要在中断函数里手动清除相应的标志位否则低优先级中断会被阻塞。我在做串口定时器双中断的时候就因为没有正确处理嵌套导致定时器中断偶尔丢失。3.3 RISC-V的启动流程从复位到main都发生了什么ARM Cortex-M的启动流程相对固定复位后从向量表取MSP和Reset_Handler然后执行启动文件里的汇编代码最后跳到main。RISC-V的启动流程更开放不同厂商的实现差异很大。CH32V103的启动流程大致是这样的复位后硬件把PC设置为0x00000000然后执行向量表里的第一条指令。向量表的第一项是跳转到启动代码启动代码负责初始化栈指针、清零BSS段、复制DATA段最后调用main。这里有个容易踩的坑栈指针的初始化。ARM的栈指针是硬件从向量表里自动加载的RISC-V需要你在启动代码里手动设置。如果你用的启动文件不对或者link.ld里栈的位置定义错了程序一跑就HardFault。我在第一次移植CH32V103的时候就因为栈指针设到了错误的地址程序跑了几条指令就挂了。后来用调试器单步跟踪发现SP的值是个乱七八糟的数才定位到问题。3.4 国产RISC-V芯片的生态现状能用但要有心理准备用了一年CH32V103和另外两款RISC-V芯片我的整体感受是能用但生态跟ARM还有明显差距。具体来说STM32的HAL库虽然被很多人吐槽臃肿但它的文档、例程、社区支持是实打实的。你遇到问题网上搜一下大概率能找到答案。RISC-V芯片的社区资料相对少很多时候需要直接看参考手册和例程代码。沁恒的例程写得还算清楚但覆盖的场景有限复杂应用需要自己摸索。另一个问题是第三方库的支持。比如你想在CH32V103上跑FreeRTOS官方有移植好的版本但版本比较老。如果你想用最新的FreeRTOS需要自己移植工作量不小。而STM32的FreeRTOS移植资料遍地都是基本是复制粘贴的事。不过话说回来如果你的应用场景比较固定不需要频繁换芯片RISC-V芯片的性价比确实高。CH32V103的价格大概是同规格STM32的三分之一到一半对于成本敏感的项目这个差价很可观。4. 外设驱动实战从ILI9341到CAN总线的国产芯片表现4.1 ILI9341屏幕驱动读ID返回A1A1的排查过程ILI9341是一块很常见的TFT屏幕驱动芯片SPI接口320x240分辨率。我在STM32上用ILI9341很顺手读ID返回0x9341然后初始化、刷屏一气呵成。换到GD32上之后读ID返回的是0xA1A1屏幕完全不亮。这个问题的排查花了我差不多两天。先说结论GD32的SPI在读取数据时时序跟STM32有细微差异。ILI9341的读ID流程是这样的先发命令0xD3读ID4然后读4个字节第2和第3个字节是ID的高低位。STM32的SPI在发送完命令后会自动插入一个时钟周期用于读取GD32的SPI没有这个自动插入需要手动多读一个字节。我一开始以为是SPI模式设错了试了Mode 0到Mode 3都不行。后来用逻辑分析仪抓了SPI的波形发现GD32在发送完命令后MISO线上直接就是数据了而STM32会有一个空周期。解决办法是在读ID之前先发一个dummy字节// GD32上读ILI9341 ID的正确方式 void ILI9341_ReadID(uint16_t *id) { uint8_t buf[4]; ILI9341_CS_LOW(); ILI9341_WriteCmd(0xD3); ILI9341_ReadData(buf, 4); // 第一个字节是dummy *id (buf[2] 8) | buf[3]; ILI9341_CS_HIGH(); }这个坑在STM32上不存在因为STM32的SPI硬件会自动处理。所以如果你从STM32换到GD32SPI外设的驱动一定要重新测一遍。4.2 CAN通信突然连不上一个典型的GD32调试问题CAN总线在工业项目里用得很多我在一个多节点采集系统里用了GD32F103的CAN外设。系统跑了大半年都正常突然有一天CAN通信断了所有节点都收不到数据。排查过程是这样的先确认硬件没问题CAN收发器供电正常终端电阻正常。然后用示波器看CAN_H和CAN_L的差分信号发现总线上有信号但波形畸变严重。再查GD32的CAN寄存器发现CAN_ESR错误状态寄存器里的错误计数器一直在涨说明总线上有错误帧。最后定位到问题GD32的CAN在总线负载较高时如果发送失败重传机制跟STM32不一样。STM32的CAN在发送失败后会一直重传直到成功或者总线关闭。GD32的CAN在重传次数达到一定值后会进入错误被动状态不再主动发送。这个行为差异导致一个节点发送失败后整个网络进入恶性循环。解决办法是在CAN初始化时把自动重传功能打开并且适当增大重传次数限制。另外在软件层面加一个心跳机制如果某个节点连续一段时间没有收到数据就主动复位CAN控制器。注意GD32的CAN和STM32的CAN在错误处理机制上有差异做多节点组网时一定要实测总线负载和错误恢复。4.3 步进电机控制定时器PWM的精度对比我用STM32和GD32分别做过五线四相步进电机的控制用的是定时器PWM方向引脚的方式。整体来说两者的定时器都能满足步进电机控制的需求但在PWM占空比精度上有细微差别。STM32的定时器在设置比较值时可以精确到1个计数周期。GD32的定时器在高速PWM下比如100KHz以上比较值的写入有延迟导致占空比偶尔会有1-2个周期的抖动。对于步进电机来说这个抖动影响不大但如果你的应用对PWM精度要求很高比如精密电源控制就需要考虑这个差异。另外GD32的定时器在编码器模式下的表现比STM32略好计数更稳定。我在一个需要读编码器反馈的项目里GD32的编码器模式几乎没有丢步STM32偶尔会丢一两个计数。当然这可能跟具体的电路设计和滤波参数有关不能一概而论。4.4 串口、I2C、SPI的稳定性长测数据为了客观评价我在实验室里做了一个简单的长测用STM32F103、GD32F103、CH32V103各做一块板子跑同样的串口收发、I2C读写EEPROM、SPI读写Flash的测试连续跑72小时记录错误次数。测试项STM32F103GD32F103CH32V103串口收发115200bps72h0错误0错误0错误I2C读写400KHz72h2次超时5次超时3次超时SPI读写18MHz72h0错误0错误1次错误定时器捕获1MHz信号72h0错误0错误0错误从数据看三者在常规外设上的稳定性都很好。I2C的超时次数差异可能跟具体的上拉电阻和布线有关不一定是芯片本身的问题。SPI在18MHz下CH32V103出现了一次错误后来把时钟降到9MHz就再没出现过。5. 一年使用后的选型建议与真实心得5.1 什么场景适合换国产芯片什么场景不建议用了一年我的结论是国产芯片在中低端应用上已经完全可用但高端和特殊场景还需要谨慎。适合换国产芯片的场景常规的工业控制、数据采集主频要求不高72MHz-144MHz成本敏感的量产项目国产芯片的价格优势明显对供货周期要求高不想被单一供应商卡脖子外设需求比较标准串口、SPI、I2C、定时器、ADC不建议换的场景需要高性能浮点运算或者DSP指令的应用对USB、以太网等复杂外设有高要求的项目需要大量第三方库支持比如LVGL、FatFS的高级功能对芯片的长期供货稳定性有极高要求有些国产芯片的长期供货承诺不如国际大厂5.2 国产替代的迁移成本到底有多大很多人以为国产替代就是换个芯片重新编译实际迁移成本比想象中高。我粗略估算了一下一个中等复杂度的STM32项目大概2万行代码用了5-6个外设迁移到GD32大概需要2-3周的调试时间。主要花在外设驱动的差异调试SPI、DMA、CAN是重灾区中断向量表和启动文件的调整低功耗模式的重新验证长时间稳定性测试如果迁移到RISC-V芯片比如CH32V103时间会更长大概4-6周因为工具链和中断配置都要重新学。5.3 给正在做选型的工程师的几条实在建议第一不要一次性全换。先在一个非关键项目上试跑上几个月确认没问题再逐步推广。我见过有团队为了赶国产化进度一次性把所有项目都换了结果问题集中爆发反而耽误了更多时间。第二保留STM32的代码分支。即使换了国产芯片也建议保留原来的STM32代码用条件编译区分。这样万一国产芯片出问题可以快速切回去。第三重视参考手册。国产芯片的参考手册质量参差不齐但再差也比没有强。遇到问题第一件事是翻手册而不是上网搜。很多差异手册里都写了只是藏得比较深。第四建立自己的测试用例库。把迁移过程中遇到的所有问题都记录下来形成测试用例。下次换芯片的时候直接跑一遍测试用例能快速发现兼容性问题。5.4 关于国产芯片真实水平的个人体会最后说点个人感受。一年前我对国产芯片是怀疑的现在我的态度是谨慎乐观。国产芯片在常规应用上已经能打了GD32的性价比确实高CH32V103在RISC-V阵营里也算成熟。但差距依然存在特别是在工具链的成熟度、文档的完善度、生态的丰富度上。我印象最深的一次是GD32的一个DMA问题我翻了半天手册没找到答案最后在一个技术论坛的角落里看到有人提了一句GD32的DMA在关闭通道前要等传输完成。这种细节STM32的社区里可能早就有人整理成博客了但国产芯片的社区还需要时间积累。不过反过来想正是因为生态还在建设期现在进入的人能积累更多经验。等国产芯片的生态完全成熟了这些经验就不值钱了。所以如果你现在正在做国产替代不妨把踩过的坑都记下来既是帮自己也是帮后来的人。这一年里我最大的收获不是省了多少钱而是对芯片底层机制的理解更深了。以前用STM32很多细节被HAL库封装了不用管。换了国产芯片之后被迫去看寄存器、看时序、看链接脚本反而把很多基础知识补回来了。从这个角度说国产芯片的不完善对工程师的成长未必是坏事。
返回列表