本帖最后由 浅末哈哈 于 2024-9-18 08:37 编辑
本帖最后由 浅末哈哈 于 2024-9-18 08:36 编辑
本帖最后由 浅末哈哈 于 2024-9-18 08:35 编辑
本帖最后由 浅末哈哈 于 2024-9-17 17:12 编辑
本帖最后由 浅末哈哈 于 2024-9-14 08:11 编辑
本帖最后由 浅末哈哈 于 2024-9-14 08:10 编辑
本帖最后由 浅末哈哈 于 2024-9-13 22:09 编辑
本帖最后由 浅末哈哈 于 2024-9-13 20:44 编辑
本帖最后由 浅末哈哈 于 2024-9-12 23:15 编辑
本帖最后由 浅末哈哈 于 2024-9-12 22:35 编辑
本帖最后由 浅末哈哈 于 2024-9-11 23:02 编辑
Flash编程原理
上一篇SPI详解:【Ai-WB2中级篇】Ai-WB2+SPI - Ai-WB2系列 - 物联网开发者社区-安信可论坛 - Powered by Discuz! (ai-thinker.com)
1、实验结果展示
-
在写入填充值之前,先进行一次读取数据,读取1K数据并打印;
-
在相同的地址中进行填充值(0x55),再进行读取;
驱动能够正确对flash进行读写操作。
2、Flash主要内容
2.1、Flash的主要知识
- Flash中单位描述为块(Block)、扇区(Sector)、页(Page)。三者之间的关系是:块 > 扇区 > 页;
- Flash擦写规则:
- 最小可擦除单位:扇区;
- 可选择擦除单位:扇区、块、全片擦除;
- 最小可写入单位:1 byte;
- 未写入Flash初始值:0xFF;
- Flash不存在覆写功能。Flash只能从1 -> 0,不能从0 -> 1,所以在写入有数据的部分前,需要对写入部分进行擦除,再将数据进行写入;
2.2、W25Q64中需要关注的部分
Flash部分选择的是W25Q64。根据W25Q64的数据手册上,我们需要关注以下几个部分:
- 硬件封装部分,主要规格参数;
- SPI通信模式;
- 存储区内部结构;
- 对Flash操作的寄存器与指令;
2.2.1、硬件规格参数
W25Q64的封装是8-pin SOIC类型。
根据数据手册上基本介绍,W25Q64有以下特性:
- 容量:8MB(64Mbits)。
- 支持标准SPI(单线),Dual SPI(2线),Qual SPI(4线)。(注:这里几线的意思是几个输出数据线)
- W25Q64 支持的最高时钟是 133MHz。
- 每个扇区最少支持 10 万次擦写,可以保存 20 年数据。
- 页大小是 256 字节,支持页编程,也就是一次编写 256 个字节,也可以一个一个编写。
- 支持 4KB 为单位的扇区擦除,也可以 32KB 或者 64KB 为单位的擦除。
擦写耗时:
- 页编程时间典型值0.4ms,最大值3ms;
- 扇区擦除时间典型值45ms,最大值400ms;
- 块擦除(32KB)时间典型值120ms,最大值1600ms;
- 块擦除(64KB)时间典型值150ms,最大值2000ms;
- 整片擦除典型值20s,最大值100s;
还有一个比较关键的参数,支持时钟频率:
关于读取数据指令 0x03
,所支持最大频率为50MHz,其他指令支持频率最大为133MHz(3.0-3.6V)。
2.2.2、SPI通信模式
W25Q64支持三种SPI的连接方式:标准SPI(单线),Dual SPI(2线),Qual SPI(4线)
- 支持两线 SPI,用到引脚 CLK、CS、IO0、IO1 。
- 支持四线 SPI,用到引脚 CLK、CS、IO0、IO1,IO2、IO3。
都能对W25Q64进行操作,不过Dual/Qual SPI模式一般使用在XIP(executed in place)的情况。
这里我使用的flash模块使用的是标准SPI接口。
W25Q64能够在SPI的mode0和mode3的场景下进行通信。
以读取JEDEC ID指令为例:
SPI中mode0和mode3都能与模块进行正常通信。
2.2.3、存储区的内部结构
W25Q64内部存储区结构:
块(Block):包含128个块,每个块大小为64KB;
扇区(Sector):每个块中包含16个扇区,每个扇区大小为4KB;
最小擦除单位:扇区,但是写入数据最小单位是1byte,所以在写入数据之前应该对相应扇区进行擦除。
2.2.4、W25Q64中的状态寄存器
W25Q64中有三个寄存器,但是只是使用标准SPI进行Flash的读写,就只需要关注Status Register-1。
Status Register-1:
我们只需要关注BUSY这一位,其他位不用关注。
BUSY位是只读的状态寄存器(S0)被设置为1状态时,表示设备正在执行程序(可能是在擦除芯片)或写状态寄存器指令。
这个时候设备将忽略传来的指令, 除了读状态寄存器和擦除暂停指令,写指令或写状态指令无效,当 BUSY位为 0 状态时,指示设备已经执行完毕,可以进行下一步操作。
2.2.5、Flash的相关操作与指令
需要编写flash的驱动,就需要关注如何与flash进行通信。
现将W25Q64的操作指令分为:擦除,写入数据,写入状态寄存器,读取数据,读取设备信息,读取寄存器这几种分类,但是对于flash的基本功能只需要其中的几种指令。
所有指令可以查阅W25Q64的数据手册,这里只介绍驱动中使用了的指令:
#define W25X_WRITE_ENABLE 0x06 //写使能
#define W25X_WRITE_DISABLE 0x04 //写失能
#define W25X_READ_STATUS_REG 0x05 //读取状态寄存器1
#define W25X_WRITE_STATUS_REG 0x01 //写状态寄存器1
#define W25X_READ_DATA 0x03 //读取数据
#define W25X_PAGE_PROGRAM 0x02 //页编程
#define W25X_BLOCK_ERASE 0xD8 //块擦除
#define W25X_SECTOR_ERASE 0x20 //扇区擦除
#define W25X_CHIP_ERASE 0xC7 //整片擦除
#define W25X_POWER_DOWN 0xB9 //关机
#define W25X_DEVICE_ID 0xAB //读取设备ID
#define W25X_MANBUFAT_DEVICE_ID 0x90 //读取厂商ID
#define W25X_JEDEC_DEVICE_ID 0x9F //读取JEDEC ID
特别注意:根据不同的连接方式,所使用的指令都是不同的,这里所描述背景是基于标准SPI的连接方式。若需要Dual/Qual SPI的连接方式,需要查阅数据手册。
3、主要文件结构
- 文件目录结构:
- 程序主要框架:
- 核心构建思想:
在
bsp_spi_bus.c
中,有三个非常关键的参数贯穿整个Flash驱动:
uint16_t g_buffer_len;
uint8_t g_tx_buffer[SPI_BUFFER_SIZE];
uint8_t g_rx_buffer[SPI_BUFFER_SIZE];
g_buffer_len
– buffer长度;
g_tx_buffer
– 开辟的tx_buffer空间,方便后期增加DMA功能;
g_rx_buffer
– 开辟的rx_buffer空间,方便后期增加DMA功能;
首先要明确,标准SPI通信是全双工的通信,即在发送数据的同时能够接收数据。
所以这里直接使用 hosal_spi.c
中的api:
/**
* @brief spi send data and recv
*
* @param[in] spi the spi device
* @param[in] tx_data spi send data
* @param[out] rx_data spi recv data
* @param[in] size spi data to be sent and recived
* @param[in] timeout timeout in milisecond, set this value to HAL_WAIT_FOREVER
* if you want to wait forever
*
* @return
* - 0 : success
* - other : error
*/
int hosal_spi_send_recv(hosal_spi_dev_t *spi, uint8_t *tx_data, uint8_t *rx_data, uint16_t size, uint32_t timeout);
将其封装为底层SPI发送和接收函数,提供给驱动层(bsp_spi_flash.c
)调用:
/**
* @brief send and recivce
*
*/
void bsp_spi_bus_transfer(void)
{
if (g_buffer_len > SPI_BUFFER_SIZE)
{
printf("buffer size overflow\r\n");
return;
}
if (hosal_spi_send_recv(&spi0, (uint8_t *)g_tx_buffer, (uint8_t *)g_rx_buffer, g_buffer_len, 1000000) != 0)
{
printf("spi transfer error\r\n");
}
}
在驱动层对 g_tx_buffer
进行填充,或者等待 g_rx_buffer
接收,伪代码:
//扇区擦除示例伪代码
g_buffer_len = 0;
g_tx_buffer[g_buffer_len ++] = W25X_SECTOR_ERASE;
g_tx_buffer[g_buffer_len ++] = ((sector_addr & 0xFF0000) >> 16);
g_tx_buffer[g_buffer_len ++] = ((sector_addr & 0xFF00) >> 8);
g_tx_buffer[g_buffer_len ++] = (sector_addr & 0xFF);
bsp_spi_bus_transfer();
4、编写驱动思路
主要思路有两个:
- 在操作flash前,需要将写使能打开,操作结束后关闭写使能;
- 在写入数据之前,需要先对相应位置进行擦除;
4.1、主要函数讲解
flash的基本功能:擦除,读取数据,写入数据;
4.1.1、擦除数据功能函数
包含全片擦除,块擦除,扇区擦除三个功能函数。
/**
* @brief erase chip
*
*/
void flash_erase_chip(void);
/**
* @brief erase block
*
* @param block_addr - the first block address
* @return uint8_t
* - 1:erase block done
* - 0:erase block error
*/
uint8_t flash_erase_block(uint32_t block_addr);
/**
* @brief erase sector
*
* @param sector_addr - the first sector address
* @return uint8_t
* - 1: erase sector done
* - 0: erase sector error
*/
uint8_t flash_erase_sector(uint32_t sector_addr);
用 flash_erase_sector
函数为例:
uint8_t flash_erase_sector(uint32_t sector_addr)
{
if(flash_get_sector_first_addr_flag(sector_addr) == 0)
{
printf("erase sector error, first sector address is wrong\r\n");
return 0;
}
else
{
flash_write_enable();
flash_set_cs(0);
g_buffer_len = 0;
g_tx_buffer[g_buffer_len ++] = W25X_SECTOR_ERASE;
g_tx_buffer[g_buffer_len ++] = ((sector_addr & 0xFF0000) >> 16);
g_tx_buffer[g_buffer_len ++] = ((sector_addr & 0xFF00) >> 8);
g_tx_buffer[g_buffer_len ++] = (sector_addr & 0xFF);
bsp_spi_bus_transfer();
flash_wait_for_write_end();
flash_set_cs(1);
printf("erase sector done\r\n");
}
return 1;
}
程序流程框图:
4.1.2、读取数据功能函数
读取功能函数包含读取JEDEC_ID,读取Flash存储内容两个功能函数。
/**
* @brief read flash JEDEC ID
*
* @return uint32_t - JEDEC ID
*/
uint32_t flash_read_id(void);
/**
* @brief read flash data
*
* @param pbuf save data when read flash data
* @param read_addr read address
* @param size read size
*/
void flash_buffer_read(uint8_t * pbuf, uint32_t read_addr, uint32_t size);
针对 flash_buffer_read
进行展开
void flash_buffer_read(uint8_t * pbuf, uint32_t read_addr, uint32_t size)
{
uint16_t rem;
uint16_t i;
if((size == 0) || (read_addr + size) > flash_dev_info.total_size)
{
printf("size = %d, (read_addr + size) = %d\r\n", size, (read_addr + size));
printf("flash read buffer error\r\n");
return;
}
flash_write_enable();
flash_set_cs(0);
g_buffer_len = 0;
g_tx_buffer[g_buffer_len ++] = W25X_READ_DATA;
g_tx_buffer[g_buffer_len ++] = ((read_addr & 0xFF0000) >> 16);
g_tx_buffer[g_buffer_len ++] = ((read_addr & 0xFF00) >> 8);
g_tx_buffer[g_buffer_len ++] = (read_addr & 0xFF);
bsp_spi_bus_transfer(); //这里先让flash进入读取模式,避免在读取的时候遗漏前4个bytes
for(i = 0; i < (size / SPI_BUFFER_SIZE); i++)
{
g_buffer_len = SPI_BUFFER_SIZE;
bsp_spi_bus_transfer(); //开始正式读取数据
memcpy(pbuf, g_rx_buffer, SPI_BUFFER_SIZE);
pbuf += SPI_BUFFER_SIZE;
}
rem = size % SPI_BUFFER_SIZE;
if(rem > 0)
{
g_buffer_len = rem;
bsp_spi_bus_transfer();
memcpy(pbuf, g_rx_buffer, rem);
}
flash_wait_for_write_end();
flash_set_cs(1);
}
程序流程框图:
4.1.3、写入数据功能函数
写入数据功能函数包含页写入(不推荐使用),buffer写入(推荐):
/**
* @brief write data on page
*
* @param pbuf prepare to write data
* @param write_addr write address
* @param size buffer size
*/
void flash_page_write(uint8_t * pbuf, uint32_t write_addr, uint16_t size);
/**
* @brief write buffer to flash anywhere you want
*
* @param pbuf prepare to write data
* @param write_addr write address
* @param size buffer size
* @return uint8_t
* - 1: write done
* - 0: write error
*/
uint8_t flash_buffer_write(uint8_t * pbuf, uint32_t write_addr, uint16_t size);
这里不对这一部分做太多展开,因为内容稍微复杂,有兴趣可以查阅代码,实现了自动擦除,可以在任意地址,写入任意大小数据。
flash_buffer_write
函数设计框图:
4.2、demo部分
在demo中,实现了对于1K的数据进行读写来验证驱动是否正常工作:
static void write_1K(uint8_t data)
{
uint32_t i;
for(i = 0; i < TEST_SIZE; i++)
{
buf[i] = data;
}
if(flash_buffer_write(buf, TEST_ADDR, TEST_SIZE) == 0)
{
printf("write 1K Flash error\r\n");
return;
}
printf("write Flash 1K end, write addr:0x%08X\r\n", TEST_ADDR);
}
static void read_1K(void)
{
uint32_t i;
flash_buffer_read(buf, TEST_ADDR, 1024); /* 读数据 */
printf("addr: 0x%08X, data size = 1024\r\n", TEST_ADDR);
/* 打印数据 */
for (i = 0; i < 1024; i++)
{
printf(" %02X", buf[i]);
if ((i & 31) == 31)
{
printf("\r\n"); /* 每行显示16字节数据 */
}
else if ((i & 31) == 15)
{
printf(" - ");
}
}
}
void demo_flash(void)
{
uint8_t ret;
bsp_init_flash();
ret = flash_erase_sector(0x011000);
if(ret == 1)
{
printf("read 1K before write:\r\n");
read_1K();
printf("writing data\r\n");
write_1K(0x55);
printf("read 1K after write:\r\n");
read_1K();
}
else
{
printf("demo is wrong\r\n");
}
}
5、实验过程
5.1、首要操作
在下载完附件之后,将其放入 ..\Ai-Thinker-WB2\applications\iot-solution\
文件路径内。
再打开附件内的 proj_config.mk
,将这句话注释掉(如果自己移植了这个flash的驱动文件就需要注释)
LOG_ENABLED_COMPONENTS:= blog_testc hosal
将BL的blog组件在这几个组件中关掉,避免在查看串口输出日志的时候有太多杂乱的信息。
5.2、编译下载
正常编译下载。
附件
git clone https://gitee.com/HenleyEn/example_w25qxx.git
视频
Ai-WB2Flash编译原理_哔哩哔哩_bilibili
TODO
- dma引入;
- 线程保护;
- 中间件SFUD+FAL+FlashDB(有空再写);
参考资料