参考:①正点原子MINI教程②STM32F103配合STM32CubeMX实现SPI读写flash_stm32f103 cube spi_zerfew的博客-CSDN博客
目录
一、理论知识
1、SPI特征
2、SPI框图
3、SPI的工作模式
4、W25Q64
4.1 NOR FLASH 的特性
4.2 W25Q64芯片引脚图
4.3 NOR FLASH 工作时序
5、SPI主要结构体
二、STM32Cube配置
1、片选脚PA2
2、SPI配置
三、代码编写
1、函数声明
2、函数调用
3、函数定义
一、理论知识
1、SPI特征
SPI 是英语 Serial Peripheral interface 缩写,顾名思义就是串行外围设备接口。 SPI 通信协
议是 Motorola 公司首先在其 MC68HCXX 系列处理器上定义的。 SPI 接口是一种高速的全双工
同步的通信总线,已经广泛应用在众多 MCU、存储芯片、 AD 转换器和 LCD 之间。 大部分
STM32 是 有 3 个 SPI 接口。
● 3线全双工同步传输
● 带或不带第三根双向数据线的双线单工同步传输
● 8或16位传输帧格式选择
● 主或从操作
● 支持多主模式
● 8个主模式波特率预分频系数(最大为fPCLK/2)
● 从模式频率 (最大为fPCLK/2)
● 主模式和从模式的快速通信
● 主模式和从模式下均可以由软件或硬件进行NSS管理:主/从操作模式的动态改变
● 可编程的时钟极性和相位
● 可编程的数据顺序, MSB在前或LSB在前
● 可触发中断的专用发送和接收标志
● SPI总线忙状态标志
● 支持可靠通信的硬件CRC
─ 在发送模式下, CRC值可以被作为最后一个字节发送
─ 在全双工模式中对接收到的最后一个字节自动进行CRC校验
● 可触发中断的主模式故障、过载以及CRC错误标志
● 支持DMA功能的1字节发送和接收缓冲器:产生发送和接受请求
2、SPI框图
①SPI 的引脚信息:
MISO(Master In / Slave Out)主设备数据输入,从设备数据输出。
MOSI(Master Out / Slave In)主设备数据输出,从设备数据输入。
SCLK(Serial Clock)时钟信号,由主设备产生。
CS(Chip Select)从设备片选信号,由主设备产生。
②SPI 的工作原理:
在主机和从机都有一个串行移位寄存器,主机通过向它的 SPI 串行寄存
器写入一个字节来发起一次传输。串行移位寄存器通过 MOSI 信号线将字节传送给从机,从机
也将自己的串行移位寄存器中的内容通过 MISO 信号线返回给主机。这样,两个移位寄存器中
的内容就被交换。外设的写操作和读操作是同步完成的。如果只是进行写操作,主机只需忽略
接收到的字节。反之,若主机要读取从机的一个字节,就必须发送一个空字节引发从机传输。
③SPI 的传输方式:
SPI 总线具有三种传输方式:全双工、单工以及半双工传输方式。
全双工通信,就是在任何时刻,主机与从机之间都可以同时进行数据的发送和接收。
单工通信,就是在同一时刻,只有一个传输的方向,发送或者是接收。
半双工通信,就是在同一时刻,只能为一个方向传输数据。
3、SPI的工作模式
CPOL,详称 Clock Polarity,就是时钟极性,当主从机没有数据传输的时候即空闲状态,
SCL 线的电平状态,假如空闲状态是高电平, CPOL=1;若空闲状态时低电平,那么 CPOL = 0。
CPHA,详称 Clock Phase,就是时钟相位。同步通信时,数据的变化和采样都是在时钟边沿上进行的,每一个时钟周期都会有上升沿和下降沿两个边沿,那么数据的变化和采样就分别安排在两个不同的边沿,由于数据在产生和到它稳定是需要一定的时间,那么假如我们在第 1 个边沿信号把数据输出了,从机只能从第 2 个边沿信号去采样这个数据。CPHA 实质指的是数据的采样时刻, CPHA = 0 的情况就表示数据的采样是从第 1 个边沿信号上即奇数边沿,具体是上升沿还是下降沿的问题,是由 CPOL 决定的。CPOL=0,有两种情况:一是 CS 使能的边沿,二是上一帧数据的最后一个时钟沿。CPHA=1 的情况就是表示数据采样是从第 2 个边沿即偶数边沿,它的边沿极性要注意一点,不是和上面 CPHA=0 一样的边沿情况。前面的是奇数边沿采样数据,从 SCL 空闲状态的直接跳变,空闲状态是高电平,那么它就是下降沿,反之就是上升沿。由于 CPHA=1 是偶数边沿采样,所以需要根据偶数边沿判断,假如第一个边沿即奇数边沿是下降沿,那么偶数边沿的边
沿极性就是上升沿。 SPI 分成了 4 种模式。使用比较多的是模式 0 和模式 3。
模式0:
CPOL=0&&CPHA=0 的时序,上图 就是串行时钟的奇数边沿上升沿采样的情况,首先由于配置了 CPOL=0,可以看到当数据未发送或者发送完毕, SCL 的状态是低电平,再者 CPHA=0 即是奇数边沿采集。所以传输的数据会在奇数边沿上升沿被采集,MOSI 和 MISO 数据的有效信号需要在 SCK 奇数边沿保持稳定且被采样,在非采样时刻MOSI 和 MISO 的有效信号才发生变化。
模式3:
上图是 CPOL=1&&CPHA=1 的情形,可以看到未发送数据和发送数据完毕, SCL
的状态是高电平,奇数边沿的边沿极性是上升沿,偶数边沿的边沿极性是下降沿。因为
CPHA=1, 所以数据在偶数边沿上升沿被采样。在奇数边沿的时候 MOSI 和 MISO 会发生变化,
在偶数边沿时候是稳定的。
SPI 控制寄存器 1(SPI_CR1)
SPI 状态寄存器(SPI_SR)
SPI 数据寄存器(SPI_DR)
4、W25Q64
4.1 NOR FLASH 的特性
W25Q64 是一款大容量 SPI FLASH 产品,其容量为 8M。它将 8M 字节的容量分为 128 个
块(Block),每一个块大小为 64K 字节,每个块又分为 16 个扇区(Sector),每一个扇区 16 页,
每页 256 个字节,即每个扇区 4K 字节。
W25Q64 的最小擦除单位为一个扇区,也就是每次必
须擦除 4K 个字节。 这样我们需要给 W25Q64 开辟一个至少 4K 的缓存区,这样对 SRAM 要求
比较高,要求芯片必须有 4K 以上的 SRAM 才能很好的操作。W25Q64 的擦写周期多达 10W 次,具有 20 年的数据保存期限,支持电压为 2.7~3.6V,W25Q64 支持标准的 SPI,还支持双输出/四输出的 SPI,最大 SPI 时钟可以到 80Mhz(双输出时相当于 160Mhz,四输出时相当于 320Mhz)。
读flash很简单,仅需要执行spi对应的指令即可;
写flash较为复杂,首先要知道,flash不支持覆盖写,仅支持擦除后再写,原因是flash的写操作只能将1变为0,不能将0变为1,若想将0变为1则只能通过擦除操作,所以写flash时,如果对应的位置值不是全1,则需要先执行擦除操作,再执行写操作。
W25Q64的最小擦除单位为一个扇区,也就是每次必须擦除 4K 个字节。所以在程序中定义一个至少4K的数组变量,用于保存待擦除扇区的内容。
4.2 W25Q64芯片引脚图
芯片引脚连接如下: CS 即片选信号输入,低电平有效; DO 是 MISO 引脚, 在 CLK 管脚
的下降沿输出数据; WP 是写保护管脚, 高电平可读可写,低电平仅仅可读; DI 是 MOSI 引脚,
主机发送的数据、地址和命令从 SI 引脚输入到芯片内部,在 CLK 管脚的上升沿捕获捕获数据;
CLK 是串行时钟引脚,为输入输出提供时钟脉冲; HOLD 是保持管脚,低电平有效。
STM32F103 通过 SPI 总线连接到 W25Q64 对应的引脚即可启动数据传输。
4.3 NOR FLASH 工作时序
①读操作时序
读数据指令是 03H,可以读出一个字节或者多个字节。发起读操作时,先把
CS 片选管脚拉低,然后通过 MOSI 引脚把 03H 发送芯片,之后再发送要读取的 24 位地址,这
些数据在 CLK 上升沿时采样。芯片接收完 24 位地址之后,就会把相对应地址的数据在 CLK
引脚下降沿从 MISO 引脚发送出去。从图中可以看出只要 CLK 一直在工作,那么通过一条读
指令就可以把整个芯片存储区的数据读出来。当主机把 CS 引脚拉高,数据传输停止
②页写时序
在发送页写指令之前,需要先发送“写使能”指令。然后主机拉低 CS 引脚,然后通过
MOSI 引脚把 02H 发送到芯片,接着发送 24 位地址,最后你就可以发送你需要写的字节数据
到芯片。完成数据写入之后,需要拉高 CS 引脚,停止数据传输。
③扇区擦除时序
扇区擦除指的是将一个扇区擦除, W25Q64 的扇区大小是 4K 字节。擦除扇区后,扇区的位全置 1,即扇区字节为 FFh。同样的,在执行扇区擦除之前,需要先执行写使能指令。这里需要注意的是当前 SPI 总线的状态,假如总线状态是 BUSY,那么这个扇区擦除是无效的,所以在拉低 CS 引脚准备发送数据前,需要先要确定 SPI 总线的状态,这就需要执行读状态寄存器指令,读取状态寄存器的 BUSY 位,需要等待 BUSY 位为 0,才可以执行擦除工作。接着按时序图分析,主机先拉低 CS 引脚,然后通过 MOSI 引脚发送指令代码 20h 到芯片,然后接着把 24 位扇区地址发送到芯片,然后需要拉高 CS 引脚,通过读取寄存器状态等待扇区擦除操作完成。
此外还有对整个芯片进行擦除的操作,时序比扇区擦除更加简单,不用发送 24bit 地址,只需要发送指令代码 C7h 到芯片即可实现芯片的擦除。在 W25Q64 手册中还有许多种方式的读/写/擦除操作,参考 W25Q64 手册
5、SPI主要结构体
typedef struct __SPI_HandleTypeDef
{
SPI_TypeDef *Instance; /* SPI 寄存器基地址 */
SPI_InitTypeDef Init; /* SPI 通信参数 */
uint8_t *pTxBuffPtr; /* SPI 的发送缓存 */
uint16_t TxXferSize; /* SPI 的发送数据大小 */
__IO uint16_t TxXferCount; /* SPI 发送端计数器 */
uint8_t *pRxBuffPtr; /* SPI 的接收缓存 */
uint16_t RxXferSize; /* SPI 的接收数据大小 */
__IO uint16_t RxXferCount; /* SPI 接收端计数器 */
void (*RxISR)(struct __SPI_HandleTypeDef *hspi); /* SPI 的接收端中断服务函数 */
void (*TxISR)(struct __SPI_HandleTypeDef *hspi); /* SPI 的发送端中断服务函数 */
DMA_HandleTypeDef *hdmatx; /* SPI 发送参数设置(DMA) */
DMA_HandleTypeDef *hdmarx; /* SPI 接收参数设置(DMA) */
HAL_LockTypeDef Lock; /* SPI 锁对象 */
__IO HAL_SPI_StateTypeDefState; /* SPI 传输状态 */
__IO uint32_t ErrorCode; /* SPI 操作错误代码 */
} SPI_HandleTypeDef;
typedef struct
{
uint32_t Mode; /* 模式:主(SPI_MODE_MASTER)从(SPI_MODE_SLAVE) */
uint32_t Direction; /* 方向:只接收模式 单线双向通信数据模式 全双工 */
uint32_t DataSize; /* 数据帧格式: 8 位/16 位 */
uint32_t CLKPolarity; /* 时钟极性 CPOL 高/低电平 */
uint32_t CLKPhase; /* 时钟相位 奇/偶数边沿采集 */
uint32_t NSS; /* SS 信号由硬件(NSS)管脚控制还是软件控制 */
uint32_t BaudRatePrescaler; /* 设置 SPI 波特率预分频值*/
uint32_t FirstBit; /* 起始位是 MSB 还是 LSB */
uint32_t TIMode; /* 帧格式 SPI motorola 模式还是 TI 模式 */
uint32_t CRCCalculation; /* 硬件 CRC 是否使能 */
uint32_t CRCPolynomial; /* 设置 CRC 多项式*/
} SPI_InitTypeDef;
二、STM32Cube配置
1、片选脚PA2
可选为上拉模式,Reset状态即低电平状态CS片选有效
2、SPI配置
选择全双工主机模式,设置CPOL CPHA,一般常用SPI工作模式0 和 3,CRC校验看需求开启
三、代码编写
1、函数声明
/* USER CODE BEGIN PFP */
void flash_write(uint8_t *pbuffer, uint32_t write_addr, uint16_t write_len);
void flash_write_page(uint8_t *pbuffer, uint32_t write_addr, uint16_t write_len);
void flash_erase(uint32_t fan_addr);
void spi_read_flash(uint8_t *pbuffer, uint32_t read_addr, uint16_t read_len);
void spi_write_flash(uint8_t *pbuffer, uint32_t write_addr, uint16_t write_len);
/* USER CODE END PFP */
2、函数调用
/* USER CODE BEGIN 2 */
uint8_t writeMsg[100] = "1111111111";
uint8_t readMsg[100] = "";
uint32_t flash_size = 8*1024*1024;
spi_write_flash(writeMsg, flash_size - 1000, sizeof(writeMsg));
spi_read_flash(readMsg, flash_size - 1000, sizeof(readMsg));
/* USER CODE END 2 */
3、函数定义
/* USER CODE BEGIN 4 */
void flash_write(uint8_t *pbuffer, uint32_t write_addr, uint16_t write_len)
{
uint16_t page_remain; // 当前页面剩余可写字节数
// 计算当前页剩余可写字节数
page_remain = 256 - write_addr % 256;
// 如果要写入的数据长度小于等于当前页剩余可写字节数,直接写入当前页
if (write_len <= page_remain)
{
page_remain = write_len;
flash_write_page(pbuffer, write_addr, page_remain);
} else
{
// 如果要写入的数据长度超过当前页剩余可写字节数,需要进行分页写入
while (1)
{
flash_write_page(pbuffer, write_addr, page_remain); // 写入当前页数据
if (write_len == page_remain)
{
break; // 数据已全部写入
} else
{
// 更新缓冲区指针、写入地址和剩余长度
pbuffer += page_remain;
write_addr += page_remain;
write_len -= page_remain;
// 根据剩余长度判断下一页要写入的字节数
if (write_len > 256)
{
page_remain = 256;
} else
{
page_remain = write_len;
}
}
}
}
}
void flash_write_page(uint8_t *pbuffer, uint32_t write_addr, uint16_t write_len)
{
uint16_t i = 0;
uint8_t send_cmd = 0; // 发送指令
uint8_t recv_cmd = 0; // 接收指令
// 写使能
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET);
send_cmd = 0x06; // 写使能指令
HAL_SPI_TransmitReceive(&hspi1, &send_cmd, &recv_cmd, 1, 0);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET);
// 写页数据
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET);
send_cmd = 0x02; // 写页指令
HAL_SPI_TransmitReceive(&hspi1, &send_cmd, &recv_cmd, 1, 0);
send_cmd = (uint8_t) (write_addr >> 16); // 获取写入地址的高位字节
HAL_SPI_TransmitReceive(&hspi1, &send_cmd, &recv_cmd, 1, 0); // 发送地址高位并接收响应
send_cmd = (uint8_t) (write_addr >> 8); // 获取写入地址的中间位字节
HAL_SPI_TransmitReceive(&hspi1, &send_cmd, &recv_cmd, 1, 0); // 发送地址中间位并接收响应
send_cmd = (uint8_t) write_addr; // 获取写入地址的低位字节
HAL_SPI_TransmitReceive(&hspi1, &send_cmd, &recv_cmd, 1, 0); // 发送地址低位并接收响应
for (i = 0; i < write_len; i++)
{
send_cmd = pbuffer[i]; // 获取要写入的数据
HAL_SPI_TransmitReceive(&hspi1, &send_cmd, &recv_cmd, 1, 0); // 发送数据并接收响应
}
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET); // 禁用片选信号(CS)
while (HAL_SPI_GetState(&hspi1) != HAL_SPI_STATE_READY)
; // 等待 SPI 完成操作
}
void flash_erase(uint32_t fan_addr)
{
uint8_t send_cmd = 0; // 发送指令
uint8_t recv_cmd = 0; // 接收指令
fan_addr *= 4096; // 将扇区地址转换为字节地址(每个扇区大小为 4096 字节)
// 写使能
send_cmd = 0x06; // 写使能指令
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET);
HAL_SPI_TransmitReceive(&hspi1, &send_cmd, &recv_cmd, 1, 0);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET);
while (HAL_SPI_GetState(&hspi1) != HAL_SPI_STATE_READY){}
// 扇区擦除
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET);
send_cmd = 0x20; // 扇区擦除指令
HAL_SPI_TransmitReceive(&hspi1, &send_cmd, &recv_cmd, 1, 0);
//8M 23字节?
send_cmd = (uint8_t) (fan_addr >> 16); // 获取擦除地址的高位字节
HAL_SPI_TransmitReceive(&hspi1, &send_cmd, &recv_cmd, 1, 0); // 发送地址高位并接收响应
send_cmd = (uint8_t) (fan_addr >> 8); // 获取擦除地址的中间位字节
HAL_SPI_TransmitReceive(&hspi1, &send_cmd, &recv_cmd, 1, 0); // 发送地址中间位并接收响应
send_cmd = (uint8_t) fan_addr; // 获取擦除地址的低位字节
HAL_SPI_TransmitReceive(&hspi1, &send_cmd, &recv_cmd, 1, 0); // 发送地址低位并接收响应
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET); // 禁用片选信号(CS)
while (HAL_SPI_GetState(&hspi1) != HAL_SPI_STATE_READY)
;
}
void spi_read_flash(uint8_t *pbuffer, uint32_t read_addr, uint16_t read_len)
{
uint8_t send_cmd; // 发送指令
uint8_t recv_cmd; // 接收指令
uint16_t i;
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET); // 选择片选信号(CS)低电平使能
send_cmd = 0x03; // 读数据指令
HAL_SPI_TransmitReceive(&hspi1, &send_cmd, &recv_cmd, 1, 0); // 发送读指令并接收响应
//8M 23位? (24位)
send_cmd = (uint8_t) (read_addr >> 16); // 获取读取地址的高位字节
HAL_SPI_TransmitReceive(&hspi1, &send_cmd, &recv_cmd, 1, 0); // 发送地址高位并接收响应
send_cmd = (uint8_t) (read_addr >> 8); // 获取读取地址的中间位字节
HAL_SPI_TransmitReceive(&hspi1, &send_cmd, &recv_cmd, 1, 0); // 发送地址中间位并接收响应
send_cmd = (uint8_t) read_addr; // 获取读取地址的低位字节
HAL_SPI_TransmitReceive(&hspi1, &send_cmd, &recv_cmd, 1, 0); // 发送地址低位并接收响应
send_cmd = 0xff; // 发送空数据以接收闪存中的数据
for (i = 0; i < read_len; i++)
{
HAL_SPI_TransmitReceive(&hspi1, &send_cmd, &recv_cmd, 1, 0); // 发送空数据并接收闪存数据
pbuffer[i] = recv_cmd; // 将接收到的数据存储在缓冲区中
}
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET); // 禁用片选信号(CS)
}
uint8_t flash_buffer[4096]; // 用于缓存闪存数据的缓冲区
void spi_write_flash(uint8_t *pbuffer, uint32_t write_addr, uint16_t write_len)
{
uint32_t sector_index; // 扇区索引
uint16_t sector_offset; // 扇区内的偏移
uint16_t sector_remain; // 扇区内剩余可写字节数
uint16_t i = 0; // 循环计数器
sector_index = write_addr / 4096; // 计算扇区索引
sector_offset = write_addr % 4096; // 计算扇区内偏移
sector_remain = 4096 - sector_offset; // 计算扇区内剩余可写字节数
// 如果要写入的数据长度小于等于扇区内剩余可写字节数,直接写入扇区
if (write_len <= sector_remain)
{
sector_remain = write_len;
flash_write(pbuffer, write_addr, sector_remain);
} else
{
// 如果要写入的数据长度超过扇区内剩余可写字节数,需要分多次写入
while (1)
{
spi_read_flash(flash_buffer, sector_index * 4096, 4096); // 读取整个扇区的数据
// 检查扇区内是否有非擦除状态的数据
for (i = 0; i < sector_remain; i++)
{
if (flash_buffer[sector_offset + i] != 0xFF)
{
break; // 如果存在非擦除状态的数据,跳出循环
}
}
if (i < sector_remain)
{
flash_erase(sector_index); // 擦除整个扇区
// 将新数据写入缓冲区
for (i = 0; i < sector_remain; i++)
{
flash_buffer[sector_offset + i] = pbuffer[i];
}
flash_write(flash_buffer, sector_index * 4096, 4096); // 将缓冲区数据写入扇区
} else
{
flash_write(pbuffer, write_addr, sector_remain); // 直接写入扇区
}
if (write_len == sector_remain)
{
break; // 数据已全部写入
} else
{
sector_index++;
sector_offset = 0;
pbuffer += sector_remain;
write_addr += sector_remain;
write_len -= sector_remain;
if (write_len > 4096)
{
sector_remain = 4096;
} else
{
sector_remain = write_len;
}
}
}
}
}
/* USER CODE END 4 */