[STM32] STM32纯硬件SPI主/从模式 库函数版(硬件NSS,SPI Master/Slave)

1. STM32 SPI

1.1 STM32的SPI接口

SPI可以设置为主、从两种模式,并且支持全双工模式,而配置为主、从模式或软件、硬件NSS,在操作上有很大的区别。由于一个项目需求,笔者对STM32的硬件模式和主从模式进行了一些研究,走了很多弯路,也查询了很多资料,现在终于调通了,因此写一篇文章记录调试心得,以及很多需要注意的地方。

以下是STM32 SPI接口的介绍:

  • 3线全双工同步传输;

  • 8或16位传输帧格式选择;

  • 主或从操作,支持多主模式;

  • 主模式和从模式下均可以由软件或硬件进行NSS管理:主/从操作模式的动态改变;

  • 可编程的时钟极性和相位;

  • 可编程的数据顺序,MSB在前或LSB在前;

  • 可触发中断的专用发送和接收标志;

  • SPI总线忙状态标志;

  • 支持可靠通信的硬件CRC;

  • 可触发中断的主模式故障、过载以及CRC错误标志;

  • 支持DMA功能的1字节发送和接收缓冲器:产生发送和接受请求。

本文主要探讨主模式和从模式NSS硬件和软件管理。

2. SPI Master 初始化及测试

2.1 硬件NSS模式

以下是初始化代码

void SPI1_Configuration(void)
{
	SPI_InitTypeDef SPI_InitStructure;
	GPIO_InitTypeDef GPIO_InitStructure;
	
/************打开时钟*************/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);  
	RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA, ENABLE ); 
	RCC_APB2PeriphClockCmd( RCC_APB2Periph_AFIO, ENABLE ); 
	
/************引脚配置*************/
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;  //SPI_NSS
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  //复用推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	GPIO_SetBits(GPIOA,GPIO_Pin_4);
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;		//SPI_SCK
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  //复用推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_6;		//SPI_MISO
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;  //浮空输入
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);

	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;        //SPI_MOSI
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  //复用推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);

/************SPI初始化配置*************/
	SPI_Cmd(SPI1, DISABLE);	 //必须先禁用,才能改变MODE
	SPI_InitStructure.SPI_Direction =SPI_Direction_2Lines_FullDuplex; //两线全双工
	SPI_InitStructure.SPI_Mode =SPI_Mode_Master; //主
	SPI_InitStructure.SPI_DataSize =SPI_DataSize_8b; //8位
	SPI_InitStructure.SPI_CPOL =SPI_CPOL_Low; //CPOL=0
	SPI_InitStructure.SPI_CPHA =SPI_CPHA_1Edge; //CPHA=0
	SPI_InitStructure.SPI_NSS =SPI_NSS_Hard; //硬件NSS
	SPI_InitStructure.SPI_BaudRatePrescaler =SPI_BaudRatePrescaler_64; //64分频
	SPI_InitStructure.SPI_FirstBit =SPI_FirstBit_MSB; //高位在前
	SPI_InitStructure.SPI_CRCPolynomial =7; //CRC7
	SPI_Init(SPI1,&SPI_InitStructure);
	
// 	SPI_Cmd(SPI1, ENABLE);			//先不打开SPI
 	SPI_SSOutputCmd(SPI1, ENABLE);   		//SPI的NSS引脚控制开启

}

SPI配置为主模式,采用硬件NSS有几点需要注意,若采用硬件NSS,一定要把NSS引脚输出设置为GPIO_Mode_AF_PP,否则程序无法正确控制。

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;

2.2 软件NSS模式

软件NSS的初始化步骤大同小异,有几个地方不一样,需要注意一下

①NSS引脚修改为GPIO_Mode_Out_PP

    /************引脚配置*************/
    	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;  //SPI_NSS
    	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;  //推挽输出
    	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    	GPIO_Init(GPIOA, &GPIO_InitStructure);
    	GPIO_SetBits(GPIOA,GPIO_Pin_4);

②将NSS配置改为SPI_NSS_Soft,然后打开SPI,屏蔽SPI_SSOutputCmd()这个函数。

    /************SPI初始化配置*************/
    	SPI_Cmd(SPI1, DISABLE);	 //必须先禁用,才能改变MODE
    	SPI_InitStructure.SPI_Direction =SPI_Direction_2Lines_FullDuplex; //两线全双工
    	SPI_InitStructure.SPI_Mode =SPI_Mode_Master; //主
    	SPI_InitStructure.SPI_DataSize =SPI_DataSize_8b; //8位
    	SPI_InitStructure.SPI_CPOL =SPI_CPOL_Low; //CPOL=0
    	SPI_InitStructure.SPI_CPHA =SPI_CPHA_1Edge; //CPHA=0
    	SPI_InitStructure.SPI_NSS =SPI_NSS_Soft; //软件NSS
    	SPI_InitStructure.SPI_BaudRatePrescaler =SPI_BaudRatePrescaler_64; //64分频
    	SPI_InitStructure.SPI_FirstBit =SPI_FirstBit_MSB; //高位在前
    	SPI_InitStructure.SPI_CRCPolynomial =7; //CRC7
    	SPI_Init(SPI1,&SPI_InitStructure);
    	
     	SPI_Cmd(SPI1, ENABLE);			//打开SPI
     	//SPI_SSOutputCmd(SPI1, ENABLE);   		//SPI的NSS引脚控制开启

