016 - STM32学习笔记 - SPI读写FLASH(一)

016 - STM32学习笔记 - SPI访问Flash(一)

之前csdn的名称是宥小稚,后来改成放学校门口见了,所以前面内容看到图片水印不要在意,都是自己学习过程中整理的,不涉及版权啥的。

1、什么是SPI?

SPI 协议是由摩托罗拉公司提出的通讯协议(Serial Peripheral Interface),即串行外围设备接口,是一种高速全双工的通信总线。

016 - STM32学习笔记 - SPI读写FLASH(一)_第1张图片

在SPI总线中,一共有四条线:

SCLK:同步时钟信号线,用于通讯数据同步。由通讯主机产生,决定了通讯的速率,不同的设备支持的最高时钟频率不一样,如 STM32 的 SPI 时钟频率最大为fpclk/2,两个设备之间通讯时,通讯速率由最低速率设备决定。

CS:片选信号线,或设备选择线,也称为SS、NSS,当多个SPI从机设备与SPI主机相连时,设备的其他信号线SCLK、MOSI、MISO同时并联到相同的SPI总线上,无论多少个从机设备,这三条总线都是公用的,唯独CS线都是从机设备与主机设备的一对一连接,SPI总线上有多少从机设备,就有多少根CS线。I2C中主机通过设备地址来寻址进行通讯;SPI中没有设备地址,只使用CS信号线进行片选,当主机要与某个从机设备进行通信时,则将该从机设备的CS信号线设置为低电平,就表示该设备被选中,接着就可以开始通讯了,当通讯结束后,CS线被拉高,则表示结束信号。

MOSI:Master Output,Slave Input,主设备输出/从设备输入引脚。主机的数据从这条信号线输出,从机由这条信号线读入主机发送的数据,数据方向为主机到从机

MISO:Master Input,Slave Output,主设备输入/从设备输出引脚。主机从这条信号线读入数据,从机的数据由这条信号线输出到主机,数据方向为从机到主机

2、SPI协议

016 - STM32学习笔记 - SPI读写FLASH(一)_第2张图片

SPI通讯过程

在SPI通讯过程中,CS、SCK、MOSI信号都是由主机产生,MISO信号是由从机产生,其中MOSI和MISO信号只有在CS片选信号为低电平时有效,在SCLK每个信号周期,MOSI和MISO都可以传输一位数据,因此SPI总线可以同时收发数据。

a、通讯起始与结束信号:

根据SPI通讯过程的图来看,当SPI总线无通信时,CS片选信号线一直保持高电平,当开始通信时,CS信号线跳变为低电平,此时SCLK、MOSI、MISO信号线上的信号才有效,通信结束后,CS跳变为高电平,表示此次通信结束了。

b、有效数据:

在SPI中,当CS片选信号低电平时,MOSI和MISO数据线在SCLK的每个时钟周期都会传输一位数据,并且MOSI和MISO上的信号时同时进行传输的,MSB(高位)先行或LSB(地位)先行并没有硬性规定,但要保证两个SPI设备之间使用同样的协定,就跟上图一样,采用MSB先行模式了。

c、CPOL/CPHA

下面要学习关于SPI的四种通讯模式,上图中的时序时SPI的其中一种通讯模式,四种通讯模式的主要区别在于总线空闲时,SCLK的时钟状态以及数据采样时刻。在此之前先看一下CPOL(时钟极性)和CPHA(时钟相位)。

CPOL:是指SPI通讯设备处于空闲状态时,SCK信号线的的电平信号(就是CS片选信号是高电平时,SCLK的状态),当CS信号线为高电平时,CPOL = 0,SCLK则为低电平;CPOL = 1,SCLK则为高电平;

CPHA:时钟相位实际指的就是数据采样的时间,当CPHA = 0的时候,采样信号在SCLK时钟线的奇数边沿,当CPHA = 1的时候,采样信号则是在SCLK时钟线的偶数边沿。

016 - STM32学习笔记 - SPI读写FLASH(一)_第3张图片

CPHA = 0

016 - STM32学习笔记 - SPI读写FLASH(一)_第4张图片

CPHA = 1

所以根据CPOL和CPHA的不同状态可以组合出四种模式,如下表

