很多玩 Arduino 的新手,第一次接触 SG90 时都会有一个直观感受:三根线、一个舵机臂,代码里写一句servo.write(90),舵机就能转到中间位置。看起来非常简单,但真正上手后,问题往往一口气冒出来:为什么上电后一动不动?为什么一直嗡嗡响?为什么角度总是对不齐?为什么接上两个舵机,Arduino 就自动重启?这些问题的根源,并不是你抄错了代码,而是没有理解 SG90 的本质:它是一个内部自带反馈的“位置伺服系统”,不是一个用 PWM 调速就能转起来的普通小电机。
这篇文章会围绕 180 度位置舵机 SG90 和 Arduino UNO,拆解三件事:第一,脉宽信号和角度之间到底是什么关系;第二,Servo.attach的pin、min、max参数为什么会影响最终控制精度;第三,SG90 内部的位置闭环和你在项目里常听到的“PID 闭环控制电机转速”有什么区别。读完你会得到一个可以直接跑通的接线方案、完整的示例代码、一套排查舵机故障的思路,也能真正分清哪些控制工作已经由舵机替你完成了,哪些闭环问题还需要你重新设计。
我的一个基本判断是:SG90 不是性能很好的舵机,但它几乎是理解“电机→传感器→控制器→执行器”这一整套反馈控制链路的最便宜入口。很多人把时间浪费在换舵机、调脉宽、猜 bug 上,不是因为动手能力差,而是因为一开始就把“位置舵机”当成了“普通减速电机”去理解。只要把这个心智模型纠正过来,后面很多现象都会自己解释清楚。
1. 先搞清楚:SG90 是“位置舵机”,不是普通小电机
1.1 SG90 的结构和工作方式
SG90 常被叫作 9g 舵机,这里的 9g 指向的是它的重量级别,不是一个严格行业标准。它在结构上可以拆成四个部分:一个小型直流电机、一组减速齿轮、一个电位器、一块很小的控制电路板。
直流电机本身转速很高、扭矩很小,直接装上负载根本没法精确控制角度。减速齿轮负责把高速旋转降到合适的输出转速,同时放大扭矩。电位器连接在输出轴上,它会随着舵机臂的转动产生一个与当前角度对应的电压。控制电路板负责读入外部给的脉宽信号,把它转换成一个“目标角度电压”,再和电位器反馈回来“实际角度电压”做比较。
如果两个电压不相等,控制板就驱动电机正转或反转,让输出轴走向目标角度;当两个电压相等,也就是实际角度已经靠近目标角度时,误差变小,电机就会停止。这个过程就是一个非常典型的闭环思想:始终用一个反馈量去修正输出量,直到反馈值等于给定值。
1.2 位置舵机和普通直流电机、步进电机的区别
新手最容易搞混的是三样东西:普通直流电机、步进电机、位置舵机。它们都能转,但控制逻辑完全不同。
| 执行器 | 输入信号 | 是否需要外部反馈 | 能否保持固定角度 | 适合场景 |
|---|---|---|---|---|
| 普通直流电机 | 电压大小、PWM 占空比 | 需要编码器等传感器才能测角度或速度 | 不能,断电后失去力矩 | 驱动轮子、风扇、泵 |
| 步进电机 | 脉冲序列 | 一般开环位置控制,但存在失步可能性 | 能通过保持力矩锁住位置 | 3D 打印机、数控机床 |
| SG90 位置舵机 | 周期 20ms 的脉宽信号 | 不需要,内置电位器反馈 | 能,控制板会不断修正偏差 | 云台、小型机械臂、舵机转向 |
判断为什么,可以从“位置保持”这一点理解。普通直流电机只要不供电,轴就可以被外力转动;步进电机在通电且有保持力矩时能停在一个确定角度,但如果负载过大、丢步了,它并不会主动告诉你角度偏了;SG90 则不一样,它的内部电位器一直在检测角度,只要你给它一个有效脉宽目标,即使外力让舵机臂偏开一点,它也会自己转回目标位置。
1.3 一个需要特别注意的型号问题
市面上常见的 SG90 有一种是“180 度位置舵机”,还有一种是“360 度连续旋转舵机”。两者的外观、线序几乎一样,但行为完全不同:位置舵机是根据脉宽转到指定角度,连续旋转舵机是根据脉宽控制转速和方向,比如 1500us 停止、1000us 正转、2000us 反转。
所以做项目前一定要先确认你买到的是哪一种。如果看到代码示例是基于 180 度角度写的,而舵机却一直转个不停,不要先怀疑代码,先检查型号是不是“连续旋转版”。本项目默认讨论的是 180 度位置舵机。
2. 控制原理:脉宽、PWM 周期与角度映射
2.1 为什么舵机不能用 analogWrite 方法控制
很多新手会用驱动直流电机的思路去驱动舵机,试图用analogWrite(9, 128)来让舵机转到中间。结果往往只有两种情况:舵机完全没反应,或者发出尖锐的抖动声。
原因是普通直流电机的 PWM 调速,关注的是整个周期内“开”的时间比例,也就是占空比。占空比越大,平均电压越高,电机越快。舵机并不是用平均电压来调速的,它关心的是信号中高电平到底持续了多少微秒,也就是脉冲宽度。analogWrite输出的是一个固定频率、不断调整占空比的方波,它和舵机需要的“宽脉冲 + 固定间隔”信号完全不同。
SG90 的控制信号通常是这样的:每隔 20ms 产生一次高电平脉冲,这个 20ms 对应频率是 50Hz。在这 20ms 中,高电平持续的时间从 0.5ms 到 2.5ms 变化,不同的脉冲宽度对应不同的目标角度。
| 高电平脉宽 | 常见对应位置 |
|---|---|
| 500us | 约 0 度 |
| 1000us | 约 45 度 |
| 1500us | 约 90 度(中位) |
| 2000us | 约 135 度 |
| 2500us | 约 180 度 |
这里需要强调:500us 到 2500us、0 度到 180 度是一个典型映射,不同厂家、不同批次的舵机端点存在差异。有的舵机实际范围可能是 550us 到 2450us,有的可能是 400us 到 2400us,以实体舵机数据手册和实际校准为准。
2.2 Arduino 里 write 与 writeMicroseconds 的关系
Arduino 的 Servo 库封装了底层细节。调用servo.write(90)时,库会先看当前舵机实例使用的是哪一组最小和最大脉宽,然后把 0 到 180 的角度等比映射到脉宽区间里。
如果舵机是默认attach(9),库在 AVR 平台上的典型默认范围大约是 544us 到 2400us。也就是说,write(0)发出去的并不是理论上的 500us,而更像是 544us;write(180)发出去的更像是 2400us。这对于支持 500us 到 2500us 的 SG90 来说,通常可以工作,但末端角度可能不会精确落在机械极限上。
如果你想绕过角度换算,直接控制高电平脉宽,可以用servo.writeMicroseconds(1500),这表示直接输出 1500us 的脉宽。在校准舵机中位、测试机械极限时,writeMicroseconds比write(angle)更直观、更可控。我把这两者的关系总结成一句话:write是给普通人看的角度层,writeMicroseconds是真正作用在电气层面的脉宽层。
2.3 SG90 内部的位置闭环是怎样完成的
前面说的脉宽信号,本质上只是告诉舵机“请到某个位置”,它不是一个持续输出扭矩的调速指令。SG90 的控制电路板会把输入脉宽转换成一个参考电压,再和电位器反馈电压比较。
当两者相等时,误差为零,电机不转;当两者不等时,控制板决定电机的旋转方向和大致驱动强度,直到误差进入一个很小的死区。这个死区就是为什么舵机定位后经常存在几度误差的原因之一,也是伺服系统稳定工作的必然设计。
注意,SG90 内部使用的未必是你在书本上看到的标准 PID 算法,甚至很多廉价舵机使用的只是“误差正负判断 + 死区滞环”这类简化逻辑。但从控制论角度看,它已经构成了一个完整的反馈闭环。对 Arduino 这一侧的代码来说,你只是发出一个位置目标,并不需要每毫秒去感知并纠正舵机的实际位置。
3. Arduino UNO 接线与供电:一个容易翻车的环节
3.1 三根线怎么接
SG90 常见三根线的颜色是棕色、红色、橙色或黄色。大多数情况下:
| 舵机线 | 常见颜色 | 作用 | 接到 Arduino UNO |
|---|---|---|---|
| 电源负极 | 棕色 | GND | 接 Arduino GND |
| 电源正极 | 红色 | VCC,通常接 5V | 接 Arduino 5V |
| 信号线 | 橙色/黄色 | 控制信号 | 接任意数字引脚,例如 D9 |
接线的第一步不是直接插杜邦线,而是先看舵机插头周围的丝印。不同厂家对三针插座的定义可能存在差异,有的插座顺序是“正极、负极、信号”,有的则是“信号、正极、负极”。如果是用卖家提供的三针插头,电源正负极接反会造成舵机损坏,所以必须先确认丝印,不能只依赖颜色。
3.2 电源供电是最大的坑
只用一个 SG90 做 180 度空载扫描时,直接接 Arduino UNO 的 5V 和 GND 基本能跑起来。但舵机在启动和堵转时会出现短时大电流,USB 口供电能力又普遍有限,所以当你接两个舵机、或者舵机臂上带了负载、或者动作非常频繁时,Arduino 会出现重启、舵机乱跳、持续抖动等奇怪现象。
更稳妥的连接方式是给舵机单独准备一个 5V 外部电源,同时把外部电源的负极和 Arduino 的 GND 接到一起。这里必须强调“共地”:控制信号是相对地平面的电压,如果舵机电源和 Arduino 不共地,信号线在舵机控制板看来可能是悬空的,导致舵机完全无反应或随机乱转。
如果使用的是 9V 电池或 12V 适配器,不能直接接到舵机 VCC,超压会损坏控制板。需要先通过 5V 稳压模块或降压模块把它变成 5V 再供电。很多新手电源模块选错,导致舵机要么没力,要么发烫,其实不是舵机坏了,而是电压根本不是舵机需要的工作电压。
3.3 安全问题:不要让舵机长时间堵转
另一个容易忽略的点是堵转。当舵机输出轴已经转到机械极限,但你继续要求它转得更远,电机处于堵转状态,电流会显著上升,齿轮也会持续受力。短时间内可能只是出现“咔咔”声,时间一长就可能烧坏控制板或打坏齿轮。
在程序中设置角度范围时,要避免让舵机长时间顶着机械限位;在手动校准限位时,也应该短时点动测试,发现声音变尖或明显卡住就立刻释放指令。这一点对任何小舵机都适用。
4. Servo.attach 参数详解与脉冲范围校准
4.1 attach 的几种常见写法
Servo 库的attach方法在 Arduino UNO 上通常有几种用法:
#include <Servo.h> Servo myServo; void setup() { myServo.attach(9); } void setup() { // 自定义最小脉宽和最大脉宽 myServo.attach(9, 500, 2500); }第一种写法只传信号引脚,库使用默认脉宽范围。第二种写法指定了引脚、最小脉宽和最大脉宽。这里的min会被当作内部角度 0 度时输出的脉宽,max会被当作角度 180 度时输出的脉宽,中间角度按线性插值计算。
为什么需要手动指定?因为 SG90 的标称范围和你实际手上的硬件写出来的并不完全一致。有些 SG90 在 500us 到 2500us 的范围内工作,但可能 500us 会让舵机臂顶着左限位发出异响,而实际最左端只到 520us。如果不校准,就很容易出现“角度到不了指定位置”或者“一端正常、一端卡死”的情况。
pin参数也不一定必须是 Arduino 上标有 PWM 波浪号的引脚。Servo 库在 UNO 上是通过定时器中断模拟出精确的脉宽信号,所以很多数字引脚都可以用于舵机控制。真正需要留意的是:当你使用 Servo 库时,它会占用 UNO 上的定时器资源,可能会让某些引脚的analogWrite功能不可用。具体占用情况和库实现、板卡型号有关,最好的做法是:一个引脚只承担一种控制方式,不要既写 Servo 又对它做analogWrite。
4.2 为什么角度对不齐:中位校准方法
刚拿到舵机时,建议先做一次最简单的中位校准。目的不是测试硬件好坏,而是让后续安装舵机臂有一个稳定参考,也让代码里的角度和机械结构能对齐。
校准流程可以这样理解:
- 先不要把舵机臂固定得太紧,通电前先让代码输出 1500us 脉宽,把舵机转到中位。
- 等舵机稳定后,把舵机臂装在一个你觉得合适的方向,例如让臂和舵机机身大致垂直。
- 再用
writeMicroseconds从 1500us 开始,每次增加一点脉宽,找到舵机能稳定到达、并且不会卡死机械结构的最右端。 - 再从 1500us 开始每次减小脉宽,找到最左端。
- 把找到的两个端点稍作收缩,写进
attach(pin, leftUs, rightUs)。
这种校准属于最基本的“角度偏置校准”,它不能消除齿轮间隙带来的回程差,也不能提高舵机的绝对精度,但能保证你代码里的 0 到 180 和实际情况基本吻合。对 SG90 这类低成本舵机来说,做到这一步已经比绝大多数直接抄默认例程的行为稳健很多。
5. Arduino UNO 完整示例代码与运行验证
下面代码都在 Arduino UNO 上以信号线接 D9 为例。接线时,舵机电源如果来自 Arduino 5V,只适合做单舵机空载学习;如果使用外部 5V 电源,记得把外部电源负极和 Arduino GND 接在一起。
5.1 示例一:0 到 180 度往复扫描
这个例子是最常见的舵机扫描验证程序,用来确认舵机、接线、代码链路是通的。
#include <Servo.h> Servo myServo; void setup() { myServo.attach(9); myServo.write(90); delay(500); } void loop() { for (int angle = 0; angle <= 180; angle += 1) { myServo.write(angle); delay(15); } for (int angle = 180; angle >= 0; angle -= 1) { myServo.write(angle); delay(15); } }代码逻辑是每 15ms 让目标角度增加或减少 1 度,从而让舵机来回转动。这里的delay(15)并不保证舵机已经转到位,只是给舵机一个反应和运动的时间。如果延时太小,舵机实际转速跟不上角度更新速度,就会出现后段角度越转越小、机械跟不上指令的情况。
运行后,如果舵机从一端摆到另一端,说明信号链路没问题。如果某一端出现“咔咔”声或舵机顶死,说明 0 度或 180 度已经超出你这个舵机支持的机械或程序范围,应该把扫描范围改小,比如从 10 度到 170 度,而不是强制让舵机去撞限位。
5.2 示例二:串口发送角度控制
很多云台项目需要根据上位机指令实时控制舵机角度。下面这个程序从串口读取整数,并控制舵机转到对应角度。
#include <Servo.h> Servo cameraServo; void setup() { Serial.begin(9600); cameraServo.attach(9, 500, 2500); cameraServo.write(90); Serial.println("Ready. Input angle 0-180:"); } void loop() { if (Serial.available() > 0) { int angle = Serial.parseInt(); if (angle >= 0 && angle <= 180) { cameraServo.write(angle); Serial.print("Target angle: "); Serial.println(angle); } else { Serial.println("Angle out of range. Please input 0-180."); } } }Serial.parseInt()会从串口缓冲区中读取一个整数。在 Arduino IDE 的串口监视器里,输入45、90、120这类数字并发送,舵机会切换到对应角度。编译器或库版本不会影响这段代码的核心逻辑,但需要保证串口波特率和代码一致,也就是 9600。
验证成功的标准是:串口输入 90,舵机转到中位;输入 0,舵机转到程序允许的位置;输入超范围数字,串口打印提示而不是驱动舵机。如果串口收到了0,但舵机没有动,先检查attach是否在setup里执行成功,再检查信号线是否插在端口 9 而不是端口 9 旁的 GND。
5.3 示例三:用 writeMicroseconds 手动校准
这个示例更适合配合上一节的校准流程,直接用串口设置微秒级脉宽,用来观察舵机真实极限。
#include <Servo.h> Servo caliServo; void setup() { Serial.begin(9600); caliServo.attach(9, 500, 2500); caliServo.writeMicroseconds(1500); delay(500); Serial.println("Calibration mode. Send 400-2600 (us):"); } void loop() { if (Serial.available() > 0) { int us = Serial.parseInt(); if (us >= 400 && us <= 2600) { caliServo.writeMicroseconds(us); Serial.print("Set pulse width: "); Serial.print(us); Serial.println(" us"); } else { Serial.println("Please send pulse width between 400 and 2600."); } } }运行后可以手动输入 500、1000、1500、2000、2500,观察舵机位置。建议先从 1500 开始,再逐渐往两边试探,不要第一次就输入 2500 强行让舵机撞限位。如果发现 2000 度已经到机械极限,而 2500 时出现卡死声音,就说明你需要把attach(pin, min, max)中的max调小,或者把代码可写入的操作范围限制在安全区域内。
5.4 一个容易误解的函数:read 读出来的是什么
在编写更复杂动作时,有人会写这样一段代码:
int currentAngle = myServo.read(); myServo.write(currentAngle + 10);这段代码本身没有语法错误,但需要理解read()返回的并不是舵机当前真实的物理角度,而是 Servo 库最后一次写入的目标角度。如果舵机还没转到位、或者被外力掰开、或者因为堵转没跟上,read()的结果都不会反映真实位置。
SG90 内部有电位器,但它的反馈信息只供内部控制电路使用,并不会通过一根线把当前角度送回 Arduino。如果你真的需要读取舵机当前角度做闭环判断,需要换用带角度反馈的总线舵机,或者外接角度传感器来测量输出轴位置,而不是指望read()。
6. 什么时候才算真正需要自己写闭环控制
6.1 项目的“闭环”到底在哪里
很多教程会提到“SG90 舵机闭环控制”,这句话容易让人误以为需要自己给舵机加 PID。实际上,SG90 已经承接了最底层的位置闭环:输入位置目标,内部电位器作为位置反馈,控制板负责纠正偏差。
因此,从 Arduino 的角度看,你控制 SG90 的方式是开环的。你并没有去读取舵机当前角度,也没有计算误差,更没有把误差送回给舵机,你只是不断给新目标。真正做位置闭环的是舵机内部那块你看不到的控制板。这个分层非常重要:单片机充当了上层的“轨迹规划器”,舵机内部完成了下层的“执行器伺服”。
6.2 外部轨迹规划不是闭环控制
有一种代码在常见开源项目里很流行,它让舵机从一个角度平滑移动到另一个角度:
void moveGradual(Servo& s, int startAngle, int endAngle, unsigned long stepDelay) { if (endAngle == startAngle) { return; } int direction = (endAngle > startAngle) ? 1 : -1; int current = startAngle; while (current != endAngle) { current += direction; s.write(current); delay(stepDelay); } }这段代码看起来像闭环控制,但它没有测量真实角度,也没有根据误差动态调整输出,只是在软件层按固定步长生成一系列目标角度,本质上属于“开环轨迹规划”。它可以实现视觉上的平滑运动,但不能修正舵机因为负载、齿轮间隙、外力干扰造成的偏差。
如果项目里真的需要闭环,比如云台被风吹偏后能自动回中、机械臂关节在负载变化下能保持精度,那么反馈必须来自真实物理量。你用外部角度传感器去测量负载端位置,再把误差交给 PID 去修正,这就进入了真正的“外部闭环”范畴。不要把软件延时造成的平滑运动和真正的反馈闭环混为一谈。
6.3 想练习 PID 应该换什么硬件
搜索词里经常出现“pid 闭环控制电机转速”,这说明很多人想在硬件上练习 PID。SG90 并不是一个适合练 PID 的平台,因为它的反馈闭环已经封装在内部,外部拿不到可靠的实时角速度或位置数据,也没有参数可以调。
更合适的学习路径是:
- 准备一个带霍尔编码器的直流减速电机,用 L298N、TB6612 或 DRV8833 驱动。
- 用 Arduino 的中断读取编码器脉冲,换算出电机转速。
- 先做最简单比例控制,再逐步加入积分和微分,观察系统响应。
- 也可以换用带磁编码器的步进电机或直流无刷电机做角度闭环。
这些平台的好处是,反馈需要通过你写的代码获取,控制量也需要你计算 PWM 占空比,这样才能真正体会 PID 的 P、I、D 三项各自解决的问题。SG90 和 PID 之间的关系,更多是“概念参照”,而不是“练手平台”。我觉得这个判断对初学者尤其重要,能避免你把三行舵机代码误解成闭环控制项目。
7. 运行结果、效果验证与常见问题排查
7.1 怎么判断舵机控制真的成功
判断一个舵机入门实验是否成功,不要只看“它能动”。建议用三个标准:
- 代码写入指定角度后,舵机能稳定停在目标附近,没有持续抖动和明显异响。
- 用手轻轻拨动舵机臂到偏转位置后,停止施力,舵机会自动回到目标位置附近。
- 串口示例中,输入 0、90、180,舵机角度不会出现明显错位,也不会发生一端顶死。
如果第一步都过不了,说明你的舵机可能没有在有效控制状态。如果第二步做不到,要么舵机控制板损坏,要么信号没有持续刷新。Servo 库在attach之后会持续周期性刷新信号,正常情况下舵机应该保持“锁住”的状态。
7.2 经典问题排查表
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 舵机完全不动 | 电源没接对、信号线接到非数字引脚、程序中没调用 attach | 检查接线颜色和丝印;检查代码是否在 setup 调用 attach;看串口是否正常打印 | 重新接线;确保信号线接 D9;启动后先执行 attach 和 write |
| 舵机乱转或跳变 | 信号受到干扰、电源电压不稳定、代码里角度更新频率过高 | 观察供电指示灯是否闪烁;用较短杜邦线连接;降低 write 调用频率 | 加 100uF 电解电容;使用外部稳压电源;缩短信号线长度 |
| 舵机持续嗡嗡响 | 供电电流不足、目标角度卡在机械极限、舵机 dead zone 过窄 | 把舵机臂拆掉再测试;减小角度范围;听是否有齿轮打滑声 | 增强供电;不要顶死限位;程序中限制角度范围 |
| 角度偏了一点 | 舵机机械误差、安装时舵机臂未在中位、脉宽范围不匹配 | 用 writeMicroseconds(1500) 验证中位;检查初始装配方向 | 重新做中位校准;微调 attach 的 min 和 max |
| 一端正常一端咔咔响 | 代码里的 0 或 180 超出该舵机实际机械限位 | 手动用小步脉宽测试两端极限 | 把扫描范围改为 10 到 170,或修改 attach 边界值 |
| 运行一段时间发烫 | 负载过大、持续堵转、供电电压过高 | 摸舵机外壳温度;检查舵机是否有阻力是否能自由转动 | 降低负载;停止堵转测试;确认供电电压为典型 5V |
每个问题出现时,第一步不要急着改代码,而是先看电源。舵机异常十有八九是供电引起的,只有排除电源问题,再去查信号线和代码才有意义。