2.3 主函数

2.3.1 硬件NSS模式

硬件NSS模式的操作步骤和软件NSS模式可谓天差地别,首先初始化的时候不需要打开SPI,而且需要单独配置打开NSS引脚,否则NSS引脚会一直输出低电平,无法控制。
其次是操作步骤,硬件NSS模式下,每一次数据读写都需要先打开SPI,操作完成后再关闭SPI,必须要按照这个步骤来,否则NSS引脚一直会是低电平。
这也许就是很多人认为硬件NSS有BUG,无法正确选通,而我也在网上查了很多资料,发现很少有人用硬件NSS。

    int main()
    { 
    	u8 SPI_TX=10,SPI_RX=0; 		//
    	SPI1_Configuration();				//spi初始化
    	while(1)	  
    	{
       		SPI_Cmd(SPI1,ENABLE); 		//启动SPI
       		
    		/**************向从设备发送一个字节*************/
    		while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) == RESET);
    		SPI_I2S_SendData(SPI1,SPI_TX);	
    		
          	/**************保存将接收到的数据*************/
    		while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE) == RESET);
    		SPI_RX = SPI_I2S_ReceiveData(SPI1);
    		
    		SPI_Cmd(SPI1,DISABLE); 		//关闭SPI
    	  	
    		SPI_TX++;			//发送的值+1
     		delay_100ms(10);		//延时1秒
    		if(SPI_TX>50)		{SPI_TX=10;}
    	}
    }
  • 效果测试

SPI-NSS-HARD-TX
上图是刚刚的代码运行的效果,用逻辑分析仪抓取的SPI波形。
从设备同样是STM32,设置为硬件从模式,采用硬件NSS控制,设置方法后文会讲。
可以看到NSS能正确拉低,主设备能正确收到从设备返回的数据。

从设备设置的是收到什么就会返回什么,由于SPI的特性是在发送的同时接收数据,且从设备不能主动向主设备发送数据,因此在发送新数据的时候才能收到上此次发送的旧数据。

2.3.2 软件NSS模式

由于代码差不多,就只贴while(1)的函数

    while(1)	  
    {
       	GPIO_ResetBits(GPIOA,GPIO_Pin_4); 		//拉低CS
		/**************向从设备发送一个字节*************/
		while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) == RESET);
		SPI_I2S_SendData(SPI1,SPI_TX);	
		
      	/**************保存将接收到的数据*************/
		while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE) == RESET);
		SPI_RX = SPI_I2S_ReceiveData(SPI1);
		
		GPIO_SetBits(GPIOA,GPIO_Pin_4); 		//拉高CS 
		
		SPI_TX++;			//发送的值+1
 		delay_100ms(10);		//延时1秒
		if(SPI_TX>50)	 {SPI_TX=10;}
	}
  • 效果测试
    SPI-NSS-SOFT-TX

软件NSS模式下,SPI是常开的,读写数据的时候需要人为控制CS引脚
可以看到CS线在最后一个CLK脉冲发送完成后就立即拉高了,和硬件模式有一点区别

如果去掉这两个函数会怎样呢?

while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) == RESET);
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE) == RESET);

主函数改为:

    while(1)	  
    {
       	GPIO_ResetBits(GPIOA,GPIO_Pin_4); 		//拉低CS
		/**************向从设备发送一个字节*************/
		SPI_I2S_SendData(SPI1,SPI_TX);	
		
      	/**************保存将接收到的数据*************/
		SPI_RX = SPI_I2S_ReceiveData(SPI1);
		
		delay_us(2);			//延时2微秒
		GPIO_SetBits(GPIOA,GPIO_Pin_4); 		//拉高CS 
		
		SPI_TX++;			//发送的值+1
 		delay_100ms(10);		//延时1秒
		if(SPI_TX>50)	 {SPI_TX=10;}
	}
  • 效果测试

SPI-NSS-SOFT-TX2
可以看到,程序似乎没有按照预想顺序进行,延时函数好像没有在SPI_I2S_ReceiveData()结束后才执行。
这也是使用硬件SPI常犯的错误,因为在使用硬件SPI时,外设在进行数据读写的时候是不占用内核时间的,内核把数据丢给外设寄存器就完事了,内核只需要等待外设返回结束标志位,刚才屏蔽掉的两个函数就是在等待外设返回结束标志,而这个等待时间其实也可以做很多事情。
经过测试,内核对外设的操作只用了大概0.4uS。
若不注意这点,可能会出现SPI正在读取芯片数据过程中,芯片的CS线被拉高,芯片可能就停止发送数据了,导致最后读取的数据异常。

