本文工程文件以及ps2数据手册在这个链接,我设置成免费了
【免费】STM32PS2解码工程以及代码(CUBEMX+HAL库)资源-CSDN文库
目录
文章来源地址https://www.toymoban.com/news/detail-854771.html
SPI简介
SPI引脚说明
一些参数的含义
通信的四种模式
通信过程简介
关于SPI的常用HAL库函数
PS2简介
ps2手柄
ps2接收器
PS2解码
CUBEMX工程配置
PS2解码
必要的前期工作
原始数据获取
原始数据解码
按键状态获取
摇杆模拟量获取
PS2状态获取
PS2数据清除函数
PS2更新函数
PS2解码实验
工程创建
代码编写
实物连接
调试结果
文章来源:https://www.toymoban.com/news/detail-854771.html
SPI简介
关于SPI在这里就不过多阐述,具体的通信原理可以参考其他博主的博文,这里只提及几个要使用的地方
SPI是串行外设接口(Serial Peripheral Interface)的缩写,是一种高速、全双工、同步通信总线,SPI没有定义速度限制,通常能达到甚至超过10M/bps。
SPI有主、从两种模式,通常由一个主模块和一个或多个从模块组成(SPI不支持多主机),主模块选择一个从模块进行同步通信,从而完成数据的交换。提供时钟的为主设备(Master),接收时钟的设备为从设备(Slave),SPI接口的读写操作,都是由主设备发起,当存在多个从设备时,通过各自的片选信号进行管理。
SPI引脚说明
-
SCLK(Serial Clock):用于提供串行时钟脉冲信号,也通常被写作SCK
-
MISO(Master Input Slave Output):主机输入,从机输出
-
MOSI(Master Output Slave Input):主机输出,从机输入
-
SS(Slave Select):片选,也写成NSS;通常多个有几个从机就有几个NSS,NSS1,NSS2……
一些参数的含义
-
LSB/MSB LSB: 在通信时先发送最低有效位 MSB: 在通信时先发送最高有效位
-
CKP/CPOL(Clock Polarity) 时钟极性,CKP=0时,时钟空闲时为低电平;CKP=1时,时钟空闲时为高电平
-
CKE/CPHA(Clock Phase(Edge)) 时钟相位,CKE=0时,在时钟信号第一个跳变沿采样;CKE=1时,在时钟信号第二个跳变沿采样
通信的四种模式
根据设置的时钟极性和时钟相位,我们可以控制SPI在时钟脉冲的不同位置采样,四种模式如图所示
通信过程简介
主机将片选信号拉低,即NSS拉低,通信开始 CKP和CKE配置为00或者11时,主机和从机在时钟高电平采集信号;配置为01或者10时,在时钟低电平采集信号。 在互补时钟信号跳变沿发送信号 主机将片选信号拉高,即NSS拉高,一次通信结束
值得注意的是:SPI没有固定的从机地址,需要哪个从机时就将对应从机的NSS拉低开始通信,未拉低的从机选择性失聪
关于SPI的常用HAL库函数
-
轮询模式
-
发送函数
-
HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);
-
-
接收函数
-
HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);
-
-
发送接收函数
-
HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, uint32_t Timeout);
-
-
-
中断模式
-
中断发送函数
-
HAL_SPI_Transmit_IT(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);
-
-
中断接收函数
-
HAL_SPI_Receive_IT(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);
-
-
中断发送接收函数
-
HAL_SPI_TransmitReceive_IT(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, uint32_t Timeout);
-
-
-
DMA模式
-
DMA发送函数
-
HAL_SPI_Transmit_DMA(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);
-
-
DMA接收函数
-
HAL_SPI_Receive_DMA(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);
-
-
DMA发送接收函数
-
HAL_SPI_TransmitReceive_DMA(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, uint32_t Timeout);
-
-
-
中断回调函数
-
发送完成回调函数
-
HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi);
-
-
接收完成回调函数
-
HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi);
-
-
发送接收完成回调函数
-
HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi);
-
-
发送过半回调函数
-
HAL_SPI_TxHalfCpltCallback(SPI_HandleTypeDef *hspi);
-
-
接收过半回调函数
-
HAL_SPI_RxHalfCpltCallback(SPI_HandleTypeDef *hspi);
-
-
发送接收过半回调函数
-
HAL_SPI_TxRxHalfCpltCallback(SPI_HandleTypeDef *hspi);
-
-
待会会使用其中一些函数构建PS2驱动文件
PS2简介
ps2手柄
PS2手柄由总共十六个按键和两个摇杆组成
(@图片来自WEEEMAKE WEKI)
不同模式下的ps2按键
当模式指示灯熄灭时,摇杆不返回模拟值,而是触发对应侧的按键,如左摇杆上下左右依次触发十字按键的上下左右,右摇杆的上下左右分别触发YAXB
当模式指示灯亮起时,摇杆返回模拟值
两种模式下其它按键的值都能被正常读取,且按mode可以切换按键
ps2接收器
我们不难看到,手柄通过SPI协议向单片机传输接收到的按键数据
注意,同时打开手柄和给接收器上电,他们会自动配对,且当二者指示灯均不闪烁,为常亮时,表示配对成功
PS2解码
CUBEMX工程配置
关于一些操作前的基础配置我们在这篇博客中有提到,这里就不再叙述了
【STM32】CUBEMX之串口:串口三种模式(轮询模式、中断模式、DMA模式)的配置与使用示例 + 串口重定向 + 使用HAL扩展函数实现不定长数据接收-CSDN博客
先按照这个博客配置好一个串口,不需要打开中断和DMA
配置完成后在connectivity下找到SPI1,这里我们选择Full-Duplex Master,First Bit 选择 LSB,预分频Prescaler选择256,CPOL选择High,CPHA选择1 Edge,片选信号NSS选择软件方式
由于选择的是软件片选,所以我们需要配置一个GPIO用作片选信号,我们直接点击PA4引脚,将其设置为GPIO_Output
接着在system core中点击GPIO,为其设置CSS的用户标签
然后点击Generate Code生成代码,到此CubeMX的配置就结束了
PS2解码
必要的前期工作
根据数据手册,我们首先需要定义一系列按键,将其与返回的数据进行关联
#define PSS_Rx 0
#define PSS_Ry 1
#define PSS_Lx 2
#define PSS_Ly 3
#define PSB_Left 0
#define PSB_Down 1
#define PSB_Right 2
#define PSB_Up 3
#define PSB_Start 4
#define PSB_RightRocker 5
#define PSB_LeftRocker 6
#define PSB_Select 7
#define PSB_Square 8
#define PSB_Cross 9
#define PSB_Circle 10
#define PSB_Triangle 11
#define PSB_X 8
#define PSB_A 9
#define PSB_B 10
#define PSB_Y 11
#define PSB_R1 12
#define PSB_L1 13
#define PSB_R2 14
#define PSB_L2 15
查阅手册得知,我们发送的命令共有3条,一次通信总共返回的原始数据共9字节,两个摇杆共四个方向,所以创建一个大小为4的数组用于储存模拟值,按钮共16个,所以再创建一个大小为16的数组储存每个按键的状态,此外,还需创建变量i用于循环语句,mode用在全局表示PS2的实时状态,这段代码如下
uint8_t CMD[3] = { 0x01,0x42,0x00 }; // 请求接受数据
uint8_t PS2OriginalValue[9] = { 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 }; //存储手柄返回数据
uint8_t RockerValue[4] = { 0x00,0x00,0x00,0x00 }; //摇杆模拟值
uint8_t ButtonValue[16] = { 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 }; //所有按键状态值
uint8_t i,mode;
由于通信速度十分快,所以需要使用HAL库定义一个微秒级的延时,用于通信过程中的必要的等待
延时函数如下
void Delay_us(uint32_t udelay) //定义hal库us级延迟
{
uint32_t startval, tickn, delays, wait;
startval = SysTick->VAL;
tickn = HAL_GetTick();
delays = udelay * 72;
if (delays > startval)
{
while (HAL_GetTick() == tickn)
{
}
wait = 72000 + startval - delays;
while (wait < SysTick->VAL)
{
}
}
else
{
wait = startval - delays;
while (wait < SysTick->VAL && HAL_GetTick() == tickn)
{
}
}
}
原始数据获取
接下来就是启动SPI通信从接受其中获得9字节的原始数据
具体流程如下:
- 拉低片选CSS,代表选中设备可以开始通信
- 根据数据手册,首先需要向接收器发送0x01请求接收数据
- 接下来需要向接收器发送0x42,请求开始通信,接收到0x01表示通信正式开始
- 然后发送0x00,可以接收到PS2ID,此ID可以代表PS2手柄目前的模式,如果为0x73,则为红灯模式,如果为0x41,则为绿灯模式,此状态下摇杆返回模拟值
- 接下来持续发送0x00,可以接收到按键信息以及摇杆信息
- 完成后将片选拉高
采用HAL_SPI_TransmitReceive(&hspi1, &CMD[1], &PS2OriginalValue[1], 1, HAL_MAX_DELAY);函数,可以同时进行收发,其中第一个参数是spi句柄,第二个参数是要发送信息的地址,第三个参数是接收到的信息存储的地址,第四个参数是发送和接收的数据大小(注意:只有一个数据大小的参数说明在调用该函数时只能同时接受和发送相同大小的数据),最后一个参数时超时时长,这里设置为无限等待即可
原始数据接收函数如下
void PS2OriginalValueGet(void)
{
short i = 0;
HAL_GPIO_WritePin(CSS_GPIO_Port, CSS_Pin, GPIO_PIN_RESET);
HAL_SPI_TransmitReceive(&hspi1, &CMD[0], &PS2OriginalValue[0], 1, HAL_MAX_DELAY);
Delay_us(10);
HAL_SPI_TransmitReceive(&hspi1, &CMD[1], &PS2OriginalValue[1], 1, HAL_MAX_DELAY);
Delay_us(10);
HAL_SPI_TransmitReceive(&hspi1, &CMD[2], &PS2OriginalValue[2], 1, HAL_MAX_DELAY);
Delay_us(10);
for (i = 3; i < 9; i++)
{
HAL_SPI_TransmitReceive(&hspi1, &CMD[2], &PS2OriginalValue[i], 1, HAL_MAX_DELAY);
Delay_us(10);
}
HAL_GPIO_WritePin(CSS_GPIO_Port, CSS_Pin, GPIO_PIN_SET);
}
原始数据解码
按键状态获取
根据数据手册,最后在while中接收到的前两字节共16位数据对应16按键的状态,后四字节数据分别是四个摇杆模拟值的八位模拟量
所以我们需要对PS2OriginalValue[3]与PS2OriginalValue[4]进行逐位判断并存储进对应的ButtonValue中
注意:由于对应的按键按下时,该位为0,为方便判断,需要将所有按键值取反
该函数具体编写步骤可简化如下
- 使用循环逐字节读取PS2OriginalValue[3]中的状态
- 使用循环逐字节读取PS2OriginalValue[4]中的状态
- 对所有按键状态取反
该函数具体代码编写可以是如下
void ButtonValueGet(void)
{
uint8_t bit = 1;
uint8_t button = 0;
for (bit = 8; bit > 0; bit--)
{
bit -= 1;
ButtonValue[button] = (PS2OriginalValue[3] & (1 << bit)) >> bit;
bit += 1;
button++;
}
for (bit = 8; bit > 0; bit--)
{
bit -= 1;
ButtonValue[button] = (PS2OriginalValue[4] & (1 << bit)) >> bit;
bit += 1;
button++;
}
for (button = 0; button < 16; button++)
{
if (ButtonValue[button] == 1) ButtonValue[button] = 0;
else ButtonValue[button] = 1;
}
}
摇杆模拟量获取
摇杆模拟量直接将PS2OriginalValue后四位注意对应着写入存储数组即可
该函数代码编写如下
void RockerValueGet(void)
{
int i;
for (i = 5; i < 9; i++)
{
PS2OriginalValue[i] = (int)PS2OriginalValue[i];
RockerValue[i - 5] = PS2OriginalValue[i];
}
}
PS2状态获取
该函数也在只需要对之前定义的mode进行判断即可
该函数代码编写如下,这里采取判断是否为红灯模式,若是红灯,返回1,不是则返回0
int PS2RedLight(void)
{
if (mode == 0X73)
return 1;
else
return 0;
}
PS2数据清除函数
在完成解码后,需要将所有的原始数据进行清零,保证下一次获取原始数据的准确性
直接对PS2OriginalValue清零即可,参考代码如下
void PS2OriginalValueClear(void)
{
for (i = 0; i < 9; i++)
{
else PS2OriginalValue[i] = 0x00;
}
}
PS2更新函数
最后为完成解码,需要对以上函数进行合理调用,并且该函数使得PS2数据更新的调用简洁
逻辑可以为
- 获取原始数据
- 更新摇杆模拟量
- 更新按键状态
- 更新模式
- 清除原始数据
代码如下
void PS2AllValueUpdate(void)
{
PS2OriginalValueGet();
RockerValueGet();
ButtonValueGet();
mode = PS2OriginalValue[1];
PS2OriginalValueClear();
}
至此,就完成了PS2的通信以及解码过程
PS2解码实验
工程创建
接在cubemx中generate code之后,我们打开创建的工程,将ps2.c和ps2.h分别粘贴在工程文件中Core-Src和Core-Inc文件夹下,.c和.h文件都在开头处给出的资源里
然后打开keil,点击品字图形的图标,找到后选定Core文件夹,在右下方点击Add Files添加文件
找到刚刚存放ps2.c的文件夹,选中ps2.c,将其添加进工程中
完成后点击ok保存,这时我们可以看到在旁边的工程文件夹中看到了ps2.c文件,就说明添加完成了
代码编写
在魔术棒中打开microlib
引用相关的头文件以及ps2.c中定义的数组
编写串口重定向函数,对串口重定向不清楚的可以看看上面的博客链接
编写主函数,由于函数比较简单,且下载文件中也有,这里就不单独写出来了
实物连接
ps2接收器
- PA4->CS
- PA5->CLK
- PA6->DAT
- PA7->CMD
调试结果
编译下载并打开串口调试助手,按动手柄上的按键,摇动摇杆,出现如下结果说明解码成功
其中1表示被按下的按键,后面四个则是摇杆模拟值
ps2手柄解码到此就结束了,欢迎大家讨论
到了这里,关于【STM32】SPI与PS2手柄解码(CUBEMX+HAL库)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!