这一篇在干嘛?

前面的实验里 LED 只有「亮」和「灭」两种状态,这一篇让它学会「渐变」——像呼吸一样由灭渐亮、再由亮渐灭。核心武器是 PWM(脉宽调制):通过高速切换亮灭并改变亮的时间比例,人眼就会看到平滑的亮度变化。你会学到周期计数器、占空比控制和 PWM 输出比较这三个经典模块的写法。

实验目标与 PWM 原理

实验任务:使用康芯开发板上的 LED,实现呼吸灯的效果,即由灭渐亮,然后再由亮渐灭。

直接给 LED 输出一个「中间电压」并不容易(FPGA 引脚只有 0 和 1),但可以利用人眼的视觉暂留:让 LED 以足够高的频率快速亮灭切换,只要**亮的时间占比(占空比)**连续变化,看起来亮度就是连续变化的。这就是 PWM 调光的基本原理。

本实验的设计思路如下,模块端口及结构框图如下图所示:

整个结构分三层:

  1. 周期信号计数器:产生 1KHz 的周期脉冲信号(周期 1ms),这是 PWM 的载波节拍;
  2. 占空比调节逻辑:每过完一个周期,就把占空比计数值递增或递减一步;
  3. 比较输出:把当前周期计数值和占空比计数值做比较,输出占空比可调的脉冲信号,驱动 LED。

几个关键数字先算清楚:系统时钟 50MHz,周期 20ns;要产生 1KHz(周期 1ms)的信号,一个周期内需要计 1ms / 20ns = 50000 个时钟周期,所以周期计数器从 0 计到 50000。占空比每次变化 25 个计数值,从 0 涨到 50000 需要 2000 个周期,即 2 秒——这就是呼吸灯「吸气」或「呼气」各约 2 秒的来源。

Verilog 实现

呼吸灯代码如下:

module breath_led(
    input sys_clk, //时钟信号50Mhz
    input sys_rst_n, //复位信号
    output led //LED
);
//reg define
reg [15:0] period_cnt; //周期计数器频率:1khz 周期:1ms 计数值:1ms/20ns=50000
reg [15:0] duty_cycle; //占空比数值
reg inc_dec_flag; //0 递增 1 递减
//******************************************************************
//main code
//******************************************************************
 
