家里翻出一个游戏手柄拆下来的双轴摇杆模块,就是那种几块钱一个、五个引脚的小板子,KY-023 这类。很多新手看它引脚多就不敢碰,其实它是入门 Arduino 模拟量输入最划算的器件之一:两个电位器加一个按键,就能把"手往哪边掰"变成单片机能读的数字。这次我把 arduinogetstarted.com 上的摇杆教程完整理了一遍,把新手会卡的地方都标出来,照着做一遍基本十分钟就能在串口监视器里看到数据跑起来。

先说这模块能干什么。最直接的用法是给遥控小车当方向输入:X 轴管左右转向,Y 轴管前进后退,中间那个按下当喇叭或者模式切换。再进阶一点,两个轴还能映射成角度去控云台上的两个舵机。学会读它,等于打开了模拟量输入这扇门。
五个引脚各管什么
摇杆模块一共五个引脚,见下面的引脚图。GND 接地,+5V 接电源(有的板子丝印是 VCC,一个意思),剩下三个是输出:VRX 输出水平位置的模拟值,VRY 输出垂直位置的模拟值,SW 是里面那个按压按键的输出。

关键点是 VRX 和 VRY 这两个。摇杆内部是两个互相垂直安装的电位器,掰动摇杆就是在同时转两个电位器。Arduino 用 analogRead 读模拟引脚,得到 0 到 1023 的数值——摇杆在中间松手不动时,读数大约在 500 上下;推到最左边 VRX 趋近 0,推到最右边趋近 1023,Y 轴同理。
SW 按键要注意:它本身是常开的,模块上一般不带上拉电阻,所以要把 SW 引脚设成 INPUT_PULLUP 模式,或者外接上拉电阻。这样不按时读 HIGH,按下读 LOW。新手最常见的坑就是这里——没上拉,引脚悬空,读出来的值乱跳,还以为模块坏了。
接线:五根杜邦线搞定
接线很简单,照下面的接线图来:GND 对 GND,+5V 对 5V,VRX 接 A1,VRY 接 A0,SW 接 D2。模拟脚随便挑 A0 到 A5 都行,代码里改一下宏定义就好;SW 接 D2 是因为后面代码用数字脚读它。

如果是长期跑的项目,或者要装进小车壳子里,杜邦线容易松。官方教程里推荐了 Screw Shield V2 这种螺丝端子扩展板,直接叠在 Arduino 上,每个引脚变成一个螺丝接线柱,线拧进去就跑不掉了。下面的图就是用了螺丝端子板之后的接法,看着乱但每根都拧得很牢。

