FlashDB 的引入
前置条件
这个简单数据库可以以任意存储介质作为实际空间。
对于 flash,通常的最小擦除单位是扇区。
如果是片上 flash,至少需要规划预留一个扇区对齐的区域供 FlashDB 使用。
如果是外部 flash,除了扇区对齐的规划,需要实现并测试读、写、擦除的 API 作为准备工作。
如果是 eeprom,同样的,实现并测试读、写、擦除的 API 作为准备工作,此时会简单很多,不必考虑扇区对齐问题。擦除 API 是为了兼容 FlashDB 默认存储介质是 flash,采用符合 flash 的行为逻辑,实际上可以将擦除 API 做空操作。
FlashDB 会自行处理好扇区对齐问题,所以在实现 API 时不必添加扇区对齐的检查,以减小开销。
引入源码包
在 armink/FlashDB 可以下载到原始源码包。
但是他没有 CMakeLists,需要了解其源码划分,才能正确引入所需的源码。
其实际上必须引入的源码文件夹如下,其他的则可以在工程中删除。下面用更清晰的 ASCII 树展示,方便在 Markdown 中阅读和复制。
FlashDB/
├─ demos/
├─ docs/
├─ inc/ # 需要加入到 include path
├─ port/
│ └─ fal/
│ ├─ docs/
│ ├─ inc/ # 需要加入到 include path
│ ├─ samples/
│ └─ src/ # 需要加入到编译的 .c 文件
├─ samples/
├─ src/ # 需要加入到编译的 .c 文件
├─ tests/
└─ zephyr/
其中标识的 .c 文件需要加入编译,同时加入以下的 includePATH
FlashDB/inc
FlashDB/port/fal/inc
FlashDB/inc 中有 fdb_cfg_template.h,复制一份并重命名为 fdb_cfg.h 放在自己的工程中,比如原地,在 includePATH 范围内就好。
最小起步时其中的默认配置不必修改,只要考虑编写 FDB_PRINT 即可,例如我编写为使用 RTT 打印,或者留空。
#define FDB_PRINT(...) SEGGER_RTT_printf(0, __VA_ARGS__)
// #define FDB_PRINT(...) \
// do \
// { \
// } \
// while(0)
新建一份 fal_cfg.h 放在自己的工程中,例如
#ifndef _FAL_CFG_H_
#define _FAL_CFG_H_
#define FAL_PART_HAS_TABLE_CFG
#define FAL_FLASHDB_DEV_NAME "norflash0"
#define FAL_FLASHDB_KV_PART_NAME "fdb_kv_cfg"
#define FAL_FLASHDB_KV_PART_OFFSET (0x00040000u)
#define FAL_FLASHDB_KV_PART_LENGTH (0x00040000u)
extern struct fal_flash_dev nor_flash0;
#define FAL_FLASH_DEV_TABLE \
{ \
&nor_flash0, \
}
#ifdef FAL_PART_HAS_TABLE_CFG
#define FAL_PART_TABLE \
{ \
{FAL_PART_MAGIC_WORD, FAL_FLASHDB_KV_PART_NAME, FAL_FLASHDB_DEV_NAME, FAL_FLASHDB_KV_PART_OFFSET, FAL_FLASHDB_KV_PART_LENGTH, 0}, \
}
#endif
#endif /* _FAL_CFG_H_ */
为工程引入 fal
注意到上面的 fal_cfg.h 其中有这样的内容,其中 FAL_FLASH_DEV_TABLE 是一个必须定义的 falsh 设备表,这里有一个 nor_flash0 设备描述对象。
这些描述对象与期望操作的 flash 抽象一一对应,比如我这里只有一个 spi-norflash,所以只使用了一个抽象对象。
extern struct fal_flash_dev nor_flash0;
#define FAL_FLASH_DEV_TABLE \
{ \
&nor_flash0, \
}
在现在,虽然使用了声明,但是这个 flash 抽象依然还没有定义,定义这个抽象内容是使其真正具备真实 flash 操作能力的关键。
这里我们可以新建一个源码进行整体绑定,使其具有 读/写/擦 接口。比如如下的核心定义,其中
.len是完整 flash 空间大小,.blk_size是擦除最小单位在 flash 中通常是扇区大小。.ops是 读/写/擦 回调,在回调中使用前置条件中已实现的 API。.write_gran是写最小字节数粒度,例如 norflash 此值定义为 1,而片上 flash 可能要求 4/8 字节对齐。
struct fal_flash_dev nor_flash0 = {
.name = FAL_FLASHDB_DEV_NAME,
.addr = 0,
.len = 1 * 1024 * 1024,
.blk_size = DEVICE_SECTOR_SIZE,
.ops = {fal_norflash_init,
fal_norflash_read,
fal_norflash_write,
fal_norflash_erase},
.write_gran = FDB_WRITE_GRAN,
};
下面会展示为了支持这个抽象内容的完整源码,在这里有一个重要的问题需要处理。因为 fal 似乎并没有按页逐步写的操作,所以在 norflash 的回调处理中,需要注意不要跨页写,分为多次写使能和写请求。
#include <NorFlashSpi.h>
/* Port */
extern uint32_t FlashSpiTransmitPort(_NOR_FLASH_MESSAGE *pd, const uint8_t *TBuf, uint32_t ui32Len);
extern uint32_t FlashSpiReceivePort(_NOR_FLASH_MESSAGE *pd, uint8_t *RBuf, uint32_t ui32Len);
extern uint32_t FlashSpiTransmitReceivePort(_NOR_FLASH_MESSAGE *pd, const uint8_t *TBuf, uint8_t *RBuf, uint32_t ui32Len);
#include <fal.h>
#include <fdb_cfg.h>
#include <includes.h>
extern volatile uint8_t fSpiTxBuf[];
extern volatile uint8_t fSpiRxBuf[];
/* 片选使能函数 */
#define CS_EN( ) HAL_GPIO_WritePin(SPI5_FLASH_CS_GPIO_Port, SPI5_FLASH_CS_Pin, GPIO_PIN_RESET);
/* 片选失能函数 */
#define CS_UN( ) HAL_GPIO_WritePin(SPI5_FLASH_CS_GPIO_Port, SPI5_FLASH_CS_Pin, GPIO_PIN_SET);
/* 定义写入最小单位 */
#define FLASH_PAGE_SIZE 256
#define FALFDB_TRACE(...) SEGGER_RTT_printf(0, "[FALNOR] " __VA_ARGS__)
static int fal_norflash_init(void);
static int fal_norflash_read(long offset, uint8_t *buf, size_t size);
static int fal_norflash_write(long offset, const uint8_t *buf, size_t size);
static int fal_norflash_erase(long offset, size_t size);
static uint32_t fal_norflash_wait_idle(void)
{
uint32_t i;
fSpiTxBuf[0] = CMD_RDSR1;
fSpiTxBuf[1] = 0;
CS_EN( );
if(FlashSpiTransmitReceivePort(NULL, (uint8_t *)fSpiTxBuf, (uint8_t *)fSpiRxBuf, 2) != RETURN_DEFAULT)
{
CS_UN( );
return RETURN_FLASH_READ_ERR;
}
CS_UN( );
i = 0;
while(((fSpiRxBuf[1] & WIP_FLAG) != 0) && (i < 0xFFFF))
{
CS_EN( );
if(FlashSpiTransmitReceivePort(NULL, (uint8_t *)fSpiTxBuf, (uint8_t *)fSpiRxBuf, 2) != RETURN_DEFAULT)
{
CS_UN( );
return RETURN_FLASH_READ_ERR;
}
CS_UN( );
i++;
}
if((fSpiRxBuf[1] & WIP_FLAG) != 0)
{
return RETURN_FLASH_BUSY;
}
return RETURN_DEFAULT;
}
struct fal_flash_dev nor_flash0 = {
.name = FAL_FLASHDB_DEV_NAME,
.addr = 0,
.len = 1 * 1024 * 1024,
.blk_size = DEVICE_SECTOR_SIZE,
.ops = {fal_norflash_init, fal_norflash_read, fal_norflash_write, fal_norflash_erase},
.write_gran = FDB_WRITE_GRAN,
};
static int fal_norflash_init(void)
{
/* 发送buffer转为 读ID指令 */
fSpiTxBuf[0] = CMD_RDID;
fSpiTxBuf[1] = 0;
fSpiTxBuf[2] = 0;
fSpiTxBuf[3] = 0;
fSpiRxBuf[1] = 0;
fSpiRxBuf[2] = 0;
fSpiRxBuf[3] = 0;
CS_EN( );
if(FlashSpiTransmitReceivePort(NULL, (uint8_t *)fSpiTxBuf, (uint8_t *)fSpiRxBuf, 4) != RETURN_DEFAULT) // 收发
{
FALFDB_TRACE("[SPI-FLASH]init warn: read id transfer failed\r\n");
}
CS_UN( );
uint32_t ChipID = ((uint32_t)fSpiRxBuf[1] << 16) | ((uint32_t)fSpiRxBuf[2] << 8) | fSpiRxBuf[3];
switch(ChipID)
{
case W25Q80_ID:
nor_flash0.len = 1 * 1024 * 1024;
nor_flash0.blk_size = 4 * 1024;
break;
case W25Q64_ID:
nor_flash0.len = 8 * 1024 * 1024;
nor_flash0.blk_size = 4 * 1024;
break;
case W25Q128_ID:
nor_flash0.len = 16 * 1024 * 1024;
nor_flash0.blk_size = 4 * 1024;
break;
case W25Q256_ID:
nor_flash0.len = 32 * 1024 * 1024;
nor_flash0.blk_size = 4 * 1024;
break;
case W25Q512_ID:
nor_flash0.len = 64 * 1024 * 1024;
nor_flash0.blk_size = 4 * 1024;
break;
default:
nor_flash0.len = 0;
nor_flash0.blk_size = 0;
FALFDB_TRACE("[SPI-FLASH]init failed: unsupported chip id=0x%08X\r\n", (unsigned)ChipID);
return -1;
}
return 0;
}
static int fal_norflash_read(long offset, uint8_t *buf, size_t size)
{
uint32_t addr;
uint32_t chunk;
size_t remaining;
size_t copied;
if(fal_norflash_wait_idle( ) != RETURN_DEFAULT)
{
FALFDB_TRACE("read failed: flash busy before read\r\n");
return -1;
}
addr = (uint32_t)offset;
/* 发送读指令和3字节地址 */
fSpiTxBuf[0] = CMD_READ;
fSpiTxBuf[1] = (addr >> 16) & 0xFF;
fSpiTxBuf[2] = (addr >> 8) & 0xFF;
fSpiTxBuf[3] = addr & 0xFF;
CS_EN( );
if(FlashSpiTransmitPort(NULL, (uint8_t *)fSpiTxBuf, 4) != RETURN_DEFAULT)
{
CS_UN( );
FALFDB_TRACE("read failed: cmd tx error off=0x%08X size=%u\r\n", (unsigned)offset, (unsigned)size);
return -1;
}
remaining = size;
copied = 0;
while(remaining > 0)
{
chunk = (remaining > DEVICE_SECTOR_SIZE) ? DEVICE_SECTOR_SIZE : (uint32_t)remaining;
if(FlashSpiTransmitReceivePort(NULL, (uint8_t *)fSpiTxBuf, (uint8_t *)fSpiRxBuf, chunk) != RETURN_DEFAULT)
{
CS_UN( );
FALFDB_TRACE("read failed: data rx error off=0x%08X remain=%u\r\n", (unsigned)(addr + copied), (unsigned)remaining);
return -1;
}
for(uint32_t i = 0; i < chunk; i++)
{
buf[copied + i] = fSpiRxBuf[i];
}
copied += chunk;
remaining -= chunk;
}
CS_UN( );
return (int)size;
}
static int fal_norflash_write(long offset, const uint8_t *buf, size_t size)
{
uint32_t addr;
size_t remaining;
size_t copied;
if(fal_norflash_wait_idle( ) != RETURN_DEFAULT)
{
FALFDB_TRACE("write failed: flash busy before write\r\n");
return -1;
}
addr = (uint32_t)offset;
remaining = size;
copied = 0;
while(remaining > 0)
{
uint32_t page_offset = addr % FLASH_PAGE_SIZE;
uint32_t page_room = FLASH_PAGE_SIZE - page_offset;
uint32_t prog_len = (remaining > page_room) ? page_room : (uint32_t)remaining;
/* 写使能 */
fSpiTxBuf[0] = CMD_WREN;
CS_EN( );
if(FlashSpiTransmitPort(NULL, (uint8_t *)fSpiTxBuf, 1) != RETURN_DEFAULT)
{
CS_UN( );
FALFDB_TRACE("write failed: wren tx error off=0x%08X\r\n", (unsigned)addr);
return -1;
}
CS_UN( );
/* 页编程命令 + 3字节地址 + 数据 */
fSpiTxBuf[0] = CMD_PP;
fSpiTxBuf[1] = (addr >> 16) & 0xFF;
fSpiTxBuf[2] = (addr >> 8) & 0xFF;
fSpiTxBuf[3] = addr & 0xFF;
for(uint32_t i = 0; i < prog_len; i++)
{
fSpiTxBuf[4 + i] = buf[copied + i];
}
CS_EN( );
if(FlashSpiTransmitReceivePort(NULL, (uint8_t *)fSpiTxBuf, (uint8_t *)fSpiRxBuf, 4 + prog_len) != RETURN_DEFAULT)
{
CS_UN( );
FALFDB_TRACE("write failed: pp tx error off=0x%08X size=%u\r\n", (unsigned)addr, (unsigned)prog_len);
return -1;
}
CS_UN( );
if(fal_norflash_wait_idle( ) != RETURN_DEFAULT)
{
FALFDB_TRACE("write failed: wait idle timeout off=0x%08X\r\n", (unsigned)addr);
return -1;
}
addr += prog_len;
copied += prog_len;
remaining -= prog_len;
}
/* 关闭写使能 */
fSpiTxBuf[0] = CMD_DISWR;
CS_EN( );
if(FlashSpiTransmitPort(NULL, (uint8_t *)fSpiTxBuf, 1) != RETURN_DEFAULT)
{
CS_UN( );
FALFDB_TRACE("write warn: diswr tx error\r\n");
return -1;
}
CS_UN( );
if(fal_norflash_wait_idle( ) != RETURN_DEFAULT)
{
FALFDB_TRACE("write failed: flash busy after diswr\r\n");
return -1;
}
return (int)size;
}
static int fal_norflash_erase(long offset, size_t size)
{
uint32_t addr;
uint32_t blk_size;
size_t remaining;
blk_size = nor_flash0.blk_size;
if(fal_norflash_wait_idle( ) != RETURN_DEFAULT)
{
FALFDB_TRACE("erase failed: flash busy before erase\r\n");
return -1;
}
addr = (uint32_t)offset;
remaining = size;
while(remaining > 0)
{
/* 写使能 */
fSpiTxBuf[0] = CMD_WREN;
CS_EN( );
if(FlashSpiTransmitPort(NULL, (uint8_t *)fSpiTxBuf, 1) != RETURN_DEFAULT)
{
CS_UN( );
FALFDB_TRACE("erase failed: wren tx error off=0x%08X\r\n", (unsigned)addr);
return -1;
}
CS_UN( );
/* 扇区擦除命令 + 3字节地址 */
fSpiTxBuf[0] = CMD_SE;
fSpiTxBuf[1] = (addr >> 16) & 0xFF;
fSpiTxBuf[2] = (addr >> 8) & 0xFF;
fSpiTxBuf[3] = addr & 0xFF;
CS_EN( );
if(FlashSpiTransmitPort(NULL, (uint8_t *)fSpiTxBuf, 4) != RETURN_DEFAULT)
{
CS_UN( );
FALFDB_TRACE("erase failed: se tx error off=0x%08X\r\n", (unsigned)addr);
return -1;
}
CS_UN( );
if(fal_norflash_wait_idle( ) != RETURN_DEFAULT)
{
FALFDB_TRACE("erase failed: wait idle timeout off=0x%08X\r\n", (unsigned)addr);
return -1;
}
addr += blk_size;
remaining -= blk_size;
}
/* 关闭写使能 */
fSpiTxBuf[0] = CMD_DISWR;
CS_EN( );
if(FlashSpiTransmitPort(NULL, (uint8_t *)fSpiTxBuf, 1) != RETURN_DEFAULT)
{
CS_UN( );
FALFDB_TRACE("erase warn: diswr tx error\r\n");
return -1;
}
CS_UN( );
if(fal_norflash_wait_idle( ) != RETURN_DEFAULT)
{
FALFDB_TRACE("erase failed: flash busy after diswr\r\n");
return -1;
}
return (int)size;
}
为工程引入 FlashDB
有了此前由下至上的引入和支持,现在可以正式处理 FlashDB 的操作流程了。
现在需要处理的主要就是按序初始化了,一个最小的可用初始化顺序为
struct fdb_kvdb g_flashdb_kvdb;
int32_t ModuleFlashDBInit(void)
{
uint32_t sec_size;
memset(&g_flashdb_kvdb, 0, sizeof(g_flashdb_kvdb));
sec_size = (4 * 1024); // 默认单扇区大小;
fdb_kvdb_control(&g_flashdb_kvdb, FDB_KVDB_CTRL_SET_LOCK, (void *)module_flashdb_lock);
fdb_kvdb_control(&g_flashdb_kvdb, FDB_KVDB_CTRL_SET_UNLOCK, (void *)module_flashdb_unlock);
fdb_kvdb_control(&g_flashdb_kvdb, FDB_KVDB_CTRL_SET_SEC_SIZE, &sec_size);
result = fdb_kvdb_init(&g_flashdb_kvdb, "cfg", FAL_FLASHDB_KV_PART_NAME, NULL, NULL);
if(result != FDB_NO_ERR)
{
FLASHDB_TRACE("init failed: fdb_kvdb_init err=%d\r\n", (int)result);
return -1;
}
return 0;
}
此后就可以使用这样的原子操作以读写键值
struct fdb_blob blob;
uint8_t verifyIp[4] = {192,168,1,100};
fdb_kv_set_blob(&g_flashdb_kvdb, "lan_ip", fdb_blob_make(&blob, verifyIp, sizeof(verifyIp)));
fdb_kv_get_blob(&g_flashdb_kvdb, "lan_ip", fdb_blob_make(&blob, verifyIp, sizeof(verifyIp)));
需要自己做的包装
- 依据你的工程框架,可以使用线程锁来额外包装一套 API,以简单支持跨线程的键值读写。
- 进阶的,在包装中可以额外增加一个读 API 传参,在未命中键时使用默认值返回。类似 esp32 的 Preferences 数据库,
int canBaud = prefs.getInt("canbtaud", 250000);。 - 更加进阶的,由于 FlashDB 是一套阻塞操作、SPI 收发相当耗时,在线程中直接使用原子操作将导致毫秒级的阻塞,这是不太能接受的。可以采用独立线程和请求队列,专用于管理 FlashDB 的读写操作。在通常的场景,采用异步使得业务逻辑更通顺。而在必须立即读写并获值参与逻辑时,也可以直接调用原子操作。