STM32F407 + RTX5 + emWin + RL‑FS + RL‑USB 移植 Lua
硬件环境:STM32F407,Keil MDK,CMSIS‑RTX5,RL‑FS 文件系统,emWin,可访问 SD 卡(RL‑FS),可以把.lua脚本放在 SD 卡运行。
Lua 标准版本:Lua 5.4.x(推荐,5.3 也可以;不要用 5.2 及更老版本)。Lua 本身是标准 C 写的,无操作系统依赖,但跑在 RTX5 下要处理内存分配、文件 IO、多线程、栈、RL‑FS 对接。
整体路线:
- 将 Lua 源码加入 Keil 工程
- 修改 Lua 内存分配器,适配 STM32/RTX 堆内存
- 替换 Lua 原生文件 IO,对接 RL‑FS(关键!标准
stdio.h在 RTOS+RL‑FS 下不能直接用) - 创建 RTX5 任务跑 Lua 虚拟机
- 从 SD 卡读取
.lua脚本,加载并执行 - 扩展 C‑Lua 绑定:给 lua 调用 emWin、GPIO、业务函数
- 踩坑:栈大小、内存碎片、CCM‑RAM、内存溢出
一、源码加入工程
下载 Lua5.4 源码,只添加核心源码到 Keil,剔除 lua.c、luac.c(这是 PC 命令行工具,单片机不用)。
需要加入的 .c:
plaintext
lapi.c
lcode.c
lctype.c
ldebug.c
ldo.c
ldump.c
lfunc.c
lgc.c
llex.c
lmem.c
lobject.c
lopcodes.c
lparser.c
lstate.c
lstring.c
ltable.c
ltm.c
lundump.c
lvm.c
lzio.c
不要加 lua.c/luac.c。头文件路径加入
lua/src。
编译宏(Keil C/C++ 预定义)
plaintext
LUA_USE_C89
LUA_COMPAT_5_3
// 不要开启 LUA_USE_POSIX、LUA_USE_DLOPEN,单片机没有动态库
Lua 默认用 libc 的
malloc/free,STM32 RTX5 下不要用标准库 malloc,要接管内存分配。
二、重定向 Lua 内存分配器(最重要)
Lua 内部所有内存由 l_Alloc 回调函数管理。 RTX5 工程,不要用 HAL_Malloc,不要用 libc malloc,优先用 RTX5 的osMemoryAlloc / osMemoryFree。
示例代码,创建一个 lua_mem.c:
c
运行
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"
#include "cmsis_os2.h"
// Lua内存分配回调
static void *stm32_lua_alloc (void *ud, void *ptr, size_t osize, size_t nsize)
{
(void)ud; (void)osize;
if (nsize == 0)
{
osMemoryFree(ptr);
return NULL;
}
if(ptr == NULL)
{
return osMemoryAlloc(nsize);
}
else
{
// realloc:RTX5没有realloc,自己实现:新分配→拷贝→释放旧
void *pnew = osMemoryAlloc(nsize);
if(pnew != NULL)
{
size_t cp = (osize < nsize) ? osize : nsize;
memcpy(pnew, ptr, cp);
osMemoryFree(ptr);
}
return pnew;
}
}
// 创建lua虚拟机,使用RTX内存
lua_State * create_lua_vm(void)
{
lua_State *L = lua_newstate(stm32_lua_alloc, NULL);
if(L)
{
luaL_openlibs(L); // 打开标准库(io、base、table等)
}
return L;
}
⚠️关键点:RTX5 的
osMemoryAlloc取自 RTX 的堆;RTX 堆大小要给 Lua 预留足够内存。 建议给 Lua 虚拟机:64KB~128KB 堆,你的 F407 内部 SRAM 紧张,如果 Lua 占用大,注意和 emWin/RL‑FS 抢内存。
GC:Lua 自带垃圾回收,在 RTX 任务循环里要定期调用
lua_gc(L, LUA_GCSTEP, 10);,否则内存只涨不释放。
三、替换 Lua IO 库:对接 RL‑FS 文件系统(大坑)
Lua 默认io.open()调用 C 库fopen/fread,在 Keil+RL‑FS 环境下,标准 stdio FILE 是无效的,RL‑FS 有自己的fopen()(RL‑FS 的 stdio 重定向)。
两种方案:
方案 1(简单,RL‑FS 已经重定向 ANSI stdio)
RL‑FS 开启 ANSI 接口后,fopen/fread/fwrite实际映射到 RL‑FS,此时 Lua io 库可以直接跑。
RL‑FS 配置:勾选 Enable ANSI File Functions。 风险:RTOS 多任务下文件操作没有互斥锁,多任务同时读写 SD 卡会崩溃。
方案 2(推荐,自定义 Lua IO 绑定)
屏蔽原生io库,自己写 C 函数注册到 Lua,直接调用 RL‑FS API:fsOpen, fsRead, fsWrite, fsClose,并且加 RTX 互斥锁保护文件操作。
示例示意:
c
运行
// RL‑FS头文件
#include "rl_fs.h"
// RTX互斥锁,保护SD/RL‑FS访问
osMutexId_t fs_mutex;
static int l_fs_open(lua_State *L)
{
const char *fname = luaL_checkstring(L,1);
const char *mode = luaL_optstring(L,2,"r");
osMutexAcquire(fs_mutex, osWaitForever);
FILE *fp = fopen(fname, mode); // RL‑FS ANSI fopen
osMutexRelease(fs_mutex);
if(!fp) return luaL_error(L,"open failed");
// 把FILE*作为userdata压入lua
FILE **fud = (FILE**)lua_newuserdata(L, sizeof(FILE*));
*fud = fp;
// 设置元表做gc自动close
luaL_getmetatable(L, "RLFS_FILE");
lua_setmetatable(L, -2);
return 1;
}
然后用lua_register注册到 lua 环境。
重要:所有 RL‑FS SD 卡访问必须加互斥锁,因为 RTX 多任务:GUI 任务、USB 任务、Lua 任务可能同时访问 SD 卡,不加锁会直接 hardfault。
四、在 RTX5 任务中运行 Lua 虚拟机
❗千万不要在中断、GUI 回调里跑 Lua 脚本;Lua 虚拟机是非线程安全的,一个 Lua_State 只能在同一个 RTX 任务内执行。
新建 RTX 任务:Lua 脚本执行任务 c 运行
#define LUA_TASK_STACK_SIZE (8*1024) // lua解析需要栈,至少8KB,建议12KB
osThreadId_t luaTaskHandle;
void luaTask(void *arg)
{
(void)arg;
lua_State *L = create_lua_vm();
if(!L)
{
while(1) osDelay(100);
}
// 从SD卡加载脚本 /test.lua
int ret = luaL_dofile(L, "/test.lua");
if(ret != LUA_OK)
{
// 打印lua错误信息
const char *err = lua_tostring(L,-1);
// 可以输出串口
lua_pop(L,1);
}
while(1)
{
// 定期执行GC
lua_gc(L, LUA_GCSTEP, 20);
osDelay(10);
}
lua_close(L);
}
// 创建任务
void luaTaskInit(void)
{
fs_mutex = osMutexNew(NULL);
const osThreadAttr_t attr = {
.stack_size = LUA_TASK_STACK_SIZE,
.priority = osPriorityNormal2
};
luaTaskHandle = osThreadNew(luaTask, NULL, &attr);
}
⚠️任务栈大小:Lua 解析脚本会消耗 C 栈,栈不能小于 8KB,复杂脚本建议 12KB。栈太小直接 HardFault。
luaL_dofile 说明
luaL_dofile(L, path) 等价于 luaL_loadfile + lua_pcall。 luaL_loadfile内部调用 io 读取文件;如果你没有做好 RL‑FS 的 io 适配,这个函数直接报错打不开 SD 卡文件。
备选:自己用 RL‑FS 读取整个脚本到内存 buffer,再调用
luaL_loadbuffer(L, buf, len, "script"),这种方式不依赖 lua io 库,更稳定,强烈推荐产品使用。
示例:读 SD 卡到内存 buffer 再跑脚本:c 运行
// 1.RL‑FS读取 /test.lua 到uint8_t script_buf[]
// 2.执行
int ret = luaL_loadbuffer(L, (const char*)script_buf, script_len, "user_script");
if(ret == LUA_OK)
{
ret = lua_pcall(L,0,0,0);
}
if(ret != LUA_OK)
{
const char* err = lua_tostring(L,-1);
lua_pop(L,1);
}
这种方式绕开 Lua IO 库,减少移植坑。
五、C 与 Lua 互相调用(对接 emWin、业务逻辑)
C 给 Lua 提供函数,例如调用 emWin 画字符串 c 运行
#include "GUI.h"
static int l_gui_drawtext(lua_State *L)
{
int x = luaL_checkinteger(L,1);
int y = luaL_checkinteger(L,2);
const char *txt = luaL_checkstring(L,3);
// emWin调用,注意:emWin本身也需要RTOS保护!
osMutexAcquire(gui_mutex, osWaitForever);
GUI_DispStringAt(txt, x, y);
osMutexRelease(gui_mutex);
return 0;
}
// 在创建虚拟机后注册
lua_register(L, "gui_drawtext", l_gui_drawtext);
Lua 脚本里就可以直接写:lua
gui_drawtext(10,10,"hello lua on stm32f407")
emWin 多任务访问同样需要互斥锁!Lua 任务和 GUI 任务都调用 emWin,不加锁会花屏崩溃。
Lua 调用 C 注意点
- 所有访问共享外设资源(emWin、SD、USB)的 C 导出函数,入口加 RTX 互斥锁。
- 不要长时间阻塞在 C 函数中,会卡死 Lua 虚拟机。
六、RAM 占用评估(加到你现有系统)
Lua5.4 虚拟机基础开销:
- 虚拟机结构体静态:~2‑3KB
- 堆内存:看脚本;简单小程序:20‑40KB;复杂脚本会冲到 60‑80KB
- RTX Lua 任务栈:8‑12KB
加到你之前系统:RTX5+emWin+RLFS+RLUSB 已经吃掉约 100KB 内部 SRAM;
STM32F407 内部 128KB SRAM,如果 Lua 再吃 40KB,内部 RAM 会吃紧甚至溢出。
优化方案二选一:
- 把 Lua 内存分配到外部 FSMC SRAM(修改
stm32_lua_alloc,用外部 SRAM 的内存池,这是最佳方案) - 限制 Lua 最大内存,
lua_gc(L, LUA_GCSETPAUSE, 80); lua_gc(L, LUA_GCSETSTEPMUL, 200);,强制频繁 GC。
禁止把 Lua 大内存放到 CCM‑RAM,CCM 不能 DMA,而且部分库函数不兼容。
七、高频踩坑清单(这个组合坑很多)
- Lua 任务栈太小 → HardFault:最少 8KB,复杂脚本 12KB。
- RL‑FS 没有互斥锁,多任务读写 SD 卡 → 随机死机。
- emWin 无互斥锁,Lua 任务和 GUI 任务同时绘图 → 花屏崩溃。
- 使用 libc 的 malloc 而不是 RTX 内存分配,内存碎片严重,内存泄漏。
- 不要多线程操作同一个
lua_State,Lua 虚拟机不是线程安全;多脚本就多创建独立虚拟机,每个虚拟机绑定一个 RTX 任务。 - 不要在 emWin 回调、中断回调里面调用任何 Lua API。
- RL‑FS 打开文件记得 close;Lua userdata 元表设置__gc 自动关闭文件句柄,防止句柄泄露。
- RAM 紧张:强烈建议 Lua 堆放到外部 SRAM,否则 F407 的 128KB 很快耗尽。
八、最简运行流程总结
- Lua 源码加入工程,重写
l_Alloc使用 RTX 内存。 - RTX 新建独立 Lua 任务,任务栈≥8KB。
- RL‑FS 读取 SD 卡
.lua脚本全部读到内存 buffer。 - 使用
luaL_loadbuffer+lua_pcall执行脚本。 - C 导出接口给 Lua,所有硬件 /emWin/FS 访问加 RTX 互斥锁。
- 任务循环定期调用 Lua GC。
如果你需要,我可以给你:
- 最小可直接复制的完整
lua_port.c移植模板; - 测试用的
test.lua示例; - 外部 SRAM 作为 Lua 内存池的修改代码。
更多推荐

所有评论(0)