STM32到CH32V系列智能风扇代码移植与硬件兼容性实战
这类项目最值得关注的点不是功能本身而是如何在 CH32V307、CH32V203/208 和 STM32 这几款主控之间实现代码兼容和硬件适配。如果你正在评估用哪款芯片做智能风扇或者想把现有项目从 STM32 迁移到国产芯片这篇文章会拆解具体怎么处理引脚映射、外设差异和开发环境切换。我一般会先看芯片的资源重叠度CH32V307 和 STM32F103 系列在 GPIO、定时器、ADC 这些基础外设上有很多相似点但中断向量表、时钟树和库函数接口差异很大。直接烧录 STM32 的 hex 文件肯定不行但通过硬件抽象层和条件编译是能在同一套代码里支持多款主控的。下面按实际移植顺序拆解关键环节。1. 先确认硬件兼容性引脚、电压和最小系统智能风扇项目通常需要这些外设PWM 输出控制电机转速、ADC检测温度或电位器、GPIO按键、指示灯、有时还会用到 UART连接温湿度传感器或调试输出。CH32V307、203/208 和 STM32F103 在这些外设的类型和数量上基本够用但引脚分布完全不同。1.1 核心引脚映射表假设风扇控制需要以下资源功能STM32F103C8T6CH32V307CH32V203备注PWM 输出PA8 (TIM1_CH1)PA8 (TIM1_CH1)PA8 (TIM1_CH1)电机控制引脚一致ADC 温度检测PA0 (ADC1_IN0)PA0 (ADC1_IN0)PA0 (ADC1_IN0)温度采样引脚一致按键输入PC13PC13PC13开关控制引脚一致UART 调试PA9/PA10 (USART1)PA9/PA10 (USART1)PA9/PA10 (USART1)日志输出引脚一致看起来很多基础功能引脚是兼容的但要注意电源电压STM32F103 和 CH32V203/208 是 3.3VCH32V307 也是 3.3V电平匹配没问题。晶振频率STM32F103 常用 8MHz 外部晶振CH32V 系列也支持 8MHz但内部 PLL 配置参数不同。Boot0 引脚CH32V 系列需要 Boot0 拉高才能进入下载模式STM32 的 BOOT0/BOOT1 配置方式略有差异。1.2 最小系统差异STM32F103 的最小系统通常需要 3.3V 稳压、8MHz 晶振、复位电路和 Boot0 下拉电阻。CH32V 系列硬件兼容但 CH32V307 的供电电流可能更大如果之前用 LDO 给 STM32F103 供电换到 V307 要确认 LDO 的最大输出电流。我一般会先画一个兼容底板把 VCC、GND、复位、晶振这些基础电路做好然后通过排针引出所有功能引脚。这样换主控芯片时只需要重新烧录程序不需要改PCB。2. 开发环境切换从 Keil 到 MRS 或 Thread IDESTM32 项目大多用 Keil MDK 或 STM32CubeIDE而 CH32V 系列是 RISC-V 内核需要用 MounRiver Studio (MRS) 或 Thread IDE。代码不能直接编译但源文件可以复用。2.1 工程结构重组假设原 STM32 项目结构如下STM32_Fan/ ├── Core/ │ ├── Src/ │ │ ├── main.c │ │ ├── stm32f1xx_it.c │ │ └── fan_control.c │ ├── Inc/ │ └── Startup/ ├── Drivers/ │ ├── STM32F1xx_HAL_Driver/ │ └── CMSIS/ └── STM32F103C8Tx_FLASH.ld移植到 CH32V307 时我会新建一个 CH32V_Fan 项目然后保留业务逻辑文件main.c、fan_control.c这些与应用相关的代码直接复制。替换库文件删除 STM32 HAL 库加入 CH32V 的官方库文件如ch32v30x.h、system_ch32v30x.c。修改启动文件STM32 的startup_stm32f103xb.s换成 CH32V 的startup_ch32v30x_D8W.S。调整链接脚本根据 CH32V307 的 Flash 和 RAM 大小修改链接脚本。2.2 浮点运算问题处理CH32V307 支持硬件单精度浮点运算但需要在编译时开启 -marchrv32imafc 选项。在 Thread IDE 中配置项目右键 → Properties → C/C Build → Settings在 Toolchain → RISCV GCC Compiler → Miscellaneous 中添加-marchrv32imafc同样的选项在 Linker 中也要添加如果浮点运算后其他变量数据出错通常是栈对齐问题。RISC-V 要求栈指针 16 字节对齐可以在启动文件中修改// 在 startup_ch32v30x_D8W.S 的 Reset_Handler 中添加 la t0, _stack_pointer // 加载栈指针 andi t0, t0, -16 // 16 字节对齐 mv sp, t0 // 设置栈指针3. 外设驱动适配GPIO、定时器、ADC 的封装策略直接操作寄存器的话STM32 和 CH32V 的寄存器地址完全不同但通过硬件抽象层 (HAL) 可以统一接口。3.1 GPIO 控制封装假设风扇需要控制一个指示灯 LED// hal_gpio.h #ifdef STM32F103 #include stm32f1xx_hal.h #define LED_GPIO_PORT GPIOA #define LED_GPIO_PIN GPIO_PIN_5 #elif defined(CH32V307) #include ch32v30x.h #define LED_GPIO_PORT GPIOA #define LED_GPIO_PIN GPIO_Pin_5 #elif defined(CH32V203) #include ch32v20x.h #define LED_GPIO_PORT GPIOA #define LED_GPIO_PIN GPIO_Pin_5 #endif void hal_gpio_init(void); void hal_gpio_toggle(void);// hal_gpio.c void hal_gpio_init(void) { #ifdef STM32F103 GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin LED_GPIO_PIN; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(LED_GPIO_PORT, GPIO_InitStruct); #elif defined(CH32V307) || defined(CH32V208) GPIO_InitTypeDef GPIO_InitStructure {0}; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin LED_GPIO_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(LED_GPIO_PORT, GPIO_InitStructure); #endif } void hal_gpio_toggle(void) { #ifdef STM32F103 HAL_GPIO_TogglePin(LED_GPIO_PORT, LED_GPIO_PIN); #elif defined(CH32V307) || defined(CH32V208) GPIO_WriteBit(LED_GPIO_PORT, LED_GPIO_PIN, (BitAction)(1 - GPIO_ReadOutputDataBit(LED_GPIO_PORT, LED_GPIO_PIN))); #endif }3.2 PWM 风扇控制实现智能风扇的核心是 PWM 调速通过定时器的 PWM 输出功能控制电机驱动模块。// hal_pwm.h typedef struct { uint32_t frequency; // PWM 频率通常 10kHz-25kHz uint8_t duty_cycle; // 占空比 0-100 } pwm_config_t; void hal_pwm_init(pwm_config_t *config); void hal_pwm_set_duty(uint8_t duty);// hal_pwm.c #ifdef STM32F103 void hal_pwm_init(pwm_config_t *config) { TIM_HandleTypeDef htim1; TIM_OC_InitTypeDef sConfigOC {0}; __HAL_RCC_TIM1_CLK_ENABLE(); htim1.Instance TIM1; htim1.Init.Prescaler SystemCoreClock / config-frequency / 1000 - 1; htim1.Init.CounterMode TIM_COUNTERMODE_UP; htim1.Init.Period 999; // 1kHz 基础频率 htim1.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(htim1); sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse config-duty_cycle * 10; // 占空比转换 sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(htim1, sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); } #elif defined(CH32V307) void hal_pwm_init(pwm_config_t *config) { TIM_OCInitTypeDef TIM_OCInitStructure {0}; TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure {0}; RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); TIM_TimeBaseInitStructure.TIM_Period 999; TIM_TimeBaseInitStructure.TIM_Prescaler SystemCoreClock / config-frequency / 1000 - 1; TIM_TimeBaseInitStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInitStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, TIM_TimeBaseInitStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse config-duty_cycle * 10; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OCInit(TIM1, TIM_OCInitStructure, TIM_OCPreload_Enable, TIM_Channel_1); TIM_CtrlPWMOutputs(TIM1, ENABLE); TIM_Cmd(TIM1, ENABLE); } #endif3.3 温度检测与风扇调速逻辑智能风扇需要根据温度自动调整转速通常用 ADC 读取温度传感器或 NTC 热敏电阻。// temperature.c #define TEMP_LOW_THRESHOLD 25 // 低温阈值低于此温度风扇停转 #define TEMP_HIGH_THRESHOLD 40 // 高温阈值高于此温度全速运转 uint16_t read_temperature(void) { #ifdef STM32F103 ADC_HandleTypeDef hadc1; // STM32 ADC 配置和读取代码 #elif defined(CH32V307) // CH32V ADC 读取代码 #endif } void update_fan_speed(void) { uint16_t temp read_temperature(); uint8_t duty_cycle; if (temp TEMP_LOW_THRESHOLD) { duty_cycle 0; // 停转 } else if (temp TEMP_HIGH_THRESHOLD) { duty_cycle 100; // 全速 } else { // 线性调速温度在 25-40℃ 之间时占空比从 0% 线性增加到 100% duty_cycle (temp - TEMP_LOW_THRESHOLD) * 100 / (TEMP_HIGH_THRESHOLD - TEMP_LOW_THRESHOLD); } hal_pwm_set_duty(duty_cycle); }4. 多平台编译与调试配置实际项目中我一般会用一个 Makefile 或 CMake 来管理多平台编译通过环境变量切换芯片型号。4.1 使用条件编译管理平台差异在 main.c 中通过宏定义区分平台// platform.h #if defined(STM32F103xx) #define PLATFORM_STM32 #include stm32f1xx_hal.h #elif defined(CH32V30x) #define PLATFORM_CH32V307 #include ch32v30x.h #elif defined(CH32V20x) #define PLATFORM_CH32V203 #include ch32v20x.h #endif // 统一类型定义 typedef uint32_t u32; typedef uint16_t u16; typedef uint8_t u8;4.2 编译配置示例Makefile 配置示例# Makefile ifeq ($(PLATFORM), stm32f103) CC arm-none-eabi-gcc CFILES src/main.c src/fan_control.c drivers/stm32f1xx_hal_gpio.c DEFINES -DSTM32F103xx -DUSE_HAL_DRIVER LDSCRIPT stm32f103c8tx.ld else ifeq ($(PLATFORM), ch32v307) CC riscv-none-embed-gcc CFILES src/main.c src/fan_control.c drivers/ch32v30x_gpio.c DEFINES -DCH32V30x -DUSE_STDPERIPH_DRIVER LDSCRIPT ch32v307vc.ld ARCH -marchrv32imafc -mabiilp32f endif CFLAGS -Os -g $(DEFINES) $(ARCH)编译时通过环境变量指定平台# 编译 STM32 版本 make PLATFORMstm32f103 # 编译 CH32V307 版本 make PLATFORMch32v3074.3 调试器配置差异STM32常用 ST-LinkOpenOCD 配置source [find interface/stlink.cfg] source [find target/stm32f1x.cfg]CH32V307使用 WCH-LinkOpenOCD 配置source [find interface/wch-link.cfg] source [find target/rv32m1.cfg]在 VSCode 的 launch.json 中配置多调试环境{ version: 0.2.0, configurations: [ { name: Debug STM32, type: cppdbg, request: launch, program: ${workspaceFolder}/build/stm32_fan.elf, servertype: external, serverpath: openocd, serverargs: -f interface/stlink.cfg -f target/stm32f1x.cfg, cwd: ${workspaceFolder} }, { name: Debug CH32V307, type: cppdbg, request: launch, program: ${workspaceFolder}/build/ch32v307_fan.elf, servertype: external, serverpath: openocd, serverargs: -f interface/wch-link.cfg -f target/rv32m1.cfg, cwd: ${workspaceFolder} } ] }5. 实际移植中的常见问题与排查顺序从 STM32 切换到 CH32V 时最容易出现的问题集中在时钟配置、中断处理和外设初始化顺序上。5.1 时钟树配置对比STM32F103 的时钟树相对简单通常配置为HSE 8MHz → PLL 9倍频 → 72MHz 系统时钟CH32V307 的时钟配置更灵活但默认情况下HSE 8MHz → PLL 倍频 → 144MHz 系统时钟如果直接使用 STM32 的延时函数在 CH32V307 上会快一倍。需要统一系统时钟频率或使用动态计算// hal_delay.h #ifdef PLATFORM_STM32 #define SYSTEM_CLOCK 72000000 #elif defined(PLATFORM_CH32V307) #define SYSTEM_CLOCK 144000000 #endif void hal_delay_us(uint32_t us) { uint32_t ticks us * (SYSTEM_CLOCK / 1000000) / 5; // 根据实际循环指令数调整 while(ticks--); }5.2 中断向量表处理STM32 的中断向量表在启动文件中定义而 CH32V 系列的中断处理机制不同。需要统一中断服务函数命名// interrupts.h #ifdef PLATFORM_STM32 #define TIM1_IRQHandler TIM1_UP_IRQHandler #elif defined(PLATFORM_CH32V307) #define TIM1_IRQHandler TIM1_UP_IRQHandler #endif void TIM1_IRQHandler(void) { // 统一的中断处理逻辑 }5.3 外设初始化顺序CH32V 系列对外设初始化顺序更敏感特别是涉及到时钟使能和外设使能的顺序先使能外设时钟配置 GPIO 复用功能初始化外设参数最后使能外设如果外设不工作先确认这个顺序再看时钟配置是否正确。5.4 内存分配问题CH32V307 有更大的 RAM64KB而 STM32F103C8T6 只有 20KB。如果项目中使用动态内存分配需要注意堆栈大小的调整// 链接脚本中调整 STM32F103: RAM (xrw) : ORIGIN 0x20000000, LENGTH 20K STACK_SIZE 0x800; /* 2KB stack */ CH32V307: RAM (xrw) : ORIGIN 0x20000000, LENGTH 64K STACK_SIZE 0x2000; /* 8KB stack */6. 生产考虑成本、供货和长期维护如果项目要考虑量产芯片选型就不只是技术问题。6.1 成本对比STM32F103C8T6价格受市场波动影响大供货不稳定时价格会显著上涨CH32V203F8P6价格相对稳定国产芯片供货更有保障CH32V307VCT6性能更强价格介于两者之间对于智能风扇这种成本敏感的应用CH32V203 可能是更好的选择资源足够且价格更有优势。6.2 开发资源评估STM32资料丰富社区活跃遇到问题容易找到解决方案CH32V资料相对较少但官方技术支持和社区在快速成长如果团队对 RISC-V 架构不熟悉前期开发时间可能会比 STM32 长一些。但一旦熟悉后开发效率差异不大。6.3 长期维护策略我一般建议在新项目中选择 CH32V 系列特别是对成本敏感且需要长期稳定供货的产品。对于已有 STM32 项目可以逐步迁移先做好硬件抽象层实现双平台支持在小批量产品中验证 CH32V 的稳定性根据验证结果决定是否全面切换这样既避免了供应链风险又给了团队足够的学习和适应时间。我个人更建议先把硬件抽象层做扎实确保核心业务逻辑与芯片无关。这样无论后续换哪款主控都只需要适配驱动层应用层代码可以完全复用。对于智能风扇这类相对简单的应用这种多平台支持的成本并不高但能显著提升项目的抗风险能力。