教程 · 入门 · 30 分钟

为什么电机一启动,机器人就复位

看起来像软件 bug 的复位,其实是供电问题。本文讲电池内阻、一份真实的电流预算,以及欠压检测器漏掉的那段电压区间。

引言

机器人在台架上是好的。你把它放到地面上,电机一转起来,一切都停了——串口监视器从头重启,LED 序列重新开始,机器人刚才在做的事全被忘光。

几乎每个人的第一反应都是去翻代码。代码里什么都没有。

真正发生的是:电机启动时拉取的电流超过了电池在电压不掉下去的前提下所能供给的,电源跌到了单片机所需电压以下,板子重启了。这就是欠压,而它是第一台机器人上最常见的单一故障——本站电机控制路线这么说,几乎每一个零件页这么说,本站一半项目的故障清单里都列着它。本文就是解释它的那一页。

先确认是复位而不是崩溃

这两者需要不同的修法,而从房间另一头看过去它们长得一模一样。

在 setup() 最顶上放一行醒目的输出:

void setup() {
  Serial.begin(115200);
  Serial.println(F("---- BOOT ----"));
}

然后让机器人跑到出问题时,盯着监视器:

  • ---- BOOT ---- 又出现了→板子复位了。是电源。继续往下读。
  • 输出停了,或者同一行永远重复→板子卡在循环里或者崩了。那是代码问题,这一页帮不上。
  • 输出变成乱码→电源在跌,但没跌到足以复位。也是电源,同样的修法适用。

在 AVR 板子上你还能再往前一步,问问芯片为什么复位。MCUSR 保存着原因,但 main() 早在需要读它之前就跑起来了,所以要在一个最先执行的 .init3 函数里捕获它:

uint8_t resetFlags __attribute__((section(".noinit")));
void captureReset(void) __attribute__((naked, used, section(".init3")));
void captureReset(void) {
  resetFlags = MCUSR;
  MCUSR = 0;
}

void setup() {
  Serial.begin(115200);
  if (resetFlags & _BV(BORF))  Serial.println(F("cause: BROWN-OUT"));
  if (resetFlags & _BV(EXTRF)) Serial.println(F("cause: reset pin"));
  if (resetFlags & _BV(PORF))  Serial.println(F("cause: power-on"));
  if (resetFlags & _BV(WDRF))  Serial.println(F("cause: watchdog"));
}

一个诚实的补充:跑 Optiboot 的 Uno,其引导程序会在你的代码看到之前就读走并清掉 MCUSR,把值藏在寄存器 r2 里。如果你板子上每个标志位都读到零,原因就在这里——退回 ---- BOOT ---- 那个测试,它什么都不需要,而且从不说谎。

电压为什么会塌

电池不是一个电压源。它是一个串联了电阻的电压源,而那个电阻决定你的机器人好不好使:

V_terminal = V_open_circuit − I × R_internal

R_internal 是电芯化学体系和尺寸的属性,而在同一个抽屉里躺着的那些电池包之间,它的变化大得惊人。

四种电池包端电压对所取电流的曲线。9 伏 PP3 块从最高的 9 伏起步却陡降,2 安时已经穿过 5 伏。四节 AA 碱性从 6 伏跌到 4.4 伏。两节 18650 锂电从 7.4 伏起步几乎保持平直在 7.2 伏。四节 AA 镍氢从最低的 4.8 伏起步,几乎保持平直到 4.56 伏。一条竖直虚线标出两台电机启动时 2 安的冲击。
两台 TT 减速电机一起启动大约拉 2 A。9 V 块在货架上开路电压最高,负载下的结果却最差——为了到达那里它丢掉了 4 V。镍氢电池包起步低 4.2 V,到达时却高 0.6 V。 下载 SVG
电池包 开路 内阻 2 A 冲击下
2×18650 锂电 7.4 V 约 0.10 Ω 7.20 V
4×AA 镍氢 4.8 V 约 0.12 Ω 4.56 V
4×AA 碱性 6.0 V 约 0.80 Ω 4.40 V
9 V PP3 块 9.0 V 约 2.0 Ω 5.00 V,而且还在掉

由此得出的三件事,比这些数字本身更值得记住:

