SPI的简介
文章目录
- SPI的简介
- 物理层
- 协议层
- 基本通讯过程
- 起始和终止信号
- 数据有效性
- CPOL/CPHA及通讯模式
- STM3的SPI特性及架构
- 通讯引脚
- 时钟控制逻辑
- 数据控制逻辑
- 整体控制逻辑
- 通讯过程
- 代码配置实现
- 指令集
- 结构体的定义
- SPI时钟信号的定义
- SPI端口定义
- SPI命令
- flash驱动代码
- 初始化代码(配置端口)
- 配置SPI模式代码
- 发送并接受一个字节
- 读取字节
- 读取ID号
- FLASH写入使能
- 等待FLASH内部时序操作完成
- 擦除FLASH指定扇区
物理层
SPI一共三条总线
SPI总线 | 功能 |
---|---|
SS( Slave Select) | 选从机 |
SCK (Serial Clock) | 时钟信号线,用于通讯数据同步。它由通讯主机产生,决定了通讯的速率 |
MOSI (Master Output,Slave Input) | 主设备输出/从设备输入引脚。这条线上数据的方向为主机到从机。 |
MISO (Master Input,,Slave Output) | 主设备输入/从设备输出引脚。在这条线上数据的方向为从机到主机。 |
协议层
基本通讯过程
- NSS为低电平时候才有效
- SCK每一个周期MOSI和MISO传输一位数据
起始和终止信号
- 起始:高变低
- 终止:低变高
数据有效性
- SPI 使用 MOSI 及 MISO 信号线来传输数据,使用 SCK 信号线进行数据同步。MOSI 及 MISO 数据线在 SCK 的每个时钟周期传输一位数据,且数据输入输出是同时进行的。数据传输时,MSB先行或 LSB 先行并没有作硬性规定,但要保证两个 SPI 通讯设备之间使用同样的协定,一般都会采用图 SPI 通讯时序 中的 MSB 先行模式。
- 即在 SCK 的下降沿时刻,MOSI 及 MISO 的数据有效,高电平时表示数据“1”,为低电平时表示数据“0”。在其它时刻,数据无效,MOSI 及 MISO 为下一次表示数据做准备。SPI 每次数据传输可以 8 位或 16 位为单位,每次传输的单位数不受限制。
CPOL/CPHA及通讯模式
CPHA:当 CPHA=0 时,MOSI 或 MISO 数据线上的信号将会在SCK 时钟线的“奇数边沿”被采样。当 CPHA=1 时,数据线在 SCK 的“偶数边沿”采样。一个边沿被设置为采样后另一个边沿只能为读取数据。
CPOL:控制SCK空闲时刻的电平,0为低电平,1为高电平。
STM3的SPI特性及架构
通讯引脚
时钟控制逻辑
由波特率发生器根据“控制寄存器 CR1”中的 BR[0:2] 位控制
数据控制逻辑
SPI 的 MOSI 及 MISO 都连接到数据移位寄存器上,数据移位寄存器的数据来源及目标接收、发送缓冲区以及 MISO、MOSI 线。
当从外部接收数据的时候,数据移位寄存器把数据线采样到的数据一位一位地存储到“接收缓冲区”中。
通过写 SPI 的“数据寄存器 DR”把数据填充到发送缓冲区中,通讯读“数据寄存器 DR”,可以获取接收缓冲区中的内容。
DR[15:0]:数据寄存器 (Data register) 待发送或者已经收到的数据
数据寄存器对应两个缓冲区:一个用于写(发送缓冲);另外一个用于读(接收缓冲)。写操作将数据写到发送缓冲区;读操作将返回接收缓冲区里的数据。
**对SPI模式的注释:**根据SPI_CR1的DFF位对数据帧格式的选择,数据的发送和接收可以是8位或者16位的。为保证正确的操作,需要在启用SPI之前就确定好数据帧格式。
对于8位的数据,缓冲器是8位的,发送和接收时只会用到SPI_DR[7:0]。在接收时,SPI_DR[15:8]被强制为0。
对于16位的数据,缓冲器是16位的,发送和接收时会用到整个数据寄存器,即SPI_DR[15:0]。
**其中数据帧:**长度可以通过“控制寄存器 CR1”的“DFF 位”配置成 8 位及 16 位模式;配置“LSBFIRST 位”可选择 MSB 先行还是 LSB 先行。
整体控制逻辑
整体控制逻辑负责协调整个 SPI 外设,控制逻辑的工作模式根据我们配置的“控制寄存器(CR1/CR2)”的参数而改变基本的控制参数包括前面提到的 SPI 模式、波特率、LSB 先行、主从模式、单双向模式等等。
通讯过程
(1) 控制 NSS 信号线,产生起始信号 (图中没有画出);
(2) 把要发送的数据写入到“数据寄存器 DR”中,该数据会被存储到发送缓冲区;
(3) 通讯开始,SCK 时钟开始运行。MOSI 把发送缓冲区中的数据一位一位地传输出去;MISO 则把数据一位一位地存储进接收缓冲区中;
(4) 当发送完一帧数据的时候,“状态寄存器 SR”中的“TXE 标志位”会被置 1,表示传输完一帧,发送缓冲区已空;类似地,当接收完一帧数据的时候,“RXNE 标志位”会被置 1,表示传输完一帧,接收缓冲区非空;
(5) 等待到“TXE 标志位”为 1 时,若还要继续发送数据,则再次往“数据寄存器 DR”写入数据即可;等待到“RXNE 标志位”为 1 时,通过读取“数据寄存器 DR”可以获取接收缓冲区中的内容。假如我们使能了 TXE 或 RXNE 中断,TXE 或 RXNE 置 1 时会产生 SPI 中断信号,进入同一个中断服务函数,到 SPI 中断服务程序后,可通过检查寄存器位来了解是哪一个事件,再分别进行处理。也可以使用 DMA 方式来收发“数据寄存器 DR”中的数据
代码配置实现
指令集
结构体的定义
SPI时钟信号的定义
#define FLASH_SPIx SPI1
#define FLASH_SPI_APBxClock_FUN RCC_APB2PeriphClockCmd
#define FLASH_SPI_CLK RCC_APB2Periph_SPI1
#define FLASH_SPI_GPIO_APBxClock_FUN RCC_APB2PeriphClockCmd
SPI端口定义
#define FLASH_SPI_SCK_PORT GPIOA
#define FLASH_SPI_SCK_PIN GPIO_Pin_5
#define FLASH_SPI_MOSI_PORT GPIOA
#define FLASH_SPI_MOSI_PIN GPIO_Pin_7
#define FLASH_SPI_MISO_PORT GPIOA
#define FLASH_SPI_MISO_PIN GPIO_Pin_6
#if (USE_BD ==1)
#define FLASH_SPI_GPIO_CLK RCC_APB2Periph_GPIOA
#define FLASH_SPI_CS_PORT GPIOA
#define FLASH_SPI_CS_PIN GPIO_Pin_4
#else
#define FLASH_SPI_GPIO_CLK (RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOC)
#define FLASH_SPI_CS_PORT GPIOC
#define FLASH_SPI_CS_PIN GPIO_Pin_0
#endif
SPI命令
#define DUMMY 0x00
#define READ_JEDEC_ID 0x9f
#define ERASE_SECTOR 0x20
#define READ_STATUS 0x05
#define READ_DATA 0x03
#define WRITE_ENABLE 0x06
#define WRITE_DATA 0x02
flash驱动代码
初始化代码(配置端口)
/**
* @brief SPII/O配置
* @param 无
* @retval 无
*/
static void SPI_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* 使能与SPI 有关的时钟 */
FLASH_SPI_APBxClock_FUN ( FLASH_SPI_CLK, ENABLE );
FLASH_SPI_GPIO_APBxClock_FUN ( FLASH_SPI_GPIO_CLK, ENABLE );
/* MISO MOSI SCK*/
GPIO_InitStructure.GPIO_Pin = FLASH_SPI_SCK_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(FLASH_SPI_SCK_PORT, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MOSI_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(FLASH_SPI_MOSI_PORT, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MISO_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(FLASH_SPI_MISO_PORT, &GPIO_InitStructure);
//初始化CS引脚,使用软件控制,所以直接设置成推挽输出
GPIO_InitStructure.GPIO_Pin = FLASH_SPI_CS_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(FLASH_SPI_CS_PORT, &GPIO_InitStructure);
FLASH_SPI_CS_HIGH;
}
配置SPI模式代码
static void SPI_Mode_Config(void)
{
SPI_InitTypeDef SPI_InitStructure;
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2 ;
//SPI 使用模式3
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge ;
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High ;
SPI_InitStructure.SPI_CRCPolynomial = 0;//不使用CRC功能,数值随便写
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex ;//双线全双工
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB ;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master ;
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft ;
SPI_Init(FLASH_SPIx,&SPI_InitStructure); //写入配置到寄存器
SPI_Cmd(FLASH_SPIx,ENABLE);//使能SPI
}
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge ;
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High ;
这段代码设置了 SPI 的时钟相位(Clock Phase),具体来说,SPI_CPHA_2Edge
表示在第二个时钟沿(第二个边沿)采样数据。让我们分解这个设置:
SPI_CPHA
是 SPI_InitTypeDef 结构体中的一个成员,用于配置 SPI 的时钟相位。SPI_CPHA_2Edge
是一个预定义的常量,它表示在第二个时钟沿(2Edge)采样数据。
时钟相位(Clock Phase)决定了在时钟的哪个边沿数据应该被采样或变更。在 SPI 通信中,时钟相位通常有两个选项:第一个时钟沿(1Edge)和第二个时钟沿(2Edge)。
- 当设置为
SPI_CPHA_1Edge
时,数据在第一个时钟沿(上升沿或下降沿)被采样或变更。 - 当设置为
SPI_CPHA_2Edge
时,数据在第二个时钟沿被采样或变更。
在这个代码片段中,通过设置 SPI_CPHA
为 SPI_CPHA_2Edge
,表明数据在第二个时钟沿被采样。这样的设置通常取决于与 SPI 设备通信的具体协议和要求。
综合一下这段代码是下降沿采样
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB ;
这段代码设置了 SPI 数据传输的起始位。具体来说,SPI_FirstBit_MSB
表示数据传输的起始位是最高有效位(Most Significant Bit,MSB)。
在 SPI 数据传输中,每个字节都由多个位组成,通常是8位。字节中的最高有效位是二进制表示中的最左边的位,而最低有效位则是最右边的位。
通过设置 SPI_FirstBit
为 SPI_FirstBit_MSB
,代码指定了数据传输时先传输最高有效位,然后依次传输剩余的位。这通常符合大多数 SPI 设备和通信协议的约定,但在某些情况下,可能需要根据具体设备的要求进行调整。
发送并接受一个字节
static uint32_t SPI_TIMEOUT_UserCallback(uint8_t errorCode)
{
/* Block communication and all processes */
FLASH_ERROR("SPI 等待超时!errorCode = %d",errorCode);
return 0;
}
uint8_t SPI_FLASH_Send_Byte(uint8_t data)
{
SPITimeout = SPIT_FLAG_TIMEOUT;
//检查并等待至TX缓冲区为空
while(SPI_I2S_GetFlagStatus(FLASH_SPIx,SPI_I2S_FLAG_TXE) == RESET)
{
if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(0);
}
//程序执行到此处,TX缓冲区已空
SPI_I2S_SendData (FLASH_SPIx,data);
SPITimeout = SPIT_FLAG_TIMEOUT;
//检查并等待至RX缓冲区为非空
while(SPI_I2S_GetFlagStatus(FLASH_SPIx,SPI_I2S_FLAG_RXNE) == RESET)
{
if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(0);
}
//程序执行到此处,说明数据发送完毕,并接收到一字字节
return SPI_I2S_ReceiveData(FLASH_SPIx);
//返回的数据是他排出的
}
这段代码涉及 SPI 数据的发送和接收,以下是代码的主要步骤解释:
-
发送数据:
SPI_I2S_SendData(FLASH_SPIx, data);
通过调用
SPI_I2S_SendData
函数,将数据data
发送到 SPI 设备。 -
等待接收缓冲区非空:
SPITimeout = SPIT_FLAG_TIMEOUT; while(SPI_I2S_GetFlagStatus(FLASH_SPIx, SPI_I2S_FLAG_RXNE) == RESET) { if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(0); }
这部分代码在一个循环中检查 SPI 接收缓冲区是否为非空(
SPI_I2S_FLAG_RXNE
表示接收缓冲区非空)。循环会一直等待,直到接收缓冲区有数据或超时。 -
接收数据:
return SPI_I2S_ReceiveData(FLASH_SPIx);
一旦接收缓冲区非空,就调用
SPI_I2S_ReceiveData
函数从 SPI 设备接收数据,并将其返回。
通过 SPI 发送数据,然后等待接收缓冲区非空,最后从接收缓冲区中读取数据。在这个过程中,使用了超时机制来处理可能的等待超时情况。这样的代码结构常见于需要同步发送和接收数据的 SPI 通信场景。
读取字节
uint8_t SPI_FLASH_Read_Byte(void)
{
return SPI_FLASH_Send_Byte(DUMMY);
}
DUMMY可以是任意值,一般是0x00或0xFF,都出来后flash就没有这个数据了(实验得出)
读取ID号
//读取ID号
uint32_t SPI_Read_ID(void)
{
uint32_t flash_id;
//片选使能
FLASH_SPI_CS_LOW;
SPI_FLASH_Send_Byte(READ_JEDEC_ID);
flash_id = SPI_FLASH_Send_Byte(DUMMY);
flash_id <<= 8;
flash_id |= SPI_FLASH_Send_Byte(DUMMY);
flash_id <<= 8;
flash_id |= SPI_FLASH_Send_Byte(DUMMY);
FLASH_SPI_CS_HIGH;
return flash_id;
}
这段代码实现了通过 SPI 读取设备的 ID 号。以下是代码的主要步骤解释:
-
片选使能:
FLASH_SPI_CS_LOW;
通过将片选信号拉低,使能 SPI 设备。
-
发送读取 JEDEC ID 的命令:
SPI_FLASH_Send_Byte(READ_JEDEC_ID);
通过调用
SPI_FLASH_Send_Byte
函数发送读取 JEDEC ID 的命令。 -
读取 ID 号的三个字节:
flash_id = SPI_FLASH_Send_Byte(DUMMY); flash_id <<= 8; flash_id |= SPI_FLASH_Send_Byte(DUMMY); flash_id <<= 8; flash_id |= SPI_FLASH_Send_Byte(DUMMY);
通过调用
SPI_FLASH_Send_Byte
函数,依次读取三个字节的 ID 号。每次读取一个字节,然后将其左移相应的位数,最终组成一个 32 位的 ID 号。 -
片选失能:
FLASH_SPI_CS_HIGH;
通过将片选信号拉高,失能 SPI 设备。
-
返回读取到的 ID 号:
return flash_id;
将读取到的 32 位 ID 号作为函数的返回值。
这段代码通过 SPI 通信协议与外部设备进行交互,发送读取 JEDEC ID 的命令,接着读取返回的三个字节,最终组成一个完整的 32 位 ID 号。这样的操作通常用于识别连接的外部设备或验证设备的身份。
FLASH写入使能
void SPI_Write_Enable(void)
{
//片选使能
FLASH_SPI_CS_LOW;//拉低代表被选中
SPI_FLASH_Send_Byte(WRITE_ENABLE);//写入赋能命令
FLASH_SPI_CS_HIGH;//表示操作完毕
}
在 SPI (Serial Peripheral Interface) 通信中,CS
通常指的是 Chip Select(芯片选择)信号。Chip Select 是一种用于选择特定从设备的信号,它告诉 SPI 总线上的从设备何时应该响应主设备的通信。
等待FLASH内部时序操作完成
void SPI_WaitForWriteEnd(void)
{
uint8_t status_reg = 0;
//片选使能
FLASH_SPI_CS_LOW;
SPI_FLASH_Send_Byte(READ_STATUS);//读取状态命令
do
{
status_reg = SPI_FLASH_Send_Byte(DUMMY);//获得Flash上寄存器的数据
}
while((status_reg & 0x01) == 1);
FLASH_SPI_CS_HIGH;
}
擦除FLASH指定扇区
void SPI_Erase_Sector(uint32_t addr)
{
SPI_Write_Enable();
//片选使能
FLASH_SPI_CS_LOW;
SPI_FLASH_Send_Byte(ERASE_SECTOR);
SPI_FLASH_Send_Byte((addr>>16)&0xff);
SPI_FLASH_Send_Byte((addr>>8)&0xff);
SPI_FLASH_Send_Byte(addr&0xff);
FLASH_SPI_CS_HIGH;
SPI_WaitForWriteEnd();
}
SPI_FLASH_Send_Byte(ERASE_SECTOR);
写入擦除命令。
SPI_FLASH_Send_Byte((addr>>16)&0xff);
SPI_FLASH_Send_Byte((addr>>8)&0xff);
SPI_FLASH_Send_Byte(addr&0xff);
地址由三个字节组成所以用这种方法。
读取和写
//读取FLASH的内容
void SPI_Read_Data(uint32_t addr,uint8_t *readBuff,uint32_t numByteToRead)
{
//片选使能
FLASH_SPI_CS_LOW;
SPI_FLASH_Send_Byte(READ_DATA);
SPI_FLASH_Send_Byte((addr>>16)&0xff);
SPI_FLASH_Send_Byte((addr>>8)&0xff);
SPI_FLASH_Send_Byte(addr&0xff);
while(numByteToRead--)
{
//这段代码是为了激活时钟信号
*readBuff = SPI_FLASH_Send_Byte(DUMMY);
readBuff++;
}
FLASH_SPI_CS_HIGH;
}
//向FLASH写入内容
//读取FLASH的内容
//写入数据前都要擦除
void SPI_Write_Data(uint32_t addr,uint8_t *writeBuff,uint32_t numByteToWrite)
{
SPI_Write_Enable();
//片选使能
FLASH_SPI_CS_LOW;
SPI_FLASH_Send_Byte(WRITE_DATA);
SPI_FLASH_Send_Byte((addr>>16)&0xff);
SPI_FLASH_Send_Byte((addr>>8)&0xff);
SPI_FLASH_Send_Byte(addr&0xff);
while(numByteToWrite--)
{
SPI_FLASH_Send_Byte(*writeBuff);
writeBuff++;
}
FLASH_SPI_CS_HIGH;
SPI_WaitForWriteEnd();
}
写入数据前都要擦除!
写入数据前都要擦除!
写入数据前都要擦除!
因为SPI中数据默认为0xFF,需要自己去擦除,不会在写入时自动擦除!
取FLASH的内容