i.MX8MP 的 PWM 使用 16 位向上计数器、12 位预分频器和 4×16 位采样 FIFO。计算波形时,需要先确定当前实例的真实时钟路径,再把一个周期的计数长度换算成 PERIOD;这两步分别出错,都会导致“寄存器写进去了,但频率不对”。
本文以 PWM3、NXP RM Rev.3 与 MCUXpresso SDK MCUX_2.16.000 的 drivers/ipwm 驱动为依据。保留原笔记的全部 21 幅图,图中较早的解释与精确值在相邻文字中修正。GPT 的 32 位计数器、STM32 的 PWM 定时器和其他 NXP PWM IP,不应直接套用这里的寄存器公式。
原笔记开头列出的 32 位计数器、两路输入捕获、三路输出比较属于 GPT。GPT 支持 restart 和 free-run:前者仅由比较通道 1 的匹配重新开始计数,后者在比较事件后继续计数直至回绕;输入捕获可选择触发边沿,输出比较可选择输出动作。捕获、比较和 rollover 可以产生相应中断,低功耗或调试状态下是否继续运行也有独立控制。这些功能应查 GPT 章节;本文后面的 16 位周期、采样 FIFO 和占空比公式属于独立的 PWM 模块。
1. 从时钟源追到 PWM3
跳转到“1. 从时钟源追到 PWM3”时钟路径可概括为:源时钟及固定分频 → root MUX → root PRE/POST 分频 → CCGR 门控 → PWM 内部 CLKSRC → 内部预分频 → 计数器。

图中体现了 CCM、PLL、门控、GPC 与复位模块的关系。配置某个时钟门不等于已经配置 PLL、解除外设复位或取得跨核资源权限。
PWM3 root 的索引为 105,相对 CCM 的目标寄存器偏移为 0xb480;RM Table 5-1 列出的该 root 最大频率为 66 MHz。这个上限不是上电后的默认值。
| PWM3 root MUX | 输入 |
|---|---|
| 0 | 24 MHz 参考时钟 |
| 1 | SYSTEM PLL2 / 10 |
| 2 | SYSTEM PLL1 / 5 |
| 3 | SYSTEM PLL1 / 20 |
| 4 | SYSTEM PLL3 |
| 5 | EXT_CLK2,需知道板上实际输入频率 |
| 6 | SYSTEM PLL1 / 10 |
| 7 | VIDEO PLL |


若当前选择 SYSTEM PLL1/5,则模块 root 频率是:
但 MUX 不是 2 时不能使用这个式子。SDK 的 CLOCK_GetRootPreDivider()、CLOCK_GetRootPostDivider() 已经把寄存器编码加 1,分别返回实际的 1~8、1~64 分频数;不应再次加 1,也不应把异常的 0 静默改成 1。
RM Table 5-2 把 PWM3 的 ipg_clk、ipg_clk_highfreq 和 ipg_clk_s 都接到 PWM3_CLK_ROOT,门控都是 CCGR42。通用 PWM IP 中 CLKSRC=3 的“32 kHz”名称,在这个 SoC 集成上不代表另一根独立的 32 kHz 输入。SDK 枚举注释沿用了通用名称,应以 SoC 时钟连接为准。


CCGR42 的偏移为 0x42a0。每个域的门控设置占一个两位字段,域依赖、PLL/root、权限与低功耗状态共同决定时钟是否可用。

原图中的 ROM 门控表不能用来假设启动后 PWM3 一定处于可用状态。Linux、U-Boot 或 M7 启动代码都可能改变相关配置,应确认当前所有者。
2. 计数器与精确周期
跳转到“2. 计数器与精确周期”
设 root 频率为 ,PWM 内部 PRESCALER 字段为 ,实际内部除数为 ,范围 1~4096。对于 PERIOD=0…0xfffe:
计数从 0 开始,到 PERIOD+1 后回到 0;一个周期有 个计数时钟。写 PERIOD=0 得到两个计数时钟;写 0xffff 与 0xfffe 的周期效果相同,而且 RM 禁止在非零 REPEAT 下使用 PERIOD=0xffff。使用规范值 0xfffe 表达最长的 65536 个计数时钟更清楚。
在默认极性 POUTC=00 下,输出在周期开始置高、比较匹配时清低。对 :
例如在已确认的 66 MHz root、内部不分频条件下产生 200 kHz:,因此 PERIOD=328。精确 50% 应设置 SAMPLE=165;SAMPLE=164 是约 49.697%,高电平约 2.485 μs。

