拿
boards/micoair/h743-v2当例子,这一篇先说 PX4 的硬件抽象层(HAL)长什么样,再顺着"加一路 PWM"这个最小需求,看改一块飞控到底要动哪些文件。
1. PX4 的代码组织(三层视角)
第一次打开 PX4 仓库容易迷路,其实跟硬件相关的就三个根目录:
| 目录 | 角色 | 改它的频率 |
|---|---|---|
boards/<vendor>/<board>/ | 板级:每块飞控一份 | 加新板必改 |
platforms/ | HAL:把外设抽象成可复用的 C++ 类 | 加通用能力才动 |
src/drivers/、src/modules/ | 设备驱动与飞行控制模块 | 加新传感器/新算法时改 |
硬件的真相都在板级目录里。HAL 看到的 io_timers、i2c_hw_description,都是板级文件用 constexpr 表填出来的,HAL 自己不写死。
1.1 HAL 内部的分层
platforms/nuttx/src/px4/(NuttX 是 PX4 跑的 RTOS,所有 STM32 飞控都在它上面):
platforms/nuttx/src/px4/
├── common/ # 跨架构共用:board_ctrl, hrt, cpuload, console_buffer
├── stm/
│ ├── stm32_common/ # STM32 跨系列共用
│ │ ├── dshot/ # DSHOT 协议驱动
│ │ ├── hrt/ # 高分辨率定时器
│ │ ├── io_pins/ # PWM/输入捕获/触发(io_timer.c, pwm_servo.c)
│ │ ├── led_pwm/ # LED PWM
│ │ ├── spi/ # SPI 总线抽象
│ │ ├── tone_alarm/ # 蜂鸣器
│ │ └── ...
│ ├── stm32f4/ stm32f7/ stm32h7/ # 系列特定(目前主要是 ADC 寄存器布局)
│ └── stm32_common/include/px4_arch/
│ ├── io_timer.h # MAX_IO_TIMERS / MAX_TIMER_IO_CHANNELS 宏
│ ├── io_timer_hw_description.h
│ ├── dshot.h
│ ├── spi_hw_description.h
│ ├── i2c_hw_description.h
│ └── ...
几个关键点:
io_timer不用 STM32 寄存器名,靠Timer::Timer1这种枚举和initIOTimer/initIOTimerChannel两个constexpr工厂,把硬件信息写到io_timers[]和timer_io_channels[]两个数组里。io_timer.c启动时读这两张表,再去配TIMx->CCMR、CCER、DMA、中断向量。MAX_IO_TIMERS和MAX_TIMER_IO_CHANNELS这两个宏是 HAL 留给板级的容量口子,定义在io_timer.h:48-55:#ifdef BOARD_NUM_IO_TIMERS #define MAX_IO_TIMERS BOARD_NUM_IO_TIMERS #else #define MAX_IO_TIMERS 2 #endif #if DIRECT_PWM_OUTPUT_CHANNELS > 8 #define MAX_TIMER_IO_CHANNELS DIRECT_PWM_OUTPUT_CHANNELS #else #define MAX_TIMER_IO_CHANNELS 8 #endifHAL 不自己定这两个数,直接读
board_config.h里的BOARD_NUM_IO_TIMERS和DIRECT_PWM_OUTPUT_CHANNELS。把板级宏改大,io_timer.c里的for (int i = 0; i < MAX_TIMER_IO_CHANNELS; i++)就跟着扩,HAL 一行不用动。src/drivers/pwm_out/PWMOut.cpp也用DIRECT_PWM_OUTPUT_CHANNELS决定_pwm_mask和setMaxNumOutputs,上层(mixer、control_allocator、uORB 的actuator_outputs)在编译期就拿到 11 个槽位。
一句话:PX4 的 HAL 几乎不写死硬件,只给一组 constexpr 工厂,让板级用表格把拓扑交出来。
2. 新增一块飞控要写哪些文件
boards/micoair/h743-v2 这个目录下的文件,就是一块飞控要准备的全部家当:
boards/micoair/h743-v2/
├── default.px4board # 普通固件要启用的 Kconfig 模块
├── bootloader.px4board # Bootloader 固件 Kconfig(更精简)
├── firmware.prototype # bin 文件命名/版本原型
├── extras/ # 额外烧录资产(如出厂 bootloader bin)
│
├── init/
│ ├── rc.board_defaults # 板级默认参数(param set-default)
│ ├── rc.board_sensors # 启动传感器驱动(bmi088, spl06, ...)
│ └── rc.board_extras # 板级额外动作(OSD、外部外设)
│
├── nuttx-config/
│ ├── include/
│ │ ├── board.h # 时钟/SDIO/RCC 等 NuttX 通用板级头
│ │ └── board_dma_map.h # DMA 通道映射(给 SPI/UART 选 DMA 流)
│ ├── nsh/defconfig # 应用固件的 NuttX 内核配置
│ ├── bootloader/defconfig # Bootloader 的 NuttX 配置
│ └── scripts/
│ ├── script.ld # 应用固件链接脚本
│ └── bootloader_script.ld # Bootloader 链接脚本
│
└── src/
├── CMakeLists.txt # 把这些 .c/.cpp 编进 drivers_board 库
├── board_config.h # 板级核心定义:IO 口、ADC、PWM 数、串口映射
├── hw_config.h # Bootloader 用的 VID/PID/CPU 型号等
├── init.c # 早期初始化(board_app_initialize)
├── bootloader_main.c # Bootloader 入口
├── led.c # 板级 LED 驱动(继而注册到 drv_led)
├── usb.c # USB 识别/OTG 初始化
├── sdio.c # SD 卡底层
├── i2c.cpp # I2C 总线注册(板级 bus 列表)
├── spi.cpp # SPI 总线注册(含 DMA、片选)
└── timer_config.cpp # PWM/IO Timer 表(io_timers + timer_io_channels)
后面实操只动带 ★ 的两个文件。
2.1 几类文件的分工
按职责归纳一下,加板就是这五类:
- 构建/裁剪(Kconfig 层)
default.px4board/bootloader.px4board:逐行CONFIG_xxx=y决定启用哪些 drivers/modules,本质是make menuconfig的快照。
- 系统集成脚本(RC 层)
init/rc.board_*:NuttX 启动后由/etc/init.d/rcS依次调用,负责用命令行拉起 sensor、设默认 param。
- NuttX 内核配置
nuttx-config/nsh/defconfig:管CONFIG_STM32H7_TIM4=y这类 NuttX 自己的 Kconfig。PWM 用到的定时器必须在这里打开,NuttX 才会给它分配中断向量和时钟门。
- 板级 C/C++ 代码
src/board_config.h:宏王国,GPIO 复用、ADC 通道、PWM 通道数、/dev/ttyS<n>映射都在这里。src/timer_config.cpp:整篇的戏眼,后面单独讲。src/init.c:stm32_boardinitialize在 NuttX 起来后第一个被调用,负责复位 PWM、把 GPIO 配成默认状态、初始化 SPI/USB。
- 链接与烧录
firmware.prototype、extras/*.bin、scripts/*.ld:管 Flash 分区和.px4产物。
加一块跟现有板子很像的飞控,直接 cp -r boards/<vendor>/<existing> boards/<vendor>/<new>,然后按这个清单 sed/改名,比从头写快得多。boards/micoair/ 下就有 h743 / h743-aio / h743-lite / h743-v2 四兄弟可以参考。
3. timer_config.cpp:把引脚变成通道的唯一入口
boards/micoair/h743-v2/src/timer_config.cpp 完整内容就几十行:
// io_timers[] 列出被 PX4 拿来当 IO 用的定时器。Timer4 选了 DMA1,DSHOT 之类的高速协议就靠这条 DMA。
constexpr io_timers_t io_timers[MAX_IO_TIMERS] = {
initIOTimer(Timer::Timer1, DMA{DMA::Index1}),
initIOTimer(Timer::Timer3, DMA{DMA::Index1}),
initIOTimer(Timer::Timer4, DMA{DMA::Index1}),
initIOTimer(Timer::Timer15),
};
// timer_io_channels[] 写每个通道对应的 (定时器, Channel, GPIO),数组下标 = pwm_out 的通道号,从 0 起。
// initIOTimerChannel 是 constexpr 工厂(io_timer_hw_description.h:42),做三件事:
// 1) 编译期断言:这个 pin 能不能映射到这个 timer/channel(手抖写错直接编译失败)
// 2) 根据 Channel1~4 算 CCR 偏移和中断 mask
// 3) 在 io_timers[] 里反查 timer 的 index,运行时 io_timer.c 拿 timer_index+channel 定位寄存器
// 顺序必须按"同一 timer 的通道连续成组"排列,否则被 initIOTimerChannelMapping 里的 constexpr_assert 拍死("Timers are not grouped together")。
constexpr timer_io_channels_t timer_io_channels[MAX_TIMER_IO_CHANNELS] = {
initIOTimerChannel(io_timers, {Timer::Timer1, Timer::Channel4}, {GPIO::PortE, GPIO::Pin14}), // PWM1 -> PE14 -> TIM1_CH4
initIOTimerChannel(io_timers, {Timer::Timer1, Timer::Channel3}, {GPIO::PortE, GPIO::Pin13}), // PWM2
initIOTimerChannel(io_timers, {Timer::Timer1, Timer::Channel2}, {GPIO::PortE, GPIO::Pin11}), // PWM3
initIOTimerChannel(io_timers, {Timer::Timer1, Timer::Channel1}, {GPIO::PortE, GPIO::Pin9}), // PWM4
initIOTimerChannel(io_timers, {Timer::Timer3, Timer::Channel4}, {GPIO::PortB, GPIO::Pin1}), // PWM5
initIOTimerChannel(io_timers, {Timer::Timer3, Timer::Channel3}, {GPIO::PortB, GPIO::Pin0}), // PWM6
initIOTimerChannel(io_timers, {Timer::Timer4, Timer::Channel1}, {GPIO::PortD, GPIO::Pin12}), // PWM7
initIOTimerChannel(io_timers, {Timer::Timer4, Timer::Channel2}, {GPIO::PortD, GPIO::Pin13}), // PWM8
initIOTimerChannel(io_timers, {Timer::Timer15, Timer::Channel1}, {GPIO::PortE, GPIO::Pin5}), // PWM9
initIOTimerChannel(io_timers, {Timer::Timer15, Timer::Channel2}, {GPIO::PortE, GPIO::Pin6}), // PWM10
};
// 把上面两张表压成一个查找表,io_timer.c 在 io_timer_channel_init 里查它。
constexpr io_timers_channel_mapping_t io_timers_channel_mapping =
initIOTimerChannelMapping(io_timers, timer_io_channels);
加一路 PWM,只在数组里多放一行就行,HAL 和 `pwm_out` 都不用动。
# 4. Case Study:给 micoair/h743-v2 加一路 PWM11(PD14 / TIM4_CH3)
## 4.0 背景:这条 PWM 通道是怎么"丢"的
`h743-v2` 板上有一个 4-pin SH1.0 接口,丝印一般标作 **LED**。这个接口是板厂按 ArduPilot 的习惯布的:
- **PD14 / TIM4_CH3**:ArduPilot 把它当作 PWM11,接 LED(单色高亮或简单调光,非 WS2812)控制信号。
- **PD15**:ArduPilot 当作 BUZZER OUTPUT,给有源蜂鸣器或者自激蜂鸣器的驱动脚。
对照 ArduPilot 源码(它的 `MicoAir743-v2` 飞控定义)可以确认 PWM11 = PD14 = TIM4_CH3,这个对应关系是定死的,跟 PX4 那边的 `timer_io_channels` 数组是不是登记了它无关。
问题在 PX4 这一侧。`boards/micoair/h743-v2/src/board_config.h:108-109` 把 PD15 拿来当 `GPIO_TONE_ALARM_IDLE` / `GPIO_TONE_ALARM_GPIO`(tunecontrol 用),所以 buzzer 这边是活的;但同一根 SH1.0 排线上的 PD14,在 `timer_config.cpp` 里**从来没被注册过**——`pwm_out` 完全看不到它,这块 LED 控制脚在 PX4 上是死的。
对比同厂的 `boards/micoair/h743`(非 v2 旧板)就清楚了,它的 `timer_config.cpp:49-52` 把 Timer4 的四个通道全开了:
```cpp
initIOTimerChannel(io_timers, {Timer::Timer4, Timer::Channel1}, {GPIO::PortD, GPIO::Pin12}), // PWM7 -> PD12
initIOTimerChannel(io_timers, {Timer::Timer4, Timer::Channel2}, {GPIO::PortD, GPIO::Pin13}), // PWM8 -> PD13
initIOTimerChannel(io_timers, {Timer::Timer4, Timer::Channel3}, {GPIO::PortD, GPIO::Pin14}), // PWM9 -> PD14
initIOTimerChannel(io_timers, {Timer::Timer4, Timer::Channel4}, {GPIO::PortD, GPIO::Pin15}), // PWM10 -> PD15
到 h743-v2,板厂把 CH4(PD15)挪去给 buzzer 用,CH1/CH2 留下,CH3(PD14)直接没补——所以这版"少了一路 PWM"不是硬件问题,是出厂固件没把它登记上去。
本节要做的就是把这条线从 ArduPilot 风格的"LED 接口"还原回 PX4 这边的可用 PWM11,顺手把 PD15 那路 buzzer 的来龙去脉也讲清楚,免得后面有人想用 tone_alarm 的时候误把它当 PWM 通道。
需求很具体:h743-v2 现在只 10 路 PWM(PWM1~PWM10),PD14 正是 TIM4_CH3 的备用脚。Timer4 也已经登记在 io_timers[] 里,所以这次就是"一行 + 一宏"的事。
4.1 改之前先确认
手册上 PD14 的 AF2 就是 TIM4_CH3,nuttx-config/nsh/defconfig 里也已经开着 CONFIG_STM32H7_TIM4=y,所以 NuttX 配置和 script.ld 都不用动。新手容易漏的是:CONFIG_STM32H7_TIM4 一旦没开,Timer4 在编译期会触发 nuttx_config_timer_enabled = false(io_timer_hw_description.h),initIOTimer(Timer::Timer4, ...) 照样编过,但运行时定时器的时钟门不会被打开,看上去就是"配了但没输出"。
4.2 改动一:board_config.h 把通道数从 10 调到 11
boards/micoair/h743-v2/src/board_config.h 第 92 行附近:
/* PWM
*/
-#define DIRECT_PWM_OUTPUT_CHANNELS 10
+#define DIRECT_PWM_OUTPUT_CHANNELS 11
这一行同时影响三处:
- HAL 的
MAX_TIMER_IO_CHANNELS(io_timer.h:53-56):if DIRECT_PWM_OUTPUT_CHANNELS > 8分支让它等于DIRECT_PWM_OUTPUT_CHANNELS,也就是 11。io_timer.c里io_timer_channel_stats[MAX_TIMER_IO_CHANNELS]等数组跟着扩到 11。 src/drivers/pwm_out/PWMOut.cpp:43-44:_pwm_mask = ((1u << DIRECT_PWM_OUTPUT_CHANNELS) - 1); _mixing_output.setMaxNumOutputs(DIRECT_PWM_OUTPUT_CHANNELS);_pwm_mask决定pwm_out公告给上层(mixer、control_allocator)的可用通道位图,11 路就是0b11111111111。init.c的board_on_reset循环for (int i = 0; i < DIRECT_PWM_OUTPUT_CHANNELS; ++i),也跟着走 11 次,保证每次复位都会把 11 个脚拉成输入态(防止 ESC 在软复位瞬间被点亮)。
不要顺手改
BOARD_NUM_IO_TIMERS(当前是 5,实际只用到 4 个 timer)。MAX_IO_TIMERS统计的是被 PX4 接管为 IO 的定时器数量,PWM11 复用了已有的Timer4,不用新增 timer 实体,这个宏保持原值。
4.3 改动二:timer_config.cpp 把 PD14 登记成第 11 个通道
**不是简单加一行。**新行必须插在 Timer4 那组的末尾(Timer4_CH2 之后、Timer15_CH1 之前),不能 append 到数组尾巴,否则会被 initIOTimerChannelMapping 里的 constexpr_assert 拍死,错误信息大概是:
timer_config.cpp:58:27: error: call to non-constexpr function ...
constexpr_assert(timer_io_channels_conf[channel - 1].timer_index == i, "Timers are not grouped together");
PX4 强制要求"同一 timer 的所有通道在数组中连续成组",运行时 io_timer.c 走 io_timers_channel_mapping 查找时也按这个前提做按 timer 分块处理。Timer4 现在有 3 个通道(PD12/PD13/PD14),必须挨着放,中间不能塞 Timer15 进去。
正确的 diff(注意 Timer15 两条跟着往下挪了两行):
constexpr timer_io_channels_t timer_io_channels[MAX_TIMER_IO_CHANNELS] = {
initIOTimerChannel(io_timers, {Timer::Timer1, Timer::Channel4}, {GPIO::PortE, GPIO::Pin14}), // 通道 0 PE14 -> TIM1_CH4 (PWM1 丝印)
initIOTimerChannel(io_timers, {Timer::Timer1, Timer::Channel3}, {GPIO::PortE, GPIO::Pin13}), // 通道 1 PE13 -> TIM1_CH3 (PWM2 丝印)
initIOTimerChannel(io_timers, {Timer::Timer1, Timer::Channel2}, {GPIO::PortE, GPIO::Pin11}), // 通道 2 PE11 -> TIM1_CH2 (PWM3 丝印)
initIOTimerChannel(io_timers, {Timer::Timer1, Timer::Channel1}, {GPIO::PortE, GPIO::Pin9}), // 通道 3 PE9 -> TIM1_CH1 (PWM4 丝印)
initIOTimerChannel(io_timers, {Timer::Timer3, Timer::Channel4}, {GPIO::PortB, GPIO::Pin1}), // 通道 4 PB1 -> TIM3_CH4 (PWM5 丝印)
initIOTimerChannel(io_timers, {Timer::Timer3, Timer::Channel3}, {GPIO::PortB, GPIO::Pin0}), // 通道 5 PB0 -> TIM3_CH3 (PWM6 丝印)
initIOTimerChannel(io_timers, {Timer::Timer4, Timer::Channel1}, {GPIO::PortD, GPIO::Pin12}), // 通道 6 PD12 -> TIM4_CH1 (PWM7 丝印)
initIOTimerChannel(io_timers, {Timer::Timer4, Timer::Channel2}, {GPIO::PortD, GPIO::Pin13}), // 通道 7 PD13 -> TIM4_CH2 (PWM8 丝印)
- initIOTimerChannel(io_timers, {Timer::Timer15, Timer::Channel1}, {GPIO::PortE, GPIO::Pin5}), // 通道 8 PE5 -> TIM15_CH1 (PWM9 丝印)
- initIOTimerChannel(io_timers, {Timer::Timer15, Timer::Channel2}, {GPIO::PortE, GPIO::Pin6}), // 通道 9 PE6 -> TIM15_CH2 (PWM10 丝印)
+ initIOTimerChannel(io_timers, {Timer::Timer4, Timer::Channel3}, {GPIO::PortD, GPIO::Pin14}), // 通道 8 PD14 -> TIM4_CH3 (PWM11 丝印)
+ initIOTimerChannel(io_timers, {Timer::Timer15, Timer::Channel1}, {GPIO::PortE, GPIO::Pin5}), // 通道 9 PE5 -> TIM15_CH1 (PWM9 丝印)
+ initIOTimerChannel(io_timers, {Timer::Timer15, Timer::Channel2}, {GPIO::PortE, GPIO::Pin6}), // 通道 10 PE6 -> TIM15_CH2 (PWM10 丝印)
};
改完之后 timer_config.cpp 完整长这样(直接 copy 走就行):
#include <px4_arch/io_timer_hw_description.h>
constexpr io_timers_t io_timers[MAX_IO_TIMERS] = {
initIOTimer(Timer::Timer1, DMA{DMA::Index1}),
initIOTimer(Timer::Timer3, DMA{DMA::Index1}),
initIOTimer(Timer::Timer4, DMA{DMA::Index1}),
initIOTimer(Timer::Timer15),
};
constexpr timer_io_channels_t timer_io_channels[MAX_TIMER_IO_CHANNELS] = {
initIOTimerChannel(io_timers, {Timer::Timer1, Timer::Channel4}, {GPIO::PortE, GPIO::Pin14}), // PWM1 PE14 TIM1_CH4
initIOTimerChannel(io_timers, {Timer::Timer1, Timer::Channel3}, {GPIO::PortE, GPIO::Pin13}), // PWM2 PE13 TIM1_CH3
initIOTimerChannel(io_timers, {Timer::Timer1, Timer::Channel2}, {GPIO::PortE, GPIO::Pin11}), // PWM3 PE11 TIM1_CH2
initIOTimerChannel(io_timers, {Timer::Timer1, Timer::Channel1}, {GPIO::PortE, GPIO::Pin9}), // PWM4 PE9 TIM1_CH1
initIOTimerChannel(io_timers, {Timer::Timer3, Timer::Channel4}, {GPIO::PortB, GPIO::Pin1}), // PWM5 PB1 TIM3_CH4
initIOTimerChannel(io_timers, {Timer::Timer3, Timer::Channel3}, {GPIO::PortB, GPIO::Pin0}), // PWM6 PB0 TIM3_CH3
initIOTimerChannel(io_timers, {Timer::Timer4, Timer::Channel1}, {GPIO::PortD, GPIO::Pin12}), // PWM7 PD12 TIM4_CH1
initIOTimerChannel(io_timers, {Timer::Timer4, Timer::Channel2}, {GPIO::PortD, GPIO::Pin13}), // PWM8 PD13 TIM4_CH2
initIOTimerChannel(io_timers, {Timer::Timer4, Timer::Channel3}, {GPIO::PortD, GPIO::Pin14}), // PWM11 PD14 TIM4_CH3 ← 新增这一行
initIOTimerChannel(io_timers, {Timer::Timer15, Timer::Channel1}, {GPIO::PortE, GPIO::Pin5}), // PWM9 PE5 TIM15_CH1
initIOTimerChannel(io_timers, {Timer::Timer15, Timer::Channel2}, {GPIO::PortE, GPIO::Pin6}), // PWM10 PE6 TIM15_CH2
};
constexpr io_timers_channel_mapping_t io_timers_channel_mapping =
initIOTimerChannelMapping(io_timers, timer_io_channels);
几个需要记住的点:
- 数组下标 ≠ 丝印编号。因为新行插在了
Timer4组末尾(数组 index 8),Timer15的两条跟着往下挪到 index 9、index 10。所以丝印PWM11(PD14) 在pwm_out里其实是channel 8,不是 channel 10。 - 丝印
PWM9/PWM10没动,但它们的pwm_out通道号从原来的 8/9 变成了 9/10。后面mixer配置文件、QGroundControl 的 servo 映射、actuator test 用的都是丝印号,这条不影响,但调试时如果按 “channel N” 报错,要看清楚是丝印号还是数组下标。 - 同一
Timer4的CH1/CH2/CH3现在共享CR1/PSC/DMA,三个通道的 PWM 周期严格同源,相位差最多一两个 tick(跟之前CH1/CH2共用的性质一样,只是多了一个兄弟)。 constexpr工厂会校验{Timer4, Channel3, PD14}在 STM32H7 复用表里合不合法。比如不小心写成PA7(它不映射到TIM4_CH3),编译期就会在Timer::Channel3分支报Timer not found(io_timer_hw_description.h:88-94)。错配在编译期就死,不会留到飞控上才炸。- 反过来想:如果以后要按某种业务顺序排(比如按丝印号 1~11 排),就得自己保证"同一 timer 的通道也连着",做不到的话只能牺牲丝印号和数组下标的一致性,选一个。
h743-v2这次选了"按 timer 分组优先"。
4.4 不需要改的地方(以及为什么)
| 路径 | 要不要改 | 原因 |
|---|---|---|
default.px4board | 否 | CONFIG_DRIVERS_PWM_OUT=y / CONFIG_DRIVERS_DSHOT=y 已经够,通道数运行时从 board_config.h 读 |
nuttx-config/nsh/defconfig | 否 | CONFIG_STM32H7_TIM4=y 已开 |
init/rc.board_* | 否 | 没引入新传感器,默认参数也不用加 |
src/spi.cpp / src/i2c.cpp | 否 | 不影响总线拓扑 |
CMakeLists.txt | 否 | timer_config.cpp 已经在 drivers_board 库列表里 |
要把这 11 路都切成 DSHOT 时,得再确认 DShotConfig(MIXER/几何)、DMA 流、反向,以及 pwm_servo 和 dshot 怎么复用同一个 CCR。
4.5 编译与上机验证
第一步是 clean 重建,不是洁癖。 参数元数据是 Tools/module_config/generate_actuators_metadata.py 扫 timer_config.cpp 重新生成的,放在 build/micoair_h743-v2/src/drivers/pwm_out/ 下面;增量构建里它不会失效,PWM_MAIN_FUNC10 / DIS10 / MIN10 / MAX10 / CENT10 / FAIL10 整组 param 就不会进固件,后面 §4.6 那个"Channel 10 全 0"现象就是这条引起的。
rm -rf build/micoair_h743-v2
make micoair_h743-v2 # 走 px4 的 cmake 工具链
make micoair_h743-v2 upload # 通过 USB DFU 烧录
烧录完开机,串口进 NSH,三步验证:
通道列表
nsh> pwm_out statuspwm_out status是src/drivers/pwm_out/PWMOut.cpp里print_status()暴露的命令(注意是pwm_out,不是pwm——PX4 里没有pwm这个模块)。会看到 11 行Channel 0 ~ 10,同时底部三行Timer N: rate: 400 channels: ...报告Timer 2拿到了三个通道(6/7/8),Timer 3拿到两个(9/10)。注意 channel 编号是数组下标 0 起,不是丝印号 1 起:PWM11丝印的 PD14 实际在Channel 8,PWM10丝印的 PE6 在Channel 10。调试时按"丝印号"还是"channel N"对,心里要有数。PWM 手动测试
nsh> actuator_test set -s 11 -v 0.5 # Servo 11 = 丝印 PWM11 (PD14),输出 0.5 = 中位 nsh> actuator_test set -s 11 -v 0 # 释放actuator_test走的是src/systemcmds/actuator_test/actuator_test.cpp,不是pwm test——后者在当前 PX4 里不存在。拿示波器或舵机测试器看PD14上有没有 1500 µs 的脉冲(默认值 1000~2000 µs,中位对应v=0.5),周期 ~50 Hz。要用电机就换-m <n>,要按功能找就-f <n>(PWM_MAIN_FUNC11配的是哪个功能就用哪个)。DSHOT(可选)
nsh> dshot start nsh> actuator_test set -s 11 -v 0.5 # DSHOT 模式下 actuator_test 一样能驱动PD14 上接支持 DSHOT 的 ESC,
actuator_test会把v转成 11-bit DSHOT 帧通过 DMA 喂给CCR3,这一步能通信就说明Timer4_CH3的 DMA 触发、帧时序都正常。要看 ESC 状态(转速/温度/电压)走dshot telemetry -d /dev/ttyS<n>(哪个 UART 接 ESC telemetry 线由硬件定,看boards/micoair/h743-v2/src/board_config.h里RC_SERIAL_PORT的注释)。
排错小贴士:
pwm_out status看到 11 行但 PD14 没输出,先dmesg | grep -i tim4看有没有stm32_tim_init失败,再看CONFIG_STM32H7_TIM4有没有被 menuconfig 误关。如果编译直接挂在constexpr_assert,那八成是把Timer4_CH3写到Timer15后面去了,回到 §4.3 那一段,把它挪到Timer4_CH2后面。如果通道数对、Timer 也认得到,但 Channel 10 这种老通道反而全 0,八成是没rm -rf build/micoair_h743-v2增量构建留下的陈旧 param,跳到 §4.6 跑一遍。io_timer.c自己不报错,出问题基本是 NuttX 那一层把 timer 整个禁掉了,或者上层 param 元数据没刷新。
4.6 Channel 10 全 0 真相(踩过的坑)
按上面 4.2 / 4.3 / 4.5 改完,没 clean 重建直接烧,串口会看到这种输出:
nsh> pwm_out status
...
Channel 0: func: 0, value: 1000, failsafe: 1000, disarmed: 1000, min: 1000, max: 2000, center: 65535
...
Channel 8: func: 0, value: 1000, failsafe: 1000, disarmed: 1000, min: 1000, max: 2000, center: 65535
Channel 9: func: 0, value: 1000, failsafe: 1000, disarmed: 1000, min: 1000, max: 2000, center: 65535
Channel 10: func: 0, value: 0, failsafe: 0, disarmed: 0, min: 0, max: 0, center: 0
Timer 1: rate: 400 channels: 4 5
Timer 2: rate: 400 channels: 6 7 8
Timer 3: rate: 400 channels: 9 10
Channel 0~9 都是 1000/1000/1000/1000/2000/65535(PWM 默认范围),只有 Channel 10 全 0。第一眼看上去是 “Timer15 没配 DMA → PE6 死掉了”,但其实不是。
先确认 ch10 到底是谁。 §4.3 那个 diff 把新行插在 Timer4_CH2 之后,Timer15_CH1 / CH2 跟着挪到数组下标 9 / 10。所以:
| 数组下标 | 硬件 | 丝印 | 改前 ch | 改后 ch |
|---|---|---|---|---|
| 6 | TIM4_CH1 / PD12 | PWM7 | 6 | 6 |
| 7 | TIM4_CH2 / PD13 | PWM8 | 7 | 7 |
| 8 | TIM4_CH3 / PD14 | PWM11 | — | 8(新增) |
| 9 | TIM15_CH1 / PE5 | PWM9 | 8 | 9 |
| 10 | TIM15_CH2 / PE6 | PWM10 | 9 | 10 |
Channel 10 = PE6 = Timer15_CH2,不是新加的 PD14。pwm_out status 的 Timer 3: rate: 400 channels: 9 10 那一行也说明 io_timer 这一层已经把 PE5/PE6 都识别成 Timer 3(也就是 Timer15)的成员,CCR、AF、CCER 全部初始化成功——硬件是活的。
真正的死因是参数元数据陈旧。 PWMOut::print_status() 走的 MixingOutput::printStatus()(src/lib/mixer_module/mixer_module.cpp:135)从 _function_assignment[i]、_disarmed_value[i]、_min_value[i]、_max_value[i]、_center_value[i] 这几个数组里读,这些数组由 updateParams() 在 initParamHandles() 阶段(mixer_module.cpp:115)通过 param_find("PWM_MAIN_FUNC<n>") / DIS / MIN / MAX / CENT / FAIL 一组一组的 param 查出来。
而 PWM_MAIN_*10 这一组是 Tools/module_config/generate_actuators_metadata.py 扫 timer_config.cpp 数出 11 行后,才在构建时生成的。增量构建里这个 JSON 缓存没失效,param 表里就根本没有 _10 这一套;param_find 全部返回 PARAM_INVALID,MixingOutput 拿不到值,i==10 这一行的所有数组保持构造函数里的零值。结果就是 Channel 10 整行 0/0/0/0/0/0——这不是 PWM 没输出,是这条通道的元数据从来就没存在过。
actualFailsafeValue(10) 那个 0 容易误读成"failsafe=0"。实际上 _failsafe_value[10] == UINT16_MAX(mixer_module.cpp:94 初始化时赋的),没人写过它,actualFailsafeValue 走 default_failsafe 路径返回 0,跟"failsafe 真设成 0"不是一回事。
怎么救: rm -rf build/micoair_h743-v2 && make micoair_h743-v2,让 generate_actuators_metadata.py 重新数到 11,把 0..10 这 11 套 param 全部打出来,Channel 10 就会回到 func: 0, value: 1000, failsafe: 1000, ... 的默认状态,PWM_MAIN_FUNC11 也会出现在 param show 里。改任何跟 timer_io_channels[] 数量有关的文件,这条都要默认走一遍。
顺带澄清几个常被一起怀疑的点,免得下次再走弯路:
- “Timer15 没传
DMA{DMA::Index1}是不是导致死?” 不是。initIOTimer里那段if (dma.index != DMA::Invalid) { ret.dshot.dma_* = ... }(io_timer_hw_description.h:283)只给 DShot 配 DMA 时才生效;PWM 是驱动里直接REG(timer, ccr_offset) = value写 CCR,不走 DMA。PWM 400Hz 跑得动就行,补不补 Timer15 的 DMA 是改 DShot 时才要碰的事。 - “PE6 被 PD15 的 buzzer 抢了?” 没抢。
board_config.h:108-109的GPIO_TONE_ALARM_*占的是 PD15,跟 PE6 没关系,AF 表里Timer15→ AF4 →PE5 / PE6这条路完全没被别的外设覆盖。 - “是新增
Timer4_CH3把谁挤掉了?” 没有。Timer4在io_timers[]里早就登记了,新加的是timer_io_channels[]里多一行,io_timers[]数组大小由BOARD_NUM_IO_TIMERS=5决定,跟timer_io_channels[]无关。
5. 小结
- PX4 的 HAL 不写死硬件,只给
constexpr工厂;加一路 PWM/IO,等价于在板级数组里多放一行。 - 新增一块飞控,文件清单是死的:构建层(
*.px4board)、集成层(init/rc.board_*)、NuttX 内核(defconfig)、板级代码(board_config.h/timer_config.cpp/init.c)、链接脚本(*.ld)。 - 这次给
h743-v2加 PWM11 只动两个文件、两行:board_config.h:DIRECT_PWM_OUTPUT_CHANNELS 10 → 11timer_config.cpp:timer_io_channels[]在Timer4_CH2之后、Timer15_CH1之前插入{Timer4, Channel3, PD14}(同 timer 的通道必须连成组,否则编译期constexpr_assert拍死)
- 但有三件事比这两行容易踩:
- 必须
rm -rf build/<board>再make——generate_actuators_metadata.py是按当前timer_io_channels[]长度生成PWM_MAIN_FUNC<n>这组 param 的,增量构建里缓存不失效,新增通道的 param 就缺,pwm_out status看上去是"新通道死",其实是元数据没生成(§4.6)。 - 数组下标 ≠ 丝印号 ≠
pwm_out的 channel 号。initIOTimerChannelMapping要求同 timer 通道连成组,新行一旦插进 Timer15 那块,会把 8/9 改成 9/10,mixer文件 / QGC servo 映射 / actuator test 用的丝印号跟pwm_out status里的Channel N不再 1:1,出错时先分清你在按哪套编号。 - Timer15 不写 DMA 不是 bug。DMA 字段只服务 DShot,纯 PWM 由
io_timer.c直接写 CCR,Timer15不配DMA{DMA::Index1}也能正常吐 400Hz。要用 DShot 时再回去补。
- 必须