以上就是SPI Master模式中的硬件NSS和软件NSS的设置和控制,接下来是SPI Slave模式的设置步骤。

3. SPI Slave 初始化及测试

3.1 硬件NSS模式

以下是初始化代码

void SPI1_Config_Slave(void)		
{
/************打开时钟*************/
	SPI_InitTypeDef SPI_InitStructure;
	GPIO_InitTypeDef GPIO_InitStructure;
	
	RCC_APB2PeriphClockCmd( RCC_APB2Periph_SPI1 , ENABLE );
	RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA, ENABLE ); 
	RCC_APB2PeriphClockCmd( RCC_APB2Periph_AFIO , ENABLE ); 
	
/************引脚配置*************/
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;  //SPI_NSS
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;  //上拉输入
// 	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	GPIO_SetBits(GPIOA,GPIO_Pin_4);
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;		//SPI_SCK
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;  //上拉输入
// 	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_6;		//SPI_MISO
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;  //复用开漏输出(多从机)
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);

	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;        //SPI_MOSI
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;  //上拉输入
// 	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);

/************SPI初始化配置*************/
	SPI_Cmd(SPI1, DISABLE);		 //必须先禁能,才能改变MODE
	SPI_InitStructure.SPI_Direction =SPI_Direction_2Lines_FullDuplex; //两线全双工
	SPI_InitStructure.SPI_Mode =SPI_Mode_Slave; //从机
	SPI_InitStructure.SPI_DataSize =SPI_DataSize_16b; //8位
	SPI_InitStructure.SPI_CPOL =SPI_CPOL_Low; //CPOL=1时钟悬空高
	SPI_InitStructure.SPI_CPHA =SPI_CPHA_1Edge; //CPHA=1 数据捕获第2个
	SPI_InitStructure.SPI_NSS =SPI_NSS_Hard; //硬件NSS
	SPI_InitStructure.SPI_BaudRatePrescaler =SPI_BaudRatePrescaler_32; //32分频(从机没用)
	SPI_InitStructure.SPI_FirstBit =SPI_FirstBit_MSB; //高位在前
	SPI_InitStructure.SPI_CRCPolynomial =7; //CRC7
	SPI_Init(SPI1,&SPI_InitStructure);
	
	SPI_Cmd(SPI1, ENABLE);

从机硬件NSS初始化和软件NSS初始化大同小异,有几个注意的地方是:

不同于主机,从机很多IO口都要改为输入模式。

然后在SPI_MISO引脚的配置上,笔者这里用的是开漏输出,因为一个主机上挂了很多从机,如果用上拉输出的话可能会造成很多问题,在这条总线上用的是外部电阻上拉。

但是如果是单从机的话,如果主机设置的是浮空输入,那就很有可能导致主机无法收到数据,因此要根据自己的实际使用情况灵活设置

好的,关于使用HAL库实现STM32作为从机的SPI通讯,可以参考以下步骤: 1. 配置SPI从机模式 在HAL库中,可以使用`HAL_SPI_Init()`函数来初始化SPI外设,并设置从机模式。可以在SPI初始化结构体中设置以下参数: - SPI_Mode:设置为SPI_MODE_SLAVE。 - SPI_NSS:设置为SPI_NSS_HARD_INPUT,表示使用外部片选信号。 - SPI_FirstBit:设置为SPI_FIRSTBIT_MSB,表示数据传输时先发送高位。 2. 配置SPI从机时钟极性和相位 在从机模式下,需要根据机的时钟极性和相位来设置从机的时钟极性和相位。可以使用`HAL_SPI_ConfigPhaseClkPolarity()`函数来设置。 例如,如果机的时钟极性为1,时钟相位为1,则从机的时钟极性应为0,时钟相位应为1,可以使用以下代码进行设置: ``` SPI_HandleTypeDef hspi; hspi.Init.CLKPolarity = SPI_POLARITY_LOW; // 时钟极性为0 hspi.Init.CLKPhase = SPI_PHASE_2EDGE; // 时钟相位为1 HAL_SPI_Init(&hspi); ``` 3. 实现从机接收数据的回调函数 在从机模式下,当机发送数据时,从机会触发接收数据的回调函数。可以使用`HAL_SPI_RxCpltCallback()`函数来实现回调函数。 在回调函数中,可以通过`HAL_SPI_Receive()`函数来接收机发送的数据。 例如,以下代码实现了一个简单的从机回调函数,每当接收到一位数据时,就会在LED上闪烁一次: ``` void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { uint8_t data; HAL_SPI_Receive(hspi, &data, 1, HAL_MAX_DELAY); HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } ``` 4. 启动SPI从机模式 在完成以上步骤后,可以使用`HAL_SPI_Start_IT()`函数来启动SPI从机模式,等待机发送数据。 完整的代码示例可以参考以下链接:https://github.com/STMicroelectronics/STM32CubeF4/blob/master/Projects/STM32F4-Discovery/Examples/SPI/SPI_FullDuplex_ComDMA/Src/main.c
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值