模拟摇杆就是两个互成直角的电位器加一个按键,所以用 analogRead() 读两支模拟引脚就能拿到它 —— 但人人都会先写的那一行 servo.write(map(analogRead(A0),0,1023,0,180)),做出来的云台会自己慢慢爬,一松手又弹回正中。解法是量出你这支摇杆真正停在哪里,再让摇杆的位移决定速度,而不是角度。
做一个摇杆控制的云台,需要哪些零件?
HW-504 摇杆模块是一块黑色小板,顶上一个磨砂黑的拇指帽,十字支架底下藏着两个薄荷绿的电位器,板边是一排直的五针公排针。两个 TowerPro MG90S 微型舵机负责水平和俯仰;它们到手时就是电机加一根引线,所以支架也是这个作品的一部分。
材料清单 —— 模拟摇杆加两个 MG90S 舵机,做 Arduino Uno 云台
选配 —— 舵机独立供电与 USB 数据线
4 节 AA 电池盒,就是「两个舵机把 USB 供电的 Uno 拖到重启」这件事的解法:四节串联标称 6 V,正好落在 MG90S 的 4.8–6 V 额定范围内,而且它只喂舵机。Uno 那个插座认的是全尺寸 USB-B(打印机那种)方头 —— 抽屉里没有这个形状的线就加一条。
两包杜邦线不是买重复了:两头的公母方向正好相反。摇杆模块伸出来的是公针,所以要用公对母杜邦线的母头去套它,另一端才落到 Uno 和面包板上。每根舵机引线的末端是一个三位的母插座,所以要用公对公杜邦线 —— 从 Uno 引到面包板电源轨的那两根也一样。用摇杆键盘扩展板或双摇杆扩展板可以把接线整个省掉;电脑跟 CH340 驱动过不去的话,就选FT232 版 Uno。
模拟摇杆模块到底是怎么工作的?
HW-504 板上那两个电位器,都接成分压器跨在你喂给模块的同一组 +5 V 和 GND 上,十字支架的两个轴各带动一个。摇杆走完整个行程,每个滑动端的电压就从接近 0 V 摆到接近 5 V,而 ATmega328P 的 10 位 ADC 把它换算成 0–1023 —— 大约每个计数 4.9 mV。
那个按键就是一个普通的轻触开关,接在 SW 脚和 GND 之间,模块上没有任何上拉电阻,所以不管它的话,这支脚就是悬空的,读到的是噪声。pinMode(PIN_SW, INPUT_PULLUP) 会把 ATmega328P 自带的 20–50 kΩ 上拉接进来:这支脚从此静态为 HIGH,一按下去就被拉到 LOW。
回中弹簧把摇杆送回的是电气中点附近,不是电气中点本身:十字摆架和电位器碳膜轨道的成型公差,会让一支静止的摇杆停在 490 多到 530 出头之间的任何位置,两个轴不一样,每一块模块也不一样。静止值是 507 的摇杆没有毛病,默认它是 512 才有。