**九伏块是你能装进机器人的最差的电池。**它印着的电压最高,而在两欧内阻背后只有大约 500 mAh 的容量。那是烟雾报警器用的电池。

**碱性电芯不硬。**它的内阻大约是镍氢电芯的六倍,而且随着放电上升——于是机器人先正常工作二十分钟,然后开始重启,这读起来像间歇性故障,而其实只是一块平电池在表现得完全符合它该有的样子。

**可充电电池赢在真正要紧的那件事上。**镍氢和锂电成为机器人的标准化学体系,不是因为它们可充电,而是因为它们在负载下守得住电压。

选电池包之前先做电流预算

把机器人吃掉的电流加两遍——一遍是它正常用的,一遍是最坏瞬间的。

零件 运行 最坏
Arduino Uno 板 45 mA 45 mA
TT 减速电机 ×2 400 mA 2000 mA(两台都堵转)
红外反射阵列 100 mA 100 mA
HC-SR04 15 mA 15 mA
SG90 舵机 150 mA 700 mA(堵转)
HC-05 蓝牙 30 mA 40 mA
ESP32(Wi-Fi) 80 mA 250 mA(发射突发)

一台普通循迹车——Uno、两台电机、一个红外阵列——运行约 545 mA,最坏瞬间 2.15 A。最坏瞬间通常很短;例外是一台在附着力极限上推的机器人,它在堵转电流上待多久取决于推多久。电池包按第一个数选,跌落按第二个数算,因为它们回答的是不同的问题:

runtime  ≈ capacity / running current      2000 mAh / 545 mA ≈ 3.7 h ideal
sag      = worst-case current × R_internal

真实的续航大约是那个数字的三分之二。你不会把电池包放到零,冲击的代价比它们的平均值更高,而容量额定值是在温和的放电率下测出来的。

欠压检测漏掉的那段区间

每一颗 AVR 都有一个欠压检测,在低于设定电压时会把芯片按在复位里。Arduino Uno 出厂时这颗熔丝是 2.7 V。

一颗跑 16 MHz 的 ATmega328P 并不被允许在 2.7 V 下工作。它的手册速度等级从 1.8 V 下的 4 MHz 一直到 4.5 V 下的 20 MHz,插值到 16 MHz 给出的下限约为 3.8 V。

ATmega328P 最高时钟频率对电源电压的曲线。一条线从 1.8 伏下的 4 兆赫升到 4.5 伏下的 20 兆赫然后变平,其下方区域用阴影标为安全区。一条水平虚线标出 Arduino Uno 运行的 16 兆赫,它与安全边界相交在 3.8 伏。2.7 伏到 3.8 伏之间的一条红色带被标注为在 16 兆赫下超出规格,但高于欠压阈值。
在 2.7 V 到 3.8 V 之间,芯片跑得比它的供电电压所允许的更快,而欠压检测——设在 2.7 V——不会介入。那 1.1 V 的区间就是最古怪的机器人故障栖身的地方。 下载 SVG

这就解释了人们最难相信的那个症状:电机一跑,机器人不干净地复位,而是干脆行为古怪。变量被破坏、舵机跳一下、I²C 传感器返回胡说八道、状态机进入一个它根本没有转移进去的状态。在那个区间里,芯片正在执行超出其保证工作条件的指令,而“保证”这个词正是干活的词。

所以不要把“它没有复位”当成电源没问题的证据。

一个电池包,两条轨

修法不是两块电池,而是一个电池包喂两条路。

  • 电机电流从电池包直接进驱动器的电机电源端子——L298N 的 +12V、或者 TB6612FNG 的 VM。它绝不经过单片机。
  • 逻辑电流来自一路稳压 5 V——驱动器板载的稳压器、一个独立的降压模块,或者开发阶段的 USB。
  • **地必须连起来。**板子与驱动器之间的每一个信号都是相对地量的电压;没有共享参考,方向引脚就毫无意义。这是继第一个之后第二常见的接线故障。

