第二课:时序逻辑入门-零基础FPGA闯关教程

🎮 第二课:时序逻辑入门-零基础FPGA闯关教程

🎯 学习目标:通过两个趣味项目,学会用按键控制LED、理解PWM调光原理

⏱️ 预计时间:2-3小时(包含动手实践)

🛠️ 需要准备

  • FPGA开发板(或仿真软件)
  • 1个LED灯
  • 1个按键开关

📖 开始前的小故事

还记得第一关我们让LED自己闪烁吗?这次我们要更进一步:

项目1 :按一下按键,LED闪烁速度变快(就像给电风扇调速)
项目2:让LED像"呼吸"一样慢慢变亮再变暗(模仿手机呼吸灯)

准备好了吗?Let's Go! 🚀



🎮 项目1:可调速LED(像遥控器调电扇速度)

🤔 先理解原理(用生活例子)

📺 类比:电视机的频道切换

复制代码
按一次遥控器 → 换到下一个频道
再按一次     → 继续换频道
按第3次      → 回到第1个频道(循环)

我们的LED也是这样:
按一次按键 → 1秒闪1次
再按一次   → 1秒闪2次  
第3次      → 1秒闪4次
第4次      → 又回到1秒闪1次

🧩 需要解决的3个小问题

问题1:按键信号不干净❌

真实情况:当你按下按键的瞬间,信号其实是这样的:

复制代码
理想信号:  ┐_________  (干净的一次按下)

实际信号:  ┐_┐_┐____  (抖动!像弹簧)
             ↑ 这些毛刺会被误认为按了好几次

解决办法防抖动 - 等待20毫秒,确认信号稳定了再认可


问题2:按住不放会一直触发❌

我们想要的 :只在"按下瞬间"切换一次速度
不想要的:按住不放,速度疯狂切换

解决办法边沿检测 - 只在信号从"1变成0"时触发

复制代码
松开状态: ‾‾‾‾‾‾
按下瞬间:       ↓ 只在这一刻触发!
按住状态:         ______

问题3:需要记住当前是哪个速度档位

解决办法状态机 - 像红绿灯一样,记住当前状态

复制代码
状态机工作流程:
    [状态0: 1Hz]
         ↓ 按键
    [状态1: 2Hz]
         ↓ 按键
    [状态2: 4Hz]
         ↓ 按键
    [状态0: 1Hz]  ← 循环回来


💻 代码实现(分3步写)

🔧 第1步:按键防抖动模块(key_debounce.v)

💡 这个模块的作用:把不干净的按键信号变干净,输出一个"按下脉冲"

verilog 复制代码
// ===================================================================
// 文件名:key_debounce.v
// 功能:按键防抖动 - 消除物理抖动,输出干净的按键信号
// ===================================================================

module key_debounce(
    input  wire clk,           // 时钟信号(50MHz)
    input  wire rst_n,         // 复位按钮(按下=0)
    input  wire key_in,        // 原始按键输入(按下=0,松开=1)
    output reg  key_press      // 输出:按下瞬间的1个脉冲
);

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 📊 第1部分:定义参数(可以理解为"设置")
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    parameter CLK_FREQ = 50_000_000;           // 时钟频率50MHz
    parameter DEBOUNCE_TIME = 20;              // 防抖时间20毫秒
    
    // 计算需要计数多少次(20毫秒需要1,000,000次时钟)
    parameter CNT_MAX = CLK_FREQ / 1000 * DEBOUNCE_TIME - 1;
    // 结果:999,999(数到这个数就是20ms)
    
    
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 🧮 第2部分:声明需要用的"变量"(寄存器)
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    reg [19:0] cnt;              // 计数器(20位可以存到1,048,575)
    reg key_sync_0;              // 同步寄存器1
    reg key_sync_1;              // 同步寄存器2
    reg key_state;               // 当前稳定的按键状态
    reg key_state_last;          // 上一次的按键状态
    
    
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 🔄 第3部分:按键信号同步(防止出错)
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 为什么要同步?因为按键信号来自外部,可能不稳定
    
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            key_sync_0 <= 1'b1;   // 复位时认为按键是松开的
            key_sync_1 <= 1'b1;
        end
        else begin
            key_sync_0 <= key_in;        // 第1次采样
            key_sync_1 <= key_sync_0;    // 第2次采样(更稳定)
        end
    end
    
    
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // ⏱️ 第4部分:防抖动计数(等待20ms确认)
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            cnt <= 20'd0;
            key_state <= 1'b1;   // 初始状态:松开
        end
        else begin
            // 如果检测到信号变化了
            if (key_sync_1 != key_state) begin
                // 开始计数,看信号能否稳定20ms
                if (cnt >= CNT_MAX) begin       
                    cnt <= 20'd0;               // 计数够了,重新开始
                    key_state <= key_sync_1;    // 确认新状态
                end
                else begin
                    cnt <= cnt + 1'b1;          // 继续数+1
                end
            end
            else begin
                // 信号没变化,计数器清零
                cnt <= 20'd0;
            end
        end
    end
    
    
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 📈 第5部分:检测"按下瞬间"(边沿检测)
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            key_state_last <= 1'b1;
            key_press <= 1'b0;
        end
        else begin
            key_state_last <= key_state;  // 记住上一次的状态
            
            // 检测:上一次是1(松开),现在是0(按下)
            if (key_state_last && !key_state) begin
                key_press <= 1'b1;   // 输出1个时钟周期的脉冲
            end
            else begin
                key_press <= 1'b0;   // 其他时候都是0
            end
        end
    end

