人生状态机 · 2022年03月14日

[走近FPGA]之点亮LED

注:本文由不愿透露姓名的

@Bulingxx

同学撰写。以下为正文。

在上一篇文章中通过数码管的显示进行介绍了组合逻辑模块化设计如何在FPGA开发板上的实现,其文章链接如下:
[走近FPGA]之组合逻辑进阶——模块化设计
本文的所有实例都使用硬木课堂Xilinx Aritx 7 FPGA板实现,且附有上板演示视频,该开发板的链接如下:
https://link.zhihu.com/?target=https://item.taobao.com/item.htm?id=612427051465
在之前的文章中,我们已经提到过数字逻辑电路分为组合逻辑电路和时序逻辑电路,并对组合逻辑电路的模块化设计进行了介绍。从这篇文章开始,我们开始介绍并分享一些涉及时序逻辑电路的实例,希望能够帮助大家理解如何在FPGA中实现时序逻辑电路。与组合逻辑电路相比,时序逻辑电路需要时钟的参与,电路中会有存储器件的参与,时序逻辑电路的输出不仅取决于某一时刻的输入,也受此时刻电路状态的影响。在本篇文章中,我们通过两个实例介绍如何点亮LED灯实现流水灯来讲解时序逻辑电路。

如何点亮LED灯

FPGA上的LED灯本质上为发光二极管,只要在其两端加以合适的正向电压即可将其导通点亮,本文所用开发板的LED灯为共阴极并联接地,其实物图和电路图如下图所示。因而若要点亮某一或若干LED灯,只需要将其正极接入高电平,也即将其置1,就可以点亮该LED。没有约束的LED灯一般会默认熄灭,也可以人为地将其点亮或是熄灭。

LED灯

开发板上LED原理图

点亮LED实现流水灯设计实例1

本文实现流水灯的第一个实现效果为:拨码开关SW1和SW0作为数据输入,实现控制效果,输入00、01、10,分别对应左移、右移和依次点亮并依次熄灭效果;拨码开关SW11作为低有效的复位开关,当置为0时,复位,LED灯全部熄灭。

实现时序逻辑电路,首先需要理解状态机的原理,在数字电路课程中已经学过,状态机主要分为两种,Moore状态机和Mealy状态机。Moore状态机的输出只和状态有关而与输入无关,Mealy状态机输出不仅和状态有关而且和输入有关系。输出是否与输入有关即为两种状态机根本的区别。要实现同样的功能,Moore状态机的状态要比Mealy状态机多一些,因为Moore状态机输出只与状态有关,比如同样要检测11序列,Moore状态机需要三个状态,而Mealy状态机只需要两个状态。从时序上来说, Mealy状态机的输出可能在输入改变下直接改变,其反应速度较快,但这样可能也会导致时序问题,而Moore状态机的输出总是在时钟沿之后才会发生变化。

Moore状态机结构图

Mealy状态机结构图

从电路结构上来说,状态机可以分为次态转移部分、次态生成部分以及寄存器输出三部分。通过状态机,可以实现任何数字逻辑系统,这里我们实现的流水灯可以看作是一种状态直接当输出的状态机。

实例1框图

我们实现的流水灯电路图如上所示。从电路图中,可以清楚的看出,时序逻辑电路涉及到时钟的参与,某一时刻输出不仅与这时的输入有关,也与上一时刻的输出相关。对比状态机,除了时钟分频模块,我们实现的流水灯电路可大致分为寄存器模块,下一时刻的寄存器输入和寄存器输出的组合逻辑模块。

时钟分频模块的作用在于通过将FPGA开发板上的晶振进行分频后,获得所需要的频率,例如,本实验所用开发板的晶振频率为50MHz,在此频率下的流水灯闪烁的非常非常快,根本看不出变化的过程,所以需要将其分频到合适的频率下进行实验,在这里,我们将其降到1Hz,这样,每过1秒,更新一次寄存器中的数据,LED输出也即变化一次。寄存器模块的作用在于存储数据,在使能信号(在本实验中,默认使能信号为1)和时钟的作用下更新数据,也是更新状态,将次态迁移至现态,每次时钟的上升沿到来时,寄存器中的数据就会更新,一直保持到下一次时钟上升沿的到来为止。下一时刻寄存器输入模块,是纯组合逻辑电路,对应状态机的次态生成部分,在这一模块中,依据所要实现的效果,弄清输入输出关系设计电路即可,在这里,控制输入控制复用器和解复用器来进行模式选择,这一时刻的输出作为数据输入,经过处理得到的输出结果就是下一时刻的输入。最后,是输出模块,将对应数据进行处理后输出即可。

