【手撕代码】—Verilog实现呼吸灯
小张最近痴迷于刷题,恰好看到了这个呼吸灯手撕题,让我回想起本科学EDA的课堂作业,今天趁机来复习一下
1. 题目
实现1s内呼吸灯从暗到亮,下一秒1s呼吸灯从亮到暗,并不断循环此过程。
2. 原理剖析
不整废话,直接干货。呼吸灯的本质是PWM占空比的改变导致led灯两端的电压的不断改变。从暗到亮的过程是占空比从0%到100%的过程。从亮到暗即对应从100%到0%。
那么不懂的小伙伴可能会问,什么是PWM?PWM是脉宽调制技术(Pulse-width modulation)。通常情况下,你看到的灯是处于全亮或全灭的状态,也就对应着0和1。而如果我们定义一个采样周期内, 一半的周期处于全亮状态(状态1),一半的处于全灭状态(状态0)。其实这就是50%的PWM脉冲。那么想想看,你会看到什么呢?
其实这取决于PWM信号的频率,频率越高,对人眼来说停滞感越强。比如频率是0.1HZ,那么你会在前5s内看到灯亮,后5s内看到灯暗。对于人眼来说这个停滞时间就是5s。而如果频率是5HZ,那停滞时间就是0.1s,你会看到灯在高频率闪烁。而如果频率是50HZ,(没错,我们平常的日关灯也是50HZ,只不过这个频率不是PWM频率而是交流信号频率,但原理相同),那停滞时间就是0.01s,你会看到灯处于常亮状态,但亮度是最大亮度的一半。产生这样的原因是因为人眼的刷新率是24帧,那么人眼的图像滞留时间约为0.04s。因此只要PWM频率大于24帧,人眼看到的都是常亮状态。因此在设计呼吸灯的时候,本质上是存在一个极限周期的,要保证停滞时间大于人演的图像滞留时间,才能实现呼吸灯的效果。 当然,呼吸灯的效果也取决于呼吸时间周期的设定。但与此题无关,扯远了。。
还记得刚刚我们说的吗?50%的PWM脉冲,如果PWM频率是50HZ,我们人眼会看到灯常亮,但亮度是最大亮度的一半。核心原因是在这个时钟周期内灯两端的电压 U L U_L UL= 1 2 \frac{1}{2} 21 U d c Udc Udc。如果假如占空比是 α \alpha α,那么灯两端的电压为 U L U_L UL= α ∗ \alpha* α∗ U d c Udc Udc。如果 α \alpha α从0增加到1,就会实现呼吸灯的效果。
3. Verilog代码实现
代码相对简单,在这里就不赘述实现方法了。
代码剖解:
1s内实现占空比从0-100%,1s分为1000份,占空比精度 0.1%,而1ms分1000份代表占空比,每1ms累加0.1%。
assign state0 = (number_1us < number_1ms)?1'b1:1'b0;//占空比
number_1us 代表运行到第几个us。number_1ms代表运行到第几个ms。
这段代码number_1ms会从0ms增加到999ms,而0ms-1ms时1000个us都会输出1,即占空比100%。而 999ms-0ms中 1000个us都会输出0。因此这也就实现了每增加1ms,占空比降低0.1%
Verilog源码:
//呼吸灯
//1s从暗到亮,下一个1s从亮到暗
//1s内实现占空比从0-100%,1s分为1000份,占空比精度 0.1%
//1ms分1000份代表占空比,每1ms累加0.1%
// date :2022/05/31
module Breath_led #(
parameter [25:0] cnt_1s = 26'd50_000_000 - 1'd1,
parameter [15:0] cnt_1ms = 16'd50_000 - 1'd1,
parameter [ 5:0] cnt_1us = 6'd50 - 1'd1
)(
input wire clk,
input wire rstn,
output wire led
);
reg flag_1s,flag_1ms,flag_1us; //计数标志位,每到1s、us ms都会有一个高脉冲
reg [25:0] count_1s;
reg [15:0] count_1ms;
reg [ 5:0] count_1us;
reg led_flag; //1s状态切换标志位
reg [ 9:0] number_1us,number_1ms; //表示系统运行到第几个us,方便进行占空比比较
wire state0; //亮到暗
wire state1; //暗到亮
assign state0 = (number_1us < number_1ms)?1'b1:1'b0;//占空比 累加
assign state1 = (number_1us < number_1ms)?1'b0:1'b1;//占空比 累减
assign led = (led_flag)?state0:state1; //判断状态
//1s计数器
always @(posedge clk or negedge rstn) begin
if(!rstn)begin
flag_1s <= 1'b0;
count_1s <= 26'd0;
end else if (count_1s == cnt_1s ) begin
count_1s <= 26'd0;
flag_1s <= ~flag_1s;
end else begin
count_1s <= count_1s + 1'd1;
flag_1s <= 1'b0;
end
end
//1ms计数器
always @(posedge clk or negedge rstn) begin
if(!rstn)begin
flag_1ms <= 1'b0;
count_1ms <= 16'd0;
end else if (count_1ms == cnt_1ms ) begin
count_1ms <= 16'd0;
flag_1ms <= ~flag_1ms;
end else begin
count_1ms <= count_1ms + 1'd1;
flag_1ms <= 1'b0;
end
end
//1us计数器
always @(posedge clk or negedge rstn) begin
if(!rstn)begin
flag_1us <= 1'b0;
count_1us <= 6'd0;
end else if (count_1us == cnt_1us ) begin
count_1us <= 6'd0;
flag_1us <= ~flag_1us;
end else begin
count_1us <= count_1us + 1'd1;
flag_1us <= 1'b0;
end
end
//定位当前系统运行到第几个us
always @(posedge clk or negedge rstn) begin
if(!rstn)
number_1us <= 10'd0;
else if(number_1us == 10'd999)
number_1us <= 10'd0;
else begin
if(flag_1us)
number_1us <= number_1us + 1'b1;
else
number_1us <= number_1us;
end
end
//定位当前系统运行到第几个ms
always @(posedge clk or negedge rstn) begin
if(!rstn)
number_1ms <= 10'd0;
else if(number_1ms == 10'd999)
number_1ms <= 10'd0;
else begin
if(flag_1ms)
number_1ms <= number_1ms + 1'b1;
else
number_1ms <= number_1ms;
end
end
// 每1s切换呼吸灯状态
always @(posedge clk or negedge rstn) begin
if(!rstn)
led_flag <= 1'b1;
else begin
if(flag_1s)
led_flag <= ~led_flag;
else
led_flag <= led_flag;
end
end
endmodule
4. TestBench验证
`timescale 1ns / 1ps
module tb_Breath_led;
// Breath_led Parameters
parameter PERIOD = 20 ; //50MHz
parameter cnt_1s = 26'd50_000_000 - 1'd1;
parameter cnt_1ms = 16'd50_000 - 1'd1;
parameter cnt_1us = 6'd50 - 1'd1 ;
// Breath_led Inputs
reg clk = 0 ;
reg rstn = 0 ;
// Breath_led Outputs
wire led ;
initial
begin
forever #(PERIOD/2) clk=~clk;
end
initial
begin
rstn = 0;
#(PERIOD*2) ;
rstn = 1;
end
Breath_led #(
.cnt_1s ( cnt_1s ),
.cnt_1ms ( cnt_1ms ),
.cnt_1us ( cnt_1us ))
u_Breath_led (
.clk ( clk ),
.rstn ( rstn ),
.led ( led )
);
/*
initial
begin
$finish;
end
*/
endmodule
仿真结果: 从暗到亮过程
仿真结果: 从暗到亮过程
<i