spi

SPI简介

SPI(Serial Peripheral Interface,串行外设接口)是一种高速、全双工、同步串行通信接口,广泛应用于嵌入式系统中,尤其是STM32微控制器。SPI接口通过主从架构实现单主设备与一个或多个从设备之间的数据交换,适用于连接各种传感器、存储器、显示屏、ADC/DAC等外设。以下是对STM32 SPI的基本概念、工作原理、功能特性、应用示例以及编程配置的详细介绍。

基本概念与工作原理

主从架构:SPI通信系统中有一个主设备(通常是MCU)和一个或多个从设备。主设备负责产生时钟信号(SCK,Serial Clock)并控制通信过程,从设备被动响应主设备的时钟信号。

同步通信:数据传输基于主设备产生的时钟信号同步进行。每个时钟周期传输一位数据,数据采样和发送均在时钟边沿(上升沿或下降沿)进行。

全双工:SPI同时支持数据的双向传输,主设备和从设备可以在同一时刻分别发送和接收数据。

四线制:标准SPI接口通常包含四根信号线:

  • SCK:主设备产生的时钟信号,同步数据传输。
  • MISO(Master In Slave Out):从设备数据输出线,主设备通过该线接收数据。
  • MOSI(Master Out Slave In):主设备数据输出线,从设备通过该线接收数据。
  • SS(Slave Select,有时称为NSS或CS):主设备用于选通某个从设备的片选信号,有效时表明数据传输开始。

模式选择:SPI还定义了四种工作模式(Mode 0-3),由CPOL(Clock Polarity)和CPHA(Clock Phase)两个参数决定时钟信号的极性和相位关系,以适应不同从设备的时序要求。

STM32 SPI功能特性

STM32微控制器通常集成多个SPI接口,每个接口支持以下主要功能:

  • 数据宽度:支持8位、16位数据传输。
  • 工作模式:支持SPI模式0-3,通过软件配置CPOL和CPHA选择。
  • 波特率:通过预分频器设置时钟频率,进而确定数据传输速率。
  • 通信模式:全双工、半双工或单线(单向)通信。
  • 数据交换模式:支持主发送从接收(Full-Duplex TX/RX)、主接收从发送(Full-Duplex RX/TX)、主发送(Simplex TX)和主接收(Simplex RX)模式。
  • 中断与DMA:提供多种中断源(如传输完成、接收溢出、错误等),并支持DMA传输以减轻CPU负担。
  • 多从设备支持:通过多个SS引脚或软件模拟SS信号,控制多个从设备。

应用示例

  • 闪存、EEPROM、FRAM读写:通过SPI接口与非易失性存储器通信,进行数据存储和读取。
  • LCD/OLED显示驱动:与图形液晶或OLED显示屏的控制器通信,发送显示数据和命令。
  • ADC/DAC数据交换:与模数/数模转换器通信,实现模拟信号与数字信号的转换。
  • 传感器数据采集:连接各类传感器(如温湿度、压力、陀螺仪等),接收传感器数据。
  • 通信接口扩展:通过SPI-to-UART、SPI-to-I²C等桥接芯片,扩展其他类型的通信接口。

编程与配置

STM32 SPI接口可通过直接操作寄存器或使用STM32官方提供的HAL库、LL库等进行配置和编程。编程步骤通常包括:

  1. 时钟使能:通过RCC寄存器或库函数启用SPI接口所需的时钟源。

  2. GPIO配置:将SPI相关的SCK、MISO、MOSI和SS(如有)引脚配置为复用开漏(或推挽)输出和浮空输入模式,并连接到对应的SPI外设。

  3. SPI初始化:配置SPI工作参数(模式、数据宽度、波特率、第一字节位序、主从模式、时钟极性/相位等),设置中断/DMA相关参数(如使能中断源、配置DMA通道等)。

  4. 启动SPI:使能SPI,配置SS引脚(硬件或软件控制),准备数据传输。

  5. 数据收发:编写发送数据函数(通过SPI_Transmit等)和接收数据函数(通过SPI_Receive等),或配置DMA传输。

  6. 错误处理:监测并处理通信错误,如CRC错误、 overrun错误、underrun错误等。

注意事项

  • 时钟极性/相位:确保主设备与从设备的CPOL和CPHA设置一致,以保证正确同步。
  • SS信号管理:主设备在每次数据传输前应拉低SS信号选通从设备,传输结束后释放SS信号,使从设备进入待机状态。对于多从设备系统,需要确保每次只选通一个从设备。
  • 数据交换顺序:SPI默认为主设备先发送数据,然后接收从设备数据。若需要改变顺序,可通过软件调整数据缓冲区或使用特定的SPI交换模式。

示例代码

/**
 * @file spi.c
 * @author wenjf
 * @brief stm32 spi通讯示例
 * @version 0.1
 * @date 2019-10-17
 * 
 * @copyright Copyright (c) 2019
 * 
 */
#include "spi.h"
#include <string.h>

/**
 * @brief spi 初始化配置
 * 
 */