这张原图的周期值 328 正确,但 164 不能标为精确 50%。如果实际 root 为 24 MHz,同样 200 kHz、50% 则需要 PERIOD=118、SAMPLE=60。
对照原来的逐步图
跳转到“对照原来的逐步图”
此图介绍 CLKSRC 的通用名称;在 i.MX8MP 上仍要采用上一节的实际 root 连接,不能从“低频时钟”字样直接代入 32768 Hz。

此图中的预分频范围 1~4096 正确。66 MHz 除以 33 只得到 2 MHz 计数时钟;若要 1 kHz 输出,还需 ,即 PERIOD=1998。


这两图应结合 PERIOD+2 和 0xffff 特例阅读。写 PWMPR 会将计数器清零并开始新周期,不是保证无毛刺的影子更新;在线改周期会改变当前波形相位。

该图已经把 164/330 写为约 49.7%;近似描述可以保留,精确 50% 时使用 165。
3. 极性、FIFO 和重复采样
跳转到“3. 极性、FIFO 和重复采样”| 字段或值 | 行为 |
|---|---|
POUTC=00 | rollover 置高、compare 清低;SAMPLE 表示高电平计数长度 |
POUTC=01 | rollover 清低、compare 置高;SAMPLE 对应低电平计数长度 |
POUTC=10/11 | PWM 输出断开;不能由这个描述推断板外引脚必为低、高阻或安全状态 |
SAMPLE=0 | POUTC=00 时恒低,POUTC=01 时恒高 |
SAMPLE>PERIOD+1 | 不发生有效比较翻转;可表达恒定输出,但受 16 位采样值范围限制 |
REPEAT=0/1/2/3 | 每个 FIFO 采样值使用 1/2/4/8 个周期,改变采样消耗速度,不改变基础 PWM 周期 |

FIFO 有四个 16 位位置。写 PWMSAR 是入队,读取则得到当前使用或将使用的值,不保证立即读回刚写的值。FIFO 耗尽后 PWM 继续使用最后的占空比,直到新数据到达或关闭 PWM。新的采样值在周期边界使用,前面仍有排队值或 REPEAT 时,会推迟到它轮到被消费。
FIFOAV[2:0] 是当前已有数据的数量,0~4;名字中的 available 不能理解为剩余空位。FWM=0/1/2/3 分别在至少有 1/2/3/4 个空位时满足 FIFO empty 条件,FE 并不总等于 FIFO 数量为 0。
RM 的功能描述称“FIFO 满时继续写入产生 FWE”;但同一 Rev.3 的 12.2.4 启动序列明确要求预装 1~3 个值,不写第四个,并警告启动受 FWE 影响。原图和原文的“第四次/第五次写出错”差异有这个手册背景。启动示例遵循更具体的序列,只预装一个有效采样值;不使用四次预写来赌边界行为。
4. 六个寄存器及启动顺序
跳转到“4. 六个寄存器及启动顺序”PWM1~4 的基址分别为 0x30660000、0x30670000、0x30680000、0x30690000,寄存器偏移相同:
| 寄存器 | 偏移 | 访问 | 关键内容 |
|---|---|---|---|
PWMCR | 0x00 | R/W | EN、SWR、REPEAT、PRESCALER、CLKSRC、POUTC、BCTR/HCTR、低功耗/调试位、FWM |
PWMSR | 0x04 | 部分 W1C、部分只读 | FWE[6]、CMP[5]、ROV[4]、FE[3];FIFOAV[2:0] 只读 |
PWMIR | 0x08 | R/W | CIE[2]、RIE[1]、FIE[0],分别允许比较、回绕、FIFO empty 中断 |
PWMSAR | 0x0c | R/W | 低 16 位 SAMPLE;写入 FIFO |
PWMPR | 0x10 | R/W | 低 16 位 PERIOD;复位值 0xfffe |
PWMCNR | 0x14 | 只读 | 低 16 位当前计数值,读取不改变计数 |
BCTR 控制采样字节交换;HCTR 控制总线写入数据的半字交换。普通 16 位数值写入不需要打开这些交换功能。
启动时先保证实例已停、时钟可访问、当前核有权限。需要清空旧 FIFO 时执行 SWR 并等待其自清除;SWR 保留 STOPEN/DOZEN/WAITEN/DBGEN,其他相关寄存器复位。随后在 EN=0 时设置计数时钟、预分频、输出极性、REPEAT 和 FWM,配置所需中断,写入 1~3 个初始值,处理 FWE/CMP/ROV,写 PERIOD,最后置 EN。