模式 CPOL CPHA 空闲时SCLK 采样时刻
0 0 0 低电平 奇数边沿
1 0 1 低电平 偶数边沿
2 1 0 高电平 奇数边沿
3 1 1 高电平 偶数边沿

在实际应用中,模式0模式3 用的比较多。

3、SPI框图

016 - STM32学习笔记 - SPI读写FLASH(一)_第5张图片

a、SPI引脚

前面已经介绍过SPI的四个引脚了,这里不多做赘述,主要看一下四个引脚在F429里面的分布,F429中,SPI一共由6组,其中SPI1、SPI4、SPI5、SPI6挂载在APB2总线上,最高通信速率可以达到45MBtis/s,SPI2、SPI3是挂载在APB1上,最高通信速率为22.5MBtis/s。

016 - STM32学习笔记 - SPI读写FLASH(一)_第6张图片

引脚 SPI1 SPI2 SPI3 SPI4 SPI5 SPI6
MOSI PA7/PB5 PB15/PC3/PI3 PB5/PC12/PD6 PE6/PE14 PF9/PF11 PG14
MISO PA6/PB4 PB14/PC2/PI2 PB4/PC11 PE5/PE13 PF8/PH7 PG12
SCK PA5/PB3 PB10/PB13/PD3 PB3/PC10 PE2/PE12 PF7/PH6 PG13
NSS PA4/PA15 PB9/PB12/PI0 PA4/PA15 PE4/PE11 PF6/PH5 PG8
SPI引脚表

b、时钟控制逻辑

SPI的时钟信号由SCLK线发出,内部通过波特率发生器根据控制寄存器CR1中的BR[2:0]位控制,该位是对fpclk时钟的分频因子,对fpclk的分频结果就是SCLK引脚输出的频率。

016 - STM32学习笔记 - SPI读写FLASH(一)_第7张图片

BR[2:0] 分频结果(SCLK频率) BR[2:0] 分频结果(SCLK频率)
000 fpclk/2 100 fpclk/32
001 fpclk/4 101 fpclk/64
010 fpclk/8 110 fpclk/128
011 fpclk/16 111 fpclk/256

c、数据控制逻辑

在SPI框图中可以看到,SPI的MOSI和MISO都是连接到数据移位寄存器上,而数据移位寄存器中的数据,来源于接收缓冲区、发送缓冲区以及MOSI和MISO线,向外发送数据时,数据向外发送时,数据移位寄存器将“发送缓冲区”的数据作为数据源,将数据按位发送出去;接收数据时,数据移位寄存器从MISO数据线上将数据按位通过“数据移位寄存器”采集到“接收缓冲区”。

此处需要注意,数据帧的长度可以通过控制寄存器CR1DFF[11]位配置成8位或16位模式,通过配置“LSBFIRST [7]位可选择 MSB 先行还是 LSB 先行。

4、SPI结构体

SPI结构体声明在stm32f4xx_spi.h中,内容如下:

typedef struct
{
    uint16_t SPI_Direction;           /* SPI单、双向模式 */
    uint16_t SPI_Mode;                /* SPI主/从机模式 */
    uint16_t SPI_DataSize;            /* SPI数据帧长度 */
    uint16_t SPI_CPOL;                /* 时钟极性CPOL设置,可选高/低电平 */
    uint16_t SPI_CPHA;                /* 时钟相位CPHA设置,可选奇/偶边沿采样 */
    uint16_t SPI_NSS;                 /* 设置CS引脚由SPI硬件控制还是软件控制 */
    uint16_t SPI_BaudRatePrescaler;   /* 设置时钟分频因子,fpclk/分频=fsck */
    uint16_t SPI_FirstBit;            /* 设置MSB/LSB先行 */
    uint16_t SPI_CRCPolynomial;       /* 设置CRC校验的表达式 */
}SPI_InitTypeDef;

a、SPI单双向选择(SPI_Direction):

在固件库中提供了以下四种模式

  • 双线全双工(SPI_Direction_2Lines_FullDuplex)
  • 双线只接收(SPI_Direction_2Lines_RxOnly)
  • 单线只接收(SPI_Direction_1Line_Rx)
  • 单线只发送模式(SPI_Direction_1Line_Tx)

