第一次把 SG90 舵机、电位器、OLED 屏拼到 Arduino UNO 上时,很多人都会有一个错觉:这三个模块我都单独跑通过,怎么连在一起后反而什么都不动了?拧电位器,舵机没反应;OLED 屏幕要么不亮,要么一直显示同一个数字;串口监视器里,angle的值却可能非常正常。于是你开始理解,真正难的不是某个模块的库怎么用,而是当你把“输入采集、数值转换、执行输出、可视化反馈”四条链路接在一起时,任何一环都可能成为断点。
这个项目的标题叫“Arduino UNO 电位器 + OLED + SG90 舵机联动验证”,听起来像一个入门实验,但它的价值远不止“让舵机转起来”。它真正训练的是系统级联调的能力:先让每个中间状态可见,再判断问题出在输入、处理还是执行层。这篇内容就围绕这个目标展开,我会把接线、代码、排查、边界全部拆开讲一遍,尽量让没有硬件经验的人也能按着做出来。
1. 这个联动验证真正的价值:让每一段信号都被看见
1.1 单模块演示最容易造成“假成功”
如果单独写一段代码让舵机转到 90 度,上传后看到舵机动了,你会觉得伺服控制不过如此。接着点亮一块 OLED,看到文字显示出来,又会觉得 I2C 显示也没什么神秘。然后你把这几个东西放在同一个工程里,问题开始暴露:
- 舵机一转,OLED 屏突然白屏或重启。
- OLED 模块和舵机同时占用引脚,程序编不过去。
- 电位器读数在 200 到 800 之间跳动,舵机跟着来回抖。
- 舵机角度看起来是反的,或者转到某个位置就发出“咔咔”声。
这些现象说明一件事:单模块成功不意味着系统成功。模块一多,引脚、电压、时序、库、GND 就全混在一起,任何一段信号链路断开,整套设备的行为都会变得不可解释。这也是“联动验证”比“点灯实验”更有价值的原因。
那为什么 OLED 在这个项目里不是可有可无的装饰品?因为它承担了一个关键职责:把电位器的输入状态可视化。如果不加 OLED,只用电位器直接控制舵机,一旦舵机没反应,你根本分不清是电位器坏了、电位器没接对,还是舵机没供电。加了 OLED 之后,舵机没动但你看到屏幕上的角度在变化,就能立刻知道输入和处理链路没问题,问题多半出自执行层或电源层。
1.2 从输入、处理、执行、显示四条链路看这个项目
这个联动项目表面上只涉及三个硬件模块,实际上可以拆成四条信号链路:
输入链路:电位器。电位器的旋转位置会改变一个引脚上的模拟电压,UNO 的 ADC 把这个电压读成 0 到 1023 的整数。
处理链路:Arduino 程序。读取到的原始模拟量需要从 0~1023 映射到舵机目标角度 0~180,同时可能做滤波、限幅、约束等处理。
执行链路:SG90 舵机。单片机发出指定角度对应的 PWM 脉冲,舵机内部电路驱动电机转到目标位置。
可视链路:OLED 屏幕。当前角度、串口读数、状态信息通过 I2C 总线显示出来。
有经验的工程师看到这个结构会立刻意识到,这就是一个典型的“传感器 -> 处理 -> 执行器”自动化闭环雏形,OLED 则是“人机交互调试窗口”。没有调试窗口时,系统是一个黑盒,你只能看最终执行结果;有调试窗口后,系统内部状态对你透明,你可以在主循环里随时判断哪一层出了问题。
这也是我给这个项目定位的主判断:它真正教会你的不是 map 函数怎么用,也不是某个舵机库的参数,而是“先让状态可见,再逐段验证”的调试思维方式。这种方式可以平移到后续几乎所有的 Arduino、ESP32、STM32 项目里。
2. 接线与供电:硬件链路比想象中更容易断
2.1 一张接线表确定物理拓扑
先不要急着上传代码。Arduino 项目的物理连接是第一步,也是最容易出错的一步。很多人习惯按网上找到的图片插线,但不同厂家模块的丝印、颜色、引脚顺序可能不同。建议先拿一张纸画出你自己的接线拓扑,再对照实物逐针确认。
下面是这个项目最常见的接法,基于 Arduino UNO:
| 模块 | 引脚 | 接到 UNO | 说明 |
|---|---|---|---|
| 电位器 10kΩ | 任意外侧脚 | 5V | 另一次外侧脚接 GND,内侧脚接 A0 即可 |
| 电位器 | 中间抽头 | A0 | 模拟输入读取旋转位置 |
| SG90 舵机 | 棕色线 | GND | 视模块不同颜色可能不同,以说明书为准 |
| SG90 舵机 | 红色线 | 5V 或外部 5V 电源 | 供电正极 |
| SG90 舵机 | 橙色/黄色线 | D9 | 信号线,选择支持 PWM 的引脚 |
| OLED SSD1306 | GND | GND | 共地很重要 |
| OLED SSD1306 | VCC | 5V | 常见 3.3V/5V 兼容,以模块标注为准 |
| OLED SSD1306 | SCL | A5 | UNO 的 I2C 时钟引脚 |
| OLED SSD1306 | SDA | A4 | UNO 的 I2C 数据引脚 |
这里最容易被忽略的是电位的两个外侧脚。很多人随手接到 A0 和 GND,结果中间抽头没有完整参考电压,读数范围会变得很奇怪。电位器本质上是一个分压器,外侧两端必须分别接到电源和地,中间抽头才能输出 0V 到 VCC 之间的连续电压。
SG90 的信号线尽量接在 D9、D10 这类硬件 PWM 能力强的引脚上。Arduino UNO 的数字引脚并不都是同一个脉宽调制实现方式,Servo 库通常可以接管定时器,但不同定时器冲突时会出现莫名其妙的问题。稳妥的做法是接在 D9。
2.2 供电是最大的隐藏坑
整个项目里,最容易让新手放弃的不是代码,而是供电。
SG90 是小舵机,但“小”不代表它不耗电。舵机启动瞬间、带负载转动、被外力堵转时,电流都会明显上升。如果直接从 UNO 的 5V 引脚给舵机供电,PC USB 口或普通充电头提供的电流一旦不足,UNO 板载稳压器的输出电压会被拉低,导致 OLED 不断重启或单片机直接复位。外部现象很可能就是“舵机刚动了一下,OLED 就灭了,程序好像重跑了一遍”。
所以我的建议分两种场景:
- 只做功能验证、电位器转得很慢、舵机没有额外负载:直接用 UNO 的 5V 给舵机供电通常能转起来,但注意不要让舵机长时间堵转。
- 想让舵机稳定响应,或者在舵机上加了机械结构:最好单独准备一个 5V / 2A 以上的外部电源给舵机供电,同时把外部电源的 GND 和 UNO 的 GND 接到一起。这样信号有统一参考地,电流又不经过 UNO 的稳压器,才能避免相互干扰。
注意:舵机电源负极端和 UNO 的 GND 必须共地。如果只给舵机接电源,却不跟 UNO 共地,信号线发出的 PWM 就没有稳定的参考电平,舵机表现会非常随机。
OLED 模块的供电最好也确认一下。大多数 0.96 寸 SSD1306 I2C 模块板上带有稳压电路,可以直接接 5V;但也有个别模块只允许 3.3V,接到 5V 可能发热甚至损坏。要以你手里具体模块的丝印说明为准。
2.3 烧录前三分钟静态检查
接线完成后,不要直接插上电脑烧程序。先做一个简单但极其有效的静态检查:
- 核对每一个 GND 是否连到了同一地。
- 核对舵机信号线是否插在 D9,而不是 A4 或 A5。
- 核对 OLED 的 SCL、SDA 是否接反。很多新手把两根线一交叉,屏幕就没有反应,而 Arduino IDE 编译依然正常。
- 用万用表或目测确认没有 5V 和 GND 直接短接。
- 检查舵机插头是否插紧。面包板上的杜邦线只要稍微松一点,就会出现“偶尔转、偶尔不转”的幽灵故障。
如果手头暂时还没有实物,可以用 Wokwi 这类在线仿真平台先把逻辑跑通。仿真能帮你验证代码结构,确认 I2C 地址、舵机引脚、map 映射值是否合理。但要注意,仿真不会真实模拟舵机的启动电流和电源跌落,所以你仍然需要回到实体板子上做一次完整供电测试。
3. 软件实现:把读取、控制、显示拆成三层
3.1 读取电位器的真实值:单次读取通常不够稳
Arduino 的analogRead(A0)会返回一个 0 到 1023 的整数,对应 A0 引脚的电压从 0V 到 5V。这个数字本身不难理解,但实际使用中有一个常见问题:电位器转动时,接触点会产生微小噪声,读取到的数组会发生抖动。
最常见的错误是直接这样写:
int angle = map(analogRead(A0), 0, 1023, 0, 180);表面上看没问题。电位器从一端转到另一端,舵机从 0 度转到 180 度。但实际转动时,舵机角度会不断小幅跳动,尤其是在中间位置,OLED 上显示的角度可能一会儿 87,一会儿 89,始终稳定不下来。
这不是map函数的问题,而是你直接把单次模拟采样的噪声当成有效信号了。更稳妥的方式是连续读几次求平均,或者做滑动滤波。例如:
int readFilteredPot() { long sum = 0; for (int i = 0; i < 8; i++) { sum += analogRead(POT_PIN); delay(2); } return (int)(sum / 8); }这段代码会把 8 次采样结果平均成一个值,对轻微抖动有很好的抑制作用。代价是每次读取需要多花十几毫秒,对于舵机控制这种低速应用完全足够。如果你想让响应更平滑,后面还可以再加阈值判断,角度变化超过 2 度才执行舵机写入。
3.2 SG90 需要的不是“角度值”,而是一段稳定 PWM 脉冲
很多人会误以为servo.write(90)就是直接输出 90 度对应的电压,实际上背后是定时器产生的 PWM 脉冲。标准 SG90 的输入信号通常是 20ms 周期,脉宽约 0.5ms 到 2.5ms,分别对应 0 度和 180 度。不同厂家、不同型号的参数会有差异,这也是为什么有的舵机在某些角度会“嗡嗡嗡”地响,因为脉冲宽度已经超过了它的实际机械行程。
所以在代码里,我建议先保留一段安全范围:
int angle = map(raw, 0, 1023, 5, 175); angle = constrain(angle, 5, 175);把角度限制在 5 到 175 度,而不是 0 到 180 度,可以避免电位器拧到两端时舵机持续撞向机械限位。这一步看似保守,但对舵机寿命有很大影响。
舵机库初始化时,也可以用三参数版本指定脉冲宽度边界:
servo.attach(SERVO_PIN, 500, 2400);这里的 500 和 2400 单位是微秒,分别表示最小脉宽和最大脉宽。如果你不确定手里舵机的参数,先用默认servo.attach(SERVO_PIN)更安全。等你确认舵机在 0 和 180 度都不会发出异常声音后,再考虑是否调整脉宽阈值。
还需要区分一个重要概念:Servo 库写入的角度是“目标角度”,不是“实际反馈角度”。SG90 这种模拟舵机在收到指定 PWM 后,内部会去驱动电机往目标位置走,但它不会把自己的实际位置反馈给 Arduino。OLED 上显示的 90 度,只代表你希望舵机停在 90 度,不代表舵机真的精确停在了 90 度。如果外物挡住舵机,它会持续堵转并发热。这个特点在后面的适用边界里还会再强调。
3.3 OLED 显示:让中间状态肉眼可见
OLED 在这个项目里的核心作用是调试可视化。当你转动电位器时,如果 OLED 能实时显示角度,你就能快速判断问题是不是出在舵机执行段。
SSD1306 是比较常见的 I2C OLED 驱动芯片,Arduino 社区通常会用 Adafruit 的两个库配合使用:
- Adafruit SSD1306
- Adafruit GFX Library
安装方式很简单:在 Arduino IDE 的库管理器里搜索并安装这两个库。代码里通常这样定义显示对象:
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);最后一个-1表示 OLED 的复位引脚没有接到 Arduino 上,这很常见。初始化时,如果你的模块地址不是默认的 0x3C,换成 0x3D 再试一次:
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { Serial.println("SSD1306 init failed"); while (1); }关于显示,有一个新手常犯的毛病:在loop里反复调用display.clearDisplay()和display.display(),每秒刷几十次,结果 OLED 内容会闪,甚至拖慢主循环。OLED 本身适合显示静态或低频变化的内容,不需要每毫秒都刷新。用一个时间阀值做节流,比如 50ms 更新一次,体验会好很多:
unsigned long lastDisplayTime = 0; const unsigned long displayInterval = 50; void loop() { int angle = getTargetAngle(); servo.write(angle); if (millis() - lastDisplayTime >= displayInterval) { lastDisplayTime = millis(); updateDisplay(angle); } }如果你买的是普通 SSD1306 模块,屏幕上显示 ASCII 英文和数字是最省事的。需要显示中文“角度”这类汉字时,普通驱动库并不内置中文字库。要么更换带有中文字库的 OLED 模块,要么用取模软件把汉字转成点阵数组。单纯把一个中文字符用display.print()打上去,通常得到的是乱码或空白。
从 I2C 总线角度看,OLED、其他 I2C 传感器等都共用 A4、A5 两个引脚,只要地址不冲突,逻辑上可以并行。但如果总线上挂了很多设备,建议把 I2C 速度调低,并且把所有设备的 GND 落实到位,否则会出现随机黑屏。
4. 完整联动流程:从第一行代码到真正跑起来
4.1 一个最小可编译示例与主循环设计
下面给出一份可以使用的示例结构。代码把读取、舵机控制、显示、串口日志拆到不同函数里,目的是让你能单独注释某一块来排查问题。
#include <Servo.h> #include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> #define OLED_ADDR 0x3C #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 #define POT_PIN A0 #define SERVO_PIN 9 Servo servo; Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1); unsigned long lastDisplayTime = 0; const unsigned long displayInterval = 50; int readFilteredPot() { long sum = 0; for (int i = 0; i < 8; i++) { sum += analogRead(POT_PIN); delay(2); } return (int)(sum / 8); } void setServoAngle(int angle) { angle = constrain(angle, 5, 175); servo.write(angle); } void updateDisplay(int angle) { display.clearDisplay(); display.setTextSize(2); display.setTextColor(SSD1306_WHITE); display.setCursor(0, 0); display.print("Angle:"); display.setCursor(0, 28); display.print(angle); display.print(" deg"); display.display(); } void setup() { Serial.begin(9600); servo.attach(SERVO_PIN); if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) { Serial.println("SSD1306 allocation failed"); while (1); } display.clearDisplay(); display.display(); delay(200); } void loop() { int raw = readFilteredPot(); int angle = map(raw, 0, 1023, 5, 175); setServoAngle(angle); if (millis() - lastDisplayTime >= displayInterval) { lastDisplayTime = millis(); updateDisplay(angle); } Serial.print("raw="); Serial.print(raw); Serial.print(" angle="); Serial.println(angle); }几个值得注意的设计点:
readFilteredPot()做了 8 次采样,降低读数抖动。setServoAngle()内部再次constrain,防止电位器转到底时舵机冲向机械极限。updateDisplay()只负责显示,不和舵机控制逻辑混在一起。Serial输出保留了原始模拟量和映射角度,方便你在 OLED 不可用时用串口监视器观察内部状态。- 显示刷新用
millis()做 50ms 节流,不会每轮循环都全屏刷新。
如果你用的是不同 I2C 地址的 OLED,把OLED_ADDR改成 0x3D。如果屏幕依然不亮,先不要继续改代码,回到接线和总线扫描排查。
4.2 通电试运行时的预期和检查点
下载代码之前,先明确每一步应该看到什么。
推荐按下面顺序验收:
| 阶段 | 操作 | 通过标准 |
|---|---|---|
| 1 | 不接舵机,单独烧录并打开串口监视器,转动电位器 | 串口打印的 raw 值随电位器平滑变化,从 0 附近到 1023 附近 |
| 2 | 接上 OLED,重新上电 | 屏幕不花屏,能显示“Angle: 0 deg”等初始文本 |
| 3 | 只执行servo.write(0)、servo.write(90)、servo.write(175)各一次 | 舵机能分别转到对应大致位置,没有明显异响 |
| 4 | 接上舵机,缓慢转动电位器 | OLED 角度持续更新,舵机跟随转动,松开时角度稳定 |
| 5 | 快速转动电位器 | 画面可能有短暂延迟,但不复位,舵机不剧烈抖动 |
第 1 步很多人会省略,但恰恰是它能帮你区分“电位器坏了”和“后面代码错了”。如果第 1 步的raw值始终是 0 或 1023,说明电位器接线或电源有问题。第 3 步则是排除舵机本身的问题。如果舵机单独都动不起来,那后面联动也不会正常。
我在实际测试时,通常会先执行第 3 步并把舵机信号线拔掉,人为制造一次“舵机没接”的状态,观察 OLED 是否还显示角度变化。如果 OLED 正常、舵机没接,说明输入到显示链路完整;再接上舵机,问题再出现,电源或信号线就是最大嫌疑。
4.3 常见故障排查顺序
一个联动项目出问题时,最忌讳的就是“看哪里不顺眼就重插哪里”。应该按链路顺序排查:
先看现象。舵机完全不动、OLED 不亮、OLED 亮但不更新、舵机乱跳,这几种现象的原因差别很大。
再看输入。串口监视器里的raw和angle是否随电位器转动?如果raw正常但angle异常,检查 map 起止值和电位器行程;如果raw始终不变,检查 A0 接线、电位器两端电源。
再看环境。是否共地?SG90 是否使用了外部供电?UNO 是否仅靠电脑 USB 口供电?OLED 的 SCL、SDA 是否接反?I2C 地址是 0x3C 还是 0x3D?
再看参数。角度是否被constrain到了合法的 5~175 范围?舵机是否使用了过大的脉宽边界?电位器的上下限是不是并不是完整的 0 到 1023?把 map 起点和终点改成实际操作中的上下限,效果会更准。
最后看工具边界。一个几十块的 SG90 本身线性度有限,不同角度之间会有机械误差。如果你发现舵机在 90 度附近回不到某一个精确位置,可能不是代码问题,而是模拟舵机的能力边界。
下面把几个高频问题单独列出来:
| 现象 | 最可能原因 | 优先检查点 |
|---|---|---|
| OLED 完全不亮 | I2C 接线错误或地址不对 | SCL 到 A5、SDA 到 A4;用 I2C 扫描确认地址 |
| OLED 亮但字符乱码 | 接线接触不良或速度过快 | 检查杜邦线,降低 I2C 速度 |
| 舵机完全不动 | 舵机没有供电或信号线接错 | 先单独写servo.write(90)测试 |
| 舵机转一下又回中或不断重启 | 供电不足,程序复位 | 改用外部 5V 电源给舵机,共地 |
| 舵机跟着电位器抖动 | 电位器噪声或电源波动 | 用滤波读取,确保电源稳定 |
| 角度方向反了 | 电位器两端接反 | 交换电位器两个外侧脚,或 map 改成反向映射 |
| 电位器转到中位舵机才大幅跳变 | 模拟引脚悬空或接触不良 | 重新插 A0 线,检查面包板 |
注意:当 OLED 初始化失败时,我会在 setup 里用
while(1)停住程序,目的是避免整个主循环继续空跑,让你误以为舵机控制代码也坏了。这个做法只适合学习阶段的“检测到异常就停机”,正式工程里应该靠状态码和日志报警。
5. 做完这个项目,最值得沉淀的是什么
5.1 哪些场景适合,哪些场景不适合
这个联动验证项目适合的人群非常明确:你第一次接触 Arduino,想做一个小型闭环控制系统,或者你想理解“输入 -> 处理 -> 执行 -> 显示”的数据流。它比单点灯、单舵机实验更能帮你建立模块化判断能力。
但我也要说清楚它的边界,尤其是以下几个容易被误用的情况:
SG90 不适合做高精度定位应用。它不是一个带位置反馈的闭环伺服系统。OLED 显示的角度只是你发送的目标角度,如果舵机受到外力阻挡,它自己并不知道偏差。真要做角度闭环,需要在舵机输出轴上安装电位器或编码器,把实际位置读回来,再让 Arduino 根据误差做出修正。这样就变成了一个真正的闭环控制系统,复杂度也完全不同。
电位器 + map 的线性映射只适合手感验证。电位器本身有机械行程误差,ADC 采样有量化误差,舵机角度响应也不是严格线性的。如果你要让某个结构精确转动到指定位置,应该选择更加可靠的传感器和执行器。
低成本舵机的扭矩很小。SG90 只能带动很轻的塑料或纸板结构。如果你打算用它作为一个关节的驱动,需要先计算扭矩、重量、力臂和供电余量,否则舵机会堵转、发热甚至烧毁。做一个桌面演示没问题,做机械臂的基础关节还需要换更大的舵机和金属齿轮舵机。
5.2 从“验证”到“工程化”还差几块拼图
做完这个联动验证,相当于把最小可运行流程跑通了。但这个流程距离一个可持续维护的 Arduino/嵌入式项目还差几块关键拼图。
第一块是日志与状态抽象。把串口打印从散落在主循环里的零散调用,整理成统一的状态上报格式,让运行中的电压、原始值、目标角度、舵机状态都能看到。将来遇到问题,第一反应是翻日志,而不是靠玄学重插线。
第二块是输入边界与异常策略。比如电位器断线时,analogRead可能持续返回同一个值,系统会表现为舵机停在某个角度不动。这时候是否要报警?是否要回到安全位?这些东西在验证demo里可以不做,但放到真实设备里必须提前想清楚。
第三块是硬件设计。面包板和杜邦线适合原型验证,不适合震动、移动或长期运行。如果要把这个联动做成一个持续工作的小装置,至少要把接线焊到洞洞板或PCB上,加入电源滤波电容,给舵机供电端并联一个几百微法的电解电容,再考虑接口保护。
第四块是平台迁移时的参数隔离。同一套联动逻辑放在 ESP32、STM32 上时,引脚定义、ADC 位数、I2C 库、PWM 实现都会变化。你在 UNO 上写代码,如果把A0、A4、A5写死在业务逻辑里,迁移会非常痛苦。更好的做法是把所有硬件资源放到一个配置区,业务代码只调用updateDisplay()、readAngle()这类抽象函数。
这也是为什么我一直建议把这个项目当作练习而不是终点。你可以在这个基础上继续扩展:再加一个按键记录多个位置并循环执行,把电位器换成旋转编码器提升手感,用 OLED 显示菜单选择不同运动模式,甚至把控制逻辑接入上位机,用串口或者无线指令让舵机按指定角度运动。每一次扩展,都会让你对“状态如何跨层传输、信号如何被环境干扰、程序如何稳定运行”有更深的理解。
最后想说的是,当你看到 OLED 上显示的角度,随手指转动电位器而实时变化,舵机也跟着转向对应位置,那种“看不见的信号变成了可见操作”的感觉,才是 Arduino 这种原型平台最值得体会的部分。下次再做任何带传感器和执行器的项目,都先把中间状态显示出来或打印出来,再谈后面的优化。这条习惯,比多掌握一个库函数值钱得多。