I.MX6U 裸机开发 25. PWM 与 LCD 背光

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))
    • 四、代码结构
      • [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 呼吸灯)
    • 五、运行现象
      • [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,只能软件模拟

本课踩到的两个坑,先记下来:

  1. lcd_init() 必须在 pwm1_init() 之前 ------ lcd_init 会把 GPIO1_IO08 配成 GPIO 输出拉高开背光,之后 pwm1_init 才把它改成 PWM1_OUT;顺序反了背光会被 GPIO 抢住,PWM 怎么调都没反应。
  2. 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 + 2
  • PWMSAR[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
相关推荐
创界工坊工作室2 小时前
35-基于AT89C51单片机的罐头生产线传送系统设计
stm32·单片机·嵌入式硬件·51单片机·proteus·生产线
qq_401700412 小时前
MCU串口接收不定长数据的三种方案,哪种最稳?
stm32·单片机·嵌入式硬件
inrichtt2 小时前
第七课时:STM32时钟系统揭秘:从零理解时钟树
c语言·数据结构·stm32·单片机·嵌入式硬件
硅基手札3 小时前
【Linux 内核专栏 17】进程间通信 IPC(上)— 管道·信号·System V IPC·mmap·文件锁
linux·运维·服务器·stm32·单片机·mcu
国科安芯3 小时前
无方向脚如何既快又稳:ASC0101S 自动方向感应机理与边沿加速应用研究
网络·嵌入式硬件·低轨卫星·商业航天·抗辐射
额额额对了4 小时前
Linux 驱动开发:基于 platform 总线 + misc 设备的 DS18B20 单总线温度驱动
linux·c语言·开发语言·驱动开发·嵌入式硬件
zcmodeltech4 小时前
沙盘模型控制系统STM32和PLC方案对比:选型逻辑与代码实现
stm32·嵌入式硬件·自动化
HQB20264 小时前
EG11731|屹晶微电子 100V 内置功率管大电流同步降压 DC‑DC
嵌入式硬件·硬件工程
2501_922639585 小时前
率能半导体 SS6209L半桥MOSFET驱动器:28V/4A集成方案,覆盖电子烟、无线充电、半桥应用三大场景
单片机·嵌入式硬件·硬件工程