FPGA安路赛项第七讲:PWM呼吸灯与PLL

第 07 讲:PWM 呼吸灯与 PLL —— 脉宽调制算法、锁相环时钟与异步复位同步释放


课前导读:让冰冷的逻辑拥有“生命的律动”

在前面的课程中,我们让 LED 实现了亮灭翻转(闪烁灯)以及位置轮换(流水灯)。但它们都有一个共同的特点:灯光是生硬突变的——非开即关,非暗即亮

今天,我们要让芯片实现一个充满科技美感的效果:呼吸灯(Breathing LED)
LED 不会猛烈跳变,而是像人类静息呼吸一样,从全黑极度平滑地缓缓变亮(吸气,耗时 2.0 秒),再缓缓变暗至熄灭(呼气,耗时 2.0 秒),以完美的 4 秒周期无限往复

这节课在数字电路领域具有里程碑意义,因为你将同时攻克三大硬核技术高地

  1. PWM(脉冲宽度调制)物理与数学模型:如何用只能输出 0 和 1 的纯数字引脚,模拟出宛如连续模拟电压的 256 级细腻灰度?
  2. PLL(锁相环)架构与 IP 核应用:现代芯片是如何利用内部的“时钟变速箱与变压器”,重构并净化高频时钟网络的?
  3. 工业级复位终极法则(异步复位、同步释放):为什么芯片复位如果处理不当会导致整块电路在松开按键的瞬间“脑死亡”?如何用 4 级移位链消灭亚稳态?

第一章:物理与数学原理——PWM(脉冲宽度调制)的秘密

1.1 现实困境:纯数字电路怎么调出“半亮”?

  • 模拟电路调节亮度,靠的是滑动变阻器或者数模转换器(DAC)去连续改变电压(例如 $0\text{V} \to 1\text{V} \to 2\text{V} \to 3.3\text{V}$);
  • 但 FPGA 内部只有开关管,引脚输出电压只能非黑即白:要么是 $0\text{V}$(关),要么是 $3.3\text{V}$(开)

破局之道:以极致的速度“欺骗”人眼的视网膜

想象一个现实中的电灯开关:

  • 如果你一秒钟只按开、关各一次,人眼看到的是一秒亮、一秒灭的狂闪;
  • 但如果你以每秒 20 万次(200kHz)的非人类速度狂按开关
    • 在每一个微观周期里,如果 前 10% 的时间导通开灯,后 90% 的时间断开熄灯
    • 由于人眼的视觉暂留效应(积分低通滤波),大脑根本无法分辨这 20 万次开闭,你看到的绝不是频闪,而是一颗极为微弱、只有 10% 亮度的幽暗小灯
    • 如果开 50%、关 50%,你看到的就是柔和的半亮(50% 灰度)
    • 如果开 90%、关 10%,你看到的就是光芒耀眼的极亮(90% 灰度)

这种在恒定高频周期内,通过调制高电平持续时间比例来控制平均输出功率的技术,就叫 PWM(Pulse Width Modulation,脉冲宽度调制)

一个固定周期 T (载波周期)
┌──────────────┐
│              │
[ 高电平时间 Ton ] [ 低电平时间 Toff ]
├── 占空比 10% ──┤ ────────────────► 平均光强弱 (微亮)
├──── 占空比 50% ────┤ ────────────► 平均光强中 (半亮)
├────── 占空比 90% ──────┤ ────────► 平均光强强 (极亮)
  • 占空比(Duty Cycle)公式
    $$\text{Duty} = \frac{T_{\text{on}}}{T_{\text{period}}} \times 100%$$
  • 等效输出电压
    $$V_{\text{eff}} = V_{\text{max}} \times \text{Duty}$$

第二章:pwm_breath_core.v 呼吸灯核心驱动源码解剖

本模块负责在后台纯算术计算出呼吸波形。它是完全独立的纯算法 IP 核心。