摇杆和两个舵机怎么接到 Uno 上?
Uno 上只有一个 5 V 插孔,而这个作品要分好几路出去,所以让面包板的电源轨把它扇开。先从 Uno 把电源轨喂起来。数引脚之前先看一眼摇杆板上的丝印:它写的是 GND、+5V、VRx、VRy、SW —— 地在最前面,不是大多数模块那种 VCC 打头的顺序。
| 从 | 接到 | 为什么 |
|---|---|---|
Uno 5V 和 GND |
近侧+ 与 − 电源轨 |
两根公对公杜邦线。一个插孔就变成一整条轨 |
摇杆 +5V、GND |
近侧+ 与 − 电源轨 |
两个分压器都由 Uno 的 5 V 供电。这里喂 6 V 会把滑动端电压顶到超过 ADC 自己的参考 |
摇杆 VRx、VRy |
A0、A1 |
ADC 要读的就是这两路滑动端电压 |
摇杆 SW |
D2 |
开关对地;代码里打开内部上拉 |
| 水平舵机橙线 | D9 |
只走信号 —— 每 20 ms 一个 1–2 ms 的脉冲 |
| 俯仰舵机橙线 | D10 |
一样,第二个通道 |
| 两个舵机的棕线 | 远侧− 电源轨 |
让舵机的回流电流不走摇杆那条地 |
| 两个舵机的红线 | 远侧+ 电源轨 |
由电池喂,绝不由 Uno 喂 —— 见供电那一节 |
近侧 − 电源轨 |
远侧 − 电源轨 |
共地。一根线,也是两组电源之间唯一的一根线 |
把两组电源放在面包板的两个长边上。400 孔面包板的每条长边都有自己的一对 + 和 − 电源条,所以 Uno 喂一边、电池喂另一边,而把两条 − 接起来的那根线,是两边之间唯一跨过去的线。要是改成把两条 + 桥起来,6 V 就落到 Uno 的 5 V 脚上 —— 这是这个作品绝对不能接的一根线。没有电池的时候,正是靠这根桥线让舵机吃 Uno 的电,而那只能算一次短暂的空载测试。
9 脚和 10 脚是个合理的默认,不是硬性要求。不管你接哪几支脚,Servo 库都会在第一次 attach() 时把 Timer1 占为己有,而 Timer1 正是产生 9 脚和 10 脚 PWM 的那个硬件 —— 所以反正这两支脚上的 analogWrite() 都会失效。3、5、6、11 脚的 PWM 不受影响。
那行显而易见的 map() 代码,为什么会漂移又抖动?
那行 map() 代码瞄错了数字。静止值 507 的摇杆映射出来是 89°,不是 90°,云台就歪了一度。更麻烦的是,位置模式让云台绝对地跟随摇杆:一松手,弹簧把摇杆送回中位,你刚对准的相机就自己转回房间正中。这是给云台用错了控制律,不是调参数能修掉的 bug。
它的死区是零。ADC 读数本来就会来回跳一两个计数,而 map() 把 1023 个计数摊到 180° 上,5.7 个计数就是一度。一度的指令大约是 10 µs 脉宽,而 MG90S 的死区带是 5 µs —— 于是没人碰的摇杆,也能让齿轮组来回找位。
它的地在动。最容易被怀疑的那一个其实是无辜的:ADC 的参考就是给电位器供电的同一条 5 V 轨,所以这条轨一塌,滑动端电压按同样比例一起塌,读数并不会变。真正抵消不掉的,是回流电流。两个舵机会把几百毫安从共用的地线和面包板触点上拉回去,这条路径上 0.2 Ω 的电阻就把 300 mA 变成 60 mV 的地电位偏移 —— 大约 12 个 ADC 计数,随着舵机动作忽有忽无。
怎么让摇杆控制速度,而不是控制角度?
速度控制只是在摇杆代码上改一小处。把 角度 = f(摇杆) 写成 角度 = 角度 + f(摇杆):摇杆决定的是每一帧加上去的度数,松手就是加零,云台停在你转到的地方。它还顺手白送了大家事后才补上的那层平滑:指令角度每一帧最多只能动 MAX_STEP,所以任何读数 —— 有噪声的、跳变的、别的什么 —— 都没办法让云台动得比这更快。均值滤波是去对付噪声;速率限制则让噪声不管多大都变得无害。
这些数字是舵机定下来的。它每 20 ms 刷新一次,帧再短,也只是在下达它执行不了的动作。摇杆推到底、每帧 1.5° 的话,云台的角速度是 75°/s,走完代码里 10°–170° 的水平行程要两秒出头 —— 从容,也拍得出来。MG90S 的额定速度是 0.08 秒转 60°,也就是 750°/s,所以 1.5° 一步它约两毫秒就走完 —— 走完之后这一帧剩下的时间它都闲着。舵机从来没有在追指令,这就是扫掠看起来平顺而不是一顿一顿的原因。60 个计数的死区是上面那笔地偏移预算的五倍,而每个方向仍然留下 88% 的行程可用。
/*
模拟摇杆(HW-504)-> 两个 MG90S 舵机,用速度控制做云台。
库:Servo(Arduino IDE 自带)。
摇杆 GND -> 电源轨 GND +5V -> 电源轨 5V
摇杆 VRx -> A0 VRy -> A1 SW -> D2(内部上拉)
舵机橙线 -> D9(水平)和 D10(俯仰)
舵机红线 -> 6V 电池正极 舵机棕线 -> GND,与 Uno 共地
摇杆的位移决定角度变化的速度,而不是角度本身。
一松手云台就停在原地。按住按键,它会自己走回中位。
*/
#include <Servo.h>
const uint8_t PIN_VRX = A0;
const uint8_t PIN_VRY = A1;
const uint8_t PIN_SW = 2;
const uint8_t PIN_PAN = 9; // Timer1 的引脚 - 见正文
const uint8_t PIN_TILT = 10;
const int DEADZONE = 60; // 实测静止值两侧各留的 ADC 计数
const float MAX_STEP = 1.5; // 摇杆推到底时每 20 ms 一帧走的度数
const uint16_t FRAME_MS = 20; // 一个舵机刷新周期
const int PAN_MIN = 10, PAN_MAX = 170; // 离机械限位留出余量
const int TILT_MIN = 30, TILT_MAX = 150;
Servo pan, tilt;
int restX, restY; // 上电时实测,而不是假定 512
float panAngle = 90.0, tiltAngle = 90.0;
uint32_t lastFrame = 0, lastReport = 0;
// 多路开关切换之后的第一次转换丢掉,再取八次平均。
int readAxis(uint8_t pin) {
analogRead(pin);
long sum = 0;
for (uint8_t i = 0; i < 8; i++) sum += analogRead(pin);
return (int)(sum / 8);
}
// 摇杆偏移量 -> 每帧度数。死区之内为零,推到底为 MAX_STEP。
float axisRate(int raw, int rest) {
int offset = raw - rest;
if (offset > -DEADZONE && offset < DEADZONE) return 0.0;
int span, past;
if (offset > 0) { past = offset - DEADZONE; span = 1023 - rest - DEADZONE; }
else { past = offset + DEADZONE; span = rest - DEADZONE; }
if (span < 1) span = 1;
float k = (float)past / (float)span;
if (k > 1.0) k = 1.0;
if (k < -1.0) k = -1.0;
return k * MAX_STEP;
}
void setup() {
Serial.begin(115200);
pinMode(PIN_SW, INPUT_PULLUP); // 模块自己没有上拉电阻
// 校准:读出你这支摇杆真正的静止值。setup 期间手不要碰。
restX = readAxis(PIN_VRX);
restY = readAxis(PIN_VRY);
Serial.print(F("rest X = ")); Serial.print(restX);
Serial.print(F(" rest Y = ")); Serial.print(restY);
Serial.println(F(" (near 512, but neither will be exactly 512)"));
pan.attach(PIN_PAN);
tilt.attach(PIN_TILT);
pan.write((int)panAngle);
tilt.write((int)tiltAngle);
}
void loop() {
if (millis() - lastFrame < FRAME_MS) return;
lastFrame = millis();
int x = readAxis(PIN_VRX);
int y = readAxis(PIN_VRY);
if (digitalRead(PIN_SW) == LOW) {
// 按住按键:按同样的速率限制走回中位。绝不瞬移。
panAngle += constrain(90.0 - panAngle, -MAX_STEP, MAX_STEP);
tiltAngle += constrain(90.0 - tiltAngle, -MAX_STEP, MAX_STEP);
} else {
panAngle = constrain(panAngle - axisRate(x, restX), PAN_MIN, PAN_MAX);
tiltAngle = constrain(tiltAngle + axisRate(y, restY), TILT_MIN, TILT_MAX);
}
pan.write((int)(panAngle + 0.5));
tilt.write((int)(tiltAngle + 0.5));
// 实时数值,好让你亲眼看着死区在干活。
if (millis() - lastReport >= 500) {
lastReport = millis();
Serial.print(F("X ")); Serial.print(x);
Serial.print(F(" Y ")); Serial.print(y);
Serial.print(F(" pan ")); Serial.print((int)panAngle);
Serial.print(F(" tilt ")); Serial.println((int)tiltAngle);
}
}
在 Uno 上这份代码编译出来占 6,030 字节 Flash —— 32 KB 的 18% —— 加 252 字节 RAM。串口监视器设 115200,第一行就会打印出你这块模块的静止值。手离开摇杆云台还会慢慢爬,就把 DEADZONE 调大;某个轴方向反了,就把那一次 axisRate 调用前的符号取反;如果模块转了 90° 安装、把两个轴对调了,就把 PIN_VRX 和 PIN_VRY 换过来。
Arduino 的 5 V 脚能带两个 MG90S 舵机吗?
MG90S 的额定电压是 4.8–6 V,空载约 10 mA,转动时 120–250 mA,堵转约 700 mA —— 堵转这个数字是实测值,不是 TowerPro 给的规格。两个一起动就是 240–500 mA,Uno 自己还要 40–50 mA,而 USB 这条路上守着一颗自恢复保险丝,超过 500 mA 就断开。这个作品是骑在那条线上,而不是落在线内,所以它会一直「正常」到俯仰舵机带上负载、板子重启为止。
给舵机单独供电。四节 AA 装在带开关的电池盒里串联,标称 6.0 V,落在 MG90S 的额定范围内,而一组全新碱性电池约 6.4 V,一带上负载就会落下来。电池盒的红线接远侧 + 电源轨,黑线接远侧 − 电源轨 —— 绝不接 Uno 的 5 V 脚。那支脚是稳压器的输出,不是输入;板子插着 USB 的时候,它和 USB 口喂进来的是同一条轨,所以从那里灌进去的 6 V 会倒着穿过稳压器,顺着线缆往你笔电的 USB 口上走。电池盒那两根裸露的线头,在插进面包板孔之前先绞紧。

