🎮 第二课:时序逻辑入门-零基础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
🎯 第2步:主控制模块(adjustable_blink_led.v)
💡 这个模块的作用:控制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
🧪 第3步:测试代码(tb_adjustable_blink_led.v)
💡 这个文件的作用:在电脑上模拟真实按键,验证代码是否正确
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始终很暗或不亮
检查点:
- 确认PWM模块有输出波形(用仿真查看)
- 检查亮度值是否正常更新
- 检查电路连接(限流电阻、正负极)
调试技巧:
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)
key_debounce.v- 按键防抖动模块adjustable_blink_led.v- 主控制模块tb_adjustable_blink_led.v- 测试文件
项目2文件(呼吸灯)
pwm_generator.v- PWM生成器gamma_correction.v- 亮度校正模块breathing_led.v- 呼吸灯主控tb_breathing_led.v- 测试文件
进阶文件(RGB彩虹灯)
rgb_breathing_led.v- RGB呼吸灯
💡 学习建议
- 先理解再动手:不要急着写代码,先用纸笔画出工作流程
- 善用仿真:每写完一个模块就测试,不要等全部写完
- 参数可调:多试试不同的参数值,观察效果变化
- 记录问题:遇到bug时记录下来,以后避免重复犯错
- 循序渐进:先做基础版,再尝试挑战项目
🎉 恭喜你完成第2关!
你已经掌握了:
- ✅ 状态机设计
- ✅ 按键输入处理
- ✅ PWM调光技术
- ✅ 模块化编程
如果你是路过的大佬,发现我哪里理解错了,请务必指出来!如果你也是正在入门的小伙伴,欢迎一起交流学习心得~