文件存储
概述
本章演示 YFAir8000(Air8000W 模组)的三种内部存储方式。由于 YFAir8000 网关没有 TF 卡 / SD 卡槽位,所有数据持久化均依赖模组内部 Flash。
LuatOS 为 Air8000 提供了三种内部存储方案:
| 存储方式 | 特点 | 典型用途 |
|---|---|---|
| 文件系统 (io) | 通用文件读写,支持目录操作 | 日志文件、配置文件、资源文件 |
| 键值对存储 (fskv) | KV 数据库,掉电不丢失 | WiFi 配置、设备参数、状态标记 |
| OTP | 一次性可编程区域,可加锁变只读 | 设备序列号、加密密钥、出厂信息 |
- 运行环境:LuatOS(合宙 Air8000W 模组)
- 存储介质:模组内部 Flash
- 文件系统:littlefs (lfs) + luadb (只读) + ram (内存)
Air8000W 模组 Flash 空间有限,不同固件版本可用空间不同。写入前建议通过 io.fsstat() 或 fskv.status() 检查剩余空间。
👉 如需了解 SD 卡 / TF 卡等外部存储方式,请参考 合宙 Air8000 存储相关官方文档。
文件系统布局
LuatOS 采用虚拟文件系统 (VFS) 统一管理多种存储介质,Air8000W 内部可用的挂载路径如下:
/ # 根分区,可读写(littlefs 格式)
├── luadb/ # 只读,刷机时写入的脚本和资源文件
└── ram/ # 内存文件系统,可读写,掉电丢失(部分固件支持)
| 路径 | 格式 | 读/写 | 掉电保存 | 说明 |
|---|---|---|---|---|
/ | littlefs | ✅/✅ | ✅ | 通用可写分区,日志/配置存放位置 |
/luadb/ | TLD 只读 | ✅/❌ | ✅ | 刷机时打包写入的脚本和资源文件 |
/ram/ | 内存 | ✅/✅ | ❌ | 占用 RAM,不消耗 Flash 写入寿命 |
程序流程
main.lua
│
├─ require("fs_demo") → 文件系统测试
│ ├─ 创建 /storage_demo 目录
│ ├─ 写入并读取 config.txt
│ ├─ 启动次数计数(bootcount.txt)
│ └─ 列举目录内容
│
├─ require("fskv_demo") → 键值对存储测试
│ ├─ fskv.init() 初始化
│ ├─ 设置多种类型键值对(字符串/数值/浮点/布尔/table)
│ ├─ 读取 & table 内部字段修改(sett)
│ ├─ 遍历所有键 & 删除键 & 查看状态
│ └─
│
├─ require("otp_demo") → OTP 一次性编程测试
│ ├─ 读取 Zone 0/1/2 前 64 字节
│ ├─ 向 Zone 2 写入测试数据
│ └─ 回读验证
│
└─ sys.run()
项目结构
03-文件存储/
├── main.lua # 主程序入口,加载三个 demo
├── fs_demo.lua # 文件系统(io)演示
├── fskv_demo.lua # 键值对存储(fskv)演示
└── otp_demo.lua # OTP 一次性编程演示
核心代码详解
1. 文件系统 — 目录创建与文件读写(fs_demo.lua)
文件系统是最通用的存储方式,语法兼容 Lua 原生 io 模块。
-- 创建目录
local dir = "/storage_demo"
if not io.dexist(dir) then
io.mkdir(dir)
end
-- 写入文件
local f = io.open(dir .. "/config.txt", "wb")
f:write("device=Air8000W\nversion=1.0.0\n")
f:close()
-- 读取文件
f = io.open(dir .. "/config.txt", "rb")
local content = f:read("*a") -- 读取全部内容
f:close()
启动计数演示了文件持久化能力——每次重启后读取计数值、加 1 再写回:
local boot_path = dir .. "/bootcount.txt"
local count = 0
f = io.open(boot_path, "rb")
if f then
count = tonumber(f:read("*a")) or 0
f:close()
end
count = count + 1
f = io.open(boot_path, "wb")
f:write(tostring(count))
f:close()
| 模式 | 说明 |
|---|---|
"rb" | 只读,二进制 |
"wb" | 写入,截断为 0 字节(覆盖) |
"wb+" | 读写,截断为 0 字节 |
"a" | 追加写入 |
2. 键值对存储 — fskv(fskv_demo.lua)
fskv 是 LuatOS 内置的 KV 数据库,全称为 Flash Key-Value,数据存储在 Flash 的 KV 分区中,掉电不丢失。支持字符串、数值、浮点、布尔、table 五种类型,单个 value 最大 4095 字节。
-- 初始化
fskv.init()
-- 设置多种类型
fskv.set("device", "Air8000W") -- 字符串
fskv.set("boot_count", 1) -- 整数
fskv.set("temperature", 25.5) -- 浮点
fskv.set("online", true) -- 布尔
fskv.set("config", { server = "mqtt.example.com", port = 1883 }) -- table
-- 读取
local cfg = fskv.get("config")
log.info("FSKV", "MQTT:", cfg.server, cfg.port)
-- 修改 table 内部字段(sett)
fskv.set("settings", { wifi = "on", mode = "auto" })
fskv.sett("settings", "mode", "manual") -- 只改 mode 字段
-- 删除键
fskv.del("online")
-- 查看状态
local used, total, cnt = fskv.status()
fskv 单个 value 最大支持 4095 字节,不支持 nil、function、userdata。
3. OTP 一次性编程(otp_demo.lua)
OTP(One-Time Programmable)是模组内的特殊存储区域,适合存放需要永久固化的关键数据。一旦调用 otp.lock() 加锁就不可逆,变为永久只读。
本 Demo 仅演示读取和写入(不加锁),每个 zone 约 256 字节,写入长度需按 4 字节对齐。
-- 读取 Zone 0/1/2 前 64 字节(Zone 0/1 通常含芯片信息)
for zone = 0, 2 do
local data = otp.read(zone, 0, 64)
if data then
log.info("OTP", "Zone", zone, "HEX:", data:toHex())
end
end
-- 向 Zone 2 写入测试数据
local test_data = "YFAir8000W_DEMO_001"
otp.write(2, test_data, 0)
-- 回读验证
local verify = otp.read(2, 0, #test_data)
log.info("OTP", verify == test_data and "✓ 验证成功" or "✗ 验证失败")
合宙所有模组的 OTP 加锁操作不可逆!一旦锁定,该区域将变为永久只读,无法再擦除或重写。加锁前必须确认数据正确。本 Demo 绝不调用 otp.lock()。
全部代码
main.lua
PROJECT = "FileStorage"
VERSION = "1.0.0"
require("fs_demo")
require("fskv_demo")
require("otp_demo")
if wdt then
wdt.init(9000)
sys.timerLoopStart(wdt.feed, 3000)
end
log.info("main", PROJECT, VERSION)
log.info("main", "三种存储方式:FS文件系统 / FSKV键值对 / OTP一次性编程")
sys.run()
fs_demo.lua
-- fs_demo.lua - 内置 Flash 文件系统演示
-- 功能:目录创建、文件读写、启动计数、目录列举
sys.taskInit(function()
sys.wait(1000)
log.info("FS", "========== 文件系统测试开始 ==========")
local dir = "/storage_demo"
-- 1. 创建目录
if not io.dexist(dir) then
log.info("FS", "创建目录:", tostring(io.mkdir(dir)))
end
-- 2. 写入并读取文件
local path = dir .. "/config.txt"
local f = io.open(path, "wb")
if f then
f:write("device=Air8000W\nversion=1.0.0\n")
f:close()
log.info("FS", "写入完成:", path)
end
f = io.open(path, "rb")
if f then
log.info("FS", "读取内容:", f:read("*a"))
f:close()
end
-- 3. 启动次数计数
local boot_path = dir .. "/bootcount.txt"
local count = 0
f = io.open(boot_path, "rb")
if f then
count = tonumber(f:read("*a")) or 0
f:close()
end
count = count + 1
log.info("FS", "第", count, "次启动")
f = io.open(boot_path, "wb")
if f then
f:write(tostring(count))
f:close()
end
-- 4. 列举目录(io.lsdir 返回 bool, data)
local ok, files = io.lsdir(dir, 20, 0)
if ok and type(files) == "table" then
log.info("FS", "目录内容:")
for _, item in ipairs(files) do
if type(item) == "table" then
log.info("FS", " ", item.name, item.size or 0, "字节")
end
end
else
log.info("FS", "列举目录失败或返回格式异常, ok=", tostring(ok))
end
log.info("FS", "========== 文件系统测试完成 ==========")
end)
fskv_demo.lua
-- fskv_demo.lua - FSKV 键值对存储演示
-- 功能:初始化、存取多种类型、table操作、遍历、删除
-- 适用场景:设备配置、校准参数等轻量数据
-- 安全转字符串(fskv.get 删除后可能返回不可 tostring 的值)
local function safeStr(v)
if v == nil then return "nil" end
local ok, s = pcall(tostring, v)
return ok and s or "<unknown>"
end
sys.taskInit(function()
sys.wait(2000)
log.info("FSKV", "========== FSKV 键值对存储测试开始 ==========")
-- 1. 初始化
local ok = fskv.init()
log.info("FSKV", "初始化:", tostring(ok))
-- 2. 设置多种类型
fskv.set("device", "Air8000W")
fskv.set("boot_count", 1)
fskv.set("temperature", 25.5)
fskv.set("online", true)
fskv.set("config", { server = "mqtt.example.com", port = 1883 })
log.info("FSKV", "已设置 5 个键值对")
-- 3. 读取数据
log.info("FSKV", "device =", safeStr(fskv.get("device")))
log.info("FSKV", "boot_count =", safeStr(fskv.get("boot_count")))
log.info("FSKV", "temperature=", safeStr(fskv.get("temperature")))
log.info("FSKV", "online =", safeStr(fskv.get("online")))
local cfg = fskv.get("config")
if cfg then
log.info("FSKV", "config.server =", cfg.server)
log.info("FSKV", "config.port =", tostring(cfg.port))
end
-- 4. Table 内部字段操作
fskv.set("settings", { wifi = "on", mode = "auto" })
fskv.sett("settings", "mode", "manual")
local s = fskv.get("settings")
if s then
log.info("FSKV", "settings.mode =", s.mode, "(期望 manual)")
end
-- 5. 遍历所有键
log.info("FSKV", "--- 遍历所有键 ---")
local iter = fskv.iter()
if iter then
while true do
local k = fskv.next(iter)
if not k then break end
local v = fskv.get(k)
log.info("FSKV", " ", k, "=", type(v) == "table" and "<table>" or safeStr(v))
end
end
-- 6. 删除键并验证
fskv.del("online")
log.info("FSKV", "删除 online 后:", safeStr(fskv.get("online")), "(期望 nil)")
-- 7. 查看状态
local used, total, cnt = fskv.status()
log.info("FSKV", "状态: 已用=" .. tostring(used) .. " 总量=" .. tostring(total) .. " 键数=" .. tostring(cnt))
log.info("FSKV", "========== FSKV 键值对测试完成 ==========")
end)
otp_demo.lua
-- otp_demo.lua - OTP 一次性可编程存储演示
-- 功能:读取 OTP 区域、写入数据、回读验证
--
-- ⚠️ OTP 一旦加锁(otp.lock)就永久只读,本 Demo 绝不调用 otp.lock!
-- 适合存储:设备序列号、加密密钥、校准数据、出厂信息
-- 每个 zone 约 256 字节,写入长度需按 4 字节对齐
sys.taskInit(function()
sys.wait(3000)
log.info("OTP", "========== OTP 一次性编程存储测试开始 ==========")
-- 1. 读取 Zone 0/1/2 前 64 字节
for zone = 0, 2 do
local data = otp.read(zone, 0, 64)
if data then
log.info("OTP", "Zone", zone, "HEX:", data:toHex())
else
log.info("OTP", "Zone", zone, "读取为空(未写入或已锁定)")
end
end
-- 2. 向 Zone 2 写入测试数据(Zone 0/1 通常含芯片信息)
local test_zone = 2
local test_data = "YFAir8000W_DEMO_001"
log.info("OTP", "写入 Zone", test_zone, "数据:", test_data)
local ret = otp.write(test_zone, test_data, 0)
log.info("OTP", "写入结果:", tostring(ret))
-- 3. 回读验证
local verify = otp.read(test_zone, 0, #test_data)
if verify then
log.info("OTP", "回读:", verify)
log.info("OTP", verify == test_data and "✓ 验证成功" or "✗ 验证失败")
else
log.info("OTP", "回读失败,Zone", test_zone, "可能已锁定")
end
log.info("OTP", "========== OTP 测试完成 ==========")
end)
存储方式选择指南
| 需求场景 | 推荐方式 | 原因 |
|---|---|---|
| WiFi / MQTT 等设备配置 | fskv | 键值对存取方便,掉电保持,API 简洁 |
| 运行日志记录 | 文件系统 (io) | 支持追加写入,按行读取,适合大量文本 |
| JSON 配置文件 | 文件系统 (io) | 整文件读写,支持 JSON 序列化 |
| 启动计数 / 状态标记 | fskv | 数值存取简单,无需管理文件格式 |
| 设备序列号 / IMEI | OTP | 固化后不可篡改,安全性高 |
| 加密密钥 | OTP | 加锁后防逆向窃取 |
| 临时缓存数据 | /ram/ 文件系统 | 不消耗 Flash 写入寿命,但掉电丢失 |
- 小型配置参数(WiFi 密码、MQTT 地址、上报间隔等)优先使用
fskv,API 最简单 - 日志和大数据使用文件系统
io,支持追加和按行读取 - 出厂信息使用 OTP 固化,确保不可篡改
- 写入前始终检查剩余空间:
fskv.status()或io.fsstat("/") - Flash 有写入寿命限制,避免高频写入,建议批量写入或使用
/ram/缓存后定期落盘
更多资料
| 资料 | 链接 |
|---|---|
| Air8000 存储相关官方文档 | https://docs.openluat.com/air8000/luatos/app/mulu/fs/ |