写在最前
关于这篇博客,我考虑了很久到底要不要写,就像考虑要不要写IIC通讯协议的时候一样,IIC通讯协议比较的复杂,并且就算我写出来了,那么我是否把IIC通讯协议的内容表述清楚,读者是否理解了,这也是不可忽视的问题。但是学习嵌入式的话,我个人觉得最重要的不是你知道怎样配置GPIO,TIM,USART这些基本的外设,而是要有自己的思维和逻辑,遇到问题能够靠自己的逻辑思维去解决它。就像我们学习本篇的通讯协议一样,当学习完这种通讯协议之后,以后遇到其他的通讯协议都能够自己理解并且能用c语言编写出来。
导读
对于我们学习嵌入式的人来说,有关通讯协议的知识是必须要掌握的。
从我们最早接触的串口来说,它的通讯协议我们非常熟悉,只需要配置好波特率、有效数据位、奇偶校验位、停止位等等,然后下载到开发板中,我们就可以通过数据线与电脑进行通信(需要通过串口调试助手来对电脑的虚拟串口进行配置)。
再到IIC通讯协议,我们知道了根据时序图来写出开始函数、停止函数、发送和读取一个字节的函数等等,通过IIC通讯协议我们可以读取许多传感器的数据,例如MPU6050,温湿度传感器SHT30,以及其他具有IIC接口的模块,我们都可以通过IIC通讯协议来进行通信。
最后到我们今天说讲的PS2的通讯协议,它可能没有串口以及IIC通讯协议那么常见,但是在我个人看来,它作为给新手来入门有关通讯协议的知识是一种非常好的选择。因为它的通讯协议相对IIC来说简单一点,对串口来说难一点,我们使用串口与电脑通信时,只需要简单的配置好串口的库函数就行了,不能学习到有关通讯时序的知识。
PS2通讯协议的原理分析
请看下面的时序图,这是PS2的出厂资料里面仅有的一段有关时序的介绍图。如果你通过这张时序图能够把PS2手柄通讯协议的代码独立的写出来,那么恭喜你,关于时序图的知识基本上已经入门了。如果不能写出它的通讯协议的代码也没事,且听我慢慢分析。
我们先不看时序图下面的文字,只看这个时序图,根据这个时序图来分析。
第一点:CS在数据输出或者输入的时候,都是低电平的,那么我们在数据传输的时候先把CS拉高再拉低,然后数据进行传输,传输完成之后再把CS拉高。
第二点:DI(Data Input)与DO(Data Output)是同时完成的,说明这是全双工通信。串口与IIC是什么呢?串口有TX和RX,可以同时发送与接收,所以是全双工通信。IIC只有SDA与SCL两条线,SCL是时钟线,用来传输数据的只有SDA这一条线,只能发送数据,或者接收数据,不能再发送数据的同时接收数据,所以是半双工通信。
第三点:在时钟上降沿的时候,DI和DO的数据有交叉,也就是说数据进行交换(数据只有0和1),这个时候我们是不能够读和写数据的,因为数据还不稳定,我们读到的数据不准确。在时钟为下降沿的时候,数据已经稳定了,我们在这个时候开始读和写数据。
第四点:由于是从0到7,可以知道有8位数据,并且是从低位到高位进行读写。我们可以把数据放到数组中。一个时钟进行一个数据位(也可以叫做比特位0或1)传输。
至此,PS2无线遥控器的基本通讯已经讲解完了,那么实际发送数据和读取数据有什么要求呢?我们再来看时序图下面的文字。时钟频率为250KHz,单片机先发出一个命令“0x01”,然后PS2无线手柄会回复它自身的ID。单片机发送0x42,手柄回复0x5A,告诉单片机“数据来了”。
当成功建立了通信之后,再就是当我们按下手柄上的按钮,单片机会接收到什么数据?这个可以就需要看PS2的数据意义对照表了(idle:数据线空闲,该数据线无数据传送)。把DI收到的数据放到数组Data中。所以数组Data[0]、Data[1]、Data[2]是不能用来存放PS2遥控器的按键值的,只有Data[3]、Data[4]能够存放遥控器的按键值。当有按键按下,对应位为“0”,其他位为“1”,例如当键“SELECT”被按下时,Data[3]=11111110B。当键“L2”被按下时,Data[4]=11111110B。
数据意义对照表
PS2无线遥控手柄的代码分析
main.c文件
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "ps2.h"
int main(void)
{
u8 key=0;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
delay_init();
uart_init(115200);
PS2_Init();
while(1)
{
key=PS2_DataKey();
if(key!=0) //有按键按下
{
printf(" rn %d is pressed rn",key);
}
delay_ms(50);
}
}
ps2.c文件
**#include "sys.h"
#include "delay.h"
#include "ps2.h"
u16 Handkey;
u8 Data[9]={0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; //数据存储缓冲区
//按键值与按键明
u16 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
};
/**
* @brief PS2_GPIO初始化
* @parm None
* @retval None
*/
void PS2_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE ); //使能GPIOB时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD ; //下拉输入
// DO->PB13 CS->PB14 CLK->PB15
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP ; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
/**
* @brief 单片机向PS2写命令
* @parm cmd
* @retval None
*/
void PS2_Cmd(u8 cmd)
{
u16 i;
for(i=0x01;i<0x100;i<<=1)//8次循环
{
PS2_CLK=1;//发送高电平,告诉PS2接收器我要准备数据了
if(i&cmd)
{
PS2_CMD=1;
}
else PS2_CMD=0;
delay_us(10);//我要准备数据了的时间,如果没有这个的话就会在一个机器周期后进入低电平,接收器反应不过来。
PS2_CLK=0;//告诉接收器,我数据准备好了,你可以读取了。
delay_us(20);
}
PS2_CLK=1;//时钟拉高,不工作
}
/**
* @brief 单片机对PS2读数据
* @parm None
* @retval None
*/
void PS2_Read(void)
{
volatile u8 byte;//必须要用volatile关键词来定义。关键词的作用请自行百度。
u16 i;
PS2_CS=0; //CS拉低
PS2_Cmd(0x01); //开始命令
PS2_Cmd(0x42); //请求数据
for(byte=2;byte<9;byte++)
{
for(i=0x01;i<0x100;i<<=1)
{
PS2_CLK=1; //单片机发送高电平,告诉接收器要准备数据了
delay_us(50); //接收器准备数据的时间
PS2_CLK=0; //发送低电平,告诉接收器 单片机要开始读数据了
if(PS2_DAT)
Data[byte] = i| Data[byte];
}
}
PS2_CS=1; //CS拉高
}
/**
* @brief 用来读出按键值的函数
* @parm None
* @retval 成功则返回index+1,失败则返回0。
*/
u8 PS2_DataKey(void)
{
u8 index;
PS2_DataClear(); //清空数组
PS2_Read(); //单片机读接收器的数据
Handkey=(Data[4]<<8)|Data[3];//根据数据意义对照表,定义一个16位的变量。
for(index=0;index<16;index++)//当我们按下遥控器的按键时,数据会传到Data[3]或者Data[4]来。我这里进行16次for循环,用来判断哪个按键按下了。
{//例如:当按下了SELECT按键,Data[3]=11111110B。Handkey=1111 1111 1111 1110B。
if((Handkey&(1<<(MASK[index]-1)))==0) //当第一次进入循环,Handkey&(1<<(MASK[0]-1)))
return index+1 //-->Handkey&(1<<0)--->1111 1111 1111 1110B & 0000 0000 0000 0001=0
}
return 0;
}
/**
* @brief 数组清空函数
* @parm None
* @retval None
*/
void PS2_DataClear(void)
{
u8 i;
for(i=0;i<9;i++)
{
Data
=0x00;
}
}
ps2.h文件
#ifndef __PSTWO_H
#define __PSTWO_H
#include "delay.h"
#include "sys.h"
//IO操作函数
#define PS2_DAT PBin(12) //DATA
#define PS2_CMD PBout(13) //CMD
#define PS2_CS PBout(14)//CS
#define PS2_CLK PBout(15)//CLK
//These are our button constants
#define PSB_SELECT 1
#define PSB_L3 2
#define PSB_R3 3
#define PSB_START 4
#define PSB_PAD_UP 5
#define PSB_PAD_RIGHT 6
#define PSB_PAD_DOWN 7
#define PSB_PAD_LEFT 8
#define PSB_L2 9
#define PSB_R2 10
#define PSB_L1 11
#define PSB_R1 12
#define PSB_GREEN 13
#define PSB_RED 14
#define PSB_BLUE 15
#define PSB_PINK 16
#define PSB_TRIANGLE 13
#define PSB_CIRCLE 14
#define PSB_CROSS 15
#define PSB_SQUARE 26
void PS2_Init(void);
void PS2_Cmd(u8 cmd);
void PS2_Read(void);
u8 PS2_DataKey(void);
void PS2_DataClear(void);
#endif
打印按键值
写在最前
关于这篇博客,我考虑了很久到底要不要写,就像考虑要不要写IIC通讯协议的时候一样,IIC通讯协议比较的复杂,并且就算我写出来了,那么我是否把IIC通讯协议的内容表述清楚,读者是否理解了,这也是不可忽视的问题。但是学习嵌入式的话,我个人觉得最重要的不是你知道怎样配置GPIO,TIM,USART这些基本的外设,而是要有自己的思维和逻辑,遇到问题能够靠自己的逻辑思维去解决它。就像我们学习本篇的通讯协议一样,当学习完这种通讯协议之后,以后遇到其他的通讯协议都能够自己理解并且能用c语言编写出来。
导读
对于我们学习嵌入式的人来说,有关通讯协议的知识是必须要掌握的。
从我们最早接触的串口来说,它的通讯协议我们非常熟悉,只需要配置好波特率、有效数据位、奇偶校验位、停止位等等,然后下载到开发板中,我们就可以通过数据线与电脑进行通信(需要通过串口调试助手来对电脑的虚拟串口进行配置)。
再到IIC通讯协议,我们知道了根据时序图来写出开始函数、停止函数、发送和读取一个字节的函数等等,通过IIC通讯协议我们可以读取许多传感器的数据,例如MPU6050,温湿度传感器SHT30,以及其他具有IIC接口的模块,我们都可以通过IIC通讯协议来进行通信。
最后到我们今天说讲的PS2的通讯协议,它可能没有串口以及IIC通讯协议那么常见,但是在我个人看来,它作为给新手来入门有关通讯协议的知识是一种非常好的选择。因为它的通讯协议相对IIC来说简单一点,对串口来说难一点,我们使用串口与电脑通信时,只需要简单的配置好串口的库函数就行了,不能学习到有关通讯时序的知识。
PS2通讯协议的原理分析
请看下面的时序图,这是PS2的出厂资料里面仅有的一段有关时序的介绍图。如果你通过这张时序图能够把PS2手柄通讯协议的代码独立的写出来,那么恭喜你,关于时序图的知识基本上已经入门了。如果不能写出它的通讯协议的代码也没事,且听我慢慢分析。
我们先不看时序图下面的文字,只看这个时序图,根据这个时序图来分析。
第一点:CS在数据输出或者输入的时候,都是低电平的,那么我们在数据传输的时候先把CS拉高再拉低,然后数据进行传输,传输完成之后再把CS拉高。
第二点:DI(Data Input)与DO(Data Output)是同时完成的,说明这是全双工通信。串口与IIC是什么呢?串口有TX和RX,可以同时发送与接收,所以是全双工通信。IIC只有SDA与SCL两条线,SCL是时钟线,用来传输数据的只有SDA这一条线,只能发送数据,或者接收数据,不能再发送数据的同时接收数据,所以是半双工通信。
第三点:在时钟上降沿的时候,DI和DO的数据有交叉,也就是说数据进行交换(数据只有0和1),这个时候我们是不能够读和写数据的,因为数据还不稳定,我们读到的数据不准确。在时钟为下降沿的时候,数据已经稳定了,我们在这个时候开始读和写数据。
第四点:由于是从0到7,可以知道有8位数据,并且是从低位到高位进行读写。我们可以把数据放到数组中。一个时钟进行一个数据位(也可以叫做比特位0或1)传输。
至此,PS2无线遥控器的基本通讯已经讲解完了,那么实际发送数据和读取数据有什么要求呢?我们再来看时序图下面的文字。时钟频率为250KHz,单片机先发出一个命令“0x01”,然后PS2无线手柄会回复它自身的ID。单片机发送0x42,手柄回复0x5A,告诉单片机“数据来了”。
当成功建立了通信之后,再就是当我们按下手柄上的按钮,单片机会接收到什么数据?这个可以就需要看PS2的数据意义对照表了(idle:数据线空闲,该数据线无数据传送)。把DI收到的数据放到数组Data中。所以数组Data[0]、Data[1]、Data[2]是不能用来存放PS2遥控器的按键值的,只有Data[3]、Data[4]能够存放遥控器的按键值。当有按键按下,对应位为“0”,其他位为“1”,例如当键“SELECT”被按下时,Data[3]=11111110B。当键“L2”被按下时,Data[4]=11111110B。
数据意义对照表
PS2无线遥控手柄的代码分析
main.c文件
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "ps2.h"
int main(void)
{
u8 key=0;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
delay_init();
uart_init(115200);
PS2_Init();
while(1)
{
key=PS2_DataKey();
if(key!=0) //有按键按下
{
printf(" rn %d is pressed rn",key);
}
delay_ms(50);
}
}
ps2.c文件
**#include "sys.h"
#include "delay.h"
#include "ps2.h"
u16 Handkey;
u8 Data[9]={0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; //数据存储缓冲区
//按键值与按键明
u16 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
};
/**
* @brief PS2_GPIO初始化
* @parm None
* @retval None
*/
void PS2_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE ); //使能GPIOB时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD ; //下拉输入
// DO->PB13 CS->PB14 CLK->PB15
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP ; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
/**
* @brief 单片机向PS2写命令
* @parm cmd
* @retval None
*/
void PS2_Cmd(u8 cmd)
{
u16 i;
for(i=0x01;i<0x100;i<<=1)//8次循环
{
PS2_CLK=1;//发送高电平,告诉PS2接收器我要准备数据了
if(i&cmd)
{
PS2_CMD=1;
}
else PS2_CMD=0;
delay_us(10);//我要准备数据了的时间,如果没有这个的话就会在一个机器周期后进入低电平,接收器反应不过来。
PS2_CLK=0;//告诉接收器,我数据准备好了,你可以读取了。
delay_us(20);
}
PS2_CLK=1;//时钟拉高,不工作
}
/**
* @brief 单片机对PS2读数据
* @parm None
* @retval None
*/
void PS2_Read(void)
{
volatile u8 byte;//必须要用volatile关键词来定义。关键词的作用请自行百度。
u16 i;
PS2_CS=0; //CS拉低
PS2_Cmd(0x01); //开始命令
PS2_Cmd(0x42); //请求数据
for(byte=2;byte<9;byte++)
{
for(i=0x01;i<0x100;i<<=1)
{
PS2_CLK=1; //单片机发送高电平,告诉接收器要准备数据了
delay_us(50); //接收器准备数据的时间
PS2_CLK=0; //发送低电平,告诉接收器 单片机要开始读数据了
if(PS2_DAT)
Data[byte] = i| Data[byte];
}
}
PS2_CS=1; //CS拉高
}
/**
* @brief 用来读出按键值的函数
* @parm None
* @retval 成功则返回index+1,失败则返回0。
*/
u8 PS2_DataKey(void)
{
u8 index;
PS2_DataClear(); //清空数组
PS2_Read(); //单片机读接收器的数据
Handkey=(Data[4]<<8)|Data[3];//根据数据意义对照表,定义一个16位的变量。
for(index=0;index<16;index++)//当我们按下遥控器的按键时,数据会传到Data[3]或者Data[4]来。我这里进行16次for循环,用来判断哪个按键按下了。
{//例如:当按下了SELECT按键,Data[3]=11111110B。Handkey=1111 1111 1111 1110B。
if((Handkey&(1<<(MASK[index]-1)))==0) //当第一次进入循环,Handkey&(1<<(MASK[0]-1)))
return index+1 //-->Handkey&(1<<0)--->1111 1111 1111 1110B & 0000 0000 0000 0001=0
}
return 0;
}
/**
* @brief 数组清空函数
* @parm None
* @retval None
*/
void PS2_DataClear(void)
{
u8 i;
for(i=0;i<9;i++)
{
Data
=0x00;
}
}
ps2.h文件
#ifndef __PSTWO_H
#define __PSTWO_H
#include "delay.h"
#include "sys.h"
//IO操作函数
#define PS2_DAT PBin(12) //DATA
#define PS2_CMD PBout(13) //CMD
#define PS2_CS PBout(14)//CS
#define PS2_CLK PBout(15)//CLK
//These are our button constants
#define PSB_SELECT 1
#define PSB_L3 2
#define PSB_R3 3
#define PSB_START 4
#define PSB_PAD_UP 5
#define PSB_PAD_RIGHT 6
#define PSB_PAD_DOWN 7
#define PSB_PAD_LEFT 8
#define PSB_L2 9
#define PSB_R2 10
#define PSB_L1 11
#define PSB_R1 12
#define PSB_GREEN 13
#define PSB_RED 14
#define PSB_BLUE 15
#define PSB_PINK 16
#define PSB_TRIANGLE 13
#define PSB_CIRCLE 14
#define PSB_CROSS 15
#define PSB_SQUARE 26
void PS2_Init(void);
void PS2_Cmd(u8 cmd);
void PS2_Read(void);
u8 PS2_DataKey(void);
void PS2_DataClear(void);
#endif
打印按键值
举报