对PS2遥控手柄与stm32单片机通讯的理解(结合平衡小车之家的说明和程序)

为了更好地应用PS2遥控手柄,我想尽量理解一下它与stm32单片机间通讯控制的过程,首先看了平衡小车之家给的PS2遥控手柄使用说明,讲解的内容比较简洁,光凭这个说明不能很轻易地理解配套的程序逻辑,接下来结合平衡小车之家的程序内容对照说明解释一下个人理解。因是我的理解并不是官方说明,若有误请帮助指出改正,很是感谢!javascript

1、本身看一遍说明

在看程序以前要先看一下说明里的介绍,大体了解一下。
说明及测试源码:
连接:https://pan.baidu.com/s/1hC4Gbjfh87vsswuJyUsH0g
提取码:fdzfjava

2、结合说明理解程序pstwo.c(.h)

Tips:请按照顺序仔细阅读,前面介绍过的一些基础在后面其余函数中一样应用到时就再也不赘述。git

1.定义的数组:

Comd[2]={0x01,0x42} :存储了两条指令码,分别是开始指令和请求数据指令。
Data[9]={0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00} :数据存储数组,初始全为0,这是最重要的数组,功能在接下来的程序理解中慢慢介绍。
MASK[16]={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} :按键名字的数组,在宏定义中对这些按键赋予了从1~16的按键值。web

2.发送命令函数PS2_Cmd(u8 CMD)

看完介绍接下来看程序内容,首先注意,DI与DO是一对同时传输的8 bit串行数据,所谓串行数据特色即按位(1 bit)传输,其次,CLK时钟信号降低沿时完成信号的发送与接收,根据这些,咱们首先理解一下PS2_Cmd(u8 CMD)这个函数的意义:数组

void PS2_Cmd(u8 CMD)
{
 volatile u16 ref=0x01;
 Data[1] = 0;
 for(ref=0x01;ref<0x0100;ref<<=1)
 {
  if(ref&CMD)
  {
   DO_H;                   //输出一位控制位
  }
  else DO_L;
  CLK_H;                        //时钟拉高
  DELAY_TIME;
  CLK_L;
  DELAY_TIME;
  CLK_H;                        //手动拉出一个降低沿使DO和DI得以同时传送
  if(DI)
   Data[1] = ref|Data[1];      //运用或运算按位存入Data[1]的8位
 }
 delay_us(16);
}

Tips:volatile是易变型变量,是防止编译器优化代码时假设这个变量的值,保证每次当心地从新读取值。
对于图中的for循环,能够得知ref的变化是一个八位二进制数中惟一一个1的位置变化,从最低位到最高位移动,从0000 0001到1000 0000。
&按位与的操做,根据定义能够理解到ref&CMD获得的结果是:当ref中1的位置对应CMD中得位置上也为1时,结果为1;当ref中1的位置对应CMD中得位置上为0时,结果为0。CMD的其余位则不影响此结果。
而这个结果为1时,DO_H即输出1,这个结果为0时,DO_L即输出0。所以for循环八次,DO的结果就是将CMD的每一位传送了过去。
每次循环中下面这一段时钟信号拉高又拉低的操做,是为了手动置出一次降低沿,在这个降低沿中,DO信号才能得以发送,同时DI的信号得以接收回来。
所以接下来又对接收到的DI进行判断:当DI为1时,运用按位或操做,根据Data[1]初始值为0000 0000,以及按位或的定义,不难理解ref | Data[1]获得新的Data[1]的过程是:ref里的惟一的1以值不变位置不变的形式给到结果的二进制数中,好比某一次循环Data[1] = 0000 0010,ref = 0000 1000,且DI=1,则ref | Data[1]=0000 1010。
而这个给1的操做,只有这一bit的DI=1时才会进行;若DI=0,则ref只进行1的移位,不给予,但其实也就至关于这一位ref是给予了0给Data[1]。因此其实判断DI并执行从句的这一步在整个for循环后的结果便是将8 bit的DI按位保存到Data[1]。
至此,能够说理解了这个发送命令的函数的逻辑组成,总结到它的功能就是:每执行一次该函数,就将参数CMD以八位二进制按位发送给手柄,同时从手柄接收信号以八位二进制按位返回给单片机并存储到Data[1]。svg

2.读取手柄数据函数PS2_ReadData(void)

对于从手柄返回给单片机的数据,最重要的应该是按键和摇杆当前的状态数据,有了这个数据才能用程序处理判断当前用户的动做,再根据按键功能执行相应的操做程序。前面说到,Data[1]已经用来存储每次执行PS2_Cmd函数时DI返回的信号数据,那么Data数组其他的7个位置存储的就应该是须要返回给单片机进行程序处理的有效数据了。
首先要注意,数据的通信传输必须在CS拉低期间进行,因此即便有了发送命令函数,在执行这个函数前也要先拉低CS,即如图程序中开头部分的CS_L,而在通信结束、数据传输完成后,还要将CS拉回高电平,以便下一次的通信,也就是这个函数的结尾的CS_H。函数

