为了更好地应用PS2遥控手柄,我想尽可能理解一下它与stm32单片机间通信控制的过程,首先看了平衡小车之家给的PS2遥控手柄使用说明,讲解的内容比较简洁,光凭这个说明不能很轻易地理解配套的程序逻辑,接下来结合平衡小车之家的程序内容对照说明解释一下我的理解。因是个人理解并非官方说明,如有误请帮助指出改正,非常感谢!
在看程序之前要先看一下说明里的介绍,大致了解一下。
说明及测试源码:
链接:https://pan.baidu.com/s/1hC4Gbjfh87vsswuJyUsH0g
提取码:fdzf
Tips:请按照顺序仔细阅读,前面介绍过的一些基础在后面其他函数中同样应用到时就不再赘述。
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的按键值。
看完介绍接下来看程序内容,首先注意,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]。
对于从手柄返回给单片机的数据,最重要的应该是按键和摇杆当前的状态数据,有了这个数据才能用程序处理判断当前用户的动作,再根据按键功能执行相应的操作程序。前面说到,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]这七个元素位置。
至此,发送命令与接收数据函数都得以理解。
//判断是否为红灯模式,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]里的,而不是其他元素位置。
//清除数据缓冲区
void PS2_ClearData(void)
{
u8 a;
for(a=0;a<9;a++)
Data[a]=0x00;
}
相信这个无需解释,就是清除之前缓冲存储在其中的数据,将Data数组中的元素全部归零,以便下次使用。
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();是必要的,这是保证现在数组里存的是当前立即更新的键值。
注意,这个函数只能检测一个按键被按下,若同时按多个按键,则只能检测到键值最小的那个,因此如果有兴趣还可以自己写一个组合按键的函数,能实现更多功能。
//得到一个摇杆的模拟量 范围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 之间,且摇杆按下的键值值 L3、R3 有效;
绿灯模式时:左右摇杆模拟值为无效,推到极限时,则对应发送为
UP、RIGHT、DOWN、LEFT、△、○、╳、□,此时按键 L3、R3 无效。
因此如果想要进行一些流畅性的控制比如小车行驶等等,则使用红灯模式比较合适,我认为像变化较大的调参用摇杆也比较方便,而模式选择MODE键是否可用在下面的函数中也可以设置。
剩下的函数主要都是靠在CS拉低期间发送各种指令码实现的,这里简单带过一下重点的三个函数:
为了游戏体验感比如赛车撞墙等等,手柄还加入了震动功能,主要由参数motor1和motor2来决定,motor1仅在为0x00时关右侧电机,其他值则开右侧电机并小幅震动,motor2的值则可从0x40~0xff,这时左侧电机震动,且motor2的值越大,震动越强。
//发送模式设置
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键设置红灯/绿灯模式。
包含完成各种配置函数及保存配置函数,其中原代码默认注释掉了震动模式的配置,可以自己开启。
在main.c中有测试代码,理解了以上函数后就很好理解了,同时也很方便自己改动设置按键功能了,虽然本篇理解有些冗长,不过在写这篇理解的过程中还是很有意思的,尤其对于按位与和按位或的逻辑功能,这辈子是忘不了了…希望本篇对于想用遥控手柄做一些控制的读者能有所帮助,再次希望如果理解有误能有大神指出,万分感谢!