从STM32转战CH32F103?手把手教你移植MPU6050小车程序(附GPIO/USART避坑点)
从STM32到CH32F103:MPU6050智能车程序移植实战指南
当我在实验室翻出那块落灰的CH32F103开发板时,一个大胆的想法冒了出来——能否用它替代STM32F103,驱动我那辆基于MPU6050的平衡小车?这个念头很快演变成了一场充满技术细节的探索之旅。作为国产MCU的代表,CH32系列以其出色的性价比吸引了不少开发者,但库函数差异带来的移植难题也让许多人望而却步。本文将带你亲历整个移植过程,重点攻克I2C通信、USART调试和GPIO配置三大核心难题。
1. 开发环境搭建与工程迁移
移植工作的第一步是搭建适合CH32的开发环境。与STM32的Keil或IAR生态不同,CH32F103更推荐使用MounRiver Studio(MRS)作为IDE。安装完MRS后,需要特别注意编译器版本的选择——建议使用GCC for RISC-V 8.2.0版本,这是经过官方验证最稳定的工具链。
创建新工程时,我发现一个实用技巧:直接从官方提供的GPIO例程开始修改,比从头搭建更高效。将原有STM32工程中的用户代码逐步迁移过来时,需要特别注意以下文件差异:
User/ ├── main.c # 主循环和初始化代码 ├── mpu6050.c # 传感器驱动 ├── pid.c # 控制算法 └── motor.c # 电机驱动提示:CH32的标准库文件命名与STM32不同,如
ch32v10x.h对应STM32的stm32f10x.h,这个细节在包含头文件时需要特别注意。
2. GPIO配置的"暗礁"与解决方案
在移植MPU6050的I2C接口时,我遇到了第一个"坑":虽然CH32和STM32的GPIO寄存器地址完全兼容,但库函数命名却有微妙差异。例如配置I2C引脚时:
STM32标准库写法:
GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure);CH32对应修改:
GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure);关键差异点在于GPIO_Mode参数:
- STM32使用
GPIO_Mode_AF_OD表示复用开漏输出 - CH32则简化为
GPIO_Mode_Out_OD
这种命名差异在USART、SPI等外设配置中同样存在。经过反复测试,我整理出以下常用模式对照表:
| 功能描述 | STM32库定义 | CH32库定义 |
|---|---|---|
| 推挽输出 | GPIO_Mode_Out_PP | GPIO_Mode_Out_PP |
| 开漏输出 | GPIO_Mode_Out_OD | GPIO_Mode_Out_OD |
| 复用推挽输出 | GPIO_Mode_AF_PP | GPIO_Mode_Out_PP |
| 复用开漏输出 | GPIO_Mode_AF_OD | GPIO_Mode_Out_OD |
| 模拟输入 | GPIO_Mode_AIN | GPIO_Mode_IN_FLOATING |
3. I2C通信的适配与优化
MPU6050通过I2C接口通信,这是平衡小车程序的核心。CH32的I2C外设与STM32高度兼容,但库函数接口有以下关键区别:
初始化函数参数顺序不同:
- STM32:
I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct) - CH32:
I2C_Init(I2C_InitTypeDef* I2C_InitStruct, I2C_TypeDef* I2Cx)
- STM32:
时钟配置方式变化:
// STM32的时钟使能方式 RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); // CH32对应修改 RCC_APB1PeriphClockCmd(I2C1, ENABLE);- 超时处理机制: CH32的I2C库函数默认不包含超时检测,需要开发者自行实现。这是我改进后的MPU6050初始化代码片段:
#define I2C_TIMEOUT 1000 uint8_t MPU6050_Init(void) { uint32_t timeout = I2C_TIMEOUT; I2C_GenerateSTART(I2C1, ENABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)) { if((timeout--) == 0) return 0; } // ...后续初始化代码 }注意:CH32的I2C事件标志定义与STM32完全相同,这是移植过程中的一个好消息,意味着相关判断逻辑可以直接复用。
4. USART调试输出的重定向难题
printf重定向是调试过程中不可或缺的功能。在STM32上,我们通常通过重写fputc函数实现串口输出,但CH32需要额外注意:
USART寄存器名称差异:
- STM32使用
USART_DR作为数据寄存器 - CH32对应寄存器名为
USARTx->DATAR
- STM32使用
重定向实现示例:
#include <stdio.h> int fputc(int ch, FILE *f) { while((USART1->STATR & USART_FLAG_TXE) == RESET); USART1->DATAR = (uint8_t)ch; return ch; }- 波特率计算器差异: 在相同时钟配置下,CH32的实际波特率可能与STM32有微小差异。建议使用示波器验证实际波特率,这是我测试得到的一组可靠配置:
| 目标波特率 | STM32 BRR值 | CH32 BRR值 |
|---|---|---|
| 115200 | 0x1D4C | 0x1D4C |
| 9600 | 0xEA6 | 0xEA7 |
| 57600 | 0x3A98 | 0x3A99 |
5. 中断系统的隐藏差异
平衡小车需要精确的定时中断来实现PID控制。在移植定时器中断时,发现了几个关键差异点:
- NVIC优先级分组设置:
// STM32的设置方式 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // CH32的对应修改 NVIC_PriorityGroupConfig(NVIC_GROUP_2);中断服务函数命名:
- STM32:
TIM2_IRQHandler - CH32:
TMR2_IRQHandler
- STM32:
中断标志清除方式:
// STM32的标志清除 TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // CH32的对应操作 TMR_ClearITPendingBit(TMR2, TMR_IT_Update);移植完成后,我在平衡小车上进行了实地测试。最初电机出现异常抖动,通过逻辑分析仪抓取PWM波形后发现,CH32的定时器自动重装载寄存器(TMRx->ATRLR)默认值与STM32不同。调整后的小车PID参数:
// 最终稳定的PID参数 PID_InitStruct.Kp = 12.5f; PID_InitStruct.Ki = 0.8f; PID_InitStruct.Kd = 0.5f; PID_InitStruct.OutputLimit = 1000;经过三天调试,这辆基于CH32的平衡小车终于能够稳定站立。整个移植过程中最深刻的体会是:虽然库函数名称有所变化,但底层硬件设计的高度兼容性使得大部分功能只需做适配性修改即可正常工作。
