之前使用315MHZ可以进行远程控制,但是那个只有4个按钮,理论上只能控制4个变量的输入,并且偶尔信号还会串,所以需要探索其他远程控制方式。

  PS2是Sony PlayStation2游戏机的遥控手柄,它采用的是2.4G无线通讯技术,有效控制范围能达到10米。恰好有人已经破解了通讯协议,使得手柄能够接到其他器件上遥控使用,比如这个叙利亚战场上使用PS2手柄控制的遥控机枪,我们这里只是简单的用在智能小车上,当然可以扩展到很多需要控制的地方,他的特点是性价比非常高(只要几十块钱就能买到一套山寨的控制器和接口,正版Playstation 2是不可能这个价格的,但是功能一点不差),而且按键丰富,方便扩展。

PS2手柄介绍

   PS2手柄和接收器如下图:

▲ PS2 手柄和接收器

   手柄,主要用来发送按键信息,接收器与主机相连(主机可以是树莓派,也可以是Arduino,STM32等等)用来接收手柄发出的信息,并传递给主机。主机也可以通过接收器向手柄发送命令,用来配置手柄的发送模式。

PS2接收器接口说明

   在接线之前,要说明一下PS2接收器的各个针脚,一般购买时上面会有标识。

▲ PS2接收器针脚

   总共有六个针脚可用。其中:

  • DI/DATA:信号流向,从手柄像主机发送信号,对于主机来说是信号输入,这里需要加电阻。信号是一个8位的串行数据,同步传送与时钟下降沿。信号的读取在时钟由高到低的变化过程中完成。
  • DO/CMD:信号流向,从主机到手柄发送信号,对于主机来说是信号删除,这个信号跟DI/DAT信号相对,信号是一个8位的串行数据,同步于时钟的下降沿。
  • GND:接地
  • VDD:接电源正极,范围3~5V
  • CS:用于提供手柄触发信号。在通讯期间,处于低电平。
  • CLK:时钟信号,由主机发出,用于保持数据同步

   在通讯过程中,一串数据通讯完成之后(9个8位Bit数据)CS才会由低转为高,在通讯期间,一直处于低电平。在时钟下降沿时,完成数据(1bit)数据的发送和接收,发送和接收是同时完成的。当主机箱读取手柄数据,或者向手柄发送命令时,先将CS置为低电平,然后发出命令0x01;手柄会回复他的ID “0x41=绿灯模式,0x73为红灯模式”;在手柄发送ID的同时,主机将发送0x42,请求数据;随后手柄发送出0x5A,告诉单片机,“数据来了”。

    一个通讯周期有9个字节,每个字节8位数据,每个字节8位数据按位传送,位位1时,输出高电平,0时输出低电平。

   

▲ 数据意义对照表,DO表示主机发送给手柄,DI表示手柄发送给主机

   这里最重要的是第3位数据,当有键按下,对应位为“0”,其他位为“"1",比如当“SELECT”键按下时,BIT0为“0”,data[3]=11111110;

   要测试购买的手柄和接收器是否正常,可以用如下方法测试:手柄需要装上2节7号电池。接收器的电源连到主机上,VCC和GND正负极接好,不接其他数据线。手柄上有电源开关“ON开/OFF关”,将手柄开关拨到ON上,手柄在没有搜索到接收器的情况下,手柄上的等会不停的闪,在一定的时间内,还未收到接收器,手柄将进入待机模式,手柄上的灯会灭掉,这时,需要通过按“START”键,唤醒手柄。接收器在供电时,如果没有匹配上手柄,接收器绿灯会不停的闪。

   手柄在打开状态,接收器供电,手柄和接收器会自动配对,这时接收器和手柄的灯会常亮,这表示配对成功,说明接收器和手柄是完好的。

