呼吸灯实际展示的效果就是一个 LED 灯的亮度由亮到暗,再由暗到亮的变化过程,并且该过程是循环往复的,像呼吸一样那么有节奏。 呼吸灯通常是采用 PWM(Pulse Width Modulation,即脉冲宽度调制) 的方式实现,在 PWM 频率固定的情况下,通过调整其占空比来控制 LED 灯亮度的变化。
在固定周期的 PWM 信号下,如果其占空比为 0,则 LED 灯不亮;如果其占空比为100%,则 LED 灯最亮。将占空比从 0 到 100%,再从 100%到 0 不断变化,就可以实现 LED 灯的“呼吸”效果,PWM 占空比调节示意图如下所示:
可以使用1个寄存器和一个比较值来控制 PWM 信号的产生,寄存器进行周期计数,控制 PWM 的周期,在计数器周期计数过程中同时将计数值与比较值比较,当周期计数器的值小于比较寄存器时输出低电平,否则输出高电平,此时通过调节比较值便可调节 PWM 占空比,调节周期计数器的最大计数值便可调节 PWM 的周期,PWM 信号产生原理如下图所示:
LED0 到 LED3 这 4 个发光二极管的阴极分别连到 S8050(NPN 三极管)的集电极上,阳极都与 3.3V 电压相连,三极管的基极分别与 FPGA 相连,这是由于 FPGA 的 IO 口的电压只有 1.5V,电压较低,所以此处连接三极管是为了起到放大电压的作用。这样就可以通过改变三极管的状态来控制 LED 的亮灭。当 FPGA 输出到为高电平时,三极管导通,LED 灯亮;当 FPGA 输出到为低电平时,三极管截止,LED 灯灭。
此实验中只用到了LED0
系统框图如下,包括两部分,分别是产生PWM信号的PWM模块和控制PWM占空比的呼吸灯模块
PWM信号生成模块包含一个周期计数器和一个比较值,比较值通过模块外部输入的占空比结合PWM周期转换得到(这里由呼吸灯模块进行占空比控制),模块代码如下:
`timescale 1ns / 1ns
module pwm_generate
(
input sys_clk, //系统时钟
input sys_rst_n, //系统复位,低电平有效
input [31:0] period, //PWM周期
input [31:0] duty_cycle, //PWM占空比
output pwm_out //输出的PWM信号
);
//PWM周期
wire [31:0] pwm_period;
//PWM比较值
wire [31:0] pwm_compare;
//周期计数器,用于控制PWM周期
reg [31:0] count;
//PWM周期,不能小于1
assign pwm_period = (period < 1) ? 1 : period;
//将PWM占空比转换为比较值
assign pwm_compare = (duty_cycle < period) ? (period - duty_cycle) : 0;
//周期计数器的值小于比较寄存器时输出低电平,否则输出高电平
assign pwm_out = (count < pwm_compare) ? 0 : 1;
//进行周期计数,用于控制PWM输出周期
always @(posedge sys_clk) begin
if(!sys_rst_n)
count <= 0;
else if(count < (period - 1))
count = count + 1;
else
count <= 0;
end
endmodule
PWM信号生成模块仿真激励代码很简单,就是在周期产生时钟信号的同时调整PWM占空比即可,代码如下:
`timescale 1ns / 1ns //仿真单位/仿真精度
module tb_pwm_generate();
reg sys_clk; //时钟
reg sys_rst_n; //复位
reg [31:0] duty_cycle; //占空比
wire pwm_out; //PWWM信号
initial begin
sys_clk = 1'b0;
sys_rst_n = 1'b0;
duty_cycle = 0;
#200
sys_rst_n = 1'b1;
//占空比为1
#1000
duty_cycle = 1;
//占空比为2
#1000
duty_cycle = 2;
//占空比为3
#1000
duty_cycle = 3;
//占空比为4
#1000
duty_cycle = 4;
//占空比为5
#1000
duty_cycle = 5;
//占空比为6
#1000
duty_cycle = 6;
//占空比为7
#1000
duty_cycle = 7;
//占空比为8
#1000
duty_cycle = 8;
//占空比为9
#1000
duty_cycle = 9;
//占空比为10
#1000
duty_cycle = 10;
end
//产生时钟
always #10 sys_clk = ~sys_clk;
pwm_generate u_tb_pwm_generate_inst (
.sys_clk(sys_clk), //系统时钟
.sys_rst_n(sys_rst_n), //系统复位,低电平有效
.period(10), //PWM周期
.duty_cycle(duty_cycle), //PWM占空比
.pwm_out(pwm_out) //输出的PWM信号
);
endmodule
呼吸灯模块用于向PWM模块输出一个占空比,内部包含以下几个部分:
`timescale 1ns / 1ns
module breath_led #(
parameter PWM_PERIOD = 100_000, //pwm信号的周期
parameter BREATH_PERIOD = 100_000_000, //呼吸灯周期
parameter BRIGHT_LEVEL = 100 //亮度等级
)
(
input sys_clk, //系统时钟
input sys_rst_n, //系统复位,低电平有效
output reg [31:0] duty_cycle //PWM占空比
);
//PWM占空比调节步进,分100个亮度等级进行占空比调节
localparam DUTY_CYCLE_STEP = PWM_PERIOD / BRIGHT_LEVEL;
//PWM占空比调节间隔,分100个亮度等级进行占空比调节
localparam STEP_INTERVAL = BREATH_PERIOD / BRIGHT_LEVEL;
//周期计数器,用于控制PWM占空比调节间隔
reg [31:0] period_count;
//占空比调节次数计数器,用于记录调节占空比的次数
reg [31:0] step_count;
//占空比调节方向控制,为0占空比加,为1占空比减
reg inc_dec_flag;
//进行周期计数,用于控制PWM占空比调节间隔
always @(posedge sys_clk) begin
if(!sys_rst_n)
period_count <= 0;
else if(period_count < (STEP_INTERVAL - 1))
period_count <= period_count + 1;
else
period_count <= 0;
end
//根据占空比调节方向标志进行占空比调节,控制LED亮度
always @(posedge sys_clk) begin
if(!sys_rst_n)
duty_cycle <= 0;
else if(period_count == (STEP_INTERVAL - 1)) begin
if((inc_dec_flag == 1'b0) && ((PWM_PERIOD - duty_cycle) >= DUTY_CYCLE_STEP))
duty_cycle <= duty_cycle + DUTY_CYCLE_STEP;
else if((inc_dec_flag == 1'b1) && (duty_cycle >= DUTY_CYCLE_STEP))
duty_cycle <= duty_cycle - DUTY_CYCLE_STEP;
end
end
//记录调节占空比的次数
always @(posedge sys_clk) begin
if(!sys_rst_n)
step_count <= 0;
else if(period_count == (STEP_INTERVAL - 1)) begin
if(step_count < (BRIGHT_LEVEL - 1))
step_count <= step_count + 1;
else
step_count <= 0;
end
end
//占空比调节方向控制,为0占空比加,为1占空比减
//当占空比连续递增(递减)到指定次数时说明占空比达到最大(最小),此时改变占空比调节方向标志
always @(posedge sys_clk) begin
if(!sys_rst_n)
inc_dec_flag <= 0;
else if(period_count == (STEP_INTERVAL - 1)) begin
if(step_count == (BRIGHT_LEVEL - 1))
inc_dec_flag <= ~inc_dec_flag;
end
end
endmodule
呼吸灯模块仿真激励代码非常简单,只需要产生激励时钟即可,代码如下:
`timescale 1ns / 1ns //仿真单位/仿真精度
module tb_breath_led();
reg sys_clk; //时钟
reg sys_rst_n; //复位
wire [31:0] duty_cycle; //占空比
initial begin
sys_clk = 1'b0;
sys_rst_n = 1'b0;
#200
sys_rst_n = 1'b1;
end
//产生时钟
always #10 sys_clk = ~sys_clk;
breath_led #(
.PWM_PERIOD(100), //pwm信号的周期
.BREATH_PERIOD(10_000), //呼吸灯周期
.BRIGHT_LEVEL(10) //亮度等级
)
u_tb_breath_led_inst(
.sys_clk(sys_clk), //系统时钟
.sys_rst_n(sys_rst_n), //系统复位,低电平有效
.duty_cycle(duty_cycle) //PWM占空比
);
endmodule
顶层模块主要用于例化PWM生成模块和呼吸灯模块,并将两个模块进行关联,代码如下:
`timescale 1ns / 1ns
module top_breath_led #(
parameter PWM_PERIOD = 100_000, //pwm信号的周期
parameter BREATH_PERIOD = 100_000_000, //呼吸灯周期
parameter BRIGHT_LEVEL = 100 //亮度等级
)
(
input sys_clk, //系统时钟
input sys_rst_n, //系统复位,低电平有效
output led //LED
);
//PWM占空比
wire [31:0] duty_cycle;
//例化呼吸灯模块
breath_led #(
.PWM_PERIOD(PWM_PERIOD), //pwm信号的周期
.BREATH_PERIOD(BREATH_PERIOD), //呼吸灯周期
.BRIGHT_LEVEL(BRIGHT_LEVEL) //亮度等级
)
u_breath_led_inst (
.sys_clk(sys_clk), //系统时钟
.sys_rst_n(sys_rst_n), //系统复位,低电平有效
.duty_cycle(duty_cycle) //PWM占空比
);
//例化PWM发生模块
pwm_generate u_pwm_generate_inst (
.sys_clk(sys_clk), //系统时钟
.sys_rst_n(sys_rst_n), //系统复位,低电平有效
.period(PWM_PERIOD), //PWM周期
.duty_cycle(duty_cycle), //PWM占空比
.pwm_out(led) //PWM,用于控制LED
);
endmodule
顶层模块仿真激励代码非常简单,只需要产生激励时钟即可,代码如下:
`timescale 1ns / 1ns //仿真单位/仿真精度
module tb_top_breath_led();
reg sys_clk; //时钟
reg sys_rst_n; //复位
wire led; //LED
initial begin
sys_clk = 1'b0;
sys_rst_n = 1'b0;
#200
sys_rst_n = 1'b1;
end
//产生时钟
always #10 sys_clk = ~sys_clk;
top_breath_led #(
.PWM_PERIOD(10), //pwm信号的周期
.BREATH_PERIOD(1000), //呼吸灯周期
.BRIGHT_LEVEL(10) //亮度等级
)
u_tb_top_breath_led_inst (
.sys_clk(sys_clk), //系统时钟
.sys_rst_n(sys_rst_n), //系统复位,低电平有效
.led(led) //LED
);
endmodule
管脚分配如下:
XDC 约束语句如下:
#时序约束
create_clock -period 20.000 -name sys_clk [get_ports sys_clk]
#IO 管脚约束
set_property -dict {PACKAGE_PIN R4 IOSTANDARD LVCMOS15} [get_ports sys_clk]
set_property -dict {PACKAGE_PIN U7 IOSTANDARD LVCMOS15} [get_ports sys_rst_n]
set_property -dict {PACKAGE_PIN V9 IOSTANDARD LVCMOS15} [get_ports led]