module pwm_breath_core #(
    parameter integer PWM_MAX  = 255,      // 灰度量化:切分为 256 级亮度 (0~255)
    parameter integer STEP_DIV = 390_625   // 呼吸节拍速度分频常数
)(
    input  wire clk,    // 高频时钟输入 (50MHz)
    input  wire rst_n,  // 安全复位输入 (低有效)
    output wire pwm     // 输出的 PWM 脉冲方波,直驱 LED
);
    reg [7:0]  pwm_counter;  // 快速载波齿轮:在 0~255 之间飞速旋转
    reg [7:0]  duty;         // 目标亮度登记本:当前应该输出几级亮度
    reg [31:0] step_counter; // 慢速呼吸齿轮:控制人类吸气呼气速度
    reg        direction_up; // 呼吸方向标:1 代表吸气变亮,0 代表呼气变暗

    // 核心无时延硬件比较器:产生 PWM 方波的心脏!
    assign pwm = (pwm_counter < duty);

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            pwm_counter  <= 8'd0;
            duty         <= 8'd0;
            step_counter <= 32'd0;
            direction_up <= 1'b1; // 上电默认从最暗向变亮起步
        end else begin
            pwm_counter <= pwm_counter + 1'b1; // 快速小齿轮永不停歇加 1

            // 慢速大齿轮计时与亮度换挡
            if (step_counter == STEP_DIV - 1) begin
                step_counter <= 32'd0; // 走完一步,秒表清零

                if (direction_up) begin
                    // 【吸气阶段:越来越亮】
                    if (duty == PWM_MAX)
                        direction_up <= 1'b0; // 亮到顶头了(255),改变方向准备变暗!
                    else
                        duty <= duty + 1'b1;  // 还没到头,亮度往上拔高一档
                end else begin
                    // 【呼气阶段:越来越暗】
                    if (duty == 0)
                        direction_up <= 1'b1; // 暗到彻底黑了(0),改变方向准备变亮!
                    else
                        duty <= duty - 1'b1;  // 还没彻底黑,亮度往下降落一档
                end
            end else begin
                step_counter <= step_counter + 1'b1; // 慢速秒表累加计时
            end
        end
    end
endmodule

2.1 显微镜级解剖:全代码最神的一行 assign pwm = (pwm_counter < duty);

在硅片里,综合器用一组 8 位的数字比较器电路把这行代码硬焊死。

我们推演 duty = 100 时,内部快速小齿轮 pwm_counter 在一个周期内的情况:

  • pwm_counter 从 0 累加到 255(需要 256 个时钟周期,耗时 $256 \times 20\text{ns} = 5.12\mu\text{s}$,折合开关频率 $195.3\text{ kHz}$!远超人眼识别极限);
  • 在区间 $[0, 99]$ 内
    • 条件 pwm_counter < 100 绝对成立pwm 输出线被强行推高为 1(点亮 LED)
  • 在区间 $[100, 255]$ 内
    • 条件 pwm_counter < 100 不成立pwm 输出线被强行拉低为 0(熄灭 LED)
  • 边界情况
    • duty = 0 时:pwm_counter < 0 永假,输出恒为 0(完全关灯);
    • duty = 255 时:只有在等于 255 的那 1 拍是 0,其余 255 拍全是 1,接近 100% 满功率发光!
pwm_counter : 0   1   2  ...  98  99 100 101 ... 254 255 | 0   1   2 ...
当前 duty=100: ─────────────────────────┐
pwm 输出线  : 1   1   1  ...   1   1  0   0  ...   0   0 | 1   1   1 ...
              └──── 约 39% 高电平 ────┘└── 约 61% 低电平 ──┘

2.2 完美的 4.0 秒呼吸周期数学公式推演

为什么作者不设别的值,偏偏把 STEP_DIV 设为 390_625 这么奇怪的数字?

我们一步一步还原工程师的设计草稿纸:

  1. 输入系统主频:$f = 50\text{ MHz}$,周期 $T = 20\text{ ns}$;
  2. 人类舒适的呼吸节奏:通常成年人静息时一分钟呼吸约 15 次,一次完整深呼吸大约需要 4 秒(吸气 2 秒,呼气 2 秒)
  3. 量化阶数计算
    我们把亮度划分为 256 个阶梯($0 \sim 255$)。从全黑到最亮,需要走 256 步;
  4. 计算每走一步允许的时间
    $$T_{\text{step}} = \frac{\text{吸气总时间 2.0 秒}}{256\text{ 步}} = 0.0078125\text{ 秒} = \mathbf{7.8125\text{ 毫秒}}$$
  5. 倒推慢速计数器需要计数的周期数
    $$\text{STEP_DIV} = \frac{T_{\text{step}}}{T_{\text{clk}}} = \frac{7.8125\text{ ms}}{20\text{ ns}} = \frac{7,812,500\text{ ns}}{20\text{ ns}} = \mathbf{390,625}\text{ 拍!}$$

