目录
1、SPI协议的概念
SPI(Serial Peripheral Interface,串行外设接口)是由摩托罗拉(Motorola)在 1980 前后提出的一种全双工同步串行通信接口,它用于 MCU 与各种外围设备以串行方式进 行通信以交换信息,通信速度最高可达 25MHz 以上。 SPI 接口主要应用在 EEPROM、FLASH、实时时钟、网络控制器、OLED 显示驱动器、AD 转换器,数字信号处理器、数字信号解码器等设备之间。 SPI 通常由四条线组成,一条主设备输出与从设备输入(Master Output Slave Input, MOSI),一条主设备输入与从设备输出(Master Input Slave Output,MISO),一条时钟信号(Serial Clock,SCLK),一条从设备使能选择(Chip Select,CS)。 SPI 可以一个主机连接单个或多个从机,每个从机都使用一个引脚进行片选,物理连接示意图如图所示:
2、SPI 的传输模式
2.1 SPI工作模式
SPI通讯需要使用4条线,3条总线和1条片选信号线,在 SCK 时钟周期的驱动下,主机把数据驱动到 MOSI 上传给从机,从机把数据驱动到 MISO 上传给主机,物理连接如下图:
主机发送 N 字节给从机时,必定能接收到 N 字节,至于接收到的 N 字节是否有意义由 从机决定。如果主机只想对从机进行写操作,主机只需忽略接收的从机数据即可。如果主 机只想读取从机数据,它也要发送数据给从机(发送的数据可以是空数据)。
2.2 SPI传输模式
SPI 有四种传输模式,如下表所示,主要差别在于 CPOL 和 CPHA 的不同。
-
CPOL(Clock Polarity,时钟极性)表示 SCK 在空闲时为高电平还是低电平。当 CPOL=0,SCK 空闲时为低电平,当 CPOL=1,SCK 空闲时为高电平。
-
CPHA(Clock Phase,时钟相位)表示 SCK 在第几个时钟边缘采样数据。当 CPHA=0, 在 SCK 第一个边沿采样数据,当 CPHA=1,在 SCK 第二个边沿采样数据。
2.3 SPI操作方法
在 SPI 传输里,有 2 个角色:Master、Slave。我们常用 Master,它主动发起传输、 产生时钟信号。 对于 STM32,如何使用 SPI Master? 先初始化:
① 配置 SPI_CR1 寄存器(Control Register):配置波特率 ;
② 配置 SPI_CR1 寄存器(Control Register):配置 CPOL、CPHA ;
③ 配置 SPI_CR1 寄存器(Control Register):配置 LSBFIRT 以决定先传输最高位还是最低位 ;
④ 配置 SPI_CR1 寄存器(Control Register):配置 NSS 使用软件控制,一般使用 GPIO 来 选择其他 Slave 设备,不是 NSS;
⑤ 配置 SPI_CR1 寄存器(Control Register):选择 Master 模式、使能 SPI。
如何发送数据呢?把数据写入“SPI_DR”即可,这个数据会被放入放入“Tx Buffer”, 进而放入“shift register”,它就会一位一位地出现在 MOSI 信号线上,发送给 Slave 设 备。当发送完毕后,TXE 标记位被设置。可以使用查询方式或中断方式监测到 TXE 被置位。 如何接收数据呢?即使我们只想接收数据,也要把一个数组写入“SPI_DR”,在发送数 据的同时就会接收到数据:MISO 信号线上的数据被一位一位地移入“shift register”, 然后被放入“Rx Buffer”并且 RXNE 标记位被置一。可以使用查询方式或中断方式监测到 RXNE 被置位,然后读取“SPI_DR”得到数据。
3、时序图
如下图所示,CPHA=0 时,表示在时钟第一个时钟边沿采样数据。当 CPOL=1,即空闲时为高电平,从高电平变为低电平,第一个时钟边沿(下降沿)即进行采样。当 CPOL=0,即 空闲时为低电平,从低电平变为高电平,第一个时钟边沿(上升沿)即进行采样。
4、代码实现
4.1 SPI HAL库编程
使用查询方式读取SPI设备的函数如下:
/* 发送同时接收数据 */ HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, uint32_t Timeout); /* 发送数据 */ HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout); /* 接收数据 */ HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);
函数中的参数说明:
4.2 中断方式
通过编写spi中断回调函数代码,实现对flash的数据写入以及读取的控制和等待,在main文件中调用函数,从而在oled屏幕上显示出字符串“www.100ask.com”的值。
spi.c
static volatile int g_spi1_tx_complete = 0; static volatile int g_spi1_rx_complete = 0; static volatile int g_spi1_txrx_complete = 0; /* *SPI发送完成中断回调函数,当SPI1发送完成,将全局变量g_spi1_tx_complete设置成1 */ void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if(hspi == &hspi1) { g_spi1_tx_complete = 1; } } /*这是一个等待SPI1发送完成的函数,它会不断检查g_spi1_tx_complete的值,直到它变为1或者超时。 超时时间由参数timeout指定, 每次循环都会延时1毫秒 */ void Wait_SPI1_TxCplt(int timeout) { while(g_spi1_tx_complete == 0 && timeout--) { HAL_Delay(1); } g_spi1_tx_complete = 0; } /* *SPI接收完成中断回调函数,当SPI1接收完成,将全局变量g_spi1_rx_complete设置成1 */ void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if(hspi == &hspi1) { g_spi1_rx_complete = 1; } } /* 这是一个等待SPI1接收完成的函数,它会不断检查g_spi1_rx_complete的值,直到它变为1或者超时。 超时时间由参数timeout指定,每次循环都会延时1毫秒。 */ void Wait_SPI1_RxCplt(int timeout) { while(g_spi1_rx_complete == 0 && timeout--) { HAL_Delay(1); } g_spi1_rx_complete = 0; } /*这是一个SPI发送和接收都完成的中断回调函数,当SPI1发送和接收都完成后, 将全局变量g_spi1_txrx_complete设置为1。 */ void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) { if(hspi == &hspi1) { g_spi1_txrx_complete = 1; } } void Wait_SPI1_TxRxCplt(int timeout) { while(g_spi1_txrx_complete == 0 && timeout--) { HAL_Delay(1); } g_spi1_txrx_complete = 0; }
**driver_spi_flash.c**
#include "driver_spi_flash.h" #include "stm32f1xx_hal.h" #define SPI_FLASH_TIMEOUT 1000 void Wait_SPI1_TxCplt(int timeout); void Wait_SPI1_TxRxCplt(int timeout); void Wait_SPI1_RxCplt(int timeout); extern SPI_HandleTypeDef hspi1; /* 内部函数 */ static void SPIFlash_Select(void) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_RESET); } static void SPIFlash_DeSelect(void) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_SET); } static int SPIFlash_WriteEnable(void) { uint8_t buf[1] = {0x06}; SPIFlash_Select(); HAL_SPI_Transmit_IT(&hspi1, buf, 1); Wait_SPI1_TxCplt(SPI_FLASH_TIMEOUT); SPIFlash_DeSelect(); } static int SPIFlash_ReadStatus(void) { uint8_t txbuf[2] = {0x05, 0xff}; uint8_t rxbuf[2] = {0, 0}; SPIFlash_Select(); HAL_SPI_TransmitReceive_IT(&hspi1, txbuf, rxbuf, 2); Wait_SPI1_TxRxCplt(SPI_FLASH_TIMEOUT); SPIFlash_DeSelect(); return rxbuf[1]; } static int SPIFlash_WaitReady(void) { while (SPIFlash_ReadStatus() & 1 == 1); } /* 对外的函数 */ int SPIFlash_ReadID(void) { uint8_t txbuf[2] = {0x9F, 0xff}; uint8_t rxbuf[2] = {0, 0}; SPIFlash_Select(); HAL_SPI_TransmitReceive_IT(&hspi1, txbuf, rxbuf, 2); Wait_SPI1_TxRxCplt(SPI_FLASH_TIMEOUT); SPIFlash_DeSelect(); return rxbuf[1]; } int SPIFlash_EraseSector(uint32_t addr) { uint8_t txbuf[4] = {0x20}; /* write enable */ SPIFlash_WriteEnable(); /* erase */ txbuf[1]= (addr>>16) & 0xff; txbuf[2]= (addr>>8) & 0xff; txbuf[3]= (addr>>0) & 0xff; SPIFlash_Select(); HAL_SPI_Transmit_IT(&hspi1, txbuf, 4); Wait_SPI1_TxCplt(SPI_FLASH_TIMEOUT); SPIFlash_DeSelect(); /* wait ready */ SPIFlash_WaitReady(); return 0; } int SPIFlash_Write(uint32_t addr, uint8_t *datas, uint32_t len) { uint8_t txbuf[4] = {0x02}; /* write enable */ SPIFlash_WriteEnable(); /* program */ txbuf[1]= (addr>>16) & 0xff; txbuf[2]= (addr>>8) & 0xff; txbuf[3]= (addr>>0) & 0xff; SPIFlash_Select(); /* 发送命令和地址 */ HAL_SPI_Transmit_IT(&hspi1, txbuf, 4); Wait_SPI1_TxCplt(SPI_FLASH_TIMEOUT); /* 发送数据 */ HAL_SPI_Transmit_IT(&hspi1, datas, len); Wait_SPI1_TxCplt(SPI_FLASH_TIMEOUT); SPIFlash_DeSelect(); /* wait ready */ SPIFlash_WaitReady(); return 0; } int SPIFlash_Read(uint32_t addr, uint8_t *datas, uint32_t len) { uint8_t txbuf[4] = {0x03}; /* read */ txbuf[1]= (addr>>16) & 0xff; txbuf[2]= (addr>>8) & 0xff; txbuf[3]= (addr>>0) & 0xff; SPIFlash_Select(); /* 发送命令和地址 */ HAL_SPI_Transmit_IT(&hspi1, txbuf, 4); Wait_SPI1_TxCplt(SPI_FLASH_TIMEOUT); /* 读取数据 */ HAL_SPI_Receive_IT(&hspi1, datas, len); Wait_SPI1_RxCplt(SPI_FLASH_TIMEOUT); SPIFlash_DeSelect(); return 0; }
**driver_spi_flash.h**
#ifndef __DRIVER_SPI_FLASH_H #define __DRIVER_SPI_FLASH_H #include <stdint.h> int SPIFlash_ReadID(void); int SPIFlash_EraseSector(uint32_t addr); int SPIFlash_Write(uint32_t addr, uint8_t *datas, uint32_t len); int SPIFlash_Read(uint32_t addr, uint8_t *datas, uint32_t len); #endif /* __DRIVER_SPI_FLASH_H */
**main.c**
char *str = "www.100ask.net"; char flash_buf[20]; SPIFlash_EraseSector(4096); SPIFlash_Write(4096, str, strlen(str)+1); SPIFlash_Read(4096, flash_buf, 20); OLED_PrintString(0, 2, flash_buf);
实现现象
4.3 DMA方式函数说明
使用DMA方式读取SPI设备的函数如下:
/* 发送同时接收数据 */ HAL_StatusTypeDef HAL_SPI_TransmitReceive_DMA(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size); /* 发送数据 */ HAL_StatusTypeDef HAL_SPI_Transmit_DMA(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size); /* 接收数据 */ HAL_StatusTypeDef HAL_SPI_Receive_DMA(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size);
函数中参数说明
这几个函数只是启动 SPI 传输,以后完全由 DMA 来传输后续数据。这两个函数返回后, 并不表示传输已经完成,需要在回调函数里判断。 要使用 DMA 方式,还需要使用 STM32CubeMX 配置 DMA,如下:
数据读写完成或者出错调用以下函数:
/* 发送、接收完成回调函数 */ void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi); /* 发送完成回调函数 */ void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi); /* 接收完成回调函数 */ void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) /* 出错回调函数 */ void HAL_SPI_ErrorCallback(SPI_HandleTypeDef *hspi);
5、总结
5.1 SPI协议的优点
-
SPI通信协议易于理解和实现,硬件接口仅涉及几根信号线,包括时钟线(SCLK)、主设备输出/从设备输入线(MOSI)、主设备输入/从设备输入线(MISO)、片选信号线(SS)。
-
SPI支持全双工通信,允许主设备和从设备同时发送和接收数据。
-
能够实现高速传输、支持多种工作模式。
5.2 SPI协议的缺点
-
缺乏流控制:SPI不提供内置的流量控制机制,如RTS(请求发送)和CTS(清除发送),在通信过程中,发送方无法知道接收方是否准备好接受数据,可能会导致数据溢出或丢失。
-
调试困难:由于SPI是同步通信协议,并且缺乏内置的错误检测和纠错机制,调试SPI通信问题可能比较困难。
-
共享总线限制:在SPI通信中,所有从设备共享相同的总线,即MISO、MOSI和SCLK线。当多个从设备同时通信时,总线上负载增加,可能会导致信号失真和传输延迟。这种共享总线架构限制了系统的扩展和性能。