PS2通讯协议代码实现

   在用.NET Core实现之前,网上全部都是PS2通讯协议的Python,C++,C++,Arduino,STM32的版本代码,就连我在淘宝上的买家都不提供.NET Core的C#版本,没办法,只有自己研究,我下面的C#实现,是参考了这位网友 使用树莓派gpio连接ps2手柄模块(附程序)的实现,他是用的Python,我之前在GitHub上也找了C++实现,试着写了一下,不成功。

  其实这种位的数据传送跟前面我写的 .NET Core IoT 驱动 TM1637四位数码管 这篇文章非常类似。在上图中,我们可以看到,一个通讯包含9个字节,每个字节8位,9个字节是挨个1个字节1个字节传输的,所以,我们先实现单1个字节的传输,1个字节包含8位二进制数据,比如,“01010101“来表示,遇到“0“时我们通过GPIO 往CMD口写入低电平,遇到“1”时写入高电平。方法如下:

 private void TransmitByte(byte input)
        {
            // Data byte received
            PS2Data[1] = 0;

            foreach (int refV in enumrableInts)
            {
                // If the bit to be transmitted is 1 then set the command pin to 1
                if ((refV & input) == refV)
                {
                    gpio.Write(commandPin, PinValue.High);
                }
                else // Otherwise set it to 0.
                {
                    gpio.Write(commandPin, PinValue.Low);
                }

                // Pull clock low to transfer bit
                gpio.Write(clockPin, PinValue.High);
                Thread.Sleep(CLK_DELAY);
                gpio.Write(clockPin, PinValue.Low);
                Thread.Sleep(CLK_DELAY);
                gpio.Write(clockPin, PinValue.High);
                // If the data pin is now high then save the input.
                if (gpio.Read(dataPin) == PinValue.High)
                {
                    //  RXdata = (byte)i;
                    PS2Data[1] = (byte)(refV | PS2Data[1]);
                }
            }
            Thread.Sleep(BYTE_DELAY);
        }

   这里的enumerableInts的定义是:

private int[] enumrableInts = { 1, 2, 4, 8, 16, 32, 64, 128 };

   将输入的byte,跟enumrableInts里面的1~128,其实就是00000001,00000010,...10000000,取位运算与“&”,表示当两个对应位都为1时,才是true,否则为false。这里用的(refV & input) == refV,表示如果refV对应的位位1,那么跟对应位为1,其余为为0的数据进行与,其结果等于本身。如果对应为为1,则向CMDPin输出高电平,否则输出低电平。

   输入完成之后,将CLK从高切换到低,再切换到高完成传输。

   发送完成之后,接着读取dataPin,即手柄发送给主机的数据,如果,获取到的数据为高电平,则将PS2Data[1]对应的位,设置为1,设置方法为,将refV跟PS2Data[1]进行“|”或运算。PS2Data[1]默认的8个字节都是0,refV对应的位数为1,其余为0,所以“|”运算就会将ref对应的位在PS2Data[1]对应的位设为1,其余设置为0.跟当读到改为的信号为高电平时,将改位置为1逻辑相符合。PS2Data[]的结构定义如下:

 public byte[] PS2Data = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };

   PS2Data为9个字节,每字节8位数据,用来对应读取手柄发送给主机的数据。

   上面的方法演示了主机向手柄发送单个字节8位的方法,以及读取方法,接下来,将该方法封装一下,就可以直接发送字节数组了,方法如下:

private void TransmitBytes(byte[] commands)
{
    foreach (var b in commands)
    {
        TransmitByte(b);
    }
}

   现在,我们要通过主机向手柄发送模式信息,即发送[0x01,0x42] 数据,这里先定义一个变量:

private byte[] command = { 0x01, 0x42 };

  然后用一个方法获取手柄发送给主机的ID,判断是那种模式:

/// <summary>
/// 判断是否为红灯模式,0x41=模拟绿灯 160,0x73=模拟红灯 185
/// </summary>
/// <returns></returns>
public bool RedModel()
{
    gpio.Write(csPin, PinValue.Low);
    TransmitBytes(command);
    gpio.Write(csPin, PinValue.High);
    if (PS2Data[1] == DIGITALMODE)//ANALOGMODE
    {
        return true;
    }
    return false;
}

    我们需要的是红灯模式,即,通过主机向手柄请求command数据之后,读取手柄返回的第二个字节PS2Data[1],记为手柄的ID,如果是  DIGITALMODE = 160;//0x41;绿灯模式,我们返回true,该方法一般供外部循环调用判断模式。

   另外,一个很重要的一点是,在发送发送字节数组前,一定要将CSPin置为低电平,发送完成之后,置回高电平。

  模式确认好了之后,可以开始读取数据了。

