1. 项目背景与核心需求
在无人机、机器人等嵌入式控制系统中,传统遥控器信号(PWM)与CAN总线通信协议的转换一直是个痛点。许多飞控系统(如ArduPilot、PX4)虽然支持CAN总线扩展,但原生遥控器输入仍依赖PWM信号。这个项目通过APM(ArduPilot Mega)飞控的LUA脚本环境,实现了PWM信号到CAN协议信号的实时转换,为多设备协同控制提供了新思路。
我曾在一个农业无人机项目中遇到过这个问题:需要将遥控器摇杆信号通过CAN总线分发给多个执行器节点。当时市面上现成的PWM-CAN转换器要么延迟高,要么配置不灵活。最终选择在APM飞控上开发LUA脚本方案,实测信号转换延迟<5ms,完全满足实时控制需求。
2. 硬件架构与信号流设计
2.1 系统组成要素
- 信号输入:标准RC遥控器(如FrSky Taranis)通过PPM/PWM接收机(如SBUS)连接APM飞控
- 处理核心:APM飞控(如Pixhawk 4)运行ArduCopter固件,启用LUA脚本环境
- 信号输出:飞控内置CAN接口或通过CAN转接模块(如CANable USB适配器)
- 终端设备:支持CAN协议的ESC电调、伺服舵机或传感器节点
2.2 信号转换流程
code复制遥控器PWM → 接收机SBUS → APM飞控 → LUA脚本解析 → CAN报文封装 → CAN总线 → 终端设备
关键点在于:
- PWM信号在APM中会被归一化为-100%~+100%的数值
- LUA脚本通过
rc:get_pwm()获取各通道原始值 - 根据CAN协议规范重组数据帧
- 使用
can:send()函数发送报文
3. LUA脚本实现详解
3.1 基础脚本框架
lua复制-- 初始化CAN总线(500kbps标准速率)
local can_port = CAN.get_device("can0")
CAN.init(can_port, 500000)
-- 主循环函数
function update()
-- 获取第1通道PWM值(单位:微秒)
local pwm1 = rc:get_pwm(1)
-- 转换为百分比(假设中立点1500μs)
local percent1 = (pwm1 - 1500) / 500 * 100
-- 构建CAN报文(标准帧,ID 0x201)
local can_msg = CANFrame.new(0x201, 8)
can_msg:data(0, percent1) -- 第1字节存储通道1数据
-- 可继续添加其他通道数据...
-- 发送CAN报文
CAN.send(can_port, can_msg)
return update, 10 -- 10ms周期执行
end
return update()
3.2 关键函数解析
-
PWM信号获取:
rc:get_pwm(ch)获取指定通道原始PWM脉宽(μs)- 典型值范围:1000μs(最小)~2000μs(最大),中立点1500μs
-
CAN报文构造:
CANFrame.new(id, dlc)创建新帧id:11位标准帧标识符(建议0x200-0x2FF用于遥控信号)dlc:数据长度(1-8字节)
-
数据映射技巧:
lua复制-- 将-100%~+100%映射到0-255整型(节省带宽) local byte_val = math.floor((percent1 + 100) * 1.275) can_msg:data(0, byte_val)
4. CAN协议设计规范
4.1 报文ID分配方案
| ID范围 | 用途 | 优先级 |
|---|---|---|
| 0x200-0x20F | 摇杆通道1-16 | 高 |
| 0x210-0x21F | 开关/旋钮通道 | 中 |
| 0x2F0-0x2FF | 系统状态/心跳包 | 低 |
4.2 数据帧布局示例(8字节)
| 字节 | 内容 | 说明 |
|---|---|---|
| 0 | 通道1值(0-255) | 对应摇杆X轴 |
| 1 | 通道2值(0-255) | 对应摇杆Y轴 |
| 2 | 通道3值(0-255) | 油门通道 |
| 3 | 通道4值(0-255) | 航向通道 |
| 4 | 开关状态(bitmask) | 每位对应一个两档开关 |
| 5 | 旋钮1(0-255) | 模拟旋钮输入 |
| 6 | CRC校验低字节 | 异或校验 |
| 7 | CRC校验高字节 | 求和校验 |
实际项目中建议添加时间戳字段(2字节),用于接收端检测信号丢失
5. 性能优化与实测数据
5.1 延迟测试方法
- 在遥控器端接示波器探头测量PWM输出沿
- 在CAN接收端捕捉报文到达时间
- 计算两者时间差(包含无线传输+飞控处理+CAN传输)
5.2 优化措施与效果
| 优化项 | 原始延迟 | 优化后 | 方法说明 |
|---|---|---|---|
| LUA执行周期 | 20ms | 5ms | 调整return update返回值 |
| CAN总线速率 | 250kbps | 1Mbps | 需终端设备支持 |
| 数据压缩 | 8字节 | 4字节 | 使用位域存储开关状态 |
| 禁用调试输出 | 2ms | 0.1ms | 移除所有print()语句 |
实测在Pixhawk 4硬件上,最优配置可实现3.8ms端到端延迟,完全满足大多数实时控制场景。
6. 典型问题排查指南
6.1 CAN报文发送失败
现象:CAN.send()返回false
- 检查步骤:
- 确认CAN接口初始化成功:
CAN.get_device()返回值非nil - 测量CAN总线终端电阻(应≈60Ω)
- 用CAN分析仪抓包观察总线活动
- 确认CAN接口初始化成功:
案例:曾遇到因CAN线序接反导致通信失败,症状是逻辑分析仪能看到波形但设备无响应。解决方法:
lua复制-- 在脚本开头添加硬件检测
if not CAN.init(can_port, 500000) then
gcs:send_text(0, "CAN init failed!")
end
6.2 信号跳变不稳定
现象:接收端数值出现毛刺
- 可能原因:
- PWM输入信号受到干扰(检查接收机天线)
- LUA执行周期不稳定(使用
system:ticks()监测) - CAN总线负载过高(统计带宽使用率)
优化代码:
lua复制local last_ticks = system:ticks()
function update()
local now = system:ticks()
if now - last_ticks > 15 then -- 超过15ms视为异常
gcs:send_text(0, "LUA loop delay!")
end
last_ticks = now
-- ...原有逻辑...
end
7. 扩展应用场景
7.1 多设备同步控制
通过CAN广播特性,单个遥控器可同时控制:
- 无人机云台舵机
- 机械臂关节
- 灯光控制系统
- 载荷投放机构
实现技巧:
lua复制-- 使用不同CAN ID区分设备类型
local device_ids = {
gimbal = 0x210,
arm = 0x220,
light = 0x230
}
-- 发送云台控制指令
local gimbal_msg = CANFrame.new(device_ids.gimbal, 2)
gimbal_msg:data(0, pitch_value)
gimbal_msg:data(1, yaw_value)
CAN.send(can_port, gimbal_msg)
7.2 双向通信增强
在CAN协议中增加反馈通道:
- 终端设备发送状态报文(如电流、温度)
- LUA脚本解析后通过遥测链路回传地面站
- 实现遥控器振动提示等交互功能
反馈处理示例:
lua复制-- 注册CAN接收回调
CAN.register_rx_callback(function(msg)
if msg:id() == 0x2F0 then -- 状态报文ID
local temp = msg:data(0)
gcs:send_text(0, "Motor temp:"..temp.."C")
end
end)
8. 硬件选型建议
8.1 飞控兼容性列表
| 飞控型号 | CAN接口类型 | 最大LUA频率 |
|---|---|---|
| Pixhawk 4 | 双CAN FD | 1kHz |
| Cube Orange | 1xCAN 2.0B | 500Hz |
| Matek H743-Wing | 1xCAN 2.0A | 200Hz |
| Omnibus F4 Pro | 需外接转换器 | 100Hz |
8.2 CAN收发器推荐
- TJA1050:经典选择,最高1Mbps,适合大多数应用
- TCAN1042:支持CAN FD,抗干扰能力强
- MCP2562:3.3V供电,适合小型化设计
注意:长距离传输(>10m)建议使用带隔离的CAN模块,如Waveshare ISO1050
9. 开发调试技巧
9.1 实时监控方法
-
Mission Planner日志:
- 启用"LUA Script"日志项
- 监控脚本内存和CPU使用率
-
CAN总线分析仪:
- 推荐使用PCAN-USB或cantools
- 过滤特定ID报文:
candump can0 -i 0x200:0x2FF
-
LUA调试输出:
lua复制-- 条件编译调试语句 local DEBUG = true if DEBUG then gcs:send_text(0, string.format("CH1:%.1f%%", percent1)) end
9.2 性能瓶颈定位
通过system:ticks()测量关键段耗时:
lua复制function update()
local t1 = system:ticks()
-- ...代码段A...
local t2 = system:ticks()
gcs:send_text(0, "A段耗时:"..(t2-t1).."ms")
-- ...代码段B...
local t3 = system:ticks()
gcs:send_text(0, "B段耗时:"..(t3-t2).."ms")
end
10. 安全注意事项
-
信号失效保护:
- 在CAN报文中添加心跳机制
- 接收端应检测超时(>100ms无更新触发安全模式)
-
范围限制:
lua复制-- 强制限制输出范围 percent1 = math.max(-100, math.min(100, percent1)) -
总线负载管理:
- 单个脚本CAN带宽占用建议<30%
- 计算示例:
lua复制local can_load = (8*8)/(bitrate/1000) * freq -- 单位:% -- 8字节报文,1Mbps,100Hz发送频率 => 6.4%负载
在实际部署中,建议先在地面进行充分测试,特别是验证以下场景:
- 遥控器信号丢失后的行为
- CAN总线短路的系统反应
- 高负载下的延迟变化