STM32嵌入式基础开发04-PS2手柄SPI通讯数据输出(4_SPI)

1 博客内容

      博客内容基于STM32F103 RET6芯片,介绍SPI通讯读取数据,细节方面理解不够尽善尽美,记录信息方便追溯回顾。程序使用结果使用电压表测试,相关参考资料方面,除了官方文件《STM32中文参考手册_V10x》和 B站:刘火良老师野火STM32F103教学视频 ,相关资料信息如下:
STM32_SPI

2 SPI通讯原理

      这里借用B站视频中刘老师的图,图比较经典。理解1个字节(8位)传递过程。
在这里插入图片描述
      传递前:
            (1)初始化片选信号CS=1;
            (2)初始化CPOL=1和CPHA=1。
      传递过程:
            (1)初始化片选信号拉低,即CS=0。
            (2)依据初始化CPOL=1表示SCK初始电平为高电平, CPHA=1CPHA=1确定偶数边缘采样。通过SCK电平 1→到0(第1个边缘)→到1(第2个边缘),偶数2边缘时采样。
            (3)图示16个时钟边缘(8个时钟),传递8个位数据。传递完成后,片选信号CS拉高,时钟信号拉高。
            (4)在SCK时钟电平的作用下,同步从设备(手柄)和主设备(STM32芯片)接收缓冲器的数据,PS手柄连续发送8个字节(bit)数据,按这个方法重复8次。

      引用平衡小车之家代码,举个收发数据的例子:收发数据方式好似步枪,一发一发打(来自从设备-手柄),靶心一发一发收(MISO-主设备STM32芯片)。一个人(SCK时钟)扣动64次扳机,打64发,用8个靶心。

//读取手柄数据
void PS2_ReadData(void)
{
	volatile u8 byte=0;
	volatile u16 ref=0x01;
	CS_L;
	PS2_Cmd(Comd[0]);  //开始命令
	PS2_Cmd(Comd[1]);  //请求数据
	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;
}

      引用技新小车代码,方式好似机枪,扣一次扳机(动作一次),靶心一次收。案例不太恰当,但是原理如此。

/ SPI读写一个字节
// TxData:要写入的字节
// 返回值:读取到的字节
//-----------------------------------------------------------------------
u8 SPI2_ReadWriteByte(u8 TxData)
{		
	u8 TxWait = 0;
	u8 RxWait = 0;
	
	// 等待发送缓存为空
	while(SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET)
	{
		TxWait++;
		if(TxWait>250)	// 等待时间过长则放弃本次读写
		return 0;
	}	

	SPI_I2S_SendData(SPI2, TxData); // SPI2写一个字节
	
	// 等待接收缓存为空
	while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) == RESET)
	{
		RxWait++;
		if(RxWait>250)	// 等待时间过长则放弃本次读写
		return 0;
	}	  	
	
	return SPI_I2S_ReceiveData(SPI2); // 将读到的字节返回					    
}
//-----------------------------------------------------------------------

uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx)
{
  /* Check the parameters */
  assert_param(IS_SPI_ALL_PERIPH(SPIx));
  
  /* Return the data in the DR register */
  return SPIx->DR;
}

      按照ST的文件,接收缓冲器存放8位同步数据,通过操作SPI1->DR,Return即可,技新的程序效率更高。
在这里插入图片描述
在这里插入图片描述
      有关技新程序数据读取原理,可以了解“ 作者未知:STM32 SPI使用知识 ” 这篇文章。

3 PS2与STM32通讯原理

      芯片SPI接口定义如下:

引脚信号名称功能
PC2PS2_CS片选信号,低电平有效
PA5PS2_SCK时钟信号
PA6PS2_MISO主机输入模式
/PA7PS2_MOSI主机輸出模式

在这里插入图片描述
      手柄按键定义如下:
在这里插入图片描述

      主机输出(DO)和主机输入(DI)信息:

