1. 基于HAL库配置SPI1总线
使用STM32CubeMX生成SPI1配置代码,HAL库会自动初始化该函数,因为本来存在虚函数。如下代码所示:
//SPI5底层驱动,时钟使能,引脚配置
//此函数会被HAL_SPI_Init()调用
//hspi:SPI句柄
void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi)
{
GPIO_InitTypeDef GPIO_Initure;
__HAL_RCC_GPIOA_CLK_ENABLE(); //使能GPIOA时钟
__HAL_RCC_SPI1_CLK_ENABLE(); //使能SPI1时钟
//PA5,6,7
GPIO_Initure.Pin=GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7;
GPIO_Initure.Mode=GPIO_MODE_AF_PP; //复用推挽输出
GPIO_Initure.Pull=GPIO_PULLUP; //上拉
GPIO_Initure.Speed=GPIO_SPEED_HIGH; //快速
GPIO_Initure.Alternate=GPIO_AF5_SPI1; //复用为SPI1
HAL_GPIO_Init(GPIOA,&GPIO_Initure);
}
2. W25Q64芯片
- W25Q64 (64M-bit), W25Q16(16M-bit)和W25Q32(32M-bit)是为系统提供一个最小的空间、引脚和功耗的存储器解决方案的串行 Flash 存储器。 25Q系列比普通的串行 Flash 存储器更灵活,性能更优越。基于双倍/四倍的 SPI,它们能够可以立即完成提供数据给 RAM,包括存储声音、文本和数据。芯片支持的工作电压 2.7V 到 3.6V,正常工作时电流小于 5mA,掉电时低于1uA。所有芯片提供标准的封装。
- W25Q64/16/32 由每页 256 字节组成。 每页的 256 字节用一次页编程指令即可完成。 每次可以擦除 16 页(1个扇区)、 128 页(32KB 块)、 256 页(64KB 块)和全片擦除。
- W25Q64 的内存空间结构: 一页 256 字节,4K(4096 字节)为一个扇区, 16 个扇区为 1 块, 容量为 8M 字节,共有 128 个块,2048 个扇区。
- W25Q64/16/32 支持标准串行外围接口(SPI),和高速的双倍/四倍输出,双倍/四倍用的引脚:串行时钟、片选端、串行数据I/O0(DI)、 I/O1(DO)、 I/O2(WP)和 I/O3(HOLD)。 SPI 最高支持80MHz,当用快读双倍/四倍指令时,相当于双倍输出时最高速率 160MHz,四倍输出时最高速率 320MHz。这个传输速率比得上 8位和 16 位的并行 Flash 存储器。
- 结构框图如下图所示:
3. 控制和状态寄存器
3.1 状态寄存器
忙是只读的状态寄存器(S0)被设置为1状态时,表示设备正在执行程序(可能是在擦除芯片)或写状态寄存器指令。这个时候设备将忽略传来的指令, 除了读状态寄存器和擦除暂停指令,写指令或写状态指令无效, 当 S0 为 0 状态时指示设备已经执行完毕,可以进行下一步操作。
3.2 指令表
3.3 W25Q64工作原理
3.3.1 读状态寄存器(05hor35h)
读取状态寄存器的指令是 8 位的指令。 发送指令之前, 先将/ CS 拉低, 再发送指令码“05 h”或者“35h”。 设备收到读取状态寄存器的指令后, 将状态信息(高位)依次移位发送出去, 上图 6 示。读出的状态信息, 最低位为 1 代表忙, 最低位为 0 代表可以操作, 状态信息读取完毕, 将片选线拉高。
/******************************************************
函数功能:读状态寄存器1 -05h
形参:无
返回值:rt_uint8_t status -- 返回从器件的状态(忙或空闲)
说明:发送 05H 读取状态寄存器的值之后, W25Q64 就会一直
返回状态寄存器的值,直到片选被拉高。
*******************************************************/
rt_uint8_t w25qxx_read_status(void)
{
rt_uint8_t status = 0;
SPI_FLASH_CS_LOW(); //拉低片选
spi1_read_write_byte(W25X_ReadStatusReg1); //发送指令 0x05
status = spi1_read_write_byte(0xff); //status = 读数据
SPI_FLASH_CS_HIGH();
return status;
}
3.3.2 写使能(06h)
写使能指令将状态寄存器中的Write Enable Latch (WEL)位设置为1。 WEL位必须在每个页面程序、扇区擦除、块擦除、芯片擦除和之前设置 写入状态寄存器指令。 写使能指令通过驱动/CS low输入,将指令码“06h”移至CLK上升沿的数据输入(Data Input, DI)引脚,然后驱动/CS high。
/******************************************************
函数功能:写使能———0x06
形参:无
返回值:无
说明:在执行每一个写操作之前都需要通过写使能指令将状态
寄存器的 WEL 置 1,让芯片进入写使能状态
*******************************************************/
void w25qxx_write_enble(void)
{
SPI_FLASH_CS_LOW(); //拉低片选--选中从器件
spi1_read_write_byte(W25X_WriteEnable); //发送指令 0x06
SPI_FLASH_CS_HIGH(); //拉高片选--释放从器件
}
3.3.3 读数据(03h)
读取数据指令允许按顺序读取一个字节的内存数据。当片选 CS/拉低之后, 紧随其后是一个 24 位的地址(A23-A0)(需要发送 3 次, 每次 8 个字节, 先发高位)。芯片收到地址后,将要读的数据按字节大小转移出去, 数据是先转移高位,对于单片机,时钟下降沿发送数据,上升沿接收数据。 读数据时, 地址会自动增加, 允许连续的读取数据。这意味着读取整个内存的数据,只要用一个指令就可以读完。 数据读取完成之后, 片选信号/ CS 拉高。读取数据的指令序列,如上图所示。 如果一个读数据指令而发出的时候, 设备正在擦除扇区,或者(忙= 1), 该读指令将被忽略,也不会对当前周期有什么影响。
/******************************************************
函数功能: 读字节数据-0x03
形参:
@rt_uint32_t addr : 起始地址
@rt_uint16_t size :读数据长度
@rt_uint8_t *buff :存取读取数据
返回值:无
说明:
********************************************************/
void w25qxx_read_data(rt_uint32_t addr, rt_uint8_t *buff, rt_uint8_t size)
{
rt_uint8_t i;
SPI_FLASH_CS_LOW(); //拉低片选
spi1_read_write_byte(W25X_ReadData); //发送指令 0x03
spi1_read_write_byte((addr & 0xff0000) >> 16);
spi1_read_write_byte((addr & 0x00ff00) >> 8);
spi1_read_write_byte((addr & 0x0000ff) >> 0); //发送 24 位地址,先发送高 8 位
for(i = 0; i < size; i++)
{
buff[i] = spi1_read_write_byte(0xff); //byte = 读数据
}
SPI_FLASH_CS_HIGH(); //拉高片选
}
3.3.4 页编程(02h)
页编程指令允许从一个字节到 256 字节的数据编程(一页)(编程之前必须保证内存空间是 0XFF)。允许写入指令之前,必须先发送设备写使能指令。 写使能开启后, 设备才能接收编程指令。 开启页编程先拉底/ CS, 然后发送指令代码“02 h”, 接着发送一个 24 位地址(A23-A0)(发送 3 次, 每次 8 位) 和至少一个数据字节(数据字节不能超过 256字节)。 数据字节发送完毕, 需要拉高片选线 CS/,, 并判断状态位, 等待写入结束。
/******************************************************
函数功能: 页写 - 0x02
形参:
@rt_uint32_t addr : 起始地址
@rt_uint16_t size :写多数据的长度
@rt_uint8_t *buff :写入的数据
返回值:无
说明:
*******************************************************/
void w25qxx_page_write(rt_uint32_t addr, rt_uint8_t *buff, rt_uint8_t size)
{
rt_uint8_t i;
w25qxx_write_enble(); //写使能
SPI_FLASH_CS_LOW(); //拉低片选--选中从器件
spi1_read_write_byte(W25X_PageProgram); //发送指令 0x02
spi1_read_write_byte((addr & 0xff0000) >> 16);
spi1_read_write_byte((addr & 0x00ff00) >> 8);
spi1_read_write_byte((addr & 0x0000ff) >> 0);
for(i = 0;i < size; i++)
{
spi1_read_write_byte(buff[i]);
}
SPI_FLASH_CS_HIGH();
while(w25qxx_read_status() & (1 << 0)); //判断状态位,等待可以进行写
}
3.3.5 扇区擦除(20h)
扇区擦除指令可以擦除指定一个扇区(4 k 字节)内所有数据, 将内存空间恢复到 0xFF 状态。 写入扇区擦除指令之前必须执行设备写使能(发送设备写使能指令 0x06), 并判断状态寄存器(状态寄存器位最低位必须等于 0才能操作)。发送的扇区擦除指令前, 先拉低/ CS, 接着发送扇区擦除指令码”20 h”, 和 24 位地址(A23-A0), 地址发送完毕后,拉高片选线 CS/,, 并判断状态位, 等待擦除结束。 擦除一个扇区的最少需要 150ms 时间。
/**********************************************************
函数功能: 扇区擦除-0x20
形参:
@rt_uint32_t addr : 起始地址
返回值:无
说明:将指定区域的存储数据全部置为 0xFF,假如传入地址addr,
则被擦除的空间为 addr所在的扇区的整个扇区,而不会偏移到其
他扇区。
***********************************************************/
void w25qxx_sector_erase(rt_uint32_t addr)
{
w25qxx_write_enble();//写使能
SPI_FLASH_CS_LOW();
spi1_read_write_byte(W25X_SectorErase);
spi1_read_write_byte((addr & 0xff0000) >> 16);
spi1_read_write_byte((addr & 0x00ff00) >> 8);
spi1_read_write_byte((addr & 0x0000ff) >> 0);
SPI_FLASH_CS_HIGH();
while(w25qxx_read_status() & (1 << 0));
}
3.3.6 64KB 块擦除
块擦除指令将指定块(64K-bytes)内的所有内存设置为1(FFh)的擦除状态。在设备接受块擦除之前,必须执行允许写指令 (状态寄存器位WEL必须等于1)。指令由驱动/CS引脚低启动 ,然后在一个24位的块地址(A23-A0)之后移动指令代码“D8h” 。在最后一个字节的第8位被锁存后,/CS引脚必须被驱动到高电平。 如果不这样做块擦除指令将不会被执行。 在/CS驱动高后,自动块擦除 指令将在tBE的一段时间内开始(见AC特性)。 而Block Erase循环 时,仍可访问读取状态寄存器指令,以检查忙状态。 BUSY位在块擦除周期中为1,在块擦除周期结束时为0
并且设备已经准备好再次接受其他指令。 在块擦除周期完成后 状态寄存器中的Write Enable Latch (WEL)位被清除为0。 块擦除指令不会被删除 如果寻址页面被Block Protect (SEC, TB, BP2, BP1和BP0)位保护,则执行
状态寄存器内存保护表)。
/**********************************************************
函数功能: 块擦除 - 0xD8
形参:
@rt_uint32_t addr : 起始地址
返回值:无
说明:将指定区域的存储数据全部置为 0xFF,假如传入地址addr,
则被擦除的空间为 addr所在的块区的整个块区,而不会偏移到其
他块区。
***********************************************************/
void w25qxx_block_erase(rt_uint32_t addr)
{
w25qxx_write_enble();//写使能
SPI_FLASH_CS_LOW();
spi1_read_write_byte(W25X_BlockErase);
spi1_read_write_byte((addr & 0xff0000) >> 16);
spi1_read_write_byte((addr & 0x00ff00) >> 8);
spi1_read_write_byte((addr & 0x0000ff) >> 0);
SPI_FLASH_CS_HIGH();
while(w25qxx_read_status() & (1 << 0));
}
3.3.7 全片擦除
全芯片擦除指令, 可以将整个芯片的所有内存数据擦除, 恢复到 0XFF 状态。 写入全芯片擦除指令之前必须执行设备写使能(发送设备写使能指令 0x06), 并判断状态寄存器(状态寄存器位最低位必须等于 0 才能操作)。发送全芯片擦除指令前, 先拉低/ CS, 接着发送擦除指令码”C7h”或者是”60h”, 指令码发送完毕后, 拉高片选线 CS/,, 并判断状态位, 等待擦除结束。 全片擦除指令尽量少用, 擦除会缩短设备的寿命。
/**********************************************************
函数功能: 芯片擦除-0xC7
形参:无
返回值:无
说明:
***********************************************************/
void w25qxx_chip_erase(void)
{
w25qxx_write_enble();//写使能
SPI_FLASH_CS_LOW();
spi1_read_write_byte(W25X_ChipErase);
SPI_FLASH_CS_HIGH();
while(w25qxx_read_status() & (1 << 0));
}
3.3.8 读取制造商/芯片 ID(90h)
读取制造商/设备 ID 指令可以读取,制造商 ID 和特定的设备 ID。 读取之间, 拉低 CS 片选信号, 接着发送指令代码“90h” , 紧随其后的是一个 24 位地址(A23-A0)000000h。 之后, 设备发出, 华邦电子制造商 ID(EFh) 和设备ID(w25q64 为 16h)。如果 24 位地址设置为 000001 h 的设备 ID 会先发出,然后跟着制造商 ID。制造商和设备 ID 可以连续读取。完成指令后, 片选信号/ CS 拉高。
/***********************************************************
函数功能:MCU读取W25Q64的ID函数
函数形参:None
函数返回值:读到ID
备注:90H 厂家0xEF 芯片ID:0x16 正常:0xEF16
************************************************************/
rt_uint16_t w25qxx_read_id(void)
{
rt_uint16_t id;
SPI_FLASH_CS_LOW(); //拉低片选--选中从器件
spi1_read_write_byte(W25X_ManufactDeviceID); //发送指令 0x90
spi1_read_write_byte(0x00);
spi1_read_write_byte(0x00);
spi1_read_write_byte(0x00);
id = spi1_read_write_byte(0xff);
id <<= 8;
id |= spi1_read_write_byte(0xff);
SPI_FLASH_CS_HIGH(); //拉高片选--释放从器件
return id;
}
3.3.9 跨页写
页编程只能在一页中写入数据,不能写到下一页中,若要想连续的写到下一页中,则可以通过地址的偏移来实现;如下代码所示:
/***********************************************************
函数功能:跨页写
@rt_uint32_t addr : 起始地址
@rt_uint8_t *buff :写入的内容
@rt_uint16_t size :写入数据长度
函数返回值:None
备注:
************************************************************/
void w25qxx_auto_write(rt_uint32_t addr, rt_uint8_t *buff, rt_uint8_t size)
{
rt_uint16_t sx_bytes = 0; //保存本页剩下的可写字节数
sx_bytes = 256 - addr%256; //得到本页剩下的可写字节数
if(size <= sx_bytes)
{
sx_bytes = size;
}
while(1)
{
w25qxx_page_write(addr,buff,sx_bytes);
if(sx_bytes == size)
{
break; //写完了
}
addr += sx_bytes; //256
buff += sx_bytes; //写完本页,数据地址偏移sx_bytes
size -= sx_bytes;
if( size <= 255) //说明再一页可写完
{
sx_bytes = size;
}
else
{
sx_bytes = 256;
}
}
}
3.4 片选信号CS,并初始化W25QXX
//初始化SPI FLASH的IO口
void w25qxx_init(void)
{
GPIO_InitTypeDef GPIO_Initure;
__HAL_RCC_GPIOB_CLK_ENABLE(); //使能GPIOB时钟
//PB14
GPIO_Initure.Pin=FLASH_CS_PIN; //PB14
GPIO_Initure.Mode=GPIO_MODE_OUTPUT_PP; //推挽输出
GPIO_Initure.Pull=GPIO_PULLUP; //上拉
GPIO_Initure.Speed=GPIO_SPEED_HIGH; //快速
HAL_GPIO_Init(FLASH_CS_GPIO_PORT,&GPIO_Initure); //初始化
SPI_FLASH_CS_HIGH(); //SPI FLASH不选中
spi1_init(); //初始化SPI
rt_kprintf("w25q64_id = %#X\n",w25qxx_read_id());
}