跳到主要内容

文件存储

概述

本章演示 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 字节,不支持 nilfunctionuserdata

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 加锁警告

合宙所有模组的 OTP 加锁操作不可逆!一旦锁定,该区域将变为永久只读,无法再擦除或重写。加锁前必须确认数据正确。本 Demo 绝不调用 otp.lock()


全部代码

main.lua

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
-- 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_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_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数值存取简单,无需管理文件格式
设备序列号 / IMEIOTP固化后不可篡改,安全性高
加密密钥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/