From cc40a2187b12dd48f81dfc3b699db8421b940ee2 Mon Sep 17 00:00:00 2001 From: armink Date: Sun, 24 May 2020 18:39:09 +0800 Subject: [PATCH] Add first version. --- .gitattributes | 40 + LICENSE | 201 +++ README.md | 99 ++ docs/en/api.md | 1 + docs/en/readme.md | 3 + docs/readme.md | 4 + docs/zh/api.md | 5 + docs/zh/design.md | 3 + docs/zh/images/wechat_support.png | Bin 0 -> 18611 bytes docs/zh/port.md | 5 + docs/zh/readme.md | 5 + flashdb/inc/fdb_cfg.h | 39 + flashdb/inc/fdb_def.h | 279 +++ flashdb/inc/fdb_low_lvl.h | 56 + flashdb/inc/flashdb.h | 66 + flashdb/port/fal/LICENSE | 504 ++++++ flashdb/port/fal/README.md | 310 ++++ flashdb/port/fal/docs/fal_api.md | 145 ++ flashdb/port/fal/docs/figures/fal-api.png | Bin 0 -> 47111 bytes flashdb/port/fal/docs/figures/fal-port.png | Bin 0 -> 24465 bytes .../port/fal/docs/figures/fal_framework.png | Bin 0 -> 33974 bytes flashdb/port/fal/inc/fal.h | 166 ++ flashdb/port/fal/inc/fal_def.h | 156 ++ flashdb/port/fal/samples/README.md | 4 + flashdb/port/fal/samples/porting/README.md | 108 ++ flashdb/port/fal/samples/porting/fal_cfg.h | 55 + .../fal/samples/porting/fal_flash_sfud_port.c | 110 ++ .../samples/porting/fal_flash_stm32f2_port.c | 212 +++ flashdb/port/fal/src/fal.c | 76 + flashdb/port/fal/src/fal_flash.c | 93 + flashdb/port/fal/src/fal_partition.c | 493 +++++ flashdb/port/fal/src/fal_rtt.c | 930 ++++++++++ flashdb/src/fdb.c | 100 ++ flashdb/src/fdb_cmd.c | 162 ++ flashdb/src/fdb_kvdb.c | 1582 +++++++++++++++++ flashdb/src/fdb_tsdb.c | 712 ++++++++ flashdb/src/fdb_utils.c | 270 +++ samples/kvdb_type_blob_sample.c | 96 + samples/kvdb_type_string_sample.c | 97 + samples/tsdb_sample.c | 130 ++ tests/fdb_kvdb_tc.c | 187 ++ tests/fdb_tsdb_tc.c | 156 ++ 42 files changed, 7660 insertions(+) create mode 100644 .gitattributes create mode 100644 LICENSE create mode 100644 README.md create mode 100644 docs/en/api.md create mode 100644 docs/en/readme.md create mode 100644 docs/readme.md create mode 100644 docs/zh/api.md create mode 100644 docs/zh/design.md create mode 100644 docs/zh/images/wechat_support.png create mode 100644 docs/zh/port.md create mode 100644 docs/zh/readme.md create mode 100644 flashdb/inc/fdb_cfg.h create mode 100644 flashdb/inc/fdb_def.h create mode 100644 flashdb/inc/fdb_low_lvl.h create mode 100644 flashdb/inc/flashdb.h create mode 100644 flashdb/port/fal/LICENSE create mode 100644 flashdb/port/fal/README.md create mode 100644 flashdb/port/fal/docs/fal_api.md create mode 100644 flashdb/port/fal/docs/figures/fal-api.png create mode 100644 flashdb/port/fal/docs/figures/fal-port.png create mode 100644 flashdb/port/fal/docs/figures/fal_framework.png create mode 100644 flashdb/port/fal/inc/fal.h create mode 100644 flashdb/port/fal/inc/fal_def.h create mode 100644 flashdb/port/fal/samples/README.md create mode 100644 flashdb/port/fal/samples/porting/README.md create mode 100644 flashdb/port/fal/samples/porting/fal_cfg.h create mode 100644 flashdb/port/fal/samples/porting/fal_flash_sfud_port.c create mode 100644 flashdb/port/fal/samples/porting/fal_flash_stm32f2_port.c create mode 100644 flashdb/port/fal/src/fal.c create mode 100644 flashdb/port/fal/src/fal_flash.c create mode 100644 flashdb/port/fal/src/fal_partition.c create mode 100644 flashdb/port/fal/src/fal_rtt.c create mode 100644 flashdb/src/fdb.c create mode 100644 flashdb/src/fdb_cmd.c create mode 100644 flashdb/src/fdb_kvdb.c create mode 100644 flashdb/src/fdb_tsdb.c create mode 100644 flashdb/src/fdb_utils.c create mode 100644 samples/kvdb_type_blob_sample.c create mode 100644 samples/kvdb_type_string_sample.c create mode 100644 samples/tsdb_sample.c create mode 100644 tests/fdb_kvdb_tc.c create mode 100644 tests/fdb_tsdb_tc.c diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a6ec1ac --- /dev/null +++ b/.gitattributes @@ -0,0 +1,40 @@ +* text=auto + +*.S text +*.asm text +*.c text +*.cc text +*.cpp text +*.cxx text +*.h text +*.htm text +*.html text +*.in text +*.ld text +*.m4 text +*.mak text +*.mk text +*.py text +*.rb text +*.s text +*.sct text +*.sh text +*.txt text +*.xml text +Makefile text +AUTHORS text +COPYING text + +*.LZO -text +*.Opt -text +*.Uv2 -text +*.ewp -text +*.eww -text +*.vcproj -text +*.bat -text +*.dos -text +*.icf -text +*.inf -text +*.ini -text +*.sct -text +*.xsd -text diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a7c141f --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# FlashDB:超轻量级嵌入式数据库 + +## 简介 + +FlashDB 是一款超轻量级的嵌入式数据库,专注于提供嵌入式产品的数据存储方案。与传统的基于文件系统的数据库不同,FlashDB 结合了 Flash 的特性,具有较强的性能及可靠性。并在保证极低的资源占用前提下,尽可能延长 Flash 使用寿命。 + +FlashDB 提供两种数据库模式: + +- **键值数据库** :是一种非关系数据库,它将数据存储为键值(Key-Value)对集合,其中键作为唯一标识符。KVDB 操作简洁,可扩展性强。 +- **时序数据库** :时间序列数据库 (Time Series Database , 简称 TSDB),它将数据按照 **时间顺序存储** 。TSDB 数据具有时间戳,数据存储量大,插入及查询性能高。 + +## 使用场景 + +如今,物联网产品种类越来越多,运行时产生的数据种类及总量及也在不断变大。FlashDB 提供了多样化的数据存储方案,不仅资源占用小,并且存储容量大,非常适合用于物联网产品。下面是主要应用场景: + +- **键值数据库** : + - 产品参数存储 + - 用户配置信息存储 + - 小文件管理 +- **时序数据库** : + - 存储动态产生的结构化数据:如 温湿度传感器采集的环境监测信息,智能手环实时记录的人体健康信息等 + - 记录运行日志:存储产品历史的运行日志,异常告警的记录等 + +## 主要特性 + +- 资源占用极低,内存占用几乎为 **0** ; +- 支持 多分区,**多实例** 。数据量大时,可细化分区,降低检索时间; +- 支持 **磨损平衡** ,延长 Flash 寿命; +- 支持 **掉电保护** 功能,可靠性高; +- 支持 字符串及 blob 两种 KV 类型,方便用户操作; +- 支持 KV **增量升级** ,产品固件升级后, KVDB 内容也支持自动升级; +- 支持 修改每条 TSDB 记录的状态,方便用户进行管理; + +## 性能及资源占用 + +### TSDB 性能测试1 (nor flash W25Q64) + +```shell +msh />tsl bench +Append 1250 TSL in 5 seconds, average: 250.00 tsl/S, 4.00 ms/per +Query total spent 2218 (ms) for 1251 TSL, min 1, max 2, average: 1.77 ms/per +``` + +插入平均:4 ms,查询平均:1.8 ms + +### TSDB 性能测试2 (stm32f2 onchip flash) + +```shell +msh />tsl bench +Append 13421 TSL in 5 seconds, average: 2684.20 tsl/S, 0.37 ms/per +Query total spent 1475 (ms) for 13422 TSL, min 0, max 1, average: 0.11 ms/per +``` + +插入平均:0.37 ms,查询平均:0.12 ms + +### 资源占用 (stm32f4 IAR8.20) + +```shell + Module ro code ro data rw data + ------ ------- ------- ------- + fdb.o 276 232 1 + fdb_kvdb.o 4 584 356 1 + fdb_tsdb.o 1 160 236 + fdb_utils.o 418 1 024 +``` + +上面是 IAR 的 map 文件信息,可见 FlashDB 的资源占用非常低 + +## 如何使用 + +### 移植 + +FlashDB 底层的 Flash 管理及操作依赖于 RT-Thread 的 FAL (Flash Abstraction Layer) Flash 抽象层开源软件包 ,该开源库也支持运行在 **裸机平台** [(点击查看介绍)](http://packages.rt-thread.org/detail.html?package=fal)。所以只需要将所用到的 Flash 对接到 FAL ,即可完成整个移植工作。 + + FAL 移植主要流程: + +- 定义 flash 设备,详见 ([GitHub](https://github.com/RT-Thread-packages/fal#21%E5%AE%9A%E4%B9%89-flash-%E8%AE%BE%E5%A4%87)|[Gitee](https://gitee.com/RT-Thread-Mirror/fal#21%E5%AE%9A%E4%B9%89-flash-%E8%AE%BE%E5%A4%87)) +- 定义 flash 设备表,详见 ([GitHub](https://github.com/RT-Thread-packages/fal#22%E5%AE%9A%E4%B9%89-flash-%E8%AE%BE%E5%A4%87%E8%A1%A8)|[Gitee](https://gitee.com/RT-Thread-Mirror/fal#22%E5%AE%9A%E4%B9%89-flash-%E8%AE%BE%E5%A4%87%E8%A1%A8)) +- 定义 flash 分区表,详见 ([GitHub](https://github.com/RT-Thread-packages/fal#23%E5%AE%9A%E4%B9%89-flash-%E5%88%86%E5%8C%BA%E8%A1%A8)|[Gitee](https://gitee.com/RT-Thread-Mirror/fal#23%E5%AE%9A%E4%B9%89-flash-%E5%88%86%E5%8C%BA%E8%A1%A8)) + +### 示例 + +FlashDB 提供了主要功能的示例,直接加入工程即可运行,并具有一定的参考性 + +| 文件路径 | 介绍 | 备注 | +| ------------------------------------------------------------ | --------------------------------------- | ---- | +| [`samples/kvdb_type_string_sample.c`](samples/kvdb_type_string_sample.c) | KVDB 使用字符型键值的示例 | | +| [`samples/kvdb_type_blob_sample.c`](samples/kvdb_type_blob_sample.c) | KVDB 使用 blob 型(任意类型)键值的示例 | | +| [`samples/tsdb_sample.c`](samples/tsdb_sample.c) | TSDB 示例 | | + +## 支持 + + ![support](docs/zh/images/wechat_support.png) + +如果 FlashDB 解决了你的问题,不妨扫描上面二维码请我 **喝杯咖啡**~ + +## 许可 + +采用 Apache-2.0 开源协议,细节请阅读项目中的 LICENSE 文件内容。 \ No newline at end of file diff --git a/docs/en/api.md b/docs/en/api.md new file mode 100644 index 0000000..f65681d --- /dev/null +++ b/docs/en/api.md @@ -0,0 +1 @@ +# Coming soon... \ No newline at end of file diff --git a/docs/en/readme.md b/docs/en/readme.md new file mode 100644 index 0000000..916db5a --- /dev/null +++ b/docs/en/readme.md @@ -0,0 +1,3 @@ +|File name |Description| +|:----- |:----| +|api.md |API description| \ No newline at end of file diff --git a/docs/readme.md b/docs/readme.md new file mode 100644 index 0000000..da9229a --- /dev/null +++ b/docs/readme.md @@ -0,0 +1,4 @@ +|File or folder name |Description| +|:----- |:----| +|en |English documents| +|zh |中文文档(简体)| \ No newline at end of file diff --git a/docs/zh/api.md b/docs/zh/api.md new file mode 100644 index 0000000..52f4a86 --- /dev/null +++ b/docs/zh/api.md @@ -0,0 +1,5 @@ +# FlashDB API 说明 + +--- + +马上就来…… \ No newline at end of file diff --git a/docs/zh/design.md b/docs/zh/design.md new file mode 100644 index 0000000..8c781f6 --- /dev/null +++ b/docs/zh/design.md @@ -0,0 +1,3 @@ +# FlashDB 功能设计与实现 + +马上就来…… \ No newline at end of file diff --git a/docs/zh/images/wechat_support.png b/docs/zh/images/wechat_support.png new file mode 100644 index 0000000000000000000000000000000000000000..1c786f74984f0f867dbb22614d1bcde0471467d4 GIT binary patch literal 18611 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 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * @brief configuration file + */ + +#ifndef _FDB_CFG_H_ +#define _FDB_CFG_H_ + +/* using KVDB function */ +#define FDB_USING_KVDB + +#ifdef FDB_USING_KVDB +/* Auto update KV to latest default when current KVDB version number is changed. @see fdb_kvdb.ver_num */ +/* #define FDB_KV_AUTO_UPDATE */ +#endif + +/* using TSDB (Time series database) feature */ +#define FDB_USING_TSDB + +/* the flash write granularity, unit: bit + * only support 1(nor flash)/ 8(stm32f2/f4)/ 32(stm32f1) */ +#define FDB_WRITE_GRAN /* @note you must define it for a value */ + +/* MCU Endian Configuration, default is Little Endian Order. */ +/* #define FDB_BIG_ENDIAN */ + +/* log print macro. default EF_PRINT macro is printf() */ +/* #define FDB_PRINT(...) my_printf(__VA_ARGS__) */ + +/* print debug information */ +#define FDB_DEBUG_ENABLE + +#endif /* _FDB_CFG_H_ */ diff --git a/flashdb/inc/fdb_def.h b/flashdb/inc/fdb_def.h new file mode 100644 index 0000000..34d07d4 --- /dev/null +++ b/flashdb/inc/fdb_def.h @@ -0,0 +1,279 @@ +/* + * Copyright (c) 2020, Armink, + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * @brief Public definition. + */ + +#ifndef _FDB_DEF_H_ +#define _FDB_DEF_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +/* software version number */ +#define FDB_SW_VERSION "1.0.0 beta" +#define FDB_SW_VERSION_NUM 0x10000 + +/* the KV max name length must less then it */ +#ifndef FDB_KV_NAME_MAX +#define FDB_KV_NAME_MAX 32 +#endif + +/* the KV cache table size, it will improve KV search speed when using cache */ +#ifndef FDB_KV_CACHE_TABLE_SIZE +#define FDB_KV_CACHE_TABLE_SIZE 16 +#endif + +/* the sector cache table size, it will improve KV save speed when using cache */ +#ifndef FDB_SECTOR_CACHE_TABLE_SIZE +#define FDB_SECTOR_CACHE_TABLE_SIZE 4 +#endif + +#if (FDB_KV_CACHE_TABLE_SIZE > 0) && (FDB_SECTOR_CACHE_TABLE_SIZE > 0) +#define FDB_KV_USING_CACHE +#endif + +/* log function. default FDB_PRINT macro is printf() */ +#ifndef FDB_PRINT +#define FDB_PRINT(...) printf(__VA_ARGS__) +#endif +#define FDB_LOG_PREFIX1() FDB_PRINT("[FlashDB]"FDB_LOG_TAG) +#define FDB_LOG_PREFIX2() FDB_PRINT(" ") +#define FDB_LOG_PREFIX() FDB_LOG_PREFIX1();FDB_LOG_PREFIX2() +#ifdef FDB_DEBUG_ENABLE +#define FDB_DEBUG(...) FDB_LOG_PREFIX();FDB_PRINT("(%s:%d) ", __FILE__, __LINE__);FDB_PRINT(__VA_ARGS__) +#else +#define FDB_DEBUG(...) +#endif +/* routine print function. Must be implement by user. */ +#define FDB_INFO(...) FDB_LOG_PREFIX();FDB_PRINT(__VA_ARGS__) +/* assert for developer. */ +#define FDB_ASSERT(EXPR) \ +if (!(EXPR)) \ +{ \ + FDB_DEBUG("(%s) has assert failed at %s.\n", #EXPR, __FUNCTION__); \ + while (1); \ +} + +typedef time_t fdb_time_t; +#ifdef FDB_USING_TIMESTAMP_64BIT +typedef int64_t fdb_time_t; +#endif +typedef fdb_time_t (*fdb_get_time)(void); + +struct fdb_default_kv_node { + char *key; + void *value; + size_t value_len; +}; + +struct fdb_default_kv { + struct fdb_default_kv_node *kvs; + size_t num; +}; + +/* error code */ +typedef enum { + FDB_NO_ERR, + FDB_ERASE_ERR, + FDB_READ_ERR, + FDB_WRITE_ERR, + FDB_PART_NOT_FOUND, + FDB_KV_NAME_ERR, + FDB_KV_NAME_EXIST, + FDB_KV_FULL, + FDB_INIT_FAILED, +} fdb_err_t; + +enum fdb_kv_status { + FDB_KV_UNUSED, + FDB_KV_PRE_WRITE, + FDB_KV_WRITE, + FDB_KV_PRE_DELETE, + FDB_KV_DELETED, + FDB_KV_ERR_HDR, + FDB_KV_STATUS_NUM, +}; +typedef enum fdb_kv_status fdb_kv_status_t; + +enum fdb_tsl_status { + FDB_TSL_UNUSED, + FDB_TSL_PRE_WRITE, + FDB_TSL_WRITE, + FDB_TSL_USER_STATUS1, + FDB_TSL_DELETED, + FDB_TSL_USER_STATUS2, + FDB_TSL_STATUS_NUM, +}; +typedef enum fdb_tsl_status fdb_tsl_status_t; + +/* key-value node object */ +struct fdb_kv { + fdb_kv_status_t status; /**< node status, @see fdb_kv_status_t */ + bool crc_is_ok; /**< node CRC32 check is OK */ + uint8_t name_len; /**< name length */ + uint32_t magic; /**< magic word(`K`, `V`, `4`, `0`) */ + uint32_t len; /**< node total length (header + name + value), must align by FDB_WRITE_GRAN */ + uint32_t value_len; /**< value length */ + char name[FDB_KV_NAME_MAX]; /**< name */ + struct { + uint32_t start; /**< node start address */ + uint32_t value; /**< value start address */ + } addr; +}; +typedef struct fdb_kv *fdb_kv_t; + +/* time series log node object */ +struct fdb_tsl { + fdb_tsl_status_t status; /**< node status, @see fdb_log_status_t */ + fdb_time_t time; /**< node timestamp */ + uint32_t log_len; /**< log length, must align by FDB_WRITE_GRAN */ + struct { + uint32_t index; /**< node index address */ + uint32_t log; /**< log data address */ + } addr; +}; +typedef struct fdb_tsl *fdb_tsl_t; +typedef bool (*fdb_tsl_cb)(fdb_tsl_t tsl, void *arg); + +typedef enum { + FDB_DB_TYPE_KV, + FDB_DB_TYPE_TS, +} fdb_db_type; + +/* the flash sector store status */ +enum fdb_sector_store_status { + FDB_SECTOR_STORE_UNUSED, + FDB_SECTOR_STORE_EMPTY, + FDB_SECTOR_STORE_USING, + FDB_SECTOR_STORE_FULL, + FDB_SECTOR_STORE_STATUS_NUM, +}; +typedef enum fdb_sector_store_status fdb_sector_store_status_t; + +/* the flash sector dirty status */ +enum fdb_sector_dirty_status { + FDB_SECTOR_DIRTY_UNUSED, + FDB_SECTOR_DIRTY_FALSE, + FDB_SECTOR_DIRTY_TRUE, + FDB_SECTOR_DIRTY_GC, + FDB_SECTOR_DIRTY_STATUS_NUM, +}; +typedef enum fdb_sector_dirty_status fdb_sector_dirty_status_t; + +/* KVDB section information */ +struct kvdb_sec_info { + bool check_ok; /**< sector header check is OK */ + struct { + fdb_sector_store_status_t store; /**< sector store status @see fdb_sector_store_status_t */ + fdb_sector_dirty_status_t dirty; /**< sector dirty status @see sector_dirty_status_t */ + } status; + uint32_t addr; /**< sector start address */ + uint32_t magic; /**< magic word(`E`, `F`, `4`, `0`) */ + uint32_t combined; /**< the combined next sector number, 0xFFFFFFFF: not combined */ + size_t remain; /**< remain size */ + uint32_t empty_kv; /**< the next empty KV node start address */ +}; +typedef struct kvdb_sec_info *kv_sec_info_t; + +/* TSDB section information */ +struct tsdb_sec_info { + bool check_ok; /**< sector header check is OK */ + fdb_sector_store_status_t status; /**< sector store status @see fdb_sector_store_status_t */ + uint32_t addr; /**< sector start address */ + uint32_t magic; /**< magic word(`T`, `S`, `L`, `0`) */ + fdb_time_t start_time; /**< the first start node's timestamp, 0xFFFFFFFF: unused */ + fdb_time_t end_time; /**< the last end node's timestamp, 0xFFFFFFFF: unused */ + uint32_t end_idx; /**< the last end node's index, 0xFFFFFFFF: unused */ + fdb_tsl_status_t end_info_stat[2]; /**< the last end node's info status */ + size_t remain; /**< remain size */ + uint32_t empty_idx; /**< the next empty node index address */ + uint32_t empty_data; /**< the next empty node's data end address */ +}; +typedef struct tsdb_sec_info *tsdb_sec_info_t; + +struct kv_cache_node { + uint16_t name_crc; /**< KV name's CRC32 low 16bit value */ + uint16_t active; /**< KV node access active degree */ + uint32_t addr; /**< KV node address */ +}; +typedef struct kv_cache_node *kv_cache_node_t; + +struct sector_cache_node { + uint32_t addr; /**< sector start address */ + uint32_t empty_addr; /**< sector empty address */ +}; +typedef struct sector_cache_node *sector_cache_node_t; + +/* database structure */ +typedef struct fdb_db *fdb_db_t; +struct fdb_db { + const char *name; /**< database name */ + fdb_db_type type; /**< database type */ + const struct fal_partition *part; /**< flash partition */ + uint32_t sec_size; /**< flash section size. It's a multiple of block size */ + bool init_ok; /**< initialized successfully */ + void (*lock)(fdb_db_t db); /**< lock the database operate */ + void (*unlock)(fdb_db_t db); /**< unlock the database operate */ + + void *user_data; +}; + +/* KVDB structure */ +struct fdb_kvdb { + struct fdb_db parent; /**< inherit from fdb_db */ + struct fdb_default_kv default_kvs; /**< default KV */ + bool gc_request; /**< request a GC check */ + bool in_recovery_check; /**< is in recovery check status when first reboot */ + +#ifdef FDB_KV_USING_CACHE + /* KV cache table */ + struct kv_cache_node kv_cache_table[FDB_KV_CACHE_TABLE_SIZE]; + /* sector cache table, it caching the sector info which status is current using */ + struct sector_cache_node sector_cache_table[FDB_SECTOR_CACHE_TABLE_SIZE]; +#endif /* FDB_KV_USING_CACHE */ + +#ifdef FDB_KV_AUTO_UPDATE + uint32_t ver_num; /**< setting version number for update */ +#endif + + void *user_data; +}; +typedef struct fdb_kvdb *fdb_kvdb_t; + +/* TSDB structure */ +struct fdb_tsdb { + struct fdb_db parent; /**< inherit from fdb_db */ + struct tsdb_sec_info cur_sec; /**< current using sector */ + fdb_time_t last_time; /**< last TSL timestamp */ + fdb_get_time get_time; /**< the current timestamp get function */ + size_t max_len; /**< the max log length */ + uint32_t oldest_addr; /**< the oldest sector start address */ + + void *user_data; +}; +typedef struct fdb_tsdb *fdb_tsdb_t; + +/* blob structure */ +struct fdb_blob { + void *buf; /**< blob data buffer */ + size_t size; /**< blob data buffer size */ + struct { + uint32_t meta_addr; /**< saved KV or TSL index address */ + uint32_t addr; /**< blob data saved address */ + size_t len; /**< blob data saved length */ + } saved; +}; +typedef struct fdb_blob *fdb_blob_t; + +#ifdef __cplusplus +} +#endif + +#endif /* _FDB_DEF_H_ */ diff --git a/flashdb/inc/fdb_low_lvl.h b/flashdb/inc/fdb_low_lvl.h new file mode 100644 index 0000000..c1da3c9 --- /dev/null +++ b/flashdb/inc/fdb_low_lvl.h @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020, Armink, + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * @brief low level API and definition + */ + +#ifndef _FDB_LOW_LVL_H_ +#define _FDB_LOW_LVL_H_ + +#include +#include + +#if (FDB_WRITE_GRAN == 1) +#define FDB_STATUS_TABLE_SIZE(status_number) ((status_number * FDB_WRITE_GRAN + 7)/8) +#else +#define FDB_STATUS_TABLE_SIZE(status_number) (((status_number - 1) * FDB_WRITE_GRAN + 7)/8) +#endif + +/* Return the most contiguous size aligned at specified width. RT_ALIGN(13, 4) + * would return 16. + */ +#define FDB_ALIGN(size, align) (((size) + (align) - 1) & ~((align) - 1)) +/* align by write granularity */ +#define FDB_WG_ALIGN(size) (FDB_ALIGN(size, (FDB_WRITE_GRAN + 7)/8)) +/** + * Return the down number of aligned at specified width. RT_ALIGN_DOWN(13, 4) + * would return 12. + */ +#define FDB_ALIGN_DOWN(size, align) ((size) & ~((align) - 1)) +/* align down by write granularity */ +#define FDB_WG_ALIGN_DOWN(size) (FDB_ALIGN_DOWN(size, (FDB_WRITE_GRAN + 7)/8)) + +#define FDB_STORE_STATUS_TABLE_SIZE FDB_STATUS_TABLE_SIZE(FDB_SECTOR_STORE_STATUS_NUM) +#define FDB_DIRTY_STATUS_TABLE_SIZE FDB_STATUS_TABLE_SIZE(FDB_SECTOR_DIRTY_STATUS_NUM) + +/* the data is unused */ +#define FDB_DATA_UNUSED 0xFFFFFFFF + +fdb_err_t _fdb_kv_load(fdb_kvdb_t db); +size_t _fdb_set_status(uint8_t status_table[], size_t status_num, size_t status_index); +size_t _fdb_get_status(uint8_t status_table[], size_t status_num); +uint32_t _fdb_continue_ff_addr(fdb_db_t db, uint32_t start, uint32_t end); +fdb_err_t _fdb_init_ex(fdb_db_t db, const char *name, const char *part_name, fdb_db_type type, void *user_data); +void _fdb_init_finish(fdb_db_t db, fdb_err_t result); +fdb_err_t _fdb_write_status(fdb_db_t db, uint32_t addr, uint8_t status_table[], size_t status_num, size_t status_index); +size_t _fdb_read_status(fdb_db_t db, uint32_t addr, uint8_t status_table[], size_t total_num); +fdb_err_t _fdb_flash_read(fdb_db_t db, uint32_t addr, void *buf, size_t size); +fdb_err_t _fdb_flash_erase(fdb_db_t db, uint32_t addr, size_t size); +fdb_err_t _fdb_flash_write(fdb_db_t db, uint32_t addr, const void *buf, size_t size); + +#endif /* _FDB_LOW_LVL_H_ */ diff --git a/flashdb/inc/flashdb.h b/flashdb/inc/flashdb.h new file mode 100644 index 0000000..0579163 --- /dev/null +++ b/flashdb/inc/flashdb.h @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2020, Armink, + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * @brief Public APIs. + */ + +#ifndef _FLASHDB_H_ +#define _FLASHDB_H_ + +#include +#include +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* fdb.c */ +void fdb_lock_set(fdb_db_t db, void (*lock)(fdb_db_t db), void (*unlock)(fdb_db_t db)); +void fdb_sec_size_set(fdb_db_t db, uint32_t sec_size); +fdb_err_t fdb_kvdb_init(fdb_kvdb_t db, const char *name, const char *part_name, struct fdb_default_kv *default_kv, + void *user_data); +fdb_err_t fdb_tsdb_init(fdb_tsdb_t db, const char *name, const char *part_name, fdb_get_time get_time, size_t max_len, + void *user_data); + +/* blob API */ +fdb_blob_t fdb_blob_make (fdb_blob_t blob, const void *value_buf, size_t buf_len); +size_t fdb_blob_read (fdb_db_t db, fdb_blob_t blob); + +/* Key-Value API like a KV DB */ +fdb_err_t fdb_kv_set (fdb_kvdb_t db, const char *key, const char *value); +char *fdb_kv_get (fdb_kvdb_t db, const char *key); +fdb_err_t fdb_kv_set_blob (fdb_kvdb_t db, const char *key, fdb_blob_t blob); +size_t fdb_kv_get_blob (fdb_kvdb_t db, const char *key, fdb_blob_t blob); +fdb_err_t fdb_kv_del (fdb_kvdb_t db, const char *key); +fdb_kv_t fdb_kv_get_obj (fdb_kvdb_t db, const char *key, fdb_kv_t kv); +fdb_blob_t fdb_kv_to_blob (fdb_kv_t kv, fdb_blob_t blob); +fdb_err_t fdb_kv_set_default(fdb_kvdb_t db); +void fdb_kv_print (fdb_kvdb_t db); + +/* Time series log API like a TSDB */ +fdb_err_t fdb_tsl_append (fdb_tsdb_t db, fdb_blob_t blob); +void fdb_tsl_iter (fdb_tsdb_t db, fdb_tsl_cb cb, void *cb_arg); +void fdb_tsl_iter_by_time(fdb_tsdb_t db, fdb_time_t from, fdb_time_t to, fdb_tsl_cb cb, void *cb_arg); +size_t fdb_tsl_query_count (fdb_tsdb_t db, fdb_time_t from, fdb_time_t to, fdb_tsl_status_t status); +fdb_err_t fdb_tsl_set_status (fdb_tsdb_t db, fdb_tsl_t tsl, fdb_tsl_status_t status); +void fdb_tsl_clean (fdb_tsdb_t db); +fdb_blob_t fdb_tsl_to_blob (fdb_tsl_t tsl, fdb_blob_t blob); + +/* fdb_utils.c */ +uint32_t fdb_calc_crc32(uint32_t crc, const void *buf, size_t size); + +#ifdef __cplusplus +} +#endif + +#endif /* _FLASHDB_H_ */ diff --git a/flashdb/port/fal/LICENSE b/flashdb/port/fal/LICENSE new file mode 100644 index 0000000..19e3071 --- /dev/null +++ b/flashdb/port/fal/LICENSE @@ -0,0 +1,504 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +(This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.) + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + {description} + Copyright (C) {year} {fullname} + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random + Hacker. + + {signature of Ty Coon}, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/flashdb/port/fal/README.md b/flashdb/port/fal/README.md new file mode 100644 index 0000000..718dd6c --- /dev/null +++ b/flashdb/port/fal/README.md @@ -0,0 +1,310 @@ +# FAL:Flash 抽象层 + +## 1、FAL介绍 + +FAL (Flash Abstraction Layer) Flash 抽象层,是对 Flash 及基于 Flash 的分区进行管理、操作的抽象层,对上层统一了 Flash 及 分区操作的 API (框架图如下所示),并具有以下特性: + +- 支持静态可配置的分区表,并可关联多个 Flash 设备; +- 分区表支持 **自动装载** 。避免在多固件项目,分区表被多次定义的问题; +- 代码精简,对操作系统 **无依赖** ,可运行于裸机平台,比如对资源有一定要求的 Bootloader; +- 统一的操作接口。保证了文件系统、OTA、NVM(例如:[EasyFlash](https://github.com/armink-rtt-pkgs/EasyFlash)) 等对 Flash 有一定依赖的组件,底层 Flash 驱动的可重用性; +- 自带基于 Finsh/MSH 的测试命令,可以通过 Shell 按字节寻址的方式操作(读写擦) Flash 或分区,方便开发者进行调试、测试; + +![FAL framework](docs/figures/fal_framework.png) + +### 1.1、打开 FAL + +使用 fal package 需要在 RT-Thread 的包管理器中选择它,具体路径如下: + +``` +RT-Thread online packages + system packages ---> + --- fal: Flash Abstraction Layer implement. Manage flash device and partition. + [*] Enable debug log output + [*] FAL partition table config has defined on 'fal_cfg.h' + (onchip) The flash device which saving partition table + (65536) The patition table end address relative to flash device offset. + [ ] FAL uses SFUD drivers + (norflash0) The name of the device used by FAL (NEW) + version (latest) ---> +``` + +每个功能的配置说明如下: + +- 开启调试日志输出(默认开启); +- 分区表是否在 `fal_cfg.h` 中定义(默认开启)。如果关闭此选项,fal 将会自动去指定 Flash 的指定位置去检索并装载分区表,具体配置详见下面两个选项; + - 存放分区表的 Flash 设备; + - 分区表的 **结束地址** 位于 Flash 设备上的偏移。fal 将从此地址开始往回进行检索分区表,直接读取到 Flash 顶部。如果不确定分区表具体位置,这里也可以配置为 Flash 的结束地址,fal 将会检索整个 Flash,检索时间可能会增加。 +- 启用 FAL 针对 SFUD 的移植文件(默认关闭); + - 应输入调用 `rt_sfud_flash_probe` 函数时传入的 FLASH 设备名称(也可以通过 list_device 命令查看 Block Device 的名字获取)。该名称与分区表中的 Flash 名称对应,只有正确设置设备名字,才能完成对 FLASH 的读写操作。 + +然后让 RT-Thread 的包管理器自动更新,或者使用 `pkgs --update` 命令更新包到 BSP 中。 + +### 1.2、FAL 目录 + +| 名称 | 说明 | +| ------- | ---------- | +| inc | 头文件目录 | +| src | 源代码目录 | +| samples | 例程目录 | + +### 1.3、FAL API + +FAL 相关的 API 如图所示,[点击此处查看 API 参数详解](docs/fal_api.md)。 + +![FAL API](docs/figures/fal-api.png) + +### 1.4、许可证 + +fal package 遵循 LGPLv2.1 许可,详见 `LICENSE` 文件。 + +### 1.5、依赖 + +对 RT-Thread 无依赖,也可用于裸机。 + +> 测试命令功能需要依赖 RT-Thread Finsh/MSH + +## 2、使用 FAL + +使用 FAL 的基本步骤如下所示: + +1. 打开 FAL:从 Env 中打开 fal 软件包并下载到工程。 +2. FAL 移植:定义 flash 设备、定义 flash 设备表、定义 flash 分区表。以下主要对步骤 2 展开讲解。 +3. 调用 fal_init() 初始化该库:移植完成后,可在应用层调用,如在 main 函数中调用。 + +![fal 移植](docs/figures/fal-port.png) + +### 2.1、定义 flash 设备 + +在定义 Flash 设备表前,需要先定义 Flash 设备。可以是片内 flash, 也可以是片外基于 SFUD 的 spi flash: + +- 定义片内 flash 设备可以参考 [`fal_flash_sfud_port.c`](https://github.com/RT-Thread-packages/fal/blob/master/samples/porting/fal_flash_sfud_port.c) 。 +- 定义片外 spi flash 设备可以参考 [`fal_flash_stm32f2_port.c`](https://github.com/RT-Thread-packages/fal/blob/master/samples/porting/fal_flash_stm32f2_port.c) 。 + +定义具体的 Flash 设备对象,用户需要根据自己的 Flash 情况分别实现 `init`、 `read`、 `write`、 `erase` 这些操作函数: + +- `static int init(void)`:**可选** 的初始化操作。 +- `static int read(long offset, uint8_t *buf, size_t size)`:读取操作。 + +| 参数 | 描述 | +| ------ | ------------------------- | +| offset | 读取数据的 Flash 偏移地址 | +| buf | 存放待读取数据的缓冲区 | +| size | 待读取数据的大小 | +| return | 返回实际读取的数据大小 | + +- `static int write(long offset, const uint8_t *buf, size_t size)` :写入操作。 + +| 参数 | 描述 | +| ------ | ------------------------- | +| offset | 写入数据的 Flash 偏移地址 | +| buf | 存放待写入数据的缓冲区 | +| size | 待写入数据的大小 | +| return | 返回实际写入的数据大小 | + +- `static int erase(long offset, size_t size)` :擦除操作。 + +| 参数 | 描述 | +| ------ | ------------------------- | +| offset | 擦除区域的 Flash 偏移地址 | +| size | 擦除区域的大小 | +| return | 返回实际擦除的区域大小 | + +用户需要根据自己的 Flash 情况分别实现这些操作函数。在文件最底部定义了具体的 Flash 设备对象 ,如下示例定义了 stm32f2 片上 flash:stm32f2_onchip_flash + +```c +const struct fal_flash_dev stm32f2_onchip_flash = +{ + .name = "stm32_onchip", + .addr = 0x08000000, + .len = 1024*1024, + .blk_size = 128*1024, + .ops = {init, read, write, erase}, + .write_gran = 8 +}; +``` + +- `"stm32_onchip"` : Flash 设备的名字。 +- `0x08000000`: 对 Flash 操作的起始地址。 +- `1024*1024`:Flash 的总大小(1MB)。 +- `128*1024`:Flash 块/扇区大小(因为 STM32F2 各块大小不均匀,所以擦除粒度为最大块的大小:128K)。 +- `{init, read, write, erase}` :Flash 的操作函数。 如果没有 init 初始化过程,第一个操作函数位置可以置空。 +- `8` : 设置写粒度,单位 bit, 0 表示未生效(默认值为 0 ),该成员是 fal 版本大于 0.4.0 的新增成员。各个 flash 写入粒度不尽相同,可通过该成员进行设置,以下列举几种常见 Flash 写粒度: + - nor flash: 1 bit + - stm32f4: 8 bit + - stm32f1: 32 bit + - stm32l4: 64 bit + +### 2.2、定义 flash 设备表 + +Flash 设备表定义在 `fal_cfg.h` 头文件中,定义分区表前需 **新建 `fal_cfg.h` 文件** ,请将该文件统一放在对应 BSP 或工程目录的 port 文件夹下,并将该头文件路径加入到工程。fal_cfg.h 可以参考 [示例文件 fal/samples/porting/fal_cfg.h](https://github.com/RT-Thread-packages/fal/blob/master/samples/porting/samples/porting/fal_cfg.h) 完成。 + +设备表示例: + +```c +/* ===================== Flash device Configuration ========================= */ +extern const struct fal_flash_dev stm32f2_onchip_flash; +extern struct fal_flash_dev nor_flash0; + +/* flash device table */ +#define FAL_FLASH_DEV_TABLE \ +{ \ + &stm32f2_onchip_flash, \ + &nor_flash0, \ +} +``` + +Flash 设备表中,有两个 Flash 对象,一个为 STM32F2 的片内 Flash ,一个为片外的 Nor Flash。 + +### 2.3、定义 flash 分区表 + +分区表也定义在 `fal_cfg.h` 头文件中。Flash 分区基于 Flash 设备,每个 Flash 设备又可以有 N 个分区,这些分区的集合就是分区表。在配置分区表前,务必保证已定义好 **Flash 设备** 及 **设备表**。fal_cfg.h 可以参考 [示例文件 fal/samples/porting/fal_cfg.h](https://github.com/RT-Thread-packages/fal/blob/master/samples/porting/samples/porting/fal_cfg.h) 完成。 + +分区表示例: + +```c +#define NOR_FLASH_DEV_NAME "norflash0" +/* ====================== Partition Configuration ========================== */ +#ifdef FAL_PART_HAS_TABLE_CFG +/* partition table */ +#define FAL_PART_TABLE \ +{ \ + {FAL_PART_MAGIC_WORD, "bl", "stm32_onchip", 0, 64*1024, 0}, \ + {FAL_PART_MAGIC_WORD, "app", "stm32_onchip", 64*1024, 704*1024, 0}, \ + {FAL_PART_MAGIC_WORD, "easyflash", NOR_FLASH_DEV_NAME, 0, 1024*1024, 0}, \ + {FAL_PART_MAGIC_WORD, "download", NOR_FLASH_DEV_NAME, 1024*1024, 1024*1024, 0}, \ +} +#endif /* FAL_PART_HAS_TABLE_CFG */ +``` + +上面这个分区表详细描述信息如下: + +| 分区名 | Flash 设备名 | 偏移地址 | 大小 | 说明 | +| ----------- | -------------- | --------- | ----- | ------------------ | +| "bl" | "stm32_onchip" | 0 | 64KB | 引导程序 | +| "app" | "stm32_onchip" | 64*1024 | 704KB | 应用程序 | +| "easyflash" | "norflash0" | 0 | 1MB | EasyFlash 参数存储 | +| "download" | "norflash0" | 1024*1024 | 1MB | OTA 下载区 | + +用户需要修改的分区参数包括:分区名称、关联的 Flash 设备名、偏移地址(相对 Flash 设备内部)、大小,需要注意以下几点: + +- 分区名保证 **不能重复**; +- 关联的 Flash 设备 **务必已经在 Flash 设备表中定义好** ,并且 **名称一致** ,否则会出现无法找到 Flash 设备的错误; +- 分区的起始地址和大小 **不能超过 Flash 设备的地址范围** ,否则会导致包初始化错误; + +> 注意:每个分区定义时,除了填写上面介绍的参数属性外,需在前面增加 `FAL_PART_MAGIC_WORD` 属性,末尾增加 `0` (目前用于保留功能) + +## 3、Finsh/MSH 测试命令 + +fal 提供了丰富的测试命令,项目只要在 RT-Thread 上开启 Finsh/MSH 功能即可。在做一些基于 Flash 的应用开发、调试时,这些命令会非常实用。它可以准确的写入或者读取指定位置的原始 Flash 数据,快速的验证 Flash 驱动的完整性,甚至可以对 Flash 进行性能测试。 + +具体功能如下:输入 fal 可以看到完整的命令列表 + +``` +msh />fal +Usage: +fal probe [dev_name|part_name] - probe flash device or partition by given name +fal read addr size - read 'size' bytes starting at 'addr' +fal write addr data1 ... dataN - write some bytes 'data' starting at 'addr' +fal erase addr size - erase 'size' bytes starting at 'addr' +fal bench - benchmark test with per block size + +msh /> +``` + +### 3.1、指定待操作的 Flash 设备或 Flash 分区 + +当第一次使用 fal 命令时,直接输入 `fal probe` 将会显示分区表信息。可以指定待操作的对象为分区表里的某个分区,或者某个 Flash 设备。 + +分区或者 Flash 被成功选中后,还将会显示它的一些属性情况。大致效果如下: + +``` +msh />fal probe +No flash device or partition was probed. +Usage: fal probe [dev_name|part_name] - probe flash device or partition by given name. +[I/FAL] ==================== FAL partition table ==================== +[I/FAL] | name | flash_dev | offset | length | +[I/FAL] ------------------------------------------------------------- +[I/FAL] | bl | stm32_onchip | 0x00000000 | 0x00010000 | +[I/FAL] | app | stm32_onchip | 0x00010000 | 0x000b0000 | +[I/FAL] | ef | norflash0 | 0x00000000 | 0x00100000 | +[I/FAL] | download | norflash0 | 0x00100000 | 0x00100000 | +[I/FAL] ============================================================= +msh /> +msh />fal probe download +Probed a flash partition | download | flash_dev: norflash0 | offset: 1048576 | len: 1048576 |. +msh /> +``` + +### 3.2、擦除数据 + +先输入 `fal erase` ,后面跟着待擦除数据的起始地址以及长度。以下命令为:从 0 地址(相对 Flash 或分区)开始擦除 4096 字节数据 + +> 注意:根据 Flash 特性,擦除动作将按扇区对齐进行处理。所以,如果擦除操作地址或长度未按照 Flash 的扇区对齐,将会擦除掉与其关联的整个扇区数据。 + +``` +msh />fal erase 0 4096 +Erase data success. Start from 0x00000000, size is 4096. +msh /> +``` + +### 3.3、写入数据 + +先输入 `fal write` ,后面跟着 N 个待写入的数据,并以空格隔开。以下命令为:从地址 8 的位置依次开始写入 1、2、3、4 、 5 这 5 个字节数据 + +``` +msh />fal write 8 1 2 3 4 5 +Write data success. Start from 0x00000008, size is 5. +Write data: 1 2 3 4 5 . +msh /> +``` + +### 3.4、读取数据 + +先输入 `fal read` ,后面跟着待读取数据的起始地址以及长度。以下命令为:从 0 地址开始读取 64 字节数据 + +``` +msh />fal read 0 64 +Read data success. Start from 0x00000000, size is 64. The data is: +Offset (h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F +[00000000] FF FF FF FF FF FF FF FF 01 02 03 04 05 FF FF FF +[00000010] FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF +[00000020] FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF +[00000030] FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF + +msh /> +``` + +### 3.5、性能测试 + +性能测试将会测试 Flash 的擦除、写入及读取速度,同时将会测试写入及读取数据的准确性,保证整个 Flash 或整个分区的 写入与读取 数据的一致性。 + +先输入 `fal bench` ,后面跟着待测试 Flash 的扇区大小(请查看对应的 Flash 手册,SPI Nor Flash 一般为 4096)。由于性能测试将会让整个 Flash 或者整个分区的数据丢失,所以命令最后必须跟 `yes` 。 + +``` +msh />fal bench 4096 yes +Erasing 1048576 bytes data, waiting... +Erase benchmark success, total time: 2.674S. +Writing 1048576 bytes data, waiting... +Write benchmark success, total time: 7.107S. +Reading 1048576 bytes data, waiting... +Read benchmark success, total time: 2.716S. +msh /> +``` + +## 4、常见应用 + +- [基于 FAL 分区的 fatfs 文件系统例程](https://github.com/RT-Thread/IoT_Board/tree/master/examples/15_component_fs_flash) +- [基于 FAL 分区的 littlefs 文件系统应用笔记](https://www.rt-thread.org/document/site/application-note/components/dfs/an0027-littlefs/) +- [基于 FAL 分区的 EasyFlash 移植说明](https://github.com/armink-rtt-pkgs/EasyFlash/tree/master/ports) + +## 5、常见问题 + +**1、使用 FAL 时,无法找到 `fal_cfg.h` 头文件** + +`fal_cfg.h` 为 fal 软件包的配置文件,需要用户手动新建,并定义相关的分区表信息。请将该文件统一放在 BSP 的 port 文件夹下或工程目录的 port 文件夹下(若没有则新建 port 文件夹),并将该头文件路径加入到工程,详见 "`2.2、定义 flash 设备表`" 小节。 + +## 6、联系方式 + +* 维护:[armink](https://github.com/armink) +* 主页:https://github.com/RT-Thread-packages/fal \ No newline at end of file diff --git a/flashdb/port/fal/docs/fal_api.md b/flashdb/port/fal/docs/fal_api.md new file mode 100644 index 0000000..912f916 --- /dev/null +++ b/flashdb/port/fal/docs/fal_api.md @@ -0,0 +1,145 @@ +# FAL API + +## 查找 Flash 设备 + +```C +const struct fal_flash_dev *fal_flash_device_find(const char *name) +``` + +| 参数 | 描述 | +| :----- | :----------------------- | +| name | Flash 设备名称 | +| return | 如果查找成功,将返回 Flash 设备对象,查找失败返回 NULL | + +## 查找 Flash 分区 + +```C +const struct fal_partition *fal_partition_find(const char *name) +``` + +| 参数 | 描述 | +| :----- | :----------------------- | +| name | Flash 分区名称 | +| return | 如果查找成功,将返回 Flash 分区对象,查找失败返回 NULL | + +## 获取分区表 + +```C +const struct fal_partition *fal_get_partition_table(size_t *len) +``` + +| 参数 | 描述 | +| :----- | :----------------------- | +| len | 分区表的长度 | +| return | 分区表 | + +## 临时设置分区表 + +FAL 初始化时会自动装载默认分区表。使用该设置将临时修改分区表,重启后会 **丢失** 该设置 + +```C +void fal_set_partition_table_temp(struct fal_partition *table, size_t len) +``` + +| 参数 | 描述 | +| :----- | :----------------------- | +| table | 分区表 | +| len | 分区表的长度 | + +## 从分区读取数据 + +```C +int fal_partition_read(const struct fal_partition *part, uint32_t addr, uint8_t *buf, size_t size) +``` + +| 参数 | 描述 | +| :----- | :----------------------- | +| part | 分区对象 | +| addr | 相对分区的偏移地址 | +| buf | 存放待读取数据的缓冲区 | +| size | 待读取数据的大小 | +| return | 返回实际读取的数据大小 | + +## 往分区写入数据 + +```C +int fal_partition_write(const struct fal_partition *part, uint32_t addr, const uint8_t *buf, size_t size) +``` + +| 参数 | 描述 | +| :----- | :----------------------- | +| part | 分区对象 | +| addr | 相对分区的偏移地址 | +| buf | 存放待写入数据的缓冲区 | +| size | 待写入数据的大小 | +| return | 返回实际写入的数据大小 | + +## 擦除分区数据 + +```C +int fal_partition_erase(const struct fal_partition *part, uint32_t addr, size_t size) +``` + +| 参数 | 描述 | +| :----- | :----------------------- | +| part | 分区对象 | +| addr | 相对分区的偏移地址 | +| size | 擦除区域的大小 | +| return | 返回实际擦除的区域大小 | + +## 擦除整个分区数据 + +```C +int fal_partition_erase_all(const struct fal_partition *part) +``` + +| 参数 | 描述 | +| :----- | :----------------------- | +| part | 分区对象 | +| return | 返回实际擦除的区域大小 | + +## 打印分区表 + +```c +void fal_show_part_table(void) +``` + +## 创建块设备 + +该函数可以根据指定的分区名称,创建对应的块设备,以便于在指定的分区上挂载文件系统 + +```C +struct rt_device *fal_blk_device_create(const char *parition_name) +``` + +| 参数 | 描述 | +| :----- | :----------------------- | +| parition_name | 分区名称 | +| return | 创建成功,则返回对应的块设备,失败返回空 | + +## 创建 MTD Nor Flash 设备 + +该函数可以根据指定的分区名称,创建对应的 MTD Nor Flash 设备,以便于在指定的分区上挂载文件系统 + +```C +struct rt_device *fal_mtd_nor_device_create(const char *parition_name) +``` + +| 参数 | 描述 | +| :------------ | :---------------------------------------------------- | +| parition_name | 分区名称 | +| return | 创建成功,则返回对应的 MTD Nor Flash 设备,失败返回空 | + +## 创建字符设备 + +该函数可以根据指定的分区名称,创建对应的字符设备,以便于通过 deivice 接口或 devfs 接口操作分区,开启了 POSIX 后,还可以通过 oepn/read/write 函数操作分区。 + +```C +struct rt_device *fal_char_device_create(const char *parition_name) +``` + +| 参数 | 描述 | +| :------------ | :----------------------------------------- | +| parition_name | 分区名称 | +| return | 创建成功,则返回对应的字符设备,失败返回空 | + diff --git a/flashdb/port/fal/docs/figures/fal-api.png b/flashdb/port/fal/docs/figures/fal-api.png new file mode 100644 index 0000000000000000000000000000000000000000..5885990fc30afe1e0bd76f100bcb514c4a627b85 GIT binary patch literal 47111 zcmeFZcR1F6|2O_d5kexeXQV{3Wo0I^LpGJY_oi$@$S5*HB`bTAm1OVioxS(^JTQiWgX_KrAa+I%3zEp(yi(tK8{Vm~4yDa#_IWkPW) z@b~ZeW9hX+E)lvD6O5@{4?&l=iHY{Y>-O>{3J z{u^&kYL`iw0t03i+J0#eKSz*?oTL-LE}oN)H)+l9l5|3FCa9ZCKa2Dz zT_IE=*8eW6)Zn>9EcoIw@wL&cmnzKkR2K;eCDE9wh=oQGn_E3fU3|gyrIS}M=;_G` zLwPF7*x#g=kMAZe)~)?Z*Z>crFK1l?20GTDiV~PYz6eap#tyGKCDX*+-pMJ_($q}pHbLK`z;knVFED7pG~psIDlYaf z4jZ!~IM&|T+xu=_(tmmyDT5iiPX*RFl~{P}^A5fd_5_Y$LPc~`h?$E~{4X?J&5@m7p*8w|ay zEUuZkIU16!mb2yRwfejG$IqYSJek2?tE;KhvaezKqJ94S*}yV=a5=Siv9X-e>fET1 zEG8xfK{AwsPmlK-EO^q(%3S0Mv{+;F^++2`&CMImPLEqCqbb#mjhWT*bg4GplPheM zb6>xV_(U@+q`?>s4-acpI4pkJ<4;<6nMO*ao2C?0D@wR{CBUJ*s%`)`zWeN% zi|LFWf`pQ2U$d)BKW*tMTGc7C@TO59r*0f5b)JMZK)E9HI&I{f5Rv#cva{67aH}|o zWE?x%8QqL00S%d(n|mlDgN8J=v@|R_q-C}A_O99Rb@ldQ#5|fSRET+WBkFcs+eP>$ zzkbPGzl>=yQi2z!zPMKEah3$Dax5h2&8noj1!A4=U$?pflf468(ySJ$B+U}>ayz9>9aVam3kJB2_ z5wpi4Ev%BGmpb%!6hEuRi+I;OG@O$th7my|I=?*15QFvP=H@10sI8-eSFIZn9*(b+ zn=-hp^yG>E(vsO%&0-wus?7is^UdidjS>qoVw{V=M@G`IB2QCRSF>KVxop zx>a%&Lm?Bu6&~*mPR`{(Re^$)g=Uch#@6%Qf%|t=2kFk|DQrreurK#J9(dDW4P0ld z-B(Y0i@wn}bvCHAe!;R-+nM>Nqw^7KTe&bUY9)m#EGpSf=)`BlV+y6SeqamPUREUosmICVDCOHUdqC*NNdG|7Kyq3od0aZ?_cY|4 zp$PP&S98vzB>B05Dt_m*6`lLuFFVmzjt-)IEZJl#F5%|3B9osdcI9*19G#~_iqHe+T zw+o)N;CC;7>0R`Qx|pMO+sJZbO=)#`xgnT{8D=UJ%7%l3L!AZBOstkwy}F*KKMd<9 z)mx3t&EKEj3;xIllO>-;WNT|nFY}|d^?{QU4+)$0r8*ThB_*XPBI_3~ZrtPKwEfdX z$HmPZ{0$coF0PLb_R#G!{vsxJ{z;JbSg=Z56pFLa_JKblG%Q~RQt=?T8 zY%nUUe||4DD+_DFV>@hUz^s^FSeP1Wwp2PH85x;@C)K(QA8gIkezVBf*m--Q%8Cj_ zH8sp)%{#yQmUr*IlY<=&f9@ut)}LPA1id%w+1`KCf0f9Ia5D+ zq+F(#u|uy99(Sk&?6j1PWIHs3mo+hhG`VLc%Umuk>tOp-@4|<6I(sW}Hup^_}LQHL=@92R)eZ|>;G4e$aT(=y>a8l#TDu7{*c+n+5G`J-W4y}NRT=m=P_tT*dOGXqpCAA znUK2o+~z5YH&c{9PdW&s4lZ|(j=&wK>^F@(mi^ ztX{q`YL9!_Gdw_mKqB?-R0hXLnJvT7(a}aDk%EN4TkL0|dtHX*RXzlxKCYT{WRiBmY!JK-hMZ9*D^ki2RkHjXR@gp>j3Y% zy>bq@leaGMo5seqM$BFCx)tTV8_+X0c4vU|om@Pxb<&^R58UP?NKg`2bSrUDz&(C` zGFY5BIXU#*Z`|L`?Lq_I+TK>u(OKQTmN#q>pqV2jB^7Y}F88pp=_RQsMlLR{*UKsS z1x3#1r~3>H4B|Thxf%sxjN#+lUl~4MD;c&xUJcr|(FBBqguJrKR=laGtIHN;S2>1X ztQpqW*qE~1cD$Xe=T_#_+@`o-@@ZR?7>7ugoG7s2rSpU*>GD8MsKZU7ki?aN_g>+N z%Y~>GtkQ|=;`VaAd>P*a*ScPo;+#IodqsV2hNPu_oWbSXx>D&tp}cIrMbc#WO!%y) z2h(!u&douQ;L2iI8vrljR<*OS>%kLq7u0o~c;=~-ETD?`?rpef6`2sePe@4ZHhB{j zHNDNW`oRJkg5h|T>l?<_tjpi0Z0e?LXrW%f-sR`#M+Ac{KDioj-n-}FW^+m-oetX_ zV`C?&jpov9wXgKE~+d7aB#iB26QsG5CK&c#%YoSZ!Wd27_eK#r*&ttq2gzfUFg zbJmlpkKcH-Qfglpp+N00m;Q=tfJ9rK)sojRzX`y_ z;7&paba#9z0Y8AnK69p~rqi~co)QAecUl`|q1JOAbA;`Xk&!_kj4oLH1NM^QO=7^P zguUTpmjm8Z+n@QmWd2N{PG}FPsuJAU-Mx4kGGf8Qz|L;1>f99VK676{pfNB|aaAVD zFDuCn2L|fl!-qcYM!tZ*=9E4tDJmlH+%O7W`rEhLvf87ft`1r7RGn;*APbuhfcO`ftJ<@q~ ziQP;6wpU#3o}Slwp3Uji5Ahwc12cj_GTtFSYqG3BioXu)*k5YMvH5kvk&BsYx9Rr&#G5L+27v} zMH%s#o;Ix7_>QZX-EU|^Ik|ZFSYBT0*|YSAPW@)wP=i@Xa4GSn>ntwVMdT*NKlY~{ zzjj+KXgf{uCb}Ctz*OY5@S$Fr4IM0py_I233wIoU^otiS4i!8LESqZj_M%P7^|>*$ zDS*5hIch5twG&zGJ39`2{rz+d3>Z@BN|#-B*8HEq)O`Kw*KS1dEwSF+TQi3Qmga{K zAA%wx2(mvETB5eoqt_#=PSOIG-d7ELeadOYFxj%y*XFk|EC+!m#rohU5A`gYj~N+L(BhVD&F=uDqZ{NOk4Gmp_X3-Q%8cHF(GG0yb`0?YLH*ao`As@R7d6LIH81_eAz`P1kZ*F5_Yi@MIfQ;2l&E|Qqw=X{UvxA*1M`y$`( z@ZR~(+9X$0;_Z`6aqe@HU|kobW!I=`I$nCkYm-HY#OT8!+GWR)DSall@5htA1cv=z zo|G+p>2i)w|J-!W<~ZK0X?&mLKi=!ve`lllmVU=+o@K=I37l?Anm`Pzk^T;bXEh z=LZb*X`f$mR}pg?8uCp&ESloJL7%o+=LbT{2o*--#tkZN!_0=T_=nODBqWr4hS4O$ z;!Qbhwr@~Lc7PD0s{lg9{h9>Dop{2~_;|9&@K?>j$5rowf~nl_4M6a)ffo3QWA$X% zMnPRl;4(SZnkgrt{kU9XqR8!@RFvp4$%;oTl=-;9m6UO^Z;+#|R2nIz;5Z<$Ne*#% zP8=S;h;(f4tR(Rg^}c;pnQ@npF!D7vyJXyA0iVJiFw66t&BKuRJu_%7Sc%!Ju%)Fdr{1G39|T8#!-3HVV8gK>Z#Np28Z z;D@+Be&Z_KR#JWT{r^4S|2GZ@uyxRG7VicH{b2>AOX7n5|B!Wv-ds25A|G0H;-ka| zSpHf7vvN#0bG0QcJsks>(4VCxbcByGJf#K4W%}~I@t395)usSE3J`j_1_nq5cp|IL zqGhbD*`d60k>h>&@`YAL`J;H6jK14P3|jh-3!zQZ+hv7?oPX?)hqAH_(@mj)V+)%z zEr8`;zJE{t+{g%1Ft6L>mDS+r=w$@tmaavI2c=Zlp-oqvej&6nS9vUdUx4<+Z9ae? z$|@>oNLX0dO%|4yVyRb_mv@iypG8JkelfMS4(vDcuh+k_V8=J(c-oTCWcc78DFrV> znSaT#S=QbC{x2iW%a|C*lNL~ z<%M-*PQ^u1(a^ z7!|(5wkCcigF*OM=2i^O_ChDA%-6(E1A~Bksy_c*eqm&kmX#&V*bHqFMi+%?^hTC< z-GHB_R#cFKg!P@mx{BlZ$hb)4tPP(c=o260Nx5DQE;;~Ymy(s0tt*LQtT!wO`i3PF z1w<$SPzuY3m7miyGYnMU%QVXEZU96JTq(yh{;)agX$ANcZWq|i{j~4lc`T1AZ$Bd$ zv7IWZBqb$3<>W}}=ulm~dKE!HD>5-L5q`%hIVK`Id;1p3ENC>KyulJfExw_lKPq{DblS>5XH@bA_&JIq`v z=<(J>{lh)$(h;k)qSTFPzR9|V8!D_z$K%iqnmiX zp_n2_W98-LKYsm^ke0@RC2CbZN8$k#1-2gfw}pY@u=1K3-J+?y%1Vm+PAj6iy41kU zQ1gVO$=jYD?WPg`Z^hPN7zyiA3b{2ZE~5+8WuAqCgxr5GmqrC;NW15LL)rj zVv^H(y1L;zJ3DWV-{Z_GUjZuc>J@r{N7*vJG5{EX5BN;c*wC2nl82@BF(#JGUVqF) z1iKMqI#$*#K%hi_8A*Cx`t_^aO6}***r4=%%FZUrBj7)S;YqjjE<3$Xi4SYvq*xO$ zMX)kci0gy}WAv6vRf`z!Fwf^?UpF?HlYWP}Z!ha&My93Ve9OX!&L<^28M=(Dt~vO zh73?{cN2K6sV#UY-oJkjbn?5EdHZ|rX-)r-`-#uC2mAUiAcj{GoISQCbouhSyGuky z&q^(Z(UG#{+)7XdK>Cf}k>ZCI#WmMjttY;?wnno2PLAnU0}d@Ovt#_-Q32B%l(b$tY@TYj;_~UNz}8EYM%<$v{EhezR-4RSwNIgZ8YmFMTz0 z_P&;018Rvk`74m=P|wx%<6(<*cLZP}u8c(o0ZIXfOTcZ>+UC%-ME~?)0IdIT-#MgZ zP{)}Hly*~77GPRZqrOHw36XJe%x-3uJQQ17TU~>L3)?Z-j501upR?XXNBjEuU4Swf zx$;^SdePa*f(Qm;RkbNqa02)p@!_|hzc9we#)c1gye;+)k&L&OmoO9|^d-+$hQ8gm zhkEC!0A?9)+@PbQd(fvi;kKM(PeZ<#!T1*5$A&9idvJHQ92Te2KL&YTqd-a5;@Idi^VL zIQS^qp1b|fMnRMy!SGC!X9kC*K))XSX3Qhhx+yOwGb-et9UEYJ58TaTr(0Thw|2ZY z2WYHM0<&MmGwr+L?P3p7FIHw0vV!hG9@cE!rO8FMHWV!73&I&Tm>#W`T%5N6u6+FX z@$MmCXuy#o2t1Vi*y}be@?7Mw!s*|Cm*RXR(&HSMfRkxw$Qu*!p@aG_P>jADEtVEJ zmBRgxi3b(vugh2P@nsOxht-kN^y1=cfdn+oJBz&>e01nQXO)(cdY^44qXfdc*YjOArwvLAMpl6w zOB)%@^9Q}54<0-kMWJ{VWk3PHj0#=AFj?KM+MhW(a=S^4^9KOmzIB1doA~(a`Pxy8 z7Qc(Ic@pr<2fm^qwpit`s^D{prYZ*QO7|GSDkoZUp(-6o1qB>=1y$p7u)_Fd0OHyFj>voS@>FqS;ZxC#x_ zt(qDX<{zD%`@>AChQI$O@{^aBZ=P>Y7#5PP|92LEPUfmlmr&JNHDJnr!NR5CFaXr( zb=%H&yQlx#A#2cG&~D+QEF8WJEucg!O$mkaRg|F;!sjkm|` zZs*|hAYoFzVBsVWMoXo?1n9Tvufm-Bm+%CXl<;)RYiVf>wJ?=3Sc6j_)(q;TM4lPn z@{g(r*MtaB^JZ4`ubEsfr^*Q z^x?^?ToZuFg~nBH!WHm3r>cq)0|O(ikW;Z!74p96%AZ#K)Q$zOm?>uV6s1_ zyzk_I|Gd=ABhBDA+syd*(XT(V+r&>k%eVEpn2HL~YpL`hV?G!gQ*h;g-l#&uF7>A{kNQ)9I15EICfSh8O3rb3 z^a5-M?Y46Jh3Rf-nrnq7pZd-E1_r`!O~sf?%srRS>aDln5zkOIfQd$YV1S|4h-N6) z!@J}Srk9uVKlfF;D{|h$SErR}fZPUD1 zGroP^fM~oxyDhatJ%31Chn<|}FFIEKteO^1y0POxLP{Dc(N}5wBY8iC8v&-^rux_ob?n)~ZH*PQh!$mNN1qn9s)OC#Sy{s&71h@#65Z z_DG2(1?b|8Zl_Q_pgLUwS^^d1ZCsqf&|+fR@X_uvn#)f@Fw0pTY4$f(OqnBgJVp9o?<{|mc92gGyO%dvN~m@)<1Iy>=Yet| zRI^n_dT2ff@D5f9-v@cyUOa(Pn3R98Eoo85ZwsE#cYFcLYHIVG5$4+3iDj9vu)%6` zIh>0FC>&T;#s_>Yh>?tMJI3oDlq&9(Fhv8=06?}^8w2yStgE?Kp5Zbj@_m0QEj;hI zJdm1_BI;p-$MeblqgXLbM=|A+N9&+K;(0(lya}2T)A&wk zNC=oSA@IuMVw7nEs*gdKYSw0fVG|B)%IjfL00%&ePhLQ2_b$SUcv(PfpkhH2 zajVw-McFen6!ahZJfqe)%^GSh41tQ_-}WkZR;F$&h6)>LL*Q(Y>PI9EHM}M9-}v_< zU0akCy#}g0Rk02oZq*D-X|t18q9StP4p+EbQFf`zTKNJ!73O2KnN!VZtYEYx-ek*> zl8d7|^;V_nSy{}pDfFRDufUc070=PIAVriqziw$^(Fg%ay(NM>%^RWSX8icphb=XuZb6O=Rp({ zple+|N;Unu+Uk7$^?;vq*|V%ab>zArI6voN=;J3?=;KmN>mL~j1&9N6;}L7`qJu`I zQ_d%Tn$EqnF;(Hc&weNwwwsZ@bJ2s%UF#ij&+`^g!2eT_Md?xkBc;~T=H|EYX@p-d z_GUe^vANqrr*z$@Bav)J(_d3wVv2>8Rl>l);A2C4*I)d*wNls#suk9tuKOJvTwMN7 zp+P|bO(@hI1~ye%ULJvh;HJdi_?rysQ#>YLuQ_G|Vg}{`K;kGA#(&Qh%lh={;uW}k z)ew40v=kJ)kK(lSX)=HJIXP6qNc@*UK}>Z2cQSJNyj?k1%nVeRgxzoc0_Yx)plmjq zNEu+i0IV5;@&y88$n5N_(rPC}-!hcx;D1n}rqlX39tdRvFiFBApa>8=%1{t_W?M*2jITMF6$Yoib`6y8;ou_AY z=Mj$o_l5Y(C;&kxhn?O5f924+zj|x4RVh|=60wj=7(*y(wD`eWci&(0f0XeZVrPV; zq*w@`mRJ2|l4@!s;Py2hv9Pe9sAZGk=5Pc6B}2os&U=N!E8781&{9CifqfY?=T<## zoB+5yc~((gf|HZ8yRXmh_3PJk-f`eGK2G4N2VxA03DPn8As4tWlrJzY4L#5K0(!uc z@6h=#U>>ayB@$CqS{nMCceD~?Y1i$KLj_C1WkA^uPELlqOZ{{rH~kCc8Q~6)kNi)3 zIQ#Q#KSHP}jYT?&5k5|2V{6-&>})hdD!tp-GrH9eKq~Ng@QUf_=+K}dfHHc#w~9OD zEiE1Rr?nM2XuWo%K`3OVA4&W%m~>vugBT4XQEoG-d#q8-pN5uCOUE?G`Uq+G`$&tS)7-$a|_O^Zs?LpBuDjd!te!kH00puPmXrq8DB9oKX zd)=vX;VF=dgUF?Lc7Iw*3bMGk2#XyBR==v<{#Rzmaqo=jK}|)wTQ~WmRi%Y%LH3m4Z0+n(?#2FaM!AONbRYEcOtV7I>Yy zDOErW$k)2&5EDLXn!2uT?%=ZIy;&M{C|DYnWMNXU*kI}a{{k@y@eHt+vr7O5jLO9} z5n6pcYZ--N0lWh2^Wec;%G!E;AD~fEQqB1#cn7X^I)UBcJHN=#4H#fSz`h@RdB(-~H;x?snTAEIJn=padUvf`W-mjc~JSq^Gy zYNCG?3I+Cs|IR1stsEkePgG~ZC|?$_O+6>Hmh6Ztc12#^jk)3U~S*iBtylLS8g&T zPk_;zY%lb$T$5oeE_7mO6G}I#zClCoZU!$6K|sF%Kg;J)#;e{%;^L*$Y4ULAh$Ub- zVD)qXV~BZlZI7ds11+^PqSWS@rI{Hz!V%PdU-%H?)BnOD8vey0@* zm+U=ft=+0RmC9PUx>1?-@9$n()E{)FP2RbE8yB>FsdSD>b5CzDWbR(SY+`A7&HCQf zWb`JbBU|TPI@m7d48_u7QtG z0FkUAi;|Q%T9tR&%tF){32U|IUSN3zQCFi74}lVxKr@6^id)Mv)PiIk9E%PWN->sx z;=)G!Y=?T;{kN{_u4^t83_OY0AwNpa(=Uh^+{JPBuI~uk-z8h4#B-D*qC!nLdwY9( zz6LAGHGyfVQ{NruuKET*3;aasnHmq zn%}=Qfkt34l5g#ppH-QM-66pRjsf))j1{Ub`)bP(9u#GP!GY=m4#Gn@IUrq9l9Csp zv@ov}DMc2+EW6QF4O;yA=!WkVu%J%ND#3d24EpJ;D&_EwS-e)AfOX4|8at{1R-mP5Wfq$AtFM9 zjg2k5TLWtH&cRRaA6tW!kZ6JS11J-fu_>ceC@L7XK*fCY^z^y}ObUltjecH|jA9hr z?NdQzFJUi1HtPD<7lp$cTW#!+DI$P(01068GhSS6kAkeMIH0O7@>P8fXz#(F50J?L zEo;2v_*dA>;Wh&RBVIy?yTWl~zC6u2PK@q_V6wuq^j(tY6!G_ zQ1s~o*4EbYr(@l>W*;b<59VIWxYaQ;UD{I*qUKLrK}2N6YQeT(17i%*;$txnx@W+d!6V_IViq zHX6?zLbb0NbcVZZPcHGqD1J)Nz3hxniH`-sJ}^vFB+s70!1eA(s(2tD%h}CVJqj$& zB$aBsLHT^sy0oTpftGY`rRmVjLFC`$1U`}`Osr975*|PkD_+)U>jx`BvJK_akkwSZ z)j+*LWFh501$5AMh@>7D>q|) z20gCk>|u1^VwhqcDH2m@Dt~0cBxXp;LUcOB?B)@TQYPr>aq2#XrY&;U{U2}?zxG6a z&OghveRlK$$3R3Nt$Q8fNp}Hx%FGWNCrc_Q5ad?xX%dr=*iBv|B*gm*ugNKRtt9mH zXp+DH_($l!gpx+M#y$6SzG{`S^%z_M+Jwpzahb9{{l?fshq?_JD4;j6$sY!af;VvG z?^B?pGEkv|gCK_T?mo?8-nH4{l23A)8R@l3v}==Pu`|5uNq$#|36nERq2X|$NkV{@ z{yjaQNTt5Rqghi2s{1dav))b0Ki@1^p!3|joJiWx7#GyR2KKa)sx5|`6LatxQC8RK zZYvlY5-KoJ^dYzG$S@w`OV{%I6IJO-IK-lW_NVX9owp+8lfoS?Nf6JWUSIVv7JL0; z0|ASKL&@Jq*xd@T&zkYQdQ@{{r@fDbBzu(MVzeA_*nIUK-4nXn?a>}&Vu(`rl#HGa z_qMFphL)C`v>Fe;>=bU&(mG?jD&~zPd`z4tc+6KLm`{Uj98NgTOigDzi0)Pp9=)~~ zvEsX$bD-~@?J~OGANS{YNtUm2Z)!7ejVPj;sBda-as43WLW9Ekchn1pMx2vK<5M+S zuODN>2U2DnNDZGj)*$fMGyPH0ppcS&dj?S0eS(nqBrTR(7dTIe(4wrWS7sCsqXwax^4PJ2u4e zyaZF|SYXpP>YB?*>aoW_G|l-=q~6SzbvAO_`?X(Ov3rew)hUlv|2f(rLX!MkH&>nY zk6x46IIT9g&9HA~zmM|>zYz~3<&nGh2A#`&5# z(Blmzs1)-C#j|>Wk8ZO{^WGx~y$Hz&U?tpdAI*Ild}jf5FW>vzi$#A}$@u+)MTT0E*2%BKo2cKKQFFu+XFPe7u8dY+&+mSh zeYsLNcv7ygs}jwd;{9B6=R#pkaNI;c^+g(Xw|qf(E9I4NbCtzhuXa zxM^4}8mHdLTZ=D*Hiol?Dh{9AWPk6A8WgE*nkIWK-r8H?bKm)85M)l$9=)RkRU{5+M~3GMh|f&VyE2iX@sQnSJy_y3tlTE; zOHFaEH8tZ!Q}29^hOIHNHk-~mHxI=3r=kg*4f1Bh;Q-zT=fki2t$~#rHk-;y%#RuO{VlO-E)W2W~cn9khzqqLw>aK_2f}4>O(D=H|ro#)Hnlx+KpJ^fIfk z{VmRBXwXYeXB%`54yv8=pC68)mK7#~8pUa1oL}X$%8~jaTRYonvoq4Sy_v2~qrN|b zg2+0HaZuDpYeopHA#e^QbONpJe>wW&#~8M@E|!aDWp3^(m5%-C8g=d+I06Z%padir zI35K2jou1w4#c=SHawLc=JY9fE#+N#wjVK8TMbqcp;p<#g^fO? zww9jZVKOyAE?0+s~giaCQWx1^_Gv<3m^`?=Ncv z8w6yMp_Vn8N^f_N^iT&jW!7yL8mDYTp~PkP(?k4=C*jVDi;11S42C4$zA3L^9rg-y zl8Q}oQv9G~?t_ay6FC)FwO(h}DES=}S&uzB&x7nRpg>62xtzAUruB*DzKbyydRWEC zZ&J;MG`6eK%EmC586Vd;gpg|wC(5!l!QX2Uwa~T9X}qrNQ+P!XO;D1y`Nljzu2gq@ z+qs>$^|?QWXndHINq_1w?~L`j+xU>Nt__+pTNzS$J|TZHo2oY%lSM-$lRK(10`2YK zIFKgDDVDXC0$S*lKo9fYL}A|cFDDh7y7!MV&=C}+nn2TE8!dN%7zJ3xa9jYy45(WL zz|j>e$)g}hwVSa$-|h<|1Y(EkttUg4IZq2Qar_N`lr`}4c!Md2&|((2wlK#D13MwC zh2sFW)nA=g0KV)zh2+OS=Vv78g+}=Ss^TJ(xs_|cTu^S`um%2o>h}k5U{{F&&VrXQ z8yRwEl-ElBy70kiDf>L9{}i?_f>agHDWnD3e_qptmF4#1bFo{k9Xen1<$&G3{<+0jKU<|ODfLBXJ>qoF(?PhQ>h@b{>l+DCQeS0%i9(olAS z2{6iK&&WzoH}q^q2+k!L2QPp|1`2&z8a*7q!>_Hs^n;V(M|IRx5yZbFzVnTLRm)b; z-m2ZQt1E4G{kYq4R?v0~2Nkd0pdPnGYe*(K9F=TiB_a+If-Lsv+Vzg+yAK{WnodEn z9x9q5<1n#+uin_i(x}f`&b(*6{`*Zf%g*qCV;Mo%yvPxw@3*1tnO7wXLI*4A-0Tma zWOt8W8`P#KYH@=7_ffSS4t@V~T>y@Y40cW7 z9VF?2xrpW=r^WA$%dgq38s|mM(6;j`l`fZPc^p%jK=1je=NdCK|E*kon)&?b5;dD{ z$bnyIW8lkkS8uEV;gxr3?Uh~{Yo|>5ko`h@;NT??P7T&r+vDG(i#r|%J!Zwm)j2l* z@Y*tMHVE{TojK>D47BW3NS4y=d1IL!QLH5%w8(V_qBLe19V&0E$#=@Q2+5f*`9PeY zb;#iO8`Q2s6>1Z%bHYV+-5YMVbpnq!CcHO2PF;7M^GJ`Q+Zd*a*l>R9t{mEGz-K>b z@jgq9Ut0!@VDnO$OmY3T*1om*Mz6G2Sn|m-gALVM!@AlS!`1EEh2K0g@uG=3Q0S+8 z@HrYHpCm|UWbnrLP0$Cjz>B;mz7G%3>NZq5>&n;9la-OA;hVGXC^YrF@!Hls@BZx1gfg5ypqxg_1je;=r+82x*oh5#gzhP5{MorMLcH8vC*#OJTgR`M!R z7PqIT2Tbn}u;r9;AXbMCvE_f6Zg2S@m*RJ=U?W)zDu*~V&0aVKYk#~3a>c|T?4gaQ7w8>U^MXMvs3zcILMx2xx9>e z55xm2yA@;T&uf$@RR03VGNy0+!XYFEkW21PNI9=M)b1R$XCS&xXI^V4zV;I+bfeSQ z*jTzQGsKP{mVipM>gGv%@NddB_%Y_)5xM8a#76Sv*78Gp3A}QC-uv-r`1XnVT@u zseXRGedb=a7qEV~W8`(VKNh4=cFCTtoOrd}da_Um-{buJZAjiywd^&otuA)-M?_T% z->Aa1(KqH^o(`4gkiDO4GV$JKPmA4ib4&_a-L73H;Z41y#k_oTCPFn6mdGo2aT-smaU&>xP`L4tick@CtXdw`1 z;xxJQ{<8CmL^>F3&ETf_&uBAMRkzeOz0B+E*l{@HyyZ6o;EisxXZ>|q>=P?<0OK`6 zuJ7cPA2gvlJT9nlcgTzS}>kK7$Z=y1*0_D!UhY-T-X#9ku-) z=8rI@0$gs%r)8FSF0Xy|4b!Q+h>DE!X=hTw6l&`~6Wb)XB!=7K|QR9`3Vi(#N$@O`bKS1(p+sgs3C) z`|l@S^QR`=>=0qc1e%?C-uM}4zrjPbUv)*nyyjGdl4Kz1HSoceKK@=2q|E9;riPdK z+AivRptp-DC$;CFBp=-#&24iAt88s&W6|%+)@7$h%vFWNoE*pK7tPx9rr^NVtoHi5 zZ%Z&H*S{>k@!rOJC|mx#u|WR$Vxe)$vH9d5UO6}BFN?i~Z@$LA*ZoKf=<$S*AyQ_tBewN8An})FHEKJ+2K3XT5 z(U5IJqshuvk=G6yN(*p>l~lMG!;jiS%uzd)$;ot@3D^6&#a2zvxbs9gIPv|@-%{A= z!OGFE=O&{sr+kCC*~<;5g}vV=Z^*c~Ct{(_Vouw_>Ds~kSR;`s%luk}pSc;&o$ zs`O+bZQ_fkj=%1){i?^dB&L+~#=iH^*(VRT?gn}8x|*6ARxZIG1Tk?#vDFm$VM~~p zJ4h~;zCxnlZuRfMeh&A2Z_J{II`{SJ{d>=vUk5l7i(R5yDZ5;y?pH^ZZiN zz#97T{<<-Us)-`rNo+b5ML%1DD7;4QMR1NCo=JbPBAxWDcBQ6@D>33^Sf`vN*3UW_ z;d|wa{xtqbkFnD|Hj7l~Ylq0xLi&e}US`H1X(PG0&{tw5FV>BWdL*($E4nsP8amg| zHyAiG1t_y)g!|OZx_goyo?3<`N`KG2A{LZsL*`=1U703SzLj45WaRvG-CF@N+|J{! z%qp33pv~`P?oYMQbco>!xlF6qQnoe8s;z}lHG8uiS^B1b#>FG(;Vt4VC8%hc3&BI4 ztx^B%;MHSv9WT7KV)ZmZ(|UF7e0JrczSf>qAhvO?nM=YwnKdKVJ2RzswkEOZ{H~<2 zjDGE*CF=W~U_R(&R$NkQJs}Sc0(?DKkAbzAOFwibNO8FJaa5P zcPro5q*nJ!@cPtJ9wa`%V9<$`efu`)$sDG>R+7a+0z3MK_}q<0k2-mV#)r3Xt)#P- z*{q$cSMKILeJ^*3&M%+2Eh^aPcs5$W1x}G@RZ{S3Z|)l-mzHNvg47if@6^^+@O~Sr zJm)lGdVE2#e!n0vi{F2o%KIz`d&awY{51LVmVe(?xO--QJRrff?~Q`H~iqW9LPQ(vGc0Y`VsFx|ix)T0nu!ou>lY&L(QnORim8 zF}Lb`a)R`U2xuz1srmnc)$=RQi(l=cmgUnzIu0d#hAL07SBcb@QvG#&>M_z)g{+^g z9NE+!d8IEZ=xR0FeBNUYVbe|BW5zDCd@4HFA+f)cQ5bPSkFGK72E5-0xLjJ5xV*mz zVn|)#x-w?OC2Ju#q^x*(;e>?af#>5JTC6c~iDG8SZ)ojGbDS>~A{glmqip_|iZ0W{ zSR(8uzcMyz&)1QWZ_b5X8?^fhLC>~+-T1o|U-VQpalgqQkyq6{jGUIWk?qXQ8M{X3 z7Z+W3+mGTH-=pK)*yHhOVx^@}HM*PKPQv;D?%ncFp&MbfGvwm$Iu$^Ok1p3ez8b zT)iUle}@lVA8I*mkTuv_&ojUjoc(gFNFbJvCvl-mj>;zf#prgU);^VPg|qFFE%Euv zcLaLpM`&po>tx0B9qG0fb2o5vA4!K=NZR6Qx6Qg+clP!&6GyR2mizB2{0sP|j#vxy znWGo*Lm5Qhg}PLKl?*wjvMfUJ6rPPS|X}6c@vW^lzo@7u{jEFbY ztB@+#E>L%>M8U^OGuHTYG1E#M{X;vGN4?xS`KVS~{8Z;yq+4ri>+Tfu z(Onpc1KJFXl?b@qhZz<;JiJp(H+I-J9|VNR!skzt-t#(=aJc`Z0N&zCkMjo3Y?GHg z@A1~g;#gVm7v5}Vl5l}u_3f%7ju5XszUGYt)CZ;8J}9!q!<#S7+TgQ!j-*8wI-mxs z9O)aD&&eA+xa!Va-I3?{^*e8i`>%>ou@aHZm4QiVp5g9xtChwqjeFO>CC?2T&kvN| zQ~C8{LEfUv)tX*w6p`$=%KgUefnMe7gtHni^g@}R*YqTa&mbaa*KNsDm&-D z?dGUftMPGb?yf96bk#{0mhJr_Csn_f5jM;#xgWur=>07&vLoZ_K$RKFQC*@DE{n=u z{*-H|lAZaJ_LkY6xtywk?lfTyt@;liS>v9~%<11}xtPriSE^LsL!=w>V|lEsZF2Ta zp7*y$yK#HFn}JW_xATv5Lw+fwIXbVLyNQ{2fI8+WCSmgBPy+3`c&S${T2+5RFR{W! z>=`iB-bO@BcQx5+zm1F}M7VrI>M#O0Xl)Al0@F%&pSy!X{XT3ly@tld3JIDmcAF(^ z+2f`q%fR29ITH*9#Es8w7kV;o#}}@Pz|B$C+ZIP*4o%;y zjO(?xDDPXqYRxdvQ?XGS=APw@t6z9SnS9lDustFE(KOmzTlC4TA^B6KOJrlazb$Lk z%RfAV1wOGCfOTPBED2mVcC6Ca#1llBfJss_xL?G!lv9)R*vWEgNv%7qW zh=gvb%&?ZRKi1;J#QQalcEn(VXUTXh?f!Dw>T68k#k?uhFM4Yhula9)N2N@v5cISle4`zN!7UI%5Ul?-47Sf z{l#5N@X)lKdKR>uA3Y=nTsKve<43n$3Q&0rMIcWDd;T(TyHOIEgI3;L@Rm2xv| zkL2a#09IDRX%s2JDS=yEdeuR#U+RLzdR|!6Sy)s`%gtpD8__!4uJcqJo)2mA_L=`Q z)aNsiL$FPz&Kd*J;{ny@qF$0BHFBvvzl!XBLhT)TIx(3@CDn+?%#^zM%#31(>F&^T zE*+Zwdp|ORBB#+i-c?lfmYF$7oH|uu#^pL95X1N$b9H^p4Q6JxNsOik*1XmN&!LW( z!~^EKo?{NzT-6$~moULS>T#E+78(-qi@PRZ!Df_e2F^&m6n)a{+K6H9$K%MhczTJQ z{p)!KQU>8SYFX+qQkAdC1-HJ?e(Ebbk>2yV566Cdxb++w6~GxgO;5jIuMMM*(DUmZ zexj6{lMA;VC2;v$wU5^2TMr+;o%aDGg*`l@t~QIX4Arn^e_?Yv1|}OBLxO^XQ;LcrAcBKhipmq_JHyT@_$xjkBM(JJDA02W!6;LzPOFVZ{?ov5$3htoYmvUVrAuI3K1Y`LijF8Nc*Y^o*I@(*?u4 z%7ga8=N&DDQi5>3O!$G^sI@twReLSAjj81ERoW51+Mg}$?JjfC+vC8lqjp=e ziWARqB_qWuq;hd_eeUQW_nmC;FLe6#(Dd6o7%^h!=5CvOm#UN2cVy9>DljkHRgC>b zske_J=^U1f+33AvyKOsK?6-|YJ8dbveDC9Rl&dmFEp#hQ(P^3Tb7-{J#5`fC3n_DM zJ(gL!!i+IU>FkGV{i4J5wrRCL#f7J2?5LX-qnEY9-ZmlEc)NtBb(aLv1ko3_6#8ww zc|!B``4)K$XAU^&{^;bsMVD<2&AM6TjD-F27YUcn^SID;3SHn3Ug}uFd z^T8AB;c|2$qw3Sa>XqC+w&iq0%Egxv*@i@h{>53e-u;pCq78y?n((x@9V|JGTVozu5au!Kj35^5tn$%^z!JVj7z$13?8K022@rhQ-8?)S_{tv-aA#<|%|A$pH+^OpoEO%h#;>xIl^k zQ@wWuP+N62GMHCiPiNlh>borAw#q_&V-*FH(x`bR^W@rKWn4_rVa@ODVR(m$*;!dA zv0;qR+U4lZf(5o#+kMdev|p8h?kSXi^%2!s-bBL(b~kXh+oxk+)fBnu#|%aN7Kp>} zBw3j;O7APOcVhI#WdrHk8~qhW#q^dwn2TyCNf%-!2?LnC=*Q16s8jXP)a0pitGUcL z1P}Fhd}&NeQ}U9B^~FE2Eq0lcCn0_px~7Jt#Z^PZ=1+Z$Vv<|C>jFl9SVQSgPzoLeUUSVaYSFXAAfX zDz{q;7Q*Z_3d9f7`y_1T-phw>tIp2kw(M;P`LFc#CVJ zRej)AiXKj~DOJR!&-{|69Z|Twzh0=+YtE&^9EeTJnf}n**VhY2As2dAK+WTU+N0)q zwotysg#39thFzb1B*mYGU(fP4L~__hGUlY%8c7He#o68oaQoJ_mXMoy3R1mF@^RO9 z*?n$fTPI#;1u9HVPQr9a%aS)F3hI(pY$UnnhQcf?z9*>IuNv0BkSUq5{hUiWn2Igih2_MzZiblvzNym%r1+Hm!;0lnS*f6jNuu*PYTD!mUX3j zaAz}vGRiDJep)A{#A}%AMqlO7zPeg$qAgxA7$tRCIDFXXH<0u*KBs$x_A`-n#cA~oFPCfQI1Y=m)IN59y3G=} z7ln6nfq5pc!|_Sw6vacI-C7^v9J(&JWn}U(gZf#;HCYR;8b^zAW-BcfZGxV~8_d_~ zp6IiEVAZcE&4=hzuC;ViyY7RM>u@9gxavFj$eb>cHoF|~p-b7!or3xri*IY|iY*ZL zk(#L15aH$Z^yuf>vx*IvmOpG3Q)lS_eIMslW|8PazJg4~LG>QF&`0jK#NV+A0Yyu)IS`0v)GxmlqCV2yg zbKRA&E71+0w`hsDOd6#h7@MU)s8_@D^b|?4c;FrFAFT-~KldEiZXJe2=sA&n)MCu7 z;*?YuA?%AXmnZHL{Tj;Lv9mS>I2>T4LxF?{{$Rg*owJoLz~wMg`- z5g)Y}9#^bZ%%9nyc_I%$9zeQi;CfrDVivG&%PoXO0=7hijZ^(>dAdbx@UO(@wL&{U z$|1KGrilGHht9gTej2?Ca%4LS`XmZY_t*8Pq=>szZ7g4C{CTy2DE6m+zjDJ03Roz> z@3#ni$lT_z^eO+^o0z*cC18_fE-vBp%f$1b&?j2aX@N^AjzVgL0rF)Ez1fFLN@sK{ zrIwUV+YJ}^=EeuYZ8u{+Q4QFMyd2#N3cVS+y8|{HS1<>6*HVW1mi;J_@ZSJ{wR%PC z+;ekg@6rQSjp?n^QX>X`Uhhu5?Y?o&?+k1O%ZpWqKi``9uSgEOqHMFGA_9esK%q)F z0rj7%Znjg!crZKWJ;e_h1Rh)EPe@Cnz8t6;ciC}3-9Gg-e9eQy2&<%4bIo!x6#HzB zRm4fGTYSO$?O&DB;~L4W3~lZPeCgtJ?TD)VSC-Kze4GJqI$L07CNp=eG%{ z`Uea4+`=gO--Gx}HtY)N29(4Ai}^P8|KKKL z5We{rae~Ca!|A@I<(1>lpVmDm8lM;wP=sNz#!zNt7Kz~aaY1+H^IEY0>fQENan64j z?B)%y2nso3AQ}7_b-qLe@P|>);}hdm{;hAc;rM~Nyz@(c^x3oGQX`)-=E7A1O4eJCTeYS*JQJRpjBXt)lTYK%r-tX2&BCrg&MN)-&iy_6&@ZKUJBV@^QwX4GAl+T|= zCHwHzWnhMOMU(ylIJZIE#>zEC8QSqE7^GAl2Ff{zM1fNLz!dt0dC*^MiY`knIBB`b z>tzaWP3kd2h2Tgk*^{(x`wuKLGvPuSlxBrn-k0p7As3$cawimZbx%TEEi!V1f7*p9 z=I9J0+LNEVj=h}~zXiD2BD@aNdht?|0ByW`cOx89YLxKHF~BT|Bo2JTj2o#&gkHnn z7SwOlK_XoeHj?SEDjO-wvbS_?<=+Kd?y9-ufOi8Ucm^<1e1bcJLpYH4xs{hcxY zm8>ZuSY%fM)5Tgyg-1%@y%>xCPgo9d*U%?F4dSjz0vxGI3W~G8rfv-rg6ib8M8VAq zgyIPKX$;iwM*Q1r4Bd3@8A1ex!AajF#w2tTLl+TtB)@9dPzj)n{1G11;w&FK!C>>0yX! zP53ib@Ib8&1|ZphNdSlo;4kV~3v?DrLof+}j5t*3t*CO2JosOOI?jJVolo=cVC?bo zl`9UzM=)v(lOf{eF8dp)X=zy!J+3whBjFGnVEx4H4L|Tq`u&Cy*&W+klO4@`Rm~0Z z2|t5EarhKaPT5nX6#zIUukUi|<>9Dj9z+ARsZ7Euz`Pu34Y#^~%?)w}z|;Cg&5$W~ zQU`%tQ!`)9)s=`?lP@yfU$5jqmYl#VxJ$B{N0*{!Gb-)ICTC$y`lTeru_mH_T)&_L z<*c_dKJCVn@TUD$G!5LD-9j_fx!iKidv`S@Q8FQnE2=?O<9+1RvzK@aduua z>+|-&Uz4dlv5Sf6y*m-n0mz1vhC|1y> z-dZqgL$BIvaBBPZ{sjN(-b8}{Z>+$=l9!iBo*uE1+P{#KH^v^}0C}s0xA=`-ivoo7 z!#1~weVkt1d9pf%*odg3l28UqHEfqosowLf;Adk{nHl?&BN4{(bX@U6sk`HL(OcyP zCtph&H>P_GzbbeL@t^1UC-gq5x?1gcebWPCte6}6My;qS@=Z~ z;Nu0By;lHU0PJyR$4$_7WMI11>ME`3O@`~{6d?Hr;Ej>7vAC@j5cB2^)pDnD#}jn4 zb>U8;EKkRxkBYwg!*p5i5&Z8yyUO#Y8e*8fm4N|Gz{u_?C_L^+5VysU-l#c(I&gjn zjxTXrr6M#B{*&l-@18?Vbm9Jmn7SHyOyBY|iX|DFC2BDGcPwRoi^GNgO#7U+>yHgq2pe`47>o-WC8501lu%gAGF@RcnD-yG;tmYE9H1m8xQ%E3b ze=a%HV*4}2) zTQ8FKt#In97hCX=h2u=8%Hbs>BD5veMAod*8#g>~R$!7E8Ff5ImsjV?XxuXX< z+bZ;w$K6~Sv;kuL+M(14cs{M&emoCHZW+JBq8nVWNOq7Iw2}ns+DjjcLFfw>Tc*ol zZ~p=(ImB}+^a6$ReYkU(`!p0i+VLx`9b}oc)-{$3Xn_o%J!^WF>dLv5k3sdjgNlc!ToRWx#RvR>`X7&{wh zrn>*#SmRg%T$X;vaclA~o%Ki6xTD(;5p|*-#Bqy|4#+%PT8PF= zn;CgwLm+tmJTgarpFO;L1q2M?5?}cH3%T)xH{UilzjW&sBD!_VZXAtsABp1Bvbd$H zdIse0GRKPZ_rJvmoGcvtmW8QqfdHs>xjy?*;f;BJu_5hg#j(NQO`q#wzRK zR9WF^gw=Xf;ld5k@%*Jr!~n(syb4f?t-5EDjs2q?5w6AYbA34PW54Tv^`-dt_!r-j zkcjWx-A;WHI((OQ=OWa7+7w_GYn{I0-t>6Ux+Kj3B0hu$hb$z=F%(M~#u}9#& zhw3`hn@(6jO5zhti<@^|YjM{fYGJ3K&6Ff1JRoC$P5FX4T=6Mh@A6zh&L>D#3*cx% zaxz$oqyQm8YM!}mWrazf8LSn2lvBCrxy8mH4g&rFL>q%J)-NX**cUnQM&xp!!8ek; zVUb)mM34it$P9n=>pf9o-Dyf1jnrq)3b`jj@h^?jRXd=Au^Ao+VX?10(5)J}T z`=H(wKZgc|b(kw=S4#K>IcP8b%~YktvFz>J!&8n+Ljv#P*<%*h8kuLe`t_eI*#?l+ zcH^|(Z(VuZH{^lqVh@oDuUfhw%3N-Jqbqj)295DFz(3Yy*64m8C zqus92*)~2tsRQY1`VwT?NJChWfqO!uCLt%JkXOwO0=i<#iDf36#aDfa<>*4(Y{}70 z*pFhH%2}jLfd8LPmAKoXcWe4+i52Nx!J&{vq-`Eb#w7I1`{2Mi`|iDqw%=91DpV!{ z+|&W_&J8lwfY|H3hHqoGKM5$w(bYRuzxZX+#S*NZG$H5aCJbT=hTmg^Earw@nqN6l zne^!5>Z{lpxLh-6!kuE)J4!n@BcLII&N!^qPK4CYX>H3{#tFV@=5F7$Nb+mEnB_RU8B(td`?u-?QeN548b(72GN*bu5Li;+arHUSb8w#NP2vX zM?#{_GC4)sRHgm{zyVq@S&tj&hR35c&H6Dx5p9DDO?A0{Pz)p^9t!^7U;`XSysR(G zg_fcCKh81Xw6G`*Gz*~&*`zY4*qtoLK2%ajUB8R8u}7g{biA&v4ipie8Aq`O2s}G&?3)J?` zV+tvf_>iYn`Fi%AIhu=TJ01Zk?Ykvs>osklels@7aSbuVC7j_*0#`+#v6?5nBWGs_^&k1EJH(%iED` zyuPsk+Ax)M;#4;GM*DTf@P$IS6%a9~8SiwJG&VsN{^M-lkMSxZ6wokMH~KSv zTV>`PEIr4eXkiog<+wdF6RNypuwoG1udHJ8czoXnDRq!;Dr)ccCecLgGI=LQnsA&C zT$gyYEU($j>QrymMY^BLn@fDPH1DR^6;j|crQ8`udfyOhT0fF%0QclGE=}<}ucdFp zHY70l4!`N0R?bb>*9>Li6Op}%k5>RWCpdSQ7{h^0UT0viI#DN7>KYs#8Tkb;Bad9At=k79TJSY23A5C39X;5hg8Tow!Rs@%)6hkERMgrGS=|8 zDW}pd0SfH79}BHPK}_c%0&O;5MyhG3rr<-{yLvqvHt_iPg8}M1JR~A9wCBA10`;b+ zi)k3r4gSl(u!kn6yL}g{t@KKCBDw{-ai$2V!o3`K$^gC?0L#oG5AX*j+5K4vy*aO2RYC7NQ-?lv0a?PXKFL_omZTld)ZLX1bL=XF|=D*_cfUQ zo}3)$T;yUazFz`*2nuAw(rv)}0y@1kyn2gk5>iD1$Zlj*4DQX85po4?#P?`@<57~~ z0P}w;!;Su1Wq2LXR5aggpGGyvgg%Bu-8liuuCfg?W6hEuClWlT7zGL}MD**%CES!= zjv}ec1a!^TfUw~PH-wC}*??1bAZ(Wej$%>5=JTvFe2~&Y9gJs3QU%Bknb!^;Yhwrp z9M3bk75}*{oWBrgi z)PiIYknIY;7=?PWUUsEQ9`*%bNHj=R0IM5`70~?hs`g0~bxk}BZHfDSF){>E`%h_UV7doG8g-UA zHGUP z%-b|1KRIuk#I4GM9^{+YbzP|gghsV0lV%7a)oVfvUP_7{`kTQdyxxX6(u4KV8;MC~ z7#9B%w%(%^fStXC{kn75a}R-xW^s}qTJYNwcub+~e6arX2mq_Ad(&r*k9c#eWFIk3 zv5$7-Nc&%dRaW&(+THB;%7~s-k|C3Tn%Zsh>NZ19ct!v&)zuCIEu{Z1`9ow;Tj2dyK3zToK@g}L$hlV@ZdlEiS$6@ArlPjvzc0d=5(~YB14*n zU4S%kx7>;KqwO&1(F}B1J<|W9WvI`pd6X)(0%w8)oR=i)EL`;~imZKHp37Nf$N1{9GANVKgV{jQ9$PRq%q_%1Z^xGLAB z{bRndYOw8{N0(vO`&Vx+Qhx@n*6t6gYp6qU4~nj2BK_Y}D>o{qEyf-liBUUX!?gB2 zt6)z-7=a`EbnDB7ES(51*MmKKy{m=_C#4W<k* zL2!Jqh4B9X3+N=?SBHf}mr)5pRJBtl?5%mMQ);(8wpcy5Jy++*k4{7)0;holnWXFp zcuXmPZ1GBp-32^)#J=AgSZV_Eq9cS;>(^3hw>lrG4X!@aM{);(DtI!~UP~G-m;NFJ z^9cx2fUSm_sIXW3XyM&_ru@9#Kaw|+F;(KAbGbN#JqcoAbq3~Lj#iwkaI%CB^V6Op zdz#1g*vw39Q-1#m;07d>+XC$gePUL#@q#v5Rr_V-#eU1pV1g467^uuz4!z&>#-%bx zR(;1V;lBqGKyXan4s1XgSz7@L^!Sl)W^B#^RL5d3^5xHRBcSwWg_xFJP7Telw*e6N zYiT|mt>oksPbKJ_7?7jiBd?4^(7tZTw*YA%fE0pedJJ%ne~LVIJ$lCLC7pOKeL#iF zhNJsy!3(F-saXyVoDlRFuVg%-$NN5NY=TJatkxJ_Z@#mCdmX488Lx)Xp5|>ZfxFbG! z;Q9uO?dnyEpf5?qC7<1epkV9I)unf~mc}7gP!SPlpcdX~U_Pt`<Prz)2e{n7h(NFRjf0b5oV4x>I`M@}YWOo$zgqq>ho&9>tp|rgY6(8hU7;g|^q#t=JXmB_01yZUyYK-jyPe z;uAJCdt27E#d0sUi5mU`7znwU2*AJxbxhuh$|&1JWf<&~|1SUoTiA?@{TQL2;w(2f zvY-tr%CMAS5QA`C!>fvJ(oyup7N!M>tZhZ}bSOig3sOgW$nY53si7$V%S8mX5jJsq z{|`YAA>_abY7F@p0u65w6sPKSgmp;RYmT0W>;oY!!&#eA-J`3gP_Yd#FM>NoP@feM z*Ud>^wesiq039noEv-`%M~7yd$k`6<8VXoMC%@|Wwo?o8deZd@rtBJe z&&gQ_n%CjDz>pAAAU8ne|28H@&fGk`;RQDJdpkW9)$=ZM)m!7?UHyB@fr@Fy>(kBP zGWJ0Hz;JK#SDPi~qhQ^xz>`JKn{Ms1Ls%=D{oU6|3Z4gRjqbm<`l0cf1SW7*>ZVn4 z@3F;W=%RZ%2($S+>&czDD%W%TMrRDex-2gY0b@A{l68GABhxr<61yrzjgwy>p>f*4 zxe9CG67Vc55^LI*j9=;6<0t|`$2_BvE>hTIjekc6_RRD%S6APrG4}QJ<~FKqXDK7B zKS-}W!3ht?4JCBinj!Z0N?$3GB?^soswpRe{D~hhOs};yZzxQ@z!`)TFl6C9MI1Tq z55H5SZm`{-tzv#?3FHWXt02K)WY2LA_sgx~wopPZ%>fW#8dl3`8`a3{6SuoDd(CTS z)dR48f02L{ z(#q`xTp!k~g9$@cNvi9q{XJoCsLOrjNE;aYitv!0KP?FgRVo}Z$55rloMwrAD70uf znD@1K^z_2wp7YxBtI80DsG)9bNm{jw6A}!UzJeazd;Z6ARr@=h?i8mxQ^Ltd#da3B zZpjCH{WWkD3Z=KkUqE2|-v9xD7Ykm$dMclKoxe?l)WBV~prHC3EU`e3s19^KM3F?aOQVb?@)_JcFk({TKMOx8IE#XQM&7dzZSn`A!G*pd1{ShSNN?b~A zi%qYrtgKf}*;Y{4v_)K2q2t;(E%@i7wnLj9Kmji+S7~CXz0b3e{&#QyD7_FwpfU!j z%HgZYUZk={zQX)xyZ!$XQb;-h9Vwci6l}$0)EA?VMMXu}3V010sp!6CU*G}IK%44c zxziGeIqyvRGhgE3GC|Zetw+lvfv=f{X1#EMn4DZg@+=Ka0XVRlw#D25W)aL4bLo~x z0Ei;GNc09Y5!VFF(Iuewh*t^ZPEfGUb6!yQ|LrWw2sI^BqW*OSaES3am||{jZwFRl zFlaL$4{h$~-~dt%()fMB^eXjje+K$3a@_;REWo~}b==~pl#PQU%LeQ%%%E6>ReB9f zpVN%_`T14fVBw$zKUM%wVTKnK@cK0Y3kQe)qe4^Az3j;csE-Nf2@MHf?$;IbvZFS^ zCw7v4LF4&DeeSlF&_z&0d|ua?%>tF*9aYr`Sv>91zD{xL!2Ww9UVl#85OgUw7Z(AN zV1h!agU`OtNEEMr`->A)aohRiZvi)(bOkSJ(D?`t3;X!Y#=wB(U%f0UswpQ$*Sezg z^Z;mQ$pq8QP`qtuXcz>UUC{WlldRWvz5Zx~7UU2v!f;-JX+Wnn%DGS2DQS!4e9s2-mZ+tHRxwX7Zqpn^mHVUzqZ$e$$-eSel#XJAK?2N^IV zX=+T$LYrVyIf-DR~)~@V;Zh0PT+fZ4FlP6EYRIQqUQ$aOIVc<{@Oca;0Tb zc!~4M6%yo$3->O=`-U(}w!;w(KRes`Q5psKc56ls^eaIrYoOejQ@h0W@$UKzYTj%~ zp1okyZHTz0*~Ec%sYH`&akL^D+MSyo;-EljdAm9v8lTNAEuer7(9Jz%<*+?naKr+A zQBYhH zit_6+huKgW^h1!1`y|2kxA=($N%;cz4O2*tpk9fHiaO22)D9qrT1|3tvf3)Rk6>ic z*Xe>`Vf2DDifW2z!pO-bCQ}m=igV|FIqAdZp0^nvJxnM9ou;MYmF@Yl4%qczO_QQt zO^R&BIXgrt*<K) z)brolvsJ1{)RHtdHonZw%?5SgVV<=y5^qK0z|~?MNPJXz3R3V<;$#g=%8F)Mi)tsD zMXSL-!7{>Svi{|jogvI3*x87{=&@`llUhwjOUsXg-3_o`3xLJ2G3L1fqE3T(hR?t8 zJYmyPaFM_NN-NI*4+<_zr!}wyPJ{ChyJp-T&4oPA5fN;Z~uzPSoyw}hsBqJzQ@ zpe*cA!GBk}NQwO4vUYJg<=6klxK{OX0zv9z2-f>%5D~awALZo%C*%C-RkuKFdC?F+ z@E1QkUb$l|P`p%*E=e2h_*Wm1zT2?t0`hcjv-W5-c)^}$KV}HD$}A+D-NZE?0J4GE2~(? zUrB^et-)soNnX20jy5o;lgY3Q0kkhyw-tH^#3UrZ5~w(3ocmwnAQhGrkXe+`95!?%Sjy`yHBK10&nur(@Y`>X`m1;-LLA(j*K{{x% zAvIcbblS0RMHbzNraQcy^+4WJ7&ic^V9@TgkHPZVhR8CbBY;5wdEmIUcH^EYPMSEFK(CAIwA+`q0=G3ICiJEyFb~ct)uYOU)?UH>F+<$4n;m4!hNJ%{54CbeK~kqkU*g1Ikb zRK|p#&u!HJc#FXKA!GrB5az`AATf7NNRf}uT!A$8m0}t`$fSuCodseS#7$Z_IlCo* z>tR3GSYE@+%L@{;2m=XhGr-Yw3W764h60#xfG~FI!=BYRrndH`zYXLE@SzLrr({qt z_%OY?kM#HVf7tvZfArSq)E)#zN^0smIy!N(v)>oCAnSpkb)$w@2%@2|^So+M949Ad z5Oik?VXvm)Ld_*j5E8R!I@~45ACNi&T#77`YUfv*nxR-rf|)&$m+FpN#f$T<3A7G`YX zM4dCgbSwm_q$z^3oz}F@pN7>>8W9a`=-weZiy>VT@Q^(``Z@6`MOv;Vm{`)rGuJ&&Ok~^3Mq*o z!(<0PEt`IcF_)CQya(v)Sm@Qt$oNS!B4-`sEAT9P#I9&TdkK;+Ztk3}-?`fQSzt2= zv#!#;iy`zvA)q6DaB$Eg*@y5ZcUC7~Gl)FgYN%eOwSIgKW?+7T8Z*pmih5nN)u&%1 zE!wC4N5-YH4+?4w!Hjq~?xDtk)egU8be*mlIxT{NeM@|>BP9>lLVa_rzl4YN!a)g@ z0@6@LDnTq1f<~|qpuB;S_}0C9fqPEU`_pwM{PsV;ojZn)4;35C+f|@LEobq;v^hB` z=^1zy3JfSpkHl*vSLwmqA$)LXfnm%>aiCUwyAbn{8zJMvss^r#L6$u!Ir)>k5Dd6R zg5q%qBzOXk+PojW+a}L&X)izj#rsT`!PQdIKGLq1^GF_;d;Z1^9W?m^ZEbmksrRnm z+_?!l;K*bjQn13la$ROX0e{LAZW&k?^Lg#qVMr=`NqCV~$Su=Xw&$=QKO2N(=A0Jc* zlXwf%CA-TNE2z??;$fZQlz656@euZF*9ym6=zc6$ZWlS?TaQ;))ossvitAPp#dwn1 zyhpI0x!PQvGQ`0YA;|-LWs57+4HQ2)XJ0=cb(OUMzo&O2dusMP`U7#=4`mu=PetSXO+jp z4HS44OT(m9I9g14ZiR=X#S|-!T+cu4A>PK&EfXQnnq8d1>=Lx`rYt4!!zpFocgnA9 z7!>j@HGSq6klZ}%9HwXdUCfrpjp=x8wzy%8!IqHCdG_gf+JFc*T$lK~vqKpJK)K-a za1a!G!h`+uWBzmC%Lkr$mtqcS2=KsL1@zmVvE0n+HMNfPGvE2`4IZUy)oE0M+iNtw zFJ(;q{r$sfDzn?ny9~B}z;v8nIu2YeAOYop;)Mg0Den(wSJ^4YG$~#%!nSsT;nQHa z$zt%<#(gY#__@=S1h*)<8EEoHVGsIFoLvB=`#b$jGY%kH3C&-487Ht~{(R5;#n}KZ zJ{r2NZjfhvf8RKSmT$88b29+KtQRogcmsc+4upTv`F!#j#kE&C4=!E!evO|yDV8hy zm<)Ia`zYVlczLXt0QLgDnv;|RgC z2vl4(Gm38)SL$Kzp5ZX&crWpyBzA7kZTst<)#25@x5`>be2 zYyC-$AD4Xp%K9(#txYU zgzwr&PK}8zk6k`LumnM)#Y5X+cyPUU0X$)04OOP5^;*tU6a@ z7SOna1(e{)L{}FfmjalSoiKxa_c^Iy<7?EmCG>!9-5dOS<;)#=Nga-m)E{_3&fS83 z_wDYnr#B6f;#pF8IwbbF#=uxB(B(NxfPE+E`-Z&&h{1ku(*j=II~!bO0pmeDea3Av z!2tF7^XJN6n&Lzpzg9I_RPK05Y~paq;4B0Ba}T~(B^|j8m8+RZF{kohbPJ4NsgtLGE%n*N-@)y~GZx}!ECH|m2zYNH-y{g@ zf)|5t!5@CQA-88uH;&wEBvrB$g zrskcwQoyhK6Lh{-Z*=xKjtR-wH~1I58k?7=AL=HD5C4@VjKCLlLeJ0mBPD2KEU5IllgN)9vnu?O9-gW1osUoLcUPFtHz}W0j2DUR6P#Lm zw1Nt$@VGi5;xlXwJDVaLe@N@y-N3FcWkk2{+#?=~0m*0b)gPLA&gl`N4itARK08m& zF28O2!eAkI{Ko8B`gRK6;0=$P0UifOhcmR^BL|$7mR@8tDF9n5)C@d#wINy+n@np8 z=Y3r|8!ukN9BzZ!`T7>Kwk^H87F*HfKF3;Avww{%E|Gp~Ol_g4l02Y!l@IWDock6mLam^c~%k9O7V2Fe$XVz4bT9IJFk zq2Ax8!R8DHkFPX>Z4ek#?DC~`UoJO=+T%^LiSo({_>_RX6-ZrzGlZKwI{t2@98i0;?N%t?j=RcP{_D z+kJKyxfo`lBNCL|_^Vib9xHi`N!?+*%H4K}#e+KB1#A9WA~a4z!IbrDrCCJr{u0Mf zUQP!WGpp5FIv7RfpTWSRX7T1ilBbun{;S0r9h|C81gE{H~feBb+fG{fx z1t%Vu4p6+9&8@AE<>emrUbH8FZGzc-{gR6@*x>N# z4-oSqF|HIh=D7y!=&9lH!|mG@Z(U%0&L|H*;G$*_MHONsD3-Z%R#ph zAI{Q>lA~X|>LoX5lg7!$`8Z6my7jxDByA6=KMfY%>FT$GuxI#%pKzr+&*w$Z;P?s2 zV2a9yLrPWKT|Cr8H0E4h5ex0^iL+JtwA-Pz0|U?CwOEv@ug(uRGwlVOe7NnIep4UW zOcBBI($Q<}36dz(vOcA!`XmI+BIbjW$op#qcTCn2$s&8J-Sg-vY3KpHZ*0b;rIkaj z%z+Lk@KQ9mN^2k(yBpyHzEWD++Qwtx40{fB6F^?XT4)yXpR>u3Yk)7j{x=s`_|S8~ ze}DgY%{Vz2eMia>f>{wtIoa={#fh$v=*UPg7Ht40$qdz3A>)o6;&0?qz$=Ns_nnl^ zmB=JWE?|oRYaj7rhxdaAPDf9F$4IoB*e8sIT?l+Q8@HE+IfjBS6GB@0@Fz@4G{V5j zYIx!S0n+nb`<0fcSR`Na8o66LWomhImq1Wag;7AkX!1bkpj_l#tR<}NeIHSVLhkrU zwQTQ2$z7=u@*|hiH|<0vb+Qw2dbb-o#_%X!b78oye1-#|x@32}jGKlr1avw^P`=Jq za&}{$l(11Kr;6_sh8*Q~+fEy~_KI>JiLBP?;}H<uYd6<*P!S>CyzSZqC;A8OHg zoq+FxPu7F1Uiee$v2|t9MGWpHSQV5RsAZ_8XJk;`@sO97M{*R1^UGFn5cqz$bO6?+ z6Kig65*w=iTe=hkUBd8oU}KpCmWvP>T`JHcXg~%b|CRw4CXno>qoadtxodd%B)Ff6 ze$72g@O1x7jBA^OhG0r!Ci%zB?QJ7qSiy%toX$NwJ@4Se;uXDXS|dlKX50?_hOErZ zDYn9cgF0rX8sWt{J3DKC8-{guZc8;5f(Px#c$0u$+ibpjCcI;X)C+}5SaQ@Ix4`Rb zdkdIk$W58OR`G_0hk@@Ua9;bPV9okBZX2KMpw6$Y)#FF+XP1uEGK*G(mv_JpiNx^) zQ{=(<`=wInoRgo%+D^CR=T>K>we4huS^d(vsjzknGNosR%^61btTp}-v^&>;*DfYM~>^FjV@{^ZHo85*;CBSAqK zC=DS^KX3!z_7DbnG_Jh#BhH@4v4=6h!&^^ddy~N8qOX>8-<$9ht zaee*S?miTz;I2#;d|W>U-fq6v6bLg!qx%McYebMUH|d z5RXo&FEB6Iioydlm-y=&FP`&UOUN-#L}#K6AV>Z#Gj}X1aAGfIqc(juYA7JOfnJWE zn$Bn@fk~WYVwha=MXUba+6M>1gltc?$z#{!U07+jcdkTn|MvLS^XjeIodKM_Wc`%V z(VL)A{c6*I|IyW%hf~$IZF~_Ta|lr+rBWn9hRB>GnX=3D;4zetJf+MsY!OXDl8_{2 zh%%2E+NmU&DVq!#G9`-gox9%WczeGNM}Mqi@3q!m!*yTR?>g`MR7z?xW?A1_!MnjY z=#KX#vqyA9*pDMd7!DsEGofF8&L<#rEZ4(7+j$8e3fXb? zla%YfTt0bvX(L$G=i!Z!#yIlr@om_XHnF zYxX#m+3XB+$Eg}t>8ZGlh25`akJ*1Kfa4NTg{IqvYm_^!#~Z}YKl5XLm2uMDeI-%B z?24=t&k*g|OXmprev9n=bC=IU)pTQ)n1&VT_rfmw_UYF%z3^7M;ZZhx+b6ZlZXz$B zDGHspx+k<79=2L(GdS;lXIdnfphW^U6c5}NoP*_VliADnwwi|9epfg8lIQYsqpsB( zL2JI)&8&QF5uV3`?rd>j?Irs9sA(2Qa}^&vx35Xq!NEEgq-|`U=ix79?e8Y3qnk6$ zdyzzv-B>RzS0i*>V$F0&K;7ud1p$>@&prO%ZqhAJ2Pf!8F2A4O^Yi`uxmv$a#sj$? zKaWiJ>WU1pa%AwJOhyLoQQC`AEaF^J)37EDAJAa5wpBrWSJcYIU2xxCn)G6}me+pN z>@4=0GUy3$2l&slwBhEN1VsP@bcEII?bDpUleie*0Du~wIhc54o_N@7Z7OwsvAM0S zelc-d%AT6>c_V7P0f$WDUjpuC!OnYkc0?MDi_3ru0X6Ks-DLN%(#3*c5te>plnKrd-$=_%8c`N zX3j*tdHROyh;Ib3)L3)QetiFB)MU=zL-~-+*Q|M^MD*FsCS=Hq^L46p&YmUIUmxjT z&IoWB*|pw(2`i)bZc1!y;ANItn`QAsT}w$rg<~Q>h+WM`*OVtdV(R9yRb9?8MYT9x zPZC8|$NPuP*Rwxl6M4FtOf729jI})?f^FsTv4el$Cuds0UI-FaaD5N9c z^}aM1$Wjy$yuzpIv z7q8`AGZ3liKIrAA;$!dk)OcdW1r)focO1;P`6CE#hgcTN%@!oL(IC#~zT!z${{s(p zTdpm6*~0ZPvR=wtZERf?A`O@NPMf)e47ZA{5*yEx47c-ypB}2^Z#X;nGSsm2N=z#* zsqaGJHE!;<*P0?Jxwa7_7TyuEqFdTX?0p%@J%QLtZ*kBo6Y9?X%u0urm7Cp4mEDWOBRNwqK@IFSF&- z$G<5sM}(;qdO1=hn@ueg6clD3+rwRTD9(FvwCxQ(nW2W;!pEr3} zI=|3o-VH{k_V}!TfI#23k5#V{E~9XxCnvq;>azalpWTT1IyyS=LQTA$7wyV zeU?mHpXC)qHKTpXp27?KQgrHCe%(eW`HMUJia&3aavj#opGFx@5Fbpp?-!u{U0jwj z7r0}HA47)ryD?4eAwTVbm?%=nyixJs%AN90`|{t@@qQepw!f+_8wwW*;2^x!?7$B; z{p9g?Es@>1lR#*TX}G{6Vc3U)Wa@nRlEAaExzvh^+Zn8=GiGA@e^W(Nm`f7AjJ}F! zkB*Ft=#jPyvN2T<6qtLx2+NaWDYxcC=r8SkU6&1$U5W#|&O;jqFWy(L5-Tx?KUBy( z2{Z5of&J-k$s=|sWde`Qi{-ksY<5dF`|>9`j1-y{mc%ifU0kX$0|3c=E|zF(4t_Gt zg=ldo#R$ubO#3dpVcwj`@)zq;zqSR*2Lt=&YY}lQ2K9z2c29n>;nn|M{$CW);4D|I z_#g(|%Xd#TccIqwx1X$SIpkJ_f^TGbNrfQmAC>t`x*0>Iatkl?;%eSsBvkH&t*KSG7jkozIB(If!dh{h}A*>>*h>2O}(7;K4;oHX=XTk!$ z-P~uoW5dx(>z`LbGmKol9iBKB7C4^_pHE^rN{IQS9Ag|S5TZT*I+q^GCfD#chOV~{ zh}m7|u$|%YM;B7TSJ=CzQ_@6T$9jx_sST6&vR|Xr#yolY)XvL`7fP3*$n(fiWfT>k zAK(q$<@Ea2DQP!-35&WKq8X!TFQsat1NzcYxG@2$pQ~iRy-t;kR`@Xb+tGJ&#e_P%_eij zpt7S?BMq*}5hSK=Q7WqEg~(ZZLLp;7Gt@!xq5KXH*_8_Gsr2u}uu{EfzNKBC;{#2( z_scDRS#xanJ=HZ+<37b7_U5V+;Z4eYCUTn~tSKqWv$Nh#O_SPNnUyAVEJEgOx!*rS zL{YhG$!+#7KQ30Q;hqqdSj3n^R;<9HWEFIw0I=LGZ(2L>l;BksF(KXQzJzVX*P0o; zN0|6hY8%xyrwHvYTcv7G(pGErjc5B|7;7-1`Pe8V?zzC*byP;K4Y{kiCG7-DB0>yH zxlgPwQOnk2bQ|iu`Q?el?3*8{>)vWGdO5d-8QI0E9?){Wdu<0Zt8PXwc`|!>&3C4X zzANU0%3I;JL8=AICoN~J>ZcFAChLc4Kf9YV7`sM_-Kw6-d1$I4pPw-QJYK;Uy2KcG za)Yqss7jlEMxvLtH(UyC-(X)$gP=k{DN$5&=JydpZ{G}_a`$NzD~-_iOQ!`4&?`EI zteqi4bje7&X0&HhzpM2{thY*%l~8sJ2>(%i$-r)Kt*(&d8rA4EbL$#+XI_=6KK2{v z*7EZJEQ-;fr{?BW)?%`Haed{pua&3kh1~B_Bmty9tuS)|$cz_LS?IgqVwGSB3~KcY zT+3*8K%;~tDLl^lJPYjNTs}ktdy1K)Y-7hxO~{i0%ZrL$BU~jVB_87)GSIlL1X|{q zSaT{8n&#;5dm5AsxoQuBE93YH7qm1!jnsY=?`;|yZOzI%KQ~(NbU%tH{-o}=qPOGrNGfQul{Zm|6JJ7ai3Vi z9vU(xSFP*3?4X^q&H!Y+XFcuLK8qrq<}NakZ+7Eikl49V=&JJL4GFuQQmr=Nx3;}W z-7R)HUS`Ev-%b4OmDh=ODf%EnyoiY*`%r;Xt(S(UM4)o;m0sn?GMYEK!f2OJYm`Q| zBbD<#!d>ay7MxbJFGr}05-jx9j3*gRMPAXPXI7`DLrn5?3>e$pz{!l}@hb%dUH zyo`iXYWXi)j;s;a7mU9H{G z?PGP=7n}f_dwY}K0h!8fY(VSbzWxeaa5+aP9WeTBk(AVg95V0@nI>m+*d@rYPaPVH zu&^-1VS+$PyV`Xlodgp!hy zRt4#iIewec=lR7vXj_QDPS-Hw%#Xts%51Uvp{VG30+xUDBm;C++wQzmT%6GD^kdbW z&1N2Vy8&dBot-RTHN^d_ES@CyVf7 z9y_qSc=5vcxCCEGX=!tNdpJ1h^=`~ZL*`3EVYT^M25h`kbYbG}{R#MH^^&S>$>g`& zV)dZ~<0Pg!>|?4wc|qv-%vF8`0ot?Ni%K>1F2ZnFwQV3A-kvrqJoE_{?aNamUFcZ7~vN3?O1x})<*R6T-NRBll7jG+y zdQckRV+_alv$N}gIJFM&;BU2L2B=DS5oD*9aS_qj0{012Y+a0fIV&ASKq z=;0fS^n{h4snGTFbJ}}(|>`8vR5t7FL zFeS9Mi9RtUtT~_T_qFqGla(RESdJ)jYQ^IJA|=&y|F2)a)*OGL@yI+aC1v0BWo0?J z5Kwae=)sE;1L1fPk)6($T4vu}`m2fIz&_^r*g-i3IXSW;uZ4()P0_#Dh$SYte$+=r znZlLTYhiyC?cO}FDlUc~HHq=}ri&iRcZ3lJ$S(DeOgDFRT|}z{KKK5&4m#R7S;rDOh-@Jcbb(Rfx;c6&K|C`s2l`SzERY7`u%}m4qgZ!Vmh>`qX>1L zsk!;g0CEgP&yUPVun!v>*M$DWJ!x=7pPC;mg&+X1p@FsqVO=VNDJEVFyq{PM88+`t z83*e*hblW=4Y#fA9Q4F%uAbIxOS!r!wdu;Y7hOU~t!2ernGhj;r|bC9`#~y-h=dzw zKk6fOCPNH%;z2!Am!wy zh6Z1i;6k*Pt6n)t318BA#+a>>79`5NH0kien;Oc0)lj;#GC+;6h1Lxx0(fQlWtR=f zM?ZZvn$rklXy+6yelDeBFmXlTIQ4a$L{zEH_sHI>nCyBYnVqC1zP9DfQ#xB{3|J=W zgvOkk_CN3|vcfP$Q$a*UM}x3cO`O3}N#aBQAT6Amn^Os{OHEfdS{R)>4;9mlydYvU zUU%ypg}<&VsI28(YsTbu@pxhPrJx`>|0M{d#8O=36PsIFFgV!Ct+ii8huj`FH#=ME z=ijeF)Of~sC9okYgGz$;;5pV{0vJ71gKi%em`aB4e-$cw@6r3@k>}2Z3~`1Juf}@< z15^{K-9ArK%WuipkhCv*AZBX_y3vXNOwjtT|S(6dt;$qG5VKMu6LE({*(G}j! zNE;f-E*y~^5J~Z=PaW_adYO=V;-lxDf}=a|cwrAnMYK$+sI1Q{9CR~H!I7vi6H0Zy z)i4n8ZPU6-Z+s1}QN)!RQeD~7jU%AV)Zk)(bO0(EZRQPejf`=Pp$IA1BK4JVA6an1 zdTq69xd-v{Bk{ThWp?x5#&lkR= z$!drx&%aWhJM3B?v=^=rwdXd!NjKgKxYpd!@ti-mZZ*fl)!GFwcoU-j5V1@^#FE)` zwcN<;$dRS6@9!-+53vDnb8U?`{Bl98NG_1m>j3bKlx zQPOnwq52&{xmB-pkJ1&X(6+tzE_PC0UjE06iR!ZIte*0zeg??2=yTc|V`R|09UU9{ zMxP7KV#R8!tk%@5tjP9ZTwE0~$yj|_^BAo*j7n%(vaz$LWM;Y;aKFc6M{w=Xv1@rz z^@{Oi3fjuQjVC{I@~prc3O&M^(j<4W6yr?zi9i@a^(+>9z@1B$iX1fz|6O<%>7nr4 z;z%nz7g|*XeixWQcrj6_c25|VupJdstG@v2-Dc+yL@pbfE13k|KEu-U3c1reJ<6b^zezbdhd^}1| z8v``__zJe1yz_@%WzWBQm5sM(ZL9Xwd)-L#G=aL|Vgj`hb=up=+`L{P+E%&7%-mdO zyuaQPN|oa1PaP>Iw(&^6P`(kD=q1y+B*>ZYCY(b5>rL5O#VTDE*lz7J@w8*+o3BdA z)@u~+pIIL!o8sBmkiYQcJnYeW*p}@2Gb-F*M!|&-Kt!qa9AMSmZtLvj=%al^3X#GORZihV$eA1g|B~N8vLbN z+Tj0lHrd|RmO#5$nl=q}|C1xCIGjW1@1qW2Y8=|2HPBLW_M96|5)c%G(uK?-3USq_ xlgOCjJ_BqxXDdDqS5l~3r!&(=vtZzu3?zqX-PwuWu+{{aTRu2}#8 literal 0 HcmV?d00001 diff --git a/flashdb/port/fal/docs/figures/fal-port.png b/flashdb/port/fal/docs/figures/fal-port.png new file mode 100644 index 0000000000000000000000000000000000000000..35c1557a7afe5c517c59d31b8cf7247740a6b612 GIT binary patch literal 24465 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 +#include +#include "fal_def.h" + +/** + * FAL (Flash Abstraction Layer) initialization. + * It will initialize all flash device and all flash partition. + * + * @return >= 0: partitions total number + */ +int fal_init(void); + +/* =============== flash device operator API =============== */ +/** + * find flash device by name + * + * @param name flash device name + * + * @return != NULL: flash device + * NULL: not found + */ +const struct fal_flash_dev *fal_flash_device_find(const char *name); + +/* =============== partition operator API =============== */ +/** + * find the partition by name + * + * @param name partition name + * + * @return != NULL: partition + * NULL: not found + */ +const struct fal_partition *fal_partition_find(const char *name); + +/** + * get the partition table + * + * @param len return the partition table length + * + * @return partition table + */ +const struct fal_partition *fal_get_partition_table(size_t *len); + +/** + * set partition table temporarily + * This setting will modify the partition table temporarily, the setting will be lost after restart. + * + * @param table partition table + * @param len partition table length + */ +void fal_set_partition_table_temp(struct fal_partition *table, size_t len); + +/** + * read data from partition + * + * @param part partition + * @param addr relative address for partition + * @param buf read buffer + * @param size read size + * + * @return >= 0: successful read data size + * -1: error + */ +int fal_partition_read(const struct fal_partition *part, uint32_t addr, uint8_t *buf, size_t size); + +/** + * write data to partition + * + * @param part partition + * @param addr relative address for partition + * @param buf write buffer + * @param size write size + * + * @return >= 0: successful write data size + * -1: error + */ +int fal_partition_write(const struct fal_partition *part, uint32_t addr, const uint8_t *buf, size_t size); + +/** + * erase partition data + * + * @param part partition + * @param addr relative address for partition + * @param size erase size + * + * @return >= 0: successful erased data size + * -1: error + */ +int fal_partition_erase(const struct fal_partition *part, uint32_t addr, size_t size); + +/** + * erase partition all data + * + * @param part partition + * + * @return >= 0: successful erased data size + * -1: error + */ +int fal_partition_erase_all(const struct fal_partition *part); + +/** + * print the partition table + */ +void fal_show_part_table(void); + +/* =============== API provided to RT-Thread =============== */ +/** + * create RT-Thread block device by specified partition + * + * @param parition_name partition name + * + * @return != NULL: created block device + * NULL: created failed + */ +struct rt_device *fal_blk_device_create(const char *parition_name); + +#if defined(RT_USING_MTD_NOR) +/** + * create RT-Thread MTD NOR device by specified partition + * + * @param parition_name partition name + * + * @return != NULL: created MTD NOR device + * NULL: created failed + */ +struct rt_device *fal_mtd_nor_device_create(const char *parition_name); +#endif /* defined(RT_USING_MTD_NOR) */ + +/** + * create RT-Thread char device by specified partition + * + * @param parition_name partition name + * + * @return != NULL: created char device + * NULL: created failed + */ +struct rt_device *fal_char_device_create(const char *parition_name); + +#endif /* _FAL_H_ */ diff --git a/flashdb/port/fal/inc/fal_def.h b/flashdb/port/fal/inc/fal_def.h new file mode 100644 index 0000000..bca071b --- /dev/null +++ b/flashdb/port/fal/inc/fal_def.h @@ -0,0 +1,156 @@ +/* + * File : fal_def.h + * This file is part of FAL (Flash Abstraction Layer) package + * COPYRIGHT (C) 2006 - 2019, RT-Thread Development Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Change Logs: + * Date Author Notes + * 2018-05-17 armink the first version + */ + +#ifndef _FAL_DEF_H_ +#define _FAL_DEF_H_ + +#include +#include + +#define FAL_SW_VERSION "0.5.0" + +#ifndef FAL_MALLOC +#define FAL_MALLOC malloc +#endif + +#ifndef FAL_CALLOC +#define FAL_CALLOC calloc +#endif + +#ifndef FAL_REALLOC +#define FAL_REALLOC realloc +#endif + +#ifndef FAL_FREE +#define FAL_FREE free +#endif + +#ifndef FAL_DEBUG +#define FAL_DEBUG 0 +#endif + +#ifndef FAL_PRINTF +#ifdef RT_VER_NUM +/* for RT-Thread platform */ +extern void rt_kprintf(const char *fmt, ...); +#define FAL_PRINTF rt_kprintf +#else +#define FAL_PRINTF printf +#endif /* RT_VER_NUM */ +#endif /* FAL_PRINTF */ + +#if FAL_DEBUG +#ifdef assert +#undef assert +#endif +#define assert(EXPR) \ +if (!(EXPR)) \ +{ \ + FAL_PRINTF("(%s) has assert failed at %s.\n", #EXPR, __FUNCTION__); \ + while (1); \ +} + +/* debug level log */ +#ifdef log_d +#undef log_d +#endif +#define log_d(...) FAL_PRINTF("[D/FAL] (%s:%d) ", __FUNCTION__, __LINE__); FAL_PRINTF(__VA_ARGS__);FAL_PRINTF("\n") + +#else + +#ifdef assert +#undef assert +#endif +#define assert(EXPR) ((void)0); + +/* debug level log */ +#ifdef log_d +#undef log_d +#endif +#define log_d(...) +#endif /* FAL_DEBUG */ + +/* error level log */ +#ifdef log_e +#undef log_e +#endif +#define log_e(...) FAL_PRINTF("\033[31;22m[E/FAL] (%s:%d) ", __FUNCTION__, __LINE__);FAL_PRINTF(__VA_ARGS__);FAL_PRINTF("\033[0m\n") + +/* info level log */ +#ifdef log_i +#undef log_i +#endif +#define log_i(...) FAL_PRINTF("\033[32;22m[I/FAL] "); FAL_PRINTF(__VA_ARGS__);FAL_PRINTF("\033[0m\n") + +/* FAL flash and partition device name max length */ +#ifndef FAL_DEV_NAME_MAX +#define FAL_DEV_NAME_MAX 24 +#endif + +struct fal_flash_dev +{ + char name[FAL_DEV_NAME_MAX]; + + /* flash device start address and len */ + uint32_t addr; + size_t len; + /* the block size in the flash for erase minimum granularity */ + size_t blk_size; + + struct + { + int (*init)(void); + int (*read)(long offset, uint8_t *buf, size_t size); + int (*write)(long offset, const uint8_t *buf, size_t size); + int (*erase)(long offset, size_t size); + } ops; + + /* write minimum granularity, unit: bit. + 1(nor flash)/ 8(stm32f4)/ 32(stm32f1)/ 64(stm32l4) + 0 will not take effect. */ + size_t write_gran; +}; +typedef struct fal_flash_dev *fal_flash_dev_t; + +/** + * FAL partition + */ +struct fal_partition +{ + uint32_t magic_word; + + /* partition name */ + char name[FAL_DEV_NAME_MAX]; + /* flash device name for partition */ + char flash_name[FAL_DEV_NAME_MAX]; + + /* partition offset address on flash device */ + long offset; + size_t len; + + uint32_t reserved; +}; +typedef struct fal_partition *fal_partition_t; + +#endif /* _FAL_DEF_H_ */ diff --git a/flashdb/port/fal/samples/README.md b/flashdb/port/fal/samples/README.md new file mode 100644 index 0000000..fcbb58a --- /dev/null +++ b/flashdb/port/fal/samples/README.md @@ -0,0 +1,4 @@ +| 文件夹 | 说明 | +| :------ | :----------------------- | +| porting | 移植相关的示例代码及文档 | + diff --git a/flashdb/port/fal/samples/porting/README.md b/flashdb/port/fal/samples/porting/README.md new file mode 100644 index 0000000..36c04e1 --- /dev/null +++ b/flashdb/port/fal/samples/porting/README.md @@ -0,0 +1,108 @@ +# Flash 设备及分区移植示例 + +本示例主要演示 Flash 设备及分区相关的移植。 + +## 1、Flash 设备 + +在定义 Flash 设备表前,需要先定义 Flash 设备,参考 [`fal_flash_sfud_port.c`](fal_flash_sfud_port.c) (基于 [SFUD](https://github.com/armink/SFUD) 万能 SPI Flash 驱动的 Flash 设备)与 [`fal_flash_stm32f2_port.c`](fal_flash_stm32f2_port.c) (STM32F2 片内 Flash)这两个文件。这里简介下 `fal_flash_stm32f2_port.c` 里的代码实现。 + +### 1.1 定义 Flash 设备 + +针对 Flash 的不同操作,这里定义了如下几个操作函数: + +- `static int init(void)`:**可选** 的初始化操作 + +- `static int read(long offset, uint8_t *buf, size_t size)`:读取操作 + +|参数 |描述| +|:----- |:----| +|offset |读取数据的 Flash 偏移地址| +|buf |存放待读取数据的缓冲区| +|size |待读取数据的大小| +|return |返回实际读取的数据大小| + +- `static int write(long offset, const uint8_t *buf, size_t size)` :写入操作 + +| 参数 | 描述 | +| :----- | :------------------------ | +| offset | 写入数据的 Flash 偏移地址 | +| buf | 存放待写入数据的缓冲区 | +| size | 待写入数据的大小 | +| return | 返回实际写入的数据大小 | + +- `static int erase(long offset, size_t size)` :擦除操作 + +| 参数 | 描述 | +| :----- | :------------------------ | +| offset | 擦除区域的 Flash 偏移地址 | +| size | 擦除区域的大小 | +| return | 返回实际擦除的区域大小 | + +用户需要根据自己的 Flash 情况分别实现这些操作函数。在文件最底部定义了具体的 Flash 设备对象(stm32f2_onchip_flash): + +`const struct fal_flash_dev stm32f2_onchip_flash = { "stm32_onchip", 0x08000000, 1024*1024, 128*1024, {init, read, write, erase} };` + +- `"stm32_onchip"` : Flash 设备的名字 +- 0x08000000: 对 Flash 操作的起始地址 +- 1024*1024:Flash 的总大小(1MB) +- 128*1024:Flash 块/扇区大小(因为 STM32F2 各块大小不均匀,所以擦除粒度为最大块的大小:128K) +- {init, read, write, erase} }:Flash 的操作函数。 如果没有 init 初始化过程,第一个操作函数位置可以置空。 + +### 1.2 定义 Flash 设备表 + +Flash 设备表定义在 `fal_cfg.h` 头文件中,定义分区表前需 **新建 `fal_cfg.h` 文件** 。 + +参考 [示例文件 samples/porting/fal_cfg.h](samples/porting/fal_cfg.h) 或如下代码: + +```c +/* ===================== Flash device Configuration ========================= */ +extern const struct fal_flash_dev stm32f2_onchip_flash; +extern struct fal_flash_dev nor_flash0; + +/* flash device table */ +#define FAL_FLASH_DEV_TABLE \ +{ \ + &stm32f2_onchip_flash, \ + &nor_flash0, \ +} +``` + +Flash 设备表中,有两个 Flash 对象,一个为 STM32F2 的片内 Flash ,一个为片外的 Nor Flash。 + +## 2、Flash 分区 + +Flash 分区基于 Flash 设备,每个 Flash 设备又可以有 N 个分区,这些分区的集合就是分区表。在配置分区表前,务必保证已定义好 Flash 设备及设备表。 + +分区表也定义在 `fal_cfg.h` 头文件中。参考 [示例文件 samples/porting/fal_cfg.h](samples/porting/fal_cfg.h) 或如下代码: + +```C +#define NOR_FLASH_DEV_NAME "norflash0" +/* ====================== Partition Configuration ========================== */ +#ifdef FAL_PART_HAS_TABLE_CFG +/* partition table */ +#define FAL_PART_TABLE \ +{ \ + {FAL_PART_MAGIC_WORD, "bl", "stm32_onchip", 0, 64*1024, 0}, \ + {FAL_PART_MAGIC_WORD, "app", "stm32_onchip", 64*1024, 704*1024, 0}, \ + {FAL_PART_MAGIC_WORD, "easyflash", NOR_FLASH_DEV_NAME, 0, 1024*1024, 0}, \ + {FAL_PART_MAGIC_WORD, "download", NOR_FLASH_DEV_NAME, 1024*1024, 1024*1024, 0}, \ +} +#endif /* FAL_PART_HAS_TABLE_CFG */ +``` + +上面这个分区表详细描述信息如下: + +| 分区名 | Flash 设备名 | 偏移地址 | 大小 | 说明 | +| :---------- | :------------- | :-------- | :---- | :----------------- | +| "bl" | "stm32_onchip" | 0 | 64KB | 引导程序 | +| "app" | "stm32_onchip" | 64*1024 | 704KB | 应用程序 | +| "easyflash" | "norflash0" | 0 | 1MB | EasyFlash 参数存储 | +| "download" | "norflash0" | 1024*1024 | 1MB | OTA 下载区 | + +用户需要修改的分区参数包括:分区名称、关联的 Flash 设备名、偏移地址(相对 Flash 设备内部)、大小,需要注意以下几点: + +- 分区名保证 **不能重复** +- 关联的 Flash 设备 **务必已经在 Flash 设备表中定义好** ,并且 **名称一致** ,否则会出现无法找到 Flash 设备的错误 +- 分区的起始地址和大小 **不能超过 Flash 设备的地址范围** ,否则会导致包初始化错误 + +> 注意:每个分区定义时,除了填写上面介绍的参数属性外,需在前面增加 `FAL_PART_MAGIC_WORD` 属性,末尾增加 `0` (目前用于保留功能) diff --git a/flashdb/port/fal/samples/porting/fal_cfg.h b/flashdb/port/fal/samples/porting/fal_cfg.h new file mode 100644 index 0000000..af043fc --- /dev/null +++ b/flashdb/port/fal/samples/porting/fal_cfg.h @@ -0,0 +1,55 @@ +/* + * File : fal_cfg.h + * This file is part of FAL (Flash Abstraction Layer) package + * COPYRIGHT (C) 2006 - 2018, RT-Thread Development Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Change Logs: + * Date Author Notes + * 2018-05-17 armink the first version + */ + +#ifndef _FAL_CFG_H_ +#define _FAL_CFG_H_ + +#include +#include + +#define NOR_FLASH_DEV_NAME "norflash0" + +/* ===================== Flash device Configuration ========================= */ +extern const struct fal_flash_dev stm32f2_onchip_flash; +extern struct fal_flash_dev nor_flash0; + +/* flash device table */ +#define FAL_FLASH_DEV_TABLE \ +{ \ + &stm32f2_onchip_flash, \ + &nor_flash0, \ +} +/* ====================== Partition Configuration ========================== */ +#ifdef FAL_PART_HAS_TABLE_CFG +/* partition table */ +#define FAL_PART_TABLE \ +{ \ + {FAL_PART_MAGIC_WORD, "bl", "stm32_onchip", 0, 64*1024, 0}, \ + {FAL_PART_MAGIC_WORD, "app", "stm32_onchip", 64*1024, 704*1024, 0}, \ + {FAL_PART_MAGIC_WORD, "easyflash", NOR_FLASH_DEV_NAME, 0, 1024*1024, 0}, \ + {FAL_PART_MAGIC_WORD, "download", NOR_FLASH_DEV_NAME, 1024*1024, 1024*1024, 0}, \ +} +#endif /* FAL_PART_HAS_TABLE_CFG */ + +#endif /* _FAL_CFG_H_ */ diff --git a/flashdb/port/fal/samples/porting/fal_flash_sfud_port.c b/flashdb/port/fal/samples/porting/fal_flash_sfud_port.c new file mode 100644 index 0000000..a3f30c2 --- /dev/null +++ b/flashdb/port/fal/samples/porting/fal_flash_sfud_port.c @@ -0,0 +1,110 @@ +/* + * File : fal_flash_sfud_port.c + * This file is part of FAL (Flash Abstraction Layer) package + * COPYRIGHT (C) 2006 - 2018, RT-Thread Development Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Change Logs: + * Date Author Notes + * 2018-01-26 armink the first version + */ + +#include +#include + +#ifdef FAL_USING_SFUD_PORT +#ifdef RT_USING_SFUD +#include +#endif + +#ifndef FAL_USING_NOR_FLASH_DEV_NAME +#define FAL_USING_NOR_FLASH_DEV_NAME "norflash0" +#endif + +static int init(void); +static int read(long offset, uint8_t *buf, size_t size); +static int write(long offset, const uint8_t *buf, size_t size); +static int erase(long offset, size_t size); + +static sfud_flash_t sfud_dev = NULL; +struct fal_flash_dev nor_flash0 = +{ + .name = FAL_USING_NOR_FLASH_DEV_NAME, + .addr = 0, + .len = 8 * 1024 * 1024, + .blk_size = 4096, + .ops = {init, read, write, erase}, + .write_gran = 1 +}; + +static int init(void) +{ + +#ifdef RT_USING_SFUD + /* RT-Thread RTOS platform */ + sfud_dev = rt_sfud_flash_find_by_dev_name(FAL_USING_NOR_FLASH_DEV_NAME); +#else + /* bare metal platform */ + extern sfud_flash sfud_norflash0; + sfud_dev = &sfud_norflash0; +#endif + + if (NULL == sfud_dev) + { + return -1; + } + + /* update the flash chip information */ + nor_flash0.blk_size = sfud_dev->chip.erase_gran; + nor_flash0.len = sfud_dev->chip.capacity; + + return 0; +} + +static int read(long offset, uint8_t *buf, size_t size) +{ + assert(sfud_dev); + assert(sfud_dev->init_ok); + sfud_read(sfud_dev, nor_flash0.addr + offset, size, buf); + + return size; +} + +static int write(long offset, const uint8_t *buf, size_t size) +{ + assert(sfud_dev); + assert(sfud_dev->init_ok); + if (sfud_write(sfud_dev, nor_flash0.addr + offset, size, buf) != SFUD_SUCCESS) + { + return -1; + } + + return size; +} + +static int erase(long offset, size_t size) +{ + assert(sfud_dev); + assert(sfud_dev->init_ok); + if (sfud_erase(sfud_dev, nor_flash0.addr + offset, size) != SFUD_SUCCESS) + { + return -1; + } + + return size; +} +#endif /* FAL_USING_SFUD_PORT */ + diff --git a/flashdb/port/fal/samples/porting/fal_flash_stm32f2_port.c b/flashdb/port/fal/samples/porting/fal_flash_stm32f2_port.c new file mode 100644 index 0000000..0fd246d --- /dev/null +++ b/flashdb/port/fal/samples/porting/fal_flash_stm32f2_port.c @@ -0,0 +1,212 @@ +/* + * File : fal_flash_stm32f2_port.c + * This file is part of FAL (Flash Abstraction Layer) package + * COPYRIGHT (C) 2006 - 2018, RT-Thread Development Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Change Logs: + * Date Author Notes + * 2018-01-26 armink the first version + */ + +#include + +#include + +/* base address of the flash sectors */ +#define ADDR_FLASH_SECTOR_0 ((uint32_t)0x08000000) /* Base address of Sector 0, 16 K bytes */ +#define ADDR_FLASH_SECTOR_1 ((uint32_t)0x08004000) /* Base address of Sector 1, 16 K bytes */ +#define ADDR_FLASH_SECTOR_2 ((uint32_t)0x08008000) /* Base address of Sector 2, 16 K bytes */ +#define ADDR_FLASH_SECTOR_3 ((uint32_t)0x0800C000) /* Base address of Sector 3, 16 K bytes */ +#define ADDR_FLASH_SECTOR_4 ((uint32_t)0x08010000) /* Base address of Sector 4, 64 K bytes */ +#define ADDR_FLASH_SECTOR_5 ((uint32_t)0x08020000) /* Base address of Sector 5, 128 K bytes */ +#define ADDR_FLASH_SECTOR_6 ((uint32_t)0x08040000) /* Base address of Sector 6, 128 K bytes */ +#define ADDR_FLASH_SECTOR_7 ((uint32_t)0x08060000) /* Base address of Sector 7, 128 K bytes */ +#define ADDR_FLASH_SECTOR_8 ((uint32_t)0x08080000) /* Base address of Sector 8, 128 K bytes */ +#define ADDR_FLASH_SECTOR_9 ((uint32_t)0x080A0000) /* Base address of Sector 9, 128 K bytes */ +#define ADDR_FLASH_SECTOR_10 ((uint32_t)0x080C0000) /* Base address of Sector 10, 128 K bytes */ +#define ADDR_FLASH_SECTOR_11 ((uint32_t)0x080E0000) /* Base address of Sector 11, 128 K bytes */ + +/** + * Get the sector of a given address + * + * @param address flash address + * + * @return The sector of a given address + */ +static uint32_t stm32_get_sector(uint32_t address) +{ + uint32_t sector = 0; + + if ((address < ADDR_FLASH_SECTOR_1) && (address >= ADDR_FLASH_SECTOR_0)) + { + sector = FLASH_Sector_0; + } + else if ((address < ADDR_FLASH_SECTOR_2) && (address >= ADDR_FLASH_SECTOR_1)) + { + sector = FLASH_Sector_1; + } + else if ((address < ADDR_FLASH_SECTOR_3) && (address >= ADDR_FLASH_SECTOR_2)) + { + sector = FLASH_Sector_2; + } + else if ((address < ADDR_FLASH_SECTOR_4) && (address >= ADDR_FLASH_SECTOR_3)) + { + sector = FLASH_Sector_3; + } + else if ((address < ADDR_FLASH_SECTOR_5) && (address >= ADDR_FLASH_SECTOR_4)) + { + sector = FLASH_Sector_4; + } + else if ((address < ADDR_FLASH_SECTOR_6) && (address >= ADDR_FLASH_SECTOR_5)) + { + sector = FLASH_Sector_5; + } + else if ((address < ADDR_FLASH_SECTOR_7) && (address >= ADDR_FLASH_SECTOR_6)) + { + sector = FLASH_Sector_6; + } + else if ((address < ADDR_FLASH_SECTOR_8) && (address >= ADDR_FLASH_SECTOR_7)) + { + sector = FLASH_Sector_7; + } + else if ((address < ADDR_FLASH_SECTOR_9) && (address >= ADDR_FLASH_SECTOR_8)) + { + sector = FLASH_Sector_8; + } + else if ((address < ADDR_FLASH_SECTOR_10) && (address >= ADDR_FLASH_SECTOR_9)) + { + sector = FLASH_Sector_9; + } + else if ((address < ADDR_FLASH_SECTOR_11) && (address >= ADDR_FLASH_SECTOR_10)) + { + sector = FLASH_Sector_10; + } + else + { + sector = FLASH_Sector_11; + } + + return sector; +} + +/** + * Get the sector size + * + * @param sector sector + * + * @return sector size + */ +static uint32_t stm32_get_sector_size(uint32_t sector) { + assert(IS_FLASH_SECTOR(sector)); + + switch (sector) { + case FLASH_Sector_0: return 16 * 1024; + case FLASH_Sector_1: return 16 * 1024; + case FLASH_Sector_2: return 16 * 1024; + case FLASH_Sector_3: return 16 * 1024; + case FLASH_Sector_4: return 64 * 1024; + case FLASH_Sector_5: return 128 * 1024; + case FLASH_Sector_6: return 128 * 1024; + case FLASH_Sector_7: return 128 * 1024; + case FLASH_Sector_8: return 128 * 1024; + case FLASH_Sector_9: return 128 * 1024; + case FLASH_Sector_10: return 128 * 1024; + case FLASH_Sector_11: return 128 * 1024; + default : return 128 * 1024; + } +} +static int init(void) +{ + /* do nothing now */ +} + +static int read(long offset, uint8_t *buf, size_t size) +{ + size_t i; + uint32_t addr = stm32f2_onchip_flash.addr + offset; + for (i = 0; i < size; i++, addr++, buf++) + { + *buf = *(uint8_t *) addr; + } + + return size; +} + +static int write(long offset, const uint8_t *buf, size_t size) +{ + size_t i; + uint32_t read_data; + uint32_t addr = stm32f2_onchip_flash.addr + offset; + + FLASH_Unlock(); + FLASH_ClearFlag( + FLASH_FLAG_EOP | FLASH_FLAG_OPERR | FLASH_FLAG_WRPERR | FLASH_FLAG_PGAERR | FLASH_FLAG_PGPERR + | FLASH_FLAG_PGSERR); + for (i = 0; i < size; i++, buf++, addr++) + { + /* write data */ + FLASH_ProgramByte(addr, *buf); + read_data = *(uint8_t *) addr; + /* check data */ + if (read_data != *buf) + { + return -1; + } + } + FLASH_Lock(); + + return size; +} + +static int erase(long offset, size_t size) +{ + FLASH_Status flash_status; + size_t erased_size = 0; + uint32_t cur_erase_sector; + uint32_t addr = stm32f2_onchip_flash.addr + offset; + + /* start erase */ + FLASH_Unlock(); + FLASH_ClearFlag( + FLASH_FLAG_EOP | FLASH_FLAG_OPERR | FLASH_FLAG_WRPERR | FLASH_FLAG_PGAERR | FLASH_FLAG_PGPERR + | FLASH_FLAG_PGSERR); + /* it will stop when erased size is greater than setting size */ + while (erased_size < size) + { + cur_erase_sector = stm32_get_sector(addr + erased_size); + flash_status = FLASH_EraseSector(cur_erase_sector, VoltageRange_3); + if (flash_status != FLASH_COMPLETE) + { + return -1; + } + erased_size += stm32_get_sector_size(cur_erase_sector); + } + FLASH_Lock(); + + return size; +} + +const struct fal_flash_dev stm32f2_onchip_flash = +{ + .name = "stm32_onchip", + .addr = 0x08000000, + .len = 1024*1024, + .blk_size = 128*1024, + .ops = {init, read, write, erase}, + .write_gran = 8 +}; + diff --git a/flashdb/port/fal/src/fal.c b/flashdb/port/fal/src/fal.c new file mode 100644 index 0000000..3351689 --- /dev/null +++ b/flashdb/port/fal/src/fal.c @@ -0,0 +1,76 @@ +/* + * File : fal.c + * This file is part of FAL (Flash Abstraction Layer) package + * COPYRIGHT (C) 2006 - 2018, RT-Thread Development Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Change Logs: + * Date Author Notes + * 2018-05-17 armink the first version + */ + +#include + +static uint8_t init_ok = 0; + +/** + * FAL (Flash Abstraction Layer) initialization. + * It will initialize all flash device and all flash partition. + * + * @return >= 0: partitions total number + */ +int fal_init(void) +{ + extern int fal_flash_init(void); + extern int fal_partition_init(void); + + int result; + + /* initialize all flash device on FAL flash table */ + result = fal_flash_init(); + + if (result < 0) { + goto __exit; + } + + /* initialize all flash partition on FAL partition table */ + result = fal_partition_init(); + +__exit: + + if ((result > 0) && (!init_ok)) + { + init_ok = 1; + log_i("RT-Thread Flash Abstraction Layer (V%s) initialize success.", FAL_SW_VERSION); + } + else if(result <= 0) + { + init_ok = 0; + log_e("RT-Thread Flash Abstraction Layer (V%s) initialize failed.", FAL_SW_VERSION); + } + + return result; +} + +/** + * Check if the FAL is initialized successfully + * + * @return 0: not init or init failed; 1: init success + */ +int fal_init_check(void) +{ + return init_ok; +} diff --git a/flashdb/port/fal/src/fal_flash.c b/flashdb/port/fal/src/fal_flash.c new file mode 100644 index 0000000..8bb837f --- /dev/null +++ b/flashdb/port/fal/src/fal_flash.c @@ -0,0 +1,93 @@ +/* + * File : fal_flash.c + * This file is part of FAL (Flash Abstraction Layer) package + * COPYRIGHT (C) 2006 - 2018, RT-Thread Development Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Change Logs: + * Date Author Notes + * 2018-05-17 armink the first version + */ + +#include +#include + +/* flash device table, must defined by user */ +#if !defined(FAL_FLASH_DEV_TABLE) +#error "You must defined flash device table (FAL_FLASH_DEV_TABLE) on 'fal_cfg.h'" +#endif + +static const struct fal_flash_dev * const device_table[] = FAL_FLASH_DEV_TABLE; +static const size_t device_table_len = sizeof(device_table) / sizeof(device_table[0]); +static uint8_t init_ok = 0; + +/** + * Initialize all flash device on FAL flash table + * + * @return result + */ +int fal_flash_init(void) +{ + size_t i; + + if (init_ok) + { + return 0; + } + + for (i = 0; i < device_table_len; i++) + { + assert(device_table[i]->ops.read); + assert(device_table[i]->ops.write); + assert(device_table[i]->ops.erase); + /* init flash device on flash table */ + if (device_table[i]->ops.init) + { + device_table[i]->ops.init(); + } + log_d("Flash device | %*.*s | addr: 0x%08lx | len: 0x%08x | blk_size: 0x%08x |initialized finish.", + FAL_DEV_NAME_MAX, FAL_DEV_NAME_MAX, device_table[i]->name, device_table[i]->addr, device_table[i]->len, + device_table[i]->blk_size); + } + + init_ok = 1; + return 0; +} + +/** + * find flash device by name + * + * @param name flash device name + * + * @return != NULL: flash device + * NULL: not found + */ +const struct fal_flash_dev *fal_flash_device_find(const char *name) +{ + assert(init_ok); + assert(name); + + size_t i; + + for (i = 0; i < device_table_len; i++) + { + if (!strncmp(name, device_table[i]->name, FAL_DEV_NAME_MAX)) { + return device_table[i]; + } + } + + return NULL; +} diff --git a/flashdb/port/fal/src/fal_partition.c b/flashdb/port/fal/src/fal_partition.c new file mode 100644 index 0000000..cf42912 --- /dev/null +++ b/flashdb/port/fal/src/fal_partition.c @@ -0,0 +1,493 @@ +/* + * File : fal_partition.c + * This file is part of FAL (Flash Abstraction Layer) package + * COPYRIGHT (C) 2006 - 2018, RT-Thread Development Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Change Logs: + * Date Author Notes + * 2018-05-17 armink the first version + */ + +#include +#include +#include + +/* partition magic word */ +#define FAL_PART_MAGIC_WORD 0x45503130 +#define FAL_PART_MAGIC_WORD_H 0x4550L +#define FAL_PART_MAGIC_WORD_L 0x3130L +#define FAL_PART_MAGIC_WROD 0x45503130 + +/** + * FAL partition table config has defined on 'fal_cfg.h'. + * When this option is disable, it will auto find the partition table on a specified location in flash partition. + */ +#ifdef FAL_PART_HAS_TABLE_CFG + +/* check partition table definition */ +#if !defined(FAL_PART_TABLE) +#error "You must defined FAL_PART_TABLE on 'fal_cfg.h'" +#endif + +#ifdef __CC_ARM /* ARM Compiler */ + #define SECTION(x) __attribute__((section(x))) + #define USED __attribute__((used)) +#elif defined (__IAR_SYSTEMS_ICC__) /* for IAR Compiler */ + #define SECTION(x) @ x + #define USED __root +#elif defined (__GNUC__) /* GNU GCC Compiler */ + #define SECTION(x) __attribute__((section(x))) + #define USED __attribute__((used)) +#else + #error not supported tool chain +#endif /* __CC_ARM */ +USED static const struct fal_partition partition_table_def[] SECTION("FalPartTable") = FAL_PART_TABLE; +static const struct fal_partition *partition_table = NULL; + +#else /* FAL_PART_HAS_TABLE_CFG */ + +#if !defined(FAL_PART_TABLE_FLASH_DEV_NAME) +#error "You must defined FAL_PART_TABLE_FLASH_DEV_NAME on 'fal_cfg.h'" +#endif + +/* check partition table end offset address definition */ +#if !defined(FAL_PART_TABLE_END_OFFSET) +#error "You must defined FAL_PART_TABLE_END_OFFSET on 'fal_cfg.h'" +#endif + +static struct fal_partition *partition_table = NULL; +#endif /* FAL_PART_HAS_TABLE_CFG */ + +static uint8_t init_ok = 0; +static size_t partition_table_len = 0; + +/** + * print the partition table + */ +void fal_show_part_table(void) +{ + char *item1 = "name", *item2 = "flash_dev"; + size_t i, part_name_max = strlen(item1), flash_dev_name_max = strlen(item2); + const struct fal_partition *part; + + if (partition_table_len) + { + for (i = 0; i < partition_table_len; i++) + { + part = &partition_table[i]; + if (strlen(part->name) > part_name_max) + { + part_name_max = strlen(part->name); + } + if (strlen(part->flash_name) > flash_dev_name_max) + { + flash_dev_name_max = strlen(part->flash_name); + } + } + } + log_i("==================== FAL partition table ===================="); + log_i("| %-*.*s | %-*.*s | offset | length |", part_name_max, FAL_DEV_NAME_MAX, item1, flash_dev_name_max, + FAL_DEV_NAME_MAX, item2); + log_i("-------------------------------------------------------------"); + for (i = 0; i < partition_table_len; i++) + { + +#ifdef FAL_PART_HAS_TABLE_CFG + part = &partition_table[i]; +#else + part = &partition_table[partition_table_len - i - 1]; +#endif + + log_i("| %-*.*s | %-*.*s | 0x%08lx | 0x%08x |", part_name_max, FAL_DEV_NAME_MAX, part->name, flash_dev_name_max, + FAL_DEV_NAME_MAX, part->flash_name, part->offset, part->len); + } + log_i("============================================================="); +} + +/** + * Initialize all flash partition on FAL partition table + * + * @return partitions total number + */ +int fal_partition_init(void) +{ + size_t i; + const struct fal_flash_dev *flash_dev = NULL; + + if (init_ok) + { + return partition_table_len; + } + +#ifdef FAL_PART_HAS_TABLE_CFG + partition_table = &partition_table_def[0]; + partition_table_len = sizeof(partition_table_def) / sizeof(partition_table_def[0]); +#else + /* load partition table from the end address FAL_PART_TABLE_END_OFFSET, error return 0 */ + long part_table_offset = FAL_PART_TABLE_END_OFFSET; + size_t table_num = 0, table_item_size = 0; + uint8_t part_table_find_ok = 0; + uint32_t read_magic_word; + fal_partition_t new_part = NULL; + + flash_dev = fal_flash_device_find(FAL_PART_TABLE_FLASH_DEV_NAME); + if (flash_dev == NULL) + { + log_e("Initialize failed! Flash device (%s) NOT found.", FAL_PART_TABLE_FLASH_DEV_NAME); + goto _exit; + } + + /* check partition table offset address */ + if (part_table_offset < 0 || part_table_offset >= (long) flash_dev->len) + { + log_e("Setting partition table end offset address(%ld) out of flash bound(<%d).", part_table_offset, flash_dev->len); + goto _exit; + } + + table_item_size = sizeof(struct fal_partition); + new_part = (fal_partition_t)FAL_MALLOC(table_item_size); + if (new_part == NULL) + { + log_e("Initialize failed! No memory for table buffer."); + goto _exit; + } + + /* find partition table location */ + { + uint8_t read_buf[64]; + + part_table_offset -= sizeof(read_buf); + while (part_table_offset >= 0) + { + if (flash_dev->ops.read(part_table_offset, read_buf, sizeof(read_buf)) > 0) + { + /* find magic word in read buf */ + for (i = 0; i < sizeof(read_buf) - sizeof(read_magic_word) + 1; i++) + { + read_magic_word = read_buf[0 + i] + (read_buf[1 + i] << 8) + (read_buf[2 + i] << 16) + (read_buf[3 + i] << 24); + if (read_magic_word == ((FAL_PART_MAGIC_WORD_H << 16) + FAL_PART_MAGIC_WORD_L)) + { + part_table_find_ok = 1; + part_table_offset += i; + log_d("Find the partition table on '%s' offset @0x%08lx.", FAL_PART_TABLE_FLASH_DEV_NAME, + part_table_offset); + break; + } + } + } + else + { + /* read failed */ + break; + } + + if (part_table_find_ok) + { + break; + } + else + { + /* calculate next read buf position */ + if (part_table_offset >= (long)sizeof(read_buf)) + { + part_table_offset -= sizeof(read_buf); + part_table_offset += (sizeof(read_magic_word) - 1); + } + else if (part_table_offset != 0) + { + part_table_offset = 0; + } + else + { + /* find failed */ + break; + } + } + } + } + + /* load partition table */ + while (part_table_find_ok) + { + memset(new_part, 0x00, table_num); + if (flash_dev->ops.read(part_table_offset - table_item_size * (table_num), (uint8_t *) new_part, + table_item_size) < 0) + { + log_e("Initialize failed! Flash device (%s) read error!", flash_dev->name); + table_num = 0; + break; + } + + if (new_part->magic_word != ((FAL_PART_MAGIC_WORD_H << 16) + FAL_PART_MAGIC_WORD_L)) + { + break; + } + + partition_table = (fal_partition_t) FAL_REALLOC(partition_table, table_item_size * (table_num + 1)); + if (partition_table == NULL) + { + log_e("Initialize failed! No memory for partition table"); + table_num = 0; + break; + } + + memcpy(partition_table + table_num, new_part, table_item_size); + + table_num++; + }; + + if (table_num == 0) + { + log_e("Partition table NOT found on flash: %s (len: %d) from offset: 0x%08x.", FAL_PART_TABLE_FLASH_DEV_NAME, + FAL_DEV_NAME_MAX, FAL_PART_TABLE_END_OFFSET); + goto _exit; + } + else + { + partition_table_len = table_num; + } +#endif /* FAL_PART_HAS_TABLE_CFG */ + + /* check the partition table device exists */ + + for (i = 0; i < partition_table_len; i++) + { + flash_dev = fal_flash_device_find(partition_table[i].flash_name); + if (flash_dev == NULL) + { + log_d("Warning: Do NOT found the flash device(%s).", partition_table[i].flash_name); + continue; + } + + if (partition_table[i].offset >= (long)flash_dev->len) + { + log_e("Initialize failed! Partition(%s) offset address(%ld) out of flash bound(<%d).", + partition_table[i].name, partition_table[i].offset, flash_dev->len); + partition_table_len = 0; + goto _exit; + } + } + + init_ok = 1; + +_exit: + +#if FAL_DEBUG + fal_show_part_table(); +#endif + +#ifndef FAL_PART_HAS_TABLE_CFG + if (new_part) + { + FAL_FREE(new_part); + } +#endif /* !FAL_PART_HAS_TABLE_CFG */ + + return partition_table_len; +} + +/** + * find the partition by name + * + * @param name partition name + * + * @return != NULL: partition + * NULL: not found + */ +const struct fal_partition *fal_partition_find(const char *name) +{ + assert(init_ok); + + size_t i; + + for (i = 0; i < partition_table_len; i++) + { + if (!strcmp(name, partition_table[i].name)) + { + return &partition_table[i]; + } + } + + return NULL; +} + +/** + * get the partition table + * + * @param len return the partition table length + * + * @return partition table + */ +const struct fal_partition *fal_get_partition_table(size_t *len) +{ + assert(init_ok); + assert(len); + + *len = partition_table_len; + + return partition_table; +} + +/** + * set partition table temporarily + * This setting will modify the partition table temporarily, the setting will be lost after restart. + * + * @param table partition table + * @param len partition table length + */ +void fal_set_partition_table_temp(struct fal_partition *table, size_t len) +{ + assert(init_ok); + assert(table); + + partition_table_len = len; + partition_table = table; +} + +/** + * read data from partition + * + * @param part partition + * @param addr relative address for partition + * @param buf read buffer + * @param size read size + * + * @return >= 0: successful read data size + * -1: error + */ +int fal_partition_read(const struct fal_partition *part, uint32_t addr, uint8_t *buf, size_t size) +{ + int ret = 0; + const struct fal_flash_dev *flash_dev = NULL; + + assert(part); + assert(buf); + + if (addr + size > part->len) + { + log_e("Partition read error! Partition address out of bound."); + return -1; + } + + flash_dev = fal_flash_device_find(part->flash_name); + if (flash_dev == NULL) + { + log_e("Partition read error! Don't found flash device(%s) of the partition(%s).", part->flash_name, part->name); + return -1; + } + + ret = flash_dev->ops.read(part->offset + addr, buf, size); + if (ret < 0) + { + log_e("Partition read error! Flash device(%s) read error!", part->flash_name); + } + + return ret; +} + +/** + * write data to partition + * + * @param part partition + * @param addr relative address for partition + * @param buf write buffer + * @param size write size + * + * @return >= 0: successful write data size + * -1: error + */ +int fal_partition_write(const struct fal_partition *part, uint32_t addr, const uint8_t *buf, size_t size) +{ + int ret = 0; + const struct fal_flash_dev *flash_dev = NULL; + + assert(part); + assert(buf); + + if (addr + size > part->len) + { + log_e("Partition write error! Partition address out of bound."); + return -1; + } + + flash_dev = fal_flash_device_find(part->flash_name); + if (flash_dev == NULL) + { + log_e("Partition write error! Don't found flash device(%s) of the partition(%s).", part->flash_name, part->name); + return -1; + } + + ret = flash_dev->ops.write(part->offset + addr, buf, size); + if (ret < 0) + { + log_e("Partition write error! Flash device(%s) write error!", part->flash_name); + } + + return ret; +} + +/** + * erase partition data + * + * @param part partition + * @param addr relative address for partition + * @param size erase size + * + * @return >= 0: successful erased data size + * -1: error + */ +int fal_partition_erase(const struct fal_partition *part, uint32_t addr, size_t size) +{ + int ret = 0; + const struct fal_flash_dev *flash_dev = NULL; + + assert(part); + + if (addr + size > part->len) + { + log_e("Partition erase error! Partition address out of bound."); + return -1; + } + + flash_dev = fal_flash_device_find(part->flash_name); + if (flash_dev == NULL) + { + log_e("Partition erase error! Don't found flash device(%s) of the partition(%s).", part->flash_name, part->name); + return -1; + } + + ret = flash_dev->ops.erase(part->offset + addr, size); + if (ret < 0) + { + log_e("Partition erase error! Flash device(%s) erase error!", part->flash_name); + } + + return ret; +} + +/** + * erase partition all data + * + * @param part partition + * + * @return >= 0: successful erased data size + * -1: error + */ +int fal_partition_erase_all(const struct fal_partition *part) +{ + return fal_partition_erase(part, 0, part->len); +} diff --git a/flashdb/port/fal/src/fal_rtt.c b/flashdb/port/fal/src/fal_rtt.c new file mode 100644 index 0000000..5f06a30 --- /dev/null +++ b/flashdb/port/fal/src/fal_rtt.c @@ -0,0 +1,930 @@ +/* + * File : fal_rtt.c + * This file is part of FAL (Flash Abstraction Layer) package + * COPYRIGHT (C) 2006 - 2018, RT-Thread Development Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Change Logs: + * Date Author Notes + * 2018-06-23 armink the first version + * 2019-08-22 MurphyZhao adapt to none rt-thread case + */ + +#include + +#ifdef RT_VER_NUM +#include +#include +#include + +/* ========================== block device ======================== */ +struct fal_blk_device +{ + struct rt_device parent; + struct rt_device_blk_geometry geometry; + const struct fal_partition *fal_part; +}; + +/* RT-Thread device interface */ +#if RTTHREAD_VERSION >= 30000 +static rt_err_t blk_dev_control(rt_device_t dev, int cmd, void *args) +#else +static rt_err_t blk_dev_control(rt_device_t dev, rt_uint8_t cmd, void *args) +#endif +{ + struct fal_blk_device *part = (struct fal_blk_device*) dev; + + assert(part != RT_NULL); + + if (cmd == RT_DEVICE_CTRL_BLK_GETGEOME) + { + struct rt_device_blk_geometry *geometry; + + geometry = (struct rt_device_blk_geometry *) args; + if (geometry == RT_NULL) + { + return -RT_ERROR; + } + + memcpy(geometry, &part->geometry, sizeof(struct rt_device_blk_geometry)); + } + else if (cmd == RT_DEVICE_CTRL_BLK_ERASE) + { + rt_uint32_t *addrs = (rt_uint32_t *) args, start_addr = addrs[0], end_addr = addrs[1], phy_start_addr; + rt_size_t phy_size; + + if (addrs == RT_NULL || start_addr > end_addr) + { + return -RT_ERROR; + } + + if (end_addr == start_addr) + { + end_addr++; + } + + phy_start_addr = start_addr * part->geometry.bytes_per_sector; + phy_size = (end_addr - start_addr) * part->geometry.bytes_per_sector; + + if (fal_partition_erase(part->fal_part, phy_start_addr, phy_size) < 0) + { + return -RT_ERROR; + } + } + + return RT_EOK; +} + +static rt_size_t blk_dev_read(rt_device_t dev, rt_off_t pos, void* buffer, rt_size_t size) +{ + int ret = 0; + struct fal_blk_device *part = (struct fal_blk_device*) dev; + + assert(part != RT_NULL); + + ret = fal_partition_read(part->fal_part, pos * part->geometry.block_size, buffer, size * part->geometry.block_size); + + if (ret != (int)(size * part->geometry.block_size)) + { + ret = 0; + } + else + { + ret = size; + } + + return ret; +} + +static rt_size_t blk_dev_write(rt_device_t dev, rt_off_t pos, const void* buffer, rt_size_t size) +{ + int ret = 0; + struct fal_blk_device *part; + rt_off_t phy_pos; + rt_size_t phy_size; + + part = (struct fal_blk_device*) dev; + assert(part != RT_NULL); + + /* change the block device's logic address to physical address */ + phy_pos = pos * part->geometry.bytes_per_sector; + phy_size = size * part->geometry.bytes_per_sector; + + ret = fal_partition_erase(part->fal_part, phy_pos, phy_size); + + if (ret == (int) phy_size) + { + ret = fal_partition_write(part->fal_part, phy_pos, buffer, phy_size); + } + + if (ret != (int) phy_size) + { + ret = 0; + } + else + { + ret = size; + } + + return ret; +} + +#ifdef RT_USING_DEVICE_OPS +const static struct rt_device_ops blk_dev_ops = +{ + RT_NULL, + RT_NULL, + RT_NULL, + blk_dev_read, + blk_dev_write, + blk_dev_control +}; +#endif + +/** + * create RT-Thread block device by specified partition + * + * @param parition_name partition name + * + * @return != NULL: created block device + * NULL: created failed + */ +struct rt_device *fal_blk_device_create(const char *parition_name) +{ + struct fal_blk_device *blk_dev; + const struct fal_partition *fal_part = fal_partition_find(parition_name); + const struct fal_flash_dev *fal_flash = NULL; + + if (!fal_part) + { + log_e("Error: the partition name (%s) is not found.", parition_name); + return NULL; + } + + if ((fal_flash = fal_flash_device_find(fal_part->flash_name)) == NULL) + { + log_e("Error: the flash device name (%s) is not found.", fal_part->flash_name); + return NULL; + } + + blk_dev = (struct fal_blk_device*) rt_malloc(sizeof(struct fal_blk_device)); + if (blk_dev) + { + blk_dev->fal_part = fal_part; + blk_dev->geometry.bytes_per_sector = fal_flash->blk_size; + blk_dev->geometry.block_size = fal_flash->blk_size; + blk_dev->geometry.sector_count = fal_part->len / fal_flash->blk_size; + + /* register device */ + blk_dev->parent.type = RT_Device_Class_Block; + +#ifdef RT_USING_DEVICE_OPS + blk_dev->parent.ops = &blk_dev_ops; +#else + blk_dev->parent.init = NULL; + blk_dev->parent.open = NULL; + blk_dev->parent.close = NULL; + blk_dev->parent.read = blk_dev_read; + blk_dev->parent.write = blk_dev_write; + blk_dev->parent.control = blk_dev_control; +#endif + + /* no private */ + blk_dev->parent.user_data = RT_NULL; + + log_i("The FAL block device (%s) created successfully", fal_part->name); + rt_device_register(RT_DEVICE(blk_dev), fal_part->name, RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_STANDALONE); + } + else + { + log_e("Error: no memory for create FAL block device"); + } + + return RT_DEVICE(blk_dev); +} + +/* ========================== MTD nor device ======================== */ +#if defined(RT_USING_MTD_NOR) + +struct fal_mtd_nor_device +{ + struct rt_mtd_nor_device parent; + const struct fal_partition *fal_part; +}; + +static rt_size_t mtd_nor_dev_read(struct rt_mtd_nor_device* device, rt_off_t offset, rt_uint8_t* data, rt_uint32_t length) +{ + int ret = 0; + struct fal_mtd_nor_device *part = (struct fal_mtd_nor_device*) device; + + assert(part != RT_NULL); + + ret = fal_partition_read(part->fal_part, offset, data, length); + + if (ret != (int)length) + { + ret = 0; + } + else + { + ret = length; + } + + return ret; +} + +static rt_size_t mtd_nor_dev_write(struct rt_mtd_nor_device* device, rt_off_t offset, const rt_uint8_t* data, rt_uint32_t length) +{ + int ret = 0; + struct fal_mtd_nor_device *part; + + part = (struct fal_mtd_nor_device*) device; + assert(part != RT_NULL); + + ret = fal_partition_write(part->fal_part, offset, data, length); + + if (ret != (int) length) + { + ret = 0; + } + else + { + ret = length; + } + + return ret; +} + +static rt_err_t mtd_nor_dev_erase(struct rt_mtd_nor_device* device, rt_off_t offset, rt_uint32_t length) +{ + int ret = 0; + struct fal_mtd_nor_device *part; + + part = (struct fal_mtd_nor_device*) device; + assert(part != RT_NULL); + + ret = fal_partition_erase(part->fal_part, offset, length); + + if (ret != length) + { + return -RT_ERROR; + } + else + { + return RT_EOK; + } +} + +static const struct rt_mtd_nor_driver_ops _ops = +{ + RT_NULL, + mtd_nor_dev_read, + mtd_nor_dev_write, + mtd_nor_dev_erase, +}; + +/** + * create RT-Thread MTD NOR device by specified partition + * + * @param parition_name partition name + * + * @return != NULL: created MTD NOR device + * NULL: created failed + */ +struct rt_device *fal_mtd_nor_device_create(const char *parition_name) +{ + struct fal_mtd_nor_device *mtd_nor_dev; + const struct fal_partition *fal_part = fal_partition_find(parition_name); + const struct fal_flash_dev *fal_flash = NULL; + + if (!fal_part) + { + log_e("Error: the partition name (%s) is not found.", parition_name); + return NULL; + } + + if ((fal_flash = fal_flash_device_find(fal_part->flash_name)) == NULL) + { + log_e("Error: the flash device name (%s) is not found.", fal_part->flash_name); + return NULL; + } + + mtd_nor_dev = (struct fal_mtd_nor_device*) rt_malloc(sizeof(struct fal_mtd_nor_device)); + if (mtd_nor_dev) + { + mtd_nor_dev->fal_part = fal_part; + + mtd_nor_dev->parent.block_start = 0; + mtd_nor_dev->parent.block_end = fal_part->len / fal_flash->blk_size; + mtd_nor_dev->parent.block_size = fal_flash->blk_size; + + /* set ops */ + mtd_nor_dev->parent.ops = &_ops; + + log_i("The FAL MTD NOR device (%s) created successfully", fal_part->name); + rt_mtd_nor_register_device(fal_part->name, &mtd_nor_dev->parent); + } + else + { + log_e("Error: no memory for create FAL MTD NOR device"); + } + + return RT_DEVICE(&mtd_nor_dev->parent); +} + +#endif /* defined(RT_USING_MTD_NOR) */ + + +/* ========================== char device ======================== */ +struct fal_char_device +{ + struct rt_device parent; + const struct fal_partition *fal_part; +}; + +/* RT-Thread device interface */ +static rt_size_t char_dev_read(rt_device_t dev, rt_off_t pos, void *buffer, rt_size_t size) +{ + int ret = 0; + struct fal_char_device *part = (struct fal_char_device *) dev; + + assert(part != RT_NULL); + + if (pos + size > part->fal_part->len) + size = part->fal_part->len - pos; + + ret = fal_partition_read(part->fal_part, pos, buffer, size); + + if (ret != (int)(size)) + ret = 0; + + return ret; +} + +static rt_size_t char_dev_write(rt_device_t dev, rt_off_t pos, const void *buffer, rt_size_t size) +{ + int ret = 0; + struct fal_char_device *part; + + part = (struct fal_char_device *) dev; + assert(part != RT_NULL); + + if (pos == 0) + { + fal_partition_erase_all(part->fal_part); + } + else if (pos + size > part->fal_part->len) + { + size = part->fal_part->len - pos; + } + + ret = fal_partition_write(part->fal_part, pos, buffer, size); + + if (ret != (int) size) + ret = 0; + + return ret; +} + +#ifdef RT_USING_DEVICE_OPS +const static struct rt_device_ops char_dev_ops = +{ + RT_NULL, + RT_NULL, + RT_NULL, + char_dev_read, + char_dev_write, + RT_NULL +}; +#endif + +#ifdef RT_USING_POSIX +#include + +/* RT-Thread device filesystem interface */ +static int char_dev_fopen(struct dfs_fd *fd) +{ + struct fal_char_device *part = (struct fal_char_device *) fd->data; + + assert(part != RT_NULL); + + switch (fd->flags & O_ACCMODE) + { + case O_RDONLY: + break; + case O_WRONLY: + case O_RDWR: + /* erase partition when device file open */ + fal_partition_erase_all(part->fal_part); + break; + default: + break; + } + fd->pos = 0; + + return RT_EOK; +} + +static int char_dev_fread(struct dfs_fd *fd, void *buf, size_t count) +{ + int ret = 0; + struct fal_char_device *part = (struct fal_char_device *) fd->data; + + assert(part != RT_NULL); + + if (fd->pos + count > part->fal_part->len) + count = part->fal_part->len - fd->pos; + + ret = fal_partition_read(part->fal_part, fd->pos, buf, count); + + if (ret != (int)(count)) + return 0; + + fd->pos += ret; + + return ret; +} + +static int char_dev_fwrite(struct dfs_fd *fd, const void *buf, size_t count) +{ + int ret = 0; + struct fal_char_device *part = (struct fal_char_device *) fd->data; + + assert(part != RT_NULL); + + if (fd->pos + count > part->fal_part->len) + count = part->fal_part->len - fd->pos; + + ret = fal_partition_write(part->fal_part, fd->pos, buf, count); + + if (ret != (int) count) + return 0; + + fd->pos += ret; + + return ret; +} + +static const struct dfs_file_ops char_dev_fops = +{ + char_dev_fopen, + RT_NULL, + RT_NULL, + char_dev_fread, + char_dev_fwrite, + RT_NULL, /* flush */ + RT_NULL, /* lseek */ + RT_NULL, /* getdents */ + RT_NULL, +}; +#endif /* defined(RT_USING_POSIX) */ + +/** + * create RT-Thread char device by specified partition + * + * @param parition_name partition name + * + * @return != NULL: created char device + * NULL: created failed + */ +struct rt_device *fal_char_device_create(const char *parition_name) +{ + struct fal_char_device *char_dev; + const struct fal_partition *fal_part = fal_partition_find(parition_name); + + if (!fal_part) + { + log_e("Error: the partition name (%s) is not found.", parition_name); + return NULL; + } + + if ((fal_flash_device_find(fal_part->flash_name)) == NULL) + { + log_e("Error: the flash device name (%s) is not found.", fal_part->flash_name); + return NULL; + } + + char_dev = (struct fal_char_device *) rt_malloc(sizeof(struct fal_char_device)); + if (char_dev) + { + char_dev->fal_part = fal_part; + + /* register device */ + char_dev->parent.type = RT_Device_Class_Char; + +#ifdef RT_USING_DEVICE_OPS + char_dev->parent.ops = &char_dev_ops; +#else + char_dev->parent.init = NULL; + char_dev->parent.open = NULL; + char_dev->parent.close = NULL; + char_dev->parent.read = char_dev_read; + char_dev->parent.write = char_dev_write; + char_dev->parent.control = NULL; + /* no private */ + char_dev->parent.user_data = NULL; +#endif + + rt_device_register(RT_DEVICE(char_dev), fal_part->name, RT_DEVICE_FLAG_RDWR); + log_i("The FAL char device (%s) created successfully", fal_part->name); + +#ifdef RT_USING_POSIX + /* set fops */ + char_dev->parent.fops = &char_dev_fops; +#endif + + } + else + { + log_e("Error: no memory for create FAL char device"); + } + + return RT_DEVICE(char_dev); +} + +#if defined(RT_USING_FINSH) && defined(FINSH_USING_MSH) + +#include +extern int fal_init_check(void); + +static void fal(uint8_t argc, char **argv) { + +#define __is_print(ch) ((unsigned int)((ch) - ' ') < 127u - ' ') +#define HEXDUMP_WIDTH 16 +#define CMD_PROBE_INDEX 0 +#define CMD_READ_INDEX 1 +#define CMD_WRITE_INDEX 2 +#define CMD_ERASE_INDEX 3 +#define CMD_BENCH_INDEX 4 + + int result; + static const struct fal_flash_dev *flash_dev = NULL; + static const struct fal_partition *part_dev = NULL; + size_t i = 0, j = 0; + + const char* help_info[] = + { + [CMD_PROBE_INDEX] = "fal probe [dev_name|part_name] - probe flash device or partition by given name", + [CMD_READ_INDEX] = "fal read addr size - read 'size' bytes starting at 'addr'", + [CMD_WRITE_INDEX] = "fal write addr data1 ... dataN - write some bytes 'data' starting at 'addr'", + [CMD_ERASE_INDEX] = "fal erase addr size - erase 'size' bytes starting at 'addr'", + [CMD_BENCH_INDEX] = "fal bench - benchmark test with per block size", + }; + + if (fal_init_check() != 1) + { + rt_kprintf("\n[Warning] FAL is not initialized or failed to initialize!\n\n"); + return; + } + + if (argc < 2) + { + rt_kprintf("Usage:\n"); + for (i = 0; i < sizeof(help_info) / sizeof(char*); i++) + { + rt_kprintf("%s\n", help_info[i]); + } + rt_kprintf("\n"); + } + else + { + const char *operator = argv[1]; + uint32_t addr, size; + + if (!strcmp(operator, "probe")) + { + if (argc >= 3) + { + char *dev_name = argv[2]; + if ((flash_dev = fal_flash_device_find(dev_name)) != NULL) + { + part_dev = NULL; + } + else if ((part_dev = fal_partition_find(dev_name)) != NULL) + { + flash_dev = NULL; + } + else + { + rt_kprintf("Device %s NOT found. Probe failed.\n", dev_name); + flash_dev = NULL; + part_dev = NULL; + } + } + + if (flash_dev) + { + rt_kprintf("Probed a flash device | %s | addr: %ld | len: %d |.\n", flash_dev->name, + flash_dev->addr, flash_dev->len); + } + else if (part_dev) + { + rt_kprintf("Probed a flash partition | %s | flash_dev: %s | offset: %ld | len: %d |.\n", + part_dev->name, part_dev->flash_name, part_dev->offset, part_dev->len); + } + else + { + rt_kprintf("No flash device or partition was probed.\n"); + rt_kprintf("Usage: %s.\n", help_info[CMD_PROBE_INDEX]); + fal_show_part_table(); + } + } + else + { + if (!flash_dev && !part_dev) + { + rt_kprintf("No flash device or partition was probed. Please run 'fal probe'.\n"); + return; + } + if (!rt_strcmp(operator, "read")) + { + if (argc < 4) + { + rt_kprintf("Usage: %s.\n", help_info[CMD_READ_INDEX]); + return; + } + else + { + addr = strtol(argv[2], NULL, 0); + size = strtol(argv[3], NULL, 0); + uint8_t *data = rt_malloc(size); + if (data) + { + if (flash_dev) + { + result = flash_dev->ops.read(addr, data, size); + } + else if (part_dev) + { + result = fal_partition_read(part_dev, addr, data, size); + } + if (result >= 0) + { + rt_kprintf("Read data success. Start from 0x%08X, size is %ld. The data is:\n", addr, + size); + rt_kprintf("Offset (h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F\n"); + for (i = 0; i < size; i += HEXDUMP_WIDTH) + { + rt_kprintf("[%08X] ", addr + i); + /* dump hex */ + for (j = 0; j < HEXDUMP_WIDTH; j++) + { + if (i + j < size) + { + rt_kprintf("%02X ", data[i + j]); + } + else + { + rt_kprintf(" "); + } + } + /* dump char for hex */ + for (j = 0; j < HEXDUMP_WIDTH; j++) + { + if (i + j < size) + { + rt_kprintf("%c", __is_print(data[i + j]) ? data[i + j] : '.'); + } + } + rt_kprintf("\n"); + } + rt_kprintf("\n"); + } + rt_free(data); + } + else + { + rt_kprintf("Low memory!\n"); + } + } + } + else if (!strcmp(operator, "write")) + { + if (argc < 4) + { + rt_kprintf("Usage: %s.\n", help_info[CMD_WRITE_INDEX]); + return; + } + else + { + addr = strtol(argv[2], NULL, 0); + size = argc - 3; + uint8_t *data = rt_malloc(size); + if (data) + { + for (i = 0; i < size; i++) + { + data[i] = strtol(argv[3 + i], NULL, 0); + } + if (flash_dev) + { + result = flash_dev->ops.write(addr, data, size); + } + else if (part_dev) + { + result = fal_partition_write(part_dev, addr, data, size); + } + if (result >= 0) + { + rt_kprintf("Write data success. Start from 0x%08X, size is %ld.\n", addr, size); + rt_kprintf("Write data: "); + for (i = 0; i < size; i++) + { + rt_kprintf("%d ", data[i]); + } + rt_kprintf(".\n"); + } + rt_free(data); + } + else + { + rt_kprintf("Low memory!\n"); + } + } + } + else if (!rt_strcmp(operator, "erase")) + { + if (argc < 4) + { + rt_kprintf("Usage: %s.\n", help_info[CMD_ERASE_INDEX]); + return; + } + else + { + addr = strtol(argv[2], NULL, 0); + size = strtol(argv[3], NULL, 0); + if (flash_dev) + { + result = flash_dev->ops.erase(addr, size); + } + else if (part_dev) + { + result = fal_partition_erase(part_dev, addr, size); + } + if (result >= 0) + { + rt_kprintf("Erase data success. Start from 0x%08X, size is %ld.\n", addr, size); + } + } + } + else if (!strcmp(operator, "bench")) + { + if (argc < 3) + { + rt_kprintf("Usage: %s.\n", help_info[CMD_BENCH_INDEX]); + return; + } + else if ((argc > 3 && strcmp(argv[3], "yes")) || argc < 4) + { + rt_kprintf("DANGER: It will erase full chip or partition! Please run 'fal bench %d yes'.\n", strtol(argv[2], NULL, 0)); + return; + } + /* full chip benchmark test */ + uint32_t start_time, time_cast; + size_t write_size = strtol(argv[2], NULL, 0), read_size = strtol(argv[2], NULL, 0), cur_read_size; + uint8_t *write_data = (uint8_t *)rt_malloc(write_size), *read_data = (uint8_t *)rt_malloc(read_size); + + if (write_data && read_data) + { + memset(write_data, 0x55, write_size); + if (flash_dev) + { + size = flash_dev->len; + } + else if (part_dev) + { + size = part_dev->len; + } + /* benchmark testing */ + rt_kprintf("Erasing %ld bytes data, waiting...\n", size); + start_time = rt_tick_get(); + if (flash_dev) + { + result = flash_dev->ops.erase(0, size); + } + else if (part_dev) + { + result = fal_partition_erase(part_dev, 0, size); + } + if (result >= 0) + { + time_cast = rt_tick_get() - start_time; + rt_kprintf("Erase benchmark success, total time: %d.%03dS.\n", time_cast / RT_TICK_PER_SECOND, + time_cast % RT_TICK_PER_SECOND / ((RT_TICK_PER_SECOND * 1 + 999) / 1000)); + } + else + { + rt_kprintf("Erase benchmark has an error. Error code: %d.\n", result); + } + /* write test */ + rt_kprintf("Writing %ld bytes data, waiting...\n", size); + start_time = rt_tick_get(); + for (i = 0; i < size; i += write_size) + { + if (flash_dev) + { + result = flash_dev->ops.write(i, write_data, write_size); + } + else if (part_dev) + { + result = fal_partition_write(part_dev, i, write_data, write_size); + } + if (result < 0) + { + break; + } + } + if (result >= 0) + { + time_cast = rt_tick_get() - start_time; + rt_kprintf("Write benchmark success, total time: %d.%03dS.\n", time_cast / RT_TICK_PER_SECOND, + time_cast % RT_TICK_PER_SECOND / ((RT_TICK_PER_SECOND * 1 + 999) / 1000)); + } + else + { + rt_kprintf("Write benchmark has an error. Error code: %d.\n", result); + } + /* read test */ + rt_kprintf("Reading %ld bytes data, waiting...\n", size); + start_time = rt_tick_get(); + for (i = 0; i < size; i += read_size) + { + if (i + read_size <= size) + { + cur_read_size = read_size; + } + else + { + cur_read_size = size - i; + } + if (flash_dev) + { + result = flash_dev->ops.read(i, read_data, cur_read_size); + } + else if (part_dev) + { + result = fal_partition_read(part_dev, i, read_data, cur_read_size); + } + /* data check */ + if (memcmp(write_data, read_data, cur_read_size)) + { + result = -RT_ERROR; + rt_kprintf("Data check ERROR! Please check you flash by other command.\n"); + } + /* has an error */ + if (result < 0) + { + break; + } + } + if (result >= 0) + { + time_cast = rt_tick_get() - start_time; + rt_kprintf("Read benchmark success, total time: %d.%03dS.\n", time_cast / RT_TICK_PER_SECOND, + time_cast % RT_TICK_PER_SECOND / ((RT_TICK_PER_SECOND * 1 + 999) / 1000)); + } + else + { + rt_kprintf("Read benchmark has an error. Error code: %d.\n", result); + } + } + else + { + rt_kprintf("Low memory!\n"); + } + rt_free(write_data); + rt_free(read_data); + } + else + { + rt_kprintf("Usage:\n"); + for (i = 0; i < sizeof(help_info) / sizeof(char*); i++) + { + rt_kprintf("%s\n", help_info[i]); + } + rt_kprintf("\n"); + return; + } + if (result < 0) { + rt_kprintf("This operate has an error. Error code: %d.\n", result); + } + } + } +} +MSH_CMD_EXPORT(fal, FAL (Flash Abstraction Layer) operate.); + +#endif /* defined(RT_USING_FINSH) && defined(FINSH_USING_MSH) */ +#endif /* RT_VER_NUM */ diff --git a/flashdb/src/fdb.c b/flashdb/src/fdb.c new file mode 100644 index 0000000..b770381 --- /dev/null +++ b/flashdb/src/fdb.c @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2020, Armink, + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * @brief Initialize interface. + * + * Some initialize interface for this library. + */ + +#include +#include + +#define FDB_LOG_TAG "" + +/** + * Set database lock and unlock funtion. + * + * @param db database object + * @param lock lock function + * @param unlock lock function + */ +void fdb_lock_set(fdb_db_t db, void (*lock)(fdb_db_t db), void (*unlock)(fdb_db_t db)) +{ + FDB_ASSERT(db); + + db->lock = lock; + db->unlock = unlock; +} + +/** + * Set the sector size for database. + * + * @note The sector size MUST align by partition block size. + * @note The sector size change MUST before database initialization. + * + * @param db database object + * @param sec_size + */ +void fdb_sec_size_set(fdb_db_t db, uint32_t sec_size) +{ + FDB_ASSERT(db); + /* the sector size change MUST before database initialization */ + FDB_ASSERT(db->init_ok == false); + + db->sec_size = sec_size; +} + +fdb_err_t _fdb_init_ex(fdb_db_t db, const char *name, const char *part_name, fdb_db_type type, void *user_data) +{ + size_t block_size; + + FDB_ASSERT(db); + FDB_ASSERT(name); + FDB_ASSERT(part_name); + + if (db->init_ok) { + return FDB_NO_ERR; + } + + db->name = name; + db->type = type; + db->user_data = user_data; + /* FAL (Flash Abstraction Layer) initialization */ + fal_init(); + /* check the flash partition */ + if ((db->part = fal_partition_find(part_name)) == NULL) { + FDB_INFO("Error: Partition (%s) not found.\n", part_name); + return FDB_PART_NOT_FOUND; + } + + block_size = fal_flash_device_find(db->part->flash_name)->blk_size; + if (db->sec_size == 0) { + db->sec_size = block_size; + } else { + /* must be aligned with block size */ + FDB_ASSERT(db->sec_size % block_size == 0); + } + + return FDB_NO_ERR; +} + +void _fdb_init_finish(fdb_db_t db, fdb_err_t result) +{ + static bool log_is_show = false; + if (result == FDB_NO_ERR) { + db->init_ok = true; + if (!log_is_show) { + FDB_INFO("FlashDB V%s is initialize success.\n", FDB_SW_VERSION); + FDB_INFO("You can get the latest version on https://github.com/armink/FlashDB .\n"); + log_is_show = true; + } + } else { + FDB_INFO("Error: %s(%s) at partition %s is initialize fail(%d).\n", db->type == FDB_DB_TYPE_KV ? "KV" : "TS", + db->name, db->part->name, result); + } +} diff --git a/flashdb/src/fdb_cmd.c b/flashdb/src/fdb_cmd.c new file mode 100644 index 0000000..c48a0fc --- /dev/null +++ b/flashdb/src/fdb_cmd.c @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2020, Armink, + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * @brief Shell commands. + * + * RT-Thread Finsh/MSH command for EasyFlash. + */ + +#include +#include + +extern struct fdb_kvdb _global_kvdb; +extern struct fdb_tsdb _global_tsdb; + +#if defined(RT_USING_FINSH) && defined(FINSH_USING_MSH) && defined(FDB_USING_KVDB) +#include +#if defined(FDB_USING_KVDB) +static void __setenv(uint8_t argc, char **argv) { + uint8_t i; + + if (argc > 3) { + /* environment variable value string together */ + for (i = 0; i < argc - 2; i++) { + argv[2 + i][rt_strlen(argv[2 + i])] = ' '; + } + } + if (argc == 1) { + rt_kprintf("Please input: setenv [value]\n"); + } else if (argc == 2) { + fdb_kv_set(&_global_kvdb, argv[1], NULL); + } else { + fdb_kv_set(&_global_kvdb, argv[1], argv[2]); + } +} +MSH_CMD_EXPORT_ALIAS(__setenv, setenv, Set an envrionment variable.); + +static void printenv(uint8_t argc, char **argv) { + fdb_kv_print(&_global_kvdb); +} +MSH_CMD_EXPORT(printenv, Print all envrionment variables.); + +static void getvalue(uint8_t argc, char **argv) { + char *value = NULL; + value = fdb_kv_get(&_global_kvdb, argv[1]); + if (value) { + rt_kprintf("The %s value is %s.\n", argv[1], value); + } else { + rt_kprintf("Can't find %s.\n", argv[1]); + } +} +MSH_CMD_EXPORT(getvalue, Get an envrionment variable by name.); + +static void resetenv(uint8_t argc, char **argv) { + fdb_kv_set_default(&_global_kvdb); +} +MSH_CMD_EXPORT(resetenv, Reset all envrionment variable to default.); + +#endif /* defined(FDB_USING_KVDB) */ + + +#if defined(FDB_USING_TSDB) +static bool tsl_cb(fdb_tsl_t tsl, void *arg) +{ + struct fdb_blob blob; + char *log = rt_malloc(tsl->log_len); + size_t read_len; + + if (log) { + fdb_blob_make(&blob, log, tsl->log_len); + read_len = fdb_blob_read((fdb_db_t)&_global_tsdb, fdb_tsl_to_blob(tsl, &blob)); + + rt_kprintf("TSL time: %d\n", tsl->time); + rt_kprintf("TSL blob content: %.*s\n", read_len, blob.buf); + rt_free(log); + } + + return false; +} + +static bool tsl_bench_cb(fdb_tsl_t tsl, void *arg) +{ + rt_tick_t *end_tick = arg; + + *end_tick = rt_tick_get(); + + return false; +} + +static void tsl(uint8_t argc, char **argv) { + struct fdb_blob blob; + struct tm tm_from = { .tm_year = 1970 - 1900, .tm_mon = 0, .tm_mday = 1, .tm_hour = 0, .tm_min = 0, .tm_sec = 0 }; + struct tm tm_to = { .tm_year = 2030 - 1900, .tm_mon = 0, .tm_mday = 1, .tm_hour = 0, .tm_min = 0, .tm_sec = 0 }; + time_t from_time = mktime(&tm_from), to_time = mktime(&tm_to); + rt_tick_t start_tick = rt_tick_get(), end_tick; + + if (!strcmp(argv[1], "add") && (argc > 2)) { + fdb_tsl_append(&_global_tsdb, fdb_blob_make(&blob, argv[2], strlen(argv[2]))); + } else if (!strcmp(argv[1], "get") && (argc > 1)) { + fdb_tsl_iter_by_time(&_global_tsdb, from_time, to_time, tsl_cb, NULL); +// fdb_ts_iter_by_time(&_global_tsdb, atoi(argv[2]), atoi(argv[3]), ts_cb, NULL); + } else if (!strcmp(argv[1], "clean") && (argc > 1)) { + fdb_tsl_clean(&_global_tsdb); + } else if (!strcmp(argv[1], "query") && (argc > 2)) { + int status = atoi(argv[2]); + size_t count; + count = fdb_tsl_query_count(&_global_tsdb, from_time, to_time, status); + rt_kprintf("query count: %d\n", count); + } else if (!strcmp(argv[1], "bench") && (argc > 1)) { +#define BENCH_TIMEOUT (5*1000) + struct fdb_blob blob; + static char data[11], log[128]; + size_t append_num = 0; + fdb_time_t start, end, cur; + rt_tick_t bench_start_tick, spent_tick, min_tick = 9999, max_tick = 0, total_tick = 0; + float temp; + + fdb_tsl_clean(&_global_tsdb); + bench_start_tick = rt_tick_get(); + start = _global_tsdb.get_time(); + while (rt_tick_get() - bench_start_tick <= (rt_tick_t)rt_tick_from_millisecond(BENCH_TIMEOUT)) { + rt_snprintf(data, sizeof(data), "%d", append_num++); + fdb_tsl_append(&_global_tsdb, fdb_blob_make(&blob, data, rt_strnlen(data, sizeof(data)))); + } + end = _global_tsdb.get_time(); + temp = (float) append_num / (float)(BENCH_TIMEOUT / 1000); + snprintf(log, sizeof(log), "Append %d TSL in %d seconds, average: %.2f tsl/S, %.2f ms/per\n", append_num, + BENCH_TIMEOUT / 1000, temp, 1000.0f / temp); + rt_kprintf("%s", log); + cur = start; + while(cur < end) { + end_tick = bench_start_tick = rt_tick_get(); + fdb_tsl_iter_by_time(&_global_tsdb, cur, cur, tsl_bench_cb, &end_tick); +// spent_tick = end_tick - bench_start_tick; + spent_tick = rt_tick_get() - bench_start_tick; + if (spent_tick < min_tick) { + min_tick = spent_tick; + } + if (spent_tick > max_tick) { + max_tick = spent_tick; + } + total_tick += spent_tick; + cur ++; + } + snprintf(log, sizeof(log), "Query total spent %u (tick) for %ld TSL, min %u, max %u, average: %.2f tick/per\n", total_tick, end - start, min_tick, max_tick, + (float) total_tick / (float) (end - start)); + rt_kprintf("%s", log); + fdb_tsl_clean(&_global_tsdb); + } else { + rt_kprintf("Please input: tsl [add log content|get [from_s to_s]]\n"); + } + + rt_kprintf("exec time: %d ticks\n", rt_tick_get() - start_tick); +} +MSH_CMD_EXPORT_ALIAS(tsl, tsl, Time series log. tsl [add log content|get [from_s to_s]|clean].); +#endif /* defined(FDB_USING_TSDB) */ + +#endif /* defined(RT_USING_FINSH) && defined(FINSH_USING_MSH) */ diff --git a/flashdb/src/fdb_kvdb.c b/flashdb/src/fdb_kvdb.c new file mode 100644 index 0000000..c82b6e2 --- /dev/null +++ b/flashdb/src/fdb_kvdb.c @@ -0,0 +1,1582 @@ +/* + * Copyright (c) 2020, Armink, + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * @brief KVDB feature. + * + * Key-Value Database feature implement source file. + */ + +#include +#include +#include + +#define FDB_LOG_TAG "[kv]" +/* rewrite log prefix */ +#undef FDB_LOG_PREFIX2 +#define FDB_LOG_PREFIX2() FDB_PRINT("[%s] ", db_name(db)) + +#if defined(FDB_USING_KVDB) + +#ifndef FDB_WRITE_GRAN +#error "Please configure flash write granularity (in fdb_cfg.h)" +#endif + +#if FDB_WRITE_GRAN != 1 && FDB_WRITE_GRAN != 8 && FDB_WRITE_GRAN != 32 +#error "the write gran can be only setting as 1, 8 and 32" +#endif + +/* magic word(`F`, `D`, `B`, `1`) */ +#define SECTOR_MAGIC_WORD 0x30424446 +/* magic word(`K`, `V`, `0`, `0`) */ +#define KV_MAGIC_WORD 0x3030564B + +/* the sector remain threshold before full status */ +#ifndef FDB_SEC_REMAIN_THRESHOLD +#define FDB_SEC_REMAIN_THRESHOLD (KV_HDR_DATA_SIZE + FDB_KV_NAME_MAX) +#endif + +/* the total remain empty sector threshold before GC */ +#ifndef FDB_GC_EMPTY_SEC_THRESHOLD +#define FDB_GC_EMPTY_SEC_THRESHOLD 1 +#endif + +/* the string KV value buffer size for legacy fdb_get_kv(db, ) function */ +#ifndef FDB_STR_KV_VALUE_MAX_SIZE +#define FDB_STR_KV_VALUE_MAX_SIZE 128 +#endif + +#if FDB_KV_CACHE_TABLE_SIZE > 0xFFFF +#error "The KV cache table size must less than 0xFFFF" +#endif + +/* the sector is not combined value */ +#define SECTOR_NOT_COMBINED 0xFFFFFFFF +/* the next address is get failed */ +#define FAILED_ADDR 0xFFFFFFFF + +#define KV_STATUS_TABLE_SIZE FDB_STATUS_TABLE_SIZE(FDB_KV_STATUS_NUM) + +#define SECTOR_NUM (db_part_size(db) / db_sec_size(db)) + +#define SECTOR_HDR_DATA_SIZE (FDB_WG_ALIGN(sizeof(struct sector_hdr_data))) +#define SECTOR_DIRTY_OFFSET ((unsigned long)(&((struct sector_hdr_data *)0)->status_table.dirty)) +#define KV_HDR_DATA_SIZE (FDB_WG_ALIGN(sizeof(struct kv_hdr_data))) +#define KV_MAGIC_OFFSET ((unsigned long)(&((struct kv_hdr_data *)0)->magic)) +#define KV_LEN_OFFSET ((unsigned long)(&((struct kv_hdr_data *)0)->len)) +#define KV_NAME_LEN_OFFSET ((unsigned long)(&((struct kv_hdr_data *)0)->name_len)) + +#define db_name(db) (((fdb_db_t)db)->name) +#define db_init_ok(db) (((fdb_db_t)db)->init_ok) +#define db_sec_size(db) (((fdb_db_t)db)->sec_size) +#define db_part_size(db) (((fdb_db_t)db)->part->len) +#define db_lock(db) \ + do { \ + if (((fdb_db_t)db)->lock) ((fdb_db_t)db)->lock((fdb_db_t)db); \ + } while(0); + +#define db_unlock(db) \ + do { \ + if (((fdb_db_t)db)->unlock) ((fdb_db_t)db)->unlock((fdb_db_t)db); \ + } while(0); + +#define VER_NUM_KV_NAME "__ver_num__" + +struct sector_hdr_data { + struct { + uint8_t store[FDB_STORE_STATUS_TABLE_SIZE]; /**< sector store status @see fdb_sector_store_status_t */ + uint8_t dirty[FDB_DIRTY_STATUS_TABLE_SIZE]; /**< sector dirty status @see fdb_sector_dirty_status_t */ + } status_table; + uint32_t magic; /**< magic word(`E`, `F`, `4`, `0`) */ + uint32_t combined; /**< the combined next sector number, 0xFFFFFFFF: not combined */ + uint32_t reserved; +}; +typedef struct sector_hdr_data *sector_hdr_data_t; + +struct kv_hdr_data { + uint8_t status_table[KV_STATUS_TABLE_SIZE]; /**< KV node status, @see fdb_kv_status_t */ + uint32_t magic; /**< magic word(`K`, `V`, `4`, `0`) */ + uint32_t len; /**< KV node total length (header + name + value), must align by FDB_WRITE_GRAN */ + uint32_t crc32; /**< KV node crc32(name_len + data_len + name + value) */ + uint8_t name_len; /**< name length */ + uint32_t value_len; /**< value length */ +}; +typedef struct kv_hdr_data *kv_hdr_data_t; + +struct alloc_kv_cb_args { + fdb_kvdb_t db; + size_t kv_size; + uint32_t *empty_kv; +}; + +static void gc_collect(fdb_kvdb_t db); + +#ifdef FDB_KV_USING_CACHE +/* + * It's only caching the current using status sector's empty_addr + */ +static void update_sector_cache(fdb_kvdb_t db, uint32_t sec_addr, uint32_t empty_addr) +{ + size_t i, empty_index = FDB_SECTOR_CACHE_TABLE_SIZE; + + for (i = 0; i < FDB_SECTOR_CACHE_TABLE_SIZE; i++) { + if ((empty_addr > sec_addr) && (empty_addr < sec_addr + db_sec_size(db))) { + /* update the sector empty_addr in cache */ + if (db->sector_cache_table[i].addr == sec_addr) { + db->sector_cache_table[i].addr = sec_addr; + db->sector_cache_table[i].empty_addr = empty_addr; + return; + } else if ((db->sector_cache_table[i].addr == FDB_DATA_UNUSED) && (empty_index == FDB_SECTOR_CACHE_TABLE_SIZE)) { + empty_index = i; + } + } else if (db->sector_cache_table[i].addr == sec_addr) { + /* delete the sector which status is not current using */ + db->sector_cache_table[i].addr = FDB_DATA_UNUSED; + return; + } + } + /* add the sector empty_addr to cache */ + if (empty_index < FDB_SECTOR_CACHE_TABLE_SIZE) { + db->sector_cache_table[empty_index].addr = sec_addr; + db->sector_cache_table[empty_index].empty_addr = empty_addr; + } +} + +/* + * Get sector info from cache. It's return true when cache is hit. + */ +static bool get_sector_from_cache(fdb_kvdb_t db, uint32_t sec_addr, uint32_t *empty_addr) +{ + size_t i; + + for (i = 0; i < FDB_SECTOR_CACHE_TABLE_SIZE; i++) { + if (db->sector_cache_table[i].addr == sec_addr) { + if (empty_addr) { + *empty_addr = db->sector_cache_table[i].empty_addr; + } + return true; + } + } + + return false; +} + +static void update_kv_cache(fdb_kvdb_t db, const char *name, size_t name_len, uint32_t addr) +{ + size_t i, empty_index = FDB_KV_CACHE_TABLE_SIZE, min_activity_index = FDB_KV_CACHE_TABLE_SIZE; + uint16_t name_crc = (uint16_t) (fdb_calc_crc32(0, name, name_len) >> 16), min_activity = 0xFFFF; + + for (i = 0; i < FDB_KV_CACHE_TABLE_SIZE; i++) { + if (addr != FDB_DATA_UNUSED) { + /* update the KV address in cache */ + if (db->kv_cache_table[i].name_crc == name_crc) { + db->kv_cache_table[i].addr = addr; + return; + } else if ((db->kv_cache_table[i].addr == FDB_DATA_UNUSED) && (empty_index == FDB_KV_CACHE_TABLE_SIZE)) { + empty_index = i; + } else if (db->kv_cache_table[i].addr != FDB_DATA_UNUSED) { + if (db->kv_cache_table[i].active > 0) { + db->kv_cache_table[i].active--; + } + if (db->kv_cache_table[i].active < min_activity) { + min_activity_index = i; + min_activity = db->kv_cache_table[i].active; + } + } + } else if (db->kv_cache_table[i].name_crc == name_crc) { + /* delete the KV */ + db->kv_cache_table[i].addr = FDB_DATA_UNUSED; + db->kv_cache_table[i].active = 0; + return; + } + } + /* add the KV to cache, using LRU (Least Recently Used) like algorithm */ + if (empty_index < FDB_KV_CACHE_TABLE_SIZE) { + db->kv_cache_table[empty_index].addr = addr; + db->kv_cache_table[empty_index].name_crc = name_crc; + db->kv_cache_table[empty_index].active = 0; + } else if (min_activity_index < FDB_KV_CACHE_TABLE_SIZE) { + db->kv_cache_table[min_activity_index].addr = addr; + db->kv_cache_table[min_activity_index].name_crc = name_crc; + db->kv_cache_table[min_activity_index].active = 0; + } +} + +/* + * Get KV info from cache. It's return true when cache is hit. + */ +static bool get_kv_from_cache(fdb_kvdb_t db, const char *name, size_t name_len, uint32_t *addr) +{ + size_t i; + uint16_t name_crc = (uint16_t) (fdb_calc_crc32(0, name, name_len) >> 16); + + for (i = 0; i < FDB_KV_CACHE_TABLE_SIZE; i++) { + if ((db->kv_cache_table[i].addr != FDB_DATA_UNUSED) && (db->kv_cache_table[i].name_crc == name_crc)) { + char saved_name[FDB_KV_NAME_MAX]; + /* read the KV name in flash */ + _fdb_flash_read((fdb_db_t)db, db->kv_cache_table[i].addr + KV_HDR_DATA_SIZE, (uint32_t *) saved_name, FDB_KV_NAME_MAX); + if (!strncmp(name, saved_name, name_len)) { + *addr = db->kv_cache_table[i].addr; + if (db->kv_cache_table[i].active >= 0xFFFF - FDB_KV_CACHE_TABLE_SIZE) { + db->kv_cache_table[i].active = 0xFFFF; + } else { + db->kv_cache_table[i].active += FDB_KV_CACHE_TABLE_SIZE; + } + return true; + } + } + } + + return false; +} +#endif /* FDB_KV_USING_CACHE */ + +/* + * find the next KV address by magic word on the flash + */ +static uint32_t find_next_kv_addr(fdb_kvdb_t db, uint32_t start, uint32_t end) +{ + uint8_t buf[32]; + uint32_t start_bak = start, i; + uint32_t magic; + +#ifdef FDB_KV_USING_CACHE + uint32_t empty_kv; + + if (get_sector_from_cache(db, FDB_ALIGN_DOWN(start, db_sec_size(db)), &empty_kv) && start == empty_kv) { + return FAILED_ADDR; + } +#endif /* FDB_KV_USING_CACHE */ + + for (; start < end; start += (sizeof(buf) - sizeof(uint32_t))) { + _fdb_flash_read((fdb_db_t)db, start, (uint32_t *) buf, sizeof(buf)); + for (i = 0; i < sizeof(buf) - sizeof(uint32_t) && start + i < end; i++) { +#ifndef FDB_BIG_ENDIAN /* Little Endian Order */ + magic = buf[i] + (buf[i + 1] << 8) + (buf[i + 2] << 16) + (buf[i + 3] << 24); +#else /* Big Endian Order */ + magic = buf[i + 3] + (buf[i + 2] << 8) + (buf[i + 1] << 16) + (buf[i] << 24); +#endif + if (magic == KV_MAGIC_WORD && (start + i - KV_MAGIC_OFFSET) >= start_bak) { + return start + i - KV_MAGIC_OFFSET; + } + } + } + + return FAILED_ADDR; +} + +static uint32_t get_next_kv_addr(fdb_kvdb_t db, kv_sec_info_t sector, fdb_kv_t pre_kv) +{ + uint32_t addr = FAILED_ADDR; + + if (sector->status.store == FDB_SECTOR_STORE_EMPTY) { + return FAILED_ADDR; + } + + if (pre_kv->addr.start == FAILED_ADDR) { + /* the first KV address */ + addr = sector->addr + SECTOR_HDR_DATA_SIZE; + } else { + if (pre_kv->addr.start <= sector->addr + db_sec_size(db)) { + if (pre_kv->crc_is_ok) { + addr = pre_kv->addr.start + pre_kv->len; + } else { + /* when pre_kv CRC check failed, maybe the flash has error data + * find_next_kv_addr after pre_kv address */ + addr = pre_kv->addr.start + FDB_WG_ALIGN(1); + } + /* check and find next KV address */ + addr = find_next_kv_addr(db, addr, sector->addr + db_sec_size(db) - SECTOR_HDR_DATA_SIZE); + + if (addr > sector->addr + db_sec_size(db) || pre_kv->len == 0) { + //TODO ģʽ + return FAILED_ADDR; + } + } else { + /* no KV */ + return FAILED_ADDR; + } + } + + return addr; +} + +static fdb_err_t read_kv(fdb_kvdb_t db, fdb_kv_t kv) +{ + struct kv_hdr_data kv_hdr; + uint8_t buf[32]; + uint32_t calc_crc32 = 0, crc_data_len, kv_name_addr; + fdb_err_t result = FDB_NO_ERR; + size_t len, size; + /* read KV header raw data */ + _fdb_flash_read((fdb_db_t)db, kv->addr.start, (uint32_t *)&kv_hdr, sizeof(struct kv_hdr_data)); + kv->status = (fdb_kv_status_t) _fdb_get_status(kv_hdr.status_table, FDB_KV_STATUS_NUM); + kv->len = kv_hdr.len; + + if (kv->len == ~0UL || kv->len > db_part_size(db) || kv->len < KV_NAME_LEN_OFFSET) { + /* the KV length was not write, so reserved the info for current KV */ + kv->len = KV_HDR_DATA_SIZE; + if (kv->status != FDB_KV_ERR_HDR) { + kv->status = FDB_KV_ERR_HDR; + FDB_DEBUG("Error: The KV @0x%08lX length has an error.\n", kv->addr.start); + _fdb_write_status((fdb_db_t)db, kv->addr.start, kv_hdr.status_table, FDB_KV_STATUS_NUM, FDB_KV_ERR_HDR); + } + kv->crc_is_ok = false; + return FDB_READ_ERR; + } else if (kv->len > db_sec_size(db) - SECTOR_HDR_DATA_SIZE && kv->len < db_part_size(db)) { + //TODO ģʽд볤ûд + FDB_ASSERT(0); + } + + /* CRC32 data len(header.name_len + header.value_len + name + value) */ + crc_data_len = kv->len - KV_NAME_LEN_OFFSET; + /* calculate the CRC32 value */ + for (len = 0, size = 0; len < crc_data_len; len += size) { + if (len + sizeof(buf) < crc_data_len) { + size = sizeof(buf); + } else { + size = crc_data_len - len; + } + + _fdb_flash_read((fdb_db_t)db, kv->addr.start + KV_NAME_LEN_OFFSET + len, (uint32_t *) buf, FDB_WG_ALIGN(size)); + calc_crc32 = fdb_calc_crc32(calc_crc32, buf, size); + } + /* check CRC32 */ + if (calc_crc32 != kv_hdr.crc32) { + kv->crc_is_ok = false; + result = FDB_READ_ERR; + } else { + kv->crc_is_ok = true; + /* the name is behind aligned KV header */ + kv_name_addr = kv->addr.start + KV_HDR_DATA_SIZE; + _fdb_flash_read((fdb_db_t)db, kv_name_addr, (uint32_t *) kv->name, FDB_WG_ALIGN(kv_hdr.name_len)); + /* the value is behind aligned name */ + kv->addr.value = kv_name_addr + FDB_WG_ALIGN(kv_hdr.name_len); + kv->value_len = kv_hdr.value_len; + kv->name_len = kv_hdr.name_len; + } + + return result; +} + +static fdb_err_t read_sector_info(fdb_kvdb_t db, uint32_t addr, kv_sec_info_t sector, bool traversal) +{ + fdb_err_t result = FDB_NO_ERR; + struct sector_hdr_data sec_hdr; + + FDB_ASSERT(addr % db_sec_size(db) == 0); + FDB_ASSERT(sector); + + /* read sector header raw data */ + _fdb_flash_read((fdb_db_t)db, addr, (uint32_t *)&sec_hdr, sizeof(struct sector_hdr_data)); + + sector->addr = addr; + sector->magic = sec_hdr.magic; + /* check magic word */ + if (sector->magic != SECTOR_MAGIC_WORD) { + sector->check_ok = false; + sector->combined = SECTOR_NOT_COMBINED; + return FDB_INIT_FAILED; + } + sector->check_ok = true; + /* get other sector info */ + sector->combined = sec_hdr.combined; + sector->status.store = (fdb_sector_store_status_t) _fdb_get_status(sec_hdr.status_table.store, FDB_SECTOR_STORE_STATUS_NUM); + sector->status.dirty = (fdb_sector_dirty_status_t) _fdb_get_status(sec_hdr.status_table.dirty, FDB_SECTOR_DIRTY_STATUS_NUM); + /* traversal all KV and calculate the remain space size */ + if (traversal) { + sector->remain = 0; + sector->empty_kv = sector->addr + SECTOR_HDR_DATA_SIZE; + if (sector->status.store == FDB_SECTOR_STORE_EMPTY) { + sector->remain = db_sec_size(db) - SECTOR_HDR_DATA_SIZE; + } else if (sector->status.store == FDB_SECTOR_STORE_USING) { + struct fdb_kv kv_obj; + +#ifdef FDB_KV_USING_CACHE + if (get_sector_from_cache(db, addr, §or->empty_kv)) { + sector->remain = db_sec_size(db) - (sector->empty_kv - sector->addr); + return result; + } +#endif /* FDB_KV_USING_CACHE */ + + sector->remain = db_sec_size(db) - SECTOR_HDR_DATA_SIZE; + kv_obj.addr.start = sector->addr + SECTOR_HDR_DATA_SIZE; + do { + + read_kv(db, &kv_obj); + if (!kv_obj.crc_is_ok) { + if (kv_obj.status != FDB_KV_PRE_WRITE && kv_obj.status != FDB_KV_ERR_HDR) { + FDB_INFO("Error: The KV (@0x%08lX) CRC32 check failed!\n", kv_obj.addr.start); + sector->remain = 0; + result = FDB_READ_ERR; + break; + } + } + sector->empty_kv += kv_obj.len; + sector->remain -= kv_obj.len; + } while ((kv_obj.addr.start = get_next_kv_addr(db, sector, &kv_obj)) != FAILED_ADDR); + /* check the empty KV address by read continue 0xFF on flash */ + { + uint32_t ff_addr; + + ff_addr = _fdb_continue_ff_addr((fdb_db_t)db, sector->empty_kv, sector->addr + db_sec_size(db)); + /* check the flash data is clean */ + if (sector->empty_kv != ff_addr) { + /* update the sector information */ + sector->empty_kv = ff_addr; + sector->remain = db_sec_size(db) - (ff_addr - sector->addr); + } + } + +#ifdef FDB_KV_USING_CACHE + update_sector_cache(db, sector->addr, sector->empty_kv); +#endif + } + } + + return result; +} + +static uint32_t get_next_sector_addr(fdb_kvdb_t db, kv_sec_info_t pre_sec) +{ + uint32_t next_addr; + + if (pre_sec->addr == FAILED_ADDR) { + /* the next sector is on the top of the partition */ + return 0; + } else { + /* check KV sector combined */ + if (pre_sec->combined == SECTOR_NOT_COMBINED) { + next_addr = pre_sec->addr + db_sec_size(db); + } else { + next_addr = pre_sec->addr + pre_sec->combined * db_sec_size(db); + } + /* check range */ + if (next_addr < db_part_size(db)) { + return next_addr; + } else { + /* no sector */ + return FAILED_ADDR; + } + } +} + +static void kv_iterator(fdb_kvdb_t db, fdb_kv_t kv, void *arg1, void *arg2, + bool (*callback)(fdb_kv_t kv, void *arg1, void *arg2)) +{ + struct kvdb_sec_info sector; + uint32_t sec_addr; + + sec_addr = 0; + /* search all sectors */ + do { + if (read_sector_info(db, sec_addr, §or, false) != FDB_NO_ERR) { + continue; + } + if (callback == NULL) { + continue; + } + /* sector has KV */ + if (sector.status.store == FDB_SECTOR_STORE_USING || sector.status.store == FDB_SECTOR_STORE_FULL) { + kv->addr.start = sector.addr + SECTOR_HDR_DATA_SIZE; + /* search all KV */ + do { + read_kv(db, kv); + /* iterator is interrupted when callback return true */ + if (callback(kv, arg1, arg2)) { + return; + } + } while ((kv->addr.start = get_next_kv_addr(db, §or, kv)) != FAILED_ADDR); + } + } while ((sec_addr = get_next_sector_addr(db, §or)) != FAILED_ADDR); +} + +static bool find_kv_cb(fdb_kv_t kv, void *arg1, void *arg2) +{ + const char *key = arg1; + bool *find_ok = arg2; + size_t key_len = strlen(key); + + if (key_len != kv->name_len) { + return false; + } + /* check KV */ + if (kv->crc_is_ok && kv->status == FDB_KV_WRITE && !strncmp(kv->name, key, key_len)) { + *find_ok = true; + return true; + } + return false; +} + +static bool find_kv_no_cache(fdb_kvdb_t db, const char *key, fdb_kv_t kv) +{ + bool find_ok = false; + + kv_iterator(db, kv, (void *)key, &find_ok, find_kv_cb); + + return find_ok; +} + +static bool find_kv(fdb_kvdb_t db, const char *key, fdb_kv_t kv) +{ + bool find_ok = false; + +#ifdef FDB_KV_USING_CACHE + size_t key_len = strlen(key); + + if (get_kv_from_cache(db, key, key_len, &kv->addr.start)) { + read_kv(db, kv); + return true; + } +#endif /* FDB_KV_USING_CACHE */ + + find_ok = find_kv_no_cache(db, key, kv); + +#ifdef FDB_KV_USING_CACHE + if (find_ok) { + update_kv_cache(db, key, key_len, kv->addr.start); + } +#endif /* FDB_KV_USING_CACHE */ + + return find_ok; +} + +static bool fdb_is_str(uint8_t *value, size_t len) +{ +#define __is_print(ch) ((unsigned int)((ch) - ' ') < 127u - ' ') + size_t i; + + for (i = 0; i < len; i++) { + if (!__is_print(value[i])) { + return false; + } + } + return true; +} + +static size_t get_kv(fdb_kvdb_t db, const char *key, void *value_buf, size_t buf_len, size_t *value_len) +{ + struct fdb_kv kv; + size_t read_len = 0; + + if (find_kv(db, key, &kv)) { + if (value_len) { + *value_len = kv.value_len; + } + if (buf_len > kv.value_len) { + read_len = kv.value_len; + } else { + read_len = buf_len; + } + if (value_buf){ + _fdb_flash_read((fdb_db_t)db, kv.addr.value, (uint32_t *) value_buf, read_len); + } + } else if (value_len) { + *value_len = 0; + } + + return read_len; +} + +/** + * Get a KV object by key name + * + * @param db database object + * @param key KV name + * @param kv KV object + * + * @return KV object when is not NULL + */ +fdb_kv_t fdb_kv_get_obj(fdb_kvdb_t db, const char *key, fdb_kv_t kv) +{ + bool find_ok = false; + + if (!db_init_ok(db)) { + FDB_INFO("Error: KV (%s) isn't initialize OK.\n", db_name(db)); + return 0; + } + + /* lock the KV cache */ + db_lock(db); + + find_ok = find_kv(db, key, kv); + + /* unlock the KV cache */ + db_unlock(db); + + return find_ok ? kv : NULL; +} + +/** + * Convert the KV object to blob object + * + * @param kv KV object + * @param blob blob object + * + * @return new blob object + */ +fdb_blob_t fdb_kv_to_blob(fdb_kv_t kv, fdb_blob_t blob) +{ + blob->saved.addr = kv->addr.start; + blob->saved.addr = kv->addr.value; + blob->saved.len = kv->value_len; + + return blob; +} + +/** + * Get a blob KV value by key name. + * + * @param db database object + * @param key KV name + * @param blob blob object + * + * @return the actually get size on successful + */ +size_t fdb_kv_get_blob(fdb_kvdb_t db, const char *key, fdb_blob_t blob) +{ + size_t read_len = 0; + + if (!db_init_ok(db)) { + FDB_INFO("Error: KV (%s) isn't initialize OK.\n", db_name(db)); + return 0; + } + + /* lock the KV cache */ + db_lock(db); + + read_len = get_kv(db, key, blob->buf, blob->size, &blob->saved.len); + + /* unlock the KV cache */ + db_unlock(db); + + return read_len; +} + +/** + * Get an KV value by key name. + * + * @note this function is NOT supported reentrant + * @note this function is DEPRECATED + * + * @param db database object + * @param key KV name + * + * @return value + */ +char *fdb_kv_get(fdb_kvdb_t db, const char *key) +{ + static char value[FDB_STR_KV_VALUE_MAX_SIZE + 1]; + size_t get_size; + struct fdb_blob blob; + + if ((get_size = fdb_kv_get_blob(db, key, fdb_blob_make(&blob, value, FDB_STR_KV_VALUE_MAX_SIZE))) > 0) { + /* the return value must be string */ + if (fdb_is_str((uint8_t *)value, get_size)) { + value[get_size] = '\0'; + return value; + } else if (blob.saved.len > FDB_STR_KV_VALUE_MAX_SIZE) { + FDB_INFO("Warning: The default string KV value buffer length (%d) is too less (%d).\n", FDB_STR_KV_VALUE_MAX_SIZE, + blob.saved.len); + } else { + FDB_INFO("Warning: The KV value isn't string. Could not be returned\n"); + return NULL; + } + } + + return NULL; +} + +static fdb_err_t write_kv_hdr(fdb_kvdb_t db, uint32_t addr, kv_hdr_data_t kv_hdr) +{ + fdb_err_t result = FDB_NO_ERR; + /* write the status will by write granularity */ + result = _fdb_write_status((fdb_db_t)db, addr, kv_hdr->status_table, FDB_KV_STATUS_NUM, FDB_KV_PRE_WRITE); + if (result != FDB_NO_ERR) { + return result; + } + /* write other header data */ + result = _fdb_flash_write((fdb_db_t)db, addr + KV_MAGIC_OFFSET, &kv_hdr->magic, sizeof(struct kv_hdr_data) - KV_MAGIC_OFFSET); + + return result; +} + +static fdb_err_t format_sector(fdb_kvdb_t db, uint32_t addr, uint32_t combined_value) +{ + fdb_err_t result = FDB_NO_ERR; + struct sector_hdr_data sec_hdr; + + FDB_ASSERT(addr % db_sec_size(db) == 0); + + result = _fdb_flash_erase((fdb_db_t)db, addr, db_sec_size(db)); + if (result == FDB_NO_ERR) { + /* initialize the header data */ + memset(&sec_hdr, 0xFF, sizeof(struct sector_hdr_data)); + _fdb_set_status(sec_hdr.status_table.store, FDB_SECTOR_STORE_STATUS_NUM, FDB_SECTOR_STORE_EMPTY); + _fdb_set_status(sec_hdr.status_table.dirty, FDB_SECTOR_DIRTY_STATUS_NUM, FDB_SECTOR_DIRTY_FALSE); + sec_hdr.magic = SECTOR_MAGIC_WORD; + sec_hdr.combined = combined_value; + sec_hdr.reserved = 0xFFFFFFFF; + /* save the header */ + result = _fdb_flash_write((fdb_db_t)db, addr, (uint32_t *)&sec_hdr, sizeof(struct sector_hdr_data)); + +#ifdef FDB_KV_USING_CACHE + /* delete the sector cache */ + update_sector_cache(db, addr, addr + db_sec_size(db)); +#endif /* FDB_KV_USING_CACHE */ + } + + return result; +} + +static fdb_err_t update_sec_status(fdb_kvdb_t db, kv_sec_info_t sector, size_t new_kv_len, bool *is_full) +{ + uint8_t status_table[FDB_STORE_STATUS_TABLE_SIZE]; + fdb_err_t result = FDB_NO_ERR; + /* change the current sector status */ + if (sector->status.store == FDB_SECTOR_STORE_EMPTY) { + /* change the sector status to using */ + result = _fdb_write_status((fdb_db_t)db, sector->addr, status_table, FDB_SECTOR_STORE_STATUS_NUM, FDB_SECTOR_STORE_USING); + } else if (sector->status.store == FDB_SECTOR_STORE_USING) { + /* check remain size */ + if (sector->remain < FDB_SEC_REMAIN_THRESHOLD || sector->remain - new_kv_len < FDB_SEC_REMAIN_THRESHOLD) { + /* change the sector status to full */ + result = _fdb_write_status((fdb_db_t)db, sector->addr, status_table, FDB_SECTOR_STORE_STATUS_NUM, FDB_SECTOR_STORE_FULL); + +#ifdef FDB_KV_USING_CACHE + /* delete the sector cache */ + update_sector_cache(db, sector->addr, sector->addr + db_sec_size(db)); +#endif /* FDB_KV_USING_CACHE */ + + if (is_full) { + *is_full = true; + } + } else if (is_full) { + *is_full = false; + } + } + + return result; +} + +static void sector_iterator(fdb_kvdb_t db, kv_sec_info_t sector, fdb_sector_store_status_t status, void *arg1, void *arg2, + bool (*callback)(kv_sec_info_t sector, void *arg1, void *arg2), bool traversal_kv) +{ + uint32_t sec_addr; + + /* search all sectors */ + sec_addr = 0; + do { + read_sector_info(db, sec_addr, sector, false); + if (status == FDB_SECTOR_STORE_UNUSED || status == sector->status.store) { + if (traversal_kv) { + read_sector_info(db, sec_addr, sector, true); + } + /* iterator is interrupted when callback return true */ + if (callback && callback(sector, arg1, arg2)) { + return; + } + } + } while ((sec_addr = get_next_sector_addr(db, sector)) != FAILED_ADDR); +} + +static bool sector_statistics_cb(kv_sec_info_t sector, void *arg1, void *arg2) +{ + size_t *empty_sector = arg1, *using_sector = arg2; + + if (sector->check_ok && sector->status.store == FDB_SECTOR_STORE_EMPTY) { + (*empty_sector)++; + } else if (sector->check_ok && sector->status.store == FDB_SECTOR_STORE_USING) { + (*using_sector)++; + } + + return false; +} + +static bool alloc_kv_cb(kv_sec_info_t sector, void *arg1, void *arg2) +{ + struct alloc_kv_cb_args *arg = arg1; + + /* 1. sector has space + * 2. the NO dirty sector + * 3. the dirty sector only when the gc_request is false */ + if (sector->check_ok && sector->remain > arg->kv_size + && ((sector->status.dirty == FDB_SECTOR_DIRTY_FALSE) + || (sector->status.dirty == FDB_SECTOR_DIRTY_TRUE && !arg->db->gc_request))) { + *(arg->empty_kv) = sector->empty_kv; + return true; + } + + return false; +} + +static uint32_t alloc_kv(fdb_kvdb_t db, kv_sec_info_t sector, size_t kv_size) +{ + uint32_t empty_kv = FAILED_ADDR; + size_t empty_sector = 0, using_sector = 0; + struct alloc_kv_cb_args arg = {db, kv_size, &empty_kv}; + + /* sector status statistics */ + sector_iterator(db, sector, FDB_SECTOR_STORE_UNUSED, &empty_sector, &using_sector, sector_statistics_cb, false); + if (using_sector > 0) { + /* alloc the KV from the using status sector first */ + sector_iterator(db, sector, FDB_SECTOR_STORE_USING, &arg, NULL, alloc_kv_cb, true); + } + if (empty_sector > 0 && empty_kv == FAILED_ADDR) { + if (empty_sector > FDB_GC_EMPTY_SEC_THRESHOLD || db->gc_request) { + sector_iterator(db, sector, FDB_SECTOR_STORE_EMPTY, &arg, NULL, alloc_kv_cb, true); + } else { + /* no space for new KV now will GC and retry */ + FDB_DEBUG("Trigger a GC check after alloc KV failed.\n"); + db->gc_request = true; + } + } + + return empty_kv; +} + +static fdb_err_t del_kv(fdb_kvdb_t db, const char *key, fdb_kv_t old_kv, bool complete_del) +{ + fdb_err_t result = FDB_NO_ERR; + uint32_t dirty_status_addr; + static bool last_is_complete_del = false; + +#if (KV_STATUS_TABLE_SIZE >= FDB_DIRTY_STATUS_TABLE_SIZE) + uint8_t status_table[KV_STATUS_TABLE_SIZE]; +#else + uint8_t status_table[DIRTY_STATUS_TABLE_SIZE]; +#endif + + /* need find KV */ + if (!old_kv) { + struct fdb_kv kv; + /* find KV */ + if (find_kv(db, key, &kv)) { + old_kv = &kv; + } else { + FDB_DEBUG("Not found '%s' in KV.\n", key); + return FDB_KV_NAME_ERR; + } + } + /* change and save the new status */ + if (!complete_del) { + result = _fdb_write_status((fdb_db_t)db, old_kv->addr.start, status_table, FDB_KV_STATUS_NUM, FDB_KV_PRE_DELETE); + last_is_complete_del = true; + } else { + result = _fdb_write_status((fdb_db_t)db, old_kv->addr.start, status_table, FDB_KV_STATUS_NUM, FDB_KV_DELETED); + + if (!last_is_complete_del && result == FDB_NO_ERR) { +#ifdef FDB_KV_USING_CACHE + /* delete the KV in flash and cache */ + if (key != NULL) { + /* when using del_kv(db, key, NULL, true) or del_kv(db, key, kv, true) in fdb_del_kv(db, ) and set_kv(db, ) */ + update_kv_cache(db, key, strlen(key), FDB_DATA_UNUSED); + } else if (old_kv != NULL) { + /* when using del_kv(db, NULL, kv, true) in move_kv(db, ) */ + update_kv_cache(db, old_kv->name, old_kv->name_len, FDB_DATA_UNUSED); + } +#endif /* FDB_KV_USING_CACHE */ + } + + last_is_complete_del = false; + } + + dirty_status_addr = FDB_ALIGN_DOWN(old_kv->addr.start, db_sec_size(db)) + SECTOR_DIRTY_OFFSET; + /* read and change the sector dirty status */ + if (result == FDB_NO_ERR + && _fdb_read_status((fdb_db_t)db, dirty_status_addr, status_table, FDB_SECTOR_DIRTY_STATUS_NUM) == FDB_SECTOR_DIRTY_FALSE) { + result = _fdb_write_status((fdb_db_t)db, dirty_status_addr, status_table, FDB_SECTOR_DIRTY_STATUS_NUM, FDB_SECTOR_DIRTY_TRUE); + } + + return result; +} + +/* + * move the KV to new space + */ +static fdb_err_t move_kv(fdb_kvdb_t db, fdb_kv_t kv) +{ + fdb_err_t result = FDB_NO_ERR; + uint8_t status_table[KV_STATUS_TABLE_SIZE]; + uint32_t kv_addr; + struct kvdb_sec_info sector; + + /* prepare to delete the current KV */ + if (kv->status == FDB_KV_WRITE) { + del_kv(db, NULL, kv, false); + } + + if ((kv_addr = alloc_kv(db, §or, kv->len)) != FAILED_ADDR) { + if (db->in_recovery_check) { + struct fdb_kv kv_bak; + char name[FDB_KV_NAME_MAX + 1] = { 0 }; + strncpy(name, kv->name, kv->name_len); + /* check the KV in flash is already create success */ + if (find_kv_no_cache(db, name, &kv_bak)) { + /* already create success, don't need to duplicate */ + result = FDB_NO_ERR; + goto __exit; + } + } + } else { + return FDB_KV_FULL; + } + /* start move the KV */ + { + uint8_t buf[32]; + size_t len, size, kv_len = kv->len; + + /* update the new KV sector status first */ + update_sec_status(db, §or, kv->len, NULL); + + _fdb_write_status((fdb_db_t)db, kv_addr, status_table, FDB_KV_STATUS_NUM, FDB_KV_PRE_WRITE); + kv_len -= KV_MAGIC_OFFSET; + for (len = 0, size = 0; len < kv_len; len += size) { + if (len + sizeof(buf) < kv_len) { + size = sizeof(buf); + } else { + size = kv_len - len; + } + _fdb_flash_read((fdb_db_t)db, kv->addr.start + KV_MAGIC_OFFSET + len, (uint32_t *) buf, FDB_WG_ALIGN(size)); + result = _fdb_flash_write((fdb_db_t)db, kv_addr + KV_MAGIC_OFFSET + len, (uint32_t *) buf, size); + } + _fdb_write_status((fdb_db_t)db, kv_addr, status_table, FDB_KV_STATUS_NUM, FDB_KV_WRITE); + +#ifdef FDB_KV_USING_CACHE + update_sector_cache(db, FDB_ALIGN_DOWN(kv_addr, db_sec_size(db)), + kv_addr + KV_HDR_DATA_SIZE + FDB_WG_ALIGN(kv->name_len) + FDB_WG_ALIGN(kv->value_len)); + update_kv_cache(db, kv->name, kv->name_len, kv_addr); +#endif /* FDB_KV_USING_CACHE */ + } + + FDB_DEBUG("Moved the KV (%.*s) from 0x%08lX to 0x%08lX.\n", kv->name_len, kv->name, kv->addr.start, kv_addr); + +__exit: + del_kv(db, NULL, kv, true); + + return result; +} + +static uint32_t new_kv(fdb_kvdb_t db, kv_sec_info_t sector, size_t kv_size) +{ + bool already_gc = false; + uint32_t empty_kv = FAILED_ADDR; + +__retry: + + if ((empty_kv = alloc_kv(db, sector, kv_size)) == FAILED_ADDR && db->gc_request && !already_gc) { + FDB_DEBUG("Warning: Alloc an KV (size %d) failed when new KV. Now will GC then retry.\n", kv_size); + gc_collect(db); + already_gc = true; + goto __retry; + } + + return empty_kv; +} + +static uint32_t new_kv_ex(fdb_kvdb_t db, kv_sec_info_t sector, size_t key_len, size_t buf_len) +{ + size_t kv_len = KV_HDR_DATA_SIZE + FDB_WG_ALIGN(key_len) + FDB_WG_ALIGN(buf_len); + + return new_kv(db, sector, kv_len); +} + +static bool gc_check_cb(kv_sec_info_t sector, void *arg1, void *arg2) +{ + size_t *empty_sec = arg1; + + if (sector->check_ok) { + *empty_sec = *empty_sec + 1; + } + + return false; + +} + +static bool do_gc(kv_sec_info_t sector, void *arg1, void *arg2) +{ + struct fdb_kv kv; + fdb_kvdb_t db = arg1; + + if (sector->check_ok && (sector->status.dirty == FDB_SECTOR_DIRTY_TRUE || sector->status.dirty == FDB_SECTOR_DIRTY_GC)) { + uint8_t status_table[FDB_DIRTY_STATUS_TABLE_SIZE]; + /* change the sector status to GC */ + _fdb_write_status((fdb_db_t)db, sector->addr + SECTOR_DIRTY_OFFSET, status_table, FDB_SECTOR_DIRTY_STATUS_NUM, FDB_SECTOR_DIRTY_GC); + /* search all KV */ + kv.addr.start = sector->addr + SECTOR_HDR_DATA_SIZE; + do { + read_kv(db, &kv); + if (kv.crc_is_ok && (kv.status == FDB_KV_WRITE || kv.status == FDB_KV_PRE_DELETE)) { + /* move the KV to new space */ + if (move_kv(db, &kv) != FDB_NO_ERR) { + FDB_DEBUG("Error: Moved the KV (%.*s) for GC failed.\n", kv.name_len, kv.name); + } + } + } while ((kv.addr.start = get_next_kv_addr(db, sector, &kv)) != FAILED_ADDR); + format_sector(db, sector->addr, SECTOR_NOT_COMBINED); + FDB_DEBUG("Collect a sector @0x%08lX\n", sector->addr); + } + + return false; +} + +/* + * The GC will be triggered on the following scene: + * 1. alloc an KV when the flash not has enough space + * 2. write an KV then the flash not has enough space + */ +static void gc_collect(fdb_kvdb_t db) +{ + struct kvdb_sec_info sector; + size_t empty_sec = 0; + + /* GC check the empty sector number */ + sector_iterator(db, §or, FDB_SECTOR_STORE_EMPTY, &empty_sec, NULL, gc_check_cb, false); + + /* do GC collect */ + FDB_DEBUG("The remain empty sector is %d, GC threshold is %d.\n", empty_sec, FDB_GC_EMPTY_SEC_THRESHOLD); + if (empty_sec <= FDB_GC_EMPTY_SEC_THRESHOLD) { + sector_iterator(db, §or, FDB_SECTOR_STORE_UNUSED, db, NULL, do_gc, false); + } + + db->gc_request = false; +} + +static fdb_err_t align_write(fdb_kvdb_t db, uint32_t addr, const uint32_t *buf, size_t size) +{ + fdb_err_t result = FDB_NO_ERR; + size_t align_remain; + +#if (FDB_WRITE_GRAN / 8 > 0) + uint8_t align_data[FDB_WRITE_GRAN / 8]; + size_t align_data_size = sizeof(align_data); +#else + /* For compatibility with C89 */ + uint8_t align_data_u8, *align_data = &align_data_u8; + size_t align_data_size = 1; +#endif + + memset(align_data, 0xFF, align_data_size); + result = _fdb_flash_write((fdb_db_t)db, addr, buf, FDB_WG_ALIGN_DOWN(size)); + + align_remain = size - FDB_WG_ALIGN_DOWN(size); + if (result == FDB_NO_ERR && align_remain) { + memcpy(align_data, (uint8_t *)buf + FDB_WG_ALIGN_DOWN(size), align_remain); + result = _fdb_flash_write((fdb_db_t)db, addr + FDB_WG_ALIGN_DOWN(size), (uint32_t *) align_data, align_data_size); + } + + return result; +} + +static fdb_err_t create_kv_blob(fdb_kvdb_t db, kv_sec_info_t sector, const char *key, const void *value, size_t len) +{ + fdb_err_t result = FDB_NO_ERR; + struct kv_hdr_data kv_hdr; + bool is_full = false; + uint32_t kv_addr = sector->empty_kv; + + if (strlen(key) > FDB_KV_NAME_MAX) { + FDB_INFO("Error: The KV name length is more than %d\n", FDB_KV_NAME_MAX); + return FDB_KV_NAME_ERR; + } + + memset(&kv_hdr, 0xFF, sizeof(struct kv_hdr_data)); + kv_hdr.magic = KV_MAGIC_WORD; + kv_hdr.name_len = strlen(key); + kv_hdr.value_len = len; + kv_hdr.len = KV_HDR_DATA_SIZE + FDB_WG_ALIGN(kv_hdr.name_len) + FDB_WG_ALIGN(kv_hdr.value_len); + + if (kv_hdr.len > db_sec_size(db) - SECTOR_HDR_DATA_SIZE) { + FDB_INFO("Error: The KV size is too big\n"); + return FDB_KV_FULL; + } + + if (kv_addr != FAILED_ADDR || (kv_addr = new_kv(db, sector, kv_hdr.len)) != FAILED_ADDR) { + size_t align_remain; + /* update the sector status */ + if (result == FDB_NO_ERR) { + result = update_sec_status(db, sector, kv_hdr.len, &is_full); + } + if (result == FDB_NO_ERR) { + uint8_t ff = 0xFF; + /* start calculate CRC32 */ + kv_hdr.crc32 = fdb_calc_crc32(0, &kv_hdr.name_len, KV_HDR_DATA_SIZE - KV_NAME_LEN_OFFSET); + kv_hdr.crc32 = fdb_calc_crc32(kv_hdr.crc32, key, kv_hdr.name_len); + align_remain = FDB_WG_ALIGN(kv_hdr.name_len) - kv_hdr.name_len; + while (align_remain--) { + kv_hdr.crc32 = fdb_calc_crc32(kv_hdr.crc32, &ff, 1); + } + kv_hdr.crc32 = fdb_calc_crc32(kv_hdr.crc32, value, kv_hdr.value_len); + align_remain = FDB_WG_ALIGN(kv_hdr.value_len) - kv_hdr.value_len; + while (align_remain--) { + kv_hdr.crc32 = fdb_calc_crc32(kv_hdr.crc32, &ff, 1); + } + /* write KV header data */ + result = write_kv_hdr(db, kv_addr, &kv_hdr); + + } + /* write key name */ + if (result == FDB_NO_ERR) { + result = align_write(db, kv_addr + KV_HDR_DATA_SIZE, (uint32_t *) key, kv_hdr.name_len); + +#ifdef FDB_KV_USING_CACHE + if (!is_full) { + update_sector_cache(db, sector->addr, + kv_addr + KV_HDR_DATA_SIZE + FDB_WG_ALIGN(kv_hdr.name_len) + FDB_WG_ALIGN(kv_hdr.value_len)); + } + update_kv_cache(db, key, kv_hdr.name_len, kv_addr); +#endif /* FDB_KV_USING_CACHE */ + } + /* write value */ + if (result == FDB_NO_ERR) { + result = align_write(db, kv_addr + KV_HDR_DATA_SIZE + FDB_WG_ALIGN(kv_hdr.name_len), value, + kv_hdr.value_len); + } + /* change the KV status to KV_WRITE */ + if (result == FDB_NO_ERR) { + result = _fdb_write_status((fdb_db_t)db, kv_addr, kv_hdr.status_table, FDB_KV_STATUS_NUM, FDB_KV_WRITE); + } + /* trigger GC collect when current sector is full */ + if (result == FDB_NO_ERR && is_full) { + FDB_DEBUG("Trigger a GC check after created KV.\n"); + db->gc_request = true; + } + } else { + result = FDB_KV_FULL; + } + + return result; +} + +/** + * Delete an KV. + * + * @param db database object + * @param key KV name + * + * @return result + */ +fdb_err_t fdb_kv_del(fdb_kvdb_t db, const char *key) +{ + fdb_err_t result = FDB_NO_ERR; + + if (!db_init_ok(db)) { + FDB_INFO("Error: KV (%s) isn't initialize OK.\n", db_name(db)); + return FDB_INIT_FAILED; + } + + /* lock the KV cache */ + db_lock(db); + + result = del_kv(db, key, NULL, true); + + /* unlock the KV cache */ + db_unlock(db); + + return result; +} + +static fdb_err_t set_kv(fdb_kvdb_t db, const char *key, const void *value_buf, size_t buf_len) +{ + fdb_err_t result = FDB_NO_ERR; + static struct fdb_kv kv; + static struct kvdb_sec_info sector; + bool kv_is_found = false; + + if (value_buf == NULL) { + result = del_kv(db, key, NULL, true); + } else { + /* make sure the flash has enough space */ + if (new_kv_ex(db, §or, strlen(key), buf_len) == FAILED_ADDR) { + return FDB_KV_FULL; + } + kv_is_found = find_kv(db, key, &kv); + /* prepare to delete the old KV */ + if (kv_is_found) { + result = del_kv(db, key, &kv, false); + } + /* create the new KV */ + if (result == FDB_NO_ERR) { + result = create_kv_blob(db, §or, key, value_buf, buf_len); + } + /* delete the old KV */ + if (kv_is_found && result == FDB_NO_ERR) { + result = del_kv(db, key, &kv, true); + } + /* process the GC after set KV */ + if (db->gc_request) { + gc_collect(db); + } + } + + return result; +} + +/** + * Set a blob KV. If it blob value is NULL, delete it. + * If not find it in flash, then create it. + * + * @param db database object + * @param key KV name + * @param blob blob object + * + * @return result + */ +fdb_err_t fdb_kv_set_blob(fdb_kvdb_t db, const char *key, fdb_blob_t blob) +{ + fdb_err_t result = FDB_NO_ERR; + + if (!db_init_ok(db)) { + FDB_INFO("Error: KV (%s) isn't initialize OK.\n", db_name(db)); + return FDB_INIT_FAILED; + } + + /* lock the KV cache */ + db_lock(db); + + result = set_kv(db, key, blob->buf, blob->size); + + /* unlock the KV cache */ + db_unlock(db); + + return result; +} + +/** + * Set a string KV. If it value is NULL, delete it. + * If not find it in flash, then create it. + * + * @param db database object + * @param key KV name + * @param value KV value + * + * @return result + */ +fdb_err_t fdb_kv_set(fdb_kvdb_t db, const char *key, const char *value) +{ + struct fdb_blob blob; + + return fdb_kv_set_blob(db, key, fdb_blob_make(&blob, value, strlen(value))); +} + +/** + * recovery all KV to default. + * + * @param db database object + * @return result + */ +fdb_err_t fdb_kv_set_default(fdb_kvdb_t db) +{ + fdb_err_t result = FDB_NO_ERR; + uint32_t addr, i, value_len; + struct kvdb_sec_info sector; + + /* lock the KV cache */ + db_lock(db); + /* format all sectors */ + for (addr = 0; addr < db_part_size(db); addr += db_sec_size(db)) { + result = format_sector(db, addr, SECTOR_NOT_COMBINED); + if (result != FDB_NO_ERR) { + goto __exit; + } + } + /* create default KV */ + for (i = 0; i < db->default_kvs.num; i++) { + /* It seems to be a string when value length is 0. + * This mechanism is for compatibility with older versions (less then V4.0). */ + if (db->default_kvs.kvs[i].value_len == 0) { + value_len = strlen(db->default_kvs.kvs[i].value); + } else { + value_len = db->default_kvs.kvs[i].value_len; + } + sector.empty_kv = FAILED_ADDR; + create_kv_blob(db, §or, db->default_kvs.kvs[i].key, db->default_kvs.kvs[i].value, value_len); + if (result != FDB_NO_ERR) { + goto __exit; + } + } + +__exit: + /* unlock the KV cache */ + db_unlock(db); + + return result; +} + +static bool print_kv_cb(fdb_kv_t kv, void *arg1, void *arg2) +{ + bool value_is_str = true, print_value = false; + size_t *using_size = arg1; + fdb_kvdb_t db = arg2; + + if (kv->crc_is_ok) { + /* calculate the total using flash size */ + *using_size += kv->len; + /* check KV */ + if (kv->status == FDB_KV_WRITE) { + FDB_PRINT("%.*s=", kv->name_len, kv->name); + + if (kv->value_len < FDB_STR_KV_VALUE_MAX_SIZE ) { + uint8_t buf[32]; + size_t len, size; +__reload: + /* check the value is string */ + for (len = 0, size = 0; len < kv->value_len; len += size) { + if (len + sizeof(buf) < kv->value_len) { + size = sizeof(buf); + } else { + size = kv->value_len - len; + } + _fdb_flash_read((fdb_db_t)db, kv->addr.value + len, (uint32_t *) buf, FDB_WG_ALIGN(size)); + if (print_value) { + FDB_PRINT("%.*s", size, buf); + } else if (!fdb_is_str(buf, size)) { + value_is_str = false; + break; + } + } + } else { + value_is_str = false; + } + if (value_is_str && !print_value) { + print_value = true; + goto __reload; + } else if (!value_is_str) { + FDB_PRINT("blob @0x%08lX %lubytes", kv->addr.value, kv->value_len); + } + FDB_PRINT("\n"); + } + } + + return false; +} + + +/** + * Print all KV. + * + * @param db database object + */ +void fdb_kv_print(fdb_kvdb_t db) +{ + struct fdb_kv kv; + size_t using_size = 0; + + if (!db_init_ok(db)) { + FDB_INFO("Error: KV (%s) isn't initialize OK.\n", db_name(db)); + return; + } + + /* lock the KV cache */ + db_lock(db); + + kv_iterator(db, &kv, &using_size, db, print_kv_cb); + + FDB_PRINT("\nmode: next generation\n"); + FDB_PRINT("size: %lu/%lu bytes.\n", using_size + (SECTOR_NUM - FDB_GC_EMPTY_SEC_THRESHOLD) * SECTOR_HDR_DATA_SIZE, + db_part_size(db) - db_sec_size(db) * FDB_GC_EMPTY_SEC_THRESHOLD); + + /* unlock the KV cache */ + db_unlock(db); +} + +#ifdef FDB_KV_AUTO_UPDATE +/* + * Auto update KV to latest default when current setting version number is changed. + */ +static void kv_auto_update(fdb_kvdb_t db) +{ + size_t saved_ver_num, setting_ver_num = db->ver_num; + + if (get_kv(db, VER_NUM_KV_NAME, &saved_ver_num, sizeof(size_t), NULL) > 0) { + /* check version number */ + if (saved_ver_num != setting_ver_num) { + struct fdb_kv kv; + size_t i, value_len; + struct kvdb_sec_info sector; + FDB_DEBUG("Update the KV from version %d to %d.\n", saved_ver_num, setting_ver_num); + for (i = 0; i < db->default_kvs.num; i++) { + /* add a new KV when it's not found */ + if (!find_kv(db, db->default_kvs.kvs[i].key, &kv)) { + /* It seems to be a string when value length is 0. + * This mechanism is for compatibility with older versions (less then V4.0). */ + if (db->default_kvs.kvs[i].value_len == 0) { + value_len = strlen(db->default_kvs.kvs[i].value); + } else { + value_len = db->default_kvs.kvs[i].value_len; + } + sector.empty_kv = FAILED_ADDR; + create_kv_blob(db, §or, db->default_kvs.kvs[i].key, db->default_kvs.kvs[i].value, value_len); + } + } + } else { + /* version number not changed now return */ + return; + } + } + + set_kv(db, VER_NUM_KV_NAME, &setting_ver_num, sizeof(size_t)); +} +#endif /* FDB_KV_AUTO_UPDATE */ + +static bool check_sec_hdr_cb(kv_sec_info_t sector, void *arg1, void *arg2) +{ + if (!sector->check_ok) { + size_t *failed_count = arg1; + fdb_kvdb_t db = arg2; + + FDB_INFO("Warning: Sector header check failed. Format this sector (0x%08lX).\n", sector->addr); + (*failed_count) ++; + format_sector(db, sector->addr, SECTOR_NOT_COMBINED); + } + + return false; +} + +static bool check_and_recovery_gc_cb(kv_sec_info_t sector, void *arg1, void *arg2) +{ + fdb_kvdb_t db = arg1; + + if (sector->check_ok && sector->status.dirty == FDB_SECTOR_DIRTY_GC) { + /* make sure the GC request flag to true */ + db->gc_request = true; + /* resume the GC operate */ + gc_collect(db); + } + + return false; +} + +static bool check_and_recovery_kv_cb(fdb_kv_t kv, void *arg1, void *arg2) +{ + fdb_kvdb_t db = arg1; + + /* recovery the prepare deleted KV */ + if (kv->crc_is_ok && kv->status == FDB_KV_PRE_DELETE) { + FDB_INFO("Found an KV (%.*s) which has changed value failed. Now will recovery it.\n", kv->name_len, kv->name); + /* recovery the old KV */ + if (move_kv(db, kv) == FDB_NO_ERR) { + FDB_DEBUG("Recovery the KV successful.\n"); + } else { + FDB_DEBUG("Warning: Moved an KV (size %lu) failed when recovery. Now will GC then retry.\n", kv->len); + return true; + } + } else if (kv->status == FDB_KV_PRE_WRITE) { + uint8_t status_table[KV_STATUS_TABLE_SIZE]; + /* the KV has not write finish, change the status to error */ + //TODO 쳣״̬װͼ + _fdb_write_status((fdb_db_t)db, kv->addr.start, status_table, FDB_KV_STATUS_NUM, FDB_KV_ERR_HDR); + return true; + } + + return false; +} + +/** + * Check and load the flash KV. + * + * @return result + */ +fdb_err_t _fdb_kv_load(fdb_kvdb_t db) +{ + fdb_err_t result = FDB_NO_ERR; + struct fdb_kv kv; + struct kvdb_sec_info sector; + size_t check_failed_count = 0; + + db->in_recovery_check = true; + /* check all sector header */ + sector_iterator(db, §or, FDB_SECTOR_STORE_UNUSED, &check_failed_count, db, check_sec_hdr_cb, false); + /* all sector header check failed */ + if (check_failed_count == SECTOR_NUM) { + FDB_INFO("Warning: All sector header check failed. Set it to default.\n"); + fdb_kv_set_default(db); + } + + /* lock the KV cache */ + db_lock(db); + /* check all sector header for recovery GC */ + sector_iterator(db, §or, FDB_SECTOR_STORE_UNUSED, db, NULL, check_and_recovery_gc_cb, false); + +__retry: + /* check all KV for recovery */ + kv_iterator(db, &kv, db, NULL, check_and_recovery_kv_cb); + if (db->gc_request) { + gc_collect(db); + goto __retry; + } + + db->in_recovery_check = false; + + /* unlock the KV cache */ + db_unlock(db); + + return result; +} + +/** + * The KV database initialization. + * + * @param db database object + * @param name database name + * @param part_name partition name + * @param default_kv the default KV set @see fdb_default_kv + * @param user_data user data + * + * @return result + */ +fdb_err_t fdb_kvdb_init(fdb_kvdb_t db, const char *name, const char *part_name, struct fdb_default_kv *default_kv, + void *user_data) +{ + fdb_err_t result = FDB_NO_ERR; + +#ifdef FDB_KV_USING_CACHE + size_t i; +#endif + + FDB_ASSERT(default_kv); + /* must be aligned with write granularity */ + FDB_ASSERT((FDB_STR_KV_VALUE_MAX_SIZE * 8) % FDB_WRITE_GRAN == 0); + + result = _fdb_init_ex((fdb_db_t)db, name, part_name, FDB_DB_TYPE_KV, user_data); + + db->gc_request = false; + db->in_recovery_check = false; + db->default_kvs = *default_kv; + /* there is at least one empty sector for GC. */ + FDB_ASSERT((FDB_GC_EMPTY_SEC_THRESHOLD > 0 && FDB_GC_EMPTY_SEC_THRESHOLD < SECTOR_NUM)) + /* must be aligned with sector size */ + FDB_ASSERT(db_part_size(db) % db_sec_size(db) == 0); + /* must be has more than 2 sector */ + FDB_ASSERT(db_part_size(db) / db_sec_size(db) >= 2); + +#ifdef FDB_KV_USING_CACHE + for (i = 0; i < FDB_SECTOR_CACHE_TABLE_SIZE; i++) { + db->sector_cache_table[i].addr = FDB_DATA_UNUSED; + } + for (i = 0; i < FDB_KV_CACHE_TABLE_SIZE; i++) { + db->kv_cache_table[i].addr = FDB_DATA_UNUSED; + } +#endif /* FDB_KV_USING_CACHE */ + + + FDB_DEBUG("KV in partition %s, size is %u bytes.\n", ((fdb_db_t)db)->part->name, db_part_size(db)); + + result = _fdb_kv_load(db); + +#ifdef FDB_KV_AUTO_UPDATE + if (result == FDB_NO_ERR) { + kv_auto_update(db); + } +#endif + + _fdb_init_finish((fdb_db_t)db, result); + + return result; +} + +#endif /* defined(FDB_USING_KVDB) */ diff --git a/flashdb/src/fdb_tsdb.c b/flashdb/src/fdb_tsdb.c new file mode 100644 index 0000000..0518620 --- /dev/null +++ b/flashdb/src/fdb_tsdb.c @@ -0,0 +1,712 @@ +/* + * Copyright (c) 2020, Armink, + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * @brief TSDB feature. + * + * Time series log (like TSDB) feature implement source file. + * + * TSL is time series log, the TSDB saved many TSLs. + */ + +#include +#include +#include + +#define FDB_LOG_TAG "[tsl]" +/* rewrite log prefix */ +#undef FDB_LOG_PREFIX2 +#define FDB_LOG_PREFIX2() FDB_PRINT("[%s] ", db_name(db)) + +#if defined(FDB_USING_TSDB) + +/* magic word(`T`, `S`, `L`, `0`) */ +#define SECTOR_MAGIC_WORD 0x304C5354 + +#define TSL_STATUS_TABLE_SIZE FDB_STATUS_TABLE_SIZE(FDB_TSL_STATUS_NUM) + +#define SECTOR_HDR_DATA_SIZE (FDB_WG_ALIGN(sizeof(struct sector_hdr_data))) +#define LOG_IDX_DATA_SIZE (FDB_WG_ALIGN(sizeof(struct log_idx_data))) +#define LOG_IDX_TS_OFFSET ((unsigned long)(&((struct log_idx_data *)0)->time)) +#define SECTOR_MAGIC_OFFSET ((unsigned long)(&((struct sector_hdr_data *)0)->magic)) +#define SECTOR_START_TIME_OFFSET ((unsigned long)(&((struct sector_hdr_data *)0)->start_time)) +#define SECTOR_END0_TIME_OFFSET ((unsigned long)(&((struct sector_hdr_data *)0)->end_info[0].time)) +#define SECTOR_END0_IDX_OFFSET ((unsigned long)(&((struct sector_hdr_data *)0)->end_info[0].index)) +#define SECTOR_END0_STATUS_OFFSET ((unsigned long)(&((struct sector_hdr_data *)0)->end_info[0].status)) +#define SECTOR_END1_TIME_OFFSET ((unsigned long)(&((struct sector_hdr_data *)0)->end_info[1].time)) +#define SECTOR_END1_IDX_OFFSET ((unsigned long)(&((struct sector_hdr_data *)0)->end_info[1].index)) +#define SECTOR_END1_STATUS_OFFSET ((unsigned long)(&((struct sector_hdr_data *)0)->end_info[1].status)) + +/* the next address is get failed */ +#define FAILED_ADDR 0xFFFFFFFF + +#define db_name(db) (((fdb_db_t)db)->name) +#define db_init_ok(db) (((fdb_db_t)db)->init_ok) +#define db_sec_size(db) (((fdb_db_t)db)->sec_size) +#define db_part_size(db) (((fdb_db_t)db)->part->len) +#define db_lock(db) \ + do { \ + if (((fdb_db_t)db)->lock) ((fdb_db_t)db)->lock((fdb_db_t)db); \ + } while(0); + +#define db_unlock(db) \ + do { \ + if (((fdb_db_t)db)->unlock) ((fdb_db_t)db)->unlock((fdb_db_t)db); \ + } while(0); + +#define _FDB_WRITE_STATUS(db, addr, status_table, status_num, status_index) \ + do { \ + result = _fdb_write_status((fdb_db_t)db, addr, status_table, status_num, status_index);\ + if (result != FDB_NO_ERR) return result; \ + } while(0); + +#define FLASH_WRITE(db, addr, buf, size) \ + do { \ + result = _fdb_flash_write((fdb_db_t)db, addr, buf, size); \ + if (result != FDB_NO_ERR) return result; \ + } while(0); + +struct sector_hdr_data { + uint8_t status[FDB_STORE_STATUS_TABLE_SIZE]; /**< sector store status @see fdb_sector_store_status_t */ + uint32_t magic; /**< magic word(`T`, `S`, `L`, `0`) */ + fdb_time_t start_time; /**< the first start node's timestamp */ + struct { + fdb_time_t time; /**< the last end node's timestamp */ + uint32_t index; /**< the last end node's index */ + uint8_t status[TSL_STATUS_TABLE_SIZE]; /**< end node status, @see fdb_tsl_status_t */ + } end_info[2]; + uint32_t reserved; +}; +typedef struct sector_hdr_data *sector_hdr_data_t; + +/* time series log node index data */ +struct log_idx_data { + uint8_t status_table[TSL_STATUS_TABLE_SIZE]; /**< node status, @see fdb_tsl_status_t */ + fdb_time_t time; /**< node timestamp */ + uint32_t log_len; /**< node total length (header + name + value), must align by FDB_WRITE_GRAN */ + uint32_t log_addr; /**< node address */ +}; +typedef struct log_idx_data *log_idx_data_t; + +struct query_count_args { + fdb_tsl_status_t status; + size_t count; +}; + +struct check_sec_hdr_cb_args { + fdb_tsdb_t db; + bool check_failed; + size_t empty_num; + uint32_t empty_addr; +}; + +static fdb_err_t read_tsl(fdb_tsdb_t db, fdb_tsl_t tsl) +{ + struct log_idx_data idx; + /* read TSL index raw data */ + _fdb_flash_read((fdb_db_t)db, tsl->addr.index, (uint32_t *) &idx, sizeof(struct log_idx_data)); + tsl->status = (fdb_tsl_status_t) _fdb_get_status(idx.status_table, FDB_TSL_STATUS_NUM); + if (tsl->status == FDB_TSL_PRE_WRITE) { + tsl->log_len = db->max_len; + tsl->addr.log = FDB_DATA_UNUSED; + tsl->time = 0; + } else { + tsl->log_len = idx.log_len; + tsl->addr.log = idx.log_addr; + tsl->time = idx.time; + } + + return FDB_NO_ERR; +} + +static uint32_t get_next_sector_addr(fdb_tsdb_t db, tsdb_sec_info_t pre_sec, uint32_t traversed_len) +{ + if (traversed_len + db_sec_size(db) <= db_part_size(db)) { + if (pre_sec->addr + db_sec_size(db) < db_part_size(db)) { + return pre_sec->addr + db_sec_size(db); + } else { + /* the next sector is on the top of the partition */ + return 0; + } + } else { + /* finished */ + return FAILED_ADDR; + } +} + +static uint32_t get_next_tsl_addr(tsdb_sec_info_t sector, fdb_tsl_t pre_tsl) +{ + uint32_t addr = FAILED_ADDR; + + if (sector->status == FDB_SECTOR_STORE_EMPTY) { + return FAILED_ADDR; + } + + if (pre_tsl->addr.index + LOG_IDX_DATA_SIZE <= sector->end_idx) { + addr = pre_tsl->addr.index + LOG_IDX_DATA_SIZE; + } else { + /* no TSL */ + return FAILED_ADDR; + } + + return addr; +} + +static fdb_err_t read_sector_info(fdb_tsdb_t db, uint32_t addr, tsdb_sec_info_t sector, bool traversal) +{ + fdb_err_t result = FDB_NO_ERR; + struct sector_hdr_data sec_hdr; + + FDB_ASSERT(sector); + + /* read sector header raw data */ + _fdb_flash_read((fdb_db_t)db, addr, (uint32_t *)&sec_hdr, sizeof(struct sector_hdr_data)); + + sector->addr = addr; + sector->magic = sec_hdr.magic; + + /* check magic word */ + if (sector->magic != SECTOR_MAGIC_WORD) { + sector->check_ok = false; + return FDB_INIT_FAILED; + } + sector->check_ok = true; + sector->status = (fdb_sector_store_status_t) _fdb_get_status(sec_hdr.status, FDB_SECTOR_STORE_STATUS_NUM); + sector->start_time = sec_hdr.start_time; + sector->end_info_stat[0] = (fdb_tsl_status_t) _fdb_get_status(sec_hdr.end_info[0].status, FDB_TSL_STATUS_NUM); + sector->end_info_stat[1] = (fdb_tsl_status_t) _fdb_get_status(sec_hdr.end_info[1].status, FDB_TSL_STATUS_NUM); + if (sector->end_info_stat[0] == FDB_TSL_WRITE) { + sector->end_time = sec_hdr.end_info[0].time; + sector->end_idx = sec_hdr.end_info[0].index; + } else if (sector->end_info_stat[1] == FDB_TSL_WRITE) { + sector->end_time = sec_hdr.end_info[1].time; + sector->end_idx = sec_hdr.end_info[1].index; + } else if (sector->end_info_stat[0] == FDB_TSL_PRE_WRITE && sector->end_info_stat[1] == FDB_TSL_PRE_WRITE) { + //TODO There is no valid end node info on this sector, need impl fast query this sector by fdb_tsl_iter_by_time + FDB_ASSERT(0); + } + /* traversal all TSL and calculate the remain space size */ + sector->empty_idx = sector->addr + SECTOR_HDR_DATA_SIZE; + sector->empty_data = sector->addr + db_sec_size(db); + /* the TSL's data is saved from sector bottom, and the TSL's index saved from the sector top */ + sector->remain = sector->empty_data - sector->empty_idx; + if (sector->status == FDB_SECTOR_STORE_USING && traversal) { + struct fdb_tsl tsl; + + tsl.addr.index = sector->empty_idx; + while (read_tsl(db, &tsl) == FDB_NO_ERR) { + if (tsl.status == FDB_TSL_UNUSED) { + break; + } + sector->end_time = tsl.time; + sector->end_idx = tsl.addr.index; + sector->empty_idx += LOG_IDX_DATA_SIZE; + sector->empty_data -= FDB_WG_ALIGN(tsl.log_len); + tsl.addr.index += LOG_IDX_DATA_SIZE; + if (sector->remain > LOG_IDX_DATA_SIZE + FDB_WG_ALIGN(tsl.log_len)) { + sector->remain -= (LOG_IDX_DATA_SIZE + FDB_WG_ALIGN(tsl.log_len)); + } else { + FDB_INFO("Error: this TSL (0x%08lX) size (%lu) is out of bound.\n", tsl.addr.index, tsl.log_len); + sector->remain = 0; + result = FDB_READ_ERR; + break; + } + } + } + + return result; +} + +static fdb_err_t format_sector(fdb_tsdb_t db, uint32_t addr) +{ + fdb_err_t result = FDB_NO_ERR; + struct sector_hdr_data sec_hdr; + + FDB_ASSERT(addr % db_sec_size(db) == 0); + + result = _fdb_flash_erase((fdb_db_t)db, addr, db_sec_size(db)); + if (result == FDB_NO_ERR) { + _FDB_WRITE_STATUS(db, addr, sec_hdr.status, FDB_SECTOR_STORE_STATUS_NUM, FDB_SECTOR_STORE_EMPTY); + /* set the magic */ + sec_hdr.magic = SECTOR_MAGIC_WORD; + FLASH_WRITE(db, addr + SECTOR_MAGIC_OFFSET, &sec_hdr.magic, sizeof(sec_hdr.magic)); + } + + return result; +} + +static void sector_iterator(fdb_tsdb_t db, tsdb_sec_info_t sector, fdb_sector_store_status_t status, void *arg1, + void *arg2, bool (*callback)(tsdb_sec_info_t sector, void *arg1, void *arg2), bool traversal) +{ + uint32_t sec_addr = sector->addr, traversed_len = 0; + + /* search all sectors */ + do { + read_sector_info(db, sec_addr, sector, false); + if (status == FDB_SECTOR_STORE_UNUSED || status == sector->status) { + if (traversal) { + read_sector_info(db, sec_addr, sector, true); + } + /* iterator is interrupted when callback return true */ + if (callback && callback(sector, arg1, arg2)) { + return; + } + } + traversed_len += db_sec_size(db); + } while ((sec_addr = get_next_sector_addr(db, sector, traversed_len)) != FAILED_ADDR); +} + +static fdb_err_t write_tsl(fdb_tsdb_t db, fdb_blob_t blob, fdb_time_t time) +{ + fdb_err_t result = FDB_NO_ERR; + struct log_idx_data idx; + uint32_t idx_addr = db->cur_sec.empty_idx; + + idx.log_len = blob->size; + idx.time = time; + idx.log_addr = db->cur_sec.empty_data - FDB_WG_ALIGN(idx.log_len); + /* write the status will by write granularity */ + _FDB_WRITE_STATUS(db, idx_addr, idx.status_table, FDB_TSL_STATUS_NUM, FDB_TSL_PRE_WRITE); + /* write other index info */ + FLASH_WRITE(db, idx_addr + LOG_IDX_TS_OFFSET, &idx.time, sizeof(struct log_idx_data) - LOG_IDX_TS_OFFSET); + /* write blob data */ + FLASH_WRITE(db, idx.log_addr, blob->buf, blob->size); + /* write the status will by write granularity */ + _FDB_WRITE_STATUS(db, idx_addr, idx.status_table, FDB_TSL_STATUS_NUM, FDB_TSL_WRITE); + + return result; +} + +static fdb_err_t update_sec_status(fdb_tsdb_t db, tsdb_sec_info_t sector, fdb_blob_t blob, fdb_time_t cur_time) +{ + fdb_err_t result = FDB_NO_ERR; + uint8_t status[FDB_STORE_STATUS_TABLE_SIZE]; + + if (sector->status == FDB_SECTOR_STORE_USING && sector->remain < LOG_IDX_DATA_SIZE + FDB_WG_ALIGN(blob->size)) { + uint8_t end_status[TSL_STATUS_TABLE_SIZE]; + uint32_t end_index = sector->empty_idx - LOG_IDX_DATA_SIZE, new_sec_addr, cur_sec_addr = sector->addr; + /* save the end node index and timestamp */ + if (sector->end_info_stat[0] == FDB_TSL_UNUSED) { + _FDB_WRITE_STATUS(db, cur_sec_addr + SECTOR_END0_STATUS_OFFSET, end_status, FDB_TSL_STATUS_NUM, FDB_TSL_PRE_WRITE); + FLASH_WRITE(db, cur_sec_addr + SECTOR_END0_TIME_OFFSET, (uint32_t * )&db->last_time, sizeof(fdb_time_t)); + FLASH_WRITE(db, cur_sec_addr + SECTOR_END0_IDX_OFFSET, &end_index, sizeof(end_index)); + _FDB_WRITE_STATUS(db, cur_sec_addr + SECTOR_END0_STATUS_OFFSET, end_status, FDB_TSL_STATUS_NUM, FDB_TSL_WRITE); + } else if (sector->end_info_stat[1] == FDB_TSL_UNUSED) { + _FDB_WRITE_STATUS(db, cur_sec_addr + SECTOR_END1_STATUS_OFFSET, end_status, FDB_TSL_STATUS_NUM, FDB_TSL_PRE_WRITE); + FLASH_WRITE(db, cur_sec_addr + SECTOR_END1_TIME_OFFSET, (uint32_t * )&db->last_time, sizeof(fdb_time_t)); + FLASH_WRITE(db, cur_sec_addr + SECTOR_END1_IDX_OFFSET, &end_index, sizeof(end_index)); + _FDB_WRITE_STATUS(db, cur_sec_addr + SECTOR_END1_STATUS_OFFSET, end_status, FDB_TSL_STATUS_NUM, FDB_TSL_WRITE); + } + /* change current sector to full */ + _FDB_WRITE_STATUS(db, cur_sec_addr, status, FDB_SECTOR_STORE_STATUS_NUM, FDB_SECTOR_STORE_FULL); + /* calculate next sector address */ + if (sector->addr + db_sec_size(db) < db_part_size(db)) { + new_sec_addr = sector->addr + db_sec_size(db); + } else { + new_sec_addr = 0; + } + read_sector_info(db, new_sec_addr, &db->cur_sec, false); + if (sector->status != FDB_SECTOR_STORE_EMPTY) { + /* calculate the oldest sector address */ + if (new_sec_addr + db_sec_size(db) < db_part_size(db)) { + db->oldest_addr = new_sec_addr + db_sec_size(db); + } else { + db->oldest_addr = 0; + } + format_sector(db, new_sec_addr); + read_sector_info(db, new_sec_addr, &db->cur_sec, false); + } + } + + if (sector->status == FDB_SECTOR_STORE_EMPTY) { + /* change the sector to using */ + sector->status = FDB_SECTOR_STORE_USING; + sector->start_time = cur_time; + _FDB_WRITE_STATUS(db, sector->addr, status, FDB_SECTOR_STORE_STATUS_NUM, FDB_SECTOR_STORE_USING); + /* save the start timestamp */ + FLASH_WRITE(db, sector->addr + SECTOR_START_TIME_OFFSET, (uint32_t *)&cur_time, sizeof(fdb_time_t)); + } + + return result; +} + +static fdb_err_t tsl_append(fdb_tsdb_t db, fdb_blob_t blob) +{ + fdb_err_t result = FDB_NO_ERR; + fdb_time_t cur_time = db->get_time(); + + FDB_ASSERT(blob->size <= db->max_len); + + update_sec_status(db, &db->cur_sec, blob, cur_time); + + /* write the TSL node */ + result = write_tsl(db, blob, cur_time); + + /* recalculate the current using sector info */ + db->cur_sec.end_idx = db->cur_sec.empty_idx; + db->cur_sec.end_time = cur_time; + db->cur_sec.empty_idx += LOG_IDX_DATA_SIZE; + db->cur_sec.empty_data -= FDB_WG_ALIGN(blob->size); + db->cur_sec.remain -= LOG_IDX_DATA_SIZE + FDB_WG_ALIGN(blob->size); + + if (cur_time >= db->last_time) { + db->last_time = cur_time; + } else { + FDB_INFO("Warning: current timestamp (%ld) is less than the last save timestamp (%ld)\n", cur_time, db->last_time); + } + + return result; +} + +/** + * Append a new log to TSDB. + * + * @param db database object + * @param blob log blob data + * + * @return result + */ +fdb_err_t fdb_tsl_append(fdb_tsdb_t db, fdb_blob_t blob) +{ + fdb_err_t result = FDB_NO_ERR; + + if (!db_init_ok(db)) { + FDB_INFO("Error: TSL (%s) isn't initialize OK.\n", db_name(db)); + return FDB_INIT_FAILED; + } + + db_lock(db); + result = tsl_append(db, blob); + db_unlock(db); + + return result; +} + +/** + * The TSDB iterator for each TSL. + * + * @param db database object + * @param cb callback + * @param arg callback argument + */ +void fdb_tsl_iter(fdb_tsdb_t db, fdb_tsl_cb cb, void *arg) +{ + struct tsdb_sec_info sector; + uint32_t sec_addr, traversed_len = 0; + struct fdb_tsl tsl; + + if (!db_init_ok(db)) { + FDB_INFO("Error: TSL (%s) isn't initialize OK.\n", db_name(db)); + } + + if (cb == NULL) { + return; + } + + sec_addr = db->oldest_addr; + /* search all sectors */ + do { + if (read_sector_info(db, sec_addr, §or, false) != FDB_NO_ERR) { + continue; + } + /* sector has TSL */ + if (sector.status == FDB_SECTOR_STORE_USING || sector.status == FDB_SECTOR_STORE_FULL) { + if (sector.status == FDB_SECTOR_STORE_USING) { + /* copy the current using sector status */ + sector = db->cur_sec; + } + tsl.addr.index = sector.addr + SECTOR_HDR_DATA_SIZE; + /* search all TSL */ + do { + read_tsl(db, &tsl); + /* iterator is interrupted when callback return true */ + if (cb(&tsl, arg)) { + return; + } + } while ((tsl.addr.index = get_next_tsl_addr(§or, &tsl)) != FAILED_ADDR); + } + traversed_len += db_sec_size(db); + } while ((sec_addr = get_next_sector_addr(db, §or, traversed_len)) != FAILED_ADDR); +} + +/** + * The TSDB iterator for each TSL by timestamp. + * + * @param db database object + * @param from starting timestap + * @param to ending timestap + * @param cb callback + * @param arg callback argument + */ +void fdb_tsl_iter_by_time(fdb_tsdb_t db, fdb_time_t from, fdb_time_t to, fdb_tsl_cb cb, void *cb_arg) +{ + struct tsdb_sec_info sector; + uint32_t sec_addr, traversed_len = 0; + struct fdb_tsl tsl; + bool found_start_tsl = false; + + if (!db_init_ok(db)) { + FDB_INFO("Error: TSL (%s) isn't initialize OK.\n", db_name(db)); + } + +// FDB_INFO("from %s", ctime((const time_t * )&from)); +// FDB_INFO("to %s", ctime((const time_t * )&to)); + + if (cb == NULL) { + return; + } + + sec_addr = db->oldest_addr; + /* search all sectors */ + do { + if (read_sector_info(db, sec_addr, §or, false) != FDB_NO_ERR) { + continue; + } + /* sector has TSL */ + if ((sector.status == FDB_SECTOR_STORE_USING || sector.status == FDB_SECTOR_STORE_FULL)) { + if (sector.status == FDB_SECTOR_STORE_USING) { + /* copy the current using sector status */ + sector = db->cur_sec; + } + if ((!found_start_tsl && ((sector.start_time <= from && sector.end_time >= from) || (sector.start_time <= to))) + || (found_start_tsl)) { + uint32_t start = sector.addr + SECTOR_HDR_DATA_SIZE, end = sector.end_idx; + + found_start_tsl = true; + /* search start TSL address, using binary search algorithm */ + while (start <= end) { + tsl.addr.index = start + ((end - start) / 2 + 1) / LOG_IDX_DATA_SIZE * LOG_IDX_DATA_SIZE; + read_tsl(db, &tsl); + if (tsl.time < from) { + start = tsl.addr.index + LOG_IDX_DATA_SIZE; + } else { + end = tsl.addr.index - LOG_IDX_DATA_SIZE; + } + } + tsl.addr.index = start; + /* search all TSL */ + do { + read_tsl(db, &tsl); + if (tsl.time >= from && tsl.time <= to) { + /* iterator is interrupted when callback return true */ + if (cb(&tsl, cb_arg)) { + return; + } + } else { + return; + } + } while ((tsl.addr.index = get_next_tsl_addr(§or, &tsl)) != FAILED_ADDR); + } + } else if (sector.status == FDB_SECTOR_STORE_EMPTY) { + return; + } + traversed_len += db_sec_size(db); + } while ((sec_addr = get_next_sector_addr(db, §or, traversed_len)) != FAILED_ADDR); +} + +static bool query_count_cb(fdb_tsl_t tsl, void *arg) +{ + struct query_count_args *args = arg; + + if (tsl->status == args->status) { + args->count++; + } + + return false; +} + +/** + * Query some TSL's count by timestamp and status. + * + * @param db database object + * @param from starting timestap + * @param to ending timestap + * @param status status + */ +size_t fdb_tsl_query_count(fdb_tsdb_t db, fdb_time_t from, fdb_time_t to, fdb_tsl_status_t status) +{ + struct query_count_args arg = { 0 }; + + arg.status = status; + + if (!db_init_ok(db)) { + FDB_INFO("Error: TSL (%s) isn't initialize OK.\n", db_name(db)); + return FDB_INIT_FAILED; + } + + fdb_tsl_iter_by_time(db, from, to, query_count_cb, &arg); + + return arg.count; + +} + +/** + * Set the TSL status. + * + * @param db database object + * @param tsl TSL object + * @param status status + * + * @return result + */ +fdb_err_t fdb_tsl_set_status(fdb_tsdb_t db, fdb_tsl_t tsl, fdb_tsl_status_t status) +{ + fdb_err_t result = FDB_NO_ERR; + uint8_t status_table[TSL_STATUS_TABLE_SIZE]; + + /* write the status will by write granularity */ + _FDB_WRITE_STATUS(db, tsl->addr.index, status_table, FDB_TSL_STATUS_NUM, status); + + return result; +} + +/** + * Convert the TSL object to blob object + * + * @param tsl TSL object + * @param blob blob object + * + * @return new blob object + */ +fdb_blob_t fdb_tsl_to_blob(fdb_tsl_t tsl, fdb_blob_t blob) +{ + blob->saved.addr = tsl->addr.log; + blob->saved.meta_addr = tsl->addr.index; + blob->saved.len = tsl->log_len; + + return blob; +} + +static bool check_sec_hdr_cb(tsdb_sec_info_t sector, void *arg1, void *arg2) +{ + struct check_sec_hdr_cb_args *arg = arg1; + fdb_tsdb_t db = arg->db; + + if (!sector->check_ok) { + FDB_INFO("Warning: Sector (0x%08lX) header check failed.\n", sector->addr); + (arg->check_failed) = true; + return true; + } else if (sector->status == FDB_SECTOR_STORE_USING) { + if (db->cur_sec.addr == FDB_DATA_UNUSED) { + memcpy(&db->cur_sec, sector, sizeof(struct tsdb_sec_info)); + } else { + FDB_INFO("Warning: Sector status is wrong, there are multiple sectors in use.\n"); + (arg->check_failed) = true; + return true; + } + } else if (sector->status == FDB_SECTOR_STORE_EMPTY) { + (arg->empty_num) += 1; + arg->empty_addr = sector->addr; + if ((arg->empty_num) == 1 && db->cur_sec.addr == FDB_DATA_UNUSED) { + memcpy(&db->cur_sec, sector, sizeof(struct tsdb_sec_info)); + } + } + + return false; +} +static bool format_all_cb(tsdb_sec_info_t sector, void *arg1, void *arg2) +{ + fdb_tsdb_t db = arg1; + + format_sector(db, sector->addr); + + return false; +} + +static void tsl_format_all(fdb_tsdb_t db) +{ + struct tsdb_sec_info sector; + + sector.addr = 0; + sector_iterator(db, §or, FDB_SECTOR_STORE_UNUSED, db, NULL, format_all_cb, false); + db->oldest_addr = 0; + db->cur_sec.addr = 0; + /* read the current using sector info */ + read_sector_info(db, db->cur_sec.addr, &db->cur_sec, false); + + FDB_INFO("All sector format finished.\n"); +} + +/** + * Clean all the data in the TSDB. + * + * @note It's DANGEROUS. This operation is not reversible. + * + * @param db database object + */ +void fdb_tsl_clean(fdb_tsdb_t db) +{ + db_lock(db); + tsl_format_all(db); + db_unlock(db); +} + +/** + * The time series database initialization. + * + * @param db database object + * @param name database name + * @param part_name partition name + * @param get_time get current time function + * @param max_len maximum length of each log + * @param user_data user data + * + * @return result + */ +fdb_err_t fdb_tsdb_init(fdb_tsdb_t db, const char *name, const char *part_name, fdb_get_time get_time, size_t max_len, void *user_data) +{ + fdb_err_t result = FDB_NO_ERR; + struct tsdb_sec_info sector; + struct check_sec_hdr_cb_args check_sec_arg = { db, false, 0, 0}; + + FDB_ASSERT(get_time); + + result = _fdb_init_ex((fdb_db_t)db, name, part_name, FDB_DB_TYPE_TS, user_data); + + db->get_time = get_time; + db->max_len = max_len; + db->oldest_addr = FDB_DATA_UNUSED; + db->cur_sec.addr = FDB_DATA_UNUSED; + /* must align with sector size */ + FDB_ASSERT(db_part_size(db) % db_sec_size(db) == 0); + /* must have more than or equal 2 sector */ + FDB_ASSERT(db_part_size(db) / db_sec_size(db) >= 2); + /* must less than sector size */ + FDB_ASSERT(max_len < db_sec_size(db)); + + /* check all sector header */ + sector.addr = 0; + sector_iterator(db, §or, FDB_SECTOR_STORE_UNUSED, &check_sec_arg, NULL, check_sec_hdr_cb, true); + /* format all sector when check failed */ + if (check_sec_arg.check_failed) { + tsl_format_all(db); + } else { + uint32_t latest_addr; + if (check_sec_arg.empty_num > 0) { + latest_addr = check_sec_arg.empty_addr; + } else { + latest_addr = db->cur_sec.addr; + } + /* db->cur_sec is the latest sector, and the next is the oldest sector */ + if (latest_addr + db_sec_size(db) >= db_part_size(db)) { + /* db->cur_sec is the the bottom of the partition */ + db->oldest_addr = 0; + } else { + db->oldest_addr = latest_addr + db_sec_size(db); + } + } + FDB_DEBUG("tsdb (%s) oldest sectors is 0x%08lX, current using sector is 0x%08lX.\n", db_name(db), db->oldest_addr, + db->cur_sec.addr); + /* read the current using sector info */ + read_sector_info(db, db->cur_sec.addr, &db->cur_sec, true); + + _fdb_init_finish((fdb_db_t)db, result); + + return result; +} + +#endif /* defined(FDB_USING_TSDB) */ diff --git a/flashdb/src/fdb_utils.c b/flashdb/src/fdb_utils.c new file mode 100644 index 0000000..e736c78 --- /dev/null +++ b/flashdb/src/fdb_utils.c @@ -0,0 +1,270 @@ +/* + * Copyright (c) 2020, Armink, + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * @brief utils + * + * Some utils for this library. + */ + +#include +#include +#include +#include + +#define FDB_LOG_TAG "[utils]" + +static const uint32_t crc32_table[] = +{ + 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, + 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, + 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, + 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, + 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, + 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, + 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, + 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, + 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, + 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, + 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106, + 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, + 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, + 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, + 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, + 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, + 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, + 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, + 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, + 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, + 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, + 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, + 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, + 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, + 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, + 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, + 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e, + 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, + 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, + 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, + 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, + 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, + 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, + 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, + 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, + 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, + 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, + 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, + 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, + 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, + 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, + 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, + 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d +}; + +/** + * Calculate the CRC32 value of a memory buffer. + * + * @param crc accumulated CRC32 value, must be 0 on first call + * @param buf buffer to calculate CRC32 value for + * @param size bytes in buffer + * + * @return calculated CRC32 value + */ +uint32_t fdb_calc_crc32(uint32_t crc, const void *buf, size_t size) +{ + const uint8_t *p; + + p = (const uint8_t *)buf; + crc = crc ^ ~0U; + + while (size--) { + crc = crc32_table[(crc ^ *p++) & 0xFF] ^ (crc >> 8); + } + + return crc ^ ~0U; +} + +size_t _fdb_set_status(uint8_t status_table[], size_t status_num, size_t status_index) +{ + size_t byte_index = ~0UL; + /* + * | write garn | status0 | status1 | status2 | + * --------------------------------------------------------------------------------- + * | 1bit | 0xFF | 0x7F | 0x3F | + * | 8bit | 0xFFFF | 0x00FF | 0x0000 | + * | 32bit | 0xFFFFFFFF FFFFFFFF | 0x00FFFFFF FFFFFFFF | 0x00FFFFFF 00FFFFFF | + */ + memset(status_table, 0xFF, FDB_STATUS_TABLE_SIZE(status_num)); + if (status_index > 0) { +#if (FDB_WRITE_GRAN == 1) + byte_index = (status_index - 1) / 8; + status_table[byte_index] &= ~(0x80 >> ((status_index - 1) % 8)); +#else + byte_index = (status_index - 1) * (FDB_WRITE_GRAN / 8); + status_table[byte_index] = 0x00; +#endif /* FDB_WRITE_GRAN == 1 */ + } + + return byte_index; +} + +size_t _fdb_get_status(uint8_t status_table[], size_t status_num) +{ + size_t i = 0, status_num_bak = --status_num; + + while (status_num --) { + /* get the first 0 position from end address to start address */ +#if (FDB_WRITE_GRAN == 1) + if ((status_table[status_num / 8] & (0x80 >> (status_num % 8))) == 0x00) { + break; + } +#else /* (FDB_WRITE_GRAN == 8) || (FDB_WRITE_GRAN == 32) || (FDB_WRITE_GRAN == 64) */ + if (status_table[status_num * FDB_WRITE_GRAN / 8] == 0x00) { + break; + } +#endif /* FDB_WRITE_GRAN == 1 */ + i++; + } + + return status_num_bak - i; +} + +fdb_err_t _fdb_write_status(fdb_db_t db, uint32_t addr, uint8_t status_table[], size_t status_num, size_t status_index) +{ + fdb_err_t result = FDB_NO_ERR; + size_t byte_index; + + FDB_ASSERT(status_index < status_num); + FDB_ASSERT(status_table); + + /* set the status first */ + byte_index = _fdb_set_status(status_table, status_num, status_index); + + /* the first status table value is all 1, so no need to write flash */ + if (byte_index == ~0UL) { + return FDB_NO_ERR; + } +#if (FDB_WRITE_GRAN == 1) + result = _fdb_flash_write(db, addr + byte_index, (uint32_t *)&status_table[byte_index], 1); +#else /* (FDB_WRITE_GRAN == 8) || (FDB_WRITE_GRAN == 32) || (FDB_WRITE_GRAN == 64) */ + /* write the status by write granularity + * some flash (like stm32 onchip) NOT supported repeated write before erase */ + result = _fdb_flash_write(db, addr + byte_index, (uint32_t *) &status_table[byte_index], FDB_WRITE_GRAN / 8); +#endif /* FDB_WRITE_GRAN == 1 */ + + return result; +} + +size_t _fdb_read_status(fdb_db_t db, uint32_t addr, uint8_t status_table[], size_t total_num) +{ + FDB_ASSERT(status_table); + + _fdb_flash_read(db, addr, (uint32_t *) status_table, FDB_STATUS_TABLE_SIZE(total_num)); + + return _fdb_get_status(status_table, total_num); +} + +/* + * find the continue 0xFF flash address to end address + */ +uint32_t _fdb_continue_ff_addr(fdb_db_t db, uint32_t start, uint32_t end) +{ + uint8_t buf[32], last_data = 0x00; + size_t i, addr = start, read_size; + + for (; start < end; start += sizeof(buf)) { + if (start + sizeof(buf) < end) { + read_size = sizeof(buf); + } else { + read_size = end - start; + } + _fdb_flash_read(db, start, (uint32_t *) buf, read_size); + for (i = 0; i < read_size; i++) { + if (last_data != 0xFF && buf[i] == 0xFF) { + addr = start + i; + } + last_data = buf[i]; + } + } + + if (last_data == 0xFF) { + return FDB_WG_ALIGN(addr); + } else { + return end; + } +} + +/** + * Make a blob object. + * + * @param blob blob object + * @param value_buf value buffer + * @param buf_len buffer length + * + * @return new blob object + */ +fdb_blob_t fdb_blob_make(fdb_blob_t blob, const void *value_buf, size_t buf_len) +{ + blob->buf = (void *)value_buf; + blob->size = buf_len; + + return blob; +} + +/** + * Read the blob object in database. + * + * @param db database object + * @param blob blob object + * + * @return read length + */ +size_t fdb_blob_read(fdb_db_t db, fdb_blob_t blob) +{ + size_t read_len = blob->size; + + if (read_len > blob->saved.len) { + read_len = blob->saved.len; + } + _fdb_flash_read(db, blob->saved.addr, blob->buf, read_len); + + return read_len; +} + +fdb_err_t _fdb_flash_read(fdb_db_t db, uint32_t addr, void *buf, size_t size) +{ + fdb_err_t result = FDB_NO_ERR; + + fal_partition_read(db->part, addr, (uint8_t *)buf, size); + + return result; +} + +fdb_err_t _fdb_flash_erase(fdb_db_t db, uint32_t addr, size_t size) +{ + fdb_err_t result = FDB_NO_ERR; + + if (fal_partition_erase(db->part, addr, size) < 0) + { + result = FDB_ERASE_ERR; + } + + return result; +} + +fdb_err_t _fdb_flash_write(fdb_db_t db, uint32_t addr, const void *buf, size_t size) +{ + fdb_err_t result = FDB_NO_ERR; + + if (fal_partition_write(db->part, addr, (uint8_t *)buf, size) < 0) + { + result = FDB_WRITE_ERR; + } + + return result; + +} diff --git a/samples/kvdb_type_blob_sample.c b/samples/kvdb_type_blob_sample.c new file mode 100644 index 0000000..f5add7e --- /dev/null +++ b/samples/kvdb_type_blob_sample.c @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2020, Armink, + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * @brief blob KV samples. + * + * Key-Value Database blob type KV feature samples + */ + +#include + +static uint32_t boot_count = 0; +static time_t boot_time[10] = {0, 1, 2, 3}; +/* default KV nodes */ +static struct fdb_default_kv_node default_kv_table[] = { + {"username", "armink", 0}, /* string KV */ + {"password", "123456", 0}, /* string KV */ + {"boot_count", &boot_count, sizeof(boot_count)}, /* int type KV */ + {"boot_time", &boot_time, sizeof(boot_time)}, /* int array type KV */ +}; +/* KVDB object */ +static struct fdb_kvdb kvdb = { 0 }; + +static void lock(fdb_db_t db) +{ + /* YOUR CODE HERE */ +} + +static void unlock(fdb_db_t db) +{ + /* YOUR CODE HERE */ +} + +void kvdb_type_blob_sample(void) +{ + fdb_err_t result; + struct fdb_blob blob; + + { /* database initialization */ + struct fdb_default_kv default_kv; + + default_kv.kvs = default_kv_table; + default_kv.num = sizeof(default_kv_table) / sizeof(default_kv_table[0]); + /* set the lock and unlock function if you want */ + fdb_lock_set((fdb_db_t)&kvdb, lock, unlock); + /* Key-Value database initialization + * + * &kvdb: database object + * "env": database name + * "fdb_kvdb1": The flash partition name base on FAL. Please make sure it's in FAL partition table. + * Please change to YOUR partition name. + * &default_kv: The default KV nodes. It will auto add to KVDB when first initialize successfully. + * NULL: The user data if you need, now is empty. + */ + result = fdb_kvdb_init(&kvdb, "env", "fdb_kvdb1", &default_kv, NULL); + + if (result != FDB_NO_ERR) { + return; + } + } + + { /* CREATE new Key-Value */ + int temp_data = 36; + + /* It will create new KV node when "temp" KV not in database. + * fdb_blob_make: It's a blob make function, and it will return the blob when make finish. + */ + fdb_kv_set_blob(&kvdb, "temp", fdb_blob_make(&blob, &temp_data, sizeof(temp_data))); + } + + { /* GET the KV value */ + int temp_data = 0; + + /* get the "temp" KV value */ + fdb_kv_get_blob(&kvdb, "temp", fdb_blob_make(&blob, &temp_data, sizeof(temp_data))); + /* the blob.saved.len is more than 0 when get the value successful */ + if (blob.saved.len > 0) { + FDB_PRINT("temp_data: %d\n", temp_data); + } + } + + { /* CHANGE the KV value */ + int temp_data = 38; + + /* change the "temp" KV's value to 38.1 */ + fdb_kv_set_blob(&kvdb, "temp", fdb_blob_make(&blob, &temp_data, sizeof(temp_data))); + } + + { /* DELETE the KV by name */ + fdb_kv_del(&kvdb, "temp"); + } +} diff --git a/samples/kvdb_type_string_sample.c b/samples/kvdb_type_string_sample.c new file mode 100644 index 0000000..e3aedac --- /dev/null +++ b/samples/kvdb_type_string_sample.c @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2020, Armink, + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * @brief string KV samples. + * + * Key-Value Database string type KV feature samples source file. + */ + +#include +#include + +static uint32_t boot_count = 0; +static time_t boot_time[10] = {0, 1, 2, 3}; +/* default KV nodes */ +static struct fdb_default_kv_node default_kv_table[] = { + {"username", "armink", 0}, /* string KV */ + {"password", "123456", 0}, /* string KV */ + {"boot_count", &boot_count, sizeof(boot_count)}, /* int type KV */ + {"boot_time", &boot_time, sizeof(boot_time)}, /* int array type KV */ +}; +/* KVDB object */ +static struct fdb_kvdb kvdb = { 0 }; + +static void lock(fdb_db_t db) +{ + /* YOUR CODE HERE */ +} + +static void unlock(fdb_db_t db) +{ + /* YOUR CODE HERE */ +} + +void kvdb_type_string_sample(void) +{ + fdb_err_t result; + + { /* database initialization */ + struct fdb_default_kv default_kv; + + default_kv.kvs = default_kv_table; + default_kv.num = sizeof(default_kv_table) / sizeof(default_kv_table[0]); + /* set the lock and unlock function if you want */ + fdb_lock_set((fdb_db_t)&kvdb, lock, unlock); + /* Key-Value database initialization + * + * &kvdb: database object + * "env": database name + * "fdb_kvdb1": The flash partition name base on FAL. Please make sure it's in FAL partition table. + * Please change to YOUR partition name. + * &default_kv: The default KV nodes. It will auto add to KVDB when first initialize successfully. + * NULL: The user data if you need, now is empty. + */ + result = fdb_kvdb_init(&kvdb, "env", "fdb_kvdb1", &default_kv, NULL); + + if (result != FDB_NO_ERR) { + return; + } + } + + { /* CREATE new Key-Value */ + char temp_data[10] = "36"; + + /* It will create new KV node when "temp" KV not in database. */ + fdb_kv_set(&kvdb, "temp", temp_data); + } + + { /* GET the KV value */ + char *return_value, temp_data[10] = { 0 }; + + /* Get the "temp" KV value. + * NOTE: The return value saved in fdb_kv_get's buffer. Please copy away as soon as possible. + */ + return_value = fdb_kv_get(&kvdb, "temp"); + /* the return value is NULL when get the value failed */ + if (return_value != NULL) { + strncpy(temp_data, return_value, sizeof(temp_data)); + FDB_PRINT("temp_data: %s\n", temp_data); + } + } + + { /* CHANGE the KV value */ + char temp_data[10] = "38"; + + /* change the "temp" KV's value to "38.1" */ + fdb_kv_set(&kvdb, "temp", temp_data); + } + + { /* DELETE the KV by name */ + fdb_kv_del(&kvdb, "temp"); + } +} diff --git a/samples/tsdb_sample.c b/samples/tsdb_sample.c new file mode 100644 index 0000000..2ae3e91 --- /dev/null +++ b/samples/tsdb_sample.c @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2020, Armink, + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * @brief TSDB samples. + * + * Time series log (like TSDB) feature samples source file. + * + * TSL is time series log, the TSDB saved many TSLs. + */ + +#include +#include + +struct env_status { + int temp; + int humi; +}; + +/* TSDB object */ +static struct fdb_tsdb tsdb = { 0 }; + +static bool query_cb(fdb_tsl_t tsl, void *arg); +static bool set_status_cb(fdb_tsl_t tsl, void *arg); + +static void lock(fdb_db_t db) +{ + /* YOUR CODE HERE */ +} + +static void unlock(fdb_db_t db) +{ + /* YOUR CODE HERE */ +} +static fdb_time_t get_time(void) +{ + return time(NULL); +} + +void tsdb_sample(void) +{ + fdb_err_t result; + struct fdb_blob blob; + + { /* database initialization */ + /* set the lock and unlock function if you want */ + fdb_lock_set((fdb_db_t)&tsdb, lock, unlock); + /* Time series database initialization + * + * &tsdb: database object + * "log": database name + * "fdb_tsdb1": The flash partition name base on FAL. Please make sure it's in FAL partition table. + * Please change to YOUR partition name. + * get_time: The get current timestamp function. + * 128: maximum length of each log + * NULL: The user data if you need, now is empty. + */ + result = fdb_tsdb_init(&tsdb, "log", "fdb_tsdb1", get_time, 128, NULL); + + if (result != FDB_NO_ERR) { + return; + } + } + + { /* APPEND new TSL (time series log) */ + struct env_status status; + + /* append new log to TSDB */ + status.temp = 36; + status.humi = 85; + fdb_tsl_append(&tsdb, fdb_blob_make(&blob, &status, sizeof(status))); + + status.temp = 38; + status.humi = 90; + fdb_tsl_append(&tsdb, fdb_blob_make(&blob, &status, sizeof(status))); + } + + { /* QUERY the TSDB */ + /* query all TSL in TSDB by iterator */ + fdb_tsl_iter(&tsdb, query_cb, &tsdb); + } + + { /* QUERY the TSDB by time */ + /* prepare query time (from 1970-01-01 00:00:00 to 2020-05-05 00:00:00) */ + struct tm tm_from = { .tm_year = 1970 - 1900, .tm_mon = 0, .tm_mday = 1, .tm_hour = 0, .tm_min = 0, .tm_sec = 0 }; + struct tm tm_to = { .tm_year = 2020 - 1900, .tm_mon = 4, .tm_mday = 5, .tm_hour = 0, .tm_min = 0, .tm_sec = 0 }; + time_t from_time = mktime(&tm_from), to_time = mktime(&tm_to); + size_t count; + /* query all TSL in TSDB by time */ + fdb_tsl_iter_by_time(&tsdb, from_time, to_time, query_cb, &tsdb); + /* query all FDB_TSL_WRITE status TSL's count in TSDB by time */ + count = fdb_tsl_query_count(&tsdb, from_time, to_time, FDB_TSL_WRITE); + FDB_PRINT("query count: %lu\n", count); + } + + { /* SET the TSL status */ + /* Change the TSL status by iterator or time iterator + * set_status_cb: the change operation will in this callback + * + * NOTE: The actions to modify the state must be in order. + * FDB_TSL_WRITE -> FDB_TSL_USER_STATUS1 -> FDB_TSL_DELETED -> FDB_TSL_USER_STATUS2 + */ + fdb_tsl_iter(&tsdb, set_status_cb, &tsdb); + } +} + +static bool query_cb(fdb_tsl_t tsl, void *arg) +{ + struct fdb_blob blob; + struct env_status status; + fdb_tsdb_t db = arg; + + fdb_blob_read((fdb_db_t) db, fdb_tsl_to_blob(tsl, fdb_blob_make(&blob, &status, sizeof(status)))); + FDB_PRINT("time: %d, temp: %d, humi: %d\n", tsl->time, status.temp, status.humi); + + return false; +} + +static bool set_status_cb(fdb_tsl_t tsl, void *arg) +{ + fdb_tsdb_t db = arg; + + fdb_tsl_set_status(db, tsl, FDB_TSL_USER_STATUS1); + + return false; +} diff --git a/tests/fdb_kvdb_tc.c b/tests/fdb_kvdb_tc.c new file mode 100644 index 0000000..4aac623 --- /dev/null +++ b/tests/fdb_kvdb_tc.c @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2020, Armink, + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * @brief KVDB testcases. + * + * This testcases is be used in RT-Thread Utest framework. + * If you want run it, please add it to RT-Thread project. + */ + +#include "utest.h" +#include +#include +#include + +#define TEST_TS_PART_NAME "fdb_kvdb1" +#define TEST_KV_BLOB_NAME "kv_blob_test" +#define TEST_KV_NAME "kv_test" + +#if defined(RT_USING_UTEST) && defined(FDB_USING_KVDB) + +static struct fdb_default_kv_node default_kv_set[] = { + {"iap_need_copy_app", "0"}, + {"iap_need_crc32_check", "0"}, + {"iap_copy_app_size", "0"}, + {"stop_in_bootloader", "0"}, +}; + +static struct fdb_kvdb test_kvdb; + +static void test_easyflash_init(void) +{ + struct fdb_default_kv default_kv; + + default_kv.kvs = default_kv_set; + default_kv.num = sizeof(default_kv_set) / sizeof(default_kv_set[0]); + uassert_true(fdb_kvdb_init(&test_kvdb, "test_kv", "fdb_kvdb1", &default_kv, NULL) == FDB_NO_ERR); +} + +static void test_fdb_create_kv_blob(void) +{ + fdb_err_t result = FDB_NO_ERR; + rt_tick_t tick = rt_tick_get(), read_tick; + size_t read_len; + struct fdb_kv kv_obj; + struct fdb_blob blob; + uint8_t value_buf[sizeof(tick)]; + + result = fdb_kv_set_blob(&test_kvdb, TEST_KV_BLOB_NAME, fdb_blob_make(&blob, &tick, sizeof(tick))); + uassert_true(result == FDB_NO_ERR); + + read_len = fdb_kv_get_blob(&test_kvdb, TEST_KV_BLOB_NAME, fdb_blob_make(&blob, &read_tick, sizeof(read_tick))); + uassert_int_equal(blob.saved.len, sizeof(read_tick)); + uassert_int_equal(blob.saved.len, read_len); + uassert_int_equal(tick, read_tick); + + uassert_true(fdb_kv_get_obj(&test_kvdb, TEST_KV_BLOB_NAME, &kv_obj) != NULL); + + fdb_blob_make(&blob, value_buf, sizeof(value_buf)); + read_len = fdb_blob_read((fdb_db_t)&test_kvdb, fdb_kv_to_blob(&kv_obj, &blob)); + uassert_int_equal(read_len, sizeof(value_buf)); + uassert_buf_equal(&tick, value_buf, sizeof(value_buf)); +} + +static void test_fdb_change_kv_blob(void) +{ + fdb_err_t result = FDB_NO_ERR; + rt_tick_t tick = rt_tick_get(), read_tick; + size_t read_len; + struct fdb_blob blob_obj, *blob = &blob_obj; + + read_len = fdb_kv_get_blob(&test_kvdb, TEST_KV_BLOB_NAME, fdb_blob_make(&blob_obj, &read_tick, sizeof(read_tick))); + uassert_int_equal(blob->saved.len, sizeof(read_tick)); + uassert_int_equal(blob->saved.len, read_len); + uassert_int_not_equal(tick, read_tick); + + result = fdb_kv_set_blob(&test_kvdb, TEST_KV_BLOB_NAME, fdb_blob_make(&blob_obj, &tick, sizeof(tick))); + uassert_true(result == FDB_NO_ERR); + + read_len = fdb_kv_get_blob(&test_kvdb, TEST_KV_BLOB_NAME, fdb_blob_make(&blob_obj, &read_tick, sizeof(read_tick))); + uassert_int_equal(blob->saved.len, sizeof(read_tick)); + uassert_int_equal(blob->saved.len, read_len); + uassert_int_equal(tick, read_tick); +} + +static void test_fdb_del_kv_blob(void) +{ + fdb_err_t result = FDB_NO_ERR; + rt_tick_t tick = rt_tick_get(), read_tick; + size_t read_len; + struct fdb_blob blob; + + read_len = fdb_kv_get_blob(&test_kvdb, TEST_KV_BLOB_NAME, fdb_blob_make(&blob, &read_tick, sizeof(read_tick))); + uassert_int_equal(blob.saved.len, sizeof(read_tick)); + uassert_int_equal(blob.saved.len, read_len); + uassert_int_not_equal(tick, read_tick); + + result = fdb_kv_set_blob(&test_kvdb, TEST_KV_BLOB_NAME, fdb_blob_make(&blob, NULL, 0)); + uassert_true(result == FDB_NO_ERR); + + read_len = fdb_kv_get_blob(&test_kvdb, TEST_KV_BLOB_NAME, fdb_blob_make(&blob, &read_tick, sizeof(read_tick))); + uassert_int_equal(blob.saved.len, 0); + uassert_int_equal(read_len, 0); +} + +static void test_fdb_create_kv(void) +{ + fdb_err_t result = FDB_NO_ERR; + rt_tick_t tick = rt_tick_get(), read_tick; + char value_buf[14], *read_value; + + snprintf(value_buf, sizeof(value_buf), "%d", tick); + result = fdb_kv_set(&test_kvdb, TEST_KV_NAME, value_buf); + uassert_true(result == FDB_NO_ERR); + + read_value = fdb_kv_get(&test_kvdb, TEST_KV_NAME); + uassert_not_null(read_value); + read_tick = atoi(read_value); + uassert_int_equal(tick, read_tick); +} + +static void test_fdb_change_kv(void) +{ + fdb_err_t result = FDB_NO_ERR; + rt_tick_t tick = rt_tick_get(), read_tick; + char value_buf[14], *read_value; + + read_value = fdb_kv_get(&test_kvdb, TEST_KV_NAME); + uassert_not_null(read_value); + read_tick = atoi(read_value); + uassert_int_not_equal(tick, read_tick); + + snprintf(value_buf, sizeof(value_buf), "%d", tick); + result = fdb_kv_set(&test_kvdb, TEST_KV_NAME, value_buf); + uassert_true(result == FDB_NO_ERR); + + read_value = fdb_kv_get(&test_kvdb, TEST_KV_NAME); + uassert_not_null(read_value); + read_tick = atoi(read_value); + uassert_int_equal(tick, read_tick); +} + +static void test_fdb_del_kv(void) +{ + fdb_err_t result = FDB_NO_ERR; + rt_tick_t tick = rt_tick_get(), read_tick; + char *read_value; + + read_value = fdb_kv_get(&test_kvdb, TEST_KV_NAME); + uassert_not_null(read_value); + read_tick = atoi(read_value); + uassert_int_not_equal(tick, read_tick); + + result = fdb_kv_del(&test_kvdb, TEST_KV_NAME); + uassert_true(result == FDB_NO_ERR); + + read_value = fdb_kv_get(&test_kvdb, TEST_KV_NAME); + uassert_null(read_value); +} + +static rt_err_t utest_tc_init(void) +{ + return RT_EOK; +} + +static rt_err_t utest_tc_cleanup(void) +{ + return RT_EOK; +} + +static void testcase(void) +{ + UTEST_UNIT_RUN(test_easyflash_init); + UTEST_UNIT_RUN(test_fdb_create_kv_blob); + UTEST_UNIT_RUN(test_fdb_change_kv_blob); + UTEST_UNIT_RUN(test_fdb_del_kv_blob); + UTEST_UNIT_RUN(test_fdb_create_kv); + UTEST_UNIT_RUN(test_fdb_change_kv); + UTEST_UNIT_RUN(test_fdb_del_kv); +} +UTEST_TC_EXPORT(testcase, "packages.tools.flashdb.kvdb", utest_tc_init, utest_tc_cleanup, 20); + +#endif /* defined(RT_USING_UTEST) && defined(FDB_USING_TSDB) */ diff --git a/tests/fdb_tsdb_tc.c b/tests/fdb_tsdb_tc.c new file mode 100644 index 0000000..6db16ad --- /dev/null +++ b/tests/fdb_tsdb_tc.c @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2020, Armink, + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * @brief TSDB testcases. + * + * This testcases is be used in RT-Thread Utest framework. + * If you want run it, please add it to RT-Thread project. + */ + +#include "utest.h" +#include +#include +#include + +#if defined(RT_USING_UTEST) && defined(FDB_USING_TSDB) + +#define TEST_TS_PART_NAME "fdb_tsdb1" +#define TEST_TS_COUNT 256 +#define TEST_TS_USER_STATUS1_COUNT (TEST_TS_COUNT/2) +#define TEST_TS_DELETED_COUNT (TEST_TS_COUNT - TEST_TS_USER_STATUS1_COUNT) + +static char log[10]; + +static struct fdb_tsdb test_tsdb; +static int cur_times = 0; + +static fdb_time_t get_time(void) +{ + return cur_times ++; +} + +static void test_fdb_tsdb_init_ex(void) +{ + uassert_true(fdb_tsdb_init(&test_tsdb, "test_ts", TEST_TS_PART_NAME, get_time, 128, NULL) == FDB_NO_ERR); +} + +static void test_fdb_tsl_append(void) +{ + struct fdb_blob blob; + int i; + + for (i = 0; i < TEST_TS_COUNT; ++i) { + rt_snprintf(log, sizeof(log), "%d", i); + uassert_true(fdb_tsl_append(&test_tsdb, fdb_blob_make(&blob, log, rt_strnlen(log, sizeof(log)))) == FDB_NO_ERR); + } +} + +static bool test_fdb_tsl_iter_cb(fdb_tsl_t tsl, void *arg) +{ + struct fdb_blob blob; + char data[sizeof(log)]; + size_t read_len; + + fdb_blob_make(&blob, data, tsl->log_len); + read_len = fdb_blob_read((fdb_db_t) &test_tsdb, fdb_tsl_to_blob(tsl, &blob)); + + data[read_len] = '\0'; + uassert_true(tsl->time == atoi(data)); + + return false; +} + +static void test_fdb_tsl_iter(void) +{ + fdb_tsl_iter(&test_tsdb, test_fdb_tsl_iter_cb, NULL); +} + +static void test_fdb_tsl_iter_by_time(void) +{ + fdb_time_t from = 0, to = TEST_TS_COUNT -1; + + fdb_tsl_iter_by_time(&test_tsdb, from, to, test_fdb_tsl_iter_cb, NULL); +} + +static void test_fdb_tsl_query_count(void) +{ + fdb_time_t from = 0, to = TEST_TS_COUNT -1; + + uassert_true(fdb_tsl_query_count(&test_tsdb, from, to, FDB_TSL_WRITE) == TEST_TS_COUNT); +} + +static bool est_fdb_tsl_set_status_cb(fdb_tsl_t tsl, void *arg) +{ + fdb_tsdb_t db = arg; + + if (tsl->time >= 0 && tsl->time < TEST_TS_USER_STATUS1_COUNT) { + uassert_true(fdb_tsl_set_status(db, tsl, FDB_TSL_USER_STATUS1) == FDB_NO_ERR); + } else { + uassert_true(fdb_tsl_set_status(db, tsl, FDB_TSL_DELETED) == FDB_NO_ERR); + } + + return false; +} + +static void test_fdb_tsl_set_status(void) +{ + fdb_time_t from = 0, to = TEST_TS_COUNT -1; + + fdb_tsl_iter_by_time(&test_tsdb, from, to, est_fdb_tsl_set_status_cb, &test_tsdb); + + uassert_true(fdb_tsl_query_count(&test_tsdb, from, to, FDB_TSL_USER_STATUS1) == TEST_TS_USER_STATUS1_COUNT); + uassert_true(fdb_tsl_query_count(&test_tsdb, from, to, FDB_TSL_DELETED) == TEST_TS_DELETED_COUNT); +} + +static bool test_fdb_tsl_clean_cb(fdb_tsl_t tsl, void *arg) +{ + size_t *count = arg; + + (*count) ++; + + return false; +} + +static void test_fdb_tsl_clean(void) +{ + size_t count = 0; + + fdb_tsl_clean(&test_tsdb); + + fdb_tsl_iter(&test_tsdb, test_fdb_tsl_clean_cb, &count); + + uassert_true(count == 0); +} + +static rt_err_t utest_tc_init(void) +{ + cur_times = 0; + rt_memset(&test_tsdb, 0, sizeof(struct fdb_tsdb)); + + return RT_EOK; +} + +static rt_err_t utest_tc_cleanup(void) +{ + return RT_EOK; +} + +static void testcase(void) +{ + UTEST_UNIT_RUN(test_fdb_tsdb_init_ex); + UTEST_UNIT_RUN(test_fdb_tsl_clean); + UTEST_UNIT_RUN(test_fdb_tsl_append); + UTEST_UNIT_RUN(test_fdb_tsl_iter); + UTEST_UNIT_RUN(test_fdb_tsl_iter_by_time); + UTEST_UNIT_RUN(test_fdb_tsl_query_count); + UTEST_UNIT_RUN(test_fdb_tsl_set_status); + UTEST_UNIT_RUN(test_fdb_tsl_clean); +} +UTEST_TC_EXPORT(testcase, "packages.tools.flashdb.tsdb", utest_tc_init, utest_tc_cleanup, 20); + +#endif /* defined(RT_USING_UTEST) && defined(FDB_USING_TSDB) */