序号DODI字节0到7内容
00x01Null
10x42data0x41:数字/0x73:红灯
2Nulldata[2]0x5A
3Nulldata[3]SELECT、摇杆左按下、摇杆右按下、STRAT、按键左_前/右/下/左(4个位)
4Nulldata[4]辅助左2、辅助右2、辅助左1、辅助右1、按键右_前/右/后/左(4个位)
5Nulldata[6]摇杆右_左右(0:左;255:右)
6Nulldata[7]摇杆右_前后(0:前;255:后)
7Nulldata[8]摇杆左_左右(0:左;255:右)
8Nulldata[9]摇杆左_前后(0:前;255:后)

      手柄与芯片通讯过程简化理解:

  • 序号0:STM32发0x01,启动通讯,SPI通讯需要CS(片选)信号为低位时通讯有效;

  • 序号 1:STM32发0x42请求手柄发送数据,手柄收到后,手柄发状态(0x41:数字,0x73:红灯)信息告知发送模式,(备注*度搜索:红灯亮的时候表示手柄工作为模拟量模式,除了 Select Start 其他的按钮都是模拟按键,即可按出轻重效果 也就是256段力度感应.当然摇杆也是模拟输入控制器。红灯灭的时候表示手柄工作为数字模式,按钮没有轻重之分);

  • 序号2:手柄发状态(0x42)信息告知开始发送数据。接下来3到8发送的的是Data;

  • 序号3-4:每个data[i] 8个字节(0或1),对应8个按钮。按钮默认是1(即Data[3]=11111111),摇杆左按下(data[3]从左边开始第2位),此时data[3]=1011111。2进制转换10进制为95;按下辅助右1,data[4]=11101111,2进制转换10进制239;

  • 序号5-8:一共4个data[i],一个data[i]由2进制8位构成(00000000-11111111,十进制为0-255),即每个轴向范围0-255,0为最左或者最上,255为最右或者最下。所以可以看到 博主chhttty 在博客中 simulink模型输入0->128->255;模型输出四个电机的参数是1000->0->-1000。本次嵌入式开发中用到只有2个摇杆(即data[5]到 data[8])和data[1](即手柄工作为模拟量模式,Simulink程序使用Mode按键使能输入)。

4 主程序(Main.c)

      参考平衡小车之家,通过是IO口模拟PS2的ISPI通信,获取手柄左右遥感X/Y数据。


//================================================
//    名称:  Main.c
//    作者:  Morven_X
//    版本:  1.1
//    编制:  2021/01/16 1:40
//    更新:  2021/01/30 22:45
//    功能:  基于STM32F103 RET6芯片,使用I/0口模拟SPI通信获取手柄信息
//    简介:  更新内容删除SPI通讯结构体_参考计新小车部分(Keil 5.28)
//    Email:  morven_xie@163.com
//================================================

# include "stm32f10x.h"
# include "LED1.h"
# include "Delay.h"
# include "PWM.h"
# include "SPI.h"

/**************************************************************************

参考作者:平衡小车之家
手柄接口初始化    输入  DI->PA0   输出  DO->PA1   CS->PA2  CLK->PA3
串口1以波特率9600输出接收到的数据

**************************************************************************/ 
 

int PS2_LX,PS2_LY,PS2_RX,PS2_RY,PS2_KEY;
int main(void)
{ 
	JTAG_ENable(1);
	Stm32_Clock_Init(9);            //=====系统时钟设置
	Delay_Init();                   //=====延时初始化
	LED_Init();                     //===== LED 连接的硬件接口
	uart_init(72,9600);           	//=====串口1初始化
	Delay_ms(1000);                 //=====延时等待初始化稳定
	PS2_Init();						//=====ps2驱动端口初始化
	PS2_SetInit();		 			//=====ps2配置初始化,配置“红绿灯模式”,并选择是否可以修改
	GPIO_SetBits(GPIOC, GPIO_Pin_3);//设置LED高电平输出
	Delay_ms(100);
	GPIO_ResetBits(GPIOC, GPIO_Pin_3);//设置LED高电平输出
	Delay_ms(100);
	GPIO_SetBits(GPIOC, GPIO_Pin_3);//设置LED高电平输出
	while(1)
		{		
			PS2_LX=PS2_AnologData(PSS_LX);    
			PS2_LY=PS2_AnologData(PSS_LY);
			PS2_RX=PS2_AnologData(PSS_RX);
			PS2_RY=PS2_AnologData(PSS_RY);
			PS2_KEY=PS2_DataKey();	

			Delay_ms(20);
		} 
}




5 SPI头文件



# ifndef  _SPI_H
# define  _SPI_H