b、SPI片选信号控制选择(SPI_NSS)

可选有两种模式:

  • 硬件模式(SPI_NSS_Hard )
  • 软件模式(SPI_NSS_Soft )

其中硬件模式中的片选信号是由SPI硬件自动产生,而软件模式则是由用户将GPIO的引脚电平拉高或者拉低产生信号,常用的为软件模式。

5、编程测试

016 - STM32学习笔记 - SPI读写FLASH(一)_第8张图片

在F429的核心板上可以看到,PF6、7、8、9引脚分别对应SPI的CS(片选)、CLK(时钟)、MOSI(输入)、MISO(输出)功能,另外Flash还有两个引脚WP(写保护控制)和HOLD(暂停通讯控制),当WP为低电平时,禁止数据写入,HOLD为低电平时,通讯暂停,MISO输出为高阻态,时钟和输入无效,这里用不到,所以直接接电源拉成高电平。

相关宏定义,在bsp_spi_flash.h中

//SPI 号及时钟初始化函数
#define FLASH_SPI                       SPI5									//根据SPI引脚表可以看到PF6-9都是挂载在SPI5下
#define FLASH_SPI_CLK                   RCC_APB2Periph_SPI5						 //SPI5时钟
//SCK 引脚PF7
#define FLASH_SPI_SCK_PIN               GPIO_Pin_7
#define FLASH_SPI_SCK_GPIO_PORT         GPIOF
#define FLASH_SPI_SCK_GPIO_CLK          RCC_AHB1Periph_GPIOF
#define FLASH_SPI_SCK_PINSOURCE         GPIO_PinSource7
#define FLASH_SPI_SCK_AF                GPIO_AF_SPI5
//MISO 引脚PF8
#define FLASH_SPI_MISO_PIN              GPIO_Pin_8
#define FLASH_SPI_MISO_GPIO_PORT        GPIOF
#define FLASH_SPI_MISO_GPIO_CLK         RCC_AHB1Periph_GPIOF
#define FLASH_SPI_MISO_PINSOURCE        GPIO_PinSource8
#define FLASH_SPI_MISO_AF               GPIO_AF_SPI5
//MOSI 引脚PF9
#define FLASH_SPI_MOSI_PIN              GPIO_Pin_9
#define FLASH_SPI_MOSI_GPIO_PORT        GPIOF
#define FLASH_SPI_MOSI_GPIO_CLK         RCC_AHB1Periph_GPIOF
#define FLASH_SPI_MOSI_PINSOURCE        GPIO_PinSource9
#define FLASH_SPI_MOSI_AF               GPIO_AF_SPI5
//CS(NSS)引脚PF6
#define FLASH_CS_PIN                    GPIO_Pin_6
#define FLASH_CS_GPIO_PORT              GPIOF
#define FLASH_CS_GPIO_CLK               RCC_AHB1Periph_GPIOF

#define SPI_FLASH_CS_LOW()              {FLASH_CS_GPIO_PORT->BSRRH=FLASH_CS_PIN;} 	/*控制CS片选信号输出低电平*/
#define SPI_FLASH_CS_HIGH()             {FLASH_CS_GPIO_PORT->BSRRL=FLASH_CS_PIN;}	/*控制CS片选信号输出高电平*/

相关GPIO配置及SPI的配置和初始化

