你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

【经验分享】HAL库 STM32CubeMX教程十四---SPI

[复制链接]
STMCU小助手 发布时间:2022-3-23 14:00
在我们的HAL库中,对硬件SPI函数做了很好的集成,使得之前SPI几百行代码,在HAL库中,只需要寥寥几行就可以完成 那么这篇文章将带你去感受下它的优异之处,这些优异的函数,也正是HAL库的优点所在

所用工具:

1、芯片: STM32F103ZET6

2、STM32CubeMx软件

3、IDE: MDK-Keil软件

4、STM32F1xx/STM32F4xxHAL库

5、SPI: 使用硬件SPI1

知识概括:

通过本篇博客您将学到:

SPI的基本原理

STM32CubeMX创建SPI例程

HAL库SPI函数库

什么是SPI
SPI 是英语Serial Peripheral interface的缩写,顾名思义就是串行外围设备接口。是Motorola(摩托罗拉)首先在其MC68HCXX系列处理器上定义的。

SPI,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,主要应用在 EEPROM,FLASH,实时时钟,AD转换器,还有数字信号处理器和数字信号解码器之间。

SPI主从模式
SPI分为主、从两种模式,一个SPI通讯系统需要包含一个(且只能是一个)主设备,一个或多个从设备。提供时钟的为主设备(Master),接收时钟的设备为从设备(Slave),SPI接口的读写操作,都是由主设备发起。当存在多个从设备时,通过各自的片选信号进行管理。

SPI是全双工且SPI没有定义速度限制,一般的实现通常能达到甚至超过10 Mbps

SPI信号线

SPI接口一般使用四条信号线通信:
SDI(数据输入),SDO(数据输出),SCK(时钟),CS(片选)

MISO: 主设备输入/从设备输出引脚。该引脚在从模式下发送数据,在主模式下接收数据。
MOSI: 主设备输出/从设备输入引脚。该引脚在主模式下发送数据,在从模式下接收数据。
SCLK:串行时钟信号,由主设备产生。
CS/SS:从设备片选信号,由主设备控制。它的功能是用来作为“片选引脚”,也就是选择指定的从设备,让主设备可以单独地与特定从设备通讯,避免数据线上的冲突。
硬件上为4根线。

SPI一对一

