目录
模块类型对比
C 模块 vs Lua 模块
LuatOS 中有两种类型的模块,使用方式完全不同:
| 特征 | C 模块(如 gpio, ledstrip) | Lua 模块(如 sys) |
|---|---|---|
| 文件类型 | .c 文件 | .lua 文件 |
| 注册方式 | luaL_requiref(L, name, func, 1) | 内嵌到 VFS 虚拟文件系统 |
| 是否需要 require | ❌ 不需要(已全局注册) | ✅ 需要 |
| 使用方式 | gpio.setup() 直接用 | local sys = require("sys") 后使用 |
| 示例模块 | gpio, uart, log, timer, ledstrip | sys, sysplus |
| 存储位置 | 编译到固件中(代码段) | 编译到固件中(字节码数组) |
使用示例
-- ✅ C 模块 - 不需要 require(直接使用全局变量)
gpio.setup(8, 1)
log.info("test", "hello")
ledstrip.init(8, 0, ledstrip.TX, 2)
-- ✅ Lua 模块 - 需要 require
local sys = require("sys")
sys.taskInit(function()
sys.wait(1000)
log.info("sys", "hello")
end)
sys.run()C 模块的注册机制
在 luat_base_idf5.c 中,所有 C 模块通过 luaL_requiref() 注册为全局变量:
static const luaL_Reg loadedlibs[] = {
{"gpio", luaopen_gpio}, // GPIO模块
{"uart", luaopen_uart}, // 串口模块
{"log", luaopen_log}, // 日志模块
{"ledstrip", luaopen_ledstrip}, // LED灯带模块
// ... 更多模块
{NULL, NULL}
};
void luat_openlibs(lua_State *L) {
const luaL_Reg *lib;
for (lib = loadedlibs; lib->func; lib++) {
luaL_requiref(L, lib->name, lib->func, 1); // glb=1 表示注册为全局变量
lua_pop(L, 1);
}
}luaL_requiref() 的第四个参数 glb 决定是否需要 require:
glb = 1(true): 模块注册为全局变量,不需要requireglb = 0(false): 模块只在package.loaded中,需要require
sys 模块加载流程
完整流程图
编译时
↓
1. sys.lua 被编译成字节码
↓
2. 字节码嵌入到 C 数组中 (luat_inline_libs.c)
↓
固件启动
↓
3. luat_vfs_init() 初始化 VFS
↓
4. 注册内联文件系统 (vfs_fs_inline)
↓
5. 挂载到 /lua/ 路径
↓
Lua 运行时
↓
6. require("sys") 被调用
↓
7. Lua 搜索路径: /lua/sys.lua
↓
8. 从内存中读取字节码
↓
9. 加载并执行,返回 sys 模块详细说明
1. 编译时嵌入(构建阶段)
文件 LuatOS/script/corelib/sys.lua 被编译成字节码并嵌入到 C 代码中:
// LuatOS/luat/vfs/luat_inline_libs.c
const char luat_inline2_sys[] = {
0x1B, 0x4C, 0x75, 0x61, 0x53, 0x00, ... // Lua 5.3 字节码
};
const luadb_file_t luat_inline2_libs[] = {
{.name="sys.lua", .size=4969, .ptr=luat_inline2_sys},
{.name="sysplus.lua", .size=2657, .ptr=luat_inline2_sysplus},
{.name="", .size=0, .ptr=NULL} // 结束标记
};字节码头部格式:
0x1B, 0x4C, 0x75, 0x61, 0x53 // "\x1BLuaS" - Lua 5.3 字节码魔数
0x00, 0x19, 0x93 // 版本信息
// ... 后续是指令和常量表2. VFS 初始化(固件启动时)
在 bsp_c3/luatos/components/luat/port/luat_fs_idf5.c 中:
int luat_fs_init(void) {
// 初始化 VFS
luat_vfs_init(NULL);
// 注册 spiffs 文件系统
luat_vfs_reg(&vfs_fs_spiffs);
// 在 luat_vfs.c 的 mount 函数中自动挂载内联文件系统:
// vfs.mounted[1].prefix = "/lua/"
// vfs.mounted[1].fs = &vfs_fs_inline
}VFS 自动挂载逻辑(luat_vfs.c):
int luat_fs_mount(luat_fs_conf_t *conf) {
// ... 挂载第一个文件系统后
#ifdef __LUATOS__
if (j == 0) {
// 自动挂载内嵌文件系统到 /lua/
vfs.mounted[j+1].fs = &vfs_fs_inline;
vfs.mounted[j+1].ok = 1;
memcpy(vfs.mounted[j+1].prefix, "/lua/", strlen("/lua/") + 1);
}
#endif
}3. Lua 模块搜索(运行时)
当执行 require("sys") 时,Lua 按以下路径搜索(loadlib.c):
static const char* search_paths[] = {
"/%s.luac", "/%s.lua", // 根目录
"/luadb/%s.luac", "/luadb/%s.lua", // luadb 分区
"/lua/%s.luac", "/lua/%s.lua", // ← sys.lua 在这里找到!
};搜索 require("sys") 的过程:
- 尝试打开
/lua/sys.lua - VFS 匹配到
/lua/前缀 - 调用
vfs_fs_inline的文件操作函数 - 从内存数组
luat_inline2_sys读取字节码 - 执行并返回模块
4. 内联文件系统实现
// LuatOS/luat/vfs/luat_fs_inline.c
FILE* luat_vfs_inline_fopen(void* userdata, const char *filename, const char *mode) {
const luadb_file_t* file = luat_inline2_libs;
// 遍历查找匹配的文件名
while (file->ptr != NULL) {
if (!memcmp(file->name, filename, strlen(filename)+1)) {
// 找到了!创建文件描述符
luat_fs_inline_t* fd = luat_heap_malloc(sizeof(luat_fs_inline_t));
fd->ptr = file->ptr; // 字节码指针
fd->size = file->size; // 文件大小
fd->offset = 0;
return (FILE*)fd;
}
file++;
}
return NULL; // 未找到
}
// 读取字节码
size_t luat_vfs_inline_fread(void* userdata, void *ptr, size_t size, size_t nmemb, FILE *stream) {
luat_fs_inline_t* fd = (luat_fs_inline_t*)stream;
size_t read_size = size * nmemb;
if (fd->offset + read_size > fd->size) {
read_size = fd->size - fd->offset;
}
memcpy(ptr, fd->ptr + fd->offset, read_size); // 从内存复制数据
fd->offset += read_size;
return read_size;
}字节码生成机制
生成工具
工具链:
- 主程序:
luatos_32bit.exe(LuatOS 自身的解释器) - 脚本:
LuatOS/script/update_inline_libs.lua - 核心函数: Lua 标准库的
string.dump()
工作原理
核心代码(update_inline_libs.lua)
-- 配置编译模式
local bittype = "32bit" -- ESP32 使用 32 位模式
-- 遍历 corelib 目录下的所有 .lua 文件
local files = {}
lsdir("corelib", files, true)
-- 生成 C 文件
local f = io.open("..\\luat\\vfs\\luat_inline_libs.c", "wb")
for _, value in ipairs(files) do
-- 关键步骤:将 Lua 源码编译为字节码
local lf = loadfile("corelib\\" .. value) -- 加载 Lua 文件
local data = string.dump(lf, true) -- 编译成字节码!
-- 写入 C 数组
f:write("const char luat_inline2_" .. value:sub(1, -5) .. "[] = {\r\n")
for i = 0, #data - 1 do
f:write(string.format("0x%02X, ", data:byte(i+1)))
end
f:write("};\r\n\r\n")
end
-- 生成文件表
f:write("const luadb_file_t luat_inline2_libs[] = {\r\n")
f:write(" {.name=\"sys.lua\",.size=4969, .ptr=luat_inline2_sys},\r\n")
f:write(" {.name=\"sysplus.lua\",.size=2657, .ptr=luat_inline2_sysplus},\r\n")
f:write(" {.name=\"\",.size=0,.ptr=NULL}\r\n")
f:write("};\r\n")string.dump() 函数
string.dump(function [, strip]) 是 Lua 5.3 的标准库函数:
- 将 Lua 函数编译成字节码
strip = true表示去除调试信息(减小体积)- 返回值是包含字节码的字符串
三种编译模式
-- 模式配置
-- local bittype = "64bit_size32" -- 64位虚拟机, 32位size_t
local bittype = "32bit" -- ✅ 默认: 32位 (ESP32使用)
-- local bittype = "source" -- 保存源码而非字节码
if bittype == "source" then
data = io.readFile("corelib\\" .. value) -- 直接读取源码
else
local lf = loadfile("corelib\\" .. value)
data = string.dump(lf, true) -- 编译为字节码
end生成的文件:
32bit模式 →luat_inline_libs.c64bit_size32模式 →luat_inline_libs_64bit_size32.csource模式 →luat_inline_libs_source.c
字节码格式
Lua 5.3 字节码头部:
0x1B ESC 字符(标识字节码开始)
0x4C 0x75 0x61 "Lua"
0x53 "S"
0x00 版本号(5.3)
0x19 0x93 格式版本
... 指令和常量表VFS 虚拟文件系统
架构
用户代码: require("sys")
↓
Lua 包加载器: 搜索 /lua/sys.lua
↓
VFS 层: 路径匹配 /lua/ → vfs_fs_inline
↓
内联文件系统: 从 luat_inline2_libs 查找
↓
内存读取: luat_inline2_sys 字节码数组
↓
Lua 虚拟机: 加载并执行字节码VFS 挂载点
| 挂载点 | 文件系统 | 说明 |
|---|---|---|
/ | spiffs | SPIFFS 文件系统(Flash) |
/lua/ | inline | 内嵌文件系统(内存中的字节码) |
/luadb/ | romfs/luadb | 脚本分区(可选) |
/sd/ | fatfs | SD 卡(需手动挂载) |
关键配置
在 luat_conf_bsp.h 中:
#define LUAT_USE_FS_VFS 1 // 启用 VFS
#define LUAT_USE_VFS_INLINE_LIB 1 // 启用内联库
#define LUA_USE_VFS_FILENAME_OFFSET 1 // 文件名偏移处理如何更新 sys 模块
方法一:使用官方工具(推荐)
步骤:
下载 LuatOS 工具
https://gitee.com/openLuat/LuatOS/attach_files 下载 luatos_32bit.exe修改源文件
编辑 LuatOS/script/corelib/sys.lua重新生成字节码
cd LuatOS\script luatos_32bit.exe update_inline_libs.lua重新编译固件
cd bsp_c3\luatos idf.py build
方法二:手动编译(高级)
如果你熟悉 Lua,可以自己编写脚本:
-- compile_sys.lua
local lf = loadfile("sys.lua")
local bytecode = string.dump(lf, true)
-- 写入 C 文件
local f = io.open("luat_inline_libs.c", "wb")
f:write("const char luat_inline2_sys[] = {\n")
for i = 1, #bytecode do
f:write(string.format("0x%02X, ", bytecode:byte(i)))
if i % 8 == 0 then f:write("\n") end
end
f:write("};\n")
f:close()生成文件说明
LuatOS/script/update_inline_libs.lua
↓ 执行后生成 ↓
LuatOS/luat/vfs/luat_inline_libs.c ← 这个文件会被编译进固件注意事项:
- ⚠️ 每次修改
sys.lua后,必须重新运行update_inline_libs.lua - ⚠️ 生成的 C 文件会自动包含在编译过程中
- ✅ 不需要手动修改 CMakeLists.txt
优势总结
sys 模块采用内嵌字节码的优势:
- ✅ 无需外部文件 - sys.lua 嵌入固件,不占用 Flash 文件系统空间
- ✅ 快速加载 - 直接从内存读取,无需 Flash I/O
- ✅ 节省空间 - 字节码比源码小(约 30-40%)
- ✅ 透明使用 - 对用户来说就像普通的
require() - ✅ 无法被误删 - 固件内嵌,不会被用户脚本覆盖
- ✅ 启动必需 - sys 是核心库,确保总是可用
与其他方案对比:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 内嵌字节码(sys) | 快速、可靠、节省空间 | 更新需重新编译固件 |
| Flash 文件(用户脚本) | 易于更新 | 可能被删除、启动慢 |
| C 模块(gpio) | 最快、最小 | 开发成本高、不灵活 |
附录:相关文件索引
关键源文件
| 文件路径 | 说明 |
|---|---|
LuatOS/script/corelib/sys.lua | sys 模块源码 |
LuatOS/script/update_inline_libs.lua | 字节码生成脚本 |
LuatOS/luat/vfs/luat_inline_libs.c | 生成的字节码 C 数组 |
LuatOS/luat/vfs/luat_fs_inline.c | 内联文件系统实现 |
LuatOS/luat/vfs/luat_vfs.c | VFS 核心逻辑 |
LuatOS/lua/src/loadlib.c | Lua 包加载器(模块搜索路径) |
bsp_c3/luatos/components/luat/port/luat_base_idf5.c | C 模块注册 |
bsp_c3/luatos/components/luat/port/luat_fs_idf5.c | ESP32 VFS 初始化 |
bsp_c3/luatos/include/luat_conf_bsp.h | BSP 配置文件 |
工具下载
- luatos_32bit.exe: https://gitee.com/openLuat/LuatOS/attach_files
- LuatOS 源码: https://gitee.com/openLuat/LuatOS
总结
sys 模块的加载路径:
require("sys")
↓
Lua 搜索 /lua/sys.lua
↓
VFS 路由到 vfs_fs_inline
↓
从内存数组 luat_inline2_sys 读取字节码
↓
Lua 虚拟机执行字节码
↓
返回 sys 模块表核心机制:
- 使用 Lua 标准库的
string.dump()将源码编译为字节码 - 通过 VFS 虚拟文件系统将内存数组伪装成文件
- Lua 的
require()透明地从"虚拟文件"加载模块
这是一个精巧的设计,兼顾了性能、可靠性和易用性!