endmodule

💡 这个模块的作用:控制LED闪烁速度,调用上面的防抖动模块

verilog 复制代码
// ===================================================================
// 文件名:adjustable_blink_led.v
// 功能:可调速LED闪烁器
// 按键切换速度:1Hz → 2Hz → 4Hz → 1Hz(循环)
// ===================================================================

module adjustable_blink_led(
    input  wire clk,         // 时钟(50MHz)
    input  wire rst_n,       // 复位按钮
    input  wire key_speed,   // 速度切换按键(按下=0)
    output reg  led          // LED输出
);

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 📊 第1部分:设置参数
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    parameter CLK_FREQ = 50_000_000;  // 50MHz时钟
    
    // 三种速度需要计数的次数(数到这个数LED翻转一次)
    parameter CNT_1HZ = CLK_FREQ / 2 - 1;      // 1Hz: 24,999,999
    parameter CNT_2HZ = CLK_FREQ / 4 - 1;      // 2Hz: 12,499,999  
    parameter CNT_4HZ = CLK_FREQ / 8 - 1;      // 4Hz: 6,249,999
    
    // 定义3个状态(用2位二进制表示:00, 01, 10)
    parameter STATE_1HZ = 2'b00;   // 状态0:1Hz模式
    parameter STATE_2HZ = 2'b01;   // 状态1:2Hz模式
    parameter STATE_4HZ = 2'b10;   // 状态2:4Hz模式
    
    
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 🧮 第2部分:声明变量
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    wire key_press;                // 按键按下脉冲(从防抖模块来)
    reg [1:0] current_state;       // 当前是哪个速度(00/01/10)
    reg [24:0] cnt_max;            // 当前速度的计数最大值
    reg [24:0] led_cnt;            // LED闪烁计数器


    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 🔘 第3部分:调用按键防抖动模块
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    key_debounce u_key_debounce(
        .clk        (clk),
        .rst_n      (rst_n),
        .key_in     (key_speed),     // 原始按键信号
        .key_press  (key_press)      // 输出:干净的按下脉冲
    );


    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // ⚙️ 第4部分:状态机(速度档位切换)
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            current_state <= STATE_1HZ;  // 复位后从1Hz开始
        end
        else if (key_press) begin        // 检测到按键按下
            // 根据当前状态,切换到下一个状态
            case (current_state)
                STATE_1HZ: current_state <= STATE_2HZ;  // 1Hz→2Hz
                STATE_2HZ: current_state <= STATE_4HZ;  // 2Hz→4Hz
                STATE_4HZ: current_state <= STATE_1HZ;  // 4Hz→1Hz(循环)
                default:   current_state <= STATE_1HZ;  // 出错保护
            endcase
        end
    end
    
    
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 🎯 第5部分:根据状态选择计数最大值
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    always @(*) begin  // 组合逻辑(立即响应)
        case (current_state)
            STATE_1HZ: cnt_max = CNT_1HZ;  // 1Hz用最大值
            STATE_2HZ: cnt_max = CNT_2HZ;  // 2Hz用中等值
            STATE_4HZ: cnt_max = CNT_4HZ;  // 4Hz用最小值
            default:   cnt_max = CNT_1HZ;  // 默认1Hz
        endcase
    end
    
    
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 💡 第6部分:LED闪烁控制
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            led_cnt <= 25'd0;
            led     <= 1'b0;
        end
        else begin
            // 计数器+1
            if (led_cnt >= cnt_max) begin     // 数到最大值了
                led_cnt <= 25'd0;             // 计数器清零
                led     <= ~led;              // LED状态翻转(0变1,1变0)
            end
            else begin
                led_cnt <= led_cnt + 1'b1;    // 继续数+1
            end
        end
    end