void SPI_GPIO_Config(void)
{
    GPIO_InitTypeDef SPI_GPIO_Initstructure;
    SPI_InitTypeDef SPI_InitStructure;
    /*  SPI四个引脚时钟使能,只要是外设,第一步一定是开启时钟!!! */
    RCC_AHB1PeriphClockCmd(FLASH_SPI_SCK_GPIO_CLK | FLASH_SPI_MISO_GPIO_CLK|FLASH_SPI_MOSI_GPIO_CLK|FLASH_CS_GPIO_CLK, ENABLE);
    /* 将SCLK、MISO、MOSI引脚都连接到复用功能,CS片选信号由软件控制,不需要 */
    GPIO_PinAFConfig(FLASH_SPI_SCK_GPIO_PORT,FLASH_SPI_SCK_PINSOURCE,FLASH_SPI_SCK_AF);
    GPIO_PinAFConfig(FLASH_SPI_MISO_GPIO_PORT,FLASH_SPI_MISO_PINSOURCE,FLASH_SPI_MISO_AF);
    GPIO_PinAFConfig(FLASH_SPI_MOSI_GPIO_PORT,FLASH_SPI_MOSI_PINSOURCE,FLASH_SPI_MOSI_AF);
    /* 配置SCLK信号引脚 */
    SPI_GPIO_Initstructure.GPIO_Pin = FLASH_SPI_SCK_PIN;
    SPI_GPIO_Initstructure.GPIO_Speed = GPIO_Speed_50MHz;
    SPI_GPIO_Initstructure.GPIO_Mode = GPIO_Mode_AF;
    SPI_GPIO_Initstructure.GPIO_OType = GPIO_OType_PP;
    SPI_GPIO_Initstructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
    /* 初始化SCLK引脚 */
    GPIO_Init(FLASH_SPI_SCK_GPIO_PORT, &SPI_GPIO_Initstructure);
    /* 配置并初始化MISO引脚 */
    SPI_GPIO_Initstructure.GPIO_Pin = FLASH_SPI_MISO_PIN;
    GPIO_Init(FLASH_SPI_MISO_GPIO_PORT, &SPI_GPIO_Initstructure);
    /* 配置并初始化MOSI引脚 */
    SPI_GPIO_Initstructure.GPIO_Pin = FLASH_SPI_MOSI_PIN;
    GPIO_Init(FLASH_SPI_MOSI_GPIO_PORT, &SPI_GPIO_Initstructure);
    /* 配置CS引脚为输出模式并初始化,这里片选由软件自行控制,因此此处模式选择为输出 */
    SPI_GPIO_Initstructure.GPIO_Pin = FLASH_CS_PIN;
    SPI_GPIO_Initstructure.GPIO_Mode = GPIO_Mode_OUT;
    GPIO_Init(FLASH_CS_GPIO_PORT, &SPI_GPIO_Initstructure);
    /* 将CS片选信号置位高电平,非选中状态 */
    SPI_FLASH_CS_HIGH();
    /*------------------------以下为SPI配置内容------------------------*/
    /* SPI时钟使能,只要是外设,第一步一定是开启时钟!!! */
    /* PF6-9是挂载在SPI5上,因此使能SPI5的时钟 */
    RCC_APB2PeriphClockCmd(FLASH_SPI_CLK, ENABLE);
    /* 配置SPI为双线全双工 */
    SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
    /* 配置SPI为主机模式 */
    SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
    /* 设置数据帧为8bit */
    SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
    /* 设置时钟极性为高电平 */
    SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;
    /* 设置时钟相位为偶采样 */
    SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;
    /* 设置片选信号由软件控制 */
    SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
    /* 设置为2分频 */
    /*F429的主频是180MHz,那么APB2的时钟fpclk1就是90MHz,而SP1、4、5、6最高频率为45MHz,因此需要2分频;
    同理如果要用到SPI2、3,最高频率为22.5MHz,	而SPI2、3挂载在AP1,总线时钟为45MHz,如果要达到最高速度,同样要2分频
    需要注意的是,此处SPI的最大速度取决于总线中设备通讯速率最低的设备 */
    SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2;
    /* 设置MSB先行 */
    SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
    /* 设置CRC校验多项式,保证通信质量,大于1就行 */
    SPI_InitStructure.SPI_CRCPolynomial = 7;
    /* 初始化SPI */
    SPI_Init(FLASH_SPI, &SPI_InitStructure);
    /* 使能 FLASH_SPI */
    SPI_Cmd(FLASH_SPI, ENABLE);
}

首先测试一下通过SPI读取FALSH的Device_ID号,根据FALSH手册,可以看到设备ID是0xEF4018,如下图

016 - STM32学习笔记 - SPI读写FLASH(一)_第9张图片

另外在看一下SPI的指令表,在指令表中,可以看到查看FALSH的指令为0x9F:

016 - STM32学习笔记 - SPI读写FLASH(一)_第10张图片

如此根据指令表编写读取FALSH的设备ID,可以看到,读取设备ID时,需要下发指令0x9F,之后Byte2-Byte4中的内容组合则为设备ID,时序如下图:

016 - STM32学习笔记 - SPI读写FLASH(一)_第11张图片

u32 SPI_FLASH_ReadID(void)
{
    u8 temp[3] = {0x00,0x00,0x00};
    /* 开始通讯:拉低片选信号CS */
    SPI_FLASH_CS_LOW();
    /* 发送 JEDEC 指令,读取 ID */
    SPI_FLASH_SendByte(0x9F);
    /* 读取一个字节数据 */
    temp[0] = SPI_FLASH_SendByte(Dummy_Byte);
    /* 读取一个字节数据 */
    temp[1] = SPI_FLASH_SendByte(Dummy_Byte);
    /* 读取一个字节数据 */
    temp[2] = SPI_FLASH_SendByte(Dummy_Byte);
    /* 停止通讯:拉高片选信号CS */
    SPI_FLASH_CS_HIGH();
    /* 组合数据返回 */
    return (temp[0] << 16) | (temp[1] << 8) | temp[2];
}

这段程序中,如果直接使用库函数SPI_I2S_ReceiveData去读取数据的话,发现读取回来的数据不是我们要的0xEF4018,因为在每次数据发送完成后,需要等待发送缓冲区为空时,才可以发送下一个要发送的数据,因此跟I2C的一样,我们需要去检测一下发送缓冲区是否为空,当发送缓冲区为空时,硬件会将TXE标志置1,因此需要调用SPI_GetFlagStatus去获取TXE状态,为此在这里实现SPI_FLASH_SendByte来尝试读取设备ID:

u8 SPI_FLASH_SendByte(u8 byte)
{
    SPITimeout = SPIT_FLAG_TIMEOUT;
    /* 等待发送缓冲区为空, TXE 事件 */
    while (SPI_GetFlagStatus(FLASH_SPI, SPI_I2S_FLAG_TXE) == RESET)
    {
        if ((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(0);
    }
    /* 写入数据寄存器,把要写入的数据写入发送缓冲区 */
    SPI_I2S_SendData(FLASH_SPI, byte);
    SPITimeout = SPIT_FLAG_TIMEOUT;
    /* 等待接收缓冲区非空, RXNE 事件 */
    while (SPI_GetFlagStatus(FLASH_SPI, SPI_I2S_FLAG_RXNE) == RESET)
    {
        if ((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(1);
    }
    /* 读取数据寄存器,获取接收缓冲区数据 */
    return SPI_I2S_ReceiveData(FLASH_SPI);
}

实现方法有点类似I2C,代码大家看一下I2C那节的内容就能理解,关于超时检测和SPI_TIMEOUT_UserCallback回调函数的实现也可以看一下I2C那节的内容。

到这里在到main函数中测试一下:

#include "stm32f4xx.h"
#include "bsp_led.h"
#include "bsp_systick.h"
#include "bsp_usart_dma.h"
#include "bsp_spi_flash.h"
#include 

int main(void)
{
    u32 device_id = 0;
    LED_Config();
    DEBUG_USART1_Config();
    SysTick_Init();  
    SPI_GPIO_Config();
    printf("这是SPI读取FLASH_Device_ID的测试实验!\n");
    device_id = SPI_FLASH_ReadID();
    printf("device_id = 0x%X\n",device_id);
    while(1)
    {
        if(device_id == 0xef4018)
        {
            LED_G_TOGGLE
            Delay_ms(1000);
        }
        else
        {
            LED_R_TOGGLE
            Delay_ms(1000);
        }
    }
}

016 - STM32学习笔记 - SPI读写FLASH(一)_第12张图片

OK,这节内容学习完毕!

根据上述内容,总给下SPI的操作顺序:

1、配置SPI对应的四个GPIO;(只要是外设,第一步一定要先打开时钟!!!)

2、将三个GPIO连接到SPI复用功能(CS引脚我们用软件自己控制,就不需要连接了!)

3、配置SPI相关参数(模式按照模式0或者模式3来配置,速度按照SPI最大速度来执行,记得要打开SPI的时钟!)

4、编写发送和读取数据的功能函数。

你可能感兴趣的:(stm32,stm32,学习,笔记)