public void ReadData()
{
    gpio.Write(csPin, PinValue.Low);
    TransmitBytes(command);

    for (int i = 2; i < 9; i++)
    {
        foreach (var j in enumrableInts)
        {
            gpio.Write(clockPin, PinValue.High);
            gpio.Write(clockPin, PinValue.Low);
            Thread.Sleep(CLK_DELAY);
            gpio.Write(clockPin, PinValue.High);
            if (gpio.Read(dataPin) == PinValue.High)
            {
                PS2Data[i] = (byte)(j | PS2Data[i]);
            }
        }
        Thread.Sleep(CLK_DELAY);
    }
    gpio.Write(csPin, PinValue.High);
    //Console.WriteLine(string.Join(",", PS2Data));
}

   首先通过TransmitBytes向手柄发送完command数据之后,因为在TranmitBytes里面已经读了1, 2 位返回的数据到PSData里,接下来,读取3~9位数据,将手柄返回的2-9位数据存储到PSData对应的PSData[2]~PSData[8]里。然后对照上表中右边的Bit0~Bit7即可判断出手柄上哪个按键被按下。

public int DataKey()
{
    int index;
    ClearData();
    ReadData();
    int handKey = (PS2Data[4] << 8) | PS2Data[3];     //这是16个按键  按下为0, 未按下为1
    for (index = 0; index < 16; index++)
    {
        if ((handKey & (1 << (mask[index]))) == 0)
            return index + 1;
    }
    return 0;
}
完整代码 

 全部完整代码如下,首先是PiPS2X类:

public class PiPS2X
{
    //These are our button constants 1
    public const int PSB_SELECT = 0;
    public const int PSB_L3 = 1;
    public const int PSB_R3 = 2;
    public const int PSB_START = 3;
    public const int PSB_PAD_UP = 4;
    public const int PSB_PAD_RIGHT = 5;
    public const int PSB_PAD_DOWN = 6;
    public const int PSB_PAD_LEFT = 7;
    public const int PSB_L2 = 8;
    public const int PSB_R2 = 9;
    public const int PSB_L1 = 10;
    public const int PSB_R1 = 11;
    public const int PSB_GREEN = 12;
    public const int PSB_RED = 13;
    public const int PSB_BLUE = 14;
    public const int PSB_PINK = 15;

    public const int PSB_TRIANGLE = 17;
    public const int PSB_CIRCLE = 18;
    public const int PSB_CROSS = 19;
    public const int PSB_SQUARE = 20;

    //These are stick values
    public const int PSS_RX = 5; //右摇杆X轴数据
    public const int PSS_RY = 6;
    public const int PSS_LX = 7;
    public const int PSS_LY = 8;
    public static TimeSpan CLK_DELAY = new TimeSpan(500);
    private static TimeSpan BYTE_DELAY = new TimeSpan(160);
    private const int READ_DELAY = 16000;
    private const int CMD_DELAY = 1;
    private const int MAX_READ_DELAY = 10;
    private const int MAX_INIT_ATTEMPT = 50;
    public const byte DIGITALMODE = 160;//0x41;
    public const byte ANALOGMODE = 185;//Red Model 0x73;
    public const byte ALLPRESSUREMODE = 0x79;
    public const byte DS2NATIVEMODE = 0xF3;
    private byte[] command = { 0x01, 0x42 };
    private byte[] shortPool = { 0x01, 0x42, 0x00, 0x00, 0x00 };
    public byte[] PS2Data = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
    private byte[] enterConfigMode = { 0x01, 0x43, 0x00, 0x01, 0x00 };
    private byte[] set_mode_analog_lock = { 0x01, 0x44, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00 };
    private byte[] exitConfigMode = { 0x01, 0x43, 0x00, 0x00, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A };
    private byte[] type_read = { 0x01, 0x45, 0x00, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A };
    private byte[] config_AllPressure = { 0x01, 0x4F, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00 };
    public byte[] mask = { PSB_SELECT,
    PSB_L3,
    PSB_R3 ,
    PSB_START,
    PSB_PAD_UP,
    PSB_PAD_RIGHT,
    PSB_PAD_DOWN,
    PSB_PAD_LEFT,
    PSB_L2,
    PSB_R2,
    PSB_L1,
    PSB_R1 ,
    PSB_GREEN,
    PSB_RED,
    PSB_BLUE,
    PSB_PINK};//按键值与按键明

