016 - STM32学习笔记 - SPI访问Flash(一)
之前csdn的名称是宥小稚,后来改成放学校门口见了,所以前面内容看到图片水印不要在意,都是自己学习过程中整理的,不涉及版权啥的。
1、什么是SPI?
SPI 协议是由摩托罗拉公司提出的通讯协议(Serial Peripheral Interface),即串行外围设备接口,是一种高速全双工的通信总线。
在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协议
在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时钟线的偶数边沿。
所以根据CPOL和CPHA的不同状态可以组合出四种模式,如下表
模式 | CPOL | CPHA | 空闲时SCLK | 采样时刻 |
---|---|---|---|---|
0 | 0 | 0 | 低电平 | 奇数边沿 |
1 | 0 | 1 | 低电平 | 偶数边沿 |
2 | 1 | 0 | 高电平 | 奇数边沿 |
3 | 1 | 1 | 高电平 | 偶数边沿 |
在实际应用中,模式0 和 模式3 用的比较多。
3、SPI框图
a、SPI引脚
前面已经介绍过SPI的四个引脚了,这里不多做赘述,主要看一下四个引脚在F429里面的分布,F429中,SPI一共由6组,其中SPI1、SPI4、SPI5、SPI6挂载在APB2总线上,最高通信速率可以达到45MBtis/s,SPI2、SPI3是挂载在APB1上,最高通信速率为22.5MBtis/s。
引脚 | 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 |
b、时钟控制逻辑
SPI的时钟信号由SCLK线发出,内部通过波特率发生器根据控制寄存器CR1中的BR[2:0]位控制,该位是对fpclk时钟的分频因子,对fpclk的分频结果就是SCLK引脚输出的频率。
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数据线上将数据按位通过“数据移位寄存器”采集到“接收缓冲区”。
此处需要注意,数据帧的长度可以通过控制寄存器CR1的DFF[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、编程测试
在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,如下图
另外在看一下SPI的指令表,在指令表中,可以看到查看FALSH的指令为0x9F:
如此根据指令表编写读取FALSH的设备ID,可以看到,读取设备ID时,需要下发指令0x9F,之后Byte2-Byte4中的内容组合则为设备ID,时序如下图:
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 <stdio.h>
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);
}
}
}
OK,这节内容学习完毕!
根据上述内容,总给下SPI的操作顺序:
1、配置SPI对应的四个GPIO;(只要是外设,第一步一定要先打开时钟!!!)
2、将三个GPIO连接到SPI复用功能(CS引脚我们用软件自己控制,就不需要连接了!)
3、配置SPI相关参数(模式按照模式0或者模式3来配置,速度按照SPI最大速度来执行,记得要打开SPI的时钟!)
4、编写发送和读取数据的功能函数。