void spi_init(void)
{
    GPIO_InitTypeDef gpio_config;
    SPI_InitTypeDef  spi_config;
    RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA, ENABLE );
    RCC_APB2PeriphClockCmd( RCC_APB2Periph_SPI1,  ENABLE );

    // ----------------------------------
    // SPI1_CS -> PA4 (此处单独配置)
    // SPI1_SCK -> PA5
    // SPI1_MISO -> PA6
    // SPI1_MOSI -> PA7
    // ----------------------------------
    gpio_config.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
    gpio_config.GPIO_Mode = GPIO_Mode_AF_PP;
    gpio_config.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &gpio_config);

    GPIO_SetBits(GPIOA,GPIO_Pin_5|GPIO_Pin_6|GPIO_Pin_7);

    // ----------------------------------
    // PA4 -> CS
    // ----------------------------------
    gpio_config.GPIO_Pin = GPIO_Pin_4;
    gpio_config.GPIO_Mode = GPIO_Mode_Out_PP;
    gpio_config.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &gpio_config);
    GPIO_SetBits(GPIOA,GPIO_Pin_4);

    // SPI 外设配置
    spi_config.SPI_Direction = SPI_Direction_2Lines_FullDuplex;		//设置SPI单向或者双向的数据模式:SPI设置为双线双向全双工
    spi_config.SPI_Mode = SPI_Mode_Master;		//设置SPI工作模式:设置为主SPI
    spi_config.SPI_DataSize = SPI_DataSize_8b;	//设置SPI的数据大小:SPI发送接收8位帧结构
    spi_config.SPI_CPOL = SPI_CPOL_Low;		//串行同步时钟的空闲状态为低电平
    spi_config.SPI_CPHA = SPI_CPHA_2Edge;		//串行同步时钟的第2个跳变沿(上升或下降)数据被采样
    spi_config.SPI_NSS = SPI_NSS_Soft;			//NSS信号由硬件(NSS管脚)还是软件(使用SSI位)管理:内部NSS信号有SSI位控制
    spi_config.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_64;	//定义波特率预分频的值:波特率预分频值为64
    spi_config.SPI_FirstBit = SPI_FirstBit_MSB;	//指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始
    spi_config.SPI_CRCPolynomial = 7;			//CRC值计算的多项式
    SPI_Init(SPI1, &spi_config); 				//根据SPI_InitStruct中指定的参数初始化外设SPIx寄存器

    // SPI 使能
    SPI_Cmd(SPI1, ENABLE);
}

/**
 * @brief spi 接口发送
 * 
 * @param sbuf 待发送的数据
 * @param slen 待发送的数据长度
 * @return 0.发送超时 1.发送成功
 * 
 * @notes 根据从设备数据收发情况进行相应的代码修改
 */
uint8_t spi_send(uint8_t * sbuf, uint32_t slen)
{
    uint32_t i = 0;
    uint32_t retry = 0;

    SPI_CS_DN();
    for(i = 0; i < slen; i++)
    {
        retry = 0;
        while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET) //0:发送缓冲非空,等待发送缓冲器变空
        {
            retry++;
            if(retry > 500)
            {
                return 0;
            }
        }
        SPI_I2S_SendData(SPI1, sbuf[i]);
    }

    while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) == SET)
        ;

    SPI_CS_EN();
    return 1;
}

/**
 * @brief spi 数据接收
 * 
 * @param rbuf 数据接收缓存
 * @param rlen 接收到的数据长度
 * @return uint8_t 0.发送超时 1.发送成功
 */
uint8_t spi_recv(uint8_t * rbuf, uint32_t rlen)
{
    uint32_t i = 0;
    uint32_t retry = 0;

    // ------------------------------------------
    // 将数据接收前的第一个字节舍弃,因为是
    // 上一次发来的最后一个字节,还在移位寄存器中
    // 正点原子的处理方式是针对该问题么?
    // 初始先发一个字节FF
    // 随后一发一收,当然了也需要从设备配合
    // ------------------------------------------
    while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET)
        ;
    SPI_I2S_ReceiveData(SPI1);


    SPI_CS_DN();
    for(i = 0; i < rlen; i++)
    {
        retry = 0;
        while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET) //0:发送缓冲非空,等待发送缓冲器变空
        {
            retry++;
            if(retry > 500)
            {
                return 0;
            }
        }
        SPI_I2S_SendData(SPI1, 0x00);
        
        retry = 0;
        while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET)//等待接收数据完成
        {
            retry++;
            if(retry > 500)
            {
                return 0;
            }
        }
        rbuf[i] = SPI_I2S_ReceiveData(SPI1); //返回最近接收的数据,SPI_DR寄存器里面的
    }

    while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) == SET)
        ;

    SPI_CS_EN();
    return 1;
}

/**
 * @brief 简单延时函数
 * 
 * @param n 1大约1ms左右 @默认时钟配置
 */
static void delay(uint32_t n)
{
    volatile uint32_t m;
    while(n--)
    {
        m = 0x00002000;
        while(m--)
            ;
    }
}


#define BUFFER_SIZE		1024
static uint8_t sbuf[BUFFER_SIZE];
static uint8_t rbuf[BUFFER_SIZE];

/**
 * @brief SPI通讯示例
 * 
 */
void spi_demo(void)
{
    uint32_t i = 0;
    uint8_t blink = 0;
    LED_Init();
    spi_init();

    LED_ON();
    delay(3000);
    for(i = 0; i < BUFFER_SIZE; i++)
    {
        sbuf[i] = i & 0xff;
    }
    memset(rbuf,0x00,BUFFER_SIZE);
    LED_OFF();
    delay(1000);

    while(1)
    {
        spi_send(sbuf,BUFFER_SIZE);
        delay(10);
        spi_recv(rbuf,BUFFER_SIZE);
        
        for(i = 0; i < BUFFER_SIZE; i++)
        {
            if(sbuf[i] != rbuf[i])
            {
                // 出错 -> 快闪烁
                while(1)
                {
                    // 正确 -> 慢闪烁
                    if(blink == 0)
                    {
                        blink = 1;
                        LED_ON();
                    }
                    else
                    {
                        blink = 0;
                        LED_OFF();
                    }
                    delay(100);
                }
            }
        }
        
        // 正确 -> 慢闪烁
        if(blink == 0)
        {
            blink = 1;
            LED_ON();
        }
        else
        {
            blink = 0;
            LED_OFF();
        }
        delay(1000);
    }
}