endmodule

💡 这个文件的作用:在电脑上模拟真实按键,验证代码是否正确

verilog 复制代码
// ===================================================================
// 文件名:tb_adjustable_blink_led.v
// 功能:测试台 - 模拟按键操作,观察LED响应
// ===================================================================

`timescale 1ns/1ps  // 时间单位:1纳秒

module tb_adjustable_blink_led;

    // 声明信号
    reg clk;
    reg rst_n;
    reg key_speed;
    wire led;
    
    
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 🔌 连接被测试的模块(加速参数,否则仿真太慢)
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    adjustable_blink_led #(
        .CLK_FREQ(1000)  // 假设时钟只有1000Hz(加速1万倍!)
    ) uut (
        .clk       (clk),
        .rst_n     (rst_n),
        .key_speed (key_speed),
        .led       (led)
    );
    
    
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // ⏰ 生成时钟信号(每10ns翻转一次 = 100MHz)
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    initial clk = 0;
    always #5 clk = ~clk;  // 每5ns翻转(周期10ns)
    
    
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 🎬 测试流程(模拟真实操作)
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    initial begin
        // 在终端显示信息
        $display("========================================");
        $display("   开始测试可调速LED");
        $display("========================================");
        
        // 1. 初始化
        rst_n = 0;          // 按下复位
        key_speed = 1;      // 按键松开(记住:松开=1)
        
        // 2. 释放复位
        #100;               // 等待100ns
        rst_n = 1;          // 松开复位按钮
        $display("[时间 %0t] 复位完成,进入1Hz模式", $time);
        
        // 3. 观察1Hz模式运行
        #1000_000;  // 等待1毫秒(对应实际的0.5秒)
        
        // 4. 第1次按键:切换到2Hz
        key_speed = 0;      // 按下按键
        #200_000;           // 保持按下200微秒(模拟真实按键)
        key_speed = 1;      // 松开按键
        $display("[时间 %0t] 按键1次,切换到2Hz模式", $time);
        
        #1000_000;  // 观察2Hz运行
        
        // 5. 第2次按键:切换到4Hz
        key_speed = 0;
        #200_000;
        key_speed = 1;
        $display("[时间 %0t] 按键2次,切换到4Hz模式", $time);
        
        #1000_000;  // 观察4Hz运行
        
        // 6. 第3次按键:循环回1Hz
        key_speed = 0;
        #200_000;
        key_speed = 1;
        $display("[时间 %0t] 按键3次,回到1Hz模式", $time);
        
        #1000_000;
        
        $display("========================================");
        $display("   测试完成!");
        $display("========================================");
        $finish;  // 结束仿真
    end
    
    
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 📊 生成波形文件(可以用GTKWave查看)
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    initial begin
        $dumpfile("adjustable_blink_led.vcd");
        $dumpvars(0, tb_adjustable_blink_led);
    end

endmodule

🎯 小测验:检查你学会了吗?

问题1:为什么要防抖动?

因为物理按键在按下的瞬间会产生抖动(像弹簧一样弹几下),如果不处理,FPGA会误认为按了好几次。防抖动就是等待20毫秒,确认信号稳定了再认可。

问题2:状态机是什么?

状态机就像一个有记忆的开关,能记住当前是第几个档位。比如:

  • 状态0:1Hz模式
  • 状态1:2Hz模式
  • 状态2:4Hz模式

每按一次按键,就切换到下一个状态。

问题3:如果我想增加一个8Hz的档位,该怎么改?

需要修改3个地方:

verilog 复制代码
// 1. 增加参数
parameter CNT_8HZ = CLK_FREQ / 16 - 1;

// 2. 增加状态
parameter STATE_8HZ = 2'b11;

// 3. 修改状态切换
STATE_4HZ: current_state <= STATE_8HZ;
STATE_8HZ: current_state <= STATE_1HZ;

// 4. 增加计数值选择
STATE_8HZ: cnt_max = CNT_8HZ;



🌟 项目2:LED呼吸灯(像手机通知灯)

🤔 什么是PWM?(用风扇来理解)

📺 类比:电风扇的速度控制

传统方法(调电压):

复制代码
档位1:220V → 慢速
档位2:180V → 中速
档位3:100V → 快速

PWM方法(快速开关):

复制代码
慢速:开0.3秒,关0.7秒,开0.3秒... (30%时间开)
中速:开0.5秒,关0.5秒,开0.5秒... (50%时间开)
快速:开0.8秒,关0.2秒,开0.8秒... (80%时间开)

💡 关键点:开关速度要足够快(比如每秒100次),人眼看起来就是"连续变化"的亮度


🎨 PWM波形示意图

复制代码
100%亮度(一直开): 
████████████████████████

75%亮度(开75%,关25%):
███████████████████░░░░░

50%亮度(开关各一半):
████████████░░░░░░░░░░░░

25%亮度(开25%,关75%):
██████░░░░░░░░░░░░░░░░░░

0%亮度(一直关):
░░░░░░░░░░░░░░░░░░░░░░░░

术语解释

  • 占空比 = 开的时间 ÷ 总时间
  • PWM频率 = 每秒开关多少次(建议1000Hz以上)

🌊 呼吸效果的秘密

简单方法(线性变化)❌

复制代码
亮度值: 0 → 50 → 100 → 150 → 200 → 255 → 200 → ...
         ↑ 每次固定增加50

效果:  ╱‾‾‾╲     看起来有点"硬",不自然
       ╱     ╲
      ╱       ╲___

高级方法(平方变化)✅

复制代码
亮度值: 0 → 10 → 40 → 90 → 160 → 255 → 160 → ...
         ↑ 增量越来越大

效果:   ╱‾╲       像真正的呼吸,柔和自然
        ╱   ╲
       ╱     ╲___

💡 为什么用平方? 因为人眼对亮度的感知是非线性的(物理学知识:韦伯-费希纳定律)



💻 代码实现(分3个模块)

🔧 模块1:PWM波形生成器(pwm_generator.v)

💡 作用:根据亮度值生成PWM信号

verilog 复制代码
// ===================================================================
// 文件名:pwm_generator.v
// 功能:生成PWM波形 - 把亮度值转换成开关信号
// ===================================================================

module pwm_generator(
    input  wire clk,              // 时钟
    input  wire rst_n,            // 复位
    input  wire [7:0] duty,       // 亮度值(0~255)
    output reg  pwm_out           // PWM输出
);

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 参数设置
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    parameter CLK_FREQ = 50_000_000;  // 50MHz时钟
    parameter PWM_FREQ = 1000;        // PWM频率1kHz(推荐500Hz~5kHz)
    
    // 计算一个PWM周期需要多少个时钟
    // 50,000,000 ÷ 1000 = 50,000个时钟
    parameter PWM_PERIOD = CLK_FREQ / PWM_FREQ - 1;
    
    
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 信号声明
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    reg [15:0] pwm_cnt;       // PWM计数器(0~49999)
    reg [7:0]  duty_reg;      // 锁存的亮度值
    
    
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // PWM生成逻辑(核心原理)
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 
    // 工作原理(举例说明):
    // 假设PWM_PERIOD=9(实际是49999),duty=6(60%亮度)
    // 
    // 时刻 0: cnt=0, (0>>8)=0 < 6 → 输出1
    // 时刻 1: cnt=1, (1>>8)=0 < 6 → 输出1
    // ...
    // 时刻 5: cnt=5, (5>>8)=0 < 6 → 输出1
    // 时刻 6: cnt=6, (6>>8)=0 < 6 → 输出1
    // 时刻 7: cnt=7, (7>>8)=0 < 6 → 输出1
    // 时刻 8: cnt=8, (8>>8)=0 < 6 → 输出1
    // 时刻 9: cnt=9, (9>>8)=0 < 6 → 输出1
    // 
    // 结果:前6个时钟输出1,后4个输出0 → 60%占空比
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            pwm_cnt  <= 16'd0;
            duty_reg <= 8'd0;
            pwm_out  <= 1'b0;
        end
        else begin
            // 步骤1:计数器循环(0→49999→0...)
            if (pwm_cnt >= PWM_PERIOD) begin
                pwm_cnt <= 16'd0;
                duty_reg <= duty;  // 在新周期开始时更新亮度值
            end
            else begin
                pwm_cnt <= pwm_cnt + 1'b1;
            end
            
            // 步骤2:比较产生PWM波形
            // 把16位计数器映射到8位分辨率(右移8位)
            if ((pwm_cnt >> 8) < duty_reg) begin
                pwm_out <= 1'b1;   // 计数值小于亮度值 → 输出高
            end
            else begin
                pwm_out <= 1'b0;   // 计数值大于等于亮度值 → 输出低
            end
        end
    end

endmodule

🧮 模块2:亮度校正模块(gamma_correction.v)

💡 作用:把线性亮度值转换成符合人眼感知的非线性值

verilog 复制代码
// ===================================================================
// 文件名:gamma_correction.v
// 功能:平方律亮度校正 - 让呼吸效果更自然
// ===================================================================

module gamma_correction(
    input  wire [7:0] brightness_in,    // 输入:线性亮度(0~255)
    output reg  [7:0] pwm_duty_out      // 输出:校正后的PWM值
);

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 平方律公式:pwm = (brightness^2) / 255
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 
    // 例子:
    //   输入64  → 64×64=4096   → 4096÷255=16   (25% → 6%)
    //   输入128 → 128×128=16384 → 16384÷255=64  (50% → 25%)
    //   输入192 → 192×192=36864 → 36864÷255=144 (75% → 56%)
    //   输入255 → 255×255=65025 → 65025÷255=255 (100% → 100%)
    // 
    // 这样低亮度时变化慢,高亮度时变化快,符合人眼特性
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    wire [15:0] square;        // 平方结果(最大255×255=65025)
    wire [23:0] compensated;   // 补偿后的结果
    
    // 步骤1:计算平方
    assign square = brightness_in * brightness_in;
    
    // 步骤2:除以255的优化算法
    // 直接除法电路复杂,用乘法近似:
    // x ÷ 255 ≈ (x × 257) ÷ 65536
    // 除以65536就是右移16位,很简单!
    assign compensated = square * 257;
    
    // 步骤3:取高8位作为结果
    always @(*) begin
        pwm_duty_out = compensated[23:16];
    end

endmodule

🌊 模块3:呼吸灯主控制(breathing_led.v)

💡 作用:生成呼吸亮度曲线,组合所有模块

verilog 复制代码
// ===================================================================
// 文件名:breathing_led.v
// 功能:LED呼吸灯主控制器
// ===================================================================

module breathing_led(
    input  wire clk,         // 50MHz时钟
    input  wire rst_n,       // 复位
    output wire led          // PWM输出到LED
);

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 📊 参数设置(可以调整这些值改变效果)
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    parameter CLK_FREQ = 50_000_000;      // 时钟频率
    parameter BREATH_SPEED = 100_000;     // 呼吸速度控制
    
    // 计算完整呼吸周期:
    // 一个周期 = BREATH_SPEED × 256 × 2 ÷ CLK_FREQ
    //          = 100,000 × 512 ÷ 50,000,000
    //          ≈ 1秒(变亮0.5秒+变暗0.5秒)
    
    
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 🧮 信号声明
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    reg [16:0] breath_cnt;     // 呼吸速度计数器
    reg [7:0]  brightness;     // 当前亮度值(0~255)
    reg        direction;      // 方向标志(0=变亮,1=变暗)
    
    wire [7:0] pwm_duty;       // 校正后的PWM占空比
    wire       pwm_out;        // PWM输出


    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 🌈 步骤1:生成呼吸亮度曲线(0→255→0循环)
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            breath_cnt <= 17'd0;
            brightness <= 8'd0;
            direction  <= 1'b0;      // 初始为变亮
        end
        else begin
            // 速度控制:每BREATH_SPEED个时钟更新一次亮度
            if (breath_cnt >= BREATH_SPEED - 1) begin
                breath_cnt <= 17'd0;
                
                // 根据方向调整亮度
                if (direction == 1'b0) begin      // 正在变亮
                    if (brightness >= 8'd255) begin
                        direction <= 1'b1;        // 到最亮了,开始变暗
                        brightness <= brightness - 1'b1;
                    end
                    else begin
                        brightness <= brightness + 1'b1;  // 继续变亮+1
                    end
                end
                else begin                         // 正在变暗
                    if (brightness == 8'd0) begin
                        direction <= 1'b0;        // 到最暗了,开始变亮
                        brightness <= brightness + 1'b1;
                    end
                    else begin
                        brightness <= brightness - 1'b1;  // 继续变暗-1
                    end
                end
            end
            else begin
                breath_cnt <= breath_cnt + 1'b1;  // 继续计数
            end
        end
    end


    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 🎨 步骤2:调用亮度校正模块
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    gamma_correction u_gamma(
        .brightness_in  (brightness),   // 输入:线性亮度
        .pwm_duty_out   (pwm_duty)      // 输出:校正后的占空比
    );


    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // ⚡ 步骤3:调用PWM生成器
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    pwm_generator u_pwm(
        .clk       (clk),
        .rst_n     (rst_n),
        .duty      (pwm_duty),    // 使用校正后的亮度
        .pwm_out   (pwm_out)
    );
    
    // 输出到LED引脚
    assign led = pwm_out;

endmodule

🧪 模块4:测试代码(tb_breathing_led.v)

💡 作用:在电脑上模拟运行,观察呼吸效果

verilog 复制代码
// ===================================================================
// 文件名:tb_breathing_led.v
// 功能:呼吸灯测试台
// ===================================================================

`timescale 1ns/1ps

