智能小车(五):PS2模块

星期日, 11月 10, 2024 | 9分钟阅读 | 更新于 星期日, 12月 8, 2024

YuanFeng Xie

历史回顾

使用 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() 函数检测串口是否有数据传入,若有数据,则读取第一个字符作为指令 cmdloop() 函数根据 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 变量用于检测摇杆状态。多个字符数组 WPQS 用于存储不同的指令数据。
  • dianyaad 用于存储电压数据。

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;
    }

  }
}

© 2024 小谢同学的修行小屋

🌱 Powered by Hugo with theme Dream.

关于我

一名喜欢RUST的菜鸟程序员

教育背景(研二在读)

  • 2019.09 - 2023.06 天津大学 · 智能与计算学部 · 网络安全专业 · 全日制 · 本科
  • 2023.09 - 2026.06 天津大学 · 智能与计算学部 · 电子信息专业 · 全日制 · 硕士研究生
浏览器漏洞挖掘项目

V8引擎的模糊测试工具改进

时间:2021.09-2022.04

内容:聚焦于优化JavaScript引擎的测试方法。研究团队基于SOAT改进部署了V8引擎测试环境,并通过分析测例覆盖率和已知CVE,优化了测试流程。

成果:成功捕获了一个谷歌引擎的逻辑漏洞,获得了修复反馈

JS引擎WASM模块漏洞挖掘研究

时间:2023.11-2024.4

内容:阅读标准手动编写了WASM语法规则,调研了主流JS引擎的WASM支持情况,并基于V8构建了WASM二进制框架。

成果:语法规则、调研报告和二进制构建框架,成功捕获特性缺陷并得到修复

数据库缺陷研究项目

关系型数据库缺陷的实证研究

时间:2022.12-2023.11

内容:通过构建统一的数据库逻辑框架,系统收集并分析了大量数据库缺陷。对777个缺陷进行了深入分析,特别聚焦SQL数据类型问题,并据此改进了模糊测试方法。

成果:构建缺陷数据集, 开发SQLT测试框架

网络协议漏洞挖掘项目

基于 RFC的 SSL/TLS模糊测试

时间:2024.4 – today

内容:梳理TLS协议模糊测试的相关工作,关注基于状态机的网络协议模糊测试方法。复现历史漏洞,了解TLS报文结构,追溯相关漏洞代码。分析RFC文档,提取字段约束,构建状态转移范式,并据此完善了TLS协议的有限状态机模型。

成果:完成相关工作调研, 漏洞复现报告, 状态范式数据集