☰
给 ArduPilot 加自定义逻辑:一个 .lua 文件跑通飞控二次开发
2026/9/25 9:43:23 网站建设 项目流程

给 ArduPilot 加自定义逻辑:一个 .lua 文件跑通飞控二次开发

【免费下载链接】ardupilotArduPlane, ArduCopter, ArduRover, ArduSub source项目地址: https://gitcode.com/GitHub_Trending/ar/ardupilot

ArduPilot 的固件功能强大,但总有改不动的时候:想提前 30 秒低电压告警、想给日志加一列自己算的字段、想让任务里某个航点触发一段专属逻辑——这些需求去翻 C++ 源码改固件,成本太高。ArduPilot 的 Lua 脚本引擎就是为此设计的:往 SD 卡里丢一个.lua文件,飞控启动时自动加载,脚本通过 ahrs、gcs、voltage_sensor 这些内置对象直接读写飞控状态,不用碰一行 C++。本文带你从"脚本跑起来"到"看懂底层怎么调度的",再到真实场景用法和那些我踩过、你也可能踩的坑。适合刚接触 ArduPilot 二次开发,或者受够了"要加个功能就得重刷固件"的开发者。

30 秒把脚本跑起来 🛫

先看前置条件,两条就够:飞控的 flash 得超过 1MB(F7/H7 这一档基本都满足,AEROFOX-H7、CubeBlack 这类都行),脚本会自动在固件里编译进来,不用你改任何宏;如果你手上没板子,SITL 模拟器天然带脚本支持,可以直接开干。

启用步骤极简:把参数SCR_ENABLE设为 1,重启飞控。系统会自动创建脚本目录——实机上在 SD 卡的/APM/scripts/,SITL 里是工作目录下的scripts/文件夹。往这个目录里丢一个.lua文件,重启后它就活了。下面这个脚本跑通,你就入门了(全文就这几个 API,记住"返回 function + 延迟毫秒"这个节奏就行):

local function update() gcs:send_text(5, "Hello from ArduPilot Lua") return update, 5000 -- 5 秒后再调我一次 end return update, 1000 -- 脚本加载 1 秒后第一次执行

几个关键参数,平时基本不动,遇到对应症状再来改:

参数默认值什么时候碰它
SCR_ENABLE0首次启用时设 1,改完必须重启
SCR_HEAP_SIZE43KB / 100KB / 200KB按内存档位自动给,大脚本 OOM 就加它
SCR_VM_I_COUNT10000单次回调的 VM 指令上限,脚本被"Exceeded CPU time"干掉时相关
SCR_THD_PRIORITYNormal脚本线程优先级,想抢资源再调,默认值就是安全的
SCR_DEBUG_OPTS0按位开关,比如第 1 位打开内存/耗时运行日志

底层是怎么转的 ⚙️

数据流一句话讲清:启动后 AP_Scripting::init() 创建一个低优先级的独立线程,脚本在这个线程里跑,和主控制回路不抢 CPU;然后 lua_scripts 扫一遍脚本目录,把每个.lua文件读进一个独立沙箱环境(脚本之间互相看不到变量,也碰不到危险 API)。

之后就是"回调-调度"循环:每个脚本执行完,用返回值告诉调度器"几毫秒后再叫我";调度器按next_run_ms维护一个有序链表,到点就取一个脚本跑。CPU 方面有个看门狗:每执行SCR_VM_I_COUNT条 VM 指令触发一次 hook,超时直接抛 "Exceeded CPU time" 并把该脚本移除——所以你在 Lua 里写死循环,飞控不会跟着卡死,只是你的脚本会被杀掉。

脚本"够得着"什么,看 bindings 生成的绑定层:ahrs 拿姿态位置、voltage_sensor 拿电池、gcs 发消息、vehicle 改参数,都是对固件 C++ 接口的封装。也就是说,Lua 不是"外挂",它和固件跑在同一颗 MCU 上,只是隔着一层受控的桥。

三个真实场景:告警、遥测、任务联动 🛠