module tb_breathing_led;

    // 信号声明
    reg clk;
    reg rst_n;
    wire led;
    
    
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 连接被测模块(加速参数)
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    breathing_led #(
        .CLK_FREQ(10000),       // 假设10kHz时钟(加速5000倍)
        .BREATH_SPEED(100)      // 快速呼吸(加速1000倍)
    ) uut (
        .clk   (clk),
        .rst_n (rst_n),
        .led   (led)
    );
    
    
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 生成时钟(100ns周期=10MHz)
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    initial clk = 0;
    always #50 clk = ~clk;  // 每50ns翻转一次
    
    
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 测试流程
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    initial begin
        $display("========================================");
        $display("   LED呼吸灯仿真测试");
        $display("========================================");
        
        // 1. 复位
        rst_n = 0;
        #1000;
        rst_n = 1;
        
        $display("[时间 %0t] 开始呼吸效果", $time);
        
        // 2. 观察2个完整呼吸周期
        #5_000_000;  // 5毫秒(对应实际约5秒)
        
        $display("[时间 %0t] 测试完成", $time);
        $finish;
    end
    
    
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 实时监测亮度值(每次更新时打印)
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    always @(posedge clk) begin
        if (uut.breath_cnt == 0) begin
            $display("[时间 %0t] 亮度=%3d, PWM占空比=%3d, %s", 
                     $time, 
                     uut.brightness,
                     uut.pwm_duty,
                     uut.direction ? "变暗中..." : "变亮中...");
        end
    end
    
    
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 生成波形文件
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    initial begin
        $dumpfile("breathing_led.vcd");
        $dumpvars(0, tb_breathing_led);
    end