//判断是否为红灯模式,0x41=模拟绿灯,0x73=模拟红灯
//返回值;0,红灯模式
// 其余,其余模式
void PS2_ReadData(void)
{
 volatile u8 byte=0;
 volatile u16 ref=0x01;
 CS_L;
 PS2_Cmd(Comd[0]);  //发送开始命令0X01
 PS2_Cmd(Comd[1]);  //发送请求数据命令0X42
 for(byte=2;byte<9;byte++)          //开始接受数据
 {
  for(ref=0x01;ref<0x100;ref<<=1)
   {
     CLK_H;
     DELAY_TIME;
     CLK_L;
     DELAY_TIME;
     CLK_H;
        if(DI)
        Data[byte] = ref|Data[byte];  
    }
        delay_us(16);
 }
 CS_H;
}

接下来注意到程序向手柄发送了两条命令,这两条命令都来自于以前定义的Comd[2]数组,所以接下来要知道,想要让手柄返回有效的按键状态数据给单片机,要先发送开始命令0x01和请求数据命令0x42,并且紧接着,手柄将会返回一个数据0x5A给单片机,意味着已经接收到请求,即将返回数据。再接下来,就是返回各按键以及摇杆的状态数据了。说明中数据意义对照表以下:测试

在这里插入图片描述
Idle表明这时此时该数据线上无含有效意义的数据传送。这张表乍一看并不太能明白,但至少前三行的三个十六进制数的含义咱们已经了解了。
回到PS2_ReadData这个函数的代码中继续看,接下来的一部分和PS2_Cmd中很是相似,不难理解,这一段的意义即为:内层循环结束后即将DI返回的八位二进制数据按位存储到了Data数组中的某一个元素位置,而外层循环则是将数据依次存储从Data[2]到Data[8]的位置。
到这里我才意识到两个函数中各个用到delay的意义,结合时序图其实很好理解,关于CLK拉低又拉高期间DELAY_TIME是CLK时钟信号频率的需求,说明中提到,若是数据接收不稳定,能够适当增长频率;而for循环结束后的delay_us应该是由于要等待DI和DO数据的发送与接收完成。
这个函数功能总结为:发送开始命令和请求数据命令,而后接收到返回的预告,存入Data[2],紧接着接收到按键及摇杆当前的状态数据,并存储到Data[3]到Data[8]这七个元素位置。
至此,发送命令与接收数据函数都得以理解。优化

3.判断模式函数u8 PS2_RedLight(void)

//判断是否为红灯模式,0x41=模拟绿灯,0x73=模拟红灯
//返回值;0,红灯模式
// 其余,其余模式
u8 PS2_RedLight(void)
{
 CS_L;
 PS2_Cmd(Comd[0]);  //发送开始命令0X01
 PS2_Cmd(Comd[1]);  //发送请求数据命令0X42
 CS_H;
 if( Data[1] == 0X73)   return 0 ;
 else return 1;
}

这个函数很简单,就是如数据意义对照表中1行,DO发送0X42同时DI返回ID,这个ID也是一个十六进制数,这个函数就是判断这个ID是什么,如果0x73,则为红灯模式,该函数返回值为0;如果其余值,则函数返回值为1。至于模式的设置咱们接下来会再介绍。
注意这里判断的是Data[1],这是由于这个ID是在DO发送0X42同时DI返回的值,按照PS2_Cmd的意义,应当是存储在Data[1]里的,而不是其余元素位置。spa

4.清除数据缓冲区PS2_ClearData()

//清除数据缓冲区
void PS2_ClearData(void)
{
 u8 a;
 for(a=0;a<9;a++)
  Data[a]=0x00;
}

相信这个无需解释,就是清除以前缓冲存储在其中的数据,将Data数组中的元素所有归零,以便下次使用。

5.分析返回数据以判断哪个按键按下u8 PS2_DataKey()

u8 PS2_DataKey()
{
 u8 index;
 PS2_ClearData();
 PS2_ReadData();
  Handkey=(Data[4]<<8)|Data[3];     //这是16个按键 按下为0, 未按下为1
  for(index=0;index<16;index++)
   {     
     if((Handkey&(1<<(MASK[index]-1)))==0)
     return index+1;
   }
 return 0;          //没有任何按键按下
}

手柄上的按键共有16个,接收到当前按键状态的数据,是以两个八位二进制数也就是两个元素存储在Data数组里的,根据读数据的函数以及数据意义对照表能够知道,便是Data[3]和Data[4],共16 bit,每一位存储一个按键当前的状态值,按键按下为0,未按为1。
Handkey在程序一开始进行了定义,是一个u16的变量,所以是16位二进制数,Data[4]<<8这一步的结果便是高8位为原Data[4],低8位为0000 0000 ,结果再与Data[3]进行按位或,获得的结果应是高8位为原Data[4],低8位为原Data[3],将这个结果赋给Handkey,则这个16位二进制数里就包含了全部的键状态值。
接下来的for循环是检测哪个按键被按下的最重要的部分:
MASK[index]取出数组中的键值,再减一,获得的结果做为一个移位的位数X,1<<(MASK[index]-1)即让0000 0000 0000 0001中惟一的1左移这个位数X,由于每一个键的键值都是它在数组中的序号加一,因此0000 0000 0000 0001移位后获得的结果中惟一的1所在的位置恰好是取出的那个键在数组中的位置(序号),移位后的结果与Handkey进行按位与,逻辑结果为:1<<(MASK[index]-1)的结果中应只有一个位置上值是1,则只有Handkey中对应一样位置上值是0时,这两者按位与的结果才为0。Handkey的其余位上值是什么不影响这个结果。
只有当结果为0时,index+1并做为函数返回值,则这个index+1就是键值。

