参考:表情机器人中使用22个舵机的案例
引言
在电子制作与自动化控制领域,Arduino 凭借其易用性和强大的扩展性备受青睐。Arduino Mega 作为其中功能较为强大的一款开发板,具备丰富的引脚资源,能够实现复杂的控制任务。舵机作为常见的执行机构,可精确控制角度,广泛应用于机器人、模型制作等场景。本文将深入探讨如何使用 Arduino Mega 控制 32 个舵机,详细分析硬件连接、代码实现,并对项目进行拓展思考。
硬件连接
材料准备
- Arduino Mega 开发板:它拥有 54 个数字输入输出引脚,其中 15 个可提供 PWM 输出,这为同时控制多个舵机提供了硬件基础。
- 32 个舵机:舵机通常有三根线,分别是电源线(VCC,一般为红色)、地线(GND,一般为黑色或棕色)和信号线(PWM,一般为橙色或黄色)。
- 杜邦线:用于连接 Arduino Mega 和舵机,保证信号和电源的传输。
- 电源模块:由于 32 个舵机同时工作时功耗较大,Arduino Mega 的板载电源可能无法满足需求,因此需要一个外部电源模块为舵机供电。
连接方式
- 电源连接:将所有舵机的电源线(VCC)连接到外部电源的正极,所有舵机的地线(GND)连接到外部电源的负极和 Arduino Mega 的 GND 引脚,确保共地。这样可以避免电源干扰,保证系统的稳定性。
- 信号连接:从 Arduino Mega 的 22 号引脚开始,依次将 32 个舵机的信号线(PWM)连接到 22 - 53 号引脚。在代码中,会将这些引脚与对应的舵机对象进行关联,从而实现对每个舵机的精确控制。
硬件连接注意事项
- 电源供应:确保外部电源能够提供足够的电流,以满足 32 个舵机同时工作的需求。否则,可能会出现舵机转动无力、抖动等问题。
- 引脚使用:避免将其他设备连接到 22 - 53 号引脚,以免与舵机控制产生冲突。
- 线路布局:合理规划杜邦线的布局,避免线路交叉和缠绕,减少信号干扰。
代码实现
代码整体功能概述
这段代码实现了使用 Arduino Mega 控制 32 个舵机,并让它们按照预设的四个不同序列进行运动,展示出多样化的动作效果。
代码详细分析
库的引入和变量定义
#include <Servo.h>
#define NUM_SERVOS 32
Servo myServo[NUM_SERVOS];
#include <Servo.h>
:引入 Servo 库,该库提供了控制舵机的基本函数和方法。#define NUM_SERVOS 32
:定义常量NUM_SERVOS
为 32,表示要控制的舵机数量。Servo myServo[NUM_SERVOS];
:创建一个包含 32 个Servo
对象的数组myServo
,用于管理每个舵机。
setup()
函数
void setup()
{for( int i=0; i<NUM_SERVOS; i++){myServo[i].attach( i + 22); }
}
- 在
setup()
函数中,通过for
循环将每个Servo
对象与 Arduino Mega 开发板上从 22 到 53 的引脚依次连接起来。attach()
函数用于将舵机对象与特定的引脚关联,以便后续向该引脚发送 PWM 信号来控制舵机的角度。
loop()
函数
loop()
函数会不断循环执行,其中包含了四个不同的舵机运动序列。
序列一:随机角度运动
for( int a=0; a<15; a++)
{for( int i=0; i<NUM_SERVOS; i++){myServo[i].write( random( 0, 181));delay( 2);}delay( 150);
}
- 该序列会循环 15 次。每次循环中,使用
random(0, 181)
函数为每个舵机生成一个 0 到 180 之间的随机角度,并通过write()
函数将该角度发送给对应的舵机。delay(2)
用于在设置每个舵机角度后稍微延迟一下,让舵机有时间响应。delay(150)
用于在完成一次所有舵机角度设置后,暂停 150 毫秒,方便观察效果。
序列二:同步角度运动
for( int i=0; i<NUM_SERVOS; i++)
{myServo[i].write( 0);
}
delay( 1000);
for( int a=0; a<3; a++)
{for( int r=0; r<=180; r++) {for( int i=0; i<NUM_SERVOS; i++){myServo[i].write( r);}delay( 6);}for( int r=180; r>=0; r--){for( int i=0; i<NUM_SERVOS; i++) {myServo[i].write( r);}delay( 6);}
}
- 首先,将所有舵机的角度设置为 0 度,即初始位置(舵机臂向左旋转),并延迟 1000 毫秒,让观察者适应这个状态。
- 然后,循环 3 次,每次先将所有舵机从 0 度逐步旋转到 180 度(向右旋转),再从 180 度逐步旋转回 0 度(向左旋转)。
delay(6)
用于控制舵机旋转的速度,让舵机有足够的时间转动到指定角度。
序列三:旋转波运动
for( int a=0; a<6; a++)
{for( int i=0; i<NUM_SERVOS; i++){for( int j=0; j<NUM_SERVOS; j++){int d = j - i;if( d < 0)d = -d;if( d > (NUM_SERVOS / 2))d = NUM_SERVOS - d;int angle = 90 - (10 * d);if( angle < 0)angle = 0;myServo[j].write( angle);}delay(40);}
}
- 该序列会循环 6 次。对于每个舵机
i
,会计算其他舵机j
与它的距离d
,并根据距离计算出对应的角度angle
。距离越近,角度越接近 90 度;距离越远,角度越小。通过不断改变i
的值,形成一个旋转的波状运动效果。delay(40)
用于控制波的传播速度。
序列四:“罗盘”运动
int pointer = NUM_SERVOS * 3 / 4;
showPointer( pointer);
delay( 1000);
for( int i=0; i<5; i++)
{showPointer( --pointer);delay( 150);
}
delay( 200);
// 后续还有多个类似的循环,省略部分代码
- 首先,将指针
pointer
初始化为NUM_SERVOS * 3 / 4
,并调用showPointer()
函数显示初始状态,延迟 1000 毫秒让观察者适应。 - 然后,通过多次调用
showPointer()
函数,让指针按照一定的规律左右移动,模拟罗盘指针的转动效果。每次移动后延迟 150 毫秒,控制转动速度。
showPointer()
函数
void showPointer( int s)
{int pointerA = s % NUM_SERVOS; int pointerB = (s + 1) % NUM_SERVOS; int tailA = (s + 16) % NUM_SERVOS;int tailB = (s + 17) % NUM_SERVOS;myServo[pointerA].write(180-56);myServo[pointerB].write(56);myServo[tailA].write(95);myServo[tailB].write(85);int n = (NUM_SERVOS / 2) - 2;int start = pointerB + 1;for( int i=0; i<n; i++){int j = (start + i) % NUM_SERVOS;myServo[j].write( 2);}start = tailB + 1;for( int i=0; i<n; i++){int j = (start + i) % NUM_SERVOS;myServo[j].write( 178);}
}
- 该函数用于创建“罗盘”效果。通过计算指针、尾部以及左右两侧舵机的索引,将它们设置为不同的角度,形成一个类似指针的形状。左右两侧的舵机分别设置为 2 度和 178 度,突出指针的效果。
GenerateDiagram()
函数
void GenerateDiagram()
{Serial.begin(115200);// 后续是生成 JSON 格式文本的代码,省略部分代码
}
- 这个函数用于生成 Wokwi 模拟器所需的
diagram.json
文件。通过串口输出 JSON 格式的文本,包含了 Arduino Mega 和 32 个舵机的位置、连接等信息。
全部代码
// ServoOverdone.ino
//
// Example for multiple Servo objects in a array.
//
// Version 1, 28 July 2021, by Koepel.
// Version 2, 15 August 2021, by Koepel.
// changed timing, a little slower
// diagram.json has servos in reverse order (I think it is visually better)
// Added fourth sequence: "compass"
//
// Public Domain
//#include <Servo.h>#define NUM_SERVOS 32
Servo myServo[NUM_SERVOS];void setup()
{// Attach pins from the Arduino Mega board to the Servo objects.// Starting from pin 22, there happen to be exactly 32 pins on the double row pins.for( int i=0; i<NUM_SERVOS; i++){myServo[i].attach( i + 22); // pin 22 up to 53 is 32 pins}
}void loop()
{// Sequence one.// All servo motor are set to a random angle.for( int a=0; a<15; a++){for( int i=0; i<NUM_SERVOS; i++){myServo[i].write( random( 0, 181));delay( 2);}delay( 150);}// Sequence two.// All servo motors move with the same angle.for( int i=0; i<NUM_SERVOS; i++){myServo[i].write( 0); // set to begin position (horn is rotated left)}delay( 1000); // wait to let the viewer get used to itfor( int a=0; a<3; a++){for( int r=0; r<=180; r++) // move horns to the right{for( int i=0; i<NUM_SERVOS; i++){myServo[i].write( r);}delay( 6);}for( int r=180; r>=0; r--){for( int i=0; i<NUM_SERVOS; i++) // move horns to the left{myServo[i].write( r);}delay( 6);}}// Sequence three.// A rotating wave.for( int a=0; a<6; a++){for( int i=0; i<NUM_SERVOS; i++){for( int j=0; j<NUM_SERVOS; j++){// Calculate distance to active servoint d = j - i;if( d < 0)d = -d;if( d > (NUM_SERVOS / 2))d = NUM_SERVOS - d;int angle = 90 - (10 * d);if( angle < 0)angle = 0;myServo[j].write( angle);}delay(40);}}// Sequence four.// A "compass"// Start by pointing upwardsint pointer = NUM_SERVOS * 3 / 4;showPointer( pointer);delay( 1000); // let the viewer get used to new patternfor( int i=0; i<5; i++){showPointer( --pointer);delay( 150);}delay( 200);for( int i=0; i<9; i++){showPointer( ++pointer);delay( 150);}delay( 200);for( int i=0; i<5; i++){showPointer( --pointer);delay( 150);}delay( 200);for( int i=0; i<4; i++){showPointer( ++pointer);delay( 150);}delay( 160);for( int i=0; i<2; i++){showPointer( --pointer);delay( 150);}delay( 80);for( int i=0; i<1; i++){showPointer( ++pointer);delay( 150);}delay( 2000);
}// This function makes a "pointer" with the servos.
// It is used to create the "compass".
// The parameter 's' is the servo motor that has the pointer.
// It is allowed that 's' is below zero or larger than the numbers of servo motors.
void showPointer( int s)
{int pointerA = s % NUM_SERVOS; // Using the '%' (remainder) for valid numberint pointerB = (s + 1) % NUM_SERVOS; // pointer is made with the next servo motorint tailA = (s + 16) % NUM_SERVOS;int tailB = (s + 17) % NUM_SERVOS;// make pointer with servo motor s and s+1.myServo[pointerA].write(180-56);myServo[pointerB].write(56);// make tail with servo motor s+16 and s+17.myServo[tailA].write(95);myServo[tailB].write(85);// Set servos right of pointerint n = (NUM_SERVOS / 2) - 2;int start = pointerB + 1;for( int i=0; i<n; i++){int j = (start + i) % NUM_SERVOS;myServo[j].write( 2);}// Set servos left of pointerstart = tailB + 1;for( int i=0; i<n; i++){int j = (start + i) % NUM_SERVOS;myServo[j].write( 178);}
}// The function GenerateDiagram() can be used to generate
// the diagram.json file for Wokwi.
// To use it, call it from the setup() function, and the
// serial output can be copied into the diagram.json file.
void GenerateDiagram()
{Serial.begin(115200);Serial.print( "{\n");Serial.print( " \"version\": 1,\n");Serial.print( " \"author\": \"Generated\",\n");Serial.print( " \"editor\": \"wokwi\",\n");Serial.print( " \"parts\": [\n");Serial.print( " {\n");Serial.print( " \"type\": \"wokwi-arduino-mega\",\n");Serial.print( " \"id\": \"mega\",\n");Serial.print( " \"top\": 270,\n");Serial.print( " \"left\": 185,\n");Serial.print( " \"attrs\": {}\n");Serial.print( " },\n");// Put the servo motor in reverse order in the diagram.json// I think that is visually better.// The horn now overlaps the next servo when the horn moves to the right.for( int i=NUM_SERVOS-1; i>=0; i--){float rotate = float( i) * (360.0 / float( NUM_SERVOS));float rad = rotate / 360.0 * 2.0 * M_PI;float top = (300.0 * sin( rad)) + 300.0;float left = (300.0 * cos( rad)) + 300.0;Serial.print( " {\n");Serial.print( " \"type\": \"wokwi-servo\",\n");Serial.print( " \"id\": \"servo");Serial.print( i);Serial.print( "\",\n");Serial.print( " \"top\": ");Serial.print( top);Serial.print( ",\n");Serial.print( " \"left\": ");Serial.print( left);Serial.print( ",\n");Serial.print( " \"rotate\": ");Serial.print( rotate);Serial.print( ",\n");Serial.print( " \"attrs\": { \"hornColor\": \"Red\" }\n");Serial.print( " }");if( i != 0)Serial.print( ",");Serial.print( "\n");} Serial.print( " ],\n");Serial.print( " \"connections\": [\n");for( int i=0; i<NUM_SERVOS; i++){int j = i + 1;if( j == NUM_SERVOS)j = 0;Serial.print( " [ \"servo");Serial.print( i);Serial.print( ":V+\", \"servo");Serial.print( j);Serial.print( ":V+\", \"Red\", [] ],\n");Serial.print( " [ \"servo");Serial.print( i);Serial.print( ":GND\", \"servo");Serial.print( j);Serial.print( ":GND\", \"Black\", [] ],\n");Serial.print( " [ \"mega:");Serial.print( i + 22);Serial.print( "\", \"servo");Serial.print( i);Serial.print( ":PWM\", \"Green\", [ ] ],\n");}Serial.print( " [ \"mega:GND.2\", \"servo9:GND\", \"Black\", [ ] ],\n");Serial.print( " [ \"mega:5V\", \"servo9:V+\", \"Red\", [ ] ]\n");Serial.print( " ]\n");Serial.print( "}\n");
}
拓展与思考
功能拓展
- 传感器结合:可以添加各类传感器,如超声波传感器、红外传感器等,根据传感器的数据动态调整舵机的运动。例如,当超声波传感器检测到前方有障碍物时,舵机可以控制机器人改变方向。
- 远程控制:通过无线通信模块(如蓝牙、Wi-Fi)实现对舵机的远程控制。用户可以使用手机应用或电脑程序发送指令,让舵机执行特定的动作。
- 增加舵机数量:虽然 Arduino Mega 有 54 个数字引脚,但受限于电源和处理能力,同时控制更多舵机可能会遇到挑战。可以考虑使用舵机驱动板(如 PCA9685)来扩展舵机控制数量。
代码优化
- 减少延迟:代码中使用了大量的
delay()
函数,这会阻塞程序的执行,影响系统的实时性。可以使用定时器或状态机的方式来替代delay()
函数,提高代码的效率。 - 模块化设计:将不同的舵机运动序列封装成独立的函数,提高代码的可读性和可维护性。同时,方便添加新的运动序列。
总结
通过本文的介绍,我们详细了解了如何使用 Arduino Mega 控制 32 个舵机。从硬件连接的注意事项到代码实现的各个细节,都进行了深入分析。同时,对项目的拓展和代码优化也提出了一些思路。这个项目不仅展示了 Arduino Mega 的强大功能,也为我们在电子制作和自动化控制领域的进一步探索提供了基础。在实际应用中,我们可以根据具体需求对硬件和代码进行调整和扩展,创造出更加复杂和有趣的项目。