STM32入门学习12-软硬件SPI驱动W25Q64闪存
(一)SPI通信协议概述
SPI与I2C同属常见通信协议,SPI相较于I2C具有更高的传输速率。两者均为同步通信,需要时钟线配合,但SPI采用双数据线架构:一根用于主机发送(MOSI),一根用于主机接收(MISO)。此外,每个SPI从设备都通过独立的片选线(SS)与主机连接,主机通过拉低对应片选信号来选中目标从设备进行通信。
| 通信协议 | 时钟线 | 数据线 | 传输模式 |
|---|---|---|---|
| I2C | SCL | SDA | 半双工 |
| SPI | SCK | MISO、MOSI | 全双工 |
SPI支持四种工作模式,通过配置时钟极性(CPOL)和时钟相位(CPHA)来决定采样时机和空闲电平。最常用的是模式0:时钟空闲时为低电平,在上升沿进行数据采样,这与I2C的时钟逻辑较为相似。
- 模式0:CPOL=0,CPHA=0,SCK空闲为低电平,数据在SCK的上升沿被采样。
- 模式1:CPOL=0,CPHA=1,SCK空闲为低电平,数据在SCK的下降沿被采样。
- 模式2:CPOL=1,CPHA=0,SCK空闲为高电平,数据在SCK的下降沿被采样。
- 模式3:CPOL=1,CPHA=1,SCK空闲为高电平,数据在SCK的上升沿被采样。
SPI的特点是发送与接收同时进行。在向指定地址写入数据时,该地址的原有数据会被同时读出,即使应用层并不需要这个数据。SPI同样采用高位先行的传输方式:主机数据的高位通过MOSI传输到从设备的低位,同时从设备的高位通过MISO传输到主机的低位。
(二)软件模拟SPI实现
通过GPIO引脚模拟SPI时序的方式称为软件模拟SPI。通信前需要先选中目标从设备,将片选线(SS)拉低。选用模式0时,SCK默认保持低电平,在上升沿采样数据。本示例中,片选线SS连接PA4,时钟线SCK连接PA5,主机输入MISO连接PA6,主机输出MOSI连接PA7。
#define SPI_SS GPIO_Pin_4
#define SPI_SCK GPIO_Pin_5
#define SPI_MISO GPIO_Pin_6
#define SPI_MOSI GPIO_Pin_7
为了简化软件模拟时的操作,封装三个基础函数:设置引脚输出高电平、设置引脚输出低电平、读取MISO引脚输入状态。
void spi_pin_set(uint16_t pin)
{
GPIO_SetBits(GPIOA, pin);
}
void spi_pin_reset(uint16_t pin)
{
GPIO_ResetBits(GPIOA, pin);
}
uint8_t spi_pin_read(void)
{
return GPIO_ReadInputDataBit(GPIOA, SPI_MISO);
}
(1)SPI初始化
对各连接引脚进行初始化配置。SCK、MOSI和SS均配置为推挽输出模式,以获得更快的电平切换速度;MISO配置为上拉输入模式,用于接收从设备返回的数据。
void spi_soft_init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef gpio_init;
gpio_init.GPIO_Mode = GPIO_Mode_Out_PP;
gpio_init.GPIO_Pin = SPI_SS | SPI_SCK | SPI_MOSI;
gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio_init);
gpio_init.GPIO_Mode = GPIO_Mode_IPU;
gpio_init.GPIO_Pin = SPI_MISO;
gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio_init);
spi_pin_set(SPI_SS);
spi_pin_reset(SPI_SCK);
}
初始化完成后,SS置高电平表示未选中任何从设备,SCK置低电平进入空闲状态。
(2)通信开始与结束
通过控制SS引脚电平来选中或释放从设备。开始通信时将SS拉低,结束通信时将SS拉高。
void spi_soft_start(void)
{
spi_pin_reset(SPI_SS);
}
void spi_soft_end(void)
{
spi_pin_set(SPI_SS);
}
(3)数据交换
采用模式0时,在SCK上升沿采样数据。从设备会自动将其高位数据放到MISO线上,主机循环执行8次即可完成一个字节的数据交换。
实现思路:在SCK低电平期间将待发送数据放在MOSI线上,然后拉高SCK产生上升沿,此时从设备数据已稳定在MISO线上,读取该引脚电平即可获得数据。最后将SCK恢复为低电平,为下一次交换做准备。
uint8_t spi_soft_exchange(uint8_t tx_data)
{
uint8_t rx_data = 0x00;
uint8_t i;
for (i = 0; i < 8; i++)
{
if (tx_data & (0x80 >> i))
{
spi_pin_set(SPI_MOSI);
}
else
{
spi_pin_reset(SPI_MOSI);
}
spi_pin_set(SPI_SCK);
if (spi_pin_read())
{
rx_data |= (0x80 >> i);
}
spi_pin_reset(SPI_SCK);
}
return rx_data;
}
若仅需发送数据而无需接收返回值,可直接调用此函数并忽略返回值;若需要接收数据,需先发送任意数据(如0xFF)来触发从设备返回数据。
(4)完整代码
将上述函数封装为独立的模块,包含头文件声明和实现文件。
#ifndef __SOFT_SPI_H__
#define __SOFT_SPI_H__
void spi_soft_init(void);
void spi_soft_start(void);
void spi_soft_end(void);
uint8_t spi_soft_exchange(uint8_t tx_data);
#endif
#include "stm32f10x.h"
#define SPI_SS GPIO_Pin_4
#define SPI_SCK GPIO_Pin_5
#define SPI_MISO GPIO_Pin_6
#define SPI_MOSI GPIO_Pin_7
void spi_pin_set(uint16_t pin)
{
GPIO_SetBits(GPIOA, pin);
}
void spi_pin_reset(uint16_t pin)
{
GPIO_ResetBits(GPIOA, pin);
}
uint8_t spi_pin_read(void)
{
return GPIO_ReadInputDataBit(GPIOA, SPI_MISO);
}
void spi_soft_init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef gpio_init;
gpio_init.GPIO_Mode = GPIO_Mode_Out_PP;
gpio_init.GPIO_Pin = SPI_SS | SPI_SCK | SPI_MOSI;
gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio_init);
gpio_init.GPIO_Mode = GPIO_Mode_IPU;
gpio_init.GPIO_Pin = SPI_MISO;
gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio_init);
spi_pin_set(SPI_SS);
spi_pin_reset(SPI_SCK);
}
void spi_soft_start(void)
{
spi_pin_reset(SPI_SS);
}
void spi_soft_end(void)
{
spi_pin_set(SPI_SS);
}
uint8_t spi_soft_exchange(uint8_t tx_data)
{
uint8_t rx_data = 0x00;
uint8_t i;
for (i = 0; i < 8; i++)
{
if (tx_data & (0x80 >> i))
{
spi_pin_set(SPI_MOSI);
}
else
{
spi_pin_reset(SPI_MOSI);
}
spi_pin_set(SPI_SCK);
if (spi_pin_read())
{
rx_data |= (0x80 >> i);
}
spi_pin_reset(SPI_SCK);
}
return rx_data;
}
(三)硬件SPI驱动
STM32内置SPI硬件控制器,可通过配置寄存器实现SPI通信。使用硬件SPI时,需将对应引脚配置为复用功能。本示例使用SPI1,引脚映射与软件模拟方案相同。
查看标准库提供的SPI相关函数:
void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);
void SPI_StructInit(SPI_InitTypeDef* SPI_InitStruct);
void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);
void SPI_DataSizeConfig(SPI_TypeDef* SPIx, uint16_t SPI_DataSize);
FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
void SPI_I2S_ClearFlag(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
ITStatus SPI_I2S_GetITStatus(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);
void SPI_I2S_ClearITPendingBit(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);
(1)SPI初始化
时钟配置
首先使能GPIO和SPI1的外设时钟。SPI1挂载在APB2总线上。
void spi_hw_clock_enable(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
}
GPIO配置
片选信号SS由软件手动控制,配置为推挽输出。SCK和MOSI配置为复用推挽输出,由硬件SPI控制器驱动。MISO配置为上拉输入。
void spi_hw_gpio_init(void)
{
GPIO_InitTypeDef gpio_init;
gpio_init.GPIO_Pin = GPIO_Pin_4;
gpio_init.GPIO_Mode = GPIO_Mode_Out_PP;
gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio_init);
gpio_init.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
gpio_init.GPIO_Mode = GPIO_Mode_AF_PP;
gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio_init);
gpio_init.GPIO_Pin = GPIO_Pin_6;
gpio_init.GPIO_Mode = GPIO_Mode_IPU;
gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio_init);
GPIO_SetBits(GPIOA, GPIO_Pin_4);
}
SPI参数配置
根据模式0的要求配置SPI初始化结构体:双向全双工模式、主机模式、8位数据帧、时钟极性低、第一个边沿采样、软件控制片选、分频系数128、高位先行。
void spi_hw_config(void)
{
SPI_InitTypeDef spi_init;
spi_init.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
spi_init.SPI_Mode = SPI_Mode_Master;
spi_init.SPI_DataSize = SPI_DataSize_8b;
spi_init.SPI_CPOL = SPI_CPOL_Low;
spi_init.SPI_CPHA = SPI_CPHA_1Edge;
spi_init.SPI_NSS = SPI_NSS_Soft;
spi_init.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;
spi_init.SPI_FirstBit = SPI_FirstBit_MSB;
spi_init.SPI_CRCPolynomial = 7;
SPI_Init(SPI1, &spi_init);
}
统一初始化
将各初始化步骤组合,并在最后使能SPI控制器。
void spi_hw_init(void)
{
spi_hw_clock_enable();
spi_hw_gpio_init();
spi_hw_config();
SPI_Cmd(SPI1, ENABLE);
}
(2)通信控制
通过SS引脚实现从设备的选通和释放。
void spi_hw_start(void)
{
GPIO_ResetBits(GPIOA, GPIO_Pin_4);
}
void spi_hw_end(void)
{
GPIO_SetBits(GPIOA, GPIO_Pin_4);
}
(3)数据交换
硬件SPI的数据交换通过状态标志位控制。发送数据前需等待发送缓冲区为空,发送完成后接收缓冲区会自动接收从设备返回的数据,再等待接收缓冲区非空即可读取。
uint8_t spi_hw_transfer(uint8_t tx_byte)
{
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET);
SPI_I2S_SendData(SPI1, tx_byte);
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET);
return SPI_I2S_ReceiveData(SPI1);
}
(4)完整代码
#ifndef __HW_SPI_H__
#define __HW_SPI_H__
void spi_hw_init(void);
void spi_hw_start(void);
void spi_hw_end(void);
uint8_t spi_hw_transfer(uint8_t tx_byte);
#endif
#include "stm32f10x.h"
void spi_hw_clock_enable(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
}
void spi_hw_gpio_init(void)
{
GPIO_InitTypeDef gpio_init;
gpio_init.GPIO_Pin = GPIO_Pin_4;
gpio_init.GPIO_Mode = GPIO_Mode_Out_PP;
gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio_init);
gpio_init.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
gpio_init.GPIO_Mode = GPIO_Mode_AF_PP;
gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio_init);
gpio_init.GPIO_Pin = GPIO_Pin_6;
gpio_init.GPIO_Mode = GPIO_Mode_IPU;
gpio_init.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio_init);
GPIO_SetBits(GPIOA, GPIO_Pin_4);
}
void spi_hw_config(void)
{
SPI_InitTypeDef spi_init;
spi_init.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
spi_init.SPI_Mode = SPI_Mode_Master;
spi_init.SPI_DataSize = SPI_DataSize_8b;
spi_init.SPI_CPOL = SPI_CPOL_Low;
spi_init.SPI_CPHA = SPI_CPHA_1Edge;
spi_init.SPI_NSS = SPI_NSS_Soft;
spi_init.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;
spi_init.SPI_FirstBit = SPI_FirstBit_MSB;
spi_init.SPI_CRCPolynomial = 7;
SPI_Init(SPI1, &spi_init);
}
void spi_hw_init(void)
{
spi_hw_clock_enable();
spi_hw_gpio_init();
spi_hw_config();
SPI_Cmd(SPI1, ENABLE);
}
void spi_hw_start(void)
{
GPIO_ResetBits(GPIOA, GPIO_Pin_4);
}
void spi_hw_end(void)
{
GPIO_SetBits(GPIOA, GPIO_Pin_4);
}
uint8_t spi_hw_transfer(uint8_t tx_byte)
{
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET);
SPI_I2S_SendData(SPI1, tx_byte);
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET);
return SPI_I2S_ReceiveData(SPI1);
}
(四)W25Q64闪存操作
W25Q64是一款容量为64Mbit的串行Flash存储器,具有掉电数据不丢失特性,支持SPI模式0和模式3。其存储空间较大,地址宽度为24位。操作W25Q64需遵循以下规则:
(1)每次操作前需先发送指令码,再发送地址码指定目标区域;
(2)写入数据前必须先发送写使能指令;
(3)写入前必须先擦除对应区域,擦除后所有位变为1;
(4)擦除和写入操作需要一定时间,此时芯片处于忙状态,无法响应新的读写请求;
主要指令码包括:读取状态寄存器(0x05)、写使能(0x06)、页编程(0x02)、读取数据(0x03)、扇区擦除(0x20)等。
(1)等待芯片空闲
进行任何操作前都需检查芯片状态,确保不在忙碌状态。读取状态寄存器,判断最低位是否为0。
void flash_wait_ready(void)
{
spi_hw_start();
spi_hw_transfer(0x05);
while ((spi_hw_transfer(0xFF) & 0x01) != 0);
spi_hw_end();
}
(2)写入数据
W25Q64支持连续地址自动递增,可一次写入多个字节。写入前需等待芯片就绪,发送写使能指令,然后发送页编程指令和24位起始地址,最后写入数据。
W25Q64存储结构划分为块、扇区和页。整片8MB存储空间包含128个64KB的块,每块包含16个4KB的扇区,每扇区包含16个256字节的页。写入超过一页长度的数据会导致地址回绕,覆盖页起始位置的数据。
void flash_write_page(uint32_t addr, uint8_t* data_buf, uint8_t len)
{
uint8_t i;
flash_wait_ready();
spi_hw_start();
spi_hw_transfer(0x06);
spi_hw_end();
spi_hw_start();
spi_hw_transfer(0x02);
spi_hw_transfer(addr >> 16);
spi_hw_transfer(addr >> 8);
spi_hw_transfer(addr);
for (i = 0; i < len; i++)
{
spi_hw_transfer(data_buf[i]);
}
spi_hw_end();
}
地址右移16位得到高8位,右移8位得到中8位,保留低8位。SPI传输采用高位先行方式。
(3)读取数据
读取操作同样需先检查芯片状态,然后发送读取指令和起始地址,随后连续读取数据,地址会自动递增。
void flash_read_data(uint32_t addr, uint8_t* data_buf, uint32_t len)
{
uint32_t i;
flash_wait_ready();
spi_hw_start();
spi_hw_transfer(0x03);
spi_hw_transfer(addr >> 16);
spi_hw_transfer(addr >> 8);
spi_hw_transfer(addr);
for (i = 0; i < len; i++)
{
data_buf[i] = spi_hw_transfer(0xFF);
}
spi_hw_end();
}
(4)扇区擦除
W25Q64不支持字节或页级别的单独擦除,只能按扇区(4KB)、块(64KB)或整片进行擦除。以下函数实现扇区擦除,擦除后该扇区内所有数据位变为1。
void flash_erase_sector(uint32_t addr)
{
flash_wait_ready();
spi_hw_start();
spi_hw_transfer(0x06);
spi_hw_end();
spi_hw_start();
spi_hw_transfer(0x20);
spi_hw_transfer(addr >> 16);
spi_hw_transfer(addr >> 8);
spi_hw_transfer(addr);
spi_hw_end();
}
(5)完整驱动代码
将SPI初始化与Flash操作函数封装在一起,便于统一调用。
#ifndef __W25Q64_H__
#define __W25Q64_H__
void flash_init(void);
void flash_write_page(uint32_t addr, uint8_t* data_buf, uint8_t len);
void flash_read_data(uint32_t addr, uint8_t* data_buf, uint32_t len);
void flash_erase_sector(uint32_t addr);
#endif
#include "stm32f10x.h"
#include "hw_spi.h"
void flash_init(void)
{
spi_hw_init();
}
void flash_wait_ready(void)
{
spi_hw_start();
spi_hw_transfer(0x05);
while ((spi_hw_transfer(0xFF) & 0x01) != 0);
spi_hw_end();
}
void flash_write_page(uint32_t addr, uint8_t* data_buf, uint8_t len)
{
uint8_t i;
flash_wait_ready();
spi_hw_start();
spi_hw_transfer(0x06);
spi_hw_end();
spi_hw_start();
spi_hw_transfer(0x02);
spi_hw_transfer(addr >> 16);
spi_hw_transfer(addr >> 8);
spi_hw_transfer(addr);
for (i = 0; i < len; i++)
{
spi_hw_transfer(data_buf[i]);
}
spi_hw_end();
}
void flash_read_data(uint32_t addr, uint8_t* data_buf, uint32_t len)
{
uint32_t i;
flash_wait_ready();
spi_hw_start();
spi_hw_transfer(0x03);
spi_hw_transfer(addr >> 16);
spi_hw_transfer(addr >> 8);
spi_hw_transfer(addr);
for (i = 0; i < len; i++)
{
data_buf[i] = spi_hw_transfer(0xFF);
}
spi_hw_end();
}
void flash_erase_sector(uint32_t addr)
{
flash_wait_ready();
spi_hw_start();
spi_hw_transfer(0x06);
spi_hw_end();
spi_hw_start();
spi_hw_transfer(0x20);
spi_hw_transfer(addr >> 16);
spi_hw_transfer(addr >> 8);
spi_hw_transfer(addr);
spi_hw_end();
}
(五)功能验证
在主函数中测试Flash的读写功能。先擦除指定扇区,然后写入测试数据,再读取验证。
#include "stm32f10x.h"
#include "OLED.h"
#include "w25q64.h"
int main(void)
{
uint8_t write_buf[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
uint8_t read_buf[5];
uint8_t i;
OLED_Init();
flash_init();
flash_erase_sector(0x00000000);
flash_write_page(0x00000000, write_buf, 10);
flash_read_data(0x00000000, read_buf, 5);
OLED_ShowString(1, 1, "Write:");
for (i = 0; i < 10; i++)
{
OLED_ShowNum(2, i + 1, write_buf[i], 1);
}
OLED_ShowString(3, 1, "Read:");
for (i = 0; i < 5; i++)
{
OLED_ShowNum(4, i + 1, read_buf[i], 1);
}
while (1);
return 0;
}