    private int commandPin;
    private int dataPin;
    private int clockPin;
    private int csPin;
    private GpioController gpio;

    private int[] enumrableInts = { 1, 2, 4, 8, 16, 32, 64, 128 };
    public PiPS2X(int _dataPin, int _commandPin, int _csPin, int _clkPin)
    {
        commandPin = _commandPin;
        dataPin = _dataPin;
        clockPin = _clkPin;
        csPin = _csPin;
        gpio = new GpioController(PinNumberingScheme.Logical);
        gpio.OpenPin(dataPin, PinMode.Input);
        gpio.OpenPin(commandPin, PinMode.Output);
        gpio.OpenPin(csPin, PinMode.Output);
        gpio.OpenPin(clockPin, PinMode.Output);
        Initial();
    }

    private void Initial()
    {
        // Set command pin and clock pin high, ready to initialize a transfer.
        gpio.Write(csPin, PinValue.High);
        gpio.Write(clockPin, PinValue.High);
        gpio.Write(commandPin, PinValue.High);
        Thread.Sleep(100);
    }


    private void TransmitBytes(byte[] commands)
    {
        foreach (var b in commands)
        {
            TransmitByte(b);
        }
    }
    private void TransmitByte(byte input)
    {
        // Data byte received
        PS2Data[1] = 0;

        foreach (int refV in enumrableInts)
        {
            // If the bit to be transmitted is 1 then set the command pin to 1
            if ((refV & input) == refV)
            {
                gpio.Write(commandPin, PinValue.High);
            }
            else // Otherwise set it to 0.
            {
                gpio.Write(commandPin, PinValue.Low);
            }

            // Pull clock low to transfer bit
            gpio.Write(clockPin, PinValue.High);
            Thread.Sleep(CLK_DELAY);
            gpio.Write(clockPin, PinValue.Low);
            Thread.Sleep(CLK_DELAY);
            gpio.Write(clockPin, PinValue.High);
            // If the data pin is now high then save the input.
            if (gpio.Read(dataPin) == PinValue.High)
            {
                //  RXdata = (byte)i;
                PS2Data[1] = (byte)(refV | PS2Data[1]);
            }
        }
        Thread.Sleep(BYTE_DELAY);
    }

    /// <summary>
    /// 判断是否为红灯模式,0x41=模拟绿灯 160,0x73=模拟红灯 185
    /// </summary>
    /// <returns></returns>
    public bool RedModel()
    {
        gpio.Write(csPin, PinValue.Low);
        TransmitBytes(command);
        gpio.Write(csPin, PinValue.High);
        if (PS2Data[1] == DIGITALMODE)//ANALOGMODE
        {
            return true;
        }
        return false;
    }

    public void ReadData()
    {
        gpio.Write(csPin, PinValue.Low);
        TransmitBytes(command);

        for (int i = 2; i < 9; i++)
        {
            foreach (var j in enumrableInts)
            {
                gpio.Write(clockPin, PinValue.High);
                gpio.Write(clockPin, PinValue.Low);
                Thread.Sleep(CLK_DELAY);
                gpio.Write(clockPin, PinValue.High);
                if (gpio.Read(dataPin) == PinValue.High)
                {
                    PS2Data[i] = (byte)(j | PS2Data[i]);
                }
            }
            Thread.Sleep(CLK_DELAY);
        }
        gpio.Write(csPin, PinValue.High);
        //Console.WriteLine(string.Join(",", PS2Data));
    }

