简介:面向电子工程初学者与51单片机爱好者,这份资料以C51语言完整实现电风扇模拟程序,涵盖PWM脉宽调制转速调节、定时器中断控制、按键交互与模块化软件设计,帮助读者掌握从电机驱动到软硬件联调的关键技能。压缩包共12个文件,包含Keil工程源文件(.c)、可烧录的.hex固件、编译中间文件(.obj/.lst/.m51/.lnp)及备份配置(.bak/.uvopt/.uvproj)等,整体仅29KB,资料轻量紧凑。已有1503人学习下载,适合作为课程设计或入门实践的参考。通过阅读源码与工程结构,可直观学习定时器初始化、PWM输出、中断服务函数及硬件接口的连接思路,对理解51单片机实际开发流程有切实帮助。
1. 51单片机风扇模拟:先分清“模拟”和“驱动”的关系
“51的风扇模拟_单片机电风扇_51单片机_”这个标题,在很多课程设计和入门项目里已经成为一套固定套路:用一块51单片机主板,接上三极管、LED、小电机、按键和数码管,把电风扇最核心的“开关、调速、定时、温控”这四件事用程序表现出来。它不让单片机去直接控制220V交流风扇,而是用小直流电机或者LED做负载,把“转速”转换成占空比这个可量化、可测的变量。PWM占空比就是风扇的“虚拟转速”。选这个题目的好处很明显:不碰高压、不需要专门驱动板,但能一次用到按键消抖、定时器中断、PWM输出、数码管显示和温度反馈,恰好把这些散装知识点串成一条完整的软硬件链路。这篇就按我从硬件到调参的完整顺序讲,每一步都是能直接复刻的写法。
2. 硬件搭建:51单片机风扇模拟的最小驱动电路
2.1 先算引脚电流,再决定用三极管还是ULN2003
经典51单片机的P1、P2、P3端口在输出高电平时,拉电流能力很弱,一般只有0.5mA左右;输出低电平时,灌电流能力大约5~25mA。这个数据直接决定了“引脚直接接风扇”这个方案不成立。即使是几十毫安的小直流电机,启动瞬间电流也可能冲到两三百毫安,引脚会直接被拖垮。
最常见的替换方案是加一个NPN三极管做低端驱动,比如S8050。电路接法是:电机正极接5V,负极接三极管集电极,发射极接地,单片机引脚通过1kΩ电阻接基极。引脚输出高电平时三极管导通,电机得电;输出低电平时电机停止。这就是“高电平有效”的开关控制。
如果负载不止一路,或者要切换更高电流的设备,我会直接换ULN2003。它是达林顿阵列,里面带续流二极管,特别适合继电器、小电机、步进电机这类感性负载。对于单路风扇模拟,一个S8050加一个1N4148续流二极管就已足够,成本不到两毛钱。
| 参数 | 选型 | 说明 |
|---|---|---|
| 开关三极管 | S8050 | NPN,放大倍数约100,连续电流500mA |
| 基极电阻 | 1kΩ | 限制基极电流在4mA左右 |
| 续流二极管 | 1N4148 | 反向并联在电机两端,吸收关断瞬间的感应电动势 |
| 负载 | 5V直流小电机 | 额定电流100mA左右,适合模拟风扇 |
2.2 独立按键和数码管的接线,别让P0裸奔
风扇模拟的操作面板一般就是几个独立按键加一个数码管。独立按键接法比矩阵键盘简单:每个按键一端接单片机引脚,另一端接GND,引脚内部或外部带上拉电阻。51单片机P3口有内部弱上拉,直接按下接地就能读到低电平。按键数量少的时候,完全没必要用矩阵键盘的扫描逻辑,写起来也更容易排错。
数码管就要小心了。AT89C51和STC89C52的P0端口是开漏输出,内部没有上拉电阻。直接拿P0接共阴数码管的段选,会导致高电平输出不正常,数码管亮度极低。我在Proteus仿真里看到的经验是,不加上拉的P0驱动数码管,段码高电平会被拉到1V左右,数字显示模糊。
所以我一般把数码管的段选放在P0,再外接一个10kΩ排阻到VCC;位选用P2.1、P2.2、P2.3来控制三极管,这样扫描刷新时不会因为端口驱动能力不足导致显示闪烁。
| 单片机引脚 | 外部连接 | 作用 |
|---|---|---|
| P2.0 | 1kΩ电阻到S8050基极 | 驱动电机/LED风扇 |
| P3.0 | 独立按键到GND | 挡位切换 |
| P3.1 | 独立按键到GND | 定时开关 |
| P0.0-P0.6 | 10kΩ排阻到VCC,再接到共阴数码管 | 段选输出 |
| P2.1 | 数码管位选三极管基极 | 位选1 |
| P2.4-P2.6 | 三个LED串联1kΩ电阻到GND | 显示1/2/3挡 |
2.3 第一版可跑代码:按键切换挡位并点亮LED
先不写PWM,把挡位的状态机跑通,这是后面所有功能的地基。下面这段代码实现的是:P3.0每按一次,挡位从0到3循环一次,三只LED分别表示低、中、高挡,P2.0同步输出高电平给三极管。
#include <REGX52.H> sbit FAN = P2^0; sbit KEY_SW = P3^0; sbit LED1 = P2^4; sbit LED2 = P2^5; sbit LED3 = P2^6; unsigned char level = 0; // 0停止,1低速,2中速,3高速 void delay_ms(unsigned int ms) { unsigned int i, j; for (i = 0; i < ms; i++) for (j = 0; j < 120; j++); } void show_level(void) { LED1 = (level >= 1) ? 1 : 0; LED2 = (level >= 2) ? 1 : 0; LED3 = (level >= 3) ? 1 : 0; if (level == 0) FAN = 0; else FAN = 1; } void main(void) { while (1) { if (KEY_SW == 0) { delay_ms(10); if (KEY_SW == 0) { while (KEY_SW == 0); // 等待按键释放 level = (level + 1) % 4; show_level(); } } } }代码里有两个关键动作:第一个是delay_ms(10)做软件消抖,避免按键按下瞬间的抖动被识别成多次触发;第二个是while (KEY_SW == 0)等待释放,防止按住按键时反复加挡。灯和电机共用同一个level条件,意味着挡位是全局状态,后面接PWM、接温度控制时,改的都是同一个状态变量。
3. 定时器PWM调速:51单片机风扇模拟的核心参数
3.1 为什么不用 delay 做调速,要用定时器中断
到这一步,很多新手会试着一个delay_ms改变电机通电时间,比如“开50ms、关50ms”来模拟一半转速。这条路在纯顺序程序里走着走着就崩了:按下按键时刚好在delay里,响应会被拖到几十毫秒之后;如果后面再接温度检测和数码管刷新,主循环会被多个延时函数切成碎片。
正确做法是用定时器中断。51单片机进入定时器中断后,每到一个固定时间点就执行一次计数和比较,PWM的周期在时间轴上非常稳定,完全不受主循环里按键、显示代码的干扰。经典51核没有自带的硬件PWM模块,所以模拟风扇最常见、也最通用的方案就是“定时器中断 + 软件翻转引脚”。
3.2 定时器初值计算和四挡PWM映射表
以12MHz晶振为例,标准51的机器周期是12 / 12MHz = 1us。我让定时器0工作在模式1,也就是16位定时器,每100us触发一次中断,PWM周期用100个中断组成,这样PWM频率刚好是100Hz。迪车电机在100Hz下转动会比较平稳,不会像几十Hz那样明显闪烁。
定时器的初值计算方法是:
重载值 = 65536 - 100 = 65436 = 0xFF9C所以中断服务函数里重新赋值为TH0 = 0xFF; TL0 = 0x9C;。如果你换成了11.0592MHz晶振,机器周期变成12 / 11.0592MHz ≈ 1.085us,同样定100us的重载值就不是0xFF9C,必须重新计算。这是很多人移植代码后PWM频率不对的头号原因。
| 挡位 | pwm_count | 占空比 | 模拟效果 |
|---|---|---|---|
| 停止 | 0 | 0% | 电机不转 |
| 低挡 | 25 | 25% | 转速低 |
| 中挡 | 50 | 50% | 转速中等 |
| 高挡 | 75 | 75% | 转速较高 |
| 全速 | 100 | 100% | 电机全速 |
这里的pwm_count是PWM周期内高电平持续的中断次数。比如pwm_count = 25,代表每100个中断里前25个输出高、后75个输出低。占空比含义直接,逻辑也容易扩展。
3.3 Timer0中断产生PWM的C代码
下面这段代码可以直接跑在一个最简系统上,P2.0接三极管驱动电机,程序会先低速转2秒,再高速转2秒。
#include <REGX52.H> #define PWM_PERIOD 100 sbit FAN = P2^0; unsigned char pwm_count = 0; // 当前占空比计数 unsigned char cnt = 0; // 中断计数器,0~99 void delay_ms(unsigned int ms) { unsigned int i, j; for (i = 0; i < ms; i++) for (j = 0; j < 120; j++); } void Timer0_Init(void) { TMOD = 0x01; // 定时器0,16位模式 TH0 = 0xFF; TL0 = 0x9C; // 100us溢出 ET0 = 1; // 开启定时器0中断 EA = 1; // 开启总中断 TR0 = 1; // 启动定时器 } void Timer0_ISR(void) interrupt 1 using 1 { TH0 = 0xFF; TL0 = 0x9C; // 重装初值 cnt++; // 中断次数累加 if (cnt >= PWM_PERIOD) // 满100次为一个PWM周期 cnt = 0; if (cnt < pwm_count) // 前pwm_count个中断输出高 FAN = 1; else FAN = 0; } void main(void) { Timer0_Init(); while (1) { pwm_count = 25; // 低挡:25%占空比 delay_ms(2000); pwm_count = 75; // 高挡:75%占空比 delay_ms(2000); } }中断服务函数里的逻辑可以这样理解:cnt从0数到99,当cnt小于pwm_count时输出高电平,否则输出低电平。pwm_count=25时,一个周期内前25次中断为高、后75次为低,所以占空比是25%。pwm_count是全局变量,主循环可以在任意时刻修改它,下一个PWM周期立刻生效。注意进入中断后必须先手动重装TH0/TL0,否则定时器会从0开始继续计数,导致中断间隔明显不稳定;12MHz晶振下100us一次中断,加上中断入口的压栈和跳转开销,实际频率会略低于理论值,但对风扇模拟来说视觉和手感上已经足够。
4. 温控风扇设计:51单片机风扇模拟的闭环逻辑
4.1 温度采集用DS18B20还是电位器+ADC
真正做“基于51单片机温控风扇设计 完整源码 原理图”时,温度传感器通常是DS18B20。DS18B20的好处是单总线协议,只占一个数据引脚,测温范围覆盖日常环境,0.5℃精度对风扇控制完全够用。缺点是时序要求严,代码里要写严格的延时操作,读一次温度有几十毫秒的阻塞时间。我的建议是:如果主循环里还要做数码管刷新和按键处理,就不要在中断里读DS18B20,放到主循环里每隔500ms读一次就行。
如果你手边没有DS18B20,还想先在Proteus里验证逻辑,更快的方案是“电位器+ADC0832”。ADC0832是8位串行ADC,把电位器输出电压转换成0~255的数字量,这个数字量直接当作“模拟温度”。不需要看懂传感器时序,就能先把温控逻辑跑通,以后再换DS18B20也只是替换数据来源而已。
4.2 温度区间到占空比的映射表
温控风扇不是温度越高转速越快就完了,必须设置死区。死区的作用是防止温度在阈值边缘波动时,风扇在相邻两个挡位之间反复切换,比如在24.9℃和25.1℃之间来回跳。实际工程里我会在代码里保留一个带滞回的控制逻辑,但先把最基础的映射关系写明。
| 温度范围 | pwm_count | 挡位效果 |
|---|---|---|
| 低于24℃ | 0 | 风扇停止 |
| 24℃~30℃ | 25 | 低挡慢转 |
| 30℃~36℃ | 50 | 中挡 |
| 36℃~42℃ | 75 | 高挡 |
| 大于42℃ | 100 | 全速运行 |
这个表的边界值可以做“左闭右开”处理:温度等于30℃时进入下一挡,等于36℃时再跳一挡。如果温度传感器分辨率是0.5℃,实际波动会比这个粒度大,所以真正落地时还要加一个“当前温度与上次挡位变化相距至少1℃”的约束,否则噪声会导致转速抖动。
4.3 把温度映射函数套进PWM代码
这里给出温控部分的骨架,它和上一章的PWM定时器代码无缝衔接。
unsigned char map_temp_to_pwm(unsigned char temp) { if (temp < 24) return 0; if (temp < 30) return 25; if (temp < 36) return 50; if (temp < 42) return 75; return 100; } unsigned char get_temperature(void) { // 实际项目里在这里读取DS18B20温度,返回整数摄氏度 // 参考电路:DS18B20的DQ接P3.7,4.7kΩ上拉到VCC return 0; } void main(void) { unsigned char temp; Timer0_Init(); while (1) { temp = get_temperature(); pwm_count = map_temp_to_pwm(temp); // 这里可以再调用数码管显示temp和当前挡位 delay_ms(500); } }代码里map_temp_to_pwm接收的是整数温度,映射表里的边界值都用整型判断,移植到不同传感器时只需要修改get_temperature一个函数。PWM中断服务函数还是上一章那套,pwm_count被主循环周期性刷新即可。需要注意的是,get_temperature如果内部有等待DS18B20的延时,主循环一次循环耗时就会变长,但PWM继续由中断维持,所以风扇不会因为读温度而停顿。这就是把PWM放在定时器里最大的收益:主循环DEBUG再乱,输出波形依然独立、稳定。
5. Proteus仿真与调参:51单片机风扇模拟的排错技巧
5.1 Keil5工程和电路晶振必须一致
把工程从开发板搬到Proteus仿真时,最常见的坑是“Keil5里设置好了芯片型号,电路里晶振频率却随便填”。Proteus里的AT89C51默认晶振是12MHz,但如果你在电路上双击芯片改了频率,就必须和Keil代码里定时器初值使用的频率保持一致。51单片机的定时器计数依靠外部晶振,晶振变了,机器周期就变,PWM实际频率也会跟着变。
我一般会这样处理:Proteus里用12MHz,Keil工程代码里也按12MHz计算初值;如果板子实际是11.0592MHz晶振,就把代码里的定时器初值整体重新算,而不是拿着同一套0xFF9C去硬套。另一个容易被忽略的是EA引脚,AT89C51的31脚要接到VCC,否则单片机会从外部程序存储器取指令,仿真里表现就是程序不运行。很多人烧录后没反应,先查这个引脚,再查晶振。
5.2 用虚拟示波器看PWM占空比
在Proteus仿真51单片机时,最好别只靠LED亮度判断转速。LED对占空比变化的响应是线性的,但人眼对亮度差异不敏感,25%和50%看起来可能差不多。正确的验证方法是打开Proteus左侧的虚拟仪器,把Virtual Oscilloscope接到P2.0上。运行后你会看到一条方波,把时间单位切到每格10ms左右,一个完整周期大约10ms,和100Hz对应。
这时调整pwm_count,观察示波器波形中高电平的宽度。pwm_count=25时,高电平大约占2.5ms;改为75时,高电平大约占7.5ms。如果高电平宽度和设定值差很多,回查定时器初值;如果波形上高电平部分有毛刺,检查中断服务函数里是否有太长语句,比如delay或数码管刷新误入中断。
5.3 没有示波器时,用万用表电压反推占空比
你手边可能没有示波器,只有万用表,这照样能验证PWM占空比。PWM输出的平均值近似等于“高电平电压 × 占空比”。对于5V供电的单片机,25%占空比时P2.0的平均电压大约1.25V,50%占空比大约2.5V,75%占空比大约3.75V。用万用表的直流电压挡去量P2.0对GND的电压,读数稳定后就可以反推占空比是否准确。
注意这个技巧只适合占空比较低的波形,如果同时驱动电机和LED,电机引起的电压纹波会让读数不稳。我实际操作时会先把负载断开,直接量引脚空载电压,确认PWM参数正确后再接电机。这一步能快速区分“程序问题”和“驱动电路问题”,是风扇模拟项目里性价比最高的调试手法。
本文还有配套的精品资源,点击获取