
刚接触Arduino那会儿我听人讲PWM控制直流电机风扇第一反应就是“这不就是调小电压吗用个电阻分压就能搞定”结果按这个思路搭完电路风扇是转了但转速不均匀、电机嗡嗡叫最后还烧了一个驱动管。后来认真查资料才弄明白PWM控制直流电机风扇核心根本不是调电压大小而是控制单位时间内通电和断电的比例。这篇文章就把这条完整链路从头捋一遍包括PWM为什么能让风扇变速、驱动电路怎么选、Arduino代码怎么写、我实际调试中遇到的“飞车”故障以及最后怎么加一层温控闭环让它变得更实用。目标是给正在学Arduino、想做智能小车或者自制供电风扇的人一个可以直接抄作业的参考方案。1. 先搞懂PWM凭什么能让风扇变速1.1 “调电压”这个误区害人不浅我在不少工坊和群里看到新人提问说的都是“想降速所以把电压调低”。这个说法表面看没错风扇确实在低电压下转得慢但“调电压”和“PWM调速”完全是两条技术路线。PWM的全称是 Pulse Width Modulation中文叫脉冲宽度调制。它输出的信号在时间轴上不是一条平稳的直线而是一连串等幅、等周期的方波。方波只有两个状态高电平比如5V或12V和低电平0V。我们改变的是高电平在一个周期内占的时间比例这个比例就是占空比。举个具体例子一个周期是10ms如果高电平占8ms、低电平占2ms占空比就是80%。风扇在每10ms里只有8ms通着电虽然断电的2ms里它在靠惯性继续转但整体感受到的“平均电压”就是电源电压乘以占空比。12V电压配上80%的占空比风扇感受到的平均电压大约是9.6V转速自然比全压12V低。这里有个关键点风扇或者任何直流电机的机械惯性足够大它的转速不会跟着1Hz、2Hz的开关频率上下抖动而是会稳定在一个与平均电压对应的转速上。这就是PWM能调速的物理基础——不是通过“减小电压”而是通过“骑行在开关波形上”让负载感受到一个虚拟的平均功率。1.2 占空比和频率两个参数要分清新手最容易混淆的就是“占空比”和“频率”的关系。简单说占空比决定“力”的大小也就是决定电机平均电压最终决定转速。频率决定“开关切换的快慢”它影响电机的噪音和平稳度。Arduino Uno的analogWrite()默认PWM频率并不高5、6号引脚大约980Hz3、9、10、11号引脚大约490Hz。490Hz这个频率恰好落在人耳听觉范围内20Hz-20kHz所以电机里会发出刺耳的“滋滋”声尤其是空载风扇转起来的时候更明显。解决噪音的办法通常是提高PWM频率到20kHz以上这样超出人耳听觉上限电机就不会啸叫了。但频率不是越高越好太高的频率会导致开关损耗上升、驱动管发热加剧所以一般在20kHz-40kHz之间取一个平衡值。1.3 为什么不能直接串个电阻降速既然平均电压能调速为什么不用一个电位器串在电机回路里降速原因有两个第一是效率问题。电阻降压消耗的能量全部变成热量而且这部分热量不在电机上做功纯粹浪费。小功率风扇凑合用可以稍微大一点电流的电机电阻会烫到不敢碰。第二是启动问题。电机启动瞬间需要的电流往往是额定电流的3到5倍如果回路里串着大电阻电压被压得很低电机可能根本没力气启动。或者更糟电机低速时扭矩不足一旦叶片上有灰尘或者阻力它就停转然后长时间堵转导致电流飙升把电机或者驱动管烧掉。所以工程上做调速首选就是PWM。它让功率开关管工作在饱和导通和完全截止两个状态导通时管子上的压降极小截止时电流几乎为零两边的损耗都很低系统的效率就上去了。一句话总结PWM调速的本质是用高速开关去控制功率的“通断比例”而不是把多余能量消耗在电阻上。2. 硬件链路怎么搭从Arduino引脚到电机端2.1 先看清你的风扇电机属于哪一类拿到一个直流风扇别急着接线。先看铭牌或者标签确认电机类型和额定参数。创客领域常见的直流风扇电机无非两大类有刷直流电机两根线通直流电就转换向靠内部的电刷和换向器。这是最容易被PWM控制的类型控制逻辑简单也是这篇博文的主线。无刷直流电机BLDC内部有三相绕组和永磁转子通过电子换向器驱动。普通的三线电脑风扇很多是无刷的里面的控制板已经把驱动做完了外部只给12V就行这种风扇有的支持PWM调速信号线通常第4根线但逻辑和直接控制有刷电机完全不同不能混为一谈。此外还要记下额定电压和额定电流。Arduino Uno的数字引脚最大只能输出40mA而一个小型直流风扇的工作电流通常在100mA到几百毫安12V的大风扇可能超过1A。如果直接把电机接到Arduino引脚上轻则引脚被拉垮、Arduino自动复位保护重则烧掉ATmega328P芯片。所以说Arduino在电路里只负责输出PWM控制信号真正驱动电机的是一个功率放大级。2.2 驱动方案怎么选从三极管到驱动芯片功率放大的方案有好几种我按适用电流从低到高整理了一张表方便你对着自己的电机电流挑。驱动方案适用电流范围优点缺点典型型号NPN三极管500mA以下价格便宜电路简单入门首选导通压降大大电流下发热严重S8050、2N2222、TIP122达林顿逻辑电平N-MOS0.5A到数安培导通电阻极低发热小效率高需要防静电带大电容负载时要注意栅极驱动能力AO3400、IRLZ44N、IRLB8721L9110S模块800mA左右内置H桥能正反转模块即插即用电流上限不高模块上的焊盘不算太扎实L9110S、MX1508TB6612FNG模块1.2A每通道内部MOS管压降小控制逻辑简单适合小车项目需要额外的逻辑电源接线比单管多TB6612FNGL298N模块2A左右每通道模块成熟功率余量大适合入门课程饱和压降大发热明显效率一般L298N如果你只是控制一个风扇而且只需要调速不需要反转那我首推低边N-MOS方案而不是L298N这种大模块。原因是MOS管导通电阻只有几十毫欧几乎不发热电路简单体积也小。我自己的常用电路是这样的选用AO3400这种逻辑电平N-MOS栅极电压2.5V就开始导通Arduino的5V就能完全驱动它不需要额外的栅极驱动电路。MOS管的Drain接电机一端Source接GND。电机另一端接外部电源正极比如12V。栅极Gate通过一个100Ω电阻接到Arduino的PWM引脚。栅极还要从信号源断开时防悬空所以加一个10kΩ下拉电阻到GND。电机两端反向并联一个肖特基二极管如1N5819阴极接电源正极阳极接MOS管的Drain端。外部电源的GND必须和Arduino的GND连在一起也就是共地否则PWM信号根本没有参考电位。如果你手头只有NPN三极管也可以搭类似的低边开关集电极接电机一端发射极接GND基极串一个1kΩ限流电阻接Arduino引脚。但要注意三极管必然有大约0.7V的饱和压降电流大了发热会比较明显小风扇够用大风扇建议直接上MOS。2.3 续流二极管和电源去耦这两个元件不能省我可以拍着胸脯说我踩的最惨的坑就是“忘了加续流二极管”。直流电机是个感性负载本质上就是一堆线圈。电感有一个性质电流不能突变。当MOS管导通时电流从电源正极经过电机和MOS管流到GND电机线圈里储存了磁场能量。当PWM信号切换到低电平、MOS管瞬间关闭时线圈会试图维持原来的电流方向于是产生一个很高的反向电动势方向是“试图继续把电流推过去”。这个反向电动势的峰值可能达到电源电压的几倍甚至十几倍如果不给它一条泄放回路它就会把高压加在MOS管的Drain和Source之间电压一旦超过MOS管的耐压值MOS管就直接击穿短路了。MOS管坏了以后的表现很像是“一直导通”——风扇变成“飞车”状态全速转不停关不掉。续流二极管的作用就是在MOS管关闭的瞬间给线圈电流提供一个低阻抗的循环回路。电流通过电机线圈、续流二极管、回到电机线圈磁场能量转化为热量慢慢释放电压被二极管钳位在一个很安全的水平。这就是为什么这个二极管不能省也不能反接。反接等于直接短路。另外电机启动和PWM开关切换时电流波动很大会拉低电源电压甚至引起Arduino复位。所以建议在电源输入端并联一个大容量的电解电容100µF到470µF具体根据电机电流决定再在靠近MOS管的位置并联一个0.1µF陶瓷电容用来吸收高频开关噪声。这个组合不是说焊上去就完事电容的耐压要留余量12V电源最低选25V耐压。3. Arduino端代码、引脚、频率三件事3.1 你其实在用定时器analogWrite背后的秘密Arduino的analogWrite()用起来很简单三个参数、一行代码风扇就变速了。但如果不理解它背后调用的定时器资源后面改频率或者和别的库冲突时会摸不着头脑。Uno的ATmega328P里有三个定时器Timer08位控制5、6号引脚的PWM同时负责millis()、delay()等时间基准。所以尽量不要动Timer0的配置否则Arduino的时间函数全部乱套。Timer116位控制9、10号引脚的PWM也是Servo库默认使用的定时器。Timer28位控制3、11号引脚的PWM。PWM引脚对应定时器默认频率Uno5、6Timer0约980Hz3、11Timer2约490Hz9、10Timer1约490HzanalogWrite(pin, duty)的duty范围是0到255它不是你直观理解的“占空比0%到100%”而是8位分辨率下的数值。255对应100%占空比128对应约50%含义是一致的。我自己的建议是默认项目里我优先用9号引脚来做电机PWM一来Timer1可以比较方便地调整频率二来不走Timer0不会干扰时间基准。但是如果用到Servo库就必须避开9、10号引脚换成3号或11号。3.2 一段可以直接跑的最小调速代码下面这个例子用一块电位器接到A0口手动旋转旋钮通过PWM控制风扇转速。它麻雀虽小五脏俱全适合作为第一个验证实验。const int pwmPin 9; const int potPin A0; void setup() { pinMode(pwmPin, OUTPUT); digitalWrite(pwmPin, LOW); Serial.begin(9600); } void loop() { int potValue analogRead(potPin); // 0 ~ 1023 int duty map(potValue, 0, 1023, 0, 255); analogWrite(pwmPin, duty); Serial.print(pot: ); Serial.print(potValue); Serial.print( duty: ); Serial.println(duty); delay(10); }这段代码里有几个细节值得展开说说。第一setup()里我把PWM引脚先设为OUTPUT再digitalWrite LOW。这个顺序很重要Arduino复位或上电瞬间引脚默认是输入状态电压不确定如果这时电机电源已经先上电引脚误触发电平会导致风扇突然转一下也就是下面要说的“飞车”问题的诱因之一。先让引脚输出低电平就能把驱动管锁死在截止状态。第二analogRead返回0到1023而PWM的占空比参数是0到255用map()一行完成线性映射。电位器调到中间时占空比大约是50%。第三delay(10)在这里纯粹用于固定采样频率和串口打印节奏。如果是做一个连续控制的系统我不建议用长时间delay()阻塞主循环后面会专门讲到无阻塞写法。3.3 电机啸叫怎么办提高PWM频率的方法跑完上面的实验你大概率会发现风扇转起来带点“滋滋”声这是490Hz的PWM频率正好落在人耳最敏感的频段导致的。想要消除这种啸叫就得把PWM频率提到20kHz以上。拿9号引脚Timer1举例可以用改预分频系数的方式提高频率void setup() { pinMode(9, OUTPUT); // Timer1控制9、10号引脚 // 默认分频64改为分频1PWM频率约31kHz TCCR1B (TCCR1B 0b11111000) | 0b00000001; } void loop() { analogWrite(9, 128); }TCCR1B寄存器的低3位用来设置预分频系数默认值是0b011对应64分频。我把它改成0b001对应1分频频率就从490Hz左右跳到31kHz左右完全听不到啸叫了。但是这里必须提醒两个坑Timer1改分频后9和10两个引脚的PWM频率都变了但占空比功能不受影响。如果项目里用了Servo库Servo库底层也在操作Timer1两者的配置会互相覆盖轻则舵机抽风重则引脚没有输出。所以用了Servo就别再改Timer1。另外PWM频率提高以后驱动管每个周期的开关损耗都会增加。对于AO3400这种高频性能好的小MOS管来说问题不大但如果是老型号的达林顿三极管切换损耗会明显发热需要实测温度。4. 一次真实的“风扇飞车”故障排查4.1 现象和第一判断有一次我把风扇接到12V电源上程序里占空比已经写成了0可风扇仍然在疯转。拔掉风扇线再插上只要MOS管电路一通电风扇立刻全速运转好像完全不受Arduino控制一样。这就是圈子里说的“PWM电机飞车”。我的第一反应是去查代码以为是不是把引脚号写错了或者是占空比映射反了。但排查完代码逻辑完全没问题。接着想到可能是MOS管坏了结果拿万用表一量好家伙Drain和Source之间的电阻几乎为零MOS管确实已经击穿短路。换了新MOS管再上电刚开始正常但只要快速插拔电源几次或者Arduino复位的那一瞬间风扇又会猛地全速转一下再过几次MOS管又废了。4.2 逐步排查的完整链路这个故障不是一次就找到根因的我把排查过程完整列出来你可以照着这个顺序去检查自己的电路先量电源12V电源输出是否稳定电流是否够。如果电源电流不够电机启动瞬间会把电压拉得很低引起系统不稳定。量MOS管D-S之间在PWM关断时是否真的截止如果阻值异常说明MOS管已经被击穿。查续流二极管确认二极管没有接反用二极管挡量它的正向压降是否正常。查Arduino引脚状态在Arduino刚上电、程序还没运行的瞬间用示波器或者万用表看引脚电压。这一步最关键我发现9号引脚在Arduino复位后的确有短暂的高电平跳变而不是稳定在低电平。查上电时序Arduino和风扇电源是不是同一个开关控制的。我之前是风扇电源先上电Arduino后上电Arduino还没初始化完引脚悬空栅极被外界噪声或者寄生电容耦合拉高MOS管就误导通了。最终根因就是两个问题叠加一是Arduino复位期间引脚还没输出低电平电机电源已经给上去了二是MOS管栅极少了一个下拉电阻导致栅极电压不确定。4.3 修复方案和以后怎么预防修复方法分三步第一步给栅极加一个10kΩ下拉电阻到GND。这样即使Arduino引脚悬空栅极电压也会被稳定在0VMOS管保持截止不会再被噪声触发。第二步改上电顺序。要么用同一个开关同时控制Arduino和电机电源要么在Arduino启动完成后通过继电器或MOS管再去接通电机电源。最简单的办法是Arduino先上电等待1到2秒再由代码里的一个延时或者按键逻辑驱动继电器接通电机电源。第三步在代码里强化引脚初始状态。我习惯在setup()开头就这么写void setup() { pinMode(9, OUTPUT); digitalWrite(9, LOW); // 先把PWM引脚拉低禁止电机转动 // 然后再进行其他初始化 }就算后面要初始化串口、接传感器只要这句在电机电源接通之前执行就能保证系统启动时风扇不会乱转。再说预防。之前那块“飞车”烧掉的MOS管归根结底是续流二极管缺失加栅极悬空两个因素叠加的结果。后面我在每个电机项目里都默认加上续流二极管和栅极下拉电阻再也没遇到烧MOS管的情况。大家做的时候可以把这个当成标准配置不要想当然地省掉。5. 从“能转”到“好用”无阻塞调速与温控闭环5.1 别用delay()卡住主循环初学阶段用delay()做简单的延时没问题但delay()会让整个程序卡在当前这一行期间什么传感器都读不了串口命令也处理不了。如果你的项目还要根据温度、按键或串口指令实时调整风扇转速delay()就会成为瓶颈。比如想做一个“缓慢加速”的效果用delay()写会变成这样先输出低占空比delay 500ms再输出高一点再delay……程序在等待期间是死的。更好的做法是用millis()记录时间戳非阻塞地每30ms调整一次占空比。下面这段是一个带串口调速的平滑渐变示例可直接运行const int pwmPin 9; int targetDuty 0; int currentDuty 0; unsigned long lastStepTime 0; const int stepInterval 15; const int stepValue 2; void setup() { pinMode(pwmPin, OUTPUT); digitalWrite(pwmPin, LOW); Serial.begin(9600); } void loop() { if (Serial.available() 0) { int value Serial.parseInt(); targetDuty constrain(value, 0, 255); } unsigned long now millis(); if (now - lastStepTime stepInterval) { lastStepTime now; if (currentDuty targetDuty) { currentDuty stepValue; } else if (currentDuty targetDuty) { currentDuty - stepValue; } analogWrite(pwmPin, currentDuty); } }串口发一个0到255之间的数字风扇就会平滑地加速或减速到目标值。这个渐变过程一方面减少了对MOS管和电机绕组的电流冲击另一方面也让风扇的机械寿命更有保障。5.2 温控风扇加一层最简单的闭环Arduino做温控风扇是个经典应用温度高了风扇转快温度低了风扇转慢甚至直接停转。这里我不展开PID只说最实用、最不容易翻车的分段控制加迟滞。我用DHT11采集环境温度然后用温度值映射到占空比。完整的DHT11接线这里不细讲重点看控制逻辑#include DHT.h #define DHTPIN 4 #define DHTTYPE DHT11 const int pwmPin 9; DHT dht(DHTPIN, DHTTYPE); int duty 0; void setup() { pinMode(pwmPin, OUTPUT); digitalWrite(pwmPin, LOW); dht.begin(); Serial.begin(9600); } void loop() { float temp dht.readTemperature(); if (isnan(temp)) { return; } // 25.0℃以下停转35.0℃以上全速 int mapped map((int)(temp * 10), 250, 350, 0, 255); duty constrain(mapped, 0, 255); analogWrite(pwmPin, duty); Serial.print(temp: ); Serial.print(temp); Serial.print( duty: ); Serial.println(duty); delay(500); }用temp乘以10再做映射是为了避免map()对浮点数的直接操作。这个逻辑的问题在于当温度在25℃附近波动时风扇会频繁地启动、停止或者来回加减速特别烦人。解决办法是加迟滞比如温度超过26℃才启动风扇但低于22℃才允许停转。迟滞区间的设置能让控制行为稳定很多这个思路在工业温控里很常用。5.3 先用仿真验证逻辑再动手接线硬件电路焊好再改耗时又费钱。我现在的习惯是先在Wokwi这类在线Arduino仿真平台上把控制逻辑跑通再接实际硬件。Wokwi的好处是内置了Arduino Uno、直流电机、电位器、DHT11等常见元件代码直接贴进去就能跑逻辑对不对一眼就能看出来。对我个人来说它特别适合调试“逻辑层”的问题比如按键消抖、串口协议解析、温度迟滞边界这些纯软件层面的坑。但有个很重要的边界要说清楚仿真平台不模拟电机的反电动势也不会模拟MOS管被击穿的过程所以我在前面讲的续流二极管、栅极下拉、共地问题在仿真里一个都验证不出来。这些属于硬件坑必须接实物去测。所以我的工作流是“仿真验逻辑硬件验参数”两边的任务分开。关于“仿真能跑不代表电路安全”的体会我最后想聊一个这些年反复验证的感受做Arduino控制风扇这类项目真正的门槛很少在“写不出PWM代码”上。analogWrite一学就会难的是理解电机是感性负载、MOS管会被反电动势打死、Arduino复位瞬间引脚状态不确定、电源要共地、启动电流要留余量这些硬件常识。它们藏在数据手册里、藏在元器件的物理特性里仿真不会告诉你教程也不会刻意强调但只要你做过一次带实际负载的项目摔过一跤就再也不会犯了。配套的硬件上电顺序、续流二极管和栅极下拉电阻这三板斧我现在已经变成肌肉记忆每次画板子或者搭面包板都会默认带上。如果你照着这篇文章搭好了电路最后发现风扇还是会偶尔“抽风”先不要怀疑代码拿万用表量一量MOS管的栅极电压、看看续流二极管有没有反插多半就是硬件层面的细节没到位。这一路调试下来踩过的坑每一个都值回票价。