场景一:低电压提前告警。默认 failsafe 的电压阈值是死的,但你常想在触发保护之前先通知地面站"电压开始不对劲了"。思路:低频轮询voltage_sensor:get_voltage(0),低于自己设的线(比如 10.5V)就发 GCS 文本,严重的直接切模式。回调间隔 1 秒足够,别用 10ms——电池电压不会一毫秒变一次:

local function monitor() local v = voltage_sensor:get_voltage(0) if v and v < 10.5 then gcs:send_text(0, "Low batt: " .. v .. " V") end return monitor, 1000 end return monitor, 1000

这种"阈值告警 + 自定义动作"的模式可以套任何传感器:温度、空速、地形高度都一样,换个 API 就行。

场景二:把算过的数据送到地面站或日志。有人要往遥测里塞自定义字段(比如自己算的电机温度均值、航向积分漂移),有人要把传感器原始帧解析后记进 DataFlash。这里推荐先看仓库里现成的 frsky_battery.lua 和 logging.lua:前者演示怎么把数据打包进 CRSF 遥测的自定义包,后者演示log对象怎么写自定义日志消息。套路都是同一个——低频回调里算好,通过绑定 API 发出去,一次别算太多。

场景三:任务指令联动脚本。上面两个场景脚本都是"常驻轮询",还有另一种玩法:脚本平时不做事,等任务(mission)里的 scripting 指令来触发。机制是固件收到任务指令后把它推进一个队列(handle_mission_command),脚本通过 mission 模块拉取并处理。落地效果就是:航线里某个航点"打电话"给你的脚本,脚本执行完自定义逻辑(比如换参数组、起降吊舱、切拍摄参数),再继续飞任务。仓库的 applets 目录 里有一批这种"半成品"——param-lockdown、advance-wp、copter-deadreckon-home这类,读一个现成的比自己从零写快十倍。

翻车现场与自救指南 🩹

启动后 GCS 报 "Unable to allocate memory"。症状是脚本线程直接没起来。方向:SCR_HEAP_SIZE太小,或者你一次性堆了十几个脚本。把堆从 43KB 提到 100KB(改完重启),实在不行先砍脚本数量——堆是固定预分配的,不是弹性内存。

某脚本突然消失,日志报 "Exceeded CPU time"。就是单次回调干太多活了:死循环、大数组、或者 10ms 频率回调里塞了复杂计算。方向:查这个脚本里最重的那段逻辑,把回调间隔拉长;实在需要算的,拆成多次回调分步算。

日志报 "did not return a function" 或 "did not return a delay"。回调里 return 的格式不对——必须是"函数引用 + 数字毫秒"两个值,少一个这个脚本就被永久移除,不会重试。这是新手最高频的翻车点,对着开头那个最小脚本抄格式就对了。

脚本明明放进去了,就是不加载。三连查:文件是不是.lua结尾(点开头隐藏文件不扫);目录对不对(实机/APM/scripts/,SITL 是工作目录下scripts/);SD 卡格式是不是飞控认的。用SCR_DEBUG_OPTS打开第 0 位,加载失败时会有明确提示而不是静默。

解锁(arming)被拦,报错带 "Scripting: ..." 字样。前起检查会把脚本的最后一条错误原样报出来——脚本加载失败会直接阻止解锁,这是保护机制不是 bug。方向:先按上面方法修好脚本错误;另外如果你启过SCR_LD_CHECKSUM/SCR_RUN_CHECKSUM校验和,换过脚本没更新校验和参数也会拦解锁。

接下来去哪 🚦

想系统学,按这个顺序翻:AP_Scripting/README.md 讲构建侧怎么启用;examples 目录 是 API 用法大全,按文件名搜你要的能力(ahrs-*、mission-*、can_*);applets 目录 是可移植场景包,每个都带一份.md说明;想读实现,入口就是 lua_scripts.cpp 的run_next_script,两百行看懂整个调度循环。

现在就动手:把开头那个 7 行的 hello world 丢进 SITL 的scripts/目录,SCR_ENABLE设 1 跑一遍模拟器,看到 "Hello from ArduPilot Lua" 出现在消息框里,这条路径你就走通了。

【免费下载链接】ardupilotArduPlane, ArduCopter, ArduRover, ArduSub source项目地址: https://gitcode.com/GitHub_Trending/ar/ardupilot

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询