[STM32 – 野火] – – – 固件库学习笔记 – – -九.SPI
1 SPI协议简介
SPI(Serial Peripheral Interface,串行外围设备接口)是一种高速、全双工、同步的串行通信接口,它由Motorola公司于20世纪80年代开发。主要用于微控制器和其他外围设备之间的通信,比如传感器、存储器、ADC、DAC等。
SPI协议因其简单、高效和易于实现,在嵌入式系统中非常流行。不过,它也有一些局限性,比如它不支持设备寻址,所有设备共享相同的通信路线,因此不适合想要多个主设备的应用。
1.1 物理层
SPI通讯设备之间的常见连接方式如下:
在物理层,SPI协议使用以下几种信号线:
主机从这条信号线读入数据,从机的数据由这条信号线输出到主机。
主机的数据从这条信号线输出,从机由这条信号线读入主机发送的数据。
它由通讯主机产生,决定了通讯的速率,不同的设备支持的最高时钟频率不一样,两个设备之间通讯时,通讯速率受限于低速设备。
当SS线为低电平时,从设备被选中,可以参与通信;当SS先为高电平时,从设备被禁用,无法进行通信。(当SS为高电平时,从设备会忽略三根线上传输的数据)
当有多个 SPI 从设备与 SPI 主机相连时,设备的其它信号线 SCK、 MOSI 及 MISO 同时并联到相同的 SPI 总线上,即无论有多少个从设备,都共同只使用这 3 条总线;每个从设备都有独立的这一条 SS 信号线,SS信号线独占主机的一个引脚,即有多少个从设备,就有多少条片选信号线。
1.2 协议层
协议层定义了通信的起始信号、停止信号、数据的有效性、时钟同步等环节。
1.2.1 基本通讯过程
上图为SPI通讯的一种通讯时序,一个主机的通讯时序。
SS、 SCK、 MOSI 信号都由主机控制产生,而 MISO 的信号由从机产生,主机通过该信号线读取从机的数据。
在这个时序图中,SCK上升沿触发(数据产生变化),SCK下降沿采样,在采样过程中的数据是稳定的,此时MOSI、MISO上的电平信号就代表了一个数据位的数据。
1.2.2 起始信号与结束信号
起始信号:SS信号线由高电平变为低电平。
结束信号:SS信号线由低电平变为高电平。
1.2.3 数据有效性
MOSI 及 MISO 数据线在 SCK 的每个时钟周期传输一位数据,且数据输入输出是同时进行的。
数据传输时, MSB先行或 LSB 先行并没有作硬性规定,但要保证两个 SPI 通讯设备之间使用同样的协定。
数据在采样过程中有效(以上图中时序为例,则下降沿的数据有效),在其他时刻,数据无效。
SPI 每次数据传输可以 8 位或 16 位为单位,可以根据实际需要传输任意位数的数据。
在实际应用中,SPI通讯会涉及到固定大小的数据块,比如一个字节或更多字节,这取决于具体的数据手册和通信协议。我们需要参考与主机通信设备的数据手册来确定数据传输的大小。
1.2.4 CPOL/CPHA及通讯模式
CPOL(Clock Polarity)和CPHA(Clock Phase)是SPI时序的两个重要参数,它们决定了数据传输的时钟极性和相位。
CPOL(Clock Polarity):定义了时钟信号在空闲时的电平。
CPHA(Clock Phase):定义了数据采样的时机。它决定了在时钟信号的哪个边沿上数据被采样以及何时数据被放置到总线上。
当CPHA等于0时,MOSI、MISO数据线上的信号将会在SCK时钟线的“奇数边沿”被采样。
当CPHA等于1时,MOSI、MISO数据线上的信号将会在SCK时钟线的“偶数边沿”被采样。
基于CPOL(Clock Polarity)和CPHA(Clock Phase)参数的不同组合,SPI定义了以下四种工作模式:其中采样较为广泛的是Mode0与Mode3。
主机与从机需要工作在相同的模式下才能正常通讯。
Mode 0:
Mode 1:
Mode 2:
Mode 3:
Mode | CPOL | CPHA | 空闲时SCK时钟 | 采样时刻 |
---|---|---|---|---|
0 | 0 | 0 | 低电平 | 奇数边沿 |
1 | 0 | 1 | 低电平 | 偶数边沿 |
2 | 1 | 0 | 高电平 | 奇数边沿 |
3 | 1 | 1 | 高电平 | 偶数边沿 |
CPOL与CPHA可通过SPI_CR1寄存器的位0与位1进行配置:
2 SPI框图
SPI框图可分为四个部分:通讯引脚、时钟控制逻辑、数据控制逻辑与整体控制逻辑。
2.1 框图介绍
其中,SPI1是APB2上的设备,最高通讯速率为36Mbit/s,SPI2、SPI3是APB1上的设备,最高通讯速率为18Mbit/s。
实际应用中,NSS引脚一般使用GPIO引脚(配置为推挽输出模式),通过软件进行控制它的电平输出,从而产生起始信号与结束信号。
SPI2与SPI3可以作为I2S通讯,这个主要用于传输音频数据。( I2S通讯与SPI通讯类似。)
注意:如果使用的是SPI3,会占用烧录引脚,这时要进行烧录的话就要按着复位键进行烧录。
上图为 《STM32F103xCDE_数据手册》 中 表2 STM32F103xC、 STM32F103xD和STM32F103xE器件功能和配置 的一部分。
其中,fPCLK为SPI所在的APB总线频率,挂载在APB1总线上则为fAPB1,挂载在APB2总线上则为fAPB2。
3.数据控制逻辑
有两个数据移位寄存器,一个用来接收数据,一个用来发送数据。
发送数据时数据寄存器DR会把数据放到发送缓冲区中。
接收数据时数据寄存器DR会读取接收缓冲区中的数据。
数据帧长度可通过SPI_CR1寄存器的位11 DFF进行配置:
帧格式可通过SPI_CR1寄存器的位7进行配置:
4.整体控制逻辑