毫无凑数的痕迹!纯数学上的极致严谨!
正是这个数字,让慢速齿轮每爬满 390,625 拍,亮度就极其细腻地微调一档,在人眼看来毫无生硬的阶梯感,只有丝般顺滑的微光呼吸!


第三章:芯片核心动力——PLL(锁相环)架构揭秘

在工程顶层 pll_pwm_board_top.v 中,我们看到了一个新的神秘元件:sys_pll

    sys_pll u_sys_pll (
        .refclk  (sys_clk),
        .reset   (~sys_rst_n),
        .clk0_out(pll_clk)
    );

3.1 什么是 PLL(Phase-Locked Loop,锁相环)?

  • 比喻:芯片内部的**“高精度无级变速箱与时钟整形净化厂”**。
  • 物理机制
    板载的有源晶振通过导线送进芯片时,会受到外界电磁环境干扰,时钟边缘常常带有毛刺和轻微的相位抖动(Jitter)。
    PLL 内部包含鉴相器(Phase Detector)、压控振荡器(VCO)和反馈分频环路。它锁定外部输入的时钟相位,能做到两件极其强大的事:
    1. 净化时钟:滤除高频抖动,吐出一个相位极度纯净、边沿陡峭的高质量内部主时钟;
    2. 倍频与分频:板子输入虽然只有固定的 50MHz,但通过 PLL 的内部压控振荡器,你可以随心所欲变换出 100MHz、200MHz 乃至 12.5MHz 的全新时钟,直接驱动内部逻辑!

第四章:工业级安全防线——异步复位、同步释放(Reset Synchronizer)

这是几乎所有中高端 FPGA 面试中必问、必画时序图的殿堂级考点

请看顶层模块中的这段代码:

    reg  [3:0] reset_release;
    wire core_rst_n = reset_release[3];

    always @(posedge pll_clk or negedge sys_rst_n) begin
        if (!sys_rst_n)
            reset_release <= 4'b0000;
        else
            reset_release <= {reset_release[2:0], 1'b1};
    end

很多同学困惑:“老师,复位直接用按键的 sys_rst_n 不就好了吗?为什么要开 4 个抽屉,搞这么复杂的移位操作?”

我们揭开一段惨烈的物理事故真相:


4.1 惨痛物理事故:为什么“直接复位”会导致芯片死机?

触发器不仅在接收数据时需要满足建立时间(Setup Time)和保持时间(Hold Time),在撤销复位时,同样有两个严格的物理时间窗口要求

  • 恢复时间(Recovery Time, $t_{\text{rec}}$):复位信号释放(由 0 变 1)的边缘,距离下一个时钟有效上升沿之间必须留出的最小安全间隔
  • 清除时间(Removal Time, $t_{\text{rem}}$):时钟有效沿过去之后,复位信号才能释放的最小维持间隔
                  时钟上升沿 (采样时刻)
                            │
            ┌───────────────┼───────────────┐
            │ 恢复时间 trec  │ 清除时间 trem │
            └───────────────┼───────────────┘
                            ▲
                    【危险违例危险区!】
  • 杀手出现:人类的手指按压或松开机械按键是极其粗暴且随意的。松手的那一微秒,电平由 0 变成 1,极大概率正好撞在时钟上升沿的危险窗口内!
  • 后果:芯片内部成千上万个触发器,有的触发器认为复位撤销了,有的认为复位还在继续。更可怕的是触发器会陷入亚稳态(逻辑卡在 1.5V 既非 0 又非 1 的振荡状态),整个芯片在一开机瞬间直接死锁发疯!

4.2 终极解决方案:异步复位、同步释放(打 4 拍缓冲链)

我们仔细拆解这 4 个串联小抽屉的运作:

按键按下 (!sys_rst_n=1) ──► 瞬间强制清零异步端 (全部瞬间变 0000!) ──► core_rst_n 瞬间为 0 (安全复位!)
                                                                         
按键松开 (sys_rst_n=1)  ──► 触发移位移位操作:
                              第 1 拍 PLL 时钟沿:  0000 ──► 0001
                              第 2 拍 PLL 时钟沿:  0001 ──► 0011 (亚稳态完全衰减消退)
                              第 3 拍 PLL 时钟沿:  0011 ──► 0111
                              第 4 拍 PLL 时钟沿:  0111 ──► 1111 ──► core_rst_n 正式变成 1 (安全撤销复位!)
  • 异步复位(Asynchronous Assert)
    当你按下按键(sys_rst_n = 0)的那一瞬间,走的是 D 触发器的底层异步清零端,不看时钟脸色,全芯片瞬间在 1 纳秒内急刹车全部归零!保证任何突发状况立刻受控。
  • 同步释放(Synchronous De-assert)
    当你松开按键时,外部电平变回 1。这个 1 不直接传给核心电路,而是作为数据输入端送入第 1 个触发器。
    • 就算第 1 拍产生轻微亚稳态,在经过第 2、第 3 级触发器的级联吸收后,亚稳态概率在数学上衰减到几乎为 0
    • 直到整整 4 个纯净的时钟周期过去之后,最高位的 core_rst_n 才以绝对与时钟沿对齐的完美姿态平稳拉高为 1,安全放行核心逻辑!

第五章:系统顶层组装——pll_pwm_board_top.v 逐行解析

这是整个呼吸灯工程的顶层主板模块:

module pll_pwm_board_top #(
    parameter LED_ACTIVE_LOW = 1'b0
)(
    input  wire       sys_clk,
    input  wire       sys_rst_n,
    output wire [3:0] led
);
    wire pll_clk;
    wire pwm_raw;
    reg  [3:0] reset_release;
    wire core_rst_n = reset_release[3];

    // 1. 组装 PLL 变速箱
    sys_pll u_sys_pll (
        .refclk  (sys_clk),
        .reset   (~sys_rst_n), // PLL IP核多为高电平复位,故需取反
        .clk0_out(pll_clk)
    );

    // 2. 注入异步复位、同步释放安全链条
    always @(posedge pll_clk or negedge sys_rst_n) begin
        if (!sys_rst_n)
            reset_release <= 4'b0000;
        else
            reset_release <= {reset_release[2:0], 1'b1};
    end

    // 3. 组装呼吸灯核心算法引擎
    pwm_breath_core #(
        .PWM_MAX(255),
        .STEP_DIV(390_625)
    ) u_pwm (
        .clk  (pll_clk),    // 使用 PLL 提纯后的干净时钟!
        .rst_n(core_rst_n), // 使用 4 级缓冲后的绝对安全复位!
        .pwm  (pwm_raw)     // 接引算出的呼吸波形导线
    );

    // 4. 板载灯珠输出驱动:仅第 0 颗灯呼吸,前 3 颗强制熄灭
    assign led = LED_ACTIVE_LOW ? {3'b111, ~pwm_raw} : {3'b000, pwm_raw};
