智能小车(五):PS2模块
星期日, 11月 10, 2024 | 9分钟阅读 | 更新于 星期日, 12月 8, 2024
历史回顾
使用 PS2 手柄控制 Arduino 电机与蜂鸣器
在这篇博客中,我们将使用 PS2 手柄控制 Arduino 控制系统,通过接收来自手柄的指令来实现对电机、蜂鸣器等外设的控制。这种手柄控制器的应用在遥控车辆、机器人等领域十分常见。接下来,我们将逐步深入,了解如何在 Arduino 上实现这一功能。
PS2 手柄控制器简介
PS2 手柄是一种常用的游戏控制器,具有方向控制、按键控制和摇杆控制等多种输入方式。它能够通过串口或其他通信协议与 Arduino 进行通信,从而将用户的控制意图传达给 Arduino 系统。通过 PS2 手柄发送特定的按键或摇杆指令,Arduino 能够接收到对应的数据并进行解析,从而实现对电机、蜂鸣器等硬件的控制。
PS2 手柄通过向 Arduino 发送指令字符(如 ‘A’, ‘B’, ‘C’ 等)来实现不同的动作。接下来,我们将编写一个简单的代码实例,展示如何接收手柄指令并反馈信息。
基础代码示例:串口接收指令并反馈
首先,我们编写一个简单的 Arduino 代码,用于接收来自手柄的指令并将接收的指令打印在串口监视器上:
char cmd; // 接收指令
void setup() {
Serial.begin(9600); // 设置串口通信波特率
}
void serialEvent() {
if (Serial.available() > 0) {
cmd = Serial.read();
}
while (Serial.read() >= 0) {} // 清除串口缓存
}
void loop() {
if(cmd=='A')
{
Serial.println(cmd);
cmd="";
}
if(cmd=='B')
{
Serial.println(cmd);
cmd="";
}
// ... 其它指令
if(cmd=='N')
{
Serial.println(cmd);
cmd="";
}
}
在上述代码中,我们通过 serialEvent()
函数检测串口是否有数据传入,若有数据,则读取第一个字符作为指令 cmd
。loop()
函数根据 cmd
的值执行不同的指令反馈操作,满足了基础的串口接收功能。
引入看门狗机制
在复杂的控制系统中,保持系统稳定运行十分关键。为避免系统因卡死或出错而停止响应,我们可以引入“看门狗”机制。看门狗是一种定时器机制,能够监测系统运行状态。如果系统在特定时间内没有执行“喂狗”操作,定时器便会触发自动复位,将系统重启。
在 Arduino 中,我们可以通过 avr/wdt.h
库使用看门狗定时器。以下是看门狗的简单初始化方法:
#include <avr/wdt.h>
void setup() {
wdt_enable(WDTO_1S); // 开启看门狗,1秒超时
}
void loop() {
wdt_reset(); // 喂狗操作,重置看门狗计时器
// 其他代码
}
在以上代码中,我们设置看门狗定时器为 1 秒,若 loop()
中每秒未执行 wdt_reset()
,系统将自动重启。这样可以确保系统不会因未处理的错误或死循环而卡死。
使用 PS2 手柄控制 Arduino 电机与蜂鸣器
此代码通过串口接收来自 PS2 手柄的指令,使用这些指令来控制电机、舵机、蜂鸣器、LED 等硬件设备。我们将代码分为几个主要功能模块,以便逐层理解每部分的功能。
1. 库和变量定义
#include <SoftPWM.h>
#include <avr/wdt.h> // 看门狗
#include <Servo.h>
Servo myservo; // 舵机对象
// 电机和舵机的控制变量
int LeftUpDown = 128, LeftLeftRight = 128, RightUpDown = 128, RightLeftRight = 128;
int RockerFlag = 0, count1, count2, count3, count4, Min = 80, Max = 200;
char W[4], P[4], Q[4], S[4]; // 接收字符数据
float ad, dianya; // 电压相关变量
int LeftBack = 2, LeftFront = 4, RightBack = 7, RightFront = 8, TurnPin = 9, Buzzerbin = 13, LedPin = 10;
int LedState = LOW; // LED 初始状态
unsigned long PreviousMillis = 0, WorkMillis = 0, AccelgyroWorkMillis = 0;
const long Interval = 500; // LED 闪烁间隔时间
解释:
- 包含了
SoftPWM
(软件 PWM 库)和avr/wdt.h
(看门狗库)用于处理 PWM 输出和系统稳定性。 - 定义了用于控制电机和舵机的引脚及变量,这些变量将用于处理电机的前进、后退、转向控制等。
RockerFlag
变量用于检测摇杆状态。多个字符数组W
、P
、Q
和S
用于存储不同的指令数据。dianya
和ad
用于存储电压数据。
2. 初始化函数 setup
void setup() {
pinMode(LedPin, OUTPUT);
pinMode(LeftBack, OUTPUT);
pinMode(LeftFront, OUTPUT);
pinMode(RightBack, OUTPUT);
pinMode(RightFront, OUTPUT);
pinMode(Buzzerbin, OUTPUT);
Serial.begin(9600);
myservo.attach(TurnPin);
wdt_enable(WDTO_1S); // 开启看门狗
SoftPWMBegin();
}
解释:
- 设置电机和 LED 引脚模式为
OUTPUT
,并将舵机初始化到指定引脚。 - 开启串口通信。
wdt_enable(WDTO_1S);
用于开启看门狗定时器,如果系统在 1 秒内未执行wdt_reset()
,看门狗将复位系统。SoftPWMBegin();
初始化软件 PWM。
3. LED 闪烁控制 Led()
void Led(void) {
unsigned long currentMillis = millis();
if (currentMillis - PreviousMillis >= Interval) {
PreviousMillis = currentMillis;
LedState = !LedState;
digitalWrite(LedPin, LedState);
}
if (currentMillis - WorkMillis >= 50) {
WorkMillis = currentMillis;
Stop();
}
}
解释:
Led()
控制 LED 的闪烁,每隔Interval
毫秒改变一次LedState
状态,实现 LED 闪烁效果。- 同时,它在超过 50 毫秒后调用
Stop()
函数,确保在无新指令时自动停止电机。
4. 蜂鸣器控制 vBeep()
与 Beep()
void vBeep(void) {
int v = analogRead(A0); // 从 A0 读取电压
ad = v * (5.0 / 1024.0) * 3.1;
dianya = ad;
if (ad <= 5.5) {
Stop();
Beep(3, 160, 500);
delay(2000);
}
}
void Beep(int count, int Frequency, int Time) {
for (int i = 0; i < count; i++) {
for (int q = 0; q < 500; q++) {
digitalWrite(Buzzerbin, HIGH);
delayMicroseconds(Frequency);
digitalWrite(Buzzerbin, LOW);
delayMicroseconds(Frequency);
}
delay(Time);
}
}
解释:
vBeep()
用于监控电压,当电压低于设定值(5.5V)时,停止电机并调用Beep()
发出蜂鸣。Beep()
控制蜂鸣器发声,根据频率和时间产生不同的蜂鸣效果。
5. 电机与舵机控制 Output()
与 ElectricMachinery()
void Output(int TurnCount, int FrontCount, int BackCount) {
myservo.write(TurnCount);
SoftPWMSet(LeftFront, FrontCount);
SoftPWMSet(RightFront, FrontCount);
SoftPWMSet(LeftBack, BackCount);
SoftPWMSet(RightBack, BackCount);
}
void ElectricMachinery(void) {
if (LeftUpDown >= 127 && LeftUpDown <= 129) {
if (LeftLeftRight >= 127 && LeftLeftRight <= 129) {
Stop();
}
if (LeftLeftRight < 127) {
Left(map(LeftLeftRight, 0, 128, 30, 90), 0, 0);
}
if (LeftLeftRight > 129) {
Right(map(LeftLeftRight, 128, 255, 90, 150), 0, 0);
}
}
if (LeftUpDown < 127) {
Front(map(LeftLeftRight, 0, 255, 30, 150), map(LeftUpDown, 128, 0, 60, 255), 0);
}
if (LeftUpDown > 129) {
Back(map(LeftLeftRight, 0, 255, 30, 150), 0, map(LeftUpDown, 128, 255, 60, 255));
}
}
void Front(int TurnCount, int FrontCount, int BackCount)
{
Output(TurnCount, FrontCount, BackCount);
}
void Back(int TurnCount, int FrontCount, int BackCount)
{
Output(TurnCount, FrontCount, BackCount);
}
void Left(int TurnCount, int FrontCount, int BackCount)
{
Output(TurnCount, FrontCount, BackCount);
}
void Right(int TurnCount, int FrontCount, int BackCount)
{
Output(TurnCount, FrontCount, BackCount);
}
void Stop(void)
{
Output(90, 0, 0);
}
解释:
Output()
设置电机前后动力和舵机转向,通过 PWM 调节各电机的输出,实现速度控制。ElectricMachinery()
根据摇杆指令决定电机和舵机的动作。例如,若LeftUpDown
在中立位置,则停止所有电机;根据摇杆位置可以实现前进、后退、左右转向等功能。
6. 串口接收函数 serialEvent()
void serialEvent() {
char inchar;
// 清空缓存,避免重复读取
static void clearSerialBuffer() {
while(Serial.read() >= 0);
}
// 处理摇杆数据
static void handleRockerData(char* array, uint8_t* count, uint8_t* value) {
if(*count == 1) {
*value = array[0] - '0';
} else if(*count == 2) {
*value = (array[0] - '0') * 10 + (array[1] - '0');
} else if(*count == 3) {
*value = (array[0] - '0') * 100 + (array[1] - '0') * 10 + (array[2] - '0');
}
}
// 重置摇杆参数
static void resetRocker(char flag) {
RockerFlag = flag;
WorkMillis = millis();
switch(flag) {
case 1:
count1 = 0;
memset(W, 0, sizeof(W));
break;
case 2:
count2 = 0;
memset(P, 0, sizeof(P));
break;
case 3:
count3 = 0;
memset(Q, 0, sizeof(Q));
break;
case 4:
count4 = 0;
memset(S, 0, sizeof(S));
break;
}
}
while(Serial.available() > 0) {
inchar = Serial.read();
// 使用switch-case替代多个if语句
switch(inchar) {
case 'A':
RockerFlag = 0;
WorkMillis = millis();
Front(90, Max, 0);
clearSerialBuffer();
return;
case 'B':
RockerFlag = 0;
WorkMillis = millis();
Back(90, 0, Max);
clearSerialBuffer();
return;
case 'C':
RockerFlag = 0;
WorkMillis = millis();
Left(30, Max, 0);
clearSerialBuffer();
return;
case 'D':
RockerFlag = 0;
WorkMillis = millis();
Right(150, Max, 0);
clearSerialBuffer();
return;
case 'E':
RockerFlag = 0;
WorkMillis = millis();
if(Max < 250) {
Max += 10;
Beep(1, 130, 100);
} else {
Beep(1, 160, 500);
}
clearSerialBuffer();
return;
case 'F':
RockerFlag = 0;
WorkMillis = millis();
if(Max > Min + 10) {
Max -= 10;
Beep(1, 130, 100);
} else {
Beep(1, 160, 500);
}
clearSerialBuffer();
return;
case 'H':
Stop();
Min = 80;
Max = 150;
LeftUpDown = 128;
LeftLeftRight = 128;
RightUpDown = 128;
RightLeftRight = 128;
RockerFlag = 0;
count1 = count2 = 0;
memset(W, 0, sizeof(W));
memset(P, 0, sizeof(P));
WorkMillis = millis();
clearSerialBuffer();
return;
case 'L':
RockerFlag = 0;
Beep(1, 90, 500);
WorkMillis = millis();
clearSerialBuffer();
return;
case 'M':
RockerFlag = 0;
WorkMillis = millis();
Left(30, 0, Max);
clearSerialBuffer();
return;
case 'N':
RockerFlag = 0;
WorkMillis = millis();
Right(150, 0, Max);
clearSerialBuffer();
return;
case 'V':
Serial.println(ad);
clearSerialBuffer();
return;
case 'W':
resetRocker(1);
break;
case 'P':
resetRocker(2);
break;
case 'Q':
resetRocker(3);
break;
case 'S':
resetRocker(4);
break;
}
// 处理摇杆数据
if(RockerFlag && isDigit(inchar)) {
switch(RockerFlag) {
case 1:
W[count1++] = inchar;
handleRockerData(P, &count2, &LeftLeftRight);
break;
case 2:
P[count2++] = inchar;
handleRockerData(W, &count1, &LeftUpDown);
break;
case 3:
Q[count3++] = inchar;
break;
case 4:
S[count4++] = inchar;
break;
}
}
// 防止数组越界
count1 = (count1 > 3) ? 0 : count1;
count2 = (count2 > 3) ? 0 : count2;
count3 = (count3 > 3) ? 0 : count3;
count4 = (count4 > 3) ? 0 : count4;
}
}
解释:
serialEvent()
检查串口是否有数据并读取输入字符,通过不同字符控制电机动作。例如:'A'
:前进'B'
:后退'C'
和'D'
:分别为左转和右转。
- 该函数通过设置
RockerFlag
控制不同方向或停止状态。
7. 主循环 loop()
void loop() {
wdt_reset(); // 喂狗,重置看门狗计时
vBeep();
Led();
if (RockerFlag != 0) {
ElectricMachinery();
}
}
解释:
loop()
是代码的核心执行循环。每次循环中,重置看门狗定时器 (wdt_reset()
),确保系统不会因超时而复位。- 调用
vBeep()
检查电压并控制蜂鸣器,Led()
控制 LED 闪烁,ElectricMachinery()
实现电机动作控制。 - 没有
SerialEvent
函数的原因:包含在core文件中,属于弱函数,可用强符号函数(普通函数)覆盖定义 - arduino代码的时间执行过程
main()
│
├─► setup()
│
└─► 无限循环
│
├─► loop()
│
└─► serialEventRun()
│
└─► serialEvent() (如果Serial有数据)
- arduino的main函数实现
// Arduino核心的main函数实现 (main.cpp)
int main(void) {
// 初始化工作
init();
initVariant();
#if defined(USBCON)
USBDevice.attach();
#endif
// 调用用户setup
setup();
// 主循环
for (;;) {
// 调用用户loop
loop();
// 检查串口事件
if (serialEventRun)
serialEventRun();
}
return 0;
}
- serialEvent函数的查看
// HardwareSerial.cpp中的实现
void serialEventRun(void) {
#if defined(HAVE_HWSERIAL0)
if (Serial0_available && serialEvent && Serial.available())
serialEvent();
#endif
#if defined(HAVE_HWSERIAL1)
if (Serial1_available && serialEvent1 && Serial1.available())
serialEvent1();
#endif
#if defined(HAVE_HWSERIAL2)
if (Serial2_available && serialEvent2 && Serial2.available())
serialEvent2();
#endif
#if defined(HAVE_HWSERIAL3)
if (Serial3_available && serialEvent3 && Serial3.available())
serialEvent3();
#endif
}
总结
这段代码通过串口接收 PS2 手柄的指令来控制电机、舵机和蜂鸣器,具有 LED 指示和低电压报警功能。代码结构模块化、易扩展,并加入了看门狗机制确保系统稳定性。通过将指令与控制逻辑分离,不仅可以方便地扩展不同的控制模式,还能适应更多类型的外设,使整个系统更加灵活和可靠。
- 串口控制:通过串口接收 PS2 手柄的指令,系统能够实现前进、后退、转向等功能,并能根据收到的不同字符指令控制不同的硬件动作。
- 看门狗机制:通过看门狗机制,系统在出现卡顿或无法正常响应时自动复位,确保系统的长时间稳定运行。
- 蜂鸣器和 LED 指示:蜂鸣器用于低电压报警,LED 则用于指示系统状态,这些都提升了系统的可用性。
- PWM 与舵机控制:使用软件 PWM 控制电机输出强度,舵机用于精确控制方向,适用于控制车轮的转向或其他机械动作。
serialEvent()
函数简化版本
void serialEvent()
{
while (Serial.available() > 0) //一直等待数据接收完成 用if的话loop函数执行一次接受1个字符
{
char inchar = Serial.read();
if (inchar == 'A' )
{
RockerFlag = 0;
WorkMillis = millis();
Front( 90, Max, 0);
inchar = "";
while (Serial.read() >= 0);
return;
}
if (inchar == 'B' )
{
RockerFlag = 0;
WorkMillis = millis();
Back(90, 0, Max);
inchar = "";
while (Serial.read() >= 0);
return;
}
if (inchar == 'C' )
{
RockerFlag = 0;
WorkMillis = millis();
Left(30, Max, 0);
inchar = "";
while (Serial.read() >= 0);
return;
}
if (inchar == 'D' )
{
RockerFlag = 0;
WorkMillis = millis();
Right(150, Max, 0);
inchar = "";
while (Serial.read() >= 0);
return;
}
if (inchar == 'E' )
{
RockerFlag = 0;
WorkMillis = millis();
if (Max < 250)
{
Max += 10;
Beep(1, 130 , 100);
inchar = "";
while (Serial.read() >= 0);
return;
}
else
{
Beep(1, 160, 500);
inchar = "";
while (Serial.read() >= 0);
return;
}
}
if (inchar == 'F' )
{
RockerFlag = 0;
WorkMillis = millis();
if (Max > Min + 10)
{
Max -= 10;
Beep(1, 130, 100);
inchar = "";
while (Serial.read() >= 0);
return;
}
else
{
Beep(1, 160, 500);
inchar = "";
while (Serial.read() >= 0);
return;
}
}
if (inchar == 'G' )
{
RockerFlag = 0;
WorkMillis = millis();
inchar = "";
while (Serial.read() >= 0);
return;
}
if (inchar == 'H' )
{
Stop();
Min = 80;
Max = 150;
LeftUpDown = 128;
LeftLeftRight = 128;
RightUpDown = 128;
RightLeftRight = 128;
RockerFlag = 0;
count1 = 0;
count2 = 0;
memset(W, NULL, 4);
memset(P, NULL, 4);
inchar = "";
WorkMillis = millis();
while (Serial.read() >= 0);
return;
}
if (inchar == 'I' )
{
RockerFlag = 0;
WorkMillis = millis(); //更新当前电机工作时间状态
inchar = "";
while (Serial.read() >= 0);
return;
}
if (inchar == 'J' )
{
RockerFlag = 0;
WorkMillis = millis();
inchar = "";
while (Serial.read() >= 0);
return;
}
if (inchar == 'K' )
{
RockerFlag = 0;
WorkMillis = millis();
inchar = "";
while (Serial.read() >= 0);
return;
}
if (inchar == 'L' )
{
RockerFlag = 0;
Beep(1,90,500);
WorkMillis = millis();
inchar = "";
while (Serial.read() >= 0);
return;
}
if (inchar == 'M' )
{
RockerFlag = 0;
WorkMillis = millis();
Left(30, 0, Max);
inchar = "";
while (Serial.read() >= 0);
return;
}
if (inchar == 'N' )
{
RockerFlag = 0;
WorkMillis = millis();
Right(150, 0, Max);
inchar = "";
while (Serial.read() >= 0);
return;
}
if (inchar == 'V' )
{
inchar = "";
// Serial.print("Voltage:");
Serial.println(ad);
while (Serial.read() >= 0);
return;
}
if (inchar == 'W')
{
RockerFlag = 1;
WorkMillis = millis();
count1 = 0;
memset(W, NULL, 4);
}
if (inchar == 'P')
{
RockerFlag = 2;
WorkMillis = millis();
count2 = 0;
memset(P, NULL, 4);
}
if (inchar == 'Q')
{
RockerFlag = 3;
WorkMillis = millis();
count3 = 0;
memset(Q, NULL, 4);
}
if (inchar == 'S')
{
RockerFlag = 4;
WorkMillis = millis();
count4 = 0;
memset(S, NULL, 4);
}
if (RockerFlag == 1)
{
if (isDigit(inchar)) //是数字就执行
{
W[count1++] = inchar;
}
if (count2 == 1)
{
LeftLeftRight = P[0] - 48;
}
if (count2 == 2)
{
LeftLeftRight = (P[0] - 48) * 10 + (P[1] - 48);
}
if (count2 == 3)
{
LeftLeftRight = (P[0] - 48) * 100 + (P[1] - 48) * 10 + (P[2]) - 48;
}
}
if (RockerFlag == 2)
{
if (isDigit(inchar)) //是数字就执行
{
P[count2++] = inchar;
}
if (count1 == 1)
{
LeftUpDown = W[0] - 48;
}
if (count1 == 2)
{
LeftUpDown = (W[0] - 48) * 10 + (W[1] - 48);
}
if (count1 == 3)
{
LeftUpDown = (W[0] - 48) * 100 + (W[1] - 48) * 10 + (W[2]) - 48;
}
}
if (RockerFlag == 3)
{
if (isDigit(inchar)) //是数字就执行
{
Q[count3++] = inchar;
}
}
if (RockerFlag == 4)
{
if (isDigit(inchar)) //是数字就执行
{
S[count4++] = inchar;
}
}
if (count1 > 3)
{
count1 = 0;
}
if (count2 > 3)
{
count2 = 0;
}
if (count3 > 3)
{
count3 = 0;
}
if (count4 > 3)
{
count4 = 0;
}
}
}