    //得到一个摇杆的模拟量	 范围0~256
    public byte AnologData(int button)
    {
        return PS2Data[button];
    }

    public int DataKey()
    {
        int index;
        ClearData();
        ReadData();
        int handKey = (PS2Data[4] << 8) | PS2Data[3];     //这是16个按键  按下为0, 未按下为1
        for (index = 0; index < 16; index++)
        {
            if ((handKey & (1 << (mask[index]))) == 0)
                return index + 1;
        }
        return 0;
    }

    void ClearData()
    {
        for (int a = 0; a < 9; a++)
            PS2Data[a] = 0x00;
    }
    private byte Set(byte x, int y)
    {
        return (byte)(x | (1 << y));
    }

    public bool Check(byte x, int y)
    {
        return (x & (1 << y)) == (1 << y);
    }

    public long Millis()
    {
        return (long)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalMilliseconds);
    }
}

   然后在主函数里,调用:

static void PiPS2XTest()
{
    PiPS2X ps2 = new PiPS2X(26, 27, 17, 22);
    Console.WriteLine("Initial Success");
    while (true)
    {
        if (ps2.RedModel())
        {
            Thread.Sleep(PiPS2X.CLK_DELAY);
            int k = ps2.DataKey();
            if (k != 0)
            {
                Console.WriteLine($"Press Key:{k}");
                byte psData = ps2.PS2Data[3];
                byte psData2 = ps2.PS2Data[4];
                // Example reading each button. 
                if (k == PiPS2X.PSB_SELECT)
                    Console.WriteLine("BTN_SELECT is pressed");
                if (k == PiPS2X.PSB_R3)
                    Console.WriteLine("BTN_RIGHT_JOY is pressed");
                if (k == PiPS2X.PSB_L3)
                    Console.WriteLine("BTN_LEFT_JOY is pressed");
                //if (!CHK(pips2.PS2data[3], BTN_START))
                //	printf("BTN_START is pressed\n");
                if (k == PiPS2X.PSB_PAD_UP)
                    Console.WriteLine("BTN_UP is pressed");
                if (k == PiPS2X.PSB_PAD_RIGHT)
                    Console.WriteLine("BTN_RIGHT is pressed");
                if (k == PiPS2X.PSB_PAD_DOWN)
                    Console.WriteLine("BTN_DOWN is pressed");
                if (k == PiPS2X.PSB_PAD_LEFT)
                    Console.WriteLine("BTN_LEFT is pressed");
                if (k == PiPS2X.PSB_L2)
                    Console.WriteLine("BTN_L2 is pressed");
                if (k == PiPS2X.PSB_R2)
                {
                    Console.WriteLine("Right Joy   Horizontal = %d\tVertical = %d", ps2.PS2Data[5], ps2.PS2Data[6]);
                }
                if (k == PiPS2X.PSB_L1)
                    Console.WriteLine("BTN_L1 is pressed");
                if (k == PiPS2X.PSB_R1)
                    Console.WriteLine("BTN_R1 is pressed");
                if (k == PiPS2X.PSB_GREEN)
                    Console.WriteLine("BTN_TRIANGLE is pressed");
                if (k == PiPS2X.PSB_RED)
                    Console.WriteLine("BTN_CIRCLE is pressed");
                if (k == PiPS2X.PSB_BLUE)
                    Console.WriteLine("X is pressed");
                if (k == PiPS2X.PSB_PINK)
                    Console.WriteLine("BTN_SQUARE is pressed");
            }
        }
        else//判断手柄不是红灯模式,指示灯LED熄灭
        {
            Thread.Sleep(PiPS2X.CLK_DELAY);
        }
        Thread.Sleep(100);
    }
}

   至此,用上述方法,就可以将PS2接入到自己的应用中去并进行控制了。