永远不要通过 Arduino 的 5 V 引脚或者它的桶形插座稳压器跑电机。Uno 的稳压器是一个小线性器件,把输入输出之差烧成热:7.4 V 进、5 V 出、500 mA,那是 (7.4 − 5) × 0.5 = 1.2 W,装在一个没有散热片的封装里。它会先发热,然后热保护关断,而机器人复位的原因与电池毫无关系。

电容到底修好了什么

标准建议是在电机电源上并一只大电解电容。这条建议是对的,而通常给出的理由是错的,算术能把这件事定下来:

ΔV = I × Δt / C

一只 1000 µF 的电容被要求扛住持续 10 ms 的 2 A 启动冲击,它会跌落 2 × 0.010 / 0.001 = 20 V。它干不了这份活。电容扛不住电机启动冲击——只有更硬的电池扛得住。

它真正扛得住的是快的东西。在 8 kHz 的 PWM 开关频率下周期是 125 µs,于是同一只电容上看到的是 2 × 0.000125 / 0.001 = 0.25 V 的纹波——这份活它干得很轻松。它同时也吸收有刷电机朝电源扔回来的换向尖峰。

所以还是装一只——电机电源上并 470 到 1000 µF,再在驱动器附近加一只 0.1 µF 瓷片——并预期它能清理掉噪声和出问题的传感器。不要预期它能修好一次复位。如果一只电容真的治好了你的复位,那是电池包本来就在临界线上,而你买到的余量微乎其微。

别猜,去量它

十分钟万用表胜过一下午的理论。

  1. 表调到直流电压档,放在电池端子上,不是放在 Arduino 上。
  2. 记下静止电压。
  3. 启动电机,最好是在地面上带真实负载。
  4. 盯着那个数。

掉了几零点几伏就是答案。要从里面把 R_internal 算出来,就做除法:一个 6.0 V 的电池包在吃掉约 2 A 时掉到 4.4 V,那就是 (6.0 − 4.4) / 2 = 0.8 Ω。

然后在电机启动时对5 V 轨重复一次测量。那才是单片机真正经历的数;如果它跌向 3.8 V,你不用示波器就找到了故障。

便宜的表平均得太慢,抓不到一毫秒跌落的最低点,所以把你读到的那个数当成乐观值。那个跌落至少有那么糟。

修法,从最便宜的开始

  1. **别再通过板子给电机供电。**免费,而且直接修好很大一部分情况。
  2. **换电池包。**碱性或 9 V → 镍氢或 18650。通常这就是全部的修法。
  3. **把地连起来。**免费,而它缺席时造成的故障比欠压还古怪。
  4. **加上大电容。**是为噪声和传感器故障,不是为复位。
  5. **给电机加斜坡而不是直接阶跃。**一条运动曲线把启动电流摊在一段时间上,而不是一瞬间全要——这也正是让轮子不打滑的同一份代码。
  6. **把电机配对。**如果一台电机被要求给出比它该给的更多的转矩,它就会画出比它该画的更多的电流;减速电机选型的算术在这个问题的上游。
  7. **把稳压分开。**给逻辑轨一颗小的降压转换器,就把它和电机跌落彻底解耦。

电源轨理顺之后,下一步是在软件里量它——而诚实地读一个电压比看上去难,因为 ADC 的默认参考就是那条轨。

出问题时

现象 通常原因
电机一转串口输出从头重启 欠压;先查电池包,再查代码
用 USB 正常,用电池就跪 电池包供不出那个冲击,而 USB 可以
干二十分钟,之后反复复位 碱性电池内阻随放电上升
行为古怪却没有干净复位 供电落在 2.7–3.8 V 区间;超出规格,但没有阈值拦它
只有行驶时传感器返回胡说八道 电源噪声;大电容加独立轨
方向引脚像是什么都不干 板子与驱动器之间没有共地
稳压器烫到碰不得 逻辑电流走了线性稳压器,而输入电压太大
电容修好了,过一阵又回来 电池包本来就在临界线上;电容买的是毫秒,不是安培

电源是机器人每一层之下的那一层,这就是为什么这里的故障会模仿别处的故障。电机一跑就有某种无法解释的事情发生时,先量电池——它花十分钟,而且比其他任何猜测都更经常地是对的。

探索关联图谱

本教程所属的搭建

包含本教程的项目和学习路线。

延伸阅读

参考资料