endmodule


🎯 小测验:检查你学会了吗?

问题1:PWM是什么?为什么能调亮度?

PWM是"脉冲宽度调制"的缩写,通过快速开关来控制平均功率。

原理

  • 开的时间长 → 平均亮度高
  • 开的时间短 → 平均亮度低
  • 只要开关够快(>100Hz),人眼看起来就是连续变化的亮度

就像电风扇:虽然是"开关开关...",但转得快了看起来就是"慢速/快速"。

问题2:为什么要用平方校正?

因为人眼对亮度的感知是非线性的(物理学原理):

  • 如果直接用线性变化(0→50→100→150...),看起来变化不均匀
  • 使用平方校正后(0→16→64→144...),低亮度时变化慢,高亮度时变化快
  • 效果更自然,像真正的呼吸

公式:pwm_duty = (brightness × brightness) ÷ 255

问题3:如果我想让呼吸变慢,改哪个参数?

修改 BREATH_SPEED 参数,数值越大越慢:

verilog 复制代码
// 原始(1秒一个周期)
parameter BREATH_SPEED = 100_000;

// 更慢(2秒一个周期)
parameter BREATH_SPEED = 200_000;

// 更快(0.5秒一个周期)
parameter BREATH_SPEED = 50_000;

计算公式:周期(秒) = BREATH_SPEED × 512 ÷ 时钟频率



