之前使用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接入到自己的应用中去并进行控制了。
感谢大神,帮我大忙了!