背景:昨天,心血来潮想使用单片机的SPI外设读取ps2手柄,但是发现的数据根本不对。
(型号:STM32F411CEU6)
结论:
1、PS2手柄是SPI全双工通信。
2、通信速度<=100khz。
3、时钟空闲时是高电平,下降沿读取。
4、数据格式LSB,小端发送。
5、如果将SPI外设时钟频率降低至100kHZ或者以下,则可以正常读出数据,但是需要降低外设上面的的总线频率,那么在这条总线上的其他外设频率也会跟着降低。而应用IO口模拟与使用SPI外设所占用的IO口数量相同,也不会影响其他外设的时钟频率,所以,综合分析,使用IO口模拟SPI读取PS2更好一点。
过程:使用网上IO口模拟的程序,发现可以正常运行,于是示波器分别测量SPI外设和IO模拟的波形,发现是SPI外设的时钟频率太快了,导致出现问题。将IO模拟的时钟延时时间改为4us的时候,数据是不准确的,将时钟延时改为1us的时候,数据读出的都是0xff。
下面这个是用IO口模拟的程序,测量其时钟频率约100khz。时钟延时10us。
//读数据、写命令
u8 PS2_Write_Read(u8 dat)
{ //数据返回值temp
u8 temp = 0;
//循环计数值i
u8 i = 0;
//命令值临时存储wt_temp
u8 wt_temp = 0;
wt_temp = dat;
for(i=0;i<8;i++)
{
PS2_SCK_H();//时钟高
delay_us(10);//时钟延时
if(wt_temp&0x01)//判断命令从0bit开始,循环8次。决定CMD电平。
{
PS2_CMD_H();
}
else
{
PS2_CMD_L();
}
PS2_SCK_L();//时钟低
delay_us(10);//时钟延时
wt_temp>>=1;
//数据第一个是第0bit,因此需要把后接收到的数据移动到第7bit。
temp |= GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_0)<<i;
}
return temp;//返回数据的值
}
波形:
可以正常读出数据。
将其改成延时1us。
//读数据、写命令
u8 PS2_Write_Read(u8 dat)
{ //数据返回值temp
u8 temp = 0;
//循环计数值i
u8 i = 0;
//命令值临时存储wt_temp
u8 wt_temp = 0;
wt_temp = dat;
for(i=0;i<8;i++)
{
PS2_SCK_H();//时钟高
delay_us(1);//时钟延时
if(wt_temp&0x01)//判断命令从0bit开始,循环8次。决定CMD电平。
{
PS2_CMD_H();
}
else
{
PS2_CMD_L();
}
PS2_SCK_L();//时钟低
delay_us(1);//时钟延时
wt_temp>>=1;
//数据第一个是第0bit,因此需要把后接收到的数据移动到第7bit。
temp |= GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_0)<<i;
}
return temp;//返回数据的值
}
读出的数据就是不正常的了。
此时使用单片机SPI外设读取。配置如下:
#include "main.h"
#include "misc.h"
#include "user_spi.h"
//配置初始化
void USER_SPI1_Init(void)
{
RCC->AHB1ENR |=0X01;//开启GPIOA时钟
RCC->APB2ENR |=1<<12;//开启外设时钟
//配置GPIOA
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_5|GPIO_Pin_6|GPIO_Pin_7;
GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_Init(GPIOA,&GPIO_InitStruct);
//CSpin
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_OUT;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_4;
GPIO_Init(GPIOA,&GPIO_InitStruct);
GPIO_PinAFConfig(GPIOA,GPIO_PinSource7,GPIO_AF_SPI1);
GPIO_PinAFConfig(GPIOA,GPIO_PinSource5,GPIO_AF_SPI1);
GPIO_PinAFConfig(GPIOA,GPIO_PinSource6,GPIO_AF_SPI1);
//APB2 外设SPI1,系统时钟100MHZ
SPI1 ->CR1 |= 1<<2; //配置为主设备
SPI1 ->CR1 |= 1<<1; //配置空闲时钟为高电平
SPI1 ->CR1 &= ~(0x01); //配置为下降沿采样
SPI1 ->CR1 |= 0X07<<3; //波特率256分频 390khz
SPI1 ->CR1 |= 0X01<<7; //LSB
SPI1 ->CR1 |= 0X01<<8; //使用软件NSS管理。用来代替外部io口输入电平
SPI1 ->CR1 |= 0X01<<9; //NSS为1,是设定SPI1为主设备的前提条件。如果不置位,则SPI1设备为从设备。
SPI1 ->CR1 |= 1<<6; //使能SPI
}
//中断初始化
void USER_SPI1_TIM_Init(void)
{
NVIC_InitTypeDef NVIC_InitStruct = {0};
NVIC_InitStruct.NVIC_IRQChannel = SPI1_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1; //0~1
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 7; //0~7
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
}
//发送接收函数
u8 USER_SPI1_TrsRec(u8 dat)
{
u8 temp = 0;
u16 time_out = 0;
while(!(SPI1->SR &1<<1))//等待上一次发送完成,1发送完成,0未完成
{
time_out++;
if(time_out >2400)//100us
{ break;}
}
time_out = 0;
SPI1->DR = dat; //发送数据
while(!(SPI1->SR &0x01))//等待收到有效数据,1有数据,0无数据
{
time_out++;
if(time_out >2400)//100us
{ break;}
}
temp = SPI1->DR; //读出数据
return temp;
}
读出的数据也是不正常的,如果想要正常读出数据,其时钟频率尽量不得大于100khz。`