很少SPI设备支持CRC校验。
2.2 通讯过程
上图为STM32作为主机的一个时序图。在整个通讯过程中,我们根据SPI_SR寄存器的标志位来进行协调。
从上图中,我们可以看到:BSY标志与SS引脚的电平相反。可通过BSY标志来判断SPI是否正在通信或发送缓冲区是否为空。
2.2.1 发送数据
1.控制STM32向SPI_DR寄存器中写入数据0xF1。
2.当数据写入到发送缓冲区时,TXE标志清0表非发送缓冲区空。
3.SPI_DR寄存器中的数据被搬运到数据移位寄存器后,发送缓冲区为空,TXE置1。
4.向SPI_DR寄存器写入第二个数据0xF2。
如果数据没被发送就一直放在SPI_DR寄存器中,等待TXE置1后将数据搬运到数据移位寄存器中。
简单来说,发送数据的过程就是:往SPI_DR寄存器中写入数据 -> 等待TXE置1 -> 写入下一个数据。
2.2.2 接收数据
1.接收到一帧数据后,RXNE标志位置1(表示接收缓冲区非空)。
2.读取SPI_DR寄存器获取接收缓冲区中的数据。
注意:如果你只是要接收数据,那么在接收数据的时候你也要往移位发送器中写入数据,以此让STM32产生时钟。
假如使能了TXE或RXNE中断,TXE或RXNE置1时会产生SPI中断信号,进入统一规格中断服务函数。进入到SPI中断服务程序后,可通过检测寄存器位来了解是哪一个时间,再分别进行处理。也可以使用DMA方式来收发SPI_DR寄存器中的数据。
3 SPI初始化结构体
/**
* @brief SPI Init structure definition
*/
typedef struct
{
uint16_t SPI_Direction; /*!< 设置 SPI 的单双向模式 */
uint16_t SPI_Mode; /*!< 设置 SPI 的主/从机端模式 */
uint16_t SPI_DataSize; /*!< 设置 SPI 的数据帧长度,可选 8/16 位 */
uint16_t SPI_CPOL; /*!< 设置时钟极性 CPOL,可选高/低电平 */
uint16_t SPI_CPHA; /*!< 设置时钟相位,可选奇/偶数边沿采样 */
uint16_t SPI_NSS; /*!< 设置 NSS 引脚由 SPI 硬件控制还是软件控制 */
uint16_t SPI_BaudRatePrescaler; /*!< 设置时钟分频因子, fpclk/分频数 =fSCK */
uint16_t SPI_FirstBit; /*!< 设置 MSB/LSB 先行 */
uint16_t SPI_CRCPolynomial; /*!< 设置 CRC 校验的表达式 */
}SPI_InitTypeDef;
/** @defgroup SPI_data_direction
* @{
*/
#define SPI_Direction_2Lines_FullDuplex ((uint16_t)0x0000) // 双线全双工
#define SPI_Direction_2Lines_RxOnly ((uint16_t)0x0400) // 双线只接收
#define SPI_Direction_1Line_Rx ((uint16_t)0x8000) // 单线只接收
#define SPI_Direction_1Line_Tx ((uint16_t)0xC000) // 单线只发送
#define IS_SPI_DIRECTION_MODE(MODE) (((MODE) == SPI_Direction_2Lines_FullDuplex) || \
((MODE) == SPI_Direction_2Lines_RxOnly) || \
((MODE) == SPI_Direction_1Line_Rx) || \
((MODE) == SPI_Direction_1Line_Tx))
若被配置为从机模式, STM32 的 SPI 外设将接受外来的 SCK 信号。
/** @defgroup SPI_mode
* @{
*/
#define SPI_Mode_Master ((uint16_t)0x0104) // 主机模式
#define SPI_Mode_Slave ((uint16_t)0x0000) // 从机模式
#define IS_SPI_MODE(MODE) (((MODE) == SPI_Mode_Master) || \
((MODE) == SPI_Mode_Slave))
/** @defgroup SPI_data_size
* @{
*/
#define SPI_DataSize_16b ((uint16_t)0x0800) // 16 位
#define SPI_DataSize_8b ((uint16_t)0x0000) // 8 位
#define IS_SPI_DATASIZE(DATASIZE) (((DATASIZE) == SPI_DataSize_16b) || \
((DATASIZE) == SPI_DataSize_8b))
/** @defgroup SPI_Clock_Polarity
* @{
*/
#define SPI_CPOL_Low ((uint16_t)0x0000) // 低电平
#define SPI_CPOL_High ((uint16_t)0x0002) // 高电平
#define IS_SPI_CPOL(CPOL) (((CPOL) == SPI_CPOL_Low) || \
((CPOL) == SPI_CPOL_High))
/** @defgroup SPI_Clock_Phase
* @{
*/
#define SPI_CPHA_1Edge ((uint16_t)0x0000) // 在 SCK 的奇数边沿采集数据
#define SPI_CPHA_2Edge ((uint16_t)0x0001) // 在 SCK 的偶数边沿采集数据
#define IS_SPI_CPHA(CPHA) (((CPHA) == SPI_CPHA_1Edge) || \
((CPHA) == SPI_CPHA_2Edge))
硬件模式中的 SPI 片选信号由 SPI 硬件自动产生,而软件模式则需要我们亲自把相应的 GPIO 端口拉高或置低产生非片选和片选信号。
/** @defgroup SPI_Slave_Select_management
* @{
*/
#define SPI_NSS_Soft ((uint16_t)0x0200) // 软件模式
#define SPI_NSS_Hard ((uint16_t)0x0000) // 硬件模式
#define IS_SPI_NSS(NSS) (((NSS) == SPI_NSS_Soft) || \
((NSS) == SPI_NSS_Hard))
/** @defgroup SPI_BaudRate_Prescaler
* @{
*/
#define SPI_BaudRatePrescaler_2 ((uint16_t)0x0000)
#define SPI_BaudRatePrescaler_4 ((uint16_t)0x0008)
#define SPI_BaudRatePrescaler_8 ((uint16_t)0x0010)
#define SPI_BaudRatePrescaler_16 ((uint16_t)0x0018)
#define SPI_BaudRatePrescaler_32 ((uint16_t)0x0020)
#define SPI_BaudRatePrescaler_64 ((uint16_t)0x0028)
#define SPI_BaudRatePrescaler_128 ((uint16_t)0x0030)
#define SPI_BaudRatePrescaler_256 ((uint16_t)0x0038)
#define IS_SPI_BAUDRATE_PRESCALER(PRESCALER) (((PRESCALER) == SPI_BaudRatePrescaler_2) || \
((PRESCALER) == SPI_BaudRatePrescaler_4) || \
((PRESCALER) == SPI_BaudRatePrescaler_8) || \
((PRESCALER) == SPI_BaudRatePrescaler_16) || \
((PRESCALER) == SPI_BaudRatePrescaler_32) || \
((PRESCALER) == SPI_BaudRatePrescaler_64) || \
((PRESCALER) == SPI_BaudRatePrescaler_128) || \
((PRESCALER) == SPI_BaudRatePrescaler_256))
/** @defgroup SPI_MSB_LSB_transmission
* @{
*/
#define SPI_FirstBit_MSB ((uint16_t)0x0000)
#define SPI_FirstBit_LSB ((uint16_t)0x0080)
#define IS_SPI_FIRST_BIT(BIT) (((BIT) == SPI_FirstBit_MSB) || \
((BIT) == SPI_FirstBit_LSB))
当我们使用 CRC 校验时,就使用这个成员的参数 (多项式),来计算 CRC 的值。
4 读写串行FLASH
4.1 FLASH简介
引脚:
DO:MISO。
DIO:MOSI。
CS:片选SS(低电平有效)。
WP:输入写保护(低电平有效)。
HOLD:暂停通讯;如果SPI正在通讯,将这个引脚拉低后可暂停通讯。
FLASH的存储特性:
在写入数据前必须先擦除
擦除时会把数据位全重置为1
写入数据时只能把为1的数据位改为0
擦除时必须按最小单位来擦除(一般最小单位为扇区)
想要擦除某一扇区,发送该扇区的最低位地址
norflash可以一个字节写入,nandflash必须以块或扇区为单位。
4.2 硬件SPI
4.2.1 步骤
1.初始化通讯使用的目标引脚及端口时钟;
2.使能 SPI 外设的时钟;
3.配置 SPI 外设的模式、地址、速率等参数并使能 SPI 外设;
4.编写基本 SPI 按字节收发的函数;
5.编写对 FLASH 擦除及读写操作的的函数;
6.编写测试程序,对读写数据进行校验。
4.2.2 初始化
// bsp_spi.h
#define DEBUG_SPIx SPI1
#define DEBUG_SPI_CLK RCC_APB2Periph_SPI1
#define DEBUG_SPI_APBxClkCmd RCC_APB2PeriphClockCmd
#define DEBUG_SPI_GPIO_CLK (RCC_APB2Periph_GPIOC|RCC_APB2Periph_GPIOA)
// SPI GPIO 引脚宏定义
/*CS(NSS)*/
#define DEBUG_SPI_CS_GPIO_PORT GPIOC
#define DEBUG_SPI_CS_GPIO_PIN GPIO_Pin_0
#define DEBUG_SPI_CS_GPIO_CLK RCC_APB2Periph_GPIOC
#define DEBUG_SPI_CS_GPIO_APBxClock RCC_APB2PeriphClockCmd
/*SCLK*/
#define DEBUG_SPI_SCLK_GPIO_PORT GPIOA
#define DEBUG_SPI_SCLK_GPIO_PIN GPIO_Pin_5
#define DEBUG_SPI_SCLK_GPIO_CLK RCC_APB2Periph_GPIOA
#define DEBUG_SPI_SCLK_GPIO_APBxClock RCC_APB2PeriphClockCmd
/*MISO*/
#define DEBUG_SPI_MISO_GPIO_PORT GPIOA
#define DEBUG_SPI_MISO_GPIO_PIN GPIO_Pin_6
#define DEBUG_SPI_MISO_GPIO_CLK RCC_APB2Periph_GPIOA
#define DEBUG_SPI_MISO_GPIO_APBxClock RCC_APB2PeriphClockCmd
/*MOSI*/
#define DEBUG_SPI_MOSI_GPIO_PORT GPIOA
#define DEBUG_SPI_MOSI_GPIO_PIN GPIO_Pin_7
#define DEBUG_SPI_MOSI_GPIO_CLK RCC_APB2Periph_GPIOA
#define DEBUG_SPI_MOSI_GPIO_APBxClock RCC_APB2PeriphClockCmd
#define SPI_CS_HIGH() GPIO_SetBits( DEBUG_SPI_CS_GPIO_PORT, DEBUG_SPI_CS_GPIO_PIN )
#define SPI_CS_LOW() GPIO_ResetBits( DEBUG_SPI_CS_GPIO_PORT, DEBUG_SPI_CS_GPIO_PIN )
// bsp_spi.c
void SPI_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
// 打开SPI GPIO的时钟
RCC_APB2PeriphClockCmd(DEBUG_SPI_GPIO_CLK, ENABLE);
// 打开SPI 外设的时钟
RCC_APB2PeriphClockCmd(DEBUG_SPI_CLK, ENABLE);
// 将 SCLK 的GPIO配置为推挽复用模式
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = DEBUG_SPI_SCLK_GPIO_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DEBUG_SPI_SCLK_GPIO_PORT, &GPIO_InitStructure);
// 将 MOSI 的GPIO配置为推挽复用模式
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = DEBUG_SPI_MOSI_GPIO_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DEBUG_SPI_MOSI_GPIO_PORT, &GPIO_InitStructure);
// 将 MISO 的GPIO配置为浮空输入模式
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_InitStructure.GPIO_Pin = DEBUG_SPI_MISO_GPIO_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DEBUG_SPI_MISO_GPIO_PORT, &GPIO_InitStructure);
// 初始化CS引脚,使用软件控制,所以直接设置为推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = DEBUG_SPI_CS_GPIO_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DEBUG_SPI_CS_GPIO_PORT, &GPIO_InitStructure);
SPI_CS_HIGH();
}
void SPI_MODE_Config(void)
{
SPI_InitTypeDef SPI_InitSturcture;
// 配置为模式3
SPI_InitSturcture.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2; // 波特率预分频值为2
SPI_InitSturcture.SPI_CPHA = SPI_CPHA_2Edge; // 模式3下CPHA数据捕获于第二个时钟沿(偶数沿)
SPI_InitSturcture.SPI_CPOL = SPI_CPOL_High; // 模式3下CPOL时钟悬空为高
SPI_InitSturcture.SPI_CRCPolynomial = 0; // 不使用CRC功能,数值随意
SPI_InitSturcture.SPI_DataSize = SPI_DataSize_8b; // SPI发送接收8位帧结构
SPI_InitSturcture.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 设置为双线全双工
SPI_InitSturcture.SPI_FirstBit = SPI_FirstBit_MSB; // 看芯片是MSB先行还是LSB先行
SPI_InitSturcture.SPI_Mode = SPI_Mode_Master; // 设置为主SPI
SPI_InitSturcture.SPI_NSS = SPI_NSS_Soft; // 软件控制NSS(CS)引脚
SPI_Init(DEBUG_SPIx, &SPI_InitSturcture);
SPI_Cmd(DEBUG_SPIx, ENABLE); // 使能SPI
}
void SPI_Config(void)
{
SPI_GPIO_Config();
SPI_MODE_Config();
}
4.2.3 SPI基本功能函数
4.2.3.1 发送一个字节
// 发送一个字节
uint8_t SPI_Send_Byte(uint8_t data)
{
uint16_t SPI_TIME_OUT = SPIT_FLAG_TIMEOUT;
// 检查并等待至 TX缓冲区 为空
while(SPI_I2S_GetFlagStatus(DEBUG_SPIx, SPI_I2S_FLAG_TXE) == RESET)
{
if((SPI_TIME_OUT--) == 0) return SPI_TIMEOUT_UserCallback(0);
}
// 程序执行到此处说明 TX缓冲区 已空
SPI_I2S_SendData(DEBUG_SPIx, data);
SPI_TIME_OUT = SPIT_FLAG_TIMEOUT;
// 检查并等待至 RX缓冲区 为非空
while(SPI_I2S_GetFlagStatus(DEBUG_SPIx, SPI_I2S_FLAG_RXNE) == RESET)
{
if((SPI_TIME_OUT--) == 0) return SPI_TIMEOUT_UserCallback(1);
}
/* 同步收发,一字节发送完毕说明一字节也接收完毕*/
// 程序执行到此处说明 RX缓冲区 非空
return SPI_I2S_ReceiveData(DEBUG_SPIx);
}
这里注意检查缓冲区数据是否为空的函数不要写错。
上图是在测试SPI_Send_Byte函数时遇到的一个问题:SPI等待超时。
原因:函数名写错了造成SPI通讯失败。
4.2.3.2 读取一个字节
// 读取一个字节
uint8_t SPI_Read_Byte(void)
{
/* flash会根据你写的命令来选择是否忽略后面发来的数据 */
return SPI_Send_Byte(DUMMY); // 这里也可以发送0xff,flash会忽略这个数据
}
4.2.3.3 读取数据
// 读取flahs id
uint32_t SPI_Read_ID(void)
{
/* 根据芯片手册,发送读取id指令,芯片会返回三个字节数据*/
uint32_t flash_id;
// 片选使能
SPI_CS_LOW();
SPI_Send_Byte(READ_JEDEC_ID);
flash_id = SPI_Send_Byte(DUMMY); // 接收数据
flash_id <<= 8;
flash_id |= SPI_Send_Byte(DUMMY);
flash_id <<= 8;
flash_id |= SPI_Send_Byte(DUMMY);
SPI_CS_HIGH();
return flash_id;
}
// 读取FLASH的内容
void SPI_Read_Data(uint32_t addr, uint8_t *data, uint32_t numByteToRead)
{
// 片选使能
SPI_CS_LOW();
SPI_Send_Byte(READ_DATA);
SPI_Send_Byte((addr>>16)&0xff);
SPI_Send_Byte((addr>>8)&0xff);
SPI_Send_Byte(addr&0xff);
while(numByteToRead--)
{
*data = SPI_Send_Byte(DUMMY);
data++;
}
SPI_CS_HIGH();
SPI_WaitForWriteEnd();
}
4.2.3.4 写数据
写入数据之前必须要加擦除操作。
// FLASH写入使能
void SPI_Write_Enable(void)
{
// 片选使能
SPI_CS_LOW();
SPI_Send_Byte(WRITE_ENABLE);
SPI_CS_HIGH();
}
// 向FLASH写入内容,最多写入256字节
void SPI_Write_Data(uint32_t addr, uint8_t *writeBuff, uint32_t numByteToRead)
{
SPI_Write_Enable();
// 片选使能
SPI_CS_LOW();
SPI_Send_Byte(WRITE_DATA);
SPI_Send_Byte((addr>>16)&0xff);
SPI_Send_Byte((addr>>8)&0xff);
SPI_Send_Byte(addr&0xff);
while(numByteToRead--)
{
SPI_Send_Byte(*writeBuff);
writeBuff++;
}
SPI_CS_HIGH();
SPI_WaitForWriteEnd();
}
void SPI_BufferWrite(uint32_t WriteAddr, uint8_t* pBuffer, uint16_t NumByteToWrite)
{
uint8_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0;
/* mod运算求余,若writeAddr是SPI_FLASH_PageSize整数倍,运算结果Addr值为0 */
Addr = WriteAddr % SPI_FLASH_PageSize;
/* 差count个数据值,刚好可以对齐到页地址 */
count = SPI_FLASH_PageSize - Addr;
/* 计算出要写多少整数页 */
NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;
/* mod运算求余,计算出剩余不满一页的字节数 */
NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;
/* Addr = 0,则writeAddr 刚好按页对齐*/
if(Addr == 0)
{
/* NumByteToWrite < SPI_FLASH_PageSize */
if(NumOfPage == 0)
{
SPI_Write_Data(WriteAddr, pBuffer, NumByteToWrite);
}
else /* NumByteToWrite > SPI_FLASH_PageSize */
{
/* 先把整数页都写了 */
while(NumOfPage--)
{
SPI_Write_Data(WriteAddr, pBuffer, SPI_FLASH_PageSize);
WriteAddr = WriteAddr + SPI_FLASH_PageSize;
pBuffer = pBuffer + SPI_FLASH_PageSize;
}
/* 若有多余的不满一页的数据,先把它写完 */
SPI_Write_Data(WriteAddr, pBuffer, NumOfSingle);
}
}
/* 若地址与SPI_FLASH_PageSize不对齐*/
else
{
/* NumByteToWrite < SPI_FLASH_PageSize */
if(NumOfPage == 0)
{
/* 当前页剩余的count个位置比NumOfSingle小,一页写不完 */
if(NumOfSingle > count)
{
temp = NumOfSingle - count;
/* 先写满当前页*/
SPI_Write_Data(WriteAddr, pBuffer, count);
WriteAddr = WriteAddr + count;
pBuffer = pBuffer + count;
/* 再写剩余的数据 */
SPI_Write_Data(WriteAddr, pBuffer, temp);
}
else /* 当前页剩余的count个位置能写完NumOfSingle个数据 */
{
SPI_Write_Data(WriteAddr, pBuffer, NumByteToWrite);
}
}
else /* NumByteToWrite > SPI_FLASH_PageSize */
{
/* 地址不对齐多出的count分开处理,不加入这个运算*/
NumByteToWrite -= count;
NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;
NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;
/* 先写完count个数据,为的是让下一次要写的地址对齐 */
SPI_Write_Data(WriteAddr, pBuffer, count);
/* 接下来就重复地址对齐的情况*/
WriteAddr += count;
pBuffer += count;
/* 把整数页都写了 */
while(NumOfPage--)
{
SPI_Write_Data(WriteAddr, pBuffer, SPI_FLASH_PageSize);
WriteAddr = WriteAddr + SPI_FLASH_PageSize;
pBuffer = pBuffer + SPI_FLASH_PageSize;
}
/* 若有对于的不满一页的数据,把它写完 */
if(NumOfSingle != 0)
{
SPI_Write_Data(WriteAddr, pBuffer, NumOfSingle);
}
}
}
}
4.2.3.5 等待FLASH内部时序操作完成
void SPI_WaitForWriteEnd(void)
{
uint8_t status_reg = 0;
// 片选使能
SPI_CS_LOW();
SPI_Send_Byte(READ_STATUS);
do
{
status_reg = SPI_Send_Byte(DUMMY);
}while((status_reg & 0x01) == 1);
SPI_CS_HIGH();
}
4.2.3.6 擦除
// 擦除FLASH指定扇区
void SPI_Erase_Sector(uint32_t addr)
{
SPI_Write_Enable(); // FLASH写入数据前要先发送写使能命令,擦除也算写操作
// 片选使能
SPI_CS_LOW();
SPI_Send_Byte(SECTOR_ERASE);
SPI_Send_Byte((addr>>16)&0xff);
SPI_Send_Byte((addr>>8)&0xff);
SPI_Send_Byte(addr&0xff);
SPI_CS_HIGH();
SPI_WaitForWriteEnd();
}
擦除指令一般使用Sector Erase或Chip Erase。
这样擦除会有一个缺陷:如果FLASH本身内部就没有写入数据,那么就验证不了我们写的函数是否起到了效果。
4.2.4 整合
// bsp_spi.h
#ifndef __BSP_SPI_H
#define __BSP_SPI_H
#include "stm32f10x.h"
#include <stdio.h>
#define DEBUG_SPIx SPI1
#define DEBUG_SPI_CLK RCC_APB2Periph_SPI1
#define DEBUG_SPI_APBxClkCmd RCC_APB2PeriphClockCmd
#define DEBUG_SPI_GPIO_CLK (RCC_APB2Periph_GPIOC|RCC_APB2Periph_GPIOA)
// SPI GPIO 引脚宏定义
/*CS(NSS)*/
#define DEBUG_SPI_CS_GPIO_PORT GPIOC
#define DEBUG_SPI_CS_GPIO_PIN GPIO_Pin_0
#define DEBUG_SPI_CS_GPIO_CLK RCC_APB2Periph_GPIOC
#define DEBUG_SPI_CS_GPIO_APBxClock RCC_APB2PeriphClockCmd
/*SCLK*/
#define DEBUG_SPI_SCLK_GPIO_PORT GPIOA
#define DEBUG_SPI_SCLK_GPIO_PIN GPIO_Pin_5
#define DEBUG_SPI_SCLK_GPIO_CLK RCC_APB2Periph_GPIOA
#define DEBUG_SPI_SCLK_GPIO_APBxClock RCC_APB2PeriphClockCmd
/*MISO*/
#define DEBUG_SPI_MISO_GPIO_PORT GPIOA
#define DEBUG_SPI_MISO_GPIO_PIN GPIO_Pin_6
#define DEBUG_SPI_MISO_GPIO_CLK RCC_APB2Periph_GPIOA
#define DEBUG_SPI_MISO_GPIO_APBxClock RCC_APB2PeriphClockCmd
/*MOSI*/
#define DEBUG_SPI_MOSI_GPIO_PORT GPIOA
#define DEBUG_SPI_MOSI_GPIO_PIN GPIO_Pin_7
#define DEBUG_SPI_MOSI_GPIO_CLK RCC_APB2Periph_GPIOA
#define DEBUG_SPI_MOSI_GPIO_APBxClock RCC_APB2PeriphClockCmd
#define SPI_CS_HIGH() GPIO_SetBits( DEBUG_SPI_CS_GPIO_PORT, DEBUG_SPI_CS_GPIO_PIN )
#define SPI_CS_LOW() GPIO_ResetBits( DEBUG_SPI_CS_GPIO_PORT, DEBUG_SPI_CS_GPIO_PIN )
#define SPIT_FLAG_TIMEOUT ((uint32_t)0x1000)
/*信息输出*/
#define EEPROM_DEBUG_ON 1
#define EEPROM_INFO(fmt,arg...) printf("<<-EEPROM-INFO->> "fmt"\n",##arg)
#define EEPROM_ERROR(fmt,arg...) printf("<<-EEPROM-ERROR->> "fmt"\n",##arg)
#define EEPROM_DEBUG(fmt,arg...) do{\
if(EEPROM_DEBUG_ON)\
printf("<<-EEPROM-DEBUG->> [%s][%d]"fmt"\n",__FILE__,__LINE__, ##arg);\
}while(0)
#define DUMMY 0x00
#define READ_JEDEC_ID 0x9f
#define SECTOR_ERASE 0x20
#define READ_STATUS 0x05
#define READ_DATA 0x03
#define WRITE_ENABLE 0x06
#define WRITE_DATA 0x02
#define SPI_FLASH_PageSize 256
void SPI_Config(void);
uint32_t SPI_Read_ID(void);
void SPI_Erase_Sector(uint32_t addr);
void SPI_Read_Data(uint32_t addr, uint8_t *data, uint32_t numByteToRead);
void SPI_WaitForWriteEnd(void);
void SPI_Write_Data(uint32_t addr, uint8_t *writeBuff, uint32_t numByteToRead);
void SPI_BufferWrite(uint32_t WriteAddr, uint8_t* pBuffer, uint16_t NumByteToWrite);
#endif /* __BSP_SPI_H */
// bsp_spi.c
#include "bsp_spi.h"
static uint32_t SPI_TIMEOUT_UserCallback(uint8_t errorCode);
void SPI_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
// 打开SPI GPIO的时钟
RCC_APB2PeriphClockCmd(DEBUG_SPI_GPIO_CLK, ENABLE);
// 打开SPI 外设的时钟
RCC_APB2PeriphClockCmd(DEBUG_SPI_CLK, ENABLE);
// 将 SCLK 的GPIO配置为推挽复用模式
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = DEBUG_SPI_SCLK_GPIO_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DEBUG_SPI_SCLK_GPIO_PORT, &GPIO_InitStructure);
// 将 MOSI 的GPIO配置为推挽复用模式
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = DEBUG_SPI_MOSI_GPIO_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DEBUG_SPI_MOSI_GPIO_PORT, &GPIO_InitStructure);
// 将 MISO 的GPIO配置为浮空输入模式
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_InitStructure.GPIO_Pin = DEBUG_SPI_MISO_GPIO_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DEBUG_SPI_MISO_GPIO_PORT, &GPIO_InitStructure);
// 初始化CS引脚,使用软件控制,所以直接设置为推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = DEBUG_SPI_CS_GPIO_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DEBUG_SPI_CS_GPIO_PORT, &GPIO_InitStructure);
SPI_CS_HIGH();
}
void SPI_MODE_Config(void)
{
SPI_InitTypeDef SPI_InitSturcture;
// 配置为模式3
SPI_InitSturcture.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2; // 波特率预分频值为2
SPI_InitSturcture.SPI_CPHA = SPI_CPHA_2Edge; // 模式3下CPHA数据捕获于第二个时钟沿(偶数沿)
SPI_InitSturcture.SPI_CPOL = SPI_CPOL_High; // 模式3下CPOL时钟悬空为高
SPI_InitSturcture.SPI_CRCPolynomial = 0; // 不使用CRC功能,数值随意
SPI_InitSturcture.SPI_DataSize = SPI_DataSize_8b; // SPI发送接收8位帧结构
SPI_InitSturcture.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 设置为双线全双工
SPI_InitSturcture.SPI_FirstBit = SPI_FirstBit_MSB; // 看芯片是MSB先行还是LSB先行
SPI_InitSturcture.SPI_Mode = SPI_Mode_Master; // 设置为主SPI
SPI_InitSturcture.SPI_NSS = SPI_NSS_Soft; // 软件控制NSS(CS)引脚
SPI_Init(DEBUG_SPIx, &SPI_InitSturcture);
SPI_Cmd(DEBUG_SPIx, ENABLE); // 使能SPI
}
void SPI_Config(void)
{
SPI_GPIO_Config();
SPI_MODE_Config();
}
// 发送一个字节
uint8_t SPI_Send_Byte(uint8_t data)
{
uint16_t SPI_TIME_OUT = SPIT_FLAG_TIMEOUT;
// 检查并等待至 TX缓冲区 为空
while(SPI_I2S_GetFlagStatus(DEBUG_SPIx, SPI_I2S_FLAG_TXE) == RESET)
{
if((SPI_TIME_OUT--) == 0) return SPI_TIMEOUT_UserCallback(0);
}
// 程序执行到此处说明 TX缓冲区 已空
SPI_I2S_SendData(DEBUG_SPIx, data);
SPI_TIME_OUT = SPIT_FLAG_TIMEOUT;
// 检查并等待至 RX缓冲区 为非空
while(SPI_I2S_GetFlagStatus(DEBUG_SPIx, SPI_I2S_FLAG_RXNE) == RESET)
{
if((SPI_TIME_OUT--) == 0) return SPI_TIMEOUT_UserCallback(1);
}
/* 同步收发,一字节发送完毕说明一字节也接收完毕*/
// 程序执行到此处说明 RX缓冲区 非空
return SPI_I2S_ReceiveData(DEBUG_SPIx);
}
// 读取一个字节
uint8_t SPI_Read_Byte(void)
{
/* flash会根据你写的命令来选择是否忽略后面发来的数据 */
return SPI_Send_Byte(DUMMY); // 这里也可以发送0xff,flash会忽略这个数据
}
// 读取flahs id
uint32_t SPI_Read_ID(void)
{
/* 根据芯片手册,发送读取id指令,芯片会返回三个字节数据*/
uint32_t flash_id;
// 片选使能
SPI_CS_LOW();
SPI_Send_Byte(READ_JEDEC_ID);
flash_id = SPI_Send_Byte(DUMMY); // 接收数据
flash_id <<= 8;
flash_id |= SPI_Send_Byte(DUMMY);
flash_id <<= 8;
flash_id |= SPI_Send_Byte(DUMMY);
SPI_CS_HIGH();
return flash_id;
}
// 等待FLASH内部时序操作完成
void SPI_WaitForWriteEnd(void)
{
uint8_t status_reg = 0;
// 片选使能
SPI_CS_LOW();
SPI_Send_Byte(READ_STATUS);
do
{
status_reg = SPI_Send_Byte(DUMMY);
}while((status_reg & 0x01) == 1);
SPI_CS_HIGH();
}
// FLASH写入使能
void SPI_Write_Enable(void)
{
// 片选使能
SPI_CS_LOW();
SPI_Send_Byte(WRITE_ENABLE);
SPI_CS_HIGH();
}
// 擦除FLASH指定扇区
void SPI_Erase_Sector(uint32_t addr)
{
SPI_Write_Enable(); // FLASH写入数据前要先发送写使能命令,擦除也算写操作
// 片选使能
SPI_CS_LOW();
SPI_Send_Byte(SECTOR_ERASE);
SPI_Send_Byte((addr>>16)&0xff);
SPI_Send_Byte((addr>>8)&0xff);
SPI_Send_Byte(addr&0xff);
SPI_CS_HIGH();
SPI_WaitForWriteEnd();
}
// 读取FLASH的内容
void SPI_Read_Data(uint32_t addr, uint8_t *data, uint32_t numByteToRead)
{
// 片选使能
SPI_CS_LOW();
SPI_Send_Byte(READ_DATA);
SPI_Send_Byte((addr>>16)&0xff);
SPI_Send_Byte((addr>>8)&0xff);
SPI_Send_Byte(addr&0xff);
while(numByteToRead--)
{
*data = SPI_Send_Byte(DUMMY);
data++;
}
SPI_CS_HIGH();
SPI_WaitForWriteEnd();
}
// 向FLASH写入内容,最多写入256字节
void SPI_Write_Data(uint32_t addr, uint8_t *writeBuff, uint32_t numByteToRead)
{
SPI_Write_Enable();
// 片选使能
SPI_CS_LOW();
SPI_Send_Byte(WRITE_DATA);
SPI_Send_Byte((addr>>16)&0xff);
SPI_Send_Byte((addr>>8)&0xff);
SPI_Send_Byte(addr&0xff);
while(numByteToRead--)
{
SPI_Send_Byte(*writeBuff);
writeBuff++;
}
SPI_CS_HIGH();
SPI_WaitForWriteEnd();
}
void SPI_BufferWrite(uint32_t WriteAddr, uint8_t* pBuffer, uint16_t NumByteToWrite)
{
uint8_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0;
/* mod运算求余,若writeAddr是SPI_FLASH_PageSize整数倍,运算结果Addr值为0 */
Addr = WriteAddr % SPI_FLASH_PageSize;
/* 差count个数据值,刚好可以对齐到页地址 */
count = SPI_FLASH_PageSize - Addr;
/* 计算出要写多少整数页 */
NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;
/* mod运算求余,计算出剩余不满一页的字节数 */
NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;
/* Addr = 0,则writeAddr 刚好按页对齐*/
if(Addr == 0)
{
/* NumByteToWrite < SPI_FLASH_PageSize */
if(NumOfPage == 0)
{
SPI_Write_Data(WriteAddr, pBuffer, NumByteToWrite);
}
else /* NumByteToWrite > SPI_FLASH_PageSize */
{
/* 先把整数页都写了 */
while(NumOfPage--)
{
SPI_Write_Data(WriteAddr, pBuffer, SPI_FLASH_PageSize);
WriteAddr = WriteAddr + SPI_FLASH_PageSize;
pBuffer = pBuffer + SPI_FLASH_PageSize;
}
/* 若有多余的不满一页的数据,先把它写完 */
SPI_Write_Data(WriteAddr, pBuffer, NumOfSingle);
}
}
/* 若地址与SPI_FLASH_PageSize不对齐*/
else
{
/* NumByteToWrite < SPI_FLASH_PageSize */
if(NumOfPage == 0)
{
/* 当前页剩余的count个位置比NumOfSingle小,一页写不完 */
if(NumOfSingle > count)
{
temp = NumOfSingle - count;
/* 先写满当前页*/
SPI_Write_Data(WriteAddr, pBuffer, count);
WriteAddr = WriteAddr + count;
pBuffer = pBuffer + count;
/* 再写剩余的数据 */
SPI_Write_Data(WriteAddr, pBuffer, temp);
}
else /* 当前页剩余的count个位置能写完NumOfSingle个数据 */
{
SPI_Write_Data(WriteAddr, pBuffer, NumByteToWrite);
}
}
else /* NumByteToWrite > SPI_FLASH_PageSize */
{
/* 地址不对齐多出的count分开处理,不加入这个运算*/
NumByteToWrite -= count;
NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;
NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;
/* 先写完count个数据,为的是让下一次要写的地址对齐 */
SPI_Write_Data(WriteAddr, pBuffer, count);
/* 接下来就重复地址对齐的情况*/
WriteAddr += count;
pBuffer += count;
/* 把整数页都写了 */
while(NumOfPage--)
{
SPI_Write_Data(WriteAddr, pBuffer, SPI_FLASH_PageSize);
WriteAddr = WriteAddr + SPI_FLASH_PageSize;
pBuffer = pBuffer + SPI_FLASH_PageSize;
}
/* 若有对于的不满一页的数据,把它写完 */
if(NumOfSingle != 0)
{
SPI_Write_Data(WriteAddr, pBuffer, NumOfSingle);
}
}
}
}
/**
* @brief Basic management of the timeout situation.
* @param errorCode:错误代码,可以用来定位是哪个环节出错.
* @retval 返回0,表示SPI读取失败.
*/
static uint32_t SPI_TIMEOUT_UserCallback(uint8_t errorCode)
{
/* Block communication and all processes */
EEPROM_ERROR("SPI 等待超时!errorCode = %d",errorCode);
return 0;
}
// main.c
#include "stm32f10x.h"
#include "bsp_spi.h"
#include "usart.h"
uint8_t readBuff[4096];
uint8_t writeBuff[4096];
int main(void)
{
uint32_t flash_id;
int i;
USART_Config();
printf("\r\n TEST \r\n");
SPI_Config();
flash_id = SPI_Read_ID();
printf("\r\n flash_id = 0x%x \r\n", flash_id);
SPI_Erase_Sector(0);
for(i = 0; i < 30; i++)
{
writeBuff[i] = i;
}
SPI_Write_Data(0, writeBuff, 30);
SPI_Read_Data(0, readBuff, sizeof(readBuff));
for(i = 0; i < 4096; i++)
{
printf("0x%x ", readBuff[i]);
if(i%10 == 0)
printf("\r\n");
}
while(1)
{
}
}
堆栈问题一般会跳到HardFault_Handler函数中,程序跑到这里多半是因为内存错误。
4.3 软件SPI
作者:CSDN_PBB