PWMSR 的事件位用写 1 清除,不要用普通寄存器的“读后按位清 0”方式;FIFOAV 则不能写清。FE 反映水位条件,清除后仍可能因条件满足再次出现。CLKSRC 只在 EN=0 时改变。
清 EN 会复位预分频器与计数器,尚未消费的 FIFO 内容保留,重启可能继续使用它们;SWR 才清空 FIFO。低功耗模式下还需相应模式允许位和源时钟都有效。调试暂停时是否继续输出由 DBGEN 及系统时钟状态决定,不能因为 CPU 停住就假定外部 ADC 不再被触发。
5. PWM3 引脚和中断号
跳转到“5. PWM3 引脚和中断号”

RM 的 A53 GIC 表用 SPI 相对编号 83 表示 PWM3;Linux 设备树也是 <GIC_SPI 83 ...>。这对应的 GIC 硬件 INTID 为 115,Linux 分配的虚拟 IRQ 则未必等于 83 或 115。M7 的 CMSIS 设备头另定义 PWM3_IRQn=83;数值相同不代表两种中断控制器的编号语义相同。

| pad | PWM3 复用模式 |
|---|---|
SPDIF_TX | ALT1 |
I2C3_SDA | ALT1 |
GPIO1_IO10 | ALT2 |
SAI5_RXC | ALT2 |
GPIO1_IO14 | ALT5 |


