4、PX4 PWM输出驱动框架:uORB消息与PWM输出的映射关系、输出驱动层代码结构
好,咱们今天聊点实在的。PWM输出驱动,说白了就是让飞控板上的电机或者舵机动起来。你想想看,飞控大脑(比如位置控制器)算出来一个油门值,比如0.5,这个值怎么变成电机上实实在在的1500us脉宽?中间这一层,就是咱们今天要讲的输出驱动框架。
我个人习惯把PX4的PWM输出分成三层看:上层是uORB消息层,中间是输出驱动抽象层,底层是硬件寄存器操作层。今天咱们重点聊前两层——消息怎么映射,代码怎么组织。
4.1 uORB消息与PWM输出的映射关系
先问一个问题:飞控里谁在发PWM指令?答案是混控器(Mixer)。混控器从uORB总线订阅actuator_controls消息,然后根据混控矩阵算出每个通道的输出值,再发布成actuator_outputs消息。驱动层就盯着这个actuator_outputs消息,把它转成硬件PWM信号。
我截个关键的消息结构给你看:
// 来自 uORB/topics/actuator_outputs.h
struct actuator_outputs_s {
uint64_t timestamp; // 时间戳
uint32_t noutputs; // 有效输出通道数
float output[ACTUATOR_OUTPUTS_MAX]; // 输出值,范围0~1
};
这里有个坑,我当年踩过。output数组的值是归一化的,范围0~1。但电机需要的PWM脉宽通常是1000us~2000us。这个映射关系在哪做?
嗯,答案是在驱动层。驱动里有一个缩放因子,把0~1映射到实际脉宽。比如:
pwm_value = (int)(1000 + output * 1000); // 0→1000us, 1→2000us
但注意,不同执行机构映射不一样。舵机可能是500~2500us,电调可能是1100~1900us。所以驱动里通常会有一个参数表,比如PWM_MIN和PWM_MAX,允许你在启动时配置。
核心映射流程:
- 混控器订阅
actuator_controls(来自控制器的期望力矩) - 混控器计算后发布
actuator_outputs(归一化0~1) - 输出驱动订阅
actuator_outputs,按参数映射为PWM脉宽 - 驱动调用硬件接口输出PWM波形
你可能会问:为什么中间要插一层actuator_outputs,而不是直接让混控器输出PWM?
我个人觉得,这是为了解耦。混控器只关心数学计算,不关心硬件是PWM还是DShot还是CAN。驱动层只关心怎么把0~1变成物理信号。各司其职,好维护。
4.2 输出驱动层代码结构
好,咱们看看代码怎么组织的。PX4的输出驱动在src/drivers/目录下,按硬件平台分。比如:
src/drivers/dshot/— DShot协议驱动src/drivers/pwm_out/— 普通PWM输出src/drivers/px4io/— 协处理器IO扩展
每个驱动里,核心文件就那么几个。我以pwm_out为例,给你拆开看看:
src/drivers/pwm_out/
├── CMakeLists.txt
├── PwmOut.cpp // 主驱动类
├── PwmOut.hpp // 头文件
└── pwm_out_params.c // 参数定义
PwmOut.cpp 里干了三件事:
- 初始化:打开定时器,配置PWM模式,设置频率和极性
- 订阅消息:在
Run()函数里等待actuator_outputs消息 - 输出更新:收到消息后,遍历每个通道,计算脉宽,写入定时器比较寄存器
我摘一段核心代码,你感受一下:
void PwmOut::Run()
{
// 阻塞等待新消息
if (_actuator_outputs_sub.update(&_actuator_outputs)) {
// 遍历所有有效输出通道
for (unsigned i = 0; i < _actuator_outputs.noutputs; i++) {
// 归一化值转PWM脉宽
float value = _actuator_outputs.output[i];
uint16_t pwm = _pwm_min + (uint16_t)(value * (_pwm_max - _pwm_min));
// 写入定时器比较寄存器
up_pwm_servo_set(i, pwm);
}
}
}
注意那个up_pwm_servo_set()函数,它是硬件抽象层(HAL)的接口。不同芯片(STM32、Kinetis、Raspberry Pi)的实现不一样,但接口统一。这就是PX4驱动设计的精髓——平台无关性。
我的小技巧: 调试PWM输出时,别一上来就接电机。先用示波器看波形,确认频率和脉宽对不对。我曾经有一次把频率设成50Hz(舵机标准),结果电调不认,折腾了半天才发现电调要400Hz。
4.3 定时器与PWM通道的绑定
说到硬件,就绕不开定时器。每个PWM通道背后都是一个定时器通道。比如STM32F4,TIM1的CH1~CH4可以输出4路PWM。驱动里怎么知道哪个定时器对应哪个通道?
答案在板级配置文件里。以boards/px4/fmu-v5为例:
// board_config.h
#define PWM_TIMER1 TIM1
#define PWM_TIMER1_CHANNELS (1 << 0 | 1 << 1 | 1 << 2 | 1 << 3)
#define PWM_TIMER2 TIM8
#define PWM_TIMER2_CHANNELS (1 << 0 | 1 << 1)
这样驱动初始化时,就知道:TIM1的CH0~CH3是前4个输出通道,TIM8的CH0~CH1是后2个。这种设计的好处是,换一块板子,只需要改board_config.h,驱动代码不用动。
注意: 不同定时器的时钟源可能不同,导致PWM频率不一致。比如TIM1挂在APB2上,TIM8挂在APB1上,频率可能差一倍。如果你要求所有通道频率一致,必须选同一个定时器,或者用同一个时钟源。
4.4 输出驱动层的整体架构图
说了这么多,我画张图帮你理清关系。这张图展示了从uORB消息到硬件PWM的完整链路:
这张图从左到右展示了数据流。你注意看,混控器是承上启下的关键节点。它把控制器的期望(力矩、推力)转成了执行机构能理解的输出值。而驱动层只负责最后一步——把0~1变成实实在在的PWM波。
4.5 实际项目中的避坑指南
最后,分享几个我踩过的坑,希望能帮你省点时间:
- 通道顺序搞反:混控器输出的通道0对应物理PWM通道几?不同板子定义不一样。我建议你开机后先打印
actuator_outputs的值,再用示波器逐个通道确认。 - 频率不匹配:电调通常要400Hz,舵机要50Hz。如果你混用,必须分两组定时器。我曾经图省事,所有通道用一个频率,结果舵机抖得跟筛子似的。
- 启动时序:电调上电后需要先发一个低油门信号(比如1000us)持续几秒,才能进入正常工作模式。驱动初始化时别忘了这个“校准”过程。
调试小技巧: 在PwmOut::Run()里加一个计数器,每输出一次就打印。这样你能看到输出频率是否稳定。如果发现丢帧,八成是uORB消息队列满了,调大ORB_QUEUE_LENGTH试试。
好了,关于PWM输出驱动框架,核心就是这些。记住三个关键词:uORB消息、混控器、定时器映射。搞懂这三者的关系,你就能自己写一个输出驱动了。
公众号:蓝海资料掘金营,微信deep3321