第一步:先把 X/Y 读数跑起来
别急着写方向判定,第一步永远是"把原始读数打到串口看"。下面这段代码从教程原文逐字符搬过来,就是不停地读 A1 和 A0,然后打印。代码里的 VRX_PIN 定义成 A1、VRY_PIN 定义成 A0,和你接的线要一一对应,接反了只是 X 和 Y 互换,不算错,但方向判定时会把你绕晕。
/*
* Created by ArduinoGetStarted.com
*
* This example code is in the public domain
*
* Tutorial page: https://arduinogetstarted.com/tutorials/arduino-joystick
*/
#define VRX_PIN A1 // Arduino pin connected to VRX pin
#define VRY_PIN A0 // Arduino pin connected to VRY pin
int xValue = 0; // To store value of the X axis
int yValue = 0; // To store value of the Y axis
void setup() {
Serial.begin(9600) ;
}
void loop() {
// read analog X and Y analog values
xValue = analogRead(VRX_PIN);
yValue = analogRead(VRY_PIN);
// print data to Serial Monitor on Arduino IDE
Serial.print("x = ");
Serial.print(xValue);
Serial.print(", y = ");
Serial.println(yValue);
delay(200);
}烧录后打开串口监视器(波特率 9600),把摇杆推到极限画圈。记住官方教程给的一个小窍门:X 读到 0 的那个位置就是"左",对面就是"右";Y 读到 0 的位置是"上",对面是"下"。不同厂家的模块中位值和方向可能不一样,所以这一步是在给你的模块"定方向",别跳过。
第二步:把 SW 按键也读上
按键部分官方用了 ezButton 库,这个库自带消抖,还能省掉手动上拉的很多细节。装库就在 Arduino IDE 左侧库管理器里搜 ezButton,作者是 ArduinoGetStarted.com,装上就行。按键接在 D2。
照例说明一下:原代码里头文件写的是尖括号引用,我们平台会把尖括号吃掉,所以下面贴成了等价的双引号写法,在 Arduino IDE 里编译效果一样。代码其余部分逐字符未动。
/*
* Created by ArduinoGetStarted.com
*
* This example code is in the public domain
*
* Tutorial page: https://arduinogetstarted.com/tutorials/arduino-joystick
*/
#include "ezButton.h"
#define VRX_PIN A1 // Arduino pin connected to VRX pin
#define VRY_PIN A0 // Arduino pin connected to VRY pin
#define SW_PIN 2 // Arduino pin connected to SW pin
ezButton button(SW_PIN);
int xValue = 0; // To store value of the X axis
int yValue = 0; // To store value of the Y axis
int bValue = 0; // To store value of the button
void setup() {
Serial.begin(9600) ;
button.setDebounceTime(50); // set debounce time to 50 milliseconds
}
void loop() {
button.loop(); // MUST call the loop() function first
// read analog X and Y analog values
xValue = analogRead(VRX_PIN);
yValue = analogRead(VRY_PIN);
// Read the button value
bValue = button.getState();
if (button.isPressed()) {
Serial.println("The button is pressed");
// TODO do something here
}
if (button.isReleased()) {
Serial.println("The button is released");
// TODO do something here
}
// print data to Serial Monitor on Arduino IDE
Serial.print("x = ");
Serial.print(xValue);
Serial.print(", y = ");
Serial.print(yValue);
Serial.print(" : button = ");
Serial.println(bValue);
}有个细节值得划重点:loop 里必须先调 button.loop(),再读状态。我看过好几个新手卡在这里——把 button.loop() 忘了或者放在最后,结果 isPressed 永远不触发。消抖时间设的 50 毫秒,按起来手感正常;如果你的模块按键特别灵或者特别肉,可以微调这个值。
第三步:从读数变成方向命令
有了原始读数,下一步是把它变成"命令"。教程的思路是设阈值:X 小于 400 判为左,大于 800 判为右;Y 同理。四个方向用四个位标志(0x01、0x02、0x04、0x08),这样摇杆斜推时可以同时带上两个方向,比如左上。中间区域 400 到 800 之间算死区,手轻碰不会误触发。
/*
* Created by ArduinoGetStarted.com
*
* This example code is in the public domain
*
* Tutorial page: https://arduinogetstarted.com/tutorials/arduino-joystick
*/
#define VRX_PIN A1 // Arduino pin connected to VRX pin
#define VRY_PIN A0 // Arduino pin connected to VRY pin
#define LEFT_THRESHOLD 400
#define RIGHT_THRESHOLD 800
#define UP_THRESHOLD 400
#define DOWN_THRESHOLD 800
#define COMMAND_NO 0x00
#define COMMAND_LEFT 0x01
#define COMMAND_RIGHT 0x02
#define COMMAND_UP 0x04
#define COMMAND_DOWN 0x08
int xValue = 0 ; // To store value of the X axis
int yValue = 0 ; // To store value of the Y axis
int command = COMMAND_NO;
void setup() {
Serial.begin(9600) ;
}
void loop() {
// read analog X and Y analog values
xValue = analogRead(VRX_PIN);
yValue = analogRead(VRY_PIN);
// converts the analog value to commands
// reset commands
command = COMMAND_NO;
// check left/right commands
if (xValue < LEFT_THRESHOLD)
command = command | COMMAND_LEFT;
else if (xValue > RIGHT_THRESHOLD)
command = command | COMMAND_RIGHT;
// check up/down commands
if (yValue < UP_THRESHOLD)
command = command | COMMAND_UP;
else if (yValue > DOWN_THRESHOLD)
command = command | COMMAND_DOWN;
// NOTE: AT A TIME, THERE MAY BE NO COMMAND, ONE COMMAND OR TWO COMMANDS
// print command to serial and process command
if (command & COMMAND_LEFT) {
Serial.println("COMMAND LEFT");
// TODO: add your task here
}
if (command & COMMAND_RIGHT) {
Serial.println("COMMAND RIGHT");
// TODO: add your task here
}
if (command & COMMAND_UP) {
Serial.println("COMMAND UP");
// TODO: add your task here
}
if (command & COMMAND_DOWN) {
Serial.println("COMMAND DOWN");
// TODO: add your task here
}
}烧进去之后推摇杆,串口里就该出现 SEND: LEFT 这类字样了。代码注释里官方也提醒了:每个摇杆的电阻不一样,阈值要按你手上这只微调。如果你的模块中位明显不是 500 左右,比如是 480 或者 520,就照着实测值把上下阈值整体平移一下。
往遥控小车上搬的思路
拿到 LEFT/RIGHT/UP/DOWN 命令之后,控小车就只是"命令到动作"的翻译题。常见做法是:接收端用一块 Arduino 加电机驱动模块(L298N、TB6612 这类都行),发射端把命令字符从串口或者蓝牙模块发过去。UP 时两个电机同向正转,DOWN 反转,LEFT/RIGHT 让左右轮差速。斜推时 command 是两个位标志的组合,正好对应"边走边拐"。
如果想做得细腻一点,不用阈值开关,而是把 0 到 1023 的读数用 map() 映射到 PWM 占空比,推得越多跑得越快,手感跟真遥控器一样。再往上一个层级,是把两个轴映射成两个舵机的角度,官方那篇 Joystick controls Servo Motor 就是这么做的,做云台正好用得上。
最后提醒几个新手常踩的点:一是中位不归零,松手后读数停在 490 或 530 都正常,别追求正好 512;二是 A0/A1 接反导致左右上下颠倒,改宏定义比改线快;三是 SW 别忘了上拉;四是主循环里别放长 delay,delay 太长会吃掉按键事件, ezButton 的 loop 调用频率跟不上就会漏按。把这几处避开,摇杆这块基本一次点亮。











map()映射PWM那招做遥控车确实爽,推多少跑多少,比纯开关阈值手感好一个档次
螺丝端子扩展板这个建议不错,我小车上的杜邦线跑两圈就接触不良,早该换
这篇把『先读数再定方向』的顺序讲得很对。我第一次玩摇杆就是跳过了画圈标方向那步,直接写判断,结果左右上下全反了,调了一晚上才反应过来是模块中位和官方示例不一样。另外 ezButton 那个 loop 必须先调的坑确实存在,我漏过,按键就是不触发,最后翻文档才看到那句 MUST call。
A0 A1接反这个太真实了,我也是宏定义改的,比拔线快多了