夏天在书桌前写东西,桌面那个小风扇永远只能对着一个方向吹。人坐偏一点,风就吹不到;坐远了,又懒得伸手去调角度。后来我干脆做了个小项目:用一块51单片机,把普通USB小风扇改成了红外遥控摇头小风扇——遥控器可以开关、调速、摇头、定时,彻底解放双手。这个项目难度不高,但一次涵盖了硬件电路、电机驱动、红外通信、机械结构和状态程序设计,非常适合刚学完51单片机、正在纠结“点灯之后还能做点什么”的同学练手。
这篇文章我从方案选型一直写到最后踩过的坑,包含所有能复现的接线方式、代码骨架和调试思路。如果你手里有一块51单片机学习板、一个坏了或闲置的小风扇,大概率可以跟着做一个出来。
1. 想法怎么落地:从“手扶风扇”到“遥控摇头小风扇”
1.1 为什么拿这个项目练手
很多人学完51单片机之后,第一个反应是“下一步干什么”。流水灯太无聊,数码管钟表又看不到成就感。而这个红外摇头小风扇的好处就是:它把单片机最常见的几个外设都用上了。
我来数一下涉及的知识点:外部中断(红外接收信号输入)、定时器(PWM调速、时间测量、定时关机)、IO输出(控制电机驱动模块)、状态机编程(不同档位和摇头状态的切换)、甚至还能加EEPROM掉电记忆。一个项目把这些全部串起来,比单独学每一个例程有用得多。
再加上机械部分,风扇本身是现成的,摇头机构可以自己搭,成本不高,但完成后的“这是一个真能用的东西”的感觉很强烈。尤其夏天,放在桌边,拿遥控器一按,风对着人吹,那个成就感完全不是跑个流水灯能比的。
1.2 整体技术路线和模块拆分
我的初步方案定得很简单,先把功能列清楚:
- 遥控器开关机
- 三档调速:低、中、高
- 摇头开启/关闭
- 30/60/120分钟定时
- 风扇关机时摇头自动复位
系统其实就五大部分:主控(51单片机)、红外接收模块、风扇电机驱动、摇头电机驱动、电源。
通信方式我直接选了红外,理由很现实:成本低,一个红外接收头几块钱,家里电视遥控器就能拿来当发射端,不需要再买遥控模块。蓝牙或者WiFi虽然听上去高级,但配对、协议栈、供电都会增加一堆额外的工作量,对一个“先跑起来”的项目来说没必要。
摇头机构的选择我在后面专门讲,因为这里有个很有意思的取舍:看起来最简单的舵机方案,实际做出来反而不是最好的。这是这个项目最容易踩的坑,值得单独说。
2. 硬件搭建:电路设计和接线避坑
2.1 核心器件清单和供电方案
我实际用到的器件清单如下,都是很常规的型号,淘宝或者积攒的开发板零件盒里基本都能翻出来。
| 模块 | 常见型号/规格 | 作用 | 关键注意点 |
|---|---|---|---|
| 主控 | 51单片机开发板或最小系统板 | 红外解码、逻辑控制、PWM输出 | 选带内部EEPROM的型号更方便做掉电记忆 |
| 红外接收头 | HS0038B / VS1838B | 接收遥控器38kHz红外信号 | 电源端并联电容,否则距离骤减 |
| H桥驱动模块 | L298N / DRV8833 双H桥模块 | 驱动风扇调速和摇头正反转 | 输入信号必须共地,PWM脚和方向脚分开 |
| 风扇电机 | 5V直流有刷电机(拆旧风扇) | 带动扇叶 | 启动电流远超额定电流,电源要留余量 |
| 摇头电机 | 5V小型减速电机 | 带动摇头机构 | 需要正反转,不能只用单路开关 |
| 限位开关 | 常开微动开关或干簧管 | 检测摇头到端点 | 接线要上拉,安装位置决定摇头角度 |
| 电源 | USB 5V 2A充电器或18650电池+升压板 | 整机供电 | 前期调试建议直接用USB,电池版本后面再说 |
供电是整个项目里最容易埋雷的地方。我一开始觉得5V USB给一个小风扇供电绰绰有余,实际一量就发现问题:风扇电机和摇头电机同时启动的那一瞬间,电流能冲到1.5A以上,USB电源的电压会被瞬间拉下来,单片机直接复位。
所以后期我采用的是“一个5V节点,但加足电容”的办法:电源入口并一个大电解电容(470uF到1000uF),电机两端各并一个104陶瓷电容,主控部分的供电线单独走线,不要和电机线缠在一起。如果条件允许,最好主控和电机分开供电,只在电池负极做单点共地。这样电机电流波动不会把主控电压拽垮。
2.2 红外接收头的接法
红外接收头看起来只有三个引脚,接错的人可不少。以HS0038B为例,引脚排列是:供电正、信号输出、地,但也有个别型号引脚定义不同,接线前一定要查数据手册或者用万用表确认。
信号输出脚可以直接接单片机的INT0(P3.2)引脚,利用外部中断做脉冲宽度测量。红外接收头输出的是解调后的数字电平:空闲时是高电平,收到38kHz载波时输出低电平,所以我们其实是在测量一段低电平和一段高电平的组合宽度。
这里要特别提醒:红外接收头对电源纹波非常敏感。我第一次接的时候,直接把接收头电源接到了电机驱动模块旁边的5V走线上,结果遥控距离只有十几厘米,还经常丢码。后来在接收头电源正负极之间并联了一个100uF电解电容和一个104陶瓷电容,遥控距离立刻恢复正常,站在两三米外都能稳定遥控。
如果你手上有示波器或者逻辑分析仪,建议把探头夹在接收头信号脚上看一眼波形。大部分“遥控不识别”问题,其实在波形上一眼就能看出来:要么信号幅度不够,要么波形毛刺太多,根本到不了解码那一步。
2.3 电机驱动和PWM接线
小风扇电机属于直流有刷电机,调速方式是改变平均电压,也就是PWM。摇头电机需要正反转才能实现来回摆头,所以最简单的方案是直接用双H桥驱动模块,一路接风扇电机,一路接摇头电机。
接线逻辑不要怕复杂,按模块分清楚就很简单:
- H桥A路输出接风扇电机:PWM输入接单片机的PWM输出脚,方向引脚固定为高电平或不用。
- H桥B路输出接摇头电机:两个方向引脚分别接两个IO口,程序里控制正转、反转、停止。
很多L298N模块上的ENA、ENB引脚需要接高电平才让对应通道工作。如果你发现电机不转,先查使能脚是不是被拉到3.3V或5V了。驱动模块和单片机之间必须共地,不然输入信号完全乱套。
风扇电机两端我建议直接并联一个104陶瓷电容,可以吸收换向火花产生的干扰,对单片机复位问题有很大缓解。摇头电机如果离主控近,也照做。电源线尽量粗一点,别用杜邦线飞几十厘米,电机启动瞬间压降会让你怀疑人生。
3. 摇头机构:机械结构和控制逻辑
3.1 三种摇头方案实测对比
摇头是这个项目里最有意思的部分。我一开始想的很简单:用舵机拉着风扇摆头不就行了?后来实际试了才明白,事情没那么简单。
| 方案 | 成本 | 控制难度 | 噪音 | 寿命 | 实际体验 |
|---|---|---|---|---|---|
| 舵机(如SG90) | 低 | 低 | 抖动明显 | 一般 | 小扭矩,带风扇头后易抖,摆动速度不自然 |
| 步进电机(28BYJ-48) | 低 | 中 | 低速噪音大 | 较长 | 能精确控制角度,但转速慢、声音难听 |
| 减速电机+限位开关 | 低 | 中低 | 低 | 较长 | 摆动自然,结构简单,成本低 |
舵机方案的坑在于:舵机是通过PWM信号控制角度的,但普通廉价舵机在带负载的情况下会不断修正角度,表现出来就是“咔咔咔”的抖动。风扇头本身不重,可一旦转到某个角度,风的反推力变化会让舵机很难受,声音会很吵。
步进电机方案我也试了,28BYJ-48这种五线四相步进电机控制逻辑不复杂,但光头转速本来就不高,带动风扇头时只能用低速,发出那种“滋滋”的声音,放在桌面上非常烦躁。
最后我选了“减速电机+曲柄连杆+两个限位开关”的方案。原理跟家里的电风扇摇头机构几乎一样:一个小减速电机转动一个偏心曲柄,连杆带动固定在转轴上的风扇底座来回摆动。电机本身不需要精确角度,只是在两个端点之间来回转,检测到端点就反转方向。
3.2 减速电机和曲柄连杆的安装要点
机械部分的制作我用了3D打印支架,没有条件的话用亚克力板、甚至硬纸板加热熔胶也能做出来,只是强度差点。尽量保证“风扇头固定在转轴上,转轴通过轴承或铜套固定在底座上”,这样摩擦小、不晃动。
曲柄连杆的核心尺寸有两个:曲柄长度决定摆动角度,连杆长度决定运动平顺性。我这里参考常见做法:曲柄半径10mm左右,连杆长度45mm左右,摇头角度大概60度,人坐在桌前完全够用。角度大了会撞到空间限制,角度小了又没意义。
装配时有两个坑:
- 连杆和曲柄的关节要留一点间隙,但不能太大。间隙太大,每个来回都会“哐当”一声,整机异响来源就在这里。
- 要避免连杆运动到接近直线位置,否则会出现“死点”,电机明明在转,风扇头却卡着不动,可能一直堵转到发热。
限位开关不是装在电机上,而是装在风扇底座转轴的左右两端。风扇转到最左边,转轴上的挡片压住左边开关;转到最右边,压住右边开关。程序读到开关信号后,让电机反转往另一边走。
3.3 限位逻辑与堵转保护
限位开关的选择上,我用过微动开关和干簧管。微动开关便宜、好买,但有机械触点,长期反复按压会疲劳;干簧管配合转轴上的小磁铁更耐用,没有物理接触,不过安装位置要精确,磁铁要离得够近。
程序里的逻辑不复杂,但一定要加堵转保护。我的做法是记录电机启动的时间,如果超过3秒还没读到对应的限位信号,就强制断电,并设置一个故障标志。因为一旦限位开关失灵,电机就会一直堵转,电流飙升,不用几分钟就烫手。
另外,摇头到端点之后,不要直接让电机硬停在那个位置。正确流程是:读到左限位信号后,电机保持反转一小段时间(比如200ms),让风扇头稍微离开限位位置,再停下。这样下次启动时不会因为刚好压在限位上而直接触发反转,也能减少机械冲击。
4. 程序实现:红外解码与状态机
4.1 NEC协议和中断解码思路
红外遥控器大多使用NEC协议。我用的遥控器也是这种,协议特点很简单:
- 引导码:9ms载波 + 4.5ms空闲
- 逻辑0:560us载波 + 560us空闲
- 逻辑1:560us载波 + 1.69ms空闲
- 重复码:9ms载波 + 2.25ms空闲 + 560us载波
红外接收头输出的是反相之后的电平:空闲为高,收到载波为低。所以我的解码思路是:用单片机的外部中断INT0监视接收头输出脚,每次下降沿触发中断,在中断里读取定时器0的计数值,得到两个下降沿之间的时间宽度,再按宽度判断是什么码。
为什么用下降沿间隔而不是直接测量高低电平宽度?因为每次数据位结束时,下一个数据位的低电平开始也是一个下降沿,两次下降沿之间的总时间恰好等于“当前位的低电平时间+高电平时间”,逻辑0大约是1120us,逻辑1大约是2250us,区分度很高。实测下来比逐段测量高电平宽度更稳定。
核心代码骨架大概是这样:
void Timer0_Init(void) { TMOD &= 0xF0; TMOD |= 0x01; // 定时器0,16位模式 TH0 = 0; TL0 = 0; TR0 = 1; } void IR_Init(void) { IT0 = 1; // INT0下降沿触发 EX0 = 1; // 开启外部中断 EA = 1; }在12MHz晶振、经典51的12T模式下,定时器每1us计数一次,16位最多数到65535us,而NEC协议最长的一段(引导码)也不到14ms,完全够用,不需要额外处理溢出。
中断服务函数里做时间测量:
void IR_ISR(void) interrupt 0 { unsigned int us = (TH0 << 8) | TL0; TH0 = 0; TL0 = 0; HandleIRWidth(us); }然后根据测量到的时间宽度判断是引导码、重复码、逻辑0还是逻辑1。因为NEC是LSB先发,所以先收到的bit放最低位:
static unsigned char bitCount = 0; static unsigned long codeValue = 0; static unsigned char state = 0; void HandleIRWidth(unsigned int us) { if (us > 10000 && us < 16000) { state = 1; bitCount = 0; codeValue = 0; return; } if (us > 10000 && us < 13000) { gRepeatFlag = 1; // 重复码,可用于按键长按 return; } if (state == 1 && bitCount < 32) { if (us > 2200 && us < 2800) { codeValue |= (1UL << bitCount); bitCount++; } else if (us > 900 && us < 1500) { bitCount++; } else { state = 0; bitCount = 0; return; } if (bitCount == 32) { gIrCode = codeValue; gIrReady = 1; state = 0; bitCount = 0; } } }这里的时间窗口我留得比较宽。实际用示波器测量时,遥控器晶振误差会让脉冲宽度漂一点,窗口太窄容易误判。
4.2 主循环状态机和PWM调速
程序主体不要写成一堆if散在外层,我习惯用一个简单状态机来管风扇状态:
typedef enum { OFF, LOW, MID, HIGH } FanState_t; FanState_t fan = OFF; bit swingOn = 0;主循环里只是不断查标志位,然后更新输出:
void main(void) { SystemInit(); while (1) { if (gIrReady) { HandleIrCmd(gIrCode); gIrReady = 0; } if (swingOn && fan != OFF) { SwingTask(); } else { StopHeadMotor(); } } }遥控命令处理的部分,把命令码对应到具体功能。以我用的遥控器为例,通常有独立的电源键、风速键、摇头键、定时键。命令码的具体数值取决于遥控器,可以先写一个临时代码,在串口打印出来,记录下每个按键对应的码值,再填进映射表里。
void HandleIrCmd(unsigned long code) { unsigned char addr = (unsigned char)(code & 0xFF); unsigned char addrInv = (unsigned char)((code >> 8) & 0xFF); unsigned char cmd = (unsigned char)((code >> 16) & 0xFF); unsigned char cmdInv = (unsigned char)((code >> 24) & 0xFF); if ((addr + addrInv != 0xFF) || (cmd + cmdInv != 0xFF)) return; switch (cmd) { case IR_POWER: if (fan == OFF) fan = LOW; else { fan = OFF; swingOn = 0; } break; case IR_SPEED: if (fan == LOW) fan = MID; else if (fan == MID) fan = HIGH; else if (fan == HIGH) fan = LOW; break; case IR_SWING: if (fan != OFF) swingOn = !swingOn; break; default: break; } }PWM调速我单独用一个定时器产生中断,例如定时器1,中断里维护一个计数变量和一个占空比变量:
void Timer1_ISR(void) interrupt 3 { static unsigned char cnt = 0; cnt++; if (cnt >= 100) cnt = 0; FAN_PWM = (cnt < pwmDuty) ? 1 : 0; }这样相当于100Hz的PWM,低档占空比设成40,中档70,高档直接100。如果你嫌电机啸叫明显,可以把定时器周期改短,把频率提到1kHz以上,代价是中断更频繁,对红外接收的解码稳定性会有一点影响,所以我在实际项目里用了100Hz再配合电机两端电容,效果完全能接受。
4.3 定时、看门狗和掉电记忆
定时功能一开始很容易写得乱。我的习惯是:用一个定时器产生1ms时基,在主循环里累加秒数,再按设定时间判断是否到点。
设置定时档位的时候,把所有正常工作都保持原样,只在主循环里判断:
if (timerSet > 0) { timeCount++; if (timeCount >= timerSet * 1000) { fan = OFF; swingOn = 0; timerSet = 0; } }如果风扇关机了,定时也要清零,不然下次开机可能莫名其妙又自动关机。这个细节我调试时踩过,莫名觉得“单片机自己乱来”,其实只是定时状态没有随关机一起复位。
看门狗要不要加?我的建议是加。因为红外解码有时候会遇到异常波形,程序如果没有正确处理可能卡死在中断里。开了单片机内部看门狗之后,主循环里定期喂狗,一旦程序跑飞,几毫秒后自动复位,比手动按复位键省心得多。
掉电记忆是加分项。STC系列单片机的内部EEPROM可以直接用,掉电前把当前档位和摇头状态存进去,上电后再读出来。这样拔电重启之后,风扇还是保持上次的设置,不会每次开机都要重新调一遍。
5. 调试排错:那些让人抓狂的问题
5.1 红外遥控距离短到怀疑人生
第一个坑是红外接收距离。刚焊完电路时,遥控器必须凑到红外接收头20cm以内才有反应,远一点就完全没响应。我当时还以为是接收头坏了,换了两个还是一样。
排查思路其实不复杂:先量电压,接收头供电脚和地之间有没有足够稳定的5V;再用逻辑分析仪看信号脚波形;最后检查一下接收头电源脚旁边有没有滤波电容。我的问题就出在第三步——接收头电源直接并联在电机驱动模块的5V上,纹波大得根本没法解码。加了一个100uF和104电容后,遥控距离直接到了六七米。
还有一次是遥控器本身的问题。家里电视遥控器的晶振频率偏得有点多,解码偶尔成功偶尔失败,换了另一把遥控器就非常稳定。如果你发现某个遥控器“很挑”,先别急着怀疑代码,换个遥控器试试。
5.2 电机一启动单片机就复位
这个现象非常经典:风扇电机启动瞬间,单片机整个重启,数码管或指示灯闪一下又恢复。
原因是电机启动电流非常大,瞬间把电源电压拉到了单片机复位电压以下。电源线越长,线阻越大,压降越明显。我的解决步骤是:
- 第一步:给总电源加一个1000uF大电解电容,相当于给整个系统一个“蓄水池”。
- 第二步:电机两端并联104陶瓷电容,吸收换向火花。
- 第三步:把电机电源线和主控电源线分开走,从电源入口单点分开。
- 第四步:实在不行,用光耦或者继电器隔离电机回路,强电和弱电彻底分离。
我的项目做到第三步就解决了。如果你用的还是那种质量很差的USB供电线,线阻高,这里也会成为压降来源,换根粗短线效果立竿见影。
5.3 摇头卡死、堵转发热
摇头机构的堵转问题在初期几乎每天都能遇到。现象是:风扇头转到最左边,程序没有收到限位信号,减速电机一直嗡嗡地想转但转不动,过一会儿摸电机外壳明显发烫。
根因基本都是限位开关的安装位置不对。我一开始把限位开关装得太靠外,转轴上的挡片压不到开关的弹片,导致开关永远不被触发。后来调整了位置,把挡片加长了一点,问题就消失了。
软件层面的堵转保护也必须有。我的做法是:记录本次摇头启动时间,如果在3秒内都没有收到端点信号,立刻停机并进入错误状态,等遥控器再给一个“摇头”指令才重新恢复。自从加了这道保护,即使限位开关坏了也不会烧电机。
5.4 整机共振和异响
整机异响在摇头风扇上特别明显,因为摇头机构在运动,任何一点间隙都会被放大成“哒哒哒”的声音。
我处理过的异响来源有四个:扇叶动平衡差、网罩松动、连杆关节间隙过大、底座太轻。扇叶拆下来换了一个,或者在扇叶背面贴一小块橡皮泥做配重;网罩螺丝全部拧紧;连杆关节处加了尼龙垫片;底座压了一个配重块。处理完之后,整机噪音从“让人烦躁”变成了“可以接受”。
这个环节没有捷径,只能一个位置一个位置排查。我的习惯是先用耳朵听,再用手指触碰各个部件,摸到哪里振动最明显,问题基本就在那里。
6. 成品效果、参数与可复用的经验
6.1 实测参数和日常使用感受
项目跑稳定之后,我简单测了一下整机参数。遥控距离实测在6到8米,隔一堵墙会明显变差,但正常使用足够;三档调速里,低档适合睡觉,中档办公,高档风力很大但噪音也明显。整机用USB 5V 2A供电跑中档加摇头,大概0.4到0.5A,如果用18650锂电池加升压板,中档摇头模式能跑三个多小时,够一下午使用了。
日常用下来的真实感受是:定时功能最实用。夏天不想让风扇吹一整晚,设个60分钟,到点自动关,比买那种几十块钱的定时插座还顺手。摇头功能也很自然,减速电机带动曲柄连杆的摆动速度和家用电风扇几乎一样,不会像舵机那样一下一下地顿。
6.2 后续升级方向和模块化思路
这个项目做完之后,我留了两个扩展方向。
一个是换一个带硬PWM输出的增强型51单片机,把软件PWM改成硬件PWM,电机啸叫能进一步降低,CPU负载也更小。另一个是加一个DS18B20温度传感器,根据环境温度自动切档:温度低用低档,温度高直接高档,把风扇变成“半自动”设备。
如果你想继续玩,还可以加离线语音识别模块,喊一声就能开关风扇;加一个OLED屏显示当前档位和剩余时间;或者用带充电管理的18650电池模块,让整机彻底摆脱USB线。每一步改动都不破坏原来的主循环,因为在结构上我已经把红外解码、摇头控制、PWM调速拆成了独立模块,后面加功能只是加一个模块的事。
我个人体会是:这个项目最值得参考的不是某一段代码,而是“先选好方案,再把方案拆成模块,最后逐个调试”的节奏。很多同学一上来就写程序,结果硬件没搭稳,出问题之后分不清是程序错还是电路错。我的做法是先把硬件调到电机动起来、红外能解码,再写上层逻辑,这样每一层都有明确验证点,排查问题快得多。如果让我再做一版,我会直接用洞洞板代替杜邦线和面包板,把接线做成焊接固定的,整机会再可靠一个档次。