2024年寒假练-基于小脚丫FPGA实现多种LED显示效果
该项目使用了WebIDE环境、Verilog语言,实现了多种LED显示效果的设计,它的主要功能为:在小脚丫FPGA核心板上,利用8个单色LED实现不同的LED显示效果,通过4个轻触按键控制不同的LED显示模式,通过4个拨动开关控制显示周期,并在数码管上显示出相应的模式和周期。
标签
FPGA
小脚丫
数字逻辑
LED
2024寒假在家一起练
冰火蓝红
更新2024-03-29
64

1 任务要求

在小脚丫FPGA核心板上,利用8个单色LED实现不同的LED显示效果

(1)使用轻触开关K1~K4切换LED的显示模式:

  • K1:循环心跳灯 - 8个LED轮流心跳,每一个LED心跳2个周期(半个周期亮、半个周期灭、半个周期亮、半个周期灭)
  • K2:呼吸灯 - 8个LED轮流呼吸,每一个LED呼吸2个周期(半个周期从灭到亮、半个周期从亮到灭、半个周期从灭到亮、半个周期从亮到灭)
  • K3:带渐灭功能的流水灯 - 8个LED构成流动显示的效果,且尾部的灯亮度逐渐变暗
  • K4:自定义模式 - 一种不同的能够让8颗LED点亮的模式。

(2)使用拨动开关控制显示的周期:

  • SW1:1秒
  • SW2:2秒
  • SW3:3秒
  • SW4:4秒

(3)在数码管上通过数值显示出相应的模式和周期:

  • 第一个数码管显示LED的显示模式
  • 第二个数码管显示LED的显示周期

2 设计思路

要完成上述任务,我们可以将总体设计大致划分为以下几项内容,再逐步完成:

(1)将拨动开关的状态转换为显示的周期;

(2)分别设计四个模块,以显示的周期为输入,8个LED信号为输出,实现四种不同的显示效果;

(3)将按键的按动情况转换为显示的模式;

(4)根据显示的模式,选择相应模块输出的LED信号作为LED的最终输出;

(5)设计数码管显示模块,以要显示的数字为输入,数码管信号为输出;

(6)将显示周期和显示模式分别传入两个数码管显示模块,实现两个数码管的显示。

3 代码架构

4 GPT生成模块演示

这里以较为简单的循环心跳灯模块为例,演示GPT生成模块的过程和效果。我以下述方式分别向谷歌的Gemini、基于GPT4的必应Copilot和Github Copilot提出要求:

请帮我编写一个Verilog模块,要求实现8个LED轮流心跳,每一个LED心跳2个周期(半个周期亮、半个周期灭、半个周期亮、半个周期灭),模块的输入输出信号如下所示:
module heartbeat_led(
input clk, // 系统时钟,12MHz
input rst, // 复位信号,低电平有效
input [2:0] period, // LED显示周期,1、2、3、4分别代表1s、2s、3s、4s
output reg [7:0] led // 8个LED,低电平有效
);

(1)Gemini生成的模块

module heartbeat_led(
input clk, // 系统时钟,12MHz
input rst, // 复位信号,低电平有效
input [2:0] period, // LED显示周期,1、2、3、4分别代表1s、2s、3s、4s
output reg [7:0] led // 8个LED,低电平有效
);
reg [7:0] led_cnt; // LED计数器
reg [2:0] led_state; // LED状态
reg [2:0] led_period; // LED周期计数器

