前置条件
这个简单数据库可以以任意存储介质作为实际空间。
对于 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 的读写操作。在通常的场景,采用异步使得业务逻辑更通顺。而在必须立即读写并获值参与逻辑时,也可以直接调用原子操作。