相关定义
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} ;按键值与按键说明。
向手柄发送命令函数
DI与DO是一对同时传输的8 bit串行数据,在时钟下降沿时,完成数据(1bit)的发送和接收,发送和接收是同时完成的。
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);
}
volatile指出 变量是随时可能发生变化的,与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的信号得以接收回来。(时钟频率可为250KHz,如果接收不稳定,可以适当增加频率)
因此接下来又对接收到的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给单片机,意味着已经接收到请求,即将返回数据。再接下来,就是返回各按键以及摇杆的状态数据了。说明中数据意义对照表如下:
当有按键按下,对应为“0”,其他为“1”,例如当键“SELECT”按下时
这一段的意义即为:内层循环结束后即将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]里的,而不是其他元素位置。
判断是哪个按键按下
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。
注意,这个函数只能检测一个按键被按下,若同时按多个按键,则只能检测到键值最小的那个。
摇杆函数
//得到一个摇杆的模拟量 范围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 无效。