
在实际嵌入式开发中智能风扇是一个很好的综合练习项目它涉及传感器数据采集、电机控制、通信协议和人机交互等多个环节。很多开发者习惯使用 STM32 作为主控但国产芯片如 CH32V307 在性价比和供应链稳定性上逐渐显现优势并且其 RISC-V 架构也值得关注。本文将以 CH32V307 为核心完整实现一个支持温控、调速和远程控制的智能风扇系统并说明如何将主控更换为 CH32V203/208 或 STM32F103 等常见型号。这个项目适合已经掌握基本单片机编程、想进一步学习综合应用或准备从 STM32 过渡到 RISC-V 平台的开发者。我们将从芯片选型、环境搭建、外设驱动、控制逻辑到多机型适配逐步展开最终形成一个可实际运行、可扩展的参考方案。1. 芯片选型与硬件设计思路智能风扇的主控需要处理温度采集、PWM 输出、按键/旋钮输入和通信接口因此要优先考虑带有 ADC、定时器、多串口和足够 GPIO 的型号。1.1 为什么选 CH32V307 作为基础方案CH32V307 是南京沁恒推出的 RISC-V 架构单片机主频 144MHz内置 64KB SRAM 和 256KB Flash支持 USB、以太网和多路串口。在智能风扇项目中它的优势在于性价比高相比同性能的 STM32F4 系列价格更低且供货稳定。生态完善沁恒提供基于 Eclipse 的 MounRiver Studio 开发环境库函数风格与 STM32 HAL 库相似上手难度低。外设丰富具备 3 个 12 位 ADC、2 个高级定时器支持 6 路 PWM、3 个 USART完全满足风扇控制需求。如果项目需要更低的成本或更小的封装可以降级使用 CH32V20348MHz20KB SRAM或 CH32V208144MHz64KB SRAM它们保持了相同的外设架构仅在内存和频率上有所缩减。1.2 STM32 替代方案对比对于习惯 STM32 的团队可以直接使用 STM32F103C8T6蓝色 Pill 板常见型号或 STM32F407VET6。这两款芯片的库函数和开发环境非常成熟但需要注意外设资源和引脚分配的差异。下表对比了三款主控的关键参数特性CH32V307CH32V203STM32F103C8T6架构RISC-VRISC-VARM Cortex-M3主频144MHz48MHz72MHzFlash256KB64KB64KBSRAM64KB20KB20KBADC3×12bit2×12bit2×12bit高级定时器2×6路PWM1×4路PWM1×4路PWM开发环境MounRiver StudioMounRiver StudioKeil/STM32CubeIDE1.3 最小系统与外设连接智能风扇的硬件模块包括温度传感器使用 DS18B20单总线或 NTC 热敏电阻接 ADC。电机驱动直流电机可用 L298N 或 TB6612无刷电机需专用驱动板。人机交互按键、旋转编码器或 OLED 显示屏I2C。通信模块可选 ESP8266AT 指令或直接使用 CH32V307 的以太网/USB。以 CH32V307 为例核心引脚分配如下功能引脚备注温度传感器PA0ADC 通道0接 NTC 分压电路PWM 输出PA8TIM1_CH1控制电机速度按键输入PB12带外部上拉用于模式切换旋转编码器PB14, PB15外部中断引脚用于调速I2C OLEDPB6(SCL), PB7(SDA)显示温度和转速ESP8266UART1(PA9-TX, PA10-RX)接收远程控制指令注意实际布线时电机驱动部分要与单片机电源隔离避免 PWM 开关噪声影响模拟电路。温度传感器尽量远离电机和驱动芯片。2. 开发环境搭建与项目配置CH32V307 的开发环境以 MounRiver StudioMRS为主它也支持 CH32V203/208。如果你计划兼容 STM32则需要额外安装 STM32CubeIDE 或 Keil MDK。2.1 MounRiver Studio 安装与配置MRS 是基于 Eclipse 的免费 IDE直接到沁恒官网下载最新版本即可。安装后需要导入芯片支持包Support Files和编译工具链RISC-V GCC。新建项目时选择 CH32V307 型号IDE 会自动生成包含系统初始化、时钟配置和启动文件的基础工程。关键配置步骤包括系统时钟设置在system_ch32v30x.c中将系统时钟源设置为 HSE外部高速晶振并配置 PLL 输出 144MHz。引脚功能分配使用 MRS 的图形化引脚配置工具将上述硬件设计中的引脚设置为对应功能ADC、PWM、GPIO 等。外设库引入在工程属性中勾选需要的外设库如 ADC、TIM、USART、I2C 等。2.2 如果使用 STM32 开发环境对于 STM32F103可以使用 STM32CubeMX 生成初始化代码在 CubeMX 中选择 STM32F103C8Tx配置时钟树HSE 8MHz → PLL 72MHz。分配引脚ADC1_IN0温度、TIM1_CH1PWM、GPIO 输入按键、USART1ESP8266。生成代码时选择 MDK-ARM 或 STM32CubeIDE 项目。注意STM32 的定时器配置与 CH32V307 略有不同特别是高级定时器 TIM1 的刹车和互补输出功能在风扇项目中通常不需要可以禁用。2.3 统一代码结构的设计为了支持多主控型号建议将硬件相关代码抽象为独立模块通过宏定义切换不同芯片的驱动实现。项目目录结构如下SmartFan/ ├── Core/ │ ├── main.c │ ├── system_*.c # 芯片特定的系统文件 │ └── interrupts.c # 中断服务程序 ├── Drivers/ │ ├── MCU/ │ │ ├── ch32v30x/ # CH32V307 外设驱动 │ │ ├── ch32v20x/ # CH32V203/208 驱动 │ │ └── stm32f1xx/ # STM32F103 驱动 │ ├── Sensor/ │ │ ├── temperature.c # 温度传感器抽象层 │ │ └── encoder.c # 编码器处理 │ └── Actuator/ │ ├── motor.c # 电机控制 │ └── display.c # OLED 显示 ├── Middleware/ │ ├── communication.c # ESP8266 通信处理 │ └── controller.c # 温控算法 └── Utilities/ ├── delay.c # 精确延时 └── debug.c # 调试信息输出在main.c中通过条件编译选择目标芯片// 在项目预定义宏中设置 TARGET_MCU #define TARGET_CH32V307 // #define TARGET_STM32F103 #include motor.h #include temperature.h #include communication.h int main(void) { SystemInit(); // 芯片特定系统初始化 Motor_Init(); // 电机 PWM 初始化 Temperature_Init(); // 温度传感器初始化 Communication_Init(); // ESP8266 初始化 while(1) { // 主循环 } }3. 核心驱动模块实现智能风扇的软件核心包括温度采集、电机调速、人机交互和通信协议。下面以 CH32V307 为例给出关键代码并说明在其他芯片上的适配要点。3.1 温度采集与滤波处理使用 NTC 热敏电阻时需要通过 ADC 读取分压值再查表或计算得到温度。为了提高稳定性通常需要加入软件滤波。// temperature.c #define NTC_ADC_CHANNEL ADC_Channel_0 #define ADC_SAMPLE_TIMES 10 static uint16_t ADC_ReadAverage(uint8_t channel) { uint32_t sum 0; for(uint8_t i 0; i ADC_SAMPLE_TIMES; i) { sum ADC_ReadSingleChannel(channel); Delay_Ms(1); // 每次采样间隔 1ms } return sum / ADC_SAMPLE_TIMES; } float Temperature_Get(void) { uint16_t adc_value ADC_ReadAverage(NTC_ADC_CHANNEL); float voltage (adc_value * 3.3) / 4095.0; // 12位ADC参考电压3.3V // 根据 NTC 分压电路计算电阻值 float ntc_resistance (voltage * 10000) / (3.3 - voltage); // 10k 上拉电阻 // 使用 Steinhart-Hart 方程计算温度简化版 float steinhart; steinhart ntc_resistance / 10000.0; // (R/R0) steinhart log(steinhart); // ln(R/R0) steinhart / 3950.0; // 1/B * ln(R/R0) steinhart 1.0 / (25.0 273.15); // (1/T0) steinhart 1.0 / steinhart; // 取倒数 steinhart - 273.15; // 开尔文转摄氏度 return steinhart; }在 STM32F103 上ADC 读取函数需要改为 HAL 库的HAL_ADC_GetValue()但滤波算法和温度计算逻辑可以完全复用。3.2 PWM 电机调速实现直流电机的速度通过 PWM 占空比控制。CH32V307 的高级定时器 TIM1 可以生成高精度 PWM。// motor.c void Motor_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; // 开启时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_TIM1, ENABLE); // 配置 PWM 输出引脚 PA8 GPIO_InitStructure.GPIO_Pin GPIO_Pin_8; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 定时器基础配置144MHz / (1440 * 1000) 100Hz PWM 频率 TIM_TimeBaseStructure.TIM_Period 1000 - 1; // 自动重装载值 TIM_TimeBaseStructure.TIM_Prescaler 1440 - 1; // 预分频器 TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, TIM_TimeBaseStructure); // PWM 模式配置 TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 0; // 初始占空比 0% TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM1, TIM_OCInitStructure); // 使能定时器 TIM_CtrlPWMOutputs(TIM1, ENABLE); TIM_Cmd(TIM1, ENABLE); } void Motor_SetSpeed(uint8_t percent) { if(percent 100) percent 100; uint16_t pulse (percent * 10); // 占空比 pulse / 1000 TIM_SetCompare1(TIM1, pulse); }在 STM32F103 上TIM1 的配置类似但需要额外调用HAL_TIM_PWM_Start()来启动 PWM 输出。3.3 旋转编码器处理旋转编码器通过两个相位差 90 度的信号判断旋转方向和步数。使用外部中断捕获信号变化。// encoder.c static int8_t encoder_count 0; void Encoder_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; EXTI_InitTypeDef EXTI_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; // 编码器引脚 PB14, PB15 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_14 | GPIO_Pin_15; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; // 上拉输入 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); // 配置外部中断 RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14); GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource15); EXTI_InitStructure.EXTI_Line EXTI_Line14 | EXTI_Line15; EXTI_InitStructure.EXTI_Mode EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger EXTI_Trigger_Falling; // 下降沿触发 EXTI_InitStructure.EXTI_LineCmd ENABLE; EXTI_Init(EXTI_InitStructure); // 配置中断优先级 NVIC_InitStructure.NVIC_IRQChannel EXTI15_10_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 1; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); } // 中断服务程序中判断旋转方向 void EXTI15_10_IRQHandler(void) { if(EXTI_GetITStatus(EXTI_Line14) ! RESET) { if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_15) 0) { encoder_count; // 顺时针 } else { encoder_count--; // 逆时针 } EXTI_ClearITPendingBit(EXTI_Line14); } if(EXTI_GetITStatus(EXTI_Line15) ! RESET) { if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_14) 0) { encoder_count--; // 逆时针 } else { encoder_count; // 顺时针 } EXTI_ClearITPendingBit(EXTI_Line15); } } int8_t Encoder_GetCount(void) { int8_t count encoder_count; encoder_count 0; // 读取后清零 return count; }注意编码器处理需要防抖措施可以在中断中加入延时判断或使用定时器定期采样来避免误触发。3.4 ESP8266 通信模块集成ESP8266 通过 AT 指令与主控通信实现远程控制功能。需要处理指令发送、响应解析和超时重试。// communication.c #define ESP8266_UART USART1 #define CMD_TIMEOUT 1000 // 指令超时时间毫秒 void ESP8266_SendCmd(const char* cmd, const char* expected, uint16_t timeout) { UART_SendString(ESP8266_UART, cmd); UART_SendString(ESP8266_UART, \r\n); uint32_t start_time GetTickCount(); while((GetTickCount() - start_time) timeout) { if(UART_ReceiveString(ESP8266_UART, response_buffer, sizeof(response_buffer))) { if(strstr(response_buffer, expected) ! NULL) { return; // 收到预期响应 } } } // 超时处理 Debug_Print(ESP8266 CMD timeout: %s, cmd); } void Communication_Init(void) { // 初始化串口 UART_InitTypeDef UART_InitStructure; UART_InitStructure.UART_BaudRate 115200; UART_InitStructure.UART_WordLength UART_WordLength_8b; UART_InitStructure.UART_StopBits UART_StopBits_1; UART_InitStructure.UART_Parity UART_Parity_No; UART_InitStructure.UART_Mode UART_Mode_Tx | UART_Mode_Rx; UART_Init(ESP8266_UART, UART_InitStructure); // 重启 ESP8266 GPIO_ResetBits(ESP_RST_PORT, ESP_RST_PIN); Delay_Ms(100); GPIO_SetBits(ESP_RST_PORT, ESP_RST_PIN); Delay_Ms(2000); // 配置 WiFi 模式 ESP8266_SendCmd(ATCWMODE1, OK, CMD_TIMEOUT); // Station 模式 ESP8266_SendCmd(ATCWJAP\SSID\,\PASSWORD\, OK, 10000); // 连接路由器 ESP8266_SendCmd(ATCIPMUX1, OK, CMD_TIMEOUT); // 多连接模式 ESP8266_SendCmd(ATCIPSERVER1,8080, OK, CMD_TIMEOUT); // 创建 TCP 服务器 }4. 控制逻辑与系统集成智能风扇的工作模式包括手动调速、自动温控和远程控制需要设计一个状态机来管理模式切换和优先级。4.1 温控算法设计自动模式下风扇转速根据环境温度动态调整。可以使用简单的分段线性控制// controller.c typedef struct { float min_temp; // 温度下限 float max_temp; // 温度上限 uint8_t min_speed; // 最小转速百分比 uint8_t max_speed; // 最大转速百分比 } TempSpeedMap; static TempSpeedMap temp_map[] { {20.0, 25.0, 0, 30}, // 20-25°C0%-30% 转速 {25.0, 30.0, 30, 60}, // 25-30°C30%-60% 转速 {30.0, 35.0, 60, 100}, // 30-35°C60%-100% 转速 }; uint8_t TemperatureToSpeed(float temp) { for(int i 0; i sizeof(temp_map)/sizeof(temp_map[0]); i) { if(temp temp_map[i].min_temp temp temp_map[i].max_temp) { // 线性插值计算转速 float ratio (temp - temp_map[i].min_temp) / (temp_map[i].max_temp - temp_map[i].min_temp); return temp_map[i].min_speed ratio * (temp_map[i].max_speed - temp_map[i].min_speed); } } return 0; // 默认关闭 }4.2 主控状态机实现主循环中根据当前模式执行相应的控制逻辑并处理模式切换// main.c typedef enum { MODE_MANUAL 0, // 手动模式 MODE_AUTO, // 自动温控 MODE_REMOTE // 远程控制 } FanMode; static FanMode current_mode MODE_MANUAL; static uint8_t target_speed 0; void Main_Loop(void) { float current_temp Temperature_Get(); int8_t encoder_delta Encoder_GetCount(); // 模式切换检测长按按键 3 秒 if(Button_LongPressDetected()) { current_mode (current_mode 1) % 3; Display_Mode(current_mode); } // 根据模式计算目标转速 switch(current_mode) { case MODE_MANUAL: if(encoder_delta ! 0) { target_speed encoder_delta * 5; // 编码器每步调整 5% if(target_speed 100) target_speed 100; if(target_speed 0) target_speed 0; } break; case MODE_AUTO: target_speed TemperatureToSpeed(current_temp); break; case MODE_REMOTE: // 远程指令通过全局变量传递 target_speed remote_target_speed; break; } // 应用转速控制 Motor_SetSpeed(target_speed); // 更新显示 Display_Update(current_temp, target_speed, current_mode); Delay_Ms(100); // 100ms 控制周期 }4.3 远程控制协议设计通过 ESP8266 的 TCP 服务器接收简单控制指令例如{ cmd: set_speed, value: 75 }或{ cmd: set_mode, value: auto }解析逻辑可以放在串口接收中断中使用状态机解析 JSON 或更简单的自定义格式。5. 常见问题与调试方法在实际开发中智能风扇项目常遇到电机干扰、温度漂移、通信超时等问题。下面列出典型问题及解决方案。5.1 电机 PWM 导致系统复位或 ADC 读数异常现象电机启动时单片机复位或温度读数出现大幅跳动。原因电机感性负载产生的反电动势通过电源干扰单片机PWM 开关噪声耦合到模拟电路。解决方案电机驱动板与单片机使用独立的电源必要时通过光耦或磁耦隔离 PWM 信号。在电机电源输入端并联大电容1000μF 以上吸收电压尖峰。ADC 基准电压使用独立的 LDO 提供并在 VREF 引脚加去耦电容。在 PWM 输出引脚串联小电阻22-100Ω并加下拉电阻减缓信号边沿。5.2 温度读数波动大现象温度值在不同时刻读数差异明显即使环境温度稳定。原因NTC 热敏电阻的自我加热效应、ADC 参考电压噪声、软件滤波不足。解决方案降低 NTC 的激励电流使用更大的上拉电阻如 100kΩ 代替 10kΩ。在 NTC 分压电路输出端加 RC 低通滤波1kΩ 1μF。增加软件采样次数使用滑动平均或中值滤波算法。在温度变化缓慢的场景下可以适当降低采样频率如 1Hz。5.3 ESP8266 连接不稳定现象ESP8266 经常断开连接AT 指令响应超时。原因WiFi 信号弱、电源供电不足、指令处理逻辑有缺陷。解决方案检查 ESP8266 的供电电压工作时电流可能超过 200mA需要稳定的 3.3V 电源。在天线位置加屏蔽避免电机驱动部分的电磁干扰。在 AT 指令处理中加入重试机制但限制最大重试次数避免死锁。定期发送心跳包检测连接状态异常时执行软重启。5.4 旋转编码器误触发现象未操作时编码器计数自动变化或一次操作触发多次计数。原因机械编码器触点抖动、中断触发方式设置不当、防抖处理不足。解决方案在编码器引脚加硬件滤波0.1μF 电容对地。将中断触发方式改为双边沿触发在中断中判断两个引脚的状态组合。使用定时器定期采样编码器状态如每 10ms代替外部中断。在软件中实现去抖逻辑要求连续多次采样状态一致才确认变化。6. 生产环境注意事项学习环境能让项目运行起来但实际产品还需要考虑长期稳定性、安全性和可维护性。6.1 固件升级与维护智能风扇作为 IoT 设备需要支持固件远程升级OTA。可以通过 ESP8266 下载新固件到外部 Flash再由单片机通过 IAP 方式更新自身程序。关键实现步骤将 Flash 分为引导程序区、主程序区和备份区。引导程序检查主程序完整性决定是否跳转或恢复备份。主程序通过 ESP8266 接收新固件校验后写入备份区。重启后引导程序将备份区固件复制到主程序区完成升级。6.2 安全防护措施即使是非关键设备也应考虑基本的安全防护通信数据加密如 AES-128防止恶意控制。指令身份验证避免未授权访问。操作频率限制防止拒绝服务攻击。异常操作日志记录便于事后分析。6.3 能耗与寿命优化对于电池供电或需要长期运行的风扇可以加入以下优化根据环境自动进入低功耗模式如温度低于阈值时关闭风扇和部分外设。使用 PWM 软启动减少电机启动电流冲击。定期检测电机堵转避免过热损坏。记录运行时间提示维护周期。这个智能风扇项目从芯片选型到系统实现展示了嵌入式开发中的硬件设计、驱动编写、控制算法和系统集成等多个关键环节。通过模块化设计和条件编译同一套代码可以适配不同主控芯片这在元器件供应多变的实际项目中非常实用。下一步可以加入更多传感器如湿度、人体感应或者集成到更大的智能家居系统中。