3. Lua语言快速入门

ArduPilot 的 Lua 脚本运行在飞控固件内置的 Lua 解释器上,受限于内存与实时性,脚本必须短小、确定、可预测。因此学习 Lua 不能只停留在“能跑”,而要理解它的数据类型、控制流与表结构如何影响脚本在飞控中的执行行为。本节围绕语法基础、数据类型、控制结构、函数、表与模块化六个要点展开,帮助你把 Lua 写成适合飞控环境的自动化脚本。

一、语法基础:轻量、动态、以块为界

Lua 是动态类型语言,变量无需声明类型,赋值即创建。语句之间不需要分号,代码块用 then/end、do/end、function/end 成对界定。注释用 -- 单行、--[[ ]] 多行。局部变量用 local 声明,这是飞控脚本中最重要的习惯:全局变量会长期占用内存并可能被其他脚本污染,而 local 变量在作用域结束后可被回收。

-- 推荐:局部变量,作用域清晰
local count = 0
local name = "copter"

-- 不推荐:全局变量,长期驻留内存
count = 0
name = "copter"

二、数据类型:八种基础类型与飞控常用值

Lua 共有 nil、boolean、number、string、table、function、userdata、thread 八种类型。飞控脚本中最常用的是 number、boolean、string、table 和 function。需要注意:Lua 5.3 之后 number 区分整数与浮点,而 ArduPilot 使用的版本对整数运算更友好;字符串可用单引号或双引号,也可用 .. 拼接;nil 表示“不存在”,常用于判断参数是否有效。

类型示例飞控脚本中的典型用途
nillocal x = nil表示未初始化或参数缺失
booleanlocal armed = true解锁状态、条件判断
numberlocal alt = 12.5高度、速度、PID 参数
stringlocal mode = "AUTO"模式名、日志文本
tablelocal p = {k=1}参数集合、配置表、队列
functionlocal f = function() end回调、定时任务、事件处理

三、控制结构:条件、循环与提前返回

Lua 提供 if/elseif/else、while、repeat/until、for 四种控制结构。飞控脚本中应优先使用 for 与 if,避免死循环和长时间阻塞。特别要注意:脚本运行在调度器中,任何循环都必须有明确退出条件,否则会拖慢甚至阻塞飞控主循环。

-- 数值 for:遍历 1 到 5
for i = 1, 5 do
  gcs:send_text(0, "index " .. i)
end

-- 泛型 for:遍历表
local params = {kp = 0.8, ki = 0.2, kd = 0.05}
for key, value in pairs(params) do
  gcs:send_text(0, key .. "=" .. value)
end

-- 条件判断:提前返回,减少嵌套
local function check_alt(alt)
  if alt == nil then return false end
  if alt < 5 then return false end
  return true
end
在飞控脚本中,while true do end 是危险写法。若确实需要周期执行,应使用 ArduPilot 提供的 update() 回调或定时器,而不是自己写死循环。

四、函数:一等公民与回调机制

Lua 中函数是一等值,可以赋值给变量、作为参数传递、作为返回值。ArduPilot 脚本框架正是利用这一点,把 update()、init() 等函数注册为回调。函数支持多返回值、可变参数 ... 和闭包。闭包在飞控中常用于保存状态,例如记录上一次的飞行模式或累计计数。

-- 多返回值
local function min_max(a, b)
  if a < b then return a, b end
  return b, a
end

-- 闭包保存状态
local function make_counter()
  local n = 0
  return function()
    n = n + 1
    return n
  end
end

local counter = make_counter()
gcs:send_text(0, "count=" .. counter())

五、表:Lua 唯一的数据结构

表(table)是 Lua 的核心,既可作为数组,也可作为字典、对象、模块。表用 {} 创建,索引从 1 开始,支持 # 取长度、table.insert 插入、table.remove 删除、ipairs 顺序遍历、pairs 键值遍历。在飞控脚本中,表常用于组织参数、航点列表、状态缓存和配置项。

-- 数组式表
local waypoints = {10, 20, 30}
table.insert(waypoints, 40)
gcs:send_text(0, "len=" .. #waypoints)

-- 字典式表
local config = {
  mode = "AUTO",
  speed = 8.5,
  failsafe = true
}
gcs:send_text(0, config.mode)

六、模块化编程:require 与返回表

模块化让脚本可复用、可测试。Lua 的模块本质是“返回一个表的文件”,通过 require 加载。ArduPilot 脚本目录中,可以把公共函数放入独立文件,再在主脚本中引入。模块内部应使用 local 隐藏实现细节,只暴露必要接口。

-- 文件:my_utils.lua
local M = {}

function M.clamp(v, lo, hi)
  if v < lo then return lo end
  if v > hi then return hi end
  return v
end

return M

-- 主脚本中引用
local utils = require("my_utils")
local out = utils.clamp(15, 0, 10)
ArduPilot 的 Lua 环境对模块路径和内存有限制,模块文件不宜过多、过大;避免在模块顶层执行耗时操作,所有初始化应放在函数中按需调用。

七、案例与复盘:一个模式监控脚本

下面这个脚本综合了本节知识点:用局部变量保存状态,用表组织配置,用函数封装逻辑,用条件与循环判断飞行模式变化,并通过 gcs:send_text 输出提示。它体现了飞控脚本“短小、确定、可读”的编写原则。

local config = { report_interval = 100 }
local last_mode = nil
local counter = 0

local function update()
  counter = counter + 1
  if counter % config.report_interval ~= 0 then return end

  local mode = vehicle:get_mode()
  if mode ~= last_mode then
    gcs:send_text(0, "mode changed to " .. mode)
    last_mode = mode
  end
end

return update, 1000

复盘要点:第一,所有状态变量都用 local,避免全局污染;第二,用表集中管理配置,便于修改;第三,用取模运算控制上报频率,避免刷屏;第四,函数职责单一,便于调试。常见误区包括:把 update() 写成阻塞循环、在循环中频繁创建表导致内存抖动、忘记处理 nil 导致脚本报错退出。掌握这些基础后,后续章节的参数读写、MAVLink 通信与任务控制都会建立在这一套 Lua 语法与模块化思维之上。

ArduPilot Lua 脚本结构与执行关系 语法基础 local / 注释 / 块结构 动态类型、无需分号 数据类型 nil / boolean / number string / table / function 控制结构 if / for / while 提前返回、避免阻塞 函数 一等值 闭包 / 回调 表 table 数组 / 字典 / 对象 / 模块 #、insert、remove、pairs 配置表、航点表、状态缓存 模块化 require 返回表的文件即模块 local 隐藏实现细节 公共函数放入 my_utils.lua 飞控脚本执行入口 init() 初始化 → update() 周期回调 gcs:send_text 输出 · vehicle:get_mode 读取状态 短小、确定、可预测,避免死循环与内存抖动