这一段若是难以理解,最简便的办法就是index取一个值,走一遍程序,就能理解了。
这个循环执行16次,即将Handkey的每一位都进行检测,检测出按键状态值为0就当即返回这个键的键值,而且结束整个函数(return的做用)。
循环结束后尚未return值的话就说明没有按键按下,则return 0。
注意,开头的PS2_ClearData();再PS2_ReadData();是必要的,这是保证如今数组里存的是当前当即更新的键值。
注意,这个函数只能检测一个按键被按下,若同时按多个按键,则只能检测到键值最小的那个,所以若是有兴趣还能够本身写一个组合按键的函数,能实现更多功能。

6.获得摇杆的状态数据u8 PS2_AnologData(u8 button)

//获得一个摇杆的模拟量 范围0~256
u8 PS2_AnologData(u8 button)
{
 return Data[button];
}

根据数据意义对照表,Data[5]到Data[8]存储的是摇杆的状态数据,分为左/右摇杆的X/Y轴向值,共四个值,当摇杆向X/Y轴推进时,不一样的位置会有不一样的数值,每一个轴向值范围都是0~256,0x00为最左或最上,0xff为最右或最下。应用时根据入口参数button的值返回Data数组相应位置序号里存储的状态数,所以在头文件中也宏定义了四个值对应的数组位置序号值5/6/7/8。

到这里咱们能够引入我上网查的资料中所述所谓红灯模式与绿灯模式:
红灯模式时:左右摇杆发送模拟值,0x00~0xFF 之间,且摇杆按下的键值值 L三、R3 有效;
绿灯模式时:左右摇杆模拟值为无效,推到极限时,则对应发送为
UP、RIGHT、DOWN、LEFT、△、○、╳、□,此时按键 L三、R3 无效。

所以若是想要进行一些流畅性的控制好比小车行驶等等,则使用红灯模式比较合适,我认为像变化较大的调参用摇杆也比较方便,而模式选择MODE键是否可用在下面的函数中也能够设置。

7.其余函数

剩下的函数主要都是靠在CS拉低期间发送各类指令码实现的,这里简单带过一下重点的三个函数:

(1)手柄震动函数PS2_Vibration(u8 motor1, u8 motor2)

为了游戏体验感好比赛车撞墙等等,手柄还加入了震动功能,主要由参数motor1和motor2来决定,motor1仅在为0x00时关右侧电机,其余值则开右侧电机并小幅震动,motor2的值则可从0x40~0xff,这时左侧电机震动,且motor2的值越大,震动越强。

(2)发送模式设置PS2_TurnOnAnalogMode(void)

//发送模式设置
void PS2_TurnOnAnalogMode(void)
{
 CS_L;
 PS2_Cmd(0x01);  
 PS2_Cmd(0x44);  
 PS2_Cmd(0X00);
 PS2_Cmd(0x01); //analog=0x01;digital=0x00 软件设置发送模式
 PS2_Cmd(0x03); //Ox03锁存设置,即不可经过按键“MODE”设置模式。
       //0xEE不锁存软件设置,可经过按键“MODE”设置模式。
 PS2_Cmd(0X00);
 PS2_Cmd(0X00);
 PS2_Cmd(0X00);
 PS2_Cmd(0X00);
 CS_H;
 delay_us(16);
}

这里的重点在于函数内的第5行和第6行:
第5行语句实现软件设置发送模式,指令值为0x01则可发送摇杆模拟量,即红灯模式;指令值为0x00则为绿灯模式,不发送模拟量。
第6行则是对于发送模式可不能够用MODE按键设置的指令,指令值为0X03则只能够经过第5行指令软件设置发送模式;指令值为0xEE则不锁存软件设置,能够经过按MODE键设置红灯/绿灯模式。

(3)手柄配置初始化函数PS2_SetInit(void)

包含完成各类配置函数及保存配置函数,其中原代码默认注释掉了震动模式的配置,能够本身开启。

8.总结:

在main.c中有测试代码,理解了以上函数后就很好理解了,同时也很方便本身改动设置按键功能了,虽然本篇理解有些冗长,不过在写这篇理解的过程当中仍是颇有意思的,尤为对于按位与和按位或的逻辑功能,这辈子是忘不了了…但愿本篇对于想用遥控手柄作一些控制的读者能有所帮助,再次但愿若是理解有误能有大神指出,万分感谢!