一、SPI通信协议介绍

(一)SPI基本概念

        SPI(Serial Peripheral Interface),即串行外围设备接口,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,正是出于这种简单易用的特性,越来越多的芯片集成了这种通信协议。其工作模式有两种:主模式和从模式。SPI是一种允许一个主设备启动一个从设备的同步通讯的协议,从而完成数据的交换。也就是SPI是一种规定好的通讯方式。这种通信方式的优点是占用端口较少,一般4根就够基本通讯了(不算电源线)。同时传输速度也很高。一般来说要求主设备要有SPI控制器(也可用模拟方式),就可以与基于SPI的芯片通讯。

(二)SPI通讯模型

箭头表示数据的流向。

(三)SPI特性

1、SPI主机通过4根线和从机进行通信:

  • MISO: 主机发送从机接收。
  • MOSI:从机发送主机接收。
  • SCK(SCLK):时钟信号,由主机产生。
  • NSS:片选信号,由主机发送,选择与那个从机通信,一般为低电平位有效信号。

2、时钟极性(CPOL):

  • CPOL = 0 :时钟空闲时为低电平 0
  • CPOL = 1 :时钟空闲时为高电平 1

3、时钟相位(CPHA):

  • CPHA = 0 :在时钟SCK的第一个跳变沿采样
  • CPHA = 1 :在时钟SCK的第二个跳变沿采样

4、SPI通讯模式

SPI MODE CPOL CPHA
MODE0 :00 0 0
MODE1 :01 0 1
MODE2 :10 1 0
MODE3 :11 1 1

(四)SPI的优缺点

1、优点

  • 全双工串行通信
  • 传输速度快
  • 极其灵活的数据传输,不限于8位,它可以是任意大小的字
  • 结构简单,实现简单

2、缺点

  • 传输距离有限
  • 使用四根信号线(I2C和UART使用两根信号线)
  • 无法确认是否已成功接收数据(I2C拥有此功能)
  • 没有任何形式的错误检查,如UART中的奇偶校验位
  • 只允许一个主设备
  • 没有定义硬件级别的错误检查协议

二、STM32的SPI

(一)SPI框图

(二)STM32的SPI配置流程

  1. SPI工作参数配置: HAL_SPI_Init()
  2. 使能SPI时钟和初始化相关引脚:        HAL_SPI_MspInit()
  3. 使能SPI: __HAL_SPI_ENABLE()
  4. SPI传输数据

(三)初始化相关重要结构体

//1、SPI handle Structure definition
typedef struct __SPI_HandleTypeDef{
    SPI_TypeDef    *Instance; //基地址
    SPI_InitTypeDef    Init; //初始化结构体
    uint8_t    *pTxBuffPtr; //发送缓存
    uint16_t    TxXferSize;/发送数据大小
    uint16_t    TxXferCount;//还剩余多少个数据要发送
    uint8_t    *pRxBuffPtr;//接收缓存
    uint16_t    RxXferSize;//接收数据大小
    uint16_t    RxXferCount;//还剩余多少个数据要接收
    DMA_HandleTypeDef    *hdmatx;//DMA 发送句柄
    DMA_HandleTypeDef    *hdmarx;//DMA 接收句柄
    void    (*RxISR)(struct __SPI_HandleTypeDef * hspi);
    void    (*TxISR)(struct __SPI_HandleTypeDef * hspi);
    HAL_LockTypeDef    Lock;
    __IO HAL_SPI_StateTypeDef State;
    __IO uint32_t    ErrorCode;
}SPI_HandleTypeDef;

//1、SPI Configuration Structure definition
typedef struct {
    uint32_t Mode;    // 模式:主(SPI_MODE_MASTER),从(SPI_MODE_SLAVE)
    uint32_t Direction;//方式:只接受模式,单线双向通信数据模式,全双工
    uint32_t DataSize; //8 位还是 16 位帧格式选择项
    uint32_t CLKPolarity; //时钟极性
    uint32_t CLKPhase; //时钟相位
    uint32_t NSS; //SS 信号由硬件(NSS 管脚)还是软件控制
    uint32_t BaudRatePrescaler;//SS 信号由硬件(NSS 管脚)还是软件控制
    uint32_t FirstBit; //SS 信号由硬件(NSS 管脚)还是软件控制
    uint32_t TIMode;//SS 信号由硬件(NSS 管脚)还是软件控制
    uint32_t CRCCalculation; //SS 信号由硬件(NSS 管脚)还是软件控制
    uint32_t CRCPolynomial;//CRC 多项式
} SPI_InitTypeDef;

(四)初始化配置代码

