ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32F103C8T6智能风扇开发:从寄存器到PWM调速

2026/9/13 6:19:10 拓冰建站 浏览量
STM32F103C8T6智能风扇开发:从寄存器到PWM调速 简介这套基于STM32F103C8T6的智能风扇项目源码与说明文档面向嵌入式课程设计、毕业设计以及单片机综合开发学习者。项目实现温湿度采集、PWM自动调速、按键手动设置、定时开关、过热保护、状态显示等功能从传感器读取、PWM输出到按键消抖和显示刷新代码中都有对应实现整体采用模块化设计覆盖主控制、数据采集、风速控制、用户界面、定时器、保护模块便于理解底层驱动到业务逻辑的完整链路。压缩包共325个文件大小14.36MB以C源文件、H头文件、Keil工程文件为主另含hex/axf固件、链接脚本、map映射表及说明文档可直接阅读工程也可编译烧录验证。说明文档对硬件选型、电路连接、模块调试和整机联调均有记录不少外设驱动代码可复用到电机控制、温度采集等项目中整体适合作为毕业设计参考。目前已有101人学习浏览适合需要完整嵌入式智能硬件案例的读者参考使用。1. 把寄存器操作盘明白智能风扇其实就这三件事很多人在拿到这个基于STM32F103C8T6的智能风扇项目时第一反应是去看主循环里怎么调度任务但真正让这个项目值回票价的其实是它把“传感器读取 - 温度决策 - PWM调速”这条链路压到了寄存器级。C8T6这颗芯片不算新但胜在外设齐全、价格低廉在智能家居的小型节点上至今仍是出货量很大的选择这也是它成为毕业设计和工程入门常客的原因你能在一颗64脚的芯片上同时看到ADC、TIM、I2C、USART协同工作而这正好覆盖了智能风扇的核心诉求——感知环境、调节输出、对外交互。这个项目的源码结构里有一个细节值得注意既有SuperFan.axf又有SuperFanV2.axf。前者是基础功能版本后者在任务调度和PWM控制逻辑上做了调整。实际调试时往往会从V1起步确认传感器数据链路和PWM占空比映射关系再切到V2验证状态机迁移。源码包内涉及的stm32f10x_tim.c、stm32f10x_adc.c、stm32f10x_i2c.c等标准外设库文件意味着这份代码可以在不依赖CubeMX的情况下直接导入Keil工程编译对想弄清楚启动流程和时钟树配置的人反而更友好。接下来我们从代码结构入手逐步拆开这套系统工程。2. 从启动文件到外设库读懂这份STM32工程的组织逻辑2.1 芯片选型和最小系统板问题STM32F103C8T6属于F1系列中的“小容量”型号64KB Flash、20KB SRAM主频最高72MHz。这个容量跑裸机状态机绰绰有余但如果计划后续上FreeRTOS20KB SRAM需要精打细算。项目中看到stm32f10x_tim.c和stm32f10x_adc.c同时存在说明PWM和采样分别占用了不同的外设实例。对于C8T6这种不带DAC的型号PWM输出是模拟量控制的唯一高效途径。国产替代方面现在市面上有很多兼容C8T6引脚定义的芯片比如GD32F103系列。需要注意寄存器和中断向量表基本兼容但ADC的采样时钟配置参数存在差异。我一般会建议在项目初期就锁定原厂ST型号等代码稳定后再评估替代方案否则排错时会把外设问题误判为逻辑问题。2.2 时钟树和外设使能的先后顺序代码中system_stm32f10x.c负责系统时钟初始化默认配置为72MHz。在做外设初始化时必须遵循“先使能外设时钟再配置外设寄存器”的顺序。以下片段是标准做法初看平平无奇却是风扇PWM无输出的头号排查点。void TIM2_PWM_Init(u16 arr, u16 psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period arr; TIM_TimeBaseStructure.TIM_Prescaler psc; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 0; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OCInitStructure.TIM_OCIdleState TIM_OCIdleState_Reset; TIM_OC1Init(TIM2, TIM_OCInitStructure); TIM_Cmd(TIM2, ENABLE); }注释里隐含了两层关键信息第一GPIOA Pin 0 是TIM2的通道1复用引脚如果这里误用GPIO_Mode_Out_PP输出会变成普通推挽而不是PWM波形第二TIM_OCMode_PWM1表示向上计数时当CNT小于CCR时输出有效电平这是最常用的模式。参数arr和psc共同决定PWM频率计算方式为f 72MHz / ((psc1)*(arr1))智能风扇的电机驱动频率通常选在25kHz左右以避开可闻噪声因此arr 999, psc 2可得到24kHz这个区间对直流无刷电机比较合适。如果发现电机啸叫优先调整psc和arr的配合关系而不是修改代码逻辑。2.3 项目文件分组哪些文件真正影响编译结果项目源码中出现的stm32f10x_flash.c是整个工程能否正确写入的关键。C8T6的Flash扇区分布和F103ZET6不同如果使用J-Link或ST-Link下载时出现校验失败一般不是芯片损坏而是下载算法选择错误。在Keil的Flash Download选项中需要手动指定STM32F10x Med-density 64K Flash算法这一点在256K Flash的芯片上开发过的工程师特别容易踩坑。stm32f10x_i2c.c的存在暗示传感器走的是I2C总线。C8T6的I2C1在PB6/PB7但有经验的开发者通常会选择软件模拟I2C原因在于STM32F1系列硬件I2C在总线繁忙状态下的处理不够理想。所以看到这个文件时建议先确认代码里实际使用的是硬件I2C还是GPIO模拟。如果后续遇到传感器偶发无响应直接切换到软件I2C往往比排查硬件状态寄存器更快。3. 温湿度采集与风速决策从ADC原始值到舒适区间的完整链路3.1 传感器数据读取的两种模式对比智能风扇项目里温度传感器可以走ADC也可以走I2C。ADC方式适合NTC热敏电阻或模拟输出的温度传感器I2C方式适合SHT30、AHT20这类数字温湿度传感器。源码包中同时包含stm32f10x_adc.c和stm32f10x_i2c.c说明设计者预留了两种采集链路。ADC采集的核心是通道配置和采样时间设置的配合。STM32F103的ADC是12位分辨率参考电压默认接VDDA通常3.3V转换结果存放在ADC_DR寄存器中。以下是标准初始化代码片段void ADC1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOB, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOB, GPIO_InitStructure); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode DISABLE; ADC_InitStructure.ADC_ContinuousConvMode ENABLE; ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 1; ADC_Init(ADC1, ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_8, 1, ADC_SampleTime_55Cycles5); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); }这里是PB0引脚对应ADC1的通道8。RCC_ADCCLKConfig(RCC_PCLK2_Div6)将ADC时钟设置为12MHz这是F1系列的推荐上限。采样时间设为55.5个周期对于NTC这类响应速度较慢的传感器足够稳定。初始化完成后启动一次软件转换u16 ADC_GetValue(void) { ADC_SoftwareStartConvCmd(ADC1, ENABLE); while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); return ADC_GetConversionValue(ADC1); }连续转换模式下ADON位保持置1后每次软件触发都会立即启动新转换。读取结果时先判断EOC标志位确保数据有效后再取出ADC_DR的值。线性转换公式为温度 (ADC值 / 4095) * 3.3V再根据NTC分压电路反查电阻值经B值公式算实际温度。如果使用数字传感器代码会在另一条初始化分支里配置I2C地址这部分内容在后续章节展开。3.2 风速档位映射区间划分和滞回控制采集到温度后接下来是把连续的温度值映射为离散的风速档位。这个环节看似简单但如果没有滞回控制温度在阈值边界波动时会导致风扇频繁换挡在夜间使用场景下这种体验是灾难性的。用以下代码示例来展示一个典型的三段式映射加滞回设计#define TEMP_LOW_ENTER 26.0f #define TEMP_LOW_EXIT 24.0f #define TEMP_HIGH_ENTER 30.0f #define TEMP_HIGH_EXIT 28.5f u8 Fan_GetLevel(float temp) { static u8 current_level 0; if(current_level 0 temp TEMP_LOW_ENTER) { current_level 1; } else if(current_level 1) { if(temp TEMP_HIGH_ENTER) current_level 2; else if(temp TEMP_LOW_EXIT) current_level 0; } else if(current_level 2 temp TEMP_HIGH_EXIT) { current_level 1; } return current_level; }进入低速档的边界是26度退出的边界是24度进入高速档的边界是30度退出的边界是28.5度。这样设计的好处是当温度从25度缓慢升到26度时风扇进入一档随后温度回落到25.5度甚至24.5度时风扇不会被立刻被关闭或减档避免了两个阈值之间的抖动。current_level被声明为static变量意味着函数的调用周期不能太长否则温度变化跨越多个区间时会自动化误判。这部分逻辑直接决定用户体验比传感器精度更值得花时间去调参。3.3 定时器模块与风扇运行时长记录定时器模块在这个项目中承担了比PWM生成更复杂的角色。stm32f10x_tim.c同时包含TIM2的PWM输出配置和TIM3的定时中断。常见做法是用TIM3产生一个1ms的时基再在主循环或中断里累计秒数和分钟数。void TIM3_Init(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); TIM_TimeBaseStructure.TIM_Period 999; TIM_TimeBaseStructure.TIM_Prescaler 71; TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); NVIC_InitStructure.NVIC_IRQChannel TIM3_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); TIM_Cmd(TIM3, ENABLE); } void TIM3_IRQHandler(void) { if(TIM_GetITStatus(TIM3, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); g_sys_tick; } }预分频71自动重载999得到1ms一次中断。在中断服务函数里只做计数器累加不在中断里执行传感器读取或PWM占空比更新避免阻塞中断响应。g_sys_tick作为全局变量在主循环中判断是否需要更新显示或执行定时开关功能。定时开关的实现逻辑就是记录开启时刻并设置目标时长每次主循环检查g_sys_tick与目标时刻的差值来关闭PWM输出。这种实现方法在上一章提到的FreeRTOS裸机代码对比中属于裸机开发的经典写法逻辑清晰且不占额外资源。4. 手动控制与蓝牙透传按键扫描、串口协议与远程指令解析4.1 按键去抖与长按短按状态机手动控制的主要形式是物理按键。在工程中会看到至少三个按键风速加、风速减、模式切换。按键输入有一个经典问题机械触点抖动。这段代码展示了一种不用延时阻塞的去抖方式依赖主循环的扫描周期。#define KEY_SCAN_PERIOD 5 u8 key_state KEY_STATE_RELEASED; u8 key_debounce_cnt 0; u8 key_value KEY_NONE; void Key_Scan(void) { u8 current_read GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_12); if(current_read KEY_ACTIVE_LOW) { if(key_state KEY_STATE_RELEASED) { key_debounce_cnt; if(key_debounce_cnt 3) { key_state KEY_STATE_PRESSED; key_value KEY_MODE_SWITCH; } } else { key_debounce_cnt 0; } } else { if(key_state KEY_STATE_PRESSED) { key_state KEY_STATE_RELEASED; } key_debounce_cnt 0; } }在主循环中每隔5ms调用一次Key_Scan函数连续三次检测到同一电平状态才确认按键变化。这样比起delay去抖的好处是执行检测的同时主循环还能处理其他任务。按键被确认后置位key_value主循环检测到该标志后执行对应操作处理完毕及时清除。如果后续想要支持长按连续调速只需在KEY_STATE_PRESSED状态下持续统计按键保持的时钟周期数并设定触发阈值不需要新增中断资源。4.2 串口指令协议设计帧头、长度、校验项目中USART模块负责与手机APP的通信。常见方案是蓝牙模块通过串口与STM32相连APP下发指令控制风扇。串口协议不复杂但设计时需要注意帧格式的健壮性。以一段典型指令解析代码为例#define FRAME_HEAD_1 0xAA #define FRAME_HEAD_2 0x55 u8 uart_rx_buf[64]; u8 uart_rx_index 0; void USART1_IRQHandler(void) { u8 rx_data; if(USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { rx_data USART_ReceiveData(USART1); if(uart_rx_index 0 rx_data ! FRAME_HEAD_1) return; if(uart_rx_index 1 rx_data ! FRAME_HEAD_2) { uart_rx_index 0; return; } uart_rx_buf[uart_rx_index] rx_data; if(uart_rx_index 4) { u8 payload_len uart_rx_buf[2]; if(uart_rx_index payload_len 4) { Uart_ParseFrame(uart_rx_buf, payload_len); uart_rx_index 0; } } if(uart_rx_index sizeof(uart_rx_buf)) { uart_rx_index 0; } } }帧格式为帧头AA 55 长度字节 数据域 校验字节。中断里先判断帧头是否匹配再根据长度字段等待整帧数据到齐后交给Uart_ParseFrame处理。如果出现帧头错位且长度字段异常直接重置接收索引可以避免缓冲区污染。校验方式可以用累加和也可以使用CRC8取决于需求中对数据完整性的严苛程度。对于风扇控制这种低风险场景累加和已经足够。APP端指令集通常包含设置风速档位、设置摇头开关、查询当前状态、进入定时模式。每类指令用数据域的第一个字节标识例如0x01代表风速设置第二个字节才是具体值。这种协议结构的好处是便于后续扩展新增指令时只需要增加一个功能码分支不需要改动帧格式。4.3 手动模式与自动模式的优先级仲裁系统在手动和自动之间切换时需要考虑PWM占空比和传感器采样线程的关系。比较稳妥的做法是定义全局的control_mode标志位主循环按以下方式调度if(g_control_mode MODE_AUTO) { temp Temp_GetCurrent(); level Fan_GetLevel(temp); Fan_SetLevel(level); } else { Fan_SetLevel(g_manual_level); }自动模式下主循环每次执行都重新计算目标档位手动模式下档位只由按键或蓝牙指令更新。这里容易忽略的是模式切换瞬间的PWM突变。比如从自动二档切到手动一档时电机转速骤降可能导致风道噪声和机械冲击我会在Fan_SetLevel函数中加入斜坡处理每100ms调整一次占空比向目标值逼近。void Fan_SetLevel(u8 target_level) { u16 target_ccr level_to_ccr_table[target_level]; u16 current_ccr TIM_GetCompare1(TIM2); if(target_ccr current_ccr) { current_ccr 10; if(current_ccr target_ccr) current_ccr target_ccr; } else if(target_ccr current_ccr) { current_ccr - 10; if(current_ccr target_ccr) current_ccr target_ccr; } TIM_SetCompare1(TIM2, current_ccr); }level_to_ccr_table是一个数组存放每个档位对应的比较寄存器值。斜坡步长设为10意味着从一档升到三档需要几百毫秒这个速度在听觉上几乎无感同时避免电流冲击。这种方式也兼容PWM频率从20kHz到30kHz不等的电机驱动板无论电机感性负载特性如何都能平滑过渡。5. 过热保护与编译烧录排错从阈值判断到AXF文件烧录5.1 电机温度采样通道和双阈值保护逻辑过热保护模块一般选一个靠近电机的NTC温度传感器作为温度采样点。在代码里通常使用与前文相同的ADC读取流程只是采样通道不同以及标定系数不同。双阈值保护指的是当温度超过第一阈值比如75度时风扇强制提高到最高档位超过第二阈值比如85度时PWM输出直接关闭。#define MOTOR_OVER_TEMP_LEVEL_1 75.0f #define MOTOR_OVER_TEMP_LEVEL_2 85.0f void Motor_OverTemp_Check(void) { float motor_temp Motor_GetTemp(); if(motor_temp MOTOR_OVER_TEMP_LEVEL_2) { TIM_SetCompare1(TIM2, 0); g_fault_code | FAULT_OVERTEMP; return; } if(motor_temp MOTOR_OVER_TEMP_LEVEL_1) { Fan_SetLevel(3); } }进入一级保护后风扇满速运转这是合理的散热策略进入二级保护后强制停机并置位故障码。故障码会通过LED或OLED屏幕显示。保护阈值建议写成宏定义方便根据电机厂商的规格书调整。另一个隐藏细节是温度采样的频率这里不需要像环境温度那样频繁读取每两秒采样一次即可这样可以在主循环中节省ADC转换的开销。5.2 Flash烧录算法选择和下载失败排查前面提到AXF文件是Keil编译链接生成的最终可执行映像。使用J-Link烧录时会遇到Flash Download failed - Could not find device的情况一般是算法文件选错。Keil工程的魔术棒Options for Target中选择Debug选项卡点击Settings进入Flash Download界面确认Programming Algorithm列表中至少包含STM32F10x Med-density 128K Flash或64K Flash选项。如果选成High-density写入地址和扇区大小不匹配校验即失败。C8T6的Flash起始地址是0x08000000默认的烧录起始地址在Target选项卡中设置。如果代码中使用了自定义的Bootloader那么起始地址会改为0x08008000或0x08004000但这份源码看起来是裸机直接运行的起始地址保持默认即可。烧录成功后如果看到串口无输出排查方向是重映射和时钟。USART1的TX/RX默认在PA9/PA10如果使用了重映射功能但在代码中切换到了PB6/PB7对应的GPIO配置和RCC外设时钟会完全不一致。另一个常见问题是将系统时钟配置为72MHz导致波特率偏差过大。检查system_stm32f10x.c中的SYSCLK_FREQ_72MHz宏是否被正确启用如果定义的是36MHz但代码里通过USART_InitStructure.USART_BaudRate 115200来初始化串口播率误差会在正常范围内但如果直接用HSE作为时钟源且未正确配置PLL误差会肉眼可见。5.3 从AXF反推代码调试器里如何判断程序运行路径拿到SuperFanV2.axf以后使用J-Link连接开发板后进入调试模式打开Disassembly窗口可以看到反汇编代码。如果怀疑程序跑飞了可以对比PC寄存器的值是否在合法Flash区间。C8T6的合法执行区间是0x08000000到0x0800FFFF如果PC寄存器跳到0x0801xxxx就说明越界了。用J-Link的RTT Viewer或者直接使用Keil的Watch窗口观察g_sys_tick变量是否在持续增加。如果g_sys_tick不走问题大概率出在时钟配置或NVIC优先级分组上。还有一种是环境温度传感器的I2C总线锁死SCL或SDA被从设备拉低导致主循环卡在等待应答的while循环中。此时用示波器测量I2C引脚电平如果SDA持续为低而SCL有脉冲多半是传感器供电不稳导致的电平竞争。6. 让控制更顺滑PWM噪声抑制和I2C总线电容的实战取舍最后一章聊一个实际项目中容易踩、但源码注释里讲不清楚的细节PWM频率与电机可闻噪声的关系。前面用24kHz作为推荐频率但如果在样机试制时手头电机驱动板的MOSFET开关特性一般高频率反而造成开关损耗和发热。这时可以把arr调整为2999、psc改为0得到同样24kHz的PWM波但分辨率提高到3000级占空比调节精度大幅提升。代价是ARR值超过16位计数器最大值时TIM_Period的有效值会被截断所以通常不超过65535。另一个容易被忽略的点是I2C信号完整性。C8T6的I2C引脚内部有弱上拉但在硬件设计中仍然需要在PCB上外接4.7kΩ上拉电阻到3.3V。如果智能风扇的PCB走线较长温湿度传感器远离主控芯片总线电容超过400pF后标准100kHz模式下信号边沿会变缓。遇到这种情况一是在I2C初始化里降低SCL时钟频率二是把传感器摆放位置靠近主控。以下参数可作为参考总线长度上拉电阻通信速率实测表现 10cm4.7kΩ400kHz稳定10cm-30cm2.2kΩ100kHz偶发无响应 30cm1kΩ100kHz需要加缓冲器我在调试一个类似项目时温湿度传感器每十分钟丢一次数据排查到最后是传感器供电电压在电机启动时跌落超过200mV。解决方式是在传感器VCC引脚附近增加一个10μF的陶瓷电容同时在电机电源和逻辑电源之间保持完整的地平面隔离。这个处理放在源码里看不到但产品级的智能风扇项目基本都有这道滤波。PWM占空比查表方式的另一个变体是按温度线性输出而不是离散档位。在SuperFanV2.axf版本中如果看到调试助手显示的风速数字是连续变化的那说明代码走的是线性映射分支u16 ccr_value (u16)((temp - TEMP_COMFORT_LOW) * PWM_PERIOD / (TEMP_COMFORT_HIGH - TEMP_COMFORT_LOW));这种方案的优点是手感细腻缺点是在温度长时间不变的情况下风速也会恒定不变用户难以感知系统是否在运行。所以也有很多项目用反向思路风扇转速随温度缓慢上摆但每变化一档都叠加一个呼吸闪烁的LED指示让状态可视。这个项目的源码里保留了两种方式的切换入口具体参数在main.c的宏定义区可以找到。最后留一个排查建议风扇转速自动控制乱跳的先打开调试串口的实时打印观察温度值和档位切换时间戳不要在电机转速上盲调PID。大部分问题出在传感器采样噪声上把ADC采样取平均连续读取三次取中间值就能过滤掉毛刺。这套代码的框架足够完整唯一需要注意的是C8T6的Flash空间在加入全部调试打印后可能逼近64KB上限发布前要关掉不必要的格式化输出。本文还有配套的精品资源点击获取