endmodule

5.1 最后一行的巧妙安排

  • 前 3 颗灯(led[3:1]):代码通过 {3'b000, ...} 强行接地,永久保持熄灭,避免多余的光源干扰视线;
  • 最后 1 颗灯(led[0]):单线直通 pwm_raw,在黑暗中独自一人优雅舒缓地呼吸。

第六章:系统全景运转数据流图

闭上眼睛,在你的脑海中现在应该建立起这样一张清晰的工业级硅片流水线全景图:

板载 50MHz 晶振 ──► 【sys_pll 锁相环】 ────(洁净时钟 pll_clk)────┐
                                                                 │
外部复位按键 ────► 【4级打拍同步链】 ──(安全复位 core_rst_n)─► 【pwm_breath_core】
                                                                 │
                                                    ┌────────────┴────────────┐
                                                    ▼                         ▼
                                         【快速齿轮 pwm_counter】  【慢速齿轮 step_counter】
                                          (0~255飞转,造 195kHz 方波)  (每 7.8ms 步进调光一阶)
                                                    │                         │
                                                    └────────────┬────────────┘
                                                                 ▼
                                                    【比较器产生波形 pwm_raw】
                                                      (4 秒周期的呼吸波形)
                                                                 │
                                                                 ▼
                                                    【板级极性选择 MUX】
                                                                 │
                                                                 ▼
                                                  物理 LED[0] 柔美呼吸闪耀!

本讲核心自测与总结

  1. PWM 的本质:以人眼不可辨的超高开关频率,通过调制一个周期内高电平所占的比例(占空比),实现连续模拟量输出控制。
  2. 双尺度时钟分解:微观载波齿轮负责无频闪的灰度积分,宏观步进齿轮负责拟合人类生理呼吸时间。
  3. PLL 不仅是变速器,更是净化器:现代高速电路必须依靠 PLL 压制时钟抖动。
  4. 异步复位同步释放是底线规范:按下立即清零保证安全,松手打拍释放消除亚稳态。