- 引言 —— 电机每转一次,主板就重启一次
- 引脚能输出的电流,与电机要求的电流
- 用独立电源和公共地 —— 不把地接在一起会怎样
- 用晶体管和 MOSFET 做开关
- 续流二极管 —— 感性反冲的物理原理
- 电机驱动 IC 与 H 桥
- 继电器模块与光耦隔离
- 去耦电容与欠压复位 —— 这不是代码问题
- 结语 —— 引脚负责发号施令,电力来自电源
引言 —— 电机每转一次,主板就重启一次
把一个小齿轮电机接到 Arduino 的 D9 上,另一端接 GND,调用 digitalWrite(9, HIGH)。
电机轻微地抖动了一下。然后串口监视器上又出现了启动信息——程序从头开始了。再打开一次电机,又重启了。
到这里,大多数人会直接去看代码:是看门狗?栈溢出?库冲突?代码没有任何问题。
问题出在一个数字上。Arduino 一个引脚能安全输出的电流是 20mA,而那个电机要求的电流是 200mA。整整十倍。
这篇文章要讲的,就是这十倍的差距去了哪里、烧掉了什么,以及正确的做法是什么。电机、继电器和电磁阀这些内部带线圈的负载都属于感性负载,而感性负载还带着一种电阻和 LED 都没有的额外风险。
引脚能输出的电流,与电机要求的电流
先把两个数字摆在一起看。
| 负载 | 正常运行电流 | 启动或堵转时的峰值 | 能否直接用GPIO驱动 |
|---|---|---|---|
| 单颗LED | 15mA | 无 | 可以 |
| 蜂鸣器(无源) | 30mA | 无 | Arduino有风险,树莓派不行 |
| SG90舵机 | 120mA | 700mA | 不可以 |
| 小型直流齿轮电机(TT电机) | 150~250mA | 800mA~1.5A | 不可以 |
| 单通道继电器模块 | 70mA | 无 | 不可以 |
| 5V电磁锁 | 600mA | 1.2A | 不可以 |
| WS2812B LED,30颗 | 单颗60mA,合计1.8A | 无 | 不可以 |
| 12V散热风扇 | 150mA(12V侧) | 400mA | 不可以(电压也不同) |
再看看板卡本身的限制:
| 板子 | 单引脚推荐值 | 单引脚绝对最大值 | GPIO总和 | 5V电源轨供电上限 |
|---|---|---|---|---|
| Arduino Uno R3 | 20mA | 40mA | 200mA | 约450mA |
| 树莓派40针 | 16mA | 16mA | 50mA | 约1000mA |
| ESP32 DevKit v1 | 12mA | 40mA | 120mA | 约500mA |
这张表里,能允许直接用 GPIO 驱动负载的只有第一行。
现在来看物理层面。把一个要求 200mA 的电机接到只能输出 20mA 的引脚上,究竟会发生什么?
引脚的输出级是一对极小的 MOSFET。写 HIGH 时,上方的 MOSFET 导通,把引脚接到 5V——但这个 MOSFET 不是完美的开关,它是有电阻的。对 ATmega328P 来说,大约是 25 欧姆。
假设电机线圈的电阻是 5 欧姆,整个电路的电阻就是 30 欧姆。
流过的电流 = 5V / 30欧姆 = 0.167A = 167mA
167mA 会流过那颗小小的 MOSFET。MOSFET 上消耗的功率是:
P = I² × R = 0.167² × 25 = 0.028 × 25 = 0.70W
0.7W。这颗 MOSFET 是按 20mA 设计的,那种情况下的功耗是:
P = 0.02² × 25 = 0.00001 W = 0.01mW
是设计值的七万倍。往一小片硅片里塞进 0.7W,结温会在毫秒级别飙升到几百摄氏度。结果只有两种:要么栅氧化层失去绝缘性,引脚被永久卡死在 HIGH 或 LOW;要么金属走线熔断,引脚彻底断开。
而重启的原因略有不同。167mA 突然抽走的瞬间,5V 电源轨会瞬间塌陷。这一点会在最后一节详细讲。
在这里先立一条原则:GPIO 引脚是发号施令的地方,不是供电的地方。 电力永远应该直接来自电源,引脚的职责仅仅是开关一个能让那份电力通过的开关。
用独立电源和公共地 —— 不把地接在一起会怎样
假设决定给电机接一个独立电源。准备一个 6V 电池组,把电机接上去,再让 Arduino 控制一个开关用的晶体管。
这里有一根新手几乎必定会漏接的线:电池负极和 Arduino GND 之间的连线。
为什么需要它,回到前一篇文章讲的电压定义就很清楚了。电压是两点之间的差。Arduino 说「我输出了 5V」时,那个 5V 是相对于 Arduino 自己的 GND 而言的。电池那边的电路,看电压时相对的则是电池自己的负极。如果这两个参考点没有连在一起,两个电路之间就没有共同的刻度。
以 MOSFET 的栅极为例,MOSFET 导通的条件是栅源之间的电压超过阈值电压。栅极来自 Arduino,源极则在电池那边的 GND 上。两个 GND 各自悬空时,栅源之间到底是多少伏根本没有定义。用一个没有定义的值去开关,症状就变成了「时灵时不灵」。
实际观察到的现象会更诡异。两个电路并不是完全隔离的,而是通过寄生电容有一点微弱的耦合。所以电机转动时产生的电气噪声,会通过这个耦合去晃动 Arduino 一侧的参考电位。用手一碰,行为就变了;接上示波器探头,又突然正常了——因为探头的地线把两个电路连在了一起。
规则很简单:使用独立电源的电路,它们的 GND 必须在一个点上汇合。 正极可以分开,但负极要共用。
这里再补一个实务技巧:把 GND 汇合的那根线,最好和承载电机电流的粗线分开走。因为承载电机电流的 GND 线也是有电阻的,会产生压降。一根 20 厘米长的细跳线电阻大约是 0.05 欧姆,通过 1A 电流时,
0.05欧姆 × 1A = 0.05V
两个电路的参考点之间就会出现 50 毫伏的差异。这对逻辑信号来说不算什么,但如果同时还在读模拟传感器,读数就会跟着晃动这么多。原则是:电机的 GND 用粗而短的线直接接回电源,逻辑部分的 GND 走另一条路径,汇聚到电源的同一个点上。
用晶体管和 MOSFET 做开关
要让引脚只负责发信号、电流直接来自电源,就需要一个开关。半导体开关有两种,选择的标准很明确。
BJT —— 用电流控制
双极型晶体管让集电极电流正比于流入基极的电流。用作开关时,需要让它工作在完全导通的饱和区,这就要求给足基极电流。
假设要打开一个继电器模块,线圈电流是 70mA。用 2N3904 这类小信号晶体管,为了确保饱和,电流放大倍数不采用数据手册上的值,而是保守地估计成 10。
所需基极电流 = 70mA / 10 = 7mA
基极电阻等于引脚电压减去基极-发射极压降 0.7V,再除以这个电流。
基极电阻 = (5V - 0.7V) / 0.007A = 614欧姆
这里有一点很关键:取整方向和 LED 电阻正好相反。LED 电流不足是安全的,所以要向上取整;而基极电阻如果电流不够,晶体管就只能半导通,自己消耗功率发热,所以要向下取整。E24 系列里 614 欧姆下面是 560 欧姆。
实际基极电流 = (5V - 0.7V) / 560欧姆 = 0.0077A = 7.7mA
7.7mA,在引脚 20mA 上限之内,安全。
即使 BJT 完全导通,集电极和发射极之间依然会残留大约 0.2V。这就是饱和压降,在 70mA 下的损耗是:
0.2V × 0.07A = 0.014W = 14mW
可以忽略。
MOSFET —— 用电压控制
电流一大,BJT 就不划算了。开关 200mA 需要 20mA 的基极电流,这正好是 Arduino 引脚的全部上限。500mA 的话需要 50mA,根本不可能。
MOSFET 只需要在栅极加电压就能导通。栅极是绝缘的,稳态下几乎不流过电流,所以能用一个小引脚控制一个大电流。
这里有个决定性的细节:必须选逻辑电平 MOSFET。普通 MOSFET 是按栅极加 10V 才能完全导通来设计的,著名的 IRF540N 就是这样。只加 5V 的话只能半导通,留下不小的电阻,这部分电阻就会发热。通过 1A 电流,若电阻是 0.5 欧姆,
P = 1² × 0.5 = 0.5W
0.5W 会在 MOSFET 上变成热量。没有散热片的话会发烫。
逻辑电平 MOSFET 只需要栅极加 4.5V 就能完全导通。IRLZ44N 是典型代表,这颗器件在栅极 5V 驱动下的导通电阻是 0.028 欧姆。
P = 1² × 0.028 = 0.028W = 28mW
相差 18 倍。这就是型号里 IRF 和 IRLZ 的区别。
不过如果是 3.3V 的板子,还得再多留一层心眼。就连 IRLZ44N,在栅极 3.3V 下也不在规格保证范围内。在树莓派或 ESP32 上,得挑那种明确标注栅极阈值在 2.5V 以下的器件。AO3400 和 IRLML2502 就是常用于这个场景的型号。
栅极电阻与下拉电阻
MOSFET 电路要一起加两个电阻。
栅极电阻串联在引脚和栅极之间。MOSFET 的栅极相当于一个电容,导通的瞬间会有一股给这个电容充电的大浪涌电流。没有电阻的话,唯一能限制这股电流的就只有引脚自身的内阻。接入 220 欧姆后,
最大瞬时栅极电流 = 5V / 220欧姆 = 0.0227A = 22.7mA
22.7mA。持续时间只有几十纳秒,Arduino 能扛住,但树莓派就没什么余量了。3.3V 配 470 欧姆的话,
3.3V / 470欧姆 = 0.007A = 7mA
7mA,安全。栅极电阻太大会让开关变慢,期间 MOSFET 停留在中间状态的时间更长、发热更多,所以 100 到 470 欧姆是比较实用的范围。
下拉电阻接在栅极和 GND 之间,大约 10 千欧姆。原因和前一篇文章里的悬空输入一样。板子启动期间,以及复位过程中,GPIO 引脚会处于输入状态的高阻态。这段时间里栅极相当于什么都没接,周围的噪声一旦把电压推高,MOSFET 就会导通。也就是说,通电的瞬间电机会抽动一下。下拉电阻会在这段时间里把栅极钳在 0V。
漏掉这一个电阻,就会出现「每次通电电机都会猛地一动」的症状——如果是机械臂,这一下猛动足以折断手臂。
续流二极管 —— 感性反冲的物理原理
做到这一步,开关电路看起来已经完整了。可继电器开关几次之后,MOSFET 就烧了。按计算,电流和电压都留有余量,却还是烧了。
原因出在线圈上。
为什么会出现几千伏
电感的根本特性是抵抗电流的变化。用公式表达就是:
V = L × (di / dt)
L 是电感量,di/dt 是电流随时间的变化率。这个公式说的是:你越想快速改变电流,电感就会产生越大的电压来反抗你。
假设继电器线圈的电感量是 100 毫亨,正流过 70mA。MOSFET 关断所需的时间大约是 1 微秒。
V = 0.1H × (0.07A / 0.000001秒)
= 0.1 × 70000
= 7000V
7000 伏。这是计算值,实际上会因为空气击穿或器件击穿而止步于更低的数值,但达到几百伏到几千伏是真实存在的。
这个电压的方向很关键。线圈想让原来流过的电流继续流下去,于是就把电流被切断那一侧的电位往上推。也就是说,MOSFET 的漏极电压会远远冲过电源电压。IRLZ44N 的漏源之间最大额定值是 55V。承受几百伏就会击穿,反复吸收这份击穿能量,最终会烧毁器件。
顺便算一下这份能量的大小,建立一点直觉。
E = 0.5 × L × I² = 0.5 × 0.1 × 0.07² = 0.000245J = 245微焦
245 微焦看起来不大,但如果在 1 微秒之内释放出来,瞬时功率就是:
0.000245J / 0.000001秒 = 245W
245 瓦。虽然极短,但每一次都会重复发生。
二极管做了什么
解决办法是给那份能量找一条出路。在线圈两端并联一个二极管,方向选成平时不导通的那个,也就是把阴极(标有色环的那端)接到正电源,阳极接到 MOSFET 的漏极一侧。
平时二极管处于反向,什么都不做。MOSFET 关断、线圈把电压往上推的瞬间,一旦漏极电压比电源电压高出二极管的正向压降,二极管就开始导通。这样一来,线圈的电流就经二极管重新流回线圈自身。有了出路,电压也就没有理由继续往上冲了。
这时漏极的最高电压是:
电源电压 + 二极管正向压降 = 5V + 0.7V = 5.7V
7000V 变成了 5.7V。就一个元件造成的差别而言,相当戏剧性。
循环的电流会在线圈自身的电阻上以热的形式消耗掉,逐渐减小。那 245 微焦会在几毫秒内悄无声息地消散。
挑选二极管与副作用
用哪种二极管是有标准的。
电流额定值必须不低于线圈电流。70mA 的话,1N4148 的 200mA 也够用,但很多人为了留余量会选 1A 的 1N4007。
反向耐压要比电源电压留足余量。1N4007 是 1000V,对任何低压电路都绰绰有余。
有些场合恢复速度很重要。用 PWM 控制电机转速时,每秒要开关几千次,而 1N4007 这类通用整流二极管反向恢复要花几微秒,期间会产生损耗。这时该换用 1N5819 这类肖特基二极管,它的正向压降也更低,只有 0.3V,钳位电压也就更低。
副作用也值得了解。加上二极管之后,继电器释放的时间会变长,因为线圈电流是慢慢减小的。通常只有几毫秒,但触点释放得越晚,电弧持续的时间就越长,触点寿命也会缩短。在这一点比较关键的电路里,会给二极管串联一个电阻或者稳压二极管,把钳位电压略微抬高一点,作为折中方案。
最后,如果买的是继电器模块或电机驱动板这类成品,这个二极管往往已经焊在板子上了。只有自己用晶体管直接开关线圈时才需要留意。不过板子上到底有没有,最好还是用眼睛确认一下比较放心。
电机驱动 IC 与 H 桥
到目前为止的电路只能开关电机,没法换向。要换向就得反转电流方向,这需要四个开关。这种结构叫 H 桥,因为形状像字母 H。
+V +V
│ │
[S1] [S2]
│ │
├──── 电机 ────────┤
│ │
[S3] [S4]
│ │
GND GND
打开S1和S4,电流从左流向右;
打开S2和S3,电流从右流向左。
同时打开S1和S3,电源和GND就会短路。
最后一行正是不该自己搭 H 桥的原因。同一侧的上下两个开关一旦同时导通,电源就会短路,这称为贯穿电流。MOSFET 导通和关断的速度并不相同,在关掉一个、打开另一个的过程中,可能会出现极短暂的两者同时导通的瞬间。所以实际设计里必须加入死区时间,而用软件精确对齐这一点是很危险的。
所以实务中都会用驱动 IC。
| 元件 | 通道数 | 单通道电流 | 类型 | 特点 |
|---|---|---|---|---|
| L293D | 2 | 600mA | BJT | 老牌标准,压降约2V,较大 |
| L298N | 2 | 2A | BJT | 常见模块,压降约2V,需要散热片 |
| DRV8833 | 2 | 1.5A | MOSFET | 压降小,适合小型机器人 |
| TB6612FNG | 2 | 1.2A | MOSFET | L298N的替代品,效率好 |
| A4988 / DRV8825 | 步进电机1轴 | 1~2A | MOSFET | 步进电机专用,电流可调 |
L298N 模块最常见,但有一点值得了解。因为内部是 BJT,每个方向都要损耗大约 1V,合计接近 2V。用 6V 电源时,真正到达电机的只有 4V,三分之一都在驱动器里变成了热量。用 3V 级别的小电机时,这份损耗是致命的,DRV8833 或 TB6612FNG 是更好的选择。
继电器模块与光耦隔离
对于需要和单片机完全电气隔离的负载——家用交流电器、12V 照明——继电器是合适的工具。
继电器的工作原理是给线圈通电产生电磁铁,再用这股力量去移动机械触点。触点和线圈之间存在物理间隙,所以两者在电气上完全分离。哪怕触点一侧接着 220V,线圈一侧依然完全待在自己的 5V 世界里。
市面上的继电器模块通常还会再加一个光耦。这是又多加了一层隔离——内部是一颗 LED 正对着一个光敏晶体管,信号被转换成光传过去,两个电路之间完全没有电气连接。
这里有个实务上很重要的地方:模块上的跳线帽。大多数继电器模块都有标着 VCC、GND、JD-VCC 的引脚,以及在它们之间架桥的跳线帽。这个跳线帽插着的时候,继电器线圈也是从单片机的 5V 取电的。就算有光耦,一旦共用了电源,隔离的意义也就没有了。
还有一个实际问题。继电器线圈耗电 70mA。四通道模块上,如果四个同时打开,
70mA × 4 = 280mA
280mA。这对 Arduino 5V 电源轨 450mA 的上限来说是不小的负担,而且继电器开启瞬间的电流变化会晃动 5V 电源轨,让主板变得不稳定。
正确的做法是拔掉跳线帽,给 JD-VCC 接上独立的 5V 电源。这样线圈电流就来自独立电源,单片机只需要给光耦的 LED 提供几毫安。这种情况下,两路电源的 GND 千万不能接在一起,接了隔离就没了。这是前面「GND 必须接在一起」这条原则唯一的例外,原因是光耦传递信号时根本不需要共享参考点。
还有一点,很多继电器模块是低电平有效的。IN 引脚必须给 LOW,继电器才会吸合。所以板子启动期间,如果这个引脚处于悬空状态,继电器就可能短暂吸合又释放。如果是灯,顶多闪一下;如果是加热器或电机,那就是个真问题了。前一篇文章里的上拉电阻,又在这里派上了用场。
去耦电容与欠压复位 —— 这不是代码问题
最后回到这篇文章开头那一幕:电机一开,主板就重启。
哪怕开关电路搭对了,也接了独立电源,这个症状往往还是挥之不去。原因出在电源走线的电阻上。
每一根导线都有电阻。1 米长的 USB 线电源部分大约 0.2 欧姆,面包板的接触电阻每个接点 0.01 到 0.05 欧姆,跳线大约 0.05 欧姆。全部加起来大约是 0.3 欧姆左右。
假设电机启动时流过的电流是 1.5A,
压降 = 1.5A × 0.3欧姆 = 0.45V
原本 5.1V 进来的电源,到板子这里就变成了 4.65V。
树莓派的 5V 电源轨一旦跌破 4.63V 就会弹出欠压警告,再往下掉就会导致 SD 卡访问失败或者重启。Arduino 的 ATmega328P 内置了欠压检测电路,默认设置下一旦跌破 2.7V 就会强制复位。给一个 5V 稳压器输入 4.65V,它的输出会更低,启动电流越大,就越容易越过那道门槛。
这就是这个症状看起来像代码问题的原因。复位发生在代码正在运行的时候,串口上还会打出启动信息,看起来完全就是个软件问题。
电容能做什么,不能做什么
应对分两层。
在每颗 IC 的电源引脚旁边贴一个 100 纳法的陶瓷电容。这负责供应纳秒到微秒级的快速电流波动。芯片内部的晶体管在开关瞬间需要瞬间抓取一股电流,而想从电源那里直接拉,会因为走线电感而来不及。旁边这颗小电容能把这一瞬间补上。
在电机电源输入附近放一个 470 微法的电解电容,这个负责平抑毫秒级的大幅波动。算一下它到底能帮多大忙。要在 100 微秒内额外供应 1A 电流、同时把压降压制在 0.2V 以内,所需的容量是:
C = I × Δt / ΔV = 1A × 0.0001秒 / 0.2V = 0.0005F = 500微法
500 微法。470 微法基本够用。
但电机的启动时间不是 100 微秒,而是几毫秒。用 5 毫秒重新算一遍,
C = 1A × 0.005秒 / 0.2V = 0.025F = 25000微法
25000 微法。这不是能插在面包板上的元件。
这个计算说明的道理很清楚:电容是用来抚平短暂尖峰的元件,不是不够用的电源的替代品。如果欠压复位一直发生,该做的不是把电容加大,而是换掉电源本身。
实际的排查顺序
出现这个症状时,该按这样的顺序去检查。
第一,看电机电源和逻辑电源是不是分开的。如果两者都从同一个 USB 上拉电,这就是原因。
第二,把导线换粗、换短。经过面包板的电源路径会累积接触电阻,电流大的路径应该直接焊接或者用粗导线直连。
第三,加电容。在电机端子两端各放一个 100 纳法,在电机电源输入端放一个 470 微法。电机端子上的陶瓷电容还能抑制电刷产生的高频噪声。
第四,加软启动。用 PWM 让速度缓慢上升,能降低启动电流的峰值。这是唯一一个纯靠代码就能实现的缓解措施。
// 用PWM驱动MOSFET栅极,让电机缓慢启动。
// 接线: D9 -> 220欧姆 -> MOSFET栅极,栅极与GND之间接10k下拉
// 电机使用独立电源,线圈两端并联1N5819续流二极管
// 两路电源的GND在一个点上连接
const int MOTOR_PWM_PIN = 9;
const int RAMP_STEP_MS = 12; // 每一档之间等待的时间
const int TARGET_DUTY = 200; // 0到255。最终速度
void setup() {
pinMode(MOTOR_PWM_PIN, OUTPUT);
analogWrite(MOTOR_PWM_PIN, 0);
delay(500); // 等待电源稳定
}
void softStart() {
// 从0到目标值,大约用2.4秒升上去。
// 如果直接写255,启动电流会原样涌入,把电源轨拖垮。
for (int duty = 0; duty <= TARGET_DUTY; duty++) {
analogWrite(MOTOR_PWM_PIN, duty);
delay(RAMP_STEP_MS);
}
}
void softStop() {
for (int duty = TARGET_DUTY; duty >= 0; duty--) {
analogWrite(MOTOR_PWM_PIN, duty);
delay(RAMP_STEP_MS);
}
}
void loop() {
softStart();
delay(3000);
softStop();
delay(2000);
}
不过不要把这段代码误当成根本的解决方案。软启动只是压低了启动电流的峰值,一旦电机遇到负载堵转,堵转电流依然会照样流过。如果电源扛不住那个电流,同样的症状还会再次出现。
接线全部搭好之后,想确认电流预算合不合适,可以把元件放进本站的接线验证工具。像舵机或 LED 灯带这类需要独立供电的元件,它会连同峰值电流一起提示需要单独供电,也会计算 5V 电源轨的总和是否超出了板子的供电上限。
结语 —— 引脚负责发号施令,电力来自电源
这篇文章讲的这些事故,表面上看起来各不相同:引脚烧毁、主板重启、MOSFET 烧毁、继电器开机时短暂吸合。
但它们全都来自同一个误解:把 GPIO 引脚当成了电源来用。
引脚是一根开关信号线。它控制的那个开关,才负责把电源到负载之间的大电流放行。守住这个结构,电流的大小就和引脚彻底无关了。不管是流过 1A 还是 10A,引脚要做的都只是用几毫安去给栅极充电这一件事。
而感性负载还多了一层。线圈厌恶电流被切断的那一瞬间,并用几千伏的电压表达它的不满。提前用一个二极管给那份能量修好一条出路,它就会安静地降到 5.7V。漏掉一个二极管和装上一个二极管,差距是一千倍。
前后五篇文章讲的内容,最终都收敛到三个数字上:这个元件上落着几伏,流过它的是几毫安,以及这个引脚究竟能扛住多大的值。电路里那些悄悄毁掉硬件的失误,几乎全都出在没有确认这三者之一。在接上元件之前,养成把这三个数字写在纸上的习惯,比读十张电路图都管用。
현재 단락 (1/193)
把一个小齿轮电机接到 Arduino 的 D9 上,另一端接 GND,调用 `digitalWrite(9, HIGH)`。