在实际嵌入式开发中,智能风扇是一个很好的综合练习项目,它涉及传感器数据采集、电机控制、通信协议和人机交互等多个环节。很多开发者习惯使用 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,完全满足风扇控制需求。
如果项目需要更低的成本或更小的封装,可以降级使用 CH32V203(48MHz,20KB SRAM)或 CH32V208(144MHz,64KB SRAM),它们保持了相同的外设架构,仅在内存和频率上有所缩减。
1.2 STM32 替代方案对比
对于习惯 STM32 的团队,可以直接使用 STM32F103C8T6(蓝色 Pill 板常见型号)或 STM32F407VET6。这两款芯片的库函数和开发环境非常成熟,但需要注意外设资源和引脚分配的差异。
下表对比了三款主控的关键参数:
| 特性 | CH32V307 | CH32V203 | STM32F103C8T6 |
|---|---|---|---|
| 架构 | RISC-V | RISC-V | ARM Cortex-M3 |
| 主频 | 144MHz | 48MHz | 72MHz |
| Flash | 256KB | 64KB | 64KB |
| SRAM | 64KB | 20KB | 20KB |
| ADC | 3×12bit | 2×12bit | 2×12bit |
| 高级定时器 | 2×(6路PWM) | 1×(4路PWM) | 1×(4路PWM) |
| 开发环境 | MounRiver Studio | MounRiver Studio | Keil/STM32CubeIDE |
1.3 最小系统与外设连接
智能风扇的硬件模块包括:
- 温度传感器:使用 DS18B20(单总线)或 NTC 热敏电阻(接 ADC)。
- 电机驱动:直流电机可用 L298N 或 TB6612,无刷电机需专用驱动板。
- 人机交互:按键、旋转编码器或 OLED 显示屏(I2C)。
- 通信模块:可选 ESP8266(AT 指令)或直接使用 CH32V307 的以太网/USB。
以 CH32V307 为例,核心引脚分配如下:
| 功能 | 引脚 | 备注 |
|---|---|---|
| 温度传感器 | PA0 | ADC 通道0,接 NTC 分压电路 |
| PWM 输出 | PA8 | TIM1_CH1,控制电机速度 |
| 按键输入 | PB12 | 带外部上拉,用于模式切换 |
| 旋转编码器 | PB14, PB15 | 外部中断引脚,用于调速 |
| I2C OLED | PB6(SCL), PB7(SDA) | 显示温度和转速 |
| ESP8266 | UART1(PA9-TX, PA10-RX) | 接收远程控制指令 |
注意:实际布线时,电机驱动部分要与单片机电源隔离,避免 PWM 开关噪声影响模拟电路。温度传感器尽量远离电机和驱动芯片。
2. 开发环境搭建与项目配置
CH32V307 的开发环境以 MounRiver Studio(MRS)为主,它也支持 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_CH1(PWM)、GPIO 输入(按键)、USART1(ESP8266)。
- 生成代码时选择 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("AT+CWMODE=1", "OK", CMD_TIMEOUT); // Station 模式 ESP8266_SendCmd("AT+CWJAP=\"SSID\",\"PASSWORD\"", "OK", 10000); // 连接路由器 ESP8266_SendCmd("AT+CIPMUX=1", "OK", CMD_TIMEOUT); // 多连接模式 ESP8266_SendCmd("AT+CIPSERVER=1,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°C:0%-30% 转速 {25.0, 30.0, 30, 60}, // 25-30°C:30%-60% 转速 {30.0, 35.0, 60, 100}, // 30-35°C:60%-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 软启动,减少电机启动电流冲击。
- 定期检测电机堵转,避免过热损坏。
- 记录运行时间,提示维护周期。
这个智能风扇项目从芯片选型到系统实现,展示了嵌入式开发中的硬件设计、驱动编写、控制算法和系统集成等多个关键环节。通过模块化设计和条件编译,同一套代码可以适配不同主控芯片,这在元器件供应多变的实际项目中非常实用。下一步可以加入更多传感器(如湿度、人体感应),或者集成到更大的智能家居系统中。