我们实现的流水灯电路,于状态机而言,实现了状态的生成和转移,在输出部分,直接将状态进行输出得到了结果。本质上,无论时序逻辑电路复杂与否,万法归宗,都可以采用状态机的概念来实现。同样的,再复杂的系统最终都应该落实到基本的数字逻辑设计的概念上来实现。

如下是时钟分频模块的RTL代码,通过计数器和寄存器得到合适的频率。

module clk_pwm (
  input clk_i,
  input rst_n,
  output clk_o 
);

reg  [24:0] cnt;
wire [24:0] cnt_nxt = (cnt == 25'd24999999) ? 25'd0 : cnt + 1'b1;

always @ (posedge clk_i or negedge rst_n) begin
  if (~rst_n) cnt <= 25'b0;
  else cnt <= cnt_nxt;
end

reg clk;

always @ (posedge clk_i or negedge rst_n) begin
  if (~rst_n) clk <= 1'b0;
  else if (cnt == 25'd24999999) clk <= ~clk;
end

assign clk_o = clk;

endmodule

下面是状态机实现部分的代码。

module generator (
  input clk_i,
  input rst_n,
  input [1:0] sw, 
  output [3:0] mode
);

reg [3:0] mode_reg;
reg [3:0] nxt_mode;

always @ (*) begin//次态生成逻辑
  if (sw == 2'b00) begin
    if (mode_reg == 4'b0 || mode_reg == 4'b1000) nxt_mode = 4'b0001;
    else nxt_mode = mode_reg << 1;
  end
  else if (sw == 2'b01) begin
    if (mode_reg == 4'b0 || mode_reg == 4'b0001) nxt_mode = 4'b1000;
    else nxt_mode = mode_reg >> 1;
  end
  else begin
    nxt_mode[3] = ~mode_reg[0]; 
    nxt_mode[2:0] = mode_reg >> 1;
  end
end

always @ (posedge clk_i or negedge rst_n) begin//次态迁移至现态
  if (~rst_n) mode_reg <= 4'b0;
  else mode_reg <= nxt_mode;
end

assign mode = mode_reg;//输出模块

endmodule

下面进行顶层文件的编写,将各个功能模块例化并进行正确的连接即可。

module WaterLight(
  input clk_i,
  input rst_n,
  input [1:0] sw,
  output [3:0] led
);

wire clk;

clk_pwm pwm (
   .clk_i       (clk_i),
   .rst_n       (rst_n),
   .clk_o       (clk)
);

generator gene (
   .clk_i       (clk),
   .rst_n      (rst_n),
   .sw        (sw),
   .mode      (led)
);

endmodule

借助Vivado的RTL分析功能得到对应RTL代码的电路原理图,如下图所示,能直观地看到两个子模块及其连接方式。

实例1的RTL分析电路原理图

接下来是对设计进行功能仿真,下面给出了实例1的testbench参考文件,值得一提的是,在仿真时不要用分频后的时钟,否则会极大的增加仿真的工作量和时间。

module WaterLight_tb();
reg clk;
reg rst_n;
reg [1:0] sw;
wire [3:0] led;

WaterLight uut (
   .clk_i      (clk),
   .rst_n      (rst_n),
   .sw        (sw),
   .led        (led)
);

initial begin
  clk = 1'b0;
  rst_n = 1'b0;
  sw = 2'b0;
  #21
  rst_n = 1'b1;
  #179
  sw = 2'b01;
  #200
  sw = 2'b10;
end

always #10 begin
  clk = ~clk;
end

endmodule

下图是本例在Modelsim中的部分仿真波形,testbench中已测试了所有的输入组合,比对输入和输出结果,可见该电路实现了正确的功能。

实例1的仿真波形

完成仿真后,就可进行后续的综合、管脚约束、布局布线以及生成比特流文件,根据实例1的功能需求,本例具体的管脚约束如下表所示。

实例1管脚约束

下面为一段该实例的上板演示视频。
image.png
实例1演示视频

点亮LED实现流水灯设计实例2

与实例1不同,本例实现流水灯通过Block Rom查找表来实现流水灯功能,实现效果为:拨码开关SW1和SW0作为数据输入,实现控制效果,输入00、01、10、11,分别对应四种不同的流水效果,并通过修改Block Rom中的数据实现了新的流水效果;拨码开关SW11作为低有效的复位开关,当置为0时,复位,LED灯全部熄灭。

实例2框图

Block Rom查找表模块的实现过程为先将需要读取的数据存放在对应的地址内,再通过输入地址读取出预先存放好的数据。因此,相比较实例1所采用的硬逻辑译码,一旦确定了要实现的电路便难以更改来说,采用Block Rom进行设计的流水灯只需要加载不同的存储内容就能修改实现效果。

通过模块化的思想,可以将电路分为时钟分频、译码器、计数器和Block Rom模块。针对本例,在Block Rom内存放好数据后,需要通过译码器和计数器等模块翻译产生读取Block Rom内特定数据所需的地址信号,具体实现为译码器将输入信号翻译为对应的地址数据范围,起始地址作为计数器的加载数据,结束地址和计数器的输出通过比较器进行比较,二者相等时产生计数器加载使能信号,将起始地址加载进计数器中开始计数,计数器在时钟作用下进行计数,这样,将计数器的计数结果作为地址信号送入Block Rom模块,通过查表输出,得到LED输出数据。

首先是时钟分频模块,将时钟频率调为所需频率,与实例1相同。

module clk_pwm (
  input clk_i,
  input rst_n,
  output clk_o // 1 Hz
);

reg  [24:0] cnt;
wire [24:0] cnt_nxt = (cnt == 25'd24999999) ? 25'd0 : cnt + 1'b1;

always @ (posedge clk_i or negedge rst_n) begin
  if (~rst_n) cnt <= 25'b0;
  else cnt <= cnt_nxt;
end

reg clk;

always @ (posedge clk_i or negedge rst_n) begin
  if (~rst_n) clk <= 1'b0;
  else if (cnt == 25'd24999999) clk <= ~clk;
end

assign clk_o = clk;

endmodule

下面是译码器的参考代码,实现对两位模式控制输入信号翻译为对应的地址数据范围信号,在四种输入下对应四种起始和结尾地址。

module decoder(
  input [1:0] sw,
  output reg [4:0] start_addr,
  output reg [4:0] end_addr
);

always @ (*) begin
  if (sw == 2'b00) begin
    start_addr = 5'd0;
    end_addr   = 5'd4;
  end
  else if (sw == 2'b01) begin
    start_addr = 5'd5; 
    end_addr   = 5'd9;
  end
  else if (sw == 2'b10) begin
    start_addr = 5'd10; 
    end_addr   = 5'd17;
  end
  else begin
    start_addr = 5'd18;
    end_addr   = 5'd25;
  end
end

endmodule

接下来是计数器模块,包含了加载数据和加载使能信号,在时钟作用下循环计数地址数据,以实现循环读数循环流水。

module counter(
  input clk_i,
  input rst_n,
  input [4:0] start_addr,
  input [4:0] end_addr,
  output [4:0] addr_o
);

reg [4:0] cnt;
wire [4:0] cnt_nxt = (cnt == end_addr) ? start_addr : cnt + 1'b1;

always @ (posedge clk_i or negedge rst_n) begin
  if (~rst_n) cnt <= start_addr;
  else cnt <= cnt_nxt;
end

assign addr_o = cnt;

endmodule

Block Rom模块的代码如下,通过ADDR\_WIDTH和DATA\_WIDTH对存储器的地址宽度和数据宽度进行定义,具体的存放内容在hex文件内,通过readermemb进行读取,读取路径为hex文件所在位置(需要更改为自己电脑上的路径,注意用左斜杠而不是右斜杠),最后,在时钟的作用下对输出数据进行更新。

module block_rom # (
  parameter ADDR_WIDTH = 5,
  parameter DATA_WIDTH = 4
)(
  input clk_i,
  input  [ADDR_WIDTH - 1:0] addr,
  output reg [DATA_WIDTH - 1:0] data_o
);

(* romstyle = "block" *) reg [DATA_WIDTH-1:0] mem[2**ADDR_WIDTH - 1:0];

initial begin
  $readmemb("C:/Users/range/Desktop/WaterLight/Task2/data.hex", mem);
end

always @ (posedge clk_i) begin
  data_o <= mem[addr];
end

endmodule

这里是data.hex文件的内容。

// data.hex
0000
0001
0010
0100
1000
0000
1000
0100
0010
0001
0000
1000
1100
1110
1111
0111
0011
0001
0000
0001
0011
0111
1111
1110
1100
1000

这里是data2hex文件的内容。这里我们方便起见,只修改了输入11所对应的流水模式。与硬逻辑译码电路在控制器制造完成后,这些逻辑电路之间的连接关系就固定下来,不易改动相比,借助Block Rom只需要改变存储器中的内容便能实现改变。

// data2.hex
0000
0001
0010
0100
1000
0000
1000
0100
0010
0001
0000
1000
1100
1110
1111
0111
0011
0001
0000
0001
0011
0111
1111
0111
0011
0001

最后是顶层文件的编写,需要注意各模块间的连接。

module WaterLight(
  input clk_i,
  input rst_n,
  input [1:0] sw,
  output [3:0] led
);

wire clk;
wire [4:0] start_addr, end_addr, addr;

clk_pwm pwm (
   .clk_i       (clk_i)
  ,.rst_n       (rst_n)
  ,.clk_o       (clk)
);

decoder dec (
   .sw          (sw)
  ,.start_addr  (start_addr)
  ,.end_addr    (end_addr)
);

counter cnt (
   .clk_i       (clk)
  ,.rst_n       (rst_n)
  ,.start_addr  (start_addr)
  ,.end_addr    (end_addr)
  ,.addr_o      (addr)
);

block_rom # (
   .ADDR_WIDTH  (5)
  ,.DATA_WIDTH  (4)
) rom_inst (
   .clk_i       (clk_i)
  ,.addr        (addr)
  ,.data_o      (led)
);

endmodule

借助Vivado的RTL分析功能得到的RTL代码的电路原理图,如下图所示,能直观地看到四个子模块及其连接方式。

实例2的RTL分析电路原理图

接下来是对设计进行功能仿真,下面给出了实例2的testbench参考文件,在这里,也要注意仿真的时钟问题。

`timescale 1ns/1ps
module WaterLight_tb();

reg clk;
reg rst_n;
reg [1:0] sw;
wire [3:0] led;

WaterLight uut (
   .clk_i      (clk)
  ,.rst_n      (rst_n)
  ,.sw         (sw)
  ,.led        (led)
);

initial begin
  clk = 1'b0;
  rst_n = 1'b0;
  sw = 2'b0;
  #21
  rst_n = 1'b1;
  #179
  sw = 2'b01;
  #200
  sw = 2'b10;
end

always #10 begin
  clk = ~clk;
end

endmodule

使用上述testbench得到的部分仿真波形如下,可以看到比流水器正常流动。

实例2的仿真波形

之后进行综合、管脚约束、布局布线以及生成比特流文件。这里同样给出了本例具体的管脚约束。

实例2管脚约束

最后给出实例2的流水灯上板演示视频:
image.png
实例2演示视频

修改Block Rom中的数据得到的流水灯的上板演示视频如下:
image.png
实例2修改Block ROM的效果

至此,点亮LED实现流水灯的实例分享结束。在这篇文章中,借助两个实例向大家介绍了在FPGA中如何通过状态机和借助Block Rom实现时序逻辑电路两种方式。

这两种方式都可以实现流水灯效果,但二者的思路却有所不同。一种是硬逻辑实现状态机,另外一种是靠存储器查表实现状态变化。这是数字逻辑设计的两种不同思路。硬逻辑的典型应用是CPU中的随机逻辑控制器电路,而存储器查表方法则体现了微码控制器的思想。

通过这次实例的分享,一方面,希望能够帮助读者加深模块化设计的概念,并学习实现时序逻辑电路的方法;另一方面,也希望能够帮助大家了解CPU中随机逻辑与微码控制器的设计思想。

END

知乎:https://zhuanlan.zhihu.com/p/180533757

推荐阅读

更多内容请关注其实我是老莫的网络书场专栏
推荐阅读
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息