//根据占空比和计数值之间的大小关系来输出 LED
assign led = (period_cnt >= duty_cycle) ? 1'b1 : 1'b0;
//周期计数器
always @(posedge sys_clk or negedge sys_rst_n) begin
    if(!sys_rst_n)
    period_cnt <= 16'd0;
    else if(period_cnt == 16'd50000)
    period_cnt <= 16'd0;
    else
    period_cnt <= period_cnt + 1'b1;
end
 
//在周期计数器的节拍下递增或递减占空比
always @(posedge sys_clk or negedge sys_rst_n) begin
    if(!sys_rst_n) begin
    duty_cycle <= 16'd0;
    inc_dec_flag <= 1'b0;
    end
    else begin
    if(period_cnt == 16'd50000) begin //计满 1ms
    if(inc_dec_flag == 1'b0) begin //占空比递增状态
    if(duty_cycle == 16'd50000) //如果占空比已递增至最大
    inc_dec_flag <= 1'b1; //则占空比开始递减
    else //否则占空比以 25 为单位递增
    duty_cycle <= duty_cycle + 16'd25;
    end
    else begin //占空比递减状态
    if(duty_cycle == 16'd0) //如果占空比已递减至 0
    inc_dec_flag <= 1'b0; //则占空比开始递增
    else //否则占空比以 25 为单位递减
    duty_cycle <= duty_cycle - 16'd25;
    end
    end
    end
end
endmodule

(说明:原文 OCR 版本在此处缺了两个 end 和注释符,上表代码已按上下文补全,所有信号定义、参数与原文完全一致。)

逐块拆解:

① PWM 比较输出(组合逻辑)

assign led = (period_cnt >= duty_cycle) ? 1'b1 : 1'b0;

在一个 1ms 周期内,period_cnt 从 0 涨到 50000。当 period_cnt >= duty_cycle 时输出高电平,否则输出低电平。这样每个周期内高电平的持续长度正好由 duty_cycle 决定——duty_cycle 越大,高电平时间越长,占空比越高。

注意有效电平与亮度方向的对应关系

代码里 period_cnt >= duty_cycle 输出 1。如果板子上的 LED 是低电平点亮(共阳接法),占空比增大时灯反而变暗,呼吸方向会「反」过来。遇到这种现象不用慌,把比较方向反过来(或输出取反)即可。这也是 PWM 调试时最常见的第一个「为什么和我想的不一样」。

② 周期计数器

period_cnt 每个时钟加 1,计到 50000 归零,周而复始,产生 1KHz 的节拍。注意它是 16 位寄存器([15:0]),最大能表示 65535,装得下 50000。

③ 占空比递增/递减控制

这块是呼吸效果的灵魂。它只在 period_cnt == 16'd50000(即每个 1ms 周期结束的节拍)时动作:

  • inc_dec_flag == 0(递增状态):duty_cycle 每次 +25;涨到 50000(最亮)后把标志翻成 1,开始递减;
  • inc_dec_flag == 1(递减状态):duty_cycle 每次 −25;减到 0(熄灭)后把标志翻回 0,开始递增。

如此循环往复,占空比就在 0 到 50000 之间走锯齿波,LED 的亮度随之由灭渐亮、再由亮渐灭。

呼吸快慢由「步长」决定

占空比每次递增/递减的数值 25 就是呼吸频率旋钮:步长越大,走完 0→50000 的周期数越少,呼吸越快;步长越小,呼吸越舒缓。想让呼吸慢下来,把 25 改成 10 试试;想明显加快就改成 100。注意步长要能整除关系合理,否则最亮/最暗点附近的判断(== 50000== 0)可能错过,导致呼吸「卡死」在某一状态。

另外提醒一点:duty_cycle 只在周期结束时变化一次,而比较输出每个时钟都在进行,所以 PWM 波形在 1ms 之内是稳定的,不会出现一个周期内占空比突跳的毛刺。

引脚约束

本实验用到系统时钟、复位和 4 个 LED(代码只输出一路 led 信号,约束表按实验板通用配置给出 LED0~LED3):

set_pin_assignment { led[0] } { LOCATION = A4; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { led[1] } { LOCATION = A3; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { led[2] } { LOCATION = C10; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { led[3] } { LOCATION = B12; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sys_clk } { LOCATION = R7; IOSTANDARD = LVCMOS33;
PULLTYPE = PULLUP; }
set_pin_assignment { sys_rst_n } { LOCATION = A2; IOSTANDARD = LVCMOS33;
PULLTYPE = PULLUP; }

整理成表格:

信号FPGA 引脚说明
sys_clkR750MHz 系统时钟
sys_rst_nA2复位按键,低有效
led[0]A4LED1
led[1]A3LED2
led[2]C10LED3
led[3]B12LED4

引脚分配的操作方法(IO Constraint 手动分配或 .adc 文件)与软件流程篇完全相同,不再赘述。

下载验证

按照标准流程编译布线后,插好下载线,下载程序至核心板。此时可以看到核心板 LED1 发光管由亮到暗,又由暗到亮,往复循环——呼吸灯跑起来了。

如果看到的现象是「呼吸」方向反了(暗→亮变成亮→暗),回忆前面 warning 里说的有效电平问题;如果灯只是匀速闪烁而不是渐变,检查占空比步长是否写错、period_cnt == 16'd50000 的判断条件是否生效。

现象与总结

本实验的关键收获:

  1. PWM 的本质:数字引脚只有高低电平,但用 1KHz 以上的快速亮灭切换 + 可变占空比,就能骗过人眼得到「模拟」的亮度;
  2. 三层结构:周期计数器(载波)→ 占空比控制(慢速锯齿)→ 比较输出(PWM 波形),这个结构是电机调速、蜂鸣器变调等一切 PWM 应用的通用骨架;
  3. 两个时间尺度的嵌套:20ns 的时钟节拍撑起 1ms 的 PWM 周期,1ms 的节拍又撑起约 4 秒的完整呼吸周期(渐亮 2 秒 + 渐灭 2 秒)。

改参数做扩展练习:把步长 25 改成 50,观察呼吸变快;把 duty_cycle 位宽和最大值改成 25000,观察亮度上限变化。

自测