SION 是强制输入路径的控制,不是 PWM 输出使能。需要结合具体底板确认 pad 是否引出、被哪个器件占用、电压域和驱动能力;不能由 SoC 的候选复用表推导所有板卡都能用这五个脚。A53/Linux 的 PWM、pinctrl、时钟和电源管理必须与 M7 的所有权安排一致。
6. 先验证参数,再启动
跳转到“6. 先验证参数,再启动”原 fl_pwm_start_convst_hz() 把 root_hz / output_hz 直接写入 PERIOD,少减了 2;还固定按 PWM3 的 PLL1/5 计算时钟,忽略 MUX 和传入实例。其 lowPulseTicks 在默认极性下实际决定高电平长度,并且四次预装 FIFO 与上面的启动约束冲突。静默截断、夹紧到 16 位范围也会改变请求的波形。
下面只接受能够整除得到的频率和非恒定波形。它显式返回失败,让调用者决定是否接受其他频率;root_hz 必须是调用时真实有效且稳定的模块 root 频率,divider 是实际除数,high_ticks 是内部计数时钟个数。
#include <stdbool.h>#include <stddef.h>#include <stdint.h>
typedef struct { uint16_t prescaler_field; uint16_t period_field; uint16_t sample_field;} pwm_wave_plan_t;
bool pwm_plan_exact(uint32_t root_hz, uint32_t output_hz, uint32_t divider, uint32_t high_ticks, pwm_wave_plan_t *plan){ if (plan == NULL || root_hz == 0U || output_hz == 0U || divider == 0U || divider > 4096U) { return false; } uint64_t denominator = (uint64_t)output_hz * divider; if (root_hz % denominator != 0U) { return false; } uint64_t ticks = root_hz / denominator; if (ticks < 2U || ticks > 65536U || high_ticks == 0U || high_ticks >= ticks) { return false; } pwm_wave_plan_t result = { (uint16_t)(divider - 1U), (uint16_t)(ticks - 2U), (uint16_t)high_ticks }; *plan = result; return true;}在同一源文件加入下面的 PWM3 启动包装。调用前须已完成板级 root 配置、引脚和权限准备,并保证 PWM3 由本调用者独占;代码不重配任何 PLL。它先计算参数,失败时不触碰 PWM。reset_poll_limit 是轮询次数上限,不是微秒超时。
#include "fsl_pwm.h"#include "fsl_clock.h"
bool pwm3_start_checked(uint32_t output_hz, uint32_t divider, uint32_t high_ticks, uint32_t reset_poll_limit){ if (reset_poll_limit == 0U || !CLOCK_IsRootEnabled(kCLOCK_RootPwm3)) { return false; } /* SDK uses a configured constant for EXT_CLK2, not a frequency meter. */ if (CLOCK_GetRootMux(kCLOCK_RootPwm3) == 5U) { return false; } uint32_t root_hz = CLOCK_GetClockRootFreq(kCLOCK_Pwm3ClkRoot); pwm_wave_plan_t plan; if (root_hz > 66000000U || !pwm_plan_exact(root_hz, output_hz, divider, high_ticks, &plan)) { return false; }
CLOCK_EnableClock(kCLOCK_Pwm3); if ((PWM3->PWMCR & PWM_PWMCR_EN_MASK) != 0U) { return false; /* An active instance must first be stopped by its owner. */ } pwm_config_t config; PWM_GetDefaultConfig(&config); config.prescale = plan.prescaler_field; config.clockSource = kPWM_PeripheralClock; config.outputConfig = kPWM_SetAtRolloverAndClearAtcomparison; config.sampleRepeat = kPWM_EachSampleOnce; if (PWM_Init(PWM3, &config) != kStatus_Success) { return false; } PWM_SoftwareReset(PWM3); while ((PWM3->PWMCR & PWM_PWMCR_SWR_MASK) != 0U && reset_poll_limit != 0U) { --reset_poll_limit; } if ((PWM3->PWMCR & PWM_PWMCR_SWR_MASK) != 0U) { return false; } if (PWM_Init(PWM3, &config) != kStatus_Success) { return false; } PWM_DisableInterrupts(PWM3, kPWM_FIFOEmptyInterruptEnable | kPWM_RolloverInterruptEnable | kPWM_CompareInterruptEnable); PWM_SetSampleValue(PWM3, plan.sample_field); if ((PWM_GetStatusFlags(PWM3) & kPWM_FIFOWriteErrorFlag) != 0U) { return false; } PWM_clearStatusFlags(PWM3, kPWM_FIFOWriteErrorFlag | kPWM_CompareFlag | kPWM_RolloverFlag); PWM_SetPeriodValue(PWM3, plan.period_field); if ((PWM_GetStatusFlags(PWM3) & kPWM_FIFOWriteErrorFlag) != 0U) { return false; } PWM_StartTimer(PWM3); return true;}CLOCK_GetClockRootFreq() 按当前 MUX、源频率模型和实际 PRE/POST 分频计算;它不是示波器,仍依赖晶振、PLL 和 SDK 常量正确。本例拒绝 EXT_CLK2,实际项目如果使用它,应提供经确认的频率路径。
这个例子不需要 IRQ,初始化后让硬件持续产生波形。正常极性下若需求是“低脉冲长 L 个计数”,则应明确使用 high_ticks=N-L,或改为反相极性并同步调整协议;不能只改参数名称。驱动 ADC CONVST 还要核对有效边沿、高低脉宽下限、忙信号以及引脚电气条件,并在实际 pad 上测量频率、占空比与启动第一脉冲。
参考资料
跳转到“参考资料”- NXP《i.MX 8M Plus Applications Processor Reference Manual》,Rev.3,5.1 时钟、12.2 PWM 与 IOMUXC 章节。
- NXP MCUX_2.16.000:i.MX PWM 驱动接口
- NXP MCUX_2.16.000:PWM 初始化及默认配置
- NXP MCUX_2.16.000:MIMX8ML8 时钟频率计算
- Linux 6.8:i.MX8MP PWM3 设备树资源