always @(posedge clk or negedge rst) begin
if (!rst) begin
led_cnt <= 8'b0;
led_state <= 3'b0;
led_period <= period;
end else begin
if (led_cnt == 8'd255) begin
led_cnt <= 8'b0;
led_state <= led_state + 1;
end else begin
led_cnt <= led_cnt + 1;
end

if (led_period == 3'b0) begin
led_period <= period;
end else begin
led_period <= led_period - 1;
end
end
end

always @(posedge clk) begin
case (led_state)
3'b0: led <= ~8'b1111_1110;
3'b1: led <= ~8'b1111_1101;
3'b2: led <= ~8'b1111_1011;
3'b3: led <= ~8'b1111_0111;
3'b4: led <= ~8'b1110_1111;
3'b5: led <= ~8'b1101_1111;
3'b6: led <= ~8'b1011_1111;
3'b7: led <= ~8'b0111_1111;
default: led <= ~8'b1111_1111;
endcase
end
endmodule

我们只需要稍加分析即可发现,Gemini生成的模块未能正确使用输入的period参数,导致LED的状态变化和period完全无关,且这段代码实现的是流水灯效果,无法达到每一个LED亮灭两次的要求。因此,该程序几乎没有任何参考价值。

(2)必应Copilot生成的模块

module heartbeat_led(
input clk, // 系统时钟,12MHz
input rst, // 复位信号,低电平有效
input [2:0] period, // LED显示周期,1、2、3、4分别代表1s、2s、3s、4s
output reg [7:0] led // 8个LED,低电平有效
);
reg [2:0] counter;
reg [2:0] half_period;
reg [7:0] led_pattern;
always @(posedge clk or posedge rst) begin
if (rst) begin
counter <= 0;
half_period <= 0;
led_pattern <= 8'b00000000;
end else begin
if (counter == 0) begin
half_period <= period;
led_pattern <= led_pattern << 1;
led_pattern[0] <= 1;
end
if (half_period > 0) begin
half_period <= half_period - 1;
end else begin
counter <= counter + 1;
end
end
end
assign led = ~led_pattern;
endmodule

经过分析我们可以发现,该模块同样未能正确使用period参数,实现的也是类似流水灯的效果,也几乎没有参考价值。

(3)Github Copilot生成的模块

module heartbeat_led(
input clk, // 系统时钟,12MHz
input rst, // 复位信号,低电平有效
input [2:0] period, // LED显示周期,1、2、3、4分别代表1s、2s、3s、4s
output reg [7:0] led // 8个LED,低电平有效
);
reg [31:0] cnt;
reg [2:0] state;
reg [2:0] led_state;
always @(posedge clk or negedge rst) begin
if (~rst) begin
cnt <= 32'd0;
state <= 3'd0;
led_state <= 3'd0;
led <= 8'b11111111;
end else begin
cnt <= cnt + 32'd1;
if (cnt >= 12_000_000 * period) begin
cnt <= 32'd0;
state <= state + 3'd1;
if (state == 3'd7) begin
state <= 3'd0;
end
end
if (state == led_state) begin
led[led_state] <= ~led[led_state];
if (cnt >= 12_000_000 * period / 2) begin
led_state <= led_state + 3'd1;
if (led_state == 3'd7) begin
led_state <= 3'd0;
end
end
end else begin
led[led_state] <= 1'b1;
end
end
end
endmodule

与前两个GPT不同,专门用于编写程序的Github Copilot正确地根据period参数对系统时钟进行分频,但其编写模块的时序逻辑存在问题,未能成功实现循环心跳灯的效果。由于该模块将对系统时钟进行分频和根据计数器的状态对LED进行控制全部放在一个always块中,因此我们难以轻易弄清该模块的时序逻辑。然而,循环心跳灯的时序逻辑实际上很简单,所以我认为Github Copilot生成的模块在可读性和简洁性方面也存在不足之处,直接在该模块上的基础上进行修改可能不是一个好的选择。


除了上述例子,我还尝试使用这三种GPT生成其他模块。结果,它们不仅不会使用PWM技术调节LED的亮度,还出现了使用for循环但不预先定义循环变量这样的语法错误。由此我们可以看出,或许是因为训练数据较少,现在常见的GPT大模型都不是很擅长编写Verilog程序,无法达到它们编写Python程序和C程序那样的优秀水平。目前,在GPT生成程序的基础上进行修改的难度和工作量可能比自己重头开始写程序还要高。

5 主要代码片段及说明

1.根据拨码开关设置显示周期

reg [2:0] period;   // LED显示周期
always @(*) begin
case(sw)
4'b0001: period = 3'd1;
4'b0010: period = 3'd2;
4'b0100: period = 3'd3;
4'b1000: period = 3'd4;
default: period = 3'd1;
endcase
end

2.呼吸灯模块

呼吸灯是指灯光的亮度不断在亮和灭之间逐渐变化,就像人在呼吸一样。可见,要实现呼吸灯效果,有两个关键点:一是要使灯光的亮度能够介于亮和灭之间,二是要使灯光的亮度可以自动地逐渐变化。针对第一点,我们可以使用形如方波的数字信号来控制LED灯,通过调节控制信号的占空比来调节灯光的亮度。针对第二点,我们需要让控制信号的占空比能够自动地随时间从0逐渐变化到100%,再从100%逐渐变化到0。我们可以使用两个计数器来实现这一点:计数器1随系统时钟同步计数,计数器2随计数器1的周期同步计数(计数器1计满时,计数器2加1),计数范围和计数器1相同,通过比较计数器1和计数器2的值,即可产生占空比能够自动变化的信号,如下图所示。

占空比自动变化的信号的产生原理

根据上述原理,编写出的程序如下所示。

reg [12:0] cnt1;	// 计数器1
reg [12:0] cnt2; // 计数器2
reg flag; // 呼吸灯变亮和变暗的标志位
reg [3:0] state; // 进行到第几个周期

always@(posedge clk or negedge rst) begin
if(!rst) begin
cnt1 <= 13'd0;
cnt2 <= 13'd0;
flag <= 1'b0;
state <= 4'd0;
end
else if(cnt1 >= CNT_NUM - 1'd1) begin
cnt1 <= 1'b0;
if(!flag) begin // 当标志位为0时计数器2递增计数,表示呼吸灯效果由暗变亮
if(cnt2 >= CNT_NUM - 1'd1) // 计数器2计满时,表示亮度已最大,标志位变高,之后计数器2开始递减
flag <= 1'b1;
else
cnt2 <= cnt2 + 1'b1;
end
else begin // 当标志位为1时计数器2递减计数
if(cnt2 <= 0) begin // 计数器2计到0,表示亮度已最小,标志位变低,之后计数器2开始递增
flag <= 1'b0;
if(state >= 4'd15)
state <= 4'd0;
else
state <= state + 1'b1;
end
else
cnt2 <= cnt2 - 1'b1;
end
end
else
cnt1 <= cnt1 + 1'b1;
end

integer i, j;
always @(*) begin
for (i = 0; i < 8; i = i + 1) begin
if (state == 4'd0 + i * 2 || state == 4'd1 + i * 2) begin
// 比较计数器1和计数器2的值,产生占空比能够自动变化的信号
led[i] = (cnt1 < cnt2) ? 1'b0 : 1'b1;
for (j = 0; j < 8; j = j + 1) begin
if(j != i)
led[j] = 1'b1; // 其余灯熄灭
end
end
end
end

呼吸灯周期的设置可以通过调整计数器的计数范围来实现:假设计数器可以计数的数量为CNT_NUM,计数器时钟频率为fc,则呼吸灯的周期(从暗到亮,再从亮到暗)为CNT_NUM*CNT_NUM/fc。我们可以预先根据所需要设置的周期计算出相应的CNT_NUM,再由程序根据输入的显示周期选择相应的CNT_NUM,即可实现周期的设置。这部分程序如下所示。

reg [12:0] CNT_NUM;	// 计数器的计数数量
always @(*) begin
case(period)
3'd1: CNT_NUM = 13'd2400; // period = (2400^2)*2/12M ~= 1s,由暗到亮0.5s,由亮到暗0.5s
3'd2: CNT_NUM = 13'd3464; // period = (3464^2)*2/12M ~= 2s,由暗到亮1s,由亮到暗1s
3'd3: CNT_NUM = 13'd4243; // period = (4243^2)*2/12M ~= 3s,由暗到亮1.5s,由亮到暗1.5s
3'd4: CNT_NUM = 13'd4899; // period = (4899^2)*2/12M ~= 4s,由暗到亮2s,由亮到暗2s
default: CNT_NUM = 13'd2400;
endcase
end

3.带渐灭功能的流水灯模块

要实现流水灯的渐灭效果,本质上是在最亮的LED移动一次时,使之前已被点亮的LED的亮度降低一级,直到所有LED的亮度均降至熄灭。因此,我们首先需要产生多种亮度级别的LED信号。为了使第8个LED灯被点亮时第1个LED灯亮度达到最低级别,我在亮和灭之间设置了7种亮度。这部分程序如下所示。

reg [9:0] cnt_light;
always @(posedge clk or negedge rst) begin
if(!rst)
cnt_light <= 1'd0;
else if(cnt_light >= 10'd1023)
cnt_light <= 1'd0;
else
cnt_light <= cnt_light + 1'd1;
end

// 在亮和灭之间7种亮度的LED
reg [6:0] led_midlight;
always @(*) begin
if(cnt_light >= 10'd1015)
led_midlight = 7'b000_0000;
else if(cnt_light >= 10'd1000 && cnt_light < 10'd1015)
led_midlight = 7'b000_0001;
else if(cnt_light >= 10'd980 && cnt_light < 10'd1000)
led_midlight = 7'b000_0011;
else if(cnt_light >= 10'd950 && cnt_light < 10'd980)
led_midlight = 7'b000_0111;
else if(cnt_light >= 10'd900 && cnt_light < 10'd950)
led_midlight = 7'b000_1111;
else if(cnt_light >= 10'd830 && cnt_light < 10'd900)
led_midlight = 7'b001_1111;
else if(cnt_light >= 10'd750 && cnt_light < 10'd830)
led_midlight = 7'b011_1111;
else
led_midlight = 7'b111_1111;
end

首先,我设计了一个在0到1023间循环计数的计数器。然后,我通过控制计数值在不同范围时,不同亮度级别的LED的明灭,使亮度级别低的LED在计数器的每个计数周期内仅点亮很短的时间,而亮度级别高的LED在计数器的每个计数周期内点亮较长时间。比如,led_midlight[0]是亮度级别最低的LED信号,它仅在计数值在1015到1023之间时才点亮。

在产生了多种亮度级别的LED信号后,我们就可以根据设置的显示的周期控制LED进行显示。这部分程序如下所示。

reg [21:0] factor;	// 分频系数
always @(*) begin
case(period)
3'd1: factor = 22'd750000;
3'd2: factor = 22'd1500000;
3'd3: factor = 22'd2250000;
3'd4: factor = 22'd3000000;
default: factor = 22'd750000;
endcase
end

// 对时钟进行分频
reg clk_led;
reg [21:0] cnt;
always @(posedge clk or negedge rst) begin
if(!rst) begin
cnt <= 1'b0;
clk_led <= 1'b0;
end
else if(cnt >= (factor >> 1) - 1) begin
cnt <= 1'b0;
clk_led <= ~clk_led;
end
else
cnt <= cnt + 1'b1;
end

reg [3:0] state; // LED显示阶段
always @(posedge clk_led or negedge rst) begin
if(!rst)
state <= 1'b0;
else if(state >= 4'd15)
state <= 1'b0;
else
state <= state + 1'b1;
end

这段程序首先根据输入的显示周期设置分频系数,然后参照分频系数对系统时钟进行分频,再通过分频得到的时钟触发LED显示阶段计数器进行累加。

最后,只需根据当前所处的显示阶段控制8个LED显示所需的亮度级别即可实现流水灯的渐灭效果。这部分程序如下所示。

always @(*) begin
case(state)
4'd0: led = 8'b1111_1111;
4'd1: begin
led[7:1] = 7'b1111_111;
led[0] = 1'b0;
end
4'd2: begin
led[7:2] = 6'b1111_11;
led[1] = 1'b0;
led[0] = led_midlight[6];
end
4'd3: begin
led[7:3] = 5'b1111_1;
led[2] = 1'b0;
led[1:0] = led_midlight[6:5];
end
4'd4: begin
led[7:4] = 4'b1111;
led[3] = 1'b0;
led[2:0] = led_midlight[6:4];
end
4'd5: begin
led[7:5] = 3'b111;
led[4] = 1'b0;
led[3:0] = led_midlight[6:3];
end
4'd6: begin
led[7:6] = 2'b11;
led[5] = 1'b0;
led[4:0] = led_midlight[6:2];
end
4'd7: begin
led[7] = 1'b1;
led[6] = 1'b0;
led[5:0] = led_midlight[6:1];
end
4'd8: begin
led[7] = 1'b0;
led = led_midlight;
end
4'd9: begin
led[7:1] = led_midlight;
led[0] = 1'b1;
end
4'd10: begin
led[7:2] = led_midlight[5:0];
led[1:0] = 2'b11;
end
4'd11: begin
led[7:3] = led_midlight[4:0];
led[2:0] = 3'b111;
end
4'd12: begin
led[7:4] = led_midlight[3:0];
led[3:0] = 4'b1111;
end
4'd13: begin
led[7:5] = led_midlight[2:0];
led[4:0] = 5'b1_1111;
end
4'd14: begin
led[7:6] = led_midlight[1:0];
led[5:0] = 6'b11_1111;
end
4'd15: begin
led[7] = led_midlight[0];
led[6:0] = 7'b111_1111;
end
default: led = 8'b1111_1111;
endcase
end

循环心跳灯和自定义的对称流水灯与该部分的实现方法类似,且不需要产生多种亮度级别的LED信号,故不再进行介绍。

4.根据按下的按键切换显示模式

reg [3:0] mode; // LED显示模式
always @(posedge clk) begin
// 按下按键切换模式
case(key)
4'b1110: mode <= 4'd1; // K1
4'b1101: mode <= 4'd2; // K2
4'b1011: mode <= 4'd3; // K3
4'b0111: mode <= 4'd4; // K4
default: mode <= mode;
endcase
end

wire [7:0] led1;
wire [7:0] led2;
wire [7:0] led3;
wire [7:0] led4;
wire rst;
assign rst = & key[3:0]; // 任何按键按下时,都对显示模块进行复位
heartbeat_led heartbeat_led(clk, rst, period, led1); // 心跳灯
breath_led breath_led(clk, rst, period, led2); // 呼吸灯
fade_led fade_led(clk, rst, period, led3); // 带渐灭功能的流水灯
flow_led flow_led(clk, rst, period, led4); // 流水灯

always @(*) begin
case(mode)
4'd1: led = led1;
4'd2: led = led2;
4'd3: led = led3;
4'd4: led = led4;
default: led = 8'b1111_1111;
endcase
end

这段程序不断扫描四个按键的状态,当四个按键中有且仅有一个按键按下时,就将LED显示模式设置为该按键的对应的模式。随后,程序调用了实现四种LED显示效果的模块,再根据LED显示模式选择相应模块输出的LED信号作为LED的最终输出。值得注意的是,我将四个按键按位与的结果作为四个LED显示模块的复位信号,可以实现当按下任意按键切换显示模式后,显示模块都进行复位,从而重新从第一个LED灯开始显示。

5.数码管显示模块

module segment(
input [3:0] num, // 要显示的数字
output reg [8:0] seg // MSB~LSB = a,b,c,d,e,f,g,dp,共阴极端
);
always @(*) begin
case(num)
4'b0000: seg = 9'b111111000; // 0
4'b0001: seg = 9'b011000000; // 1
4'b0010: seg = 9'b110110100; // 2
4'b0011: seg = 9'b111100100; // 3
4'b0100: seg = 9'b011001100; // 4
4'b0101: seg = 9'b101101100; // 5
4'b0110: seg = 9'b101111100; // 6
4'b0111: seg = 9'b111000000; // 7
4'b1000: seg = 9'b111111100; // 8
4'b1001: seg = 9'b111101100; // 9
4'b1010: seg = 9'b111011100; // A
4'b1011: seg = 9'b001111100; // b
4'b1100: seg = 9'b100111000; // c
4'b1101: seg = 9'b011110100; // d
4'b1110: seg = 9'b100111100; // E
4'b1111: seg = 9'b100011100; // F
endcase
end
endmodule

这段程序是小脚丫FPGA七段数码管的通用显示模块。输入num是一个4位的二进制数,代表要显示的数字或字母,包括0~9和a~f。输出seg是一个9位的二进制数,控制七段数码管的每一段和小数点是否点亮,以及共阴极端的状态。程序使用case语句根据输入的num确定输出的seg,从而控制数码管显示出正确的内容。

6 FPGA资源使用情况


附件下载
led.zip
设计文件及jed文件
团队介绍
四川大学 史斯予
团队成员
冰火蓝红
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号