// # include "stm32f10x.h"
/*复用推挽表示引脚的IO操作由相应的功能模块来完成,普通推挽表示你需要通过gpio寄存器来操作引脚*/
	//PC2 PS2_CS    片选信号(也称SPI_NSS),低电平传输数据有效。 配置为普通推挽模式GPIO_Mode_Out_PP()
	//PAS PS2_SCK   时钟信号。                            配置为复用推挽模式GPIO_Mode_AF_PP() 
	//PA6 PS2_MISO  主机输入模式。                        配置为下拉输入模式GPIO_Mode_IPD() 
	//PA7 PS2_MOSI  主机輸出模式。                        配置为复用推挽模式GPIO_Mode_AF_PP()


void PS2_Init(void);               //初始化SPI口

   
#define DI   GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6)  //读取输入口PA6值

#define DO_H GPIO_SetBits(GPIOA,GPIO_Pin_7);           //数据输出命令位高
#define DO_L GPIO_ResetBits(GPIOA,GPIO_Pin_7);         //数据输出命令位低

#define CS_H GPIO_SetBits(GPIOC,GPIO_Pin_2);           //CS片选拉高
#define CS_L GPIO_ResetBits(GPIOC,GPIO_Pin_2);         //CS片选CS拉低

#define CLK_H  GPIO_SetBits(GPIOA,GPIO_Pin_5);         //时钟拉高
#define CLK_L  GPIO_ResetBits(GPIOA,GPIO_Pin_5);       //时钟拉低




//以下内容引用平衡之家程序,增加注释**************************************

//辅助左2、辅助右2、辅助左1、辅助右1、按键右_前/右/后/左(4个位)
#define PSB_SELECT      1        //SELECT 按下
#define PSB_L3          2        //摇杆左 按下
#define PSB_R3          3        //摇杆右 按下
#define PSB_START       4        //STRAT  按下
#define PSB_PAD_UP      5        //按键左_前 按下
#define PSB_PAD_RIGHT   6        //按键左_右 按下
#define PSB_PAD_DOWN    7        //按键左_下 按下
#define PSB_PAD_LEFT    8        //按键左_左 按下
#define PSB_L2          9        //辅助左_2 按下
#define PSB_R2          10       //辅助右_2 按下
#define PSB_L1          11       //辅助左_1 按下
#define PSB_R1          12       //辅助右_1 按下
#define PSB_TRIANGLE    13       //按键右_前 按下
#define PSB_CIRCLE      14       //按键右_右 按下
#define PSB_CROSS       15       //按键右_后 按下
#define PSB_SQUARE      16       //按键右_左 按下

#define PSB_GREEN       13
#define PSB_RED         14
#define PSB_BLUE        15
#define PSB_PINK        16



#define PSS_RX           5      //摇杆右X轴数据YGY_X
#define PSS_RY           6      //摇杆右Y轴数据YGY_Y
#define PSS_LX           7      //摇杆左Y轴数据YGZ_X
#define PSS_LY           8      //摇杆左Y轴数据YGZ_Y

extern u8 Data[9];
extern u16 MASK[16];
extern u16 Handkey;



void Stm32_Clock_Init(u8 PLL);       //系统时钟初始化
void uart_init(u32 pclk2,u32 bound); //UART时钟初始化
u8 PS2_RedLight(void);               //判断是否为红灯模式
void PS2_ReadData(void);             //读手柄数据
void PS2_Cmd(u8 CMD);		         //向手柄发送命令
u8 PS2_DataKey(void);		         //按键值读取
u8 PS2_AnologData(u8 button);        //得到一个摇杆的模拟量
void PS2_ClearData(void);	         //清除数据缓冲区
void PS2_SetInit(void);              //手柄配置初始化

//以上内容引用平衡之家程序,增加注释**************************************


#endif

6 SPI程序(SPI.c)

      由于芯片与平衡小车之家的不一致,更改引脚。输入 DI->PA0、 输出 DO->PA1、 CS->PA2、CLK->PA3,另外强调示例中获取PS2使用IO口模拟通信。


#include "SPI.h"
#include "Delay.h"
#include "stm32f10x.h"