舵机要装在什么支架上?
MG90S 到手就是电机加一根引线,所以支架是一个要你自己拿主意的设计。在云台里,下面那个舵机扛着上面那个、再加上顶上放的东西,所以它的负载是一段力臂末端的重量。一个俯仰舵机加一个轻支架 —— 3 cm 力臂上 40 g —— 大约是 0.12 kg·cm,而 MG90S 在 6 V 下是 2.2 kg·cm。换成 5 cm 力臂上放一台 200 g 的手机,你要的就是 1.0 kg·cm:账面上还在额定范围里。不过真正把尼龙齿轮打滑牙的,很少是这种稳态负载 —— 而是磕一下、或者一头顶进限位时的冲击,一瞬间全落在一颗齿上。这就是承重那一轴该上金属齿轮的理由。这个选择剩下的部分,看我们的舵机对比指南。
支架要么打印一个,要么自己做一个。MG90S 的输出是一根 21 齿的金属花键轴,直径约 4.7 mm,中心还有一个小螺孔,所以打印出来的花键座压上去就套牢了,再往那个孔里拧一颗 M2 自攻螺丝,就能防止它受力时往上爬出来 —— Smith3D 有耗材也有打印机。
装支架之前先让舵机回到中位。舵机轴上次停在哪就还在哪,所以先把代码跑起来,让 setup() 把两个舵机都驱到 90°,然后把每个支架竖直压到它的花键轴上 —— 是竖直压下去,不是转上去,因为这时舵机正锁在那个角度。装到一根没回中的轴上,云台的机械中位就落在随便什么地方:摇杆往一边推不到头就没角度了,往另一边推就撞限位。
做桌面演示的话,硬卡纸加热熔胶就够用,代码里没有任何一处在意这个 —— 而云台一旦能稳稳停住,顶上最该放的当然是一块 ESP32-CAM。这段组装视频把这个连接拿在手上演示了一遍:

