硬件环境: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 对接

整体路线:

  1. 将 Lua 源码加入 Keil 工程
  2. 修改 Lua 内存分配器,适配 STM32/RTX 堆内存
  3. 替换 Lua 原生文件 IO,对接 RL‑FS(关键!标准stdio.h在 RTOS+RL‑FS 下不能直接用)
  4. 创建 RTX5 任务跑 Lua 虚拟机
  5. 从 SD 卡读取.lua脚本,加载并执行
  6. 扩展 C‑Lua 绑定:给 lua 调用 emWin、GPIO、业务函数
  7. 踩坑:栈大小、内存碎片、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_pcallluaL_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 注意点

  1. 所有访问共享外设资源(emWin、SD、USB)的 C 导出函数,入口加 RTX 互斥锁
  2. 不要长时间阻塞在 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 会吃紧甚至溢出

优化方案二选一:

  1. 把 Lua 内存分配到外部 FSMC SRAM(修改stm32_lua_alloc,用外部 SRAM 的内存池,这是最佳方案)
  2. 限制 Lua 最大内存,lua_gc(L, LUA_GCSETPAUSE, 80); lua_gc(L, LUA_GCSETSTEPMUL, 200);,强制频繁 GC。

禁止把 Lua 大内存放到 CCM‑RAM,CCM 不能 DMA,而且部分库函数不兼容。


七、高频踩坑清单(这个组合坑很多)

  1. Lua 任务栈太小 → HardFault:最少 8KB,复杂脚本 12KB。
  2. RL‑FS 没有互斥锁,多任务读写 SD 卡 → 随机死机
  3. emWin 无互斥锁,Lua 任务和 GUI 任务同时绘图 → 花屏崩溃
  4. 使用 libc 的 malloc 而不是 RTX 内存分配,内存碎片严重,内存泄漏。
  5. 不要多线程操作同一个lua_State,Lua 虚拟机不是线程安全;多脚本就多创建独立虚拟机,每个虚拟机绑定一个 RTX 任务。
  6. 不要在 emWin 回调、中断回调里面调用任何 Lua API。
  7. RL‑FS 打开文件记得 close;Lua userdata 元表设置__gc 自动关闭文件句柄,防止句柄泄露。
  8. RAM 紧张:强烈建议 Lua 堆放到外部 SRAM,否则 F407 的 128KB 很快耗尽。

八、最简运行流程总结

  1. Lua 源码加入工程,重写l_Alloc使用 RTX 内存。
  2. RTX 新建独立 Lua 任务,任务栈≥8KB。
  3. RL‑FS 读取 SD 卡.lua脚本全部读到内存 buffer。
  4. 使用luaL_loadbuffer + lua_pcall执行脚本。
  5. C 导出接口给 Lua,所有硬件 /emWin/FS 访问加 RTX 互斥锁。
  6. 任务循环定期调用 Lua GC。

如果你需要,我可以给你:

  1. 最小可直接复制的完整lua_port.c移植模板;
  2. 测试用的test.lua示例;
  3. 外部 SRAM 作为 Lua 内存池的修改代码。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