/*********************************************************     
**********************************************************/	 
#define DELAY_TIME  Delay_us(5); 
u16 Handkey;	// 按键值读取,零时存储。
u16 SPI_CR1,USART1_CR1;	// 按键值读取,零时存储。
u8 Comd[2]={0x01,0x42};	//开始命令。请求数据
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
	};	//按键值与按键明


	void JTAG_ENable(char JTAG_STA)
{
if(JTAG_STA==0)
{
AFIO->MAPR=0X00000000;
}
else
{
RCC->APB2ENR |= 0X00000001;  //开启APB2 AFIO时钟
	
AFIO->MAPR=0X02000000;      //JTAG做普通IO口,但SWD可用
}
return;
}

//  手柄接口初始化  输入  DI->PA6
//                 输出  DO->PA7    CS->PC2  CLK->PA5
void PS2_Init(void)
{
	
	RCC->APB2ENR|=1<<4;     //使能PORTC时钟
	GPIOC->CRL&=0XFFFFF0FF; //片选NSS信号:CS→PC 2 推挽输出   
	GPIOC->CRL|=0X00000300; 	
	
	
	
	RCC->APB2ENR|=1<<2;     //使能PORTA时钟
	GPIOA->CRL&=0X0F0FFFFF; //PA5 7推挽输出   
	GPIOA->CRL|=0X30300000;   

	GPIOA->CRL&=0XF0FFFFFF; 
	GPIOA->CRL|=0X08000000; //PA6设置成输入	默认下拉   	 	 									
	
	
	/*以下为原文件内容
	RCC->APB2ENR|=1<<2;     //原文件使能PORTA时钟
	GPIOA->CRL&=0XFFFF000F; //原文件PA1 2 3推挽输出   
	GPIOA->CRL|=0X00003330;   

	GPIOA->CRL&=0XFFFFFFF0; 
	GPIOA->CRL|=0X00000008;//PA0 设置成输入	默认下拉   	*/ 	 											  
}

//向手柄发送命令
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;
		if(DI)
			Data[1] = ref|Data[1];
	}
	SPI_CR1=SPI1->CR1;
	USART1_CR1=USART1->CR1;
	Delay_us(16);
}
//判断是否为红灯模式,0x41=模拟绿灯,0x73=模拟红灯
//返回值;0,红灯模式
//		  其他,其他模式
u8 PS2_RedLight(void)
{
	CS_L;
	PS2_Cmd(Comd[0]);         //开始命令
	PS2_Cmd(Comd[1]);         //请求数据
	CS_H;
	if( Data[1] == 0X73)   return 0 ;
	else return 1;

}
//读取手柄数据
void PS2_ReadData(void)
{
	volatile u8 byte=0;
	volatile u16 ref=0x01;
	CS_L;
	PS2_Cmd(Comd[0]);                //开始命令
	PS2_Cmd(Comd[1]);                //请求数据
	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;
}

//对读出来的PS2的数据进行处理,只处理按键部分  
//只有一个按键按下时按下为0, 未按下为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;                      //没有任何按键按下
}

//得到一个摇杆的模拟量	 范围0~256
u8 PS2_AnologData(u8 button)
{
	return Data[button];
}