真实客户身上常见的错误
两个舵机都吃 Uno 的电,然后怪摇杆。读数乱跳、云台一顿一顿、板子重启 —— 这三样全是上面那笔电流账,不是摇杆坏了。
手按着摇杆做校准。代码只在 setup() 里量一次静止值,之后每一次读数都是相对它的偏移量。板子上电时手还按着摇杆 —— 或者转到一半按了复位 —— 云台就继承了一个假的中位,于是松手它照样慢慢爬:正是校准本来要消掉的那种漂移。松手,复位,看着那两个静止值稳定在同一对数字上。
在机械上跟舵机较劲。支架其实提前挡住了舵机,你还往里写 0° 或 180°,它就会顶着限位待在那里一直吃堵转电流,直到有东西先坏 —— PAN_MIN 和 TILT_MAX 就是干这个用的。同样的道理,绝不要用手去掰一个通着电的舵机的输出轴。
常见问题
为什么我的 Arduino 摇杆读出来是 512,不是 0?
因为它是好的。摇杆停在行程中点,每个滑动端就落在碳膜轨道的一半上,输出 5 V 的一半 —— 大约是 ADC 那 1023 个计数里的 512。
为什么我的摇杆静止时读不到正好 512?
十字支架和电位器轨道的机械公差。490 多到 530 出头都算正常,而且两个轴不一样。开机时把两个轴都量一遍。
能用 Arduino 的 5 V 脚带两个舵机吗?
只能做一次短暂的空载测试。两个 MG90S 一起动要 240–500 mA,而 Uno 的 USB 供电超过 500 mA 就跳。四节 AA、两边共地,才是靠得住的接法。
为什么一用 Servo 库,9 脚和 10 脚的 analogWrite 就失效了?
它在第一次 attach() 时接管了 Timer1,也就是产生这两支脚 PWM 的那个硬件 —— 不管舵机实际接在哪几支脚上。3、5、6、11 脚的 PWM 照常。
我是不是该买摇杆扩展板,而不是模块?
不必 —— 两者都是接在模拟脚上的电位器,所以代码完全一样。扩展板省掉接线;裸模块可以装进手持外壳里。
最后更新:2026 年 8 月。有问题?WhatsApp 联系我们。



Joystick Module X Y Z axis 5 Pin Analog Joy Stick Controller for Arduino Robotic
SG90 MG90 Tower Pro Micro Servo Motor 9g 180 360 Servo Motor for Arduino Robotic - MG90- 180°
Arduino Uno Compatible SMD UNO R3 with Type B Cable - ATMEGA328P with CH340G-Microcontroller Project
MB102 Breadboard 170 400 830 Holes Breadboard Donut Board Arduino Prototype Multi Color - BREADBOARD (400 HOLES)
40pcs Dupont Wire 10cm 20cm 30cm for Breadboard DIY Experiment Jumper Wire Breadboard wire - DUPONT WIRE M-M 20CM
40pcs Dupont Wire 10cm 20cm 30cm for Breadboard DIY Experiment Jumper Wire Breadboard wire - DUPONT WIRE M-F 20CM
AA Battery Holder with Cover On/Off Switch 2/3/4 Slots Battery AA Holder Red/Black Wire - AA BATTERY CASE (4S)
Data Cable Type-A Type-C MicroUSB Type-B 0.5m 1m 30cm 0.3m 100cm Data Transfer Upload Code - TYPE-A TO TYPE-B (1.0M)