U4AND{]Q35]1A2B[VX~{)$P.png

SPI一对多

~EU(]W4UD{ZK@3[0U}6[WT5.png

SPI数据发送接收
SPI主机和从机都有一个串行移位寄存器,主机通过向它的SPI串行寄存器写入一个字节来发起一次传输。

1.首先拉低对应SS信号线,表示与该设备进行通信
2.主机通过发送SCLK时钟信号,来告诉从机写数据或者读数据
这里要注意,SCLK时钟信号可能是低电平有效,也可能是高电平有效,因为SPI有四种模式,这个我们在下面会介绍
3.主机(Master)将要发送的数据写到发送数据缓存区(Menory),缓存区经过移位寄存器(0~7),串行移位寄存器通过MOSI信号线将字节一位一位的移出去传送给从机,,同时MISO接口接收到的数据经过移位寄存器一位一位的移到接收缓存区。
4.从机(Slave)也将自己的串行移位寄存器(0~7)中的内容通过MISO信号线返回给主机。同时通过MOSI信号线接收主机发送的数据,这样,两个移位寄存器中的内容就被交换。

}9IPL%WBI(SXK~POT{1{A.png

SPI只有主模式和从模式之分,没有读和写的说法,外设的写操作和读操作是同步完成的。如果只进行写操作,主机只需忽略接收到的字节;反之,若主机要读取从机的一个字节,就必须发送一个空字节来引发从机的传输。也就是说,你发一个数据必然会收到一个数据;你要收一个数据必须也要先发一个数据。

SPI工作模式
根据时钟极性(CPOL)及相位(CPHA)不同,SPI有四种工作模式。
时钟极性(CPOL)定义了时钟空闲状态电平:
CPOL=0为时钟空闲时为低电平
CPOL=1为时钟空闲时为高电平

时钟相位(CPHA)定义数据的采集时间。
CPHA=0:在时钟的第一个跳变沿(上升沿或下降沿)进行数据采样。
CPHA=1:在时钟的第二个跳变沿(上升沿或下降沿)进行数据采样。

WSKLH~CQVK{U~ENH5BE{QEP.png

W25Q128 FLASH芯片介绍

W25Q128是一款SPI通信的FLASH芯片,可以通过标准/两线/四线SPI控制,其FLASH的大小为16M,分为 256 个块(Block),每个块大小为 64K 字节,每个块又分为 16个扇区(Sector),每个扇区 4K 个字节。通过SPI通信协议即可实现MCU(STM32)和 W25Q128 之间的通信。实现W25Q128的控制需要通过SPI协议发送相应的控制指令,并满足一定的时序。

原理图连接


S65UQWR]EZ(@1G2M{E3~IV2.png

常用指令:

写使能(Write Enable) (06h)


XPQX%GRHXNY))9_Z864R%SE.png

向FLASH发送0x06 写使能命令即可开启写使能,首先CS片选拉低,控制写入字节函数写入命令,CS片选拉高。

扇区擦除指令(Sector Erase) (0x20h)

_%`E7{PK0@02IY3PO3~E1.png
扇区擦除指令,数据写入前必须擦除对应的存储单元,该指令先拉低/CS引脚电平,接着传输“20H”指令和要24位要擦除扇区的地址。

读命令(Read Data) (03h)

读数据指令可从存储器依次一个或多个数据字节,该指令通过主器件拉低/CS电平使能设备开始传输,然后传输“03H”指令,接着通过DI管脚传输24位地址,从器件接到地址后,寻址存储器中的数据通过DO引脚输出。每传输一个字节地址自动递增,所以只要时钟继续传输,可以不断读取存储器中的数据。



状态读取命令(Read Status Register)


读状态寄存器1(05H),状态寄存器2(35H),状态寄存器3(15H)
写入命令0x05,即可读取状态寄存器的值。

`~_0F1_G[R2)T(M80VV1S8J.png

写入命令(Page Program) (02h)


REU_LMITYA3M}MQZPO8G2T3.png

在对W25Q128 FLASH的写入数据的操作中一定要先擦出扇区,在进行写入,否则将会发生数据错误。
W25Q128 FLASH一次性最大写入只有256个字节。
在进行写操作之前,一定要开启写使能(Write Enable)。
当只接收数据时不但能只检测RXNE状态 ,必须同时向发送缓冲区发送数据才能驱动SCK时钟跳变。

基于CubeMx的讲解
1设置RCC时钟


A9XVC[L){CQYZLTRY6A]3~B.png

设置高速外部时钟HSE 选择外部时钟源

2 SPI设置

SPI2设置为全双工主模式,硬件NSS关闭,如下图:


_OJ{EZE)BGBQ5P@`)0tLU.png

08)B_EEMT$II8GC[19Y_K9K.png

模式设置:

有主机模式全双工/半双工
从机模式全双工/半双工
只接收主机模式/只接收从机模式
只发送主机模式
因为我们是和W25Q128V芯片闪存芯片进行通信,所以设置为主机全双工

不使能硬件NSS


7OP}KVCLW_WDG@T$N9}Y`BJ.png

STM32有硬件NSS(片选信号),可以选择使能,也可以使用其他IO口接到芯片的NSS上进行代替

其中SIP1的片选NSS : SPI1_NSS(PA4)
其中SIP2的片选NSS : SPI2_NSS(PB12)


如果片选引脚没有连接 SPI1_NSS(PA4)或者SPI2_NSS(PB12),则需要选择软件片选

NSS管脚及我们熟知的片选信号,作为主设备NSS管脚为高电平,从设备NSS管脚为低电平。当NSS管脚为低电平时,该spi设备被选中,可以和主设备进行通信。在stm32中,每个spi控制器的NSS信号引脚都具有两种功能,即输入和输出。所谓的输入就是NSS管脚的信号给自己。所谓的输出就是将NSS的信号送出去,给从机。
对于NSS的输入,又分为软件输入和硬件输入。
软件输入:
NSS分为内部管脚和外部管脚,通过设置spi_cr1寄存器的ssm位和ssi位都为1可以设置NSS管脚为软件输入模式且内部管脚提供的电平为高电平,其中SSM位为使能软件输入位。SSI位为设置内部管脚电平位。同理通过设置SSM和SSI位1和0则此时的NSS管脚为软件输入模式但内部管脚提供的电平为0。若从设备是一个其他的带有spi接口的芯片,并不能选择NSS管脚的方式,则可以有两种办法,一种是将NSS管脚直接接低电平。另一种就是通过主设备的任何一个gpio口去输出低电平选中从设备。
硬件输入:
主机接高电平,从机接低电平。

左键对应的软件片选引脚,选择GPIO_Output(输出模式),然后点击GPIO,设置一下备注。

我这里虽然PB12是SPI2的硬件片选NSS,但是我想用软件片选,所以关闭了硬件NSS

~%UZ[)HI6C((0BNCD{K(H]7.png

S%%O[%JBTU~PH@~2S~J9@.png


SPI配置默认如下:


SPI配置中设置数据长度为8bit,MSB先输出分频为64分频,则波特率为125KBits/s。其他为默认设置。
Motorla格式,CPOL设置为Low,CPHA设置为第一个边沿。不开启CRC检验,NSS为软件控制。

MNT@YEQZ`1}LDM0BBI05MGK.png

最后记得初始化一下串口,因为需要测试例程,发送数据到上位机。

))_{$KKQG4]2X{X`R(XLEPT.png

3时钟源设置

8Y(B0VG8[GH)O[VF06P0JCU.png
我的是 外部晶振为8MHz

1选择外部时钟HSE 8MHz
2PLL锁相环倍频9倍
3系统时钟来源选择为PLL
4设置APB1分频器为 /2
5 使能CSS监视时钟

32的时钟树框图 如果不懂的话请看《【STM32】系统时钟RCC详解(超详细,超全面)》

4项目文件设置


K[I448YQ}X]MOK]ZO1[6C.png

1 设置项目名称
2 设置存储路径
3 选择所用IDE

GPR)1~0V)H[YO(A)U2I]`LF.png

5创建工程文件


然后点击GENERATE CODE 创建工程

配置下载工具

新建的工程所有配置都是默认的 我们需要自行选择下载模式,勾选上下载后复位运行

K)5%$]7K[06O]YRL~9QE)Y4.png

SPI函数详解

在stm32f1xx_hal_spi.h头文件中可以看到spi的操作函数。分别对应轮询,中断和DMA三种控制方式。

KEO@HHK%AOM{_HUQ0H4FNWN.png

轮询: 最基本的发送接收函数,就是正常的发送数据和接收数据
中断: 在SPI发送或者接收完成的时候,会进入SPI回调函数,用户可以编写回调函数,实现设定功能
DMA: DMA传输SPI数据

利用SPI接口发送和接收数据主要调用以下两个函数:
  1. HAL_StatusTypeDef  HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);//发送数据
  2. HAL_StatusTypeDef  HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);//接收数据
复制代码

SPI发送数据函数:
  1. HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);//发送数据
复制代码

参数:

*hspi: 选择SPI1/2,比如&hspi1,&hspi2
*pData : 需要发送的数据,可以为数组
Size: 发送数据的字节数,1 就是发送一个字节数据
Timeout: 超时时间,就是执行发送函数最长的时间,超过该时间自动退出发送函数

SPI接收数据函数:
  1. HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);//接收数据
复制代码

参数:
*hspi: 选择SPI1/2,比如&hspi1,&hspi2
*pData : 接收发送过来的数据的数组
Size: 接收数据的字节数,1 就是接收一个字节数据
Timeout: 超时时间,就是执行接收函数最长的时间,超过该时间自动退出接收函数


SPI接收回调函数:
  1.  HAL_SPI_TransmitReceive_IT(&hspi1, TXbuf,RXbuf,CommSize);
复制代码

当SPI上接收出现了 CommSize个字节的数据后,中断函数会调用SPI回调函数:
  1.    HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)
复制代码

用户可以重新定义回调函数,编写预定功能即可,在接收完成之后便会进入回调函数

片选引脚:

因为我们是软件使能片选,定义片选引脚,CS片选低电平为有效使能, CS片选高电平不使能

这里用两个宏定义来代替

在main.h中有宏定义命名,SPI2_CS_Pin 就是PB12
  1. //以W25Q128为例
  2. #define SPI_CS_Enable()                         HAL_GPIO_WritePin(GPIOB, SPI2_CS_Pin, GPIO_PIN_RESET)
  3. #define SPI_CS_Disable()                 HAL_GPIO_WritePin(GPIOB, SPI2_CS_Pin, GPIO_PIN_SET)
复制代码

SPI例程详解

因为不同的flash芯片通信协议以及方式都是不同的,所以这里介绍下具体的SPI的发送和接收应该怎么写,具体的请看芯片手册修改下即可,这里提供下W25QXX的驱动文件,以及测试例程,测试是正常没问题

挑几个函数讲解一下:

在w25Qxx.h钟可以修改CS片选引脚,W25Qx_Enable(),W25Qx_Disable()分别为使能和失能SPI设备,即拉低和拉高/CS电平
  1. #define W25Qx_Enable()                         HAL_GPIO_WritePin(SPI2_CS_GPIO_Port, SPI2_CS_Pin, GPIO_PIN_RESET)
  2. #define W25Qx_Disable()                 HAL_GPIO_WritePin(SPI2_CS_GPIO_Port, SPI2_CS_Pin, GPIO_PIN_SET)
复制代码

w25Qxx复位函数:

函数开始先将要发送的数据(命令(0x66)和地址(0x99))存储在cmd数组中,
拉低片选信号,开始SPI通信
然后后通过HAL_SPI_Transmit()函数发送出去
拉高片选信号,关闭SPI通信

W25Qx_TIMEOUT_VALUE是最大超时时间,在w25Qxx.h中定义为1000,单位为us

FA)X9VQ]S3IY97FX]V8X1FW.png
  1. /**
  2.   * @brief  This function reset the W25Qx.
  3.   * @retval None
  4.   */
  5. static void        BSP_W25Qx_Reset(void)
  6. {
  7.         uint8_t cmd[2] = {RESET_ENABLE_CMD,RESET_MEMORY_CMD};
  8.         
  9.         W25Qx_Enable();
  10.         /* Send the reset command */
  11.         HAL_SPI_Transmit(&hspi2, cmd, 2, W25Qx_TIMEOUT_VALUE);        
  12.         W25Qx_Disable();

  13. }
复制代码

W25QXX读函数:
三个参数:
pData 存放读取到的数据的数组
ReadAddr 读取数据的地址
Size 读取数据的大小


函数开始先将要发送的数据(命令和地址)存储在cmd数组中,
拉低片选信号,开始SPI通信
然后后通过HAL_SPI_Transmit()函数发送出去,首先发送写命令(0X03),上方有讲解,然后发送三个字节(24 Bit)的地址
接着通过HAL_SPI_Receive()接收读取的数据。
拉高片选信号,关闭SPI通信

ESCEO7$I`2621{[NT1@4MFY.png

  1. uint8_t BSP_W25Qx_Read(uint8_t* pData, uint32_t ReadAddr, uint32_t Size)
  2. {
  3.         uint8_t cmd[4];

  4.         /* Configure the command */
  5.         cmd[0] = READ_CMD;
  6.         cmd[1] = (uint8_t)(ReadAddr >> 16);
  7.         cmd[2] = (uint8_t)(ReadAddr >> 8);
  8.         cmd[3] = (uint8_t)(ReadAddr);
  9.         
  10.         W25Qx_Enable();
  11.         /* Send the read ID command */
  12.         HAL_SPI_Transmit(&hspi2, cmd, 4, W25Qx_TIMEOUT_VALUE);        
  13.         /* Reception of the data */
  14.         if (HAL_SPI_Receive(&hspi2, pData,Size,W25Qx_TIMEOUT_VALUE) != HAL_OK)
  15.   {
  16.     return W25Qx_ERROR;
  17.   }
  18.         W25Qx_Disable();
  19.         return W25Qx_OK;
  20. }
复制代码

写使能(Write Enable) (06h)


_H${X10%GJ3V4NONTH0B1}7.png

向FLASH发送0x06 写使能命令即可开启写使能,首先CS片选拉低,控制写入字节函数写入命令,CS片选拉高。

  1. uint8_t BSP_W25Qx_WriteEnable(void)
  2. {
  3.         uint8_t cmd[] = {WRITE_ENABLE_CMD};
  4.         uint32_t tickstart = HAL_GetTick();

  5.         /*Select the FLASH: Chip Select low */
  6.         W25Qx_Enable();
  7.         /* Send the read ID command */
  8.         HAL_SPI_Transmit(&hspi2, cmd, 1, W25Qx_TIMEOUT_VALUE);        
  9.         /*Deselect the FLASH: Chip Select high */
  10.         W25Qx_Disable();
  11.         
  12.         /* Wait the end of Flash writing */
  13.         while(BSP_W25Qx_GetStatus() == W25Qx_BUSY);
  14.         {
  15.                 /* Check for the Timeout */
  16.     if((HAL_GetTick() - tickstart) > W25Qx_TIMEOUT_VALUE)
  17.     {        
  18.                         return W25Qx_TIMEOUT;
  19.     }
  20.         }
  21.         
  22.         return W25Qx_OK;
  23. }
复制代码

扇区擦除函数:

扇区擦除指令(Sector Erase) (0x20h)


EY@MZDOQ(DHAJFP0UNAWDTQ.png

扇区擦除指令,数据写入前必须擦除对应的存储单元,并且使能写操作,该指令先拉低/CS引脚电平,接着传输“20H”指令和要24位要擦除扇区的地址。判断flash是否为忙状态,如果不为忙则擦除操作完成。

  1. uint8_t BSP_W25Qx_Erase_Block(uint32_t Address)
  2. {
  3.         uint8_t cmd[4];
  4.         uint32_t tickstart = HAL_GetTick();
  5.         cmd[0] = SECTOR_ERASE_CMD;
  6.         cmd[1] = (uint8_t)(Address >> 16);
  7.         cmd[2] = (uint8_t)(Address >> 8);
  8.         cmd[3] = (uint8_t)(Address);
  9.         
  10.         /* Enable write operations */
  11.         BSP_W25Qx_WriteEnable();
  12.         
  13.         /*Select the FLASH: Chip Select low */
  14.         W25Qx_Enable();
  15.         /* Send the read ID command */
  16.         HAL_SPI_Transmit(&hspi2, cmd, 4, W25Qx_TIMEOUT_VALUE);        
  17.         /*Deselect the FLASH: Chip Select high */
  18.         W25Qx_Disable();
  19.         
  20.         /* Wait the end of Flash writing */
  21.         while(BSP_W25Qx_GetStatus() == W25Qx_BUSY);
  22.         {
  23.                 /* Check for the Timeout */
  24.     if((HAL_GetTick() - tickstart) > W25Q128FV_SECTOR_ERASE_MAX_TIME)
  25.     {        
  26.                         return W25Qx_TIMEOUT;
  27.     }
  28.         }
  29.         return W25Qx_OK;
  30. }
复制代码

例程测试
重新定义printf函数

在 stm32f1xx_hal.c中包含#include <stdio.h>

  1. #include "stm32f4xx_hal.h"
  2. #include <stdio.h>
  3. extern UART_HandleTypeDef huart1;   //声明串口
复制代码

在 stm32f1xx_hal.c 中重写fget和fput函数

  1. /**
  2.   * 函数功能: 重定向c库函数printf到DEBUG_USARTx
  3.   * 输入参数: 无
  4.   * 返 回 值: 无
  5.   * 说    明:无
  6.   */
  7. int fputc(int ch, FILE *f)
  8. {
  9.   HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
  10.   return ch;
  11. }

  12. /**
  13.   * 函数功能: 重定向c库函数getchar,scanf到DEBUG_USARTx
  14.   * 输入参数: 无
  15.   * 返 回 值: 无
  16.   * 说    明:无
  17.   */
  18. int fgetc(FILE *f)
  19. {
  20.   uint8_t ch = 0;
  21.   HAL_UART_Receive(&huart1, &ch, 1, 0xffff);
  22.   return ch;
  23. }
复制代码

main.c
在main.c里添加以下代码:

  1. #include <string.h>
  2. #include "W25QXX.h"



  3. uint8_t wData[0x100];
  4. uint8_t rData[0x100];
  5. uint32_t i;
  6. uint8_t ID[2];
复制代码
  1. printf("\r\n SPI-W25Qxxx Example \r\n\r\n");

  2.   /*##-1- Read the device ID  ########################*/

  3.   BSP_W25Qx_Init();
  4.   BSP_W25Qx_Read_ID(ID);

  5.   printf(" W25Qxxx ID is : 0x%02X 0x%02X \r\n\r\n",ID[0],ID[1]);
  6.   /*##-2- Erase Block ##################################*/
  7.   if(BSP_W25Qx_Erase_Block(0) == W25Qx_OK)
  8.       printf(" SPI Erase Block ok\r\n");
  9.   else
  10.       Error_Handler();
  11.   /*##-3- Written to the flash ########################*/
  12.   /* fill buffer */
  13.   for(i =0;i<0x100;i ++)
  14.   {
  15.           wData<i> = i;
  16.         rData<i> = 0;
  17.   }
  18. if(BSP_W25Qx_Write(wData,0x00,0x100)== W25Qx_OK)
  19.       printf(" SPI Write ok\r\n");
  20.   else
  21.       Error_Handler();
  22.   /*##-4- Read the flash     ########################*/

  23.   if(BSP_W25Qx_Read(rData,0x00,0x100)== W25Qx_OK)
  24.       printf(" SPI Read ok\r\n\r\n");
  25.   else
  26.       Error_Handler();
  27.   printf("SPI Read Data : \r\n");
  28.   for(i =0;i<0x100;i++)
  29.       printf("0x%02X  ",rData<i>);
  30.   printf("\r\n\r\n");
  31.   /*##-5- check date          ########################*/   
  32.   if(memcmp(wData,rData,0x100) == 0 )
  33.       printf(" W25Q128FV SPI Test OK\r\n");
  34.   else
  35.       printf(" W25Q128FV SPI Test False\r\n");
  36. </i></i></i>
复制代码

STM32F103测试正常:

W[@WHY6O~O%7YTVLCI@9J(F.png

收藏 评论0 发布时间:2022-3-23 14:00

举报

0个回答

所属标签

相似分享

官网相关资源

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
st-img 微信公众号
st-img 手机版