PotatoPie 2.1/3.0 教程(2) —— 实验1  FPGA实现LED流水灯和呼吸灯-Anlogic-安路社区-FPGA CPLD-ChipDebug

PotatoPie 2.1/3.0 教程(2) —— 实验1 FPGA实现LED流水灯和呼吸灯

demo1演示的是流水灯和呼吸灯

demo1 实验效果如下

对PotatoPie V3.0红色灯是流水灯,彩色RGB灯是呼吸,对于V2.1版本是三个红灯是流水灯,第四个是呼吸灯。

以下视频是V3.0的呼吸灯效果:

 

1.FPGA实现流水灯

1.1 流水灯原理

流水灯的原理万变不离其宗,都是利用移位寄存器或者计数器隔一段时间改变一下LED灯的状态,需要注意延时,太快的状态变化人眼观察不到。在本设计中用内部OSC振荡器来产生时钟生成延时。

1.2 FPGA实流水灯的源码解析

我们先看看chatGPT给出的FPGA流水灯代码及解释吧。

d2b5ca33bd111524

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
上面AI生成这段代码仿真是没有问题的,但上板有两个问题:1. 需要提供时钟 2. 需要对时钟分频进行延时。

我们提供的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;

d2b5ca33bd123206

完整模块代码

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 指示输出状态 。 

20231220151914791-image

实现原理如上图所示, 脉冲信号的周期为 T, 高电平脉冲宽度为 t, 占空比为 t/T。 为了实现 PWM
脉宽调制, 我们需要保持周期 T 不变, 调整高电平脉宽 t 的时间, 从而改变占空比。
t = 0 时, 占空比为 0%, 因为我们的 LED 硬件为低电平点亮, 所以为最亮的状态。
t = T 时, 占空比为 100%LED 灯为最暗(熄灭) 的状态。
结合呼吸灯的原理, 整个呼吸的周期为最亮→最暗→最亮的时间, 即
t 的值的变化: 0T0
渐变化。

如果呼吸灯设计要求呼吸的周期为 2s, 也就是说 LED 灯从最亮的状态开始, 第一秒时间内逐渐变暗,
第二秒的时间内再逐渐变亮, 依次进行。

2.2 FPGA实现流呼吸灯的代码解析

还是先看下chatGTP提供的呼吸灯verilog代码及解释

d2b5ca33bd112801

这个代码只是给了个流程,一样是有问题的,不过他给的注释思路不错。看完他的咱再看我们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值的更新。

请注意,以上代码仅供参考,您可能需要根据实际情况和需求进行适当的修改。

请登录后发表评论