demo1演示的是流水灯和呼吸灯
demo1 实验效果如下
对PotatoPie V3.0红色灯是流水灯,彩色RGB灯是呼吸,对于V2.1版本是三个红灯是流水灯,第四个是呼吸灯。
以下视频是V3.0的呼吸灯效果:
1.FPGA实现流水灯
1.1 流水灯原理
流水灯的原理万变不离其宗,都是利用移位寄存器或者计数器隔一段时间改变一下LED灯的状态,需要注意延时,太快的状态变化人眼观察不到。在本设计中用内部OSC振荡器来产生时钟生成延时。
1.2 FPGA实流水灯的源码解析
我们先看看chatGPT给出的FPGA流水灯代码及解释吧。
module LED_Flowing;
parameter WIDTH = 4; // 定义LED数量(根据实际情况进行修改)
reg [WIDTH-1:0] leds; // 定义一个寄存器存储LED状态
reg clk;
always @(posedge clk) begin
leds <= {leds[WIDTH-2:0], leds[WIDTH-1]}; // 左移一位
end
initial begin
clk = 0;
leds = 1;
#10; // 延时10个时间单位来改变LED状态
while (1) begin
clk = ~clk; // 翻转时钟状态
#5; // 延时5个时间单位
end
end
// 添加对应的输出或LED显示逻辑
// 这里假设有一个名为led_display的模块用于显示LED状态
// 你可以根据具体平台的需要修改这部分逻辑
// 如何连接到实际的LED硬件上取决于您的平台或开发板
endmodule
我们提供的demo1源码解决了上面的问题。
代码的第一段实现了一个计数器用于延时
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt <= 25'b0;
else if(cnt == CNT_MAX)
cnt <= 25'b0;
else
cnt <= cnt + 1'b1;
第二段则实现了一个用于移位的指示标志
//cnt_flag:计数器计数满500ms标志信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_flag <= 1'b0;
else if(cnt == CNT_MAX - 1)
cnt_flag <= 1'b1;
else
cnt_flag <= 1'b0;
这里并没有直接进行分频而是采用了标志位,是一个比直接分频更好的解决方案。
第三段实现移位
//led_out_reg:led循环流水
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
led_out_reg <= 4'b0001;
else if(led_out_reg == 4'b1000 && cnt_flag == 1'b1)
led_out_reg <= 4'b0001;
else if(cnt_flag == 1'b1)
led_out_reg <= led_out_reg << 1'b1; //左移
最后一行取反,是因为LED电路是低电平点亮设计。
assign led_out = ~led_out_reg;
完整模块代码
module water_led
#(
parameter CNT_MAX = 25'd24_999_999
)
(
input wire sys_clk , //系统时钟50MHz
input wire sys_rst_n , //全局复位
output wire [3:0] led_out //输出控制led灯
);
////
//\* Parameter and Internal Signal \//
////
//reg define
reg [24:0] cnt ;
reg cnt_flag ;
reg [3:0] led_out_reg ;
////
//\* Main Code \//
////
//cnt:计数器计数500ms
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt <= 25'b0;
else if(cnt == CNT_MAX)
cnt <= 25'b0;
else
cnt <= cnt + 1'b1;
//cnt_flag:计数器计数满500ms标志信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_flag <= 1'b0;
else if(cnt == CNT_MAX - 1)
cnt_flag <= 1'b1;
else
cnt_flag <= 1'b0;
//led_out_reg:led循环流水
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
led_out_reg <= 4'b0001;
else if(led_out_reg == 4'b1000 && cnt_flag == 1'b1)
led_out_reg <= 4'b0001;
else if(cnt_flag == 1'b1)
led_out_reg <= led_out_reg << 1'b1; //左移
assign led_out = ~led_out_reg;
endmodule
2. FPGA实现呼吸灯
2.1呼吸灯原理
呼吸灯的原理实质上就是用PWM波驱动LED,并更改PWM的占空比,同样需要注意延时以适应人眼。
呼吸灯的设计较为简单, 我们使用系统时钟作为高频信号做分频处理, 调整占空比实现
PWM, 通过 LED 灯 LD 指示输出状态 。
实现原理如上图所示, 脉冲信号的周期为 T, 高电平脉冲宽度为 t, 占空比为 t/T。 为了实现 PWM
脉宽调制, 我们需要保持周期 T 不变, 调整高电平脉宽 t 的时间, 从而改变占空比。
当 t = 0 时, 占空比为 0%, 因为我们的 LED 硬件为低电平点亮, 所以为最亮的状态。
当 t = T 时, 占空比为 100%, LED 灯为最暗(熄灭) 的状态。
结合呼吸灯的原理, 整个呼吸的周期为最亮→最暗→最亮的时间, 即 t 的值的变化: 0→T→0 逐
渐变化。
如果呼吸灯设计要求呼吸的周期为 2s, 也就是说 LED 灯从最亮的状态开始, 第一秒时间内逐渐变暗,
第二秒的时间内再逐渐变亮, 依次进行。
2.2 FPGA实现流呼吸灯的代码解析
还是先看下chatGTP提供的呼吸灯verilog代码及解释
这个代码只是给了个流程,一样是有问题的,不过他给的注释思路不错。看完他的咱再看我们demo1中提供的代码。
第一部分代码分别实现us、ms和s级计数器
//cnt_1us:1us计数器
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_1us <= 6'b0;
else if(cnt_1us == CNT_1US_MAX)
cnt_1us <= 6'b0;
else
cnt_1us <= cnt_1us + 1'b1;
//cnt_1ms:1ms计数器
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_1ms <= 10'b0;
else if(cnt_1ms == CNT_1MS_MAX && cnt_1us == CNT_1US_MAX)
cnt_1ms <= 10'b0;
else if(cnt_1us == CNT_1US_MAX)
cnt_1ms <= cnt_1ms + 1'b1;
//cnt_1s:1s计数器
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_1s <= 10'b0;
else if(cnt_1s == CNT_1S_MAX && cnt_1ms == CNT_1MS_MAX
&& cnt_1us == CNT_1US_MAX)
cnt_1s <= 10'b0;
else if(cnt_1ms == CNT_1MS_MAX && cnt_1us == CNT_1US_MAX)
第二部分产生秒计数标志
//cnt_1s_en:1s计数器标志信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_1s_en <= 1'b0;
else if(cnt_1s == CNT_1S_MAX && cnt_1ms == CNT_1MS_MAX
&& cnt_1us == CNT_1US_MAX)
cnt_1s_en <= ~cnt_1s_en;
//led_out:输出信号连接到外部的led灯
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
led_out <= 1'b0;
else if((cnt_1s_en == 1'b1 && cnt_1ms < cnt_1s) ||
(cnt_1s_en == 1'b0 && cnt_1ms > cnt_1s))
led_out <= 1'b0;
else
led_out <= 1'b1;
完整模块代码
module breath_led
#(
parameter CNT_1US_MAX = 6'd49 ,
parameter CNT_1MS_MAX = 10'd999 ,
parameter CNT_1S_MAX = 10'd999
)
(
input wire sys_clk , //系统时钟50MHz
input wire sys_rst_n , //全局复位
output reg led_out //输出信号,控制led灯
);
////
//\* Parameter and Internal Signal \//
////
//reg define
reg [5:0] cnt_1us ;
reg [9:0] cnt_1ms ;
reg [9:0] cnt_1s ;
reg cnt_1s_en ;
///
//\* Main Code \//
////
//cnt_1us:1us计数器
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_1us <= 6'b0;
else if(cnt_1us == CNT_1US_MAX)
cnt_1us <= 6'b0;
else
cnt_1us <= cnt_1us + 1'b1;
//cnt_1ms:1ms计数器
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_1ms <= 10'b0;
else if(cnt_1ms == CNT_1MS_MAX && cnt_1us == CNT_1US_MAX)
cnt_1ms <= 10'b0;
else if(cnt_1us == CNT_1US_MAX)
cnt_1ms <= cnt_1ms + 1'b1;
//cnt_1s:1s计数器
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_1s <= 10'b0;
else if(cnt_1s == CNT_1S_MAX && cnt_1ms == CNT_1MS_MAX
&& cnt_1us == CNT_1US_MAX)
cnt_1s <= 10'b0;
else if(cnt_1ms == CNT_1MS_MAX && cnt_1us == CNT_1US_MAX)
cnt_1s <= cnt_1s + 1'b1;
//cnt_1s_en:1s计数器标志信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_1s_en <= 1'b0;
else if(cnt_1s == CNT_1S_MAX && cnt_1ms == CNT_1MS_MAX
&& cnt_1us == CNT_1US_MAX)
cnt_1s_en <= ~cnt_1s_en;
//led_out:输出信号连接到外部的led灯
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
led_out <= 1'b0;
else if((cnt_1s_en == 1'b1 && cnt_1ms < cnt_1s) ||
(cnt_1s_en == 1'b0 && cnt_1ms > cnt_1s))
led_out <= 1'b0;
else
led_out <= 1'b1;
endmodule
那么咱们设计中的时钟是哪里来的呢?看代码
wire sys_clk;
wire clko;
OSC_DIV u_oscdiv( .rstn(1), .stdby(0), .div(7'b000_0010), .clko(clko) );
EF2_LOGIC_BUFG BUFG_inst(
.o(sys_clk),
.i(clko)
);
是通过OSC_DIV来的,这个是内部振荡器产生的时钟,可以通过IP Generate来生成相关的IP。
EF2_LOGIC_BUFG 是芯片内的BUFG,即全局时钟BUF。
关于内部振荡器的更多例子,可以参看这个帖子 https://chipdebug.com/forum-post/52379.html
最后看一下AI给出优化和细化后的代码:
下面是进一步细化和优化的Verilog代码示例,其中封装了一个独立的LED模块,并添加了参数化的宽度和呼吸速度:
module LED_Breathing #(
parameter WIDTH = 8, // 定义PWM位宽
parameter STEP = 1, // 定义呼吸灯每个步进的大小
parameter DELAY = 5 // 定义呼吸灯的延迟周期数
) (
input wire clk, // 时钟信号
output wire [WIDTH-1:0] pwm // PWM输出
);
reg [WIDTH-1:0] pwm_reg; // PWM寄存器
reg [WIDTH-1:0] pwm_next; // 下一个PWM值
always @(posedge clk) begin
pwm_reg <= pwm_next; // 更新PWM寄存器的值
end
always_comb begin
if (pwm_reg == 0) begin
pwm_next = pwm_reg + STEP; // 呼吸灯逐渐变亮
end else if (pwm_reg == (1 << WIDTH) - 1) begin
pwm_next = pwm_reg - STEP; // 呼吸灯逐渐变暗
end else if ((pwm_reg + STEP) >= (1 << WIDTH)) begin
pwm_next = pwm_reg - STEP; // 呼吸灯逐渐变暗
end else begin
pwm_next = pwm_reg + STEP; // 呼吸灯逐渐变亮
end
end
initial begin // 添加延时来改变PWM值
pwm_reg = 0;
#DELAY;
while (1) begin
#DELAY;
end
end
endmodule
通过以上优化,代码结构更加清晰,呼吸速度和步进大小可根据参数进行调整。这段代码中,`pwm_next`在`always_comb`块中根据当前的`pwm_reg`和参数设置计算下一个PWM值。然后,在每个时钟上升沿触发时,将`pwm_next`的值复制到`pwm_reg`,实现PWM值的更新。
请注意,以上代码仅供参考,您可能需要根据实际情况和需求进行适当的修改。