//清除数据缓冲区
void PS2_ClearData()
{
	u8 a;
	for(a=0;a<9;a++)
		Data[a]=0x00;
}
/******************************************************
Function:    void PS2_Vibration(u8 motor1, u8 motor2)
Description: 手柄震动函数,
Calls:		 void PS2_Cmd(u8 CMD);
Input: motor1:右侧小震动电机 0x00关,其他开
	   motor2:左侧大震动电机 0x40~0xFF 电机开,值越大 震动越大
******************************************************/
void PS2_Vibration(u8 motor1, u8 motor2)
{
	CS_L;
	Delay_us(16);
    PS2_Cmd(0x01);         //开始命令
	PS2_Cmd(0x42);         //请求数据
	PS2_Cmd(0X00);
	PS2_Cmd(motor1);
	PS2_Cmd(motor2);
	PS2_Cmd(0X00);
	PS2_Cmd(0X00);
	PS2_Cmd(0X00);
	PS2_Cmd(0X00);
	CS_H;
	Delay_us(16);  
}
//short poll
void PS2_ShortPoll(void)
{
	CS_L;
	Delay_us(16);
	PS2_Cmd(0x01);  
	PS2_Cmd(0x42);  
	PS2_Cmd(0X00);
	PS2_Cmd(0x00);
	PS2_Cmd(0x00);
	CS_H;
	Delay_us(16);	
}
//进入配置
void PS2_EnterConfing(void)
{
    CS_L;
	Delay_us(16);
	PS2_Cmd(0x01);  
	PS2_Cmd(0x43);  
	PS2_Cmd(0X00);
	PS2_Cmd(0x01);
	PS2_Cmd(0x00);
	PS2_Cmd(0X00);
	PS2_Cmd(0X00);
	PS2_Cmd(0X00);
	PS2_Cmd(0X00);
	CS_H;
	Delay_us(16);
}
//发送模式设置
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);
}
//振动设置
void PS2_VibrationMode(void)
{
	CS_L;
	Delay_us(16);
	PS2_Cmd(0x01);  
	PS2_Cmd(0x4D);  
	PS2_Cmd(0X00);
	PS2_Cmd(0x00);
	PS2_Cmd(0X01);
	CS_H;
	Delay_us(16);	
}
//完成并保存配置
void PS2_ExitConfing(void)
{
    CS_L;
	Delay_us(16);
	PS2_Cmd(0x01);  
	PS2_Cmd(0x43);  
	PS2_Cmd(0X00);
	PS2_Cmd(0x00);
	PS2_Cmd(0x5A);
	PS2_Cmd(0x5A);
	PS2_Cmd(0x5A);
	PS2_Cmd(0x5A);
	PS2_Cmd(0x5A);
	CS_H;
	Delay_us(16);
}
//手柄配置初始化
void PS2_SetInit(void)
{
	PS2_ShortPoll();
	PS2_ShortPoll();
	PS2_ShortPoll();
	PS2_EnterConfing();		//进入配置模式
	PS2_TurnOnAnalogMode();	//“红绿灯”配置模式,并选择是否保存
	//PS2_VibrationMode();	//开启震动模式
	PS2_ExitConfing();		//完成并保存配置
}



void Stm32_Clock_Init(u8 PLL)
{
	unsigned char temp=0;   
//	MYRCC_DeInit();		  //复位并配置向量表
 	RCC->CR|=0x00010000;  //外部高速时钟使能HSEON
	while(!(RCC->CR>>17));//等待外部时钟就绪
	RCC->CFGR=0X00000400; //APB1=DIV2;APB2=DIV1;AHB=DIV1;
	PLL-=2;//抵消2个单位
	RCC->CFGR|=PLL<<18;   //设置PLL值 2~16
	RCC->CFGR|=1<<16;	  //PLLSRC ON 
	FLASH->ACR|=0x32;	  //FLASH 2个延时周期

	RCC->CR|=0x01000000;  //PLLON
	while(!(RCC->CR>>25));//等待PLL锁定
	RCC->CFGR|=0x00000002;//PLL作为系统时钟	 
	while(temp!=0x02)     //等待PLL作为系统时钟设置成功
	{   
		temp=RCC->CFGR>>2;
		temp&=0x03;
	}    
}		    

void uart_init(u32 pclk2,u32 bound)
{  	 
	float temp;
	u16 mantissa;
	u16 fraction;	   
	temp=(float)(pclk2*1000000)/(bound*16);//得到USARTDIV
	mantissa=temp;				 //得到整数部分
	fraction=(temp-mantissa)*16; //得到小数部分	 
  mantissa<<=4;
	mantissa+=fraction; 
	RCC->APB2ENR|=1<<2;   //使能PORTA口时钟  
	RCC->APB2ENR|=1<<14;  //使能串口时钟 
	GPIOA->CRH&=0XFFFFF00F;//IO状态设置
	GPIOA->CRH|=0X000008B0;//IO状态设置 
	RCC->APB2RSTR|=1<<14;   //复位串口1
	RCC->APB2RSTR&=~(1<<14);//停止复位	   	   
	//波特率设置
 	USART1->BRR=mantissa; // 波特率设置	 
	USART1->CR1|=0X200C;  //1位停止,无校验位.
}

7 PS2手柄SPI通讯展示

      Debug Analyzer模式调试结果:
在这里插入图片描述

在这里插入图片描述

      ST-Link仿真模式调试结果:
在这里插入图片描述
在这里插入图片描述

>> 更多相关内容,点击Morven_Xie博客概览

  • 2
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值