FPGA: verilog,有关第一个流水灯项目
示例程序写的还挺复杂的,但是静下心来分析一下,其实还挺好理解的,这里就来简单分析一下这个代码程序
原始代码
verilog
module led_flash(
input wire clk, // 系统时钟,传入时序信号
input wire rst_n ,// 系统重置信号,低电平有效
output reg [1:0] led // 输出 led 信号,2位寄存器输出
);
reg [27:0] cnt; // 0.25s 计数器,28位寄存器
wire add_cnd; // 计数器递增条件信号
wire end_cnt; // 计数器到达最大值信号
assign add_cnt = 1; // 计数器递增条件始终为真
assign end_cnt = add_cnt && cnt== 10_000_000 - 1; // 当计数器达到 10_000_000 - 1 时,end_cnt 为真
// cnt
// cnt 计数器模块
always@(posedge clk or negedge rst_n) begin
if(rst_n==1'b0) begin // 当复位信号 rst_n 为低电平时
cnt <= 'd0; // 将计数器 cnt 清零
end
else if(add_cnt) begin // 当计数器递增条件 add_cnt 为真时
if(end_cnt) // 如果计数器到达最大值 end_cnt 为真
cnt <= 'd0; // 计数器 cnt 清零
else // 否则
cnt <= cnt + 1'b1; // 计数器 cnt 加 1
end
end
always @(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin // 当复位信号 rst_n 为低电平时
led <= 2'b10;// 将 led 输出设置为 2'b10
end
else if(end_cnt)begin // 当计数器到达最大值 end_cnt 为真时
led <= {led[0], led[1]};// 将 led 输出的两位反转
end
else begin // 否则
led <= led; // 保持 led 输出不变
end
end
endmodule
怎么理解
这段代码其实看上去比较抽象,这个add_cnt是什么,这个end_cnt又是什么?这个cnt为什么要定义为一个28位的寄存器?
首先我们要明确一点,就是这里的所有代码实际上并没有经过特意设计,只是随便写出来的,所以不用纠结太多细节上的问题,但是为什么这里的代码会这么写,还请我娓娓道来。
简单的来说,在这段代码中,硬件电路部分、always块1和always块2都是并行电路,换句话说所有的verilog代码中always块都应该是并行的,中间的寄存器就像我们在多线程对齐同步时所用的中间变量,好处是所有的always块在单个时间片内执行的时候,都会取用寄存器中的原有值(前提是你使用非阻塞赋值,当然了,在这种环境下跨always块的行为你也应该使用非阻塞赋值)。这样就不需要考虑竞争的问题了。
- 定义module
VERILOG
module led_flash(
input wire clk, // 系统时钟,传入时序信号
input wire rst_n ,// 系统重置信号,低电平有效
output reg [1:0] led // 输出 led 信号,2位寄存器输出
);
我们首先定义了一个模块,名为led_flash,输入参数有clk和rst_n,输出参数是led。也就是说从外部硬件电路上会从外输入一个clk信号和rst_n信号,然后会输出一个led寄存器内容。
- 定义计时器以及累加
在这里定义了一些用于做多个always块(可以理解为多线程)之间沟通的变量。
verilog
reg [27:0] cnt; // 0.25s 计数器,28位寄存器
wire add_cnd; // 计数器递增条件信号
wire end_cnt; // 计数器到达最大值信号
assign add_cnt = 1; // 计数器递增条件始终为真
assign end_cnt = add_cnt && cnt== 10_000_000 - 1; // 当计数器达到 10_000_000 - 1 时,end_cnt 为真
这里有几个问题:
1. 为什么要定义一个28位的寄存器?定义一个整数类型的数字在这里行不通吗?
这个数字应该是纯粹人为定义的,这里其实取到26位计时器也可以。我们在xdc中定义的时钟是20ns一个tick,而我们在代码中规定时间一个来回是10_000_000 - 1,
也就是20ns * 10000000 = 0.2s,也就是说,我们只需要保证这个寄存器cnt不会在取到10_000_000 - 1这个值的时候溢出就行。
我们知道2^24 = 16777216,也就是0.2s内cnt可以取到这个值。也就是说极端一点reg [27:0] cnt;甚至可以换成[23:0],当然了,
你也不差这点内存。
2. 为什么要专门定义一个add_cnt和end_cnt?难道不能放一个寄存器在那里,然后让这个寄存器一直自增,然后判断是否等于10_000_000 - 1吗?
当然是可以这么做的,但是这个教程里并没有这么实现,我这里也可以试着写一写
verilog
module led_flash(
input wire clk, // 系统时钟,传入时序信号
input wire rst_n, // 系统重置信号,低电平有效
output reg [1:0] led // 输出 led 信号,2位寄存器输出
);
reg [27:0] cnt; // 0.25s 计数器,28位寄存器
// led 闪烁模块
always @(posedge clk or negedge rst_n) begin
if (rst_n == 1'b0) begin // 当复位信号 rst_n 为低电平时
led <= 2'b10; // 将 led 输出设置为 2'b10
cnt <= 'd0; // 将计数器 cnt 清零
end else begin // 否则
if (cnt == 10_000_000 - 1) begin // 当计数器达到最大值时
led <= {led[0], led[1]}; // 将 led 输出的两位反转
cnt <= 'd0; // 计数器 cnt 清零
end else begin // 否则
led <= led; // 保持 led 输出不变
cnt <= cnt + 1'b1; // 计数器 cnt 加 1
end
end
end
endmodule
这一段的效果和我们先前给到的是一样的,同样是在流水线中进行的处理,但是需要注意的时序和tick概念对于硬件很重要
就像游戏中一样,所有的事件都并行地发生在一个tick内,我们不希望某个时间处理时间过长导致某个时间片其余事件等待。
所有的always块共享一个时间片这是verilog中重要的概念,也是模拟电路中重要的概念。
我们当然希望整个硬件系统中的东西全部都是并行的,这样效率才高。
- 两个always块
verilog
always@(posedge clk or negedge rst_n) begin
if(rst_n==1'b0) begin // 当复位信号 rst_n 为低电平时
cnt <= 'd0; // 将计数器 cnt 清零
end
else if(add_cnt) begin // 当计数器递增条件 add_cnt 为真时
if(end_cnt) // 如果计数器到达最大值 end_cnt 为真
cnt <= 'd0; // 计数器 cnt 清零
else // 否则
cnt <= cnt + 1'b1; // 计数器 cnt 加 1
end
end
always @(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin // 当复位信号 rst_n 为低电平时
led <= 2'b10;// 将 led 输出设置为 2'b10
end
else if(end_cnt)begin // 当计数器到达最大值 end_cnt 为真时
led <= {led[0], led[1]};// 将 led 输出的两位反转
end
else begin // 否则
led <= led; // 保持 led 输出不变
end
end
endmodule
我们先前分析了,两个块相当于是独立的线程,在各自线程中做自己的事情。其中一个线程维护了一个cnt寄存器,另一个线程维护通过这个cnt寄存器的结果维护了一个led寄存器。
// 否则
led <= led; // 保持 led 输出不变
end
end
endmodule
我们先前分析了,两个块相当于是独立的线程,在各自线程中做自己的事情。其中一个线程维护了一个cnt寄存器,另一个线程维护通过这个cnt寄存器的结果维护了一个led寄存器。
并不是非要写这么复杂的,其实可以简单一点写,但是我们要尊重verilog也就是硬件电路的特点,不能强行把所有事情塞到一个线程里面去处理。