SPI_HandleTypeDef hspi1;
void SPI_Init(void){

    hspi1.Instance = SPI1;
    hspi1.Init.Mode = SPI_MODE_MASTER;      //主机模式
    hspi1.Init.Direction = SPI_DIRECTION_2LINES;    //双线:全双工
    hspi1.Init.DataSize = SPI_DATASIZE_8BIT;    //8位
    hspi1.Init.CLKPolarity = SPI_POLARITY_HIGH; //空闲时为高电平
    hspi1.Init.CLKPhase = SPI_PHASE_2EDGE;  //(第二个)偶数个跳变沿采样数据
    hspi1.Init.NSS = SPI_NSS_SOFT;      //片选CS由软件控制
    hspi1.Init.TIMode = SPI_TIMODE_DISABLE; //不使用TI模式
    hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; //数据MSB,高位优先
    hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2;   //定义波特率预分频的值:波特率预分频值为2,最高速度42Mhz
    hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; //关闭硬件CRC校验
    hspi1.Init.CRCPolynomial = 7;

    if(HAL_SPI_Init(&hspi1) != HAL_OK){
        printf("HAL_SPI_Init error \r\n");
        Error_Handler();
    } 

    __HAL_SPI_ENABLE(&hspi1);     //使能SPI

}

//SPI初始化回调函数
void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi){
    if (hspi->Instance == SPI1){
        GPIO_InitTypeDef GPIO_InitStruct = {0};
        //初始化SPI1
        __HAL_RCC_SPI1_CLK_ENABLE();    //使能SPI1时钟
        __HAL_RCC_GPIOB_CLK_ENABLE();   //使能GPIO时钟
        
        //初始化SCK、MISO、MOSI引脚
        GPIO_InitStruct.Pin = GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5;
        GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
        GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
        GPIO_InitStruct.Alternate = GPIO_AF5_SPI1;
        HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

        //初始化CS引脚
        GPIO_InitStruct.Pin = GPIO_PIN_14;
        GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
        GPIO_InitStruct.Pull = GPIO_PULLUP;
        GPIO_InitStruct.Speed =  GPIO_SPEED_FREQ_HIGH;
        HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

        
    }
}

(五)STM32的SPI读写函数(基于HAL库)

//1、SPI发送
HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi,
                                   const uint8_t *pData, uint16_t Size,
                                   uint32_t Timeout);
//2、SPI读数据
HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData,
                                  uint16_t Size, uint32_t Timeout);
//3、SPI读写数据,同时读和写
HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi,
                                          const uint8_t *pTxData,
                                          uint8_t *pRxData, uint16_t Size,
                                          uint32_t Timeout);

/*
HAL_XXX_Transmit_IT : 类似这种以_IT结尾的就是以中断的方式发送或接收
HAL_XXX_Transmit_DMA : 类似这种以_DMA结尾的是以DMA方式发送或者接收
*/

三、SPI使用---读取W25Q128芯片的芯片ID

(一)W25Qxxx芯片介绍

        W25Q128是Winbond公司推出的128Mb(16MB)容量NOR Flash存储器芯片,支持SPI/QSPI接口,广泛应用于嵌入式系统和数据存储领域。

        XXX代表芯片的容量为xxxMb。

(二)SPI时序

        W25Q128支持spi MODE0(空闲时时钟为低电平,奇数个跳变沿采样)和MODE3(空闲时时钟为高电平,偶数个跳变沿采样)两种模式。

读写命令表:

查看芯片手册:读取芯片ID的时序为:发送0x90,紧接着两个无效字符,一个0x00,在接收一个字节的厂商ID,一个字节的芯片ID。

(三)代码实现读取芯片ID

uint16_t Flash_ReadID(void){
	uint16_t ID = 0;
	uint8_t	byteData=0;
	uint8_t CMD=0x90;

    //CS = 0 : 开始通信
	HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_RESET);
	HAL_SPI_Transmit(&hspi1, &CMD, 1, 200);    //发送0x90
	CMD=0x00;
	HAL_SPI_Transmit(&hspi1, &CMD, 1, 200);    //第一个无效字节
	HAL_SPI_Transmit(&hspi1, &CMD, 1, 200);    //第二个无效字节
	HAL_SPI_Transmit(&hspi1, &CMD, 1, 200);    //发送0x00
	
	HAL_SPI_Receive(&hspi1, &byteData, 1, 200);    //接收厂商ID
	ID=byteData;
	ID=ID<<8;//Manufacturer ID
	
	HAL_SPI_Receive(&hspi1, &byteData, 1, 200);    //接收芯片ID
	ID|=byteData;	//Device ID
	
    //CS = 1: 结束通信
	HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_SET);

	return ID;
}
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