I.MX6U 裸机开发 25. PWM 与 LCD 背光
-
- 先看结论:本课的三条硬知识
- 一、实验环境与硬件
-
- 引脚分配
- [时钟链路(PWM 的"水源")](#时钟链路(PWM 的“水源”))
- [二、PWM 原理:频率、占空比、FIFO](#二、PWM 原理:频率、占空比、FIFO)
-
- [2.1 三个公式](#2.1 三个公式)
- [2.2 采样 FIFO](#2.2 采样 FIFO)
- [2.3 硬件 PWM vs 软件 PWM](#2.3 硬件 PWM vs 软件 PWM)
- [2.4 使用方法:配一次,还是每个周期都亲自翻脚](#2.4 使用方法:配一次,还是每个周期都亲自翻脚)
- 三、关键寄存器
-
- [3.1 `PWMx_PWMCR`(控制寄存器,offset 0x0)](#3.1
PWMx_PWMCR(控制寄存器,offset 0x0)) - [3.2 `PWMx_PWMPR`(周期,0x10) / `PWMx_PWMSAR`(采样,0xC)](#3.2
PWMx_PWMPR(周期,0x10) /PWMx_PWMSAR(采样,0xC)) - [3.3 `PWMx_PWMSR`(状态,0x4) / `PWMx_PWMIR`(中断使能,0x8)](#3.3
PWMx_PWMSR(状态,0x4) /PWMx_PWMIR(中断使能,0x8))
- [3.1 `PWMx_PWMCR`(控制寄存器,offset 0x0)](#3.1
- 四、代码结构
-
- [4.1 PWM 初始化与配置(节选 `bsp_pwm.c`)](#4.1 PWM 初始化与配置(节选
bsp_pwm.c)) - [4.2 PWM 中断服务函数](#4.2 PWM 中断服务函数)
- [4.3 用翻转中断实测频率(验证公式)](#4.3 用翻转中断实测频率(验证公式))
- [4.4 软件 PWM 呼吸灯](#4.4 软件 PWM 呼吸灯)
- [4.1 PWM 初始化与配置(节选 `bsp_pwm.c`)](#4.1 PWM 初始化与配置(节选
- 五、运行现象
-
- [5.1 串口输出内容说明](#5.1 串口输出内容说明)
- [5.2 屏幕画面(无字库,用图形表达)](#5.2 屏幕画面(无字库,用图形表达))
- [5.3 四种模式(KEY0 循环切换)](#5.3 四种模式(KEY0 循环切换))
- [5.4 KEY0 按下之后怎么切](#5.4 KEY0 按下之后怎么切)
- [5.5 蜂鸣音](#5.5 蜂鸣音)
- 六、编译与下载
- [七、与 Linux 驱动的对应关系](#七、与 Linux 驱动的对应关系)
- 八、参考资料

摘要 :本文讲清 I.MX6ULL 的 PWM 外设:一路 PWM 波形是怎么由「时钟源 → 预分频 → 周期寄存器 → 采样 FIFO」四级拼出来的,占空比和频率各自受哪个寄存器控制,以及为什么 PWM 的 FIFO 必须不断喂数据。
代码上做了两条对照线:LCD 背光走硬件 PWM1(GPIO1_IO08) ,LED0 呼吸灯只能用软件 PWM(GPIO1_IO03 没有 PWM 复用);再用「翻转中断实测频率」把时钟源切换的效果量化出来,验证公式。
结论 :亮度只由占空比决定,频率只决定"闪得快不快";而频率的上限与精度,完全由时钟源 + 预分频决定。
调试方式 :WSL(Ubuntu)交叉编译 + Windows tools/dl.py USB-OTG 下载到 DDR 运行,串口 @ 115200。
先看结论:本课的三条硬知识
| 问题 | 答案 |
|---|---|
| 频率谁决定? | Fpwm = (Fclksrc / (PRESCALER+1)) / (PERIOD+2) ------ 周期寄存器 + 预分频 + 时钟源 |
| 亮度(占空比)谁决定? | duty = PWMSAR / (PERIOD+2) ------ 采样寄存器,与频率无关 |
| 改频率亮度会变吗? | 不会 (只改 PWMPR、占空比保持 50% 时)------ 这是模式 2 要演示的 |
| 时钟源有哪些? | CLKSRC:0=关闭 / 1=ipg_clk(66MHz) / 2=ipg_clk_highfreq / 3=ipg_clk_32k(32.768kHz) |
| 采样值写哪? | 写 PWMSAR 会进 4×16bit FIFO ,每个 PWM 周期消耗 1 个,空了输出就停 |
| 本板背光脚 | GPIO1_IO08 = PWM1_OUT(ALT0) ,PWM1 基址 0x02080000,中断号 115 |
| 本板 LED0 能硬件 PWM 吗? | 不能 。GPIO1_IO03 只有 I2C1_SDA / GPT1_COMPARE3 / UART1 等复用,没有 PWM,只能软件模拟 |
本课踩到的两个坑,先记下来:
lcd_init()必须在pwm1_init()之前 ------lcd_init会把GPIO1_IO08配成 GPIO 输出拉高开背光,之后pwm1_init才把它改成PWM1_OUT;顺序反了背光会被 GPIO 抢住,PWM 怎么调都没反应。bsp_delay.c的delay_us()原来有个 bug :循环里漏了old_cnt = new_cnt,累加值按平方增长,delay_us(1000)实际只延了几十微秒。软件 PWM 全靠它计时,必须先修(已在 25 课修好,前几课的副本仍有此问题)。
一、实验环境与硬件
| 项目 | 值 |
|---|---|
| 板子 | 正点原子 I.MX6ULL ALPHA / Mini |
| LCD | ATK4384(ID 0x4384,800×480),背光脚 GPIO1_IO08 |
| 使用外设 | PWM1(8 路 PWM 中的第 1 路)、GPT1(1MHz,做软件 PWM 的高精度延时与测频时基) |
| 按键 | KEY0 = GPIO1_IO18,用于切换演示模式 |
| 拨码 | 0100(USB 下载模式) |
| 串口 | 115200 / 8 / 1 / 无(本机实测 COM16 = CH340) |
| 代码链接地址 | 0x87800000,显存 0x89000000 |
引脚分配
| 信号 | PAD | 复用 | 电气属性 | 说明 |
|---|---|---|---|---|
| 背光 PWM | GPIO1_IO08 |
ALT0 = PWM1_OUT | 0x10B0 |
高电平点亮;lcd_init 阶段先当 GPIO 用 |
| LED0 | GPIO1_IO03 |
ALT5 GPIO | 0x10B0 |
无 PWM 复用 → 软件 PWM,低电平点亮 |
| KEY0 | UART1_CTS_B |
ALT5 GPIO1_IO18 | 0xF080 |
带上拉输入,按下为低 |
| GPT1 | --- | --- | --- | 内部定时器,1MHz 时基,不占引脚 |

时钟链路(PWM 的"水源")
#mermaid-svg-ToFLVTu7XnZsweIn{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ToFLVTu7XnZsweIn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ToFLVTu7XnZsweIn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ToFLVTu7XnZsweIn .error-icon{fill:#552222;}#mermaid-svg-ToFLVTu7XnZsweIn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ToFLVTu7XnZsweIn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ToFLVTu7XnZsweIn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ToFLVTu7XnZsweIn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ToFLVTu7XnZsweIn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ToFLVTu7XnZsweIn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ToFLVTu7XnZsweIn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ToFLVTu7XnZsweIn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ToFLVTu7XnZsweIn .marker.cross{stroke:#333333;}#mermaid-svg-ToFLVTu7XnZsweIn svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ToFLVTu7XnZsweIn p{margin:0;}#mermaid-svg-ToFLVTu7XnZsweIn .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ToFLVTu7XnZsweIn .cluster-label text{fill:#333;}#mermaid-svg-ToFLVTu7XnZsweIn .cluster-label span{color:#333;}#mermaid-svg-ToFLVTu7XnZsweIn .cluster-label span p{background-color:transparent;}#mermaid-svg-ToFLVTu7XnZsweIn .label text,#mermaid-svg-ToFLVTu7XnZsweIn span{fill:#333;color:#333;}#mermaid-svg-ToFLVTu7XnZsweIn .node rect,#mermaid-svg-ToFLVTu7XnZsweIn .node circle,#mermaid-svg-ToFLVTu7XnZsweIn .node ellipse,#mermaid-svg-ToFLVTu7XnZsweIn .node polygon,#mermaid-svg-ToFLVTu7XnZsweIn .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ToFLVTu7XnZsweIn .rough-node .label text,#mermaid-svg-ToFLVTu7XnZsweIn .node .label text,#mermaid-svg-ToFLVTu7XnZsweIn .image-shape .label,#mermaid-svg-ToFLVTu7XnZsweIn .icon-shape .label{text-anchor:middle;}#mermaid-svg-ToFLVTu7XnZsweIn .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ToFLVTu7XnZsweIn .rough-node .label,#mermaid-svg-ToFLVTu7XnZsweIn .node .label,#mermaid-svg-ToFLVTu7XnZsweIn .image-shape .label,#mermaid-svg-ToFLVTu7XnZsweIn .icon-shape .label{text-align:center;}#mermaid-svg-ToFLVTu7XnZsweIn .node.clickable{cursor:pointer;}#mermaid-svg-ToFLVTu7XnZsweIn .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ToFLVTu7XnZsweIn .arrowheadPath{fill:#333333;}#mermaid-svg-ToFLVTu7XnZsweIn .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ToFLVTu7XnZsweIn .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ToFLVTu7XnZsweIn .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ToFLVTu7XnZsweIn .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ToFLVTu7XnZsweIn .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ToFLVTu7XnZsweIn .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ToFLVTu7XnZsweIn .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ToFLVTu7XnZsweIn .cluster text{fill:#333;}#mermaid-svg-ToFLVTu7XnZsweIn .cluster span{color:#333;}#mermaid-svg-ToFLVTu7XnZsweIn div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ToFLVTu7XnZsweIn .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ToFLVTu7XnZsweIn rect.text{fill:none;stroke-width:0;}#mermaid-svg-ToFLVTu7XnZsweIn .icon-shape,#mermaid-svg-ToFLVTu7XnZsweIn .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ToFLVTu7XnZsweIn .icon-shape p,#mermaid-svg-ToFLVTu7XnZsweIn .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ToFLVTu7XnZsweIn .icon-shape .label rect,#mermaid-svg-ToFLVTu7XnZsweIn .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ToFLVTu7XnZsweIn .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ToFLVTu7XnZsweIn .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ToFLVTu7XnZsweIn :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 小于 → 高电平
大于等于 → 低电平
24MHz 晶振
PLL2/PLL3 ...
ipg_clk
66MHz
PWM 时钟源选择器
PWMCRCLKSRC
ipg_clk_highfreq
ipg_clk_32k
32.768kHz
12 位预分频
÷(PRESCALER+1)
PCLK
16 位计数器
0 → PERIOD+1
PWMOUT
GPIO1_IO08
PWMSAR → FIFO(4×16)
计数值
vs SAMPLE
二、PWM 原理:频率、占空比、FIFO
2.1 三个公式
text
PCLK = F_clksrc / (PRESCALER + 1) # 预分频,12 位,1~4096 分频
Fpwm = PCLK / (PERIOD + 2) # PERIOD = PWMPR 的值
duty = SAMPLE / (PERIOD + 2) # SAMPLE = PWMSAR 的值
为什么是
+2不是+1?计数器从0x0000一直数到PERIOD+1才归零,一共PERIOD+2个 PCLK;采样比较发生在「计数值 == SAMPLE」这一刻,所以高电平占
SAMPLE个计数、低电平占PERIOD+2-SAMPLE个。Linux 内核
drivers/pwm/pwm-imx.c里也写着同样的注释:the real period value should be PERIOD value in PWMPR plus 2。
本课的配置(1kHz):
| 参数 | 值 | 计算 |
|---|---|---|
| 时钟源 | ipg_clk = 66MHz |
CLKSRC = 1 |
| 预分频 | 65(即 ÷66) | PCLK = 66MHz/66 = 1MHz |
PWMPR |
998 | Fpwm = 1MHz/(998+2) = 1000Hz |
| 占空比 50% | PWMSAR = 500 |
500/1000 = 50% |
频率档位表(改 PWMPR 即可,PCLK 保持 1MHz):
| 目标频率 | PWMPR |
验算 |
|---|---|---|
| 1 kHz | 998 | 1MHz / 1000 |
| 2 kHz | 498 | 1MHz / 500 |
| 5 kHz | 198 | 1MHz / 200 |
| 10 kHz | 98 | 1MHz / 100 |
2.2 采样 FIFO
PWM 内部有 4×16bit 的采样 FIFO ,每个 PWM 周期会从中取走 1 个采样值。写 PWMSAR 就是往 FIFO 里塞值:
- FIFO 有空间 → 值入队,下一周期生效;
- FIFO 满 时还写 → 状态位
FWE(FIFO Write Error)置 1,值被丢弃; - FIFO 空了 → 没有采样值可比较,输出停住(背光直接黑)。
所以要么开 FIFO 空中断(PWMIR[FIE])在 ISR 里补数据 ,要么主循环定时补。本驱动两条都做了:ISR 补 + 主循环 pwm1_fifo_refill() 兜底。
#mermaid-svg-oTiNd0J26AbMO0BV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-oTiNd0J26AbMO0BV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oTiNd0J26AbMO0BV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oTiNd0J26AbMO0BV .error-icon{fill:#552222;}#mermaid-svg-oTiNd0J26AbMO0BV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oTiNd0J26AbMO0BV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oTiNd0J26AbMO0BV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oTiNd0J26AbMO0BV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oTiNd0J26AbMO0BV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oTiNd0J26AbMO0BV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oTiNd0J26AbMO0BV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oTiNd0J26AbMO0BV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oTiNd0J26AbMO0BV .marker.cross{stroke:#333333;}#mermaid-svg-oTiNd0J26AbMO0BV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oTiNd0J26AbMO0BV p{margin:0;}#mermaid-svg-oTiNd0J26AbMO0BV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-oTiNd0J26AbMO0BV .cluster-label text{fill:#333;}#mermaid-svg-oTiNd0J26AbMO0BV .cluster-label span{color:#333;}#mermaid-svg-oTiNd0J26AbMO0BV .cluster-label span p{background-color:transparent;}#mermaid-svg-oTiNd0J26AbMO0BV .label text,#mermaid-svg-oTiNd0J26AbMO0BV span{fill:#333;color:#333;}#mermaid-svg-oTiNd0J26AbMO0BV .node rect,#mermaid-svg-oTiNd0J26AbMO0BV .node circle,#mermaid-svg-oTiNd0J26AbMO0BV .node ellipse,#mermaid-svg-oTiNd0J26AbMO0BV .node polygon,#mermaid-svg-oTiNd0J26AbMO0BV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oTiNd0J26AbMO0BV .rough-node .label text,#mermaid-svg-oTiNd0J26AbMO0BV .node .label text,#mermaid-svg-oTiNd0J26AbMO0BV .image-shape .label,#mermaid-svg-oTiNd0J26AbMO0BV .icon-shape .label{text-anchor:middle;}#mermaid-svg-oTiNd0J26AbMO0BV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-oTiNd0J26AbMO0BV .rough-node .label,#mermaid-svg-oTiNd0J26AbMO0BV .node .label,#mermaid-svg-oTiNd0J26AbMO0BV .image-shape .label,#mermaid-svg-oTiNd0J26AbMO0BV .icon-shape .label{text-align:center;}#mermaid-svg-oTiNd0J26AbMO0BV .node.clickable{cursor:pointer;}#mermaid-svg-oTiNd0J26AbMO0BV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-oTiNd0J26AbMO0BV .arrowheadPath{fill:#333333;}#mermaid-svg-oTiNd0J26AbMO0BV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-oTiNd0J26AbMO0BV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-oTiNd0J26AbMO0BV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oTiNd0J26AbMO0BV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-oTiNd0J26AbMO0BV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oTiNd0J26AbMO0BV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-oTiNd0J26AbMO0BV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-oTiNd0J26AbMO0BV .cluster text{fill:#333;}#mermaid-svg-oTiNd0J26AbMO0BV .cluster span{color:#333;}#mermaid-svg-oTiNd0J26AbMO0BV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-oTiNd0J26AbMO0BV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-oTiNd0J26AbMO0BV rect.text{fill:none;stroke-width:0;}#mermaid-svg-oTiNd0J26AbMO0BV .icon-shape,#mermaid-svg-oTiNd0J26AbMO0BV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oTiNd0J26AbMO0BV .icon-shape p,#mermaid-svg-oTiNd0J26AbMO0BV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-oTiNd0J26AbMO0BV .icon-shape .label rect,#mermaid-svg-oTiNd0J26AbMO0BV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oTiNd0J26AbMO0BV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-oTiNd0J26AbMO0BV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-oTiNd0J26AbMO0BV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
否
是
若没喂上
写 PWMSAR
FIFO 满?
入队, FIFOAV++
FWE=1, 数据丢弃
PWM 每周期取 1 个值
FIFO 空?
FE=1 → 触发 FIFO 空中断
ISR: 补写采样值
输出停止, 背光变黑
2.3 硬件 PWM vs 软件 PWM
| 对比项 | 硬件 PWM(背光) | 软件 PWM(LED0) |
|---|---|---|
| 波形产生 | PWM 模块自己数,CPU 只改寄存器 | CPU 翻转 GPIO + delay_us() 忙等 |
| CPU 占用 | 0(配好就自动跑) | 接近 100%(本课 5ms 周期里全在忙等) |
| 精度 | 1 个 PCLK(本课 1µs,即占空比分辨率 0.1%) | 受中断打断影响,有明显抖动 |
| 引脚要求 | 必须是带 PWMx_OUT 复用的 pad |
任意 GPIO |
| 频率上限 | 数十 MHz 级 | 几百 Hz ~ 几 kHz |
LED0只能软件PWM原因 : fsl_iomuxc.h 可知 GPIO1_IO03 的复用选项是
I2C1_SDA / GPT1_COMPARE3 / USB_OTG2_OC / USDHC1_CD_B / GPIO / CCM_DI0_EXT_CLK / UART1, 没有 PWM 。板上带 PWM 输出又方便观察的只有 GPIO1_IO08(背光)。
想用硬件 PWM 点 LED,得飞线到
GPIO1_IO04(PWM3_OUT)/GPIO1_IO09(PWM2_OUT)这类脚上。
2.4 使用方法:配一次,还是每个周期都亲自翻脚
上表说的是能力差别。落到代码上,两边的调用方式也完全不同:硬件 PWM 是「配一次寄存器,外设自己跑」;软件 PWM 是「每个周期都要 CPU 亮一段、灭一段」。
| 步骤 | 硬件 PWM(背光,bsp_pwm.c) |
软件 PWM(LED0,breath.c) |
|---|---|---|
| 1. 引脚 | 复用成 PWM1_OUT,之后这只脚归 PWM 模块,不要再当 GPIO 去写 DR |
led_init() 配成普通 GPIO 输出,电平始终由 CPU 写 |
| 2. 定频率 | 写 CLKSRC、PRESCALER、PWMPR,按 2.1 的公式一次算好 |
自己定常量 SOFTPWM_PERIOD_US(本课 5000µs,即 200Hz) |
| 3. 定亮度 | pwm1_set_duty() 算出 SAMPLE,写进 PWMSAR(进 FIFO) |
把这一个周期拆成两段微秒:hi = 周期 × duty%,lo = 周期 − hi |
| 4. 出波形 | PWMCR[EN] = 1 之后,计数、比较、翻转引脚都是硬件做的 |
每次调用 softpwm_out():led_on() + delay_us(hi),再 led_off() + delay_us(lo) |
| 5. 改亮度 | 再调一次 pwm1_set_duty() 即可,CPU 可以去做别的 |
主循环必须不停调用 softpwm_out(新占空比);中间去做别的事,这一拍就被拉长 |
| 6. 要一直亮着 | FIFO 不能空:开 FIE,ISR 里补采样值,主循环再 pwm1_fifo_refill() 兜底 |
delay_us() 必须准,而且这 5ms 里 CPU 被占住,做不了别的 |
对应到本课 main() 里,两条线是这样用的:
c
/* 硬件:初始化一次。之后主循环里只改占空比,波形继续由 PWM1 自己输出 */
lcd_init(); /* 必须在前:它会先把 GPIO1_IO08 当 GPIO */
pwm1_init(65U, 998U, 100U); /* 1kHz,先全亮 */
pwm1_set_duty(bl_duty); /* 改亮度 = 改一个寄存器量,不阻塞 */
/* 软件:没有"启动"这一步。每次调用本身就花掉整整一个周期 */
softpwm_out(led_duty); /* 阻塞 5ms,返回之前 CPU 都在 delay_us 里 */
所以背光可以一边呼吸、一边刷屏、一边用翻转中断测频;LED0 的呼吸只要被串口打印打断,就会顿一下。这不是灯坏了,是软件 PWM 的时间本来就是 CPU 一段一段数出来的。
三、关键寄存器
3.1 PWMx_PWMCR(控制寄存器,offset 0x0)
| 位 | 字段 | 说明 |
|---|---|---|
| 0 | EN |
使能 PWM |
| 2:1 | REPEAT |
FIFO 中每个数据重复用几次(0~3 → 用 1~4 次) |
| 3 | SWR |
软件复位,自清零(必须先有 CLKSRC 时钟,否则死等) |
| 15:4 | PRESCALER |
12 位预分频,实际分频 = 值 + 1 |
| 17:16 | CLKSRC |
0=关闭 / 1=ipg_clk / 2=ipg_clk_highfreq / 3=ipg_clk_32k |
| 19:18 | POUTC |
输出极性:0 = 先高后低;1 = 先低后高;2/3 = 不输出 |
| 25 | STOPEN |
停止模式下是否继续 |
| 26~ | FWM |
FIFO 水位(写满几个触发中断) |
3.2 PWMx_PWMPR(周期,0x10) / PWMx_PWMSAR(采样,0xC)
PWMPR[15:0]=PERIOD,周期计数值 =PERIOD + 2PWMSAR[15:0]=SAMPLE,决定占空比PWMCNR[15:0](0x14)= 当前计数值,调试时可直读
3.3 PWMx_PWMSR(状态,0x4) / PWMx_PWMIR(中断使能,0x8)
| 位 | 字段 | 说明 |
|---|---|---|
| 2:0 | FIFOAV |
FIFO 中有效数据个数 0~4 |
| 3 | FE |
FIFO 空标志 |
| 4 | ROV |
翻转(一个周期结束)标志,写 1 清 |
| 5 | CMP |
比较事件标志,写 1 清 |
| 6 | FWE |
FIFO 写错误,写 1 清 |
PWMIR 位 |
字段 | 说明 |
|---|---|---|
| 0 | FIE |
FIFO 空中断使能(必须开,否则输出会停) |
| 1 | RIE |
翻转中断使能(测频率用) |
| 2 | CIE |
比较中断使能 |
三种中断共用一根中断线
PWM1_IRQn = 115,必须在同一个 ISR 里按状态位逐个处理并清标志,漏清任何一种都会造成中断风暴把系统卡死。
四、代码结构
text
25_pwm/
├── apps/
│ ├── main.c # 4 种演示模式 + 屏幕 UI + 串口状态
│ ├── breath.c/.h # 软件 PWM 呼吸灯(LED0)
│ ├── led.c / key.c / beep.c
├── board/imx6u/
│ ├── bsp_pwm.c/.h # ★ PWM1 驱动:时钟源/周期/占空比/FIFO/测频
│ ├── bsp_lcd.c/.h # eLCDIF(背光脚在 lcd_init 里先当 GPIO)
│ ├── bsp_delay.c # GPT1 高精度延时(已修 delay_us bug)
│ └── ... # clk / gpio / int / uart / i2c(后续课用)
4.1 PWM 初始化与配置(节选 bsp_pwm.c)
c
/* 引脚复用:GPIO1_IO08 -> PWM1_OUT(ALT0) */
IOMUXC_SetPinMux(IOMUXC_GPIO1_IO08_PWM1_OUT, 0U);
IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO08_PWM1_OUT, 0x10B0U);
/* 关 PWM -> 先挂 ipg 时钟 -> 软复位(CLKSRC=0 时 SWR 不会自清零,会死等) */
PWM1->PWMCR = 0U;
PWM1->PWMCR = PWM_PWMCR_CLKSRC(PWM_CLKSRC_IPG) | PWM_PWMCR_PRESCALER(s_prescaler);
pwm1_sw_reset(); /* 带 guard 计数,不会真死等 */
PWM1->PWMIR = 0U;
/* 控制字:时钟源 + 预分频 + 先高后低 + FIFO 数据只用一次 */
PWM1->PWMCR = PWM_PWMCR_CLKSRC(s_clksrc)
| PWM_PWMCR_PRESCALER(s_prescaler)
| PWM_PWMCR_POUTC(0U)
| PWM_PWMCR_REPEAT(0U);
PWM1->PWMSR = PWM_PWMSR_ROV_MASK | PWM_PWMSR_CMP_MASK | PWM_PWMSR_FWE_MASK; /* W1C */
PWM1->PWMPR = PWM_PWMPR_PERIOD(s_period);
for (i = 0; i < 4; i++) /* 预填满 FIFO */
PWM1->PWMSAR = PWM_PWMSAR_SAMPLE(s_sample);
PWM1->PWMIR = PWM_PWMIR_FIE(1U); /* 开 FIFO 空中断 */
PWM1->PWMCR |= PWM_PWMCR_EN(1U); /* 使能 */
占空比只改变量,真正写 FIFO 交给中断/兜底,避免 ISR 之外争抢 FIFO:
c
void pwm1_set_duty(unsigned char duty)
{
s_duty = duty;
s_sample = (unsigned short)(((unsigned int)s_period + 2U) * s_duty / 100U);
}
void pwm1_fifo_refill(void) /* FIFO 满时不写,避免 FWE */
{
unsigned char n = 0U;
while ((pwm1_fifo_av() < 4U) && (n < 4U)) {
PWM1->PWMSAR = PWM_PWMSAR_SAMPLE(s_sample);
n++;
}
}
4.2 PWM 中断服务函数
c
void pwm1_irqhandler(unsigned int gicciar, void *param)
{
unsigned int sr = PWM1->PWMSR;
if (sr & PWM_PWMSR_FE_MASK) { pwm1_fifo_refill(); PWM1->PWMSR = PWM_PWMSR_FE_MASK; }
if (sr & PWM_PWMSR_ROV_MASK) { s_roll_tick = GPT1->CNT; s_roll++;
PWM1->PWMSR = PWM_PWMSR_ROV_MASK; } /* W1C */
if (sr & PWM_PWMSR_FWE_MASK) { s_fwe++; PWM1->PWMSR = PWM_PWMSR_FWE_MASK; }
}
4.3 用翻转中断实测频率(验证公式)
思路:开 RIE,在 ISR 里记录两次翻转的 GPT1 时间戳(GPT1 = 1MHz,1 tick = 1µs),
两个时间戳之差就是一个 PWM 周期:
c
*freq_hz = 1000000U / (t1 - t0);
pwm1_measure_start() + pwm1_measure_poll() 做成非阻塞状态机,测量期间主循环照常呼吸、刷屏。
4.4 软件 PWM 呼吸灯
c
/* 周期 5ms(200Hz),duty 按 gamma≈2 的曲线变化,每 4 个周期(20ms)推进一格 */
void softpwm_out(unsigned char duty)
{
hi = (SOFTPWM_PERIOD_US * duty) / 100U;
lo = SOFTPWM_PERIOD_US - hi;
if (hi) { led_on(); delay_us(hi); }
if (lo) { led_off(); delay_us(lo); }
}
五、运行现象
5.1 串口输出内容说明
text
=== 25_pwm: PWM / backlight ===
PWM1 -> GPIO1_IO08 (LCD backlight), ipg=66MHz prescaler=65 period=998
calc freq = 1000Hz, duty = 100% (PWMSAR=1000)
LED0 -> GPIO1_IO03, software PWM 200Hz (no PWM mux on this pin)
KEY0: switch mode
[breath] bl= 37% led= 37% src=ipg_clk(66M) pr=998 ps=65 sar= 370 f= 1000Hz roll=1234 fifo=3 fwe=0
[mode] -> 2 freq
[freq] bl= 50% led= 50% src=ipg_clk(66M) pr= 98 ps=65 sar= 50 f= 10000Hz roll=...
[mode] -> 3 clksrc
[clksrc] ipg_clk(66M) meas=1000Hz calc=1000Hz period=998
[clksrc] ipg_clk_highfreq meas=xxxxHz calc=0Hz period=998 <-- highfreq 频率以实测为准
[clksrc] ipg_clk_32k meas=0Hz calc=0Hz period=998 <-- 约 0.5Hz,肉眼可见背光慢闪
解读几个字段:pr=PWMPR、ps=预分频值、sar=采样值(= (pr+2)×duty%)、f=按公式算出的频率、
roll=翻转中断累计次数、fifo=当前 FIFO 有效数据个数、fwe=FIFO 写错误次数(正常应恒为 0)。
5.2 屏幕画面(无字库,用图形表达)
| 区域 | 含义 |
|---|---|
| 顶部色条 | 当前模式(绿=呼吸 / 青=占空比台阶 / 黄=频率 / 紫=时钟源) |
| 上部大条(灰阶) | 背光占空比,灰阶越亮表示越亮 |
| 中部红条 | LED0 软件 PWM 占空比 |
| 中央方块 | 「灯泡」:方块越大 = 占空比越大 |
| 时钟源三格 | 绿=ipg / 蓝=ipg_highfreq / 橙=32k,当前源高亮 |
| 底部档位格 | 当前模式下的档位下标 |
5.3 四种模式(KEY0 循环切换)
| 模式 | 现象 | 想说明的事 |
|---|---|---|
| 0 呼吸 | 背光与 LED 同步 0→100%→0 | 占空比连续变化 = 亮度连续变化;对比硬件/软件 PWM |
| 1 占空比台阶 | 5/25/50/75/100%,每档 1.5s | 占空比与亮度的台阶关系(gamma 校正前并不线性) |
| 2 频率档位 | 1k→2k→5k→10kHz,占空比固定 50% | 亮度不变:频率不是亮度,只是"闪得快慢" |
| 3 时钟源 | ipg → ipg_highfreq → 32k,各测一次频率 | 时钟源决定频率上限;32k 源下背光约 0.5Hz 慢闪 |
5.4 KEY0 按下之后怎么切
KEY0 接在 UART1_CTS_B(GPIO1_IO18)上,按下为低。主循环不调用 会阻塞 10ms 的 key_get_value(),而是每转一圈问一次 key_pressed()。一圈的节拍由软件 PWM 定死:softpwm_out() 阻塞 5ms,所以按键扫描、档位计时、刷屏全是以 5ms 为一拍。
key_pressed() 只认下降沿,并且按下后冷却 40 拍(200ms):
c
/* last/cur:1 = 松开,0 = 按下。只在 1→0 的那一拍返回 1 */
if (cool > 0U) {
cool--; /* 冷却期内忽略后续边沿 */
} else if ((last == 1U) && (cur == 0U)) {
hit = 1U;
cool = 40U; /* 40 × 5ms = 200ms */
}
按住不放不会连切。冷却期内就算抖动出新的边沿,也要等松手之后再按一次才算下一次。
认到一次有效按下,先把现场清回已知状态,再进入下一个模式 。顺序不能反:模式 3 可能正停在 32kHz 时钟源上,如果先跑新模式的 switch,背光会带着错误的时钟源进呼吸或台阶。
c
mode++;
if (mode >= 4U) mode = 0U; /* 0→1→2→3→0 */
idx = timer = 0U; /* 档位和 1.5s 计时从头数 */
cs_idx = cs_phase = cs_timer = 0U; /* 模式 3 的测量状态机归零 */
pwm1_set_clksrc(PWM_CLKSRC_IPG); /* 时钟源拉回 66MHz */
pwm1_set_period(998U); /* 周期拉回 1kHz */
清完现场,同一圈 就按新的 mode 更新占空比。四个模式各自往前走的方式不一样:
| 模式 | 这一拍做什么 | 什么时候进下一档 |
|---|---|---|
| 0 呼吸 | breath_next() 取当前占空比,硬件、软件 PWM 用同一个值 |
函数内部每 4 拍(20ms)才沿 gamma 曲线走一格,到顶后反向 |
| 1 台阶 | bl_duty = step_duty[idx],5/25/50/75/100 |
timer 加到 300 拍(1.5s)后 idx++,到 5 回到 0 |
| 2 频率 | 占空比固定 50%,pwm1_set_period(freq_period[idx]) |
同样 300 拍换一档:998 → 498 → 198 → 98(1k/2k/5k/10kHz) |
| 3 时钟源 | 占空比固定 50%,另外用 cs_phase 走测量状态机 |
见下面 |
模式 3 不靠 timer,单独用三态:
text
cs_phase = 0 切到 clksrc_seq[cs_idx],调用 pwm1_measure_start(6s)
cs_phase = 1 每拍 pwm1_measure_poll():还在测就什么都不做
测完 → 打印 meas/calc,cs_timer = 400(停 2s)
失败 → cs_timer = 100(停 0.5s)
cs_phase = 2 cs_timer 倒数到 0,cs_idx++(ipg → highfreq → 32k → 回到 ipg),相位回到 0
测量是非阻塞的,所以这几秒里 LED 的软件 PWM、FIFO 兜底和刷屏都还在转。再按一次 KEY0 会把 cs_phase 清掉,并立刻把时钟源和周期恢复成 ipg、1kHz,不会把 32kHz 慢闪带进下一个模式。
5.5 蜂鸣音
程序运行有的时候会听到蜂鸣音,但听到的不是板上那只蜂鸣器。引脚说明如下:
| 器件 | 引脚 | 本课实际状态 |
|---|---|---|
| LED0 | GPIO1_IO03 |
软件 PWM,固定 200Hz,只负责点灯 |
| 蜂鸣器 | SNVS_TAMPER1(GPIO5_IO01) |
beep_init() 后马上 beep_off(),主循环里没有再打开。低电平才响,脚被拉高,所以它是静音的 |
| LCD 背光 | GPIO1_IO08 = PWM1_OUT |
硬件 PWM,频率会随模式变 |
会叫的是背光升压电路里的电感。硬件 PWM 输出 1kHz~10kHz 的方波,正好落在人耳听得见的范围,线圈跟着电流来回振动,听感就是蜂鸣音。占空比接近 0% 或 100% 时输出几乎是直流,电感安静;占空比在中间、尤其 50% 时切得最厉害,声音最明显。所以只是「有时候」才听到。
用模式 2 可以确认来源:亮度不动(占空比锁在 50%),音调却按 1kHz → 2kHz → 5kHz → 10kHz 往上跳。LED0 的软件 PWM 始终是 200Hz,音调如果在变,就一定是背光这路。切到模式 3 的 32kHz 时钟源后,背光大约 0.5Hz 慢闪,音调消失,只剩缓慢亮灭。
六、编译与下载
bash
# WSL 中
cd /mnt/d/documents/learn_i.mx6u/25_pwm
make clean && make && make imx
powershell
# Windows 中(dl.py 默认镜像是 22_lcdid,其他课必须显式传路径)
python D:\documents\learn_i.mx6u\tools\dl.py D:\documents\learn_i.mx6u\25_pwm\load.imx --com COM16
# 串口被助手占用时:
python D:\documents\learn_i.mx6u\tools\dl.py D:\documents\learn_i.mx6u\25_pwm\load.imx --no-serial
步骤:先开串口助手 → 拨码 0100 + 按 RESET → 出现 SE Blank 6ULL → 跑 dl.py。
判断有没有真的重编,看
uart_printf.bin的时间戳,不要看load.imx(make imx每次都会重打包 IVT)。
本课的 Makefile 沿用通配(SRCDIRS 含 apps、board/imx6u...),新增的 bsp_pwm.c、breath.c 自动纳入编译,无需改 Makefile。
运行效果:



串口实际输出:
bash
python D:\documents\learn_i.mx6u\tools\dl.py D:\documents\learn_i.mx6u\25_pwm\load.imx --com
COM16
File: D:\documents\learn_i.mx6u\25_pwm\load.imx (24852 bytes)
IVT: entry=0x87800000 dcd=0x877FF42C self=0x877FF400
Serial: COM16 @ 115200
HID cand iface=0 usage=0xff00/1 name='SE Blank 6ULL'
HID: SE Blank 6ULL
[1] DCD_WRITE -> OCRAM 0x00910000, len=488
DCD HAB=0x56787856 status=0x128A8A12 [OK]
[2] WRITE_FILE -> 0x877FF400, len=24852
... 8192/24852
... 16384/24852
... 24576/24852
FILE HAB=0x56787856 status=0x88888888 [OK]
[3] JUMP_ADDRESS -> IVT 0x877FF400
JUMP HAB=0x56787856 status=0x00000000 [OK]
=== 下载完成 ===
读取 COM16 20s(请摸屏看坐标)...
LCD ID = 0x4384
panel...
panel ok
lcdgpio...
gpio ok
clk...
clk ok
lcd reset...
reset ok
elcdif...
enable+clear...
LCD 800x480 fb=0x89000000 ready
=== 25_pwm: PWM / backlight ===
PWM1 -> GPIO1_IO08 (LCD backlight), ipg=66MHz prescaler=65 period=998
calc freq = 1000Hz, duty = 100% (PWMSAR=1000)
LED0 -> GPIO1_IO03, software PWM 200Hz (no PWM mux on this pin)
KEY0: switch mode
[breath] bl= 6% led= 6% src=ipg_clk(66M) pr=998 ps=65 sar= 60 f= 1000Hz roll=3916 fifo=4 fwe=0
[breath] bl= 25% led= 25% src=ipg_clk(66M) pr=998 ps=65 sar= 250 f= 1000Hz roll=7820 fifo=4 fwe=0
[breath] bl= 56% led= 56% src=ipg_clk(66M) pr=998 ps=65 sar= 560 f= 1000Hz roll=12288 fifo=4 fwe=1
[breath] bl= 99% led= 99% src=ipg_clk(66M) pr=998 ps=65 sar= 990 f= 1000Hz roll=17669 fifo=4 fwe=1
[breath] bl= 55% led= 55% src=ipg_clk(66M) pr=998 ps=65 sar= 550 f= 1000Hz roll=22964 fifo=4 fwe=1
七、与 Linux 驱动的对应关系
| 裸机做法 | Linux 中的对应 |
|---|---|
IOMUXC_SetPinMux(GPIO1_IO08 → PWM1_OUT) |
设备树 pinctrl_pwm1: MX6UL_PAD_GPIO1_IO08__PWM1_OUT |
PWMCR[CLKSRC] 选时钟源 |
clocks = <&clks IMX6UL_CLK_PWM1>,由 CCF 统一管 |
PRESCALER / PWMPR / PWMSAR |
drivers/pwm/pwm-imx.c 的 pwm_imx27_apply(),按 period_ns/duty_ns 反算 |
PERIOD + 2 |
内核注释同样写着 the real period value should be PERIOD value in PWMPR plus 2 |
| FIFO 空中断补采样值 | 内核里改成写前等 FIFO 有空位(wait for FIFOAV),不用中断 |
pwm1_set_duty() |
pwm_config() / pwm_apply(),用户态 /sys/class/pwm/pwmchip0/pwm0/duty_cycle |
| LCD 背光用 PWM1 | 设备树 backlight { pwms = <&pwm1 0 5000000>; brightness-levels = ...; } + pwm-backlight 驱动 |
| 软件 PWM 翻 GPIO | drivers/pwm/pwm-gpio.c / 内核 soft-PWM 或 leds-gpio 的 ledtrig-pattern |
一句话概括:裸机里手算 PERIOD/SAMPLE 的这套活,Linux 下被 pwm-imx.c + pwm-backlight + 设备树包掉了;但要看懂驱动为什么要那么算,还是得回到本文这三个公式。
八、参考资料
- 《I.MX6ULL 参考手册》Chapter 40:Pulse Width Modulation(PWM)(时钟源、FIFO、寄存器位定义)
- 《I.MX6ULL 参考手册》Chapter 18:CCM(ipg_clk / ipg_clk_highfreq / ipg_clk_32k 的来源)
- 《I.MX6ULL 参考手册》Chapter 32:GPT(本课 GPT1 当 1MHz 时基用)
- 正点原子《I.MX6U 嵌入式 Linux 驱动开发指南》「LCD 背光调节实验」章节
- Linux 内核
drivers/pwm/pwm-imx.c、drivers/video/backlight/pwm_bl.c