🔧 动手实践:改进呼吸灯

挑战1:增加"快呼吸"和"慢呼吸"模式 ⭐

目标:用按键切换两种速度

提示

verilog 复制代码
// 增加一个状态机
reg mode;  // 0=慢速,1=快速

always @(posedge clk) begin
    if (key_press) begin
        mode <= ~mode;  // 切换模式
    end
end

// 根据模式选择速度
wire [16:0] speed;
assign speed = mode ? 50_000 : 200_000;

挑战2:RGB彩虹呼吸灯 ⭐⭐

目标:LED颜色循环变化(红→绿→蓝→红...)

提示

  • 需要3个PWM模块(控制R/G/B三个LED)
  • 三个颜色轮流呼吸,产生彩虹效果
verilog 复制代码
// ===================================================================
// 文件名:rgb_breathing_led.v
// 功能:RGB三色呼吸灯 - 产生彩虹效果
// ===================================================================

module rgb_breathing_led(
    input  wire clk,
    input  wire rst_n,
    output wire led_r,     // 红色LED
    output wire led_g,     // 绿色LED
    output wire led_b      // 蓝色LED
);

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 参数设置
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    parameter CLK_FREQ = 50_000_000;
    parameter BREATH_SPEED = 100_000;
    
    
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 信号声明
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    reg [16:0] breath_cnt;
    reg [7:0]  brightness;
    reg        direction;
    reg [1:0]  color_state;    // 颜色状态:0=红,1=绿,2=蓝
    
    wire [7:0] pwm_duty;
    wire [7:0] duty_r, duty_g, duty_b;
    
    
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 🌈 步骤1:呼吸亮度生成(跟之前一样)
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            breath_cnt <= 17'd0;
            brightness <= 8'd0;
            direction  <= 1'b0;
            color_state <= 2'd0;
        end
        else begin
            if (breath_cnt >= BREATH_SPEED - 1) begin
                breath_cnt <= 17'd0;
                
                if (direction == 1'b0) begin
                    if (brightness >= 8'd255) begin
                        direction <= 1'b1;
                        brightness <= brightness - 1'b1;
                        
                        // 到达最亮时切换颜色
                        if (color_state >= 2'd2) begin
                            color_state <= 2'd0;  // 蓝→红
                        end
                        else begin
                            color_state <= color_state + 1'b1;
                        end
                    end
                    else begin
                        brightness <= brightness + 1'b1;
                    end
                end
                else begin
                    if (brightness == 8'd0) begin
                        direction <= 1'b0;
                        brightness <= brightness + 1'b1;
                    end
                    else begin
                        brightness <= brightness - 1'b1;
                    end
                end
            end
            else begin
                breath_cnt <= breath_cnt + 1'b1;
            end
        end
    end
    
    
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 🎨 步骤2:亮度校正
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    gamma_correction u_gamma(
        .brightness_in  (brightness),
        .pwm_duty_out   (pwm_duty)
    );
    
    
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // 🌟 步骤3:根据颜色状态分配亮度
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    assign duty_r = (color_state == 2'd0) ? pwm_duty : 8'd0;
    assign duty_g = (color_state == 2'd1) ? pwm_duty : 8'd0;
    assign duty_b = (color_state == 2'd2) ? pwm_duty : 8'd0;
    
    
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // ⚡ 步骤4:生成三路PWM
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    pwm_generator u_pwm_r(
        .clk      (clk),
        .rst_n    (rst_n),
        .duty     (duty_r),
        .pwm_out  (led_r)
    );
    
    pwm_generator u_pwm_g(
        .clk      (clk),
        .rst_n    (rst_n),
        .duty     (duty_g),
        .pwm_out  (led_g)
    );
    
    pwm_generator u_pwm_b(
        .clk      (clk),
        .rst_n    (rst_n),
        .duty     (duty_b),
        .pwm_out  (led_b)
    );

endmodule

挑战3:渐变彩虹效果(高级)⭐⭐⭐

目标:红色慢慢变成绿色,绿色慢慢变成蓝色...

关键思路

复制代码
不是突然切换颜色,而是两个颜色同时变化:

阶段1:红色从满到0,绿色从0到满(红→黄→绿)
阶段2:绿色从满到0,蓝色从0到满(绿→青→蓝)
阶段3:蓝色从满到0,红色从0到满(蓝→紫→红)

代码提示(关键部分):

verilog 复制代码
// 使用8位相位控制颜色比例
reg [7:0] color_phase;  // 0~255

// 计算三色占空比(简化算法)
always @(*) begin
    case (color_state)
        2'd0: begin  // 红→绿
            duty_r = 255 - color_phase;
            duty_g = color_phase;
            duty_b = 0;
        end
        2'd1: begin  // 绿→蓝
            duty_r = 0;
            duty_g = 255 - color_phase;
            duty_b = color_phase;
        end
        2'd2: begin  // 蓝→红
            duty_r = color_phase;
            duty_g = 0;
            duty_b = 255 - color_phase;
        end
    endcase
end

// 相位每次呼吸更新
always @(posedge clk) begin
    if (breath_cnt == 0) begin
        color_phase <= color_phase + 1;
    end
end


🎓 本节知识点总结

✅ 你学会了什么?

项目1 - 可调速LED

  • 按键防抖动原理(消除物理抖动)
  • 边沿检测(只在按下瞬间触发)
  • 状态机设计(记住当前档位)
  • 模块化编程(主模块+子模块)

项目2 - 呼吸灯

  • PWM原理(脉冲宽度调制)
  • 占空比的概念
  • 非线性亮度校正(平方律)
  • 多模块协同工作

🔍 常见问题排查

❌ 问题1:按键一按LED疯狂切换

原因 :没做防抖动
解决 :确保使用了 key_debounce 模块,等待20ms确认


❌ 问题2:LED呼吸不平滑,有明显跳变

原因1:PWM频率太低

verilog 复制代码
// 错误(50Hz人眼可见闪烁)
parameter PWM_FREQ = 50;

// 正确(1kHz流畅)
parameter PWM_FREQ = 1000;

原因2:呼吸速度太快

verilog 复制代码
// 调慢速度
parameter BREATH_SPEED = 200_000;  // 增大这个值

❌ 问题3:LED始终很暗或不亮

检查点

  1. 确认PWM模块有输出波形(用仿真查看)
  2. 检查亮度值是否正常更新
  3. 检查电路连接(限流电阻、正负极)

调试技巧

verilog 复制代码
// 在testbench中添加监测
always @(posedge clk) begin
    if (uut.breath_cnt == 0) begin
        $display("亮度=%d, PWM=%d", uut.brightness, uut.pwm_duty);
    end
end


📌 与真实硬件连接

🔌 典型电路(单色LED)

复制代码
FPGA引脚 ─── 220Ω电阻 ─── LED正极
                           │
                        LED负极 ─── GND

注意:
- 电阻防止电流过大烧坏LED
- 电阻值 = (电源电压 - LED压降) ÷ 电流
  例如:(3.3V - 2V) ÷ 0.01A = 130Ω(选用150Ω或220Ω)

🌈 RGB LED连接(共阴极)

复制代码
      FPGA
       │
   ┌───┼───┐
   R   G   B  (三个PWM信号)
   │   │   │
   └─┬─┴─┬─┘
     │   │
   RGB LED
     │
    GND

提示:如果是共阳极LED,需要反相PWM信号

准备好迎接新挑战了吗?休息一下,下节课见!🚀

📚 附录:完整文件清单

项目1文件(可调速LED)

  1. key_debounce.v - 按键防抖动模块
  2. adjustable_blink_led.v - 主控制模块
  3. tb_adjustable_blink_led.v - 测试文件

项目2文件(呼吸灯)

  1. pwm_generator.v - PWM生成器
  2. gamma_correction.v - 亮度校正模块
  3. breathing_led.v - 呼吸灯主控
  4. tb_breathing_led.v - 测试文件

进阶文件(RGB彩虹灯)

  1. rgb_breathing_led.v - RGB呼吸灯

💡 学习建议

  1. 先理解再动手:不要急着写代码,先用纸笔画出工作流程
  2. 善用仿真:每写完一个模块就测试,不要等全部写完
  3. 参数可调:多试试不同的参数值,观察效果变化
  4. 记录问题:遇到bug时记录下来,以后避免重复犯错
  5. 循序渐进:先做基础版,再尝试挑战项目

🎉 恭喜你完成第2关!

你已经掌握了:

  • ✅ 状态机设计
  • ✅ 按键输入处理
  • ✅ PWM调光技术
  • ✅ 模块化编程

如果你是路过的大佬,发现我哪里理解错了,请务必指出来!如果你也是正在入门的小伙伴,欢迎一起交流学习心得~

相关推荐
Teacher.chenchong2 小时前
生态环境影响评价图件制作:融合ArcGIS与ENVI,掌握土地利用、植被覆盖、土壤侵蚀、水系提取等专题制图技术!
经验分享
小龙报3 小时前
算法通关指南:数据结构和算法篇 --- 队列相关算法题》--- 1. 【模板】队列,2. 机器翻译
c语言·开发语言·数据结构·c++·算法·学习方法·visual studio
摇滚侠3 小时前
Spring Boot3零基础教程,Reactive-Stream 发布订阅写法,笔记104 笔记105
java·spring boot·笔记
循环过三天10 小时前
3.4、Python-集合
开发语言·笔记·python·学习·算法
昌sit!11 小时前
Linux系统性基础学习笔记
linux·笔记·学习
没有钱的钱仔12 小时前
机器学习笔记
人工智能·笔记·机器学习
好望角雾眠12 小时前
第四阶段C#通讯开发-9:网络协议Modbus下的TCP与UDP
网络·笔记·网络协议·tcp/ip·c#·modbus
骑猪兜风23312 小时前
2025 年的热门 AI 编程工具评测:Cursor、Claude Code、Codex、Lovable、v0 等
经验分享
仰望—星空13 小时前
MiniEngine学习笔记 : CommandListManager
c++·windows·笔记·学习·cg·direct3d