4. ArduPilot Lua运行环境

ArduPilot 的 Lua 脚本并不是“随便丢进飞控就能跑”的普通文件,它运行在飞控固件内部一个受控的沙箱环境中。理解这个运行环境的边界,是写出稳定脚本的前提。本节围绕五个关键问题展开:脚本放在哪里、上电后如何被加载、可用内存有多少、脚本以什么节奏执行、以及如何看到调试输出。

一、脚本存放位置:SD 卡是唯一入口

ArduPilot 的 Lua 脚本统一存放在飞控 SD 卡的 /APM/scripts/ 目录下。飞控启动时,文件系统会挂载 SD 卡,Lua 引擎随后扫描该目录。需要注意几个硬性规则:

  • 脚本扩展名必须是 .lua,其他扩展名不会被加载。
  • 目录名必须是 scripts,且位于 APM 目录内,路径大小写敏感。
  • 脚本文件名建议使用小写字母、数字和下划线,避免空格和中文。
  • 脚本数量不宜过多,每个脚本都会占用内存和启动时间。

典型目录结构如下:

/APM/
├── logs/
├── terrain/
└── scripts/
    ├── hello.lua
    ├── battery_monitor.lua
    └── mission_utils.lua

如果 SD 卡没有正确挂载,或者目录名写成 Script、lua,脚本都不会被执行。这是初学者最常见的“脚本没反应”原因之一。

二、启动流程:从固件初始化到脚本执行

Lua 脚本的启动不是独立进程,而是嵌入在 ArduPilot 主循环中的。整体流程可以概括为:固件启动 → 文件系统挂载 → Lua 引擎初始化 → 扫描脚本目录 → 逐个加载脚本 → 注册回调 → 进入主循环调度。

ArduPilot Lua 脚本启动与执行流程 飞控上电 固件初始化 挂载 SD 卡 /APM/scripts/ Lua 引擎初始化 分配内存池 扫描脚本目录 加载 .lua 文件 注册回调函数 update / init / loop 进入主循环调度 按周期触发脚本 执行脚本逻辑 读取传感器 / 控制 调试输出 gcs:send_text / 日志 内存限制 脚本池约 100KB

脚本加载后,通常会注册 update() 或 loop() 回调。ArduPilot 主循环以固定频率调用这些回调,脚本本身不需要写 while true 死循环,否则会阻塞整个调度器。

三、内存限制:约 100KB 的脚本池

Lua 脚本运行在独立的内存池中,不同飞控平台略有差异,但通常可用内存约为 100KB 左右。这个数字看起来不大,但足够运行多个中小型脚本。真正的问题在于内存碎片和不当使用。

资源项 典型限制 说明
Lua 内存池 约 100KB 所有脚本共享,不是每个脚本独立
单脚本大小 建议 < 20KB 过大脚本加载慢、易失败
脚本数量 建议 ≤ 5 个 过多会拖慢启动
全局变量 尽量避免 全局变量常驻内存,增加碎片
字符串拼接 避免循环内拼接 产生大量临时对象

常见误区:在 update() 中频繁创建 table、字符串或闭包,会快速消耗内存并导致脚本被卸载。应尽量复用变量,把常量定义在脚本顶层。

四、执行周期:脚本不是实时任务

Lua 脚本由 ArduPilot 调度器按周期调用,常见周期为 10Hz 到 50Hz,具体取决于脚本注册方式和飞控负载。它不适合做微秒级实时控制,更适合做逻辑判断、状态监控、任务辅助和参数调整。

一个典型的周期脚本结构如下:

local counter = 0

function update()
    counter = counter + 1
    if counter % 10 == 0 then
        local volt = battery:voltage()
        if volt < 10.5 then
            gcs:send_text(0, "Battery low: " .. volt)
        end
    end
    return update, 100  -- 100ms 后再次调用
end

return update, 1000

这里的 return update, 100 表示下一次调用间隔为 100ms,即约 10Hz。脚本必须返回下一次调用的延迟,否则不会继续执行。这个机制让脚本可以灵活控制自己的执行频率,但也要求开发者明确知道自己在做什么。

五、调试输出方式:gcs:send_text 与日志

Lua 脚本最常用的调试手段是 gcs:send_text(),它会把消息发送到地面站的消息栏。参数为消息级别和字符串:

gcs:send_text(0, "INFO: script started")
gcs:send_text(4, "WARN: battery voltage low")
gcs:send_text(6, "ERROR: sensor read failed")

级别数字越大,消息越严重。0 为普通信息,4 为警告,6 为错误。地面站和日志中都能看到这些消息。除此之外,还可以使用 gcs:send_named_float() 发送命名浮点值,方便在日志中绘图分析。

调试建议:不要在每个周期都发送消息,否则会刷屏并占用通信带宽。可以用计数器降频,例如每 10 次循环发送一次。调试完成后及时删除或注释掉调试输出。

六、案例与复盘:一个电池监控脚本

假设我们要写一个电池低压告警脚本,放在 /APM/scripts/battery_monitor.lua:

local LOW_VOLT = 10.5
local count = 0

function update()
    count = count + 1
    if count % 20 == 0 then
        local volt = battery:voltage()
        if volt < LOW_VOLT then
            gcs:send_text(4, "Battery low: " .. volt .. "V")
        end
    end
    return update, 100
end

return update, 1000

复盘要点:脚本放在正确目录;使用局部变量减少内存占用;每 20 次循环才检查一次,降低负载;返回下一次调用间隔;调试信息只在异常时发送。如果脚本没有运行,依次检查 SD 卡挂载、目录名、扩展名、脚本语法和内存是否超限。

掌握脚本存放位置、启动流程、内存限制、执行周期和调试输出这五个要素,就能让 Lua 脚本在 ArduPilot 中稳定运行,而不是变成“写了但没反应”的黑盒。