1、状态机
一段式状态机:
localparam //三种状态(state)
A = 3'b001,
B = 3'b010,
C = 3'b100;
//状态机
always@(posedge clk or negedge rst_n)
if(!rst_n)begin //异步复位
state <= A;
......
end
else begin //组合逻辑
case(state)
A:if(condition1)
state <= B;
......
B:if(condition2)
state <= C;
......
C:......;
default:......;
endcase
end
四段式状态机(明德扬):
tip:这里的四段不是指四个always代码,而是四段代码:同步时序always模块(格式化描述次态到现态)、组合逻辑的always模块(描述状态转移条件)、用assign定义转移条件(现态 && 转移条件)、设计输出信号(一个输出信号就有一个always )
//准备阶段
localparam
IDLE = 3'b001,
S1 = 3'b010,
S2 = 3'b100;
//第一段:同步时序always模块,格式化描述次态到现态
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
state_c <= IDLE;
end
else begin
state_c <= state_n;
end
end
//第二段:组合逻辑的always模块,描述状态转移条件
always @(*) begin
case(state_c)
IDLE:begin //这个小模块中现态就是IDLE
if(idle2s1) begin
state_n = S1;
end
else begin
state_n = state_c; //用次态等于现态,来使状态保持
end
end
S1:begin
if(s12s2) begin
state_n = S2;
end
else if(s12s3) begin
state_n = S3;
end
else begin
state_n = state_c;
end
end
S2:begin
if(s22s1)begin
state_n = S3;
end
else begin
state_n = state_c;
end
end
default:begin //状态机要完备
state_n = IDLE;
end
endcase
//第三段:用assign定义转移条件,注:现态 && 转移条件
assign idle2s1 = state_c == IDLE && i1&i2;
assign s12s2 = state_c == S1 && i1&i2;
assign s12s3 = state_c == S1 && !i1&i2;
assign s22s1 = state_c == S2 && !i2&i1;
//第四段:设计输出信号,一个输出信号就有一个always
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
dout1 <= 1'b0;
end
else if(state_c == S1)begin
dout1 <= 1'b1;
end
else begin
dout1 <= 1'b0;
end
end
tip:当出现沿判断的时候,常用状态机表达,因为沿只能在一个时钟周期持续电平。
如下例子(在A信号高电平时启动计数,直到B信号上升沿来到停止计数):
localparam
START = 3'b001;
F_CNT = 3'b010;
END = 3'b100;
always@(posedge clk_100M or negedge rst_n)
if(!rst_n) begin
state <= START;
cnt <= 0;
end
else begin
case(state)
START:
if(A)
state <= F_CNT;
else
cnt <= 0;
F_CNT:
if(pedge)
state <= END;
else
cnt <= cnt + 1'b1;
END:
cnt <= cnt;
default:state <= START;
endcase
end
/****************************************************/
2、计数器
在FPGA中,离不开计数器的设计。我们常用的计数器有两种,自增计数器和自减计数器。一般在计数的长度发生变化的时候用自减,其余用自增。
通用计数器:
always @ (posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt_bit <= 0;
end
else if(add_cnt_bit)begin
if(end_cnt_bit)begin
cnt_bit <= 0;
end
else begin
cnt_bit <= cnt_bit + 1'd1;
end
end
end
assign add_cnt_bit = /*condition*/
assign end_cnt_bit = add_cnt_bit && cnt_bit == num_bit-1;
自增计数器:
parameter COUNTMAX = 50;
always@(posedge clk or negedge rst_n)begin
if(rst_n)begin
count_a <= 0;
else if(count_a==COUNTMAX)begin
count_a <= 0;
end
else begin
count_a <= count_a + 1'b1;
end
end
自减计数器:
always@(posedge clk or negedge rst_n)begin
if(rst_n)begin
count_s <= COUNTMAX;
end
else if(count_s!=0)begin
count_s <= count_s - 1'b1;
end
else begin
count_s <= count_s;
end
end
下面举一个在状态机中通过自减来实现不同计数次数的并行数据转串行数据的作用。
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
count <= 5'd0;
end
//通过系统时钟clk分频的clkgg信号的上升沿作为触发计数器条件
else if(clkgg_pedge)begin
if(count!=0)
count <= count - 1'b1;
else begin
case(state_c)
IDLE : count <= 5'd4;
S1 : count <= 5'd1;
S2 : count <= 5'd15;
default: count <= 5'd4;
endcase
end
end
else begin
count <= count;
end
end
reg data_out;
reg [15:0] din;
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
data_out <= 1'b0;
end
else if(state_c==IDLE)begin
data_out <= 1'b1;
end
else if(state_c==S1)begin
if(count==1)begin
data_out <= 1'b1;
end
else begin
data_out <= 1'b0;
end
end
else if(state_c==S2)begin
data_out <= din[count]; //重点理解这行代码!!!
end
end
3、同步寄存器
此应用于1bit的,用于解决亚稳态问题——两拍(若应用15bit的就需要15个D触发器)
需要注意的时,不能消除亚稳态,但是能阻止亚稳态的传播!
写法1:
//应用于3拍以下
always@(posedge clk or negedge rst_n)
if(!rst_n)begin
d1 <= 1'b0;// 第一个寄存器
d2 <= 1'b0;// 第二个寄存器
end
else begin
d1 <= in;
d2 <= d1;
end
写法2:
//优势在于打的拍数较多时,可以简化书写
always@(posedge clk or negedge rst_n)
if(!rst_n)begin
{d2,d1} <= 2'd0;
else
{d2,d1} <= {d1,in};
写法3:
//比写法2打的拍数更多,更简洁。
//这里是打了三拍。其中data_reg[2]等价于第三拍,data_reg[1]等价于d2,data_reg[0]等价于d1。
always@(posedge clk or negedge rst_n)
if(!rst_n)begin
data_reg <= 3'b0;
end
else begin
data_reg <= {data_reg[1:0],din}; //移位寄存器
end
应用案例:在FIFO里两个不同时钟域,写使能和读使能中会跨时钟域,需要两拍,使其不会产生亚稳态
always @(posedge clk125M or negedge rst_n)
if(!rst_n)
{wr_en2,wr_en1} <= 2'd0;
else
{wr_en2,wr_en1} <= {wr_en1,wr_en};
always @(posedge clk25M or negedge rst_n)
if(!rst_n)
{rd_en2,rd_en1} <= 2'd0;
else
{rd_en2,rd_en1} <= {rd_en1,rd_en};
assign wr_en = (full==0 && rd_en2==0)?1:0;
assign rd_en = (empty==0 && wr_en2==0)?1:0;
4、边沿捕获
其实沿捕获都是基于同步寄存器打拍后产生的。其实目前有三种沿捕获,因为打拍有几种方式,沿捕获就有几种方式。下面只给出两种,剩下的一种依葫芦画瓢完全ok。
法一(常用打拍):
always@(posedge clk or negedge rst_n)
if(!rst_n)begin
a <= 1'b0;
b <= 1'b0;
end
else begin
a <= in;
b <= a;
end
assign nedge = (!a) & b;
assign pedge = a & (!b);
//快速判定沿的方法:先看高位,再看低位。10则为下降沿(由高到低),01则为上升沿(由低到高)。b为第二拍高位,a为第一拍是低位 。
tip:打两拍,使其in变为b这个时候可以同步进入这个时钟域。在这里运用组合逻辑捕获了in的上升沿pedge,但是这个pedge是存在误差的,误差小于时钟域内的一个周期。
法二(基于移位寄存器的沿捕获):
always@(posedge clk or negedge rst_n)
if(!rst_n)begin
data_reg <= 3'b0;
end
else begin
data_reg <= {data_reg[1:0],din}; //移位寄存器
end
assgin pedge = (data_reg [2:1] == 2'b01)? 1 : 0;
assgin nedge = (data_reg [2:1] == 2'b10)? 1 : 0;
//快速判定沿的方法:先看高位,再看低位。10则为下降沿(由高到低),01则为上升沿(由低到高)。在这里data_reg[2]为高位,data_reg[1]为低位 。
tip:移位寄存器的沿捕获,是等价于第一种写法的。需注意的是这里data_reg 的0位代表信号打一拍,1位代表打两拍,2位(即data_reg[2])代表打三拍的信号。
5、移位寄存器
八位的移位寄存器两种写法
//法一:移位运算符
always @ (posedge clk or negedge rst_n)
if(!rst_n)
sel_r <= 8'b0000_0001;
else if(sel_r == 8'b1000_0000)
sel_r <= 8'b0000_0001;
//左移位
else
sel_r <= sel_r << 1;
//法二:操作位移动
always @ (posedge clk or negedge rst_n)
if(!rst_n)
sel_r <= 8'b0000_0001;
//左移位
else
sel_r <= {sel_r[6::0],sel_r [7]};
tip:移位寄存器十分重要,一般用法二。用的十分灵活,目前我接触到的有:打拍,沿捕获,串转并,并转串。(详情可参考3、4、8)
6、查找表(LUT),多选1多路器
用case语句(如果是2选1多路器,用assign a =(b)?c:d)
//下面例子是组合逻辑八选一多路器(MUX8),查找表同理。
module choose81(sel_r,disp_data,data_tmp);
input [7:0] sel_r;
input[31:0] disp_data;
output[3:0] data_tmp;
reg [3:0] data_tmp;
always @ (*)
case(sel_r) //sel_r位选通道,data_tmp为数码管显示的数字
8'b0000_0001:data_tmp = disp_data[3:0];
8'b0000_0010:data_tmp = disp_data[7:4];
8'b0000_0100:data_tmp = disp_data[11:8];
8'b0000_1000:data_tmp = disp_data[15:12];
8'b0001_0000:data_tmp = disp_data[19:16];
8'b0010_0000:data_tmp = disp_data[23:20];
8'b0100_0000:data_tmp = disp_data[27:24];
8'b1000_0000:data_tmp = disp_data[31:28];
default:data_tmp = 4'b0000;
endcase
endmodule
7、分频及时钟模块
用计数器产生的分频信号不能直接用作时钟信号(这种写法称为门控时钟,由触发器直接产生的信号当做时钟信号。其信号不稳定,且到达每个其他触发器时间不同,得到的数据不准确),实际中我们所用板卡的clk或者通过pll分频的clk是经过特殊处理优化过得(全局时钟资源,有一层非常厚的铜皮,到达每个触发器大致相同,即便有差异也可以推算出来,只需要遵守相对的建立时间和保持时间即可),如果将人为分频得到的信号作为时钟会出现很多严重的问题。如图所示,下方红色线为全局时钟可知差异,上方绿色的为门控时钟,路线不确定。若是低频且驱动的触发器较少的时候功能能实现,但不推荐。
下面介绍两种分频产生时钟的写法:
【1】分频取反形式的时钟信号,不能直接作时钟信号上升沿触发。
//分频模块:50M的时钟clk分频为1Hz的时钟
always @ (posedge clk or negedge rst_n)
if(!rst_n)
cnt <= 0;
else if(cnt == 24_999_999)
cnt <= 0;
else
cnt <= cnt + 1'b1;
always @ (posedge clk or negedge rst_n)
if(!rst_n)
clk_1Hz <= 0;
else if(cnt == 24_999_999)
clk_1Hz <= ~clk_1Hz;
else
clk_1Hz <= clk_1Hz;
/*不能把他直接当成时钟用(在测试文件中可以随意用)。若要使用这个时钟,可以考虑捕获信号的沿设为沿触发,写在条件当中。如:
always @ (posedge clk or negedge rst_n)
if(!rst_n)
.....
else if(条件) //条件为clk_1Hz的沿触发
....
else
....
用这种取反方法分频,再写一个沿触发的条件过于麻烦,常用第二种方法简洁。
【2】分频计数使能信号作为条件。
/***************正确方式——分频信号改为使能方式*****************/
//分频模块,如一个50M时钟,需要分频产生一个6.25M的时钟,即八分频。
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
counter <= 1'b0;
end
else if(counter == 7)begin
counter <= 0;
end
else begin
counter <= counter + 1'b1;
end
end
assign sclk_pulse = counter == 7; //相当于分频的使能脉冲信号
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
dout <= 0;
end
else if(sclk_pulse )begin //把分频的使能作为时钟信号,避免直接把分频信号作为时钟
dout <= din;
end
end
/***************错误方式——直接把分频信号作为时钟*****************/
//分频模块
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
counter <= 1'b0;
end
else if(counter == 7)begin
counter <= 0;
end
else begin
counter <= counter + 1'b1;
end
end
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
clk1 <= 1'b0;
end
else if(counter == 7)begin
clk1 <= 1'b1;
end
else begin
clk1 <= 1'b0;
end
end
//不能直接把分频信号产生的时钟信号clk1作为时钟信号
always @(posedge clk1 or negedge rst_n)begin
if(!rst_n)begin
dout <= 0;
end
else begin
dout <= din;
end
end
8、并行数据转串行数据
串行数据与并行数据是相对的一对概念。
串行数据是传输过程中一位一位顺序传送得数据,只用很少几根通信线,串行传送的速度低,但传送的距离可以很长,因此串行适用于长距离而速度要求不高的场合(如计算机通信)。另外串行传送信息的速率需要控制,要求双方设定通信传输的波特率。
并行数据则是各数据位同时传送的数据。算数速度很快,但是要求的排线多,适用于近距离。
tip!!!:其转换过程并行数据先发高位还是先发低位是根据相应的协议来确定,如串口uart的收发就是根据先转换并行数据的高位。
假设并行的是四位的数据,可以用查找表,自减计数,移位三种方法来实现并转串。
//法一:查找表模式,计数4位,自增
always @(posedge clk or negedge rst_n)
if(!rst_n)
data_out <= 0;
else begin
case(cnt)
0: data_out <= data_in[0];
1: data_out <= data_in[1];
2: data_out <= data_in[2];
3: data_out <= data_in[3];
default:data_out <= 0;
endcase
end
//tip:对比模块1和模块7:状态机结构是case...if,并转串结构是if...case。
//法二:计数模式,count计4个数自减
always @ (posedge clk or negedge rst_n)begin
if(!rst_n)
dout <= 1'b0;
else
dout <= data[count];
end
//法三:移位模式,
data <= {data[2:0],data[3]};
dout <= data[3];
tip:串行转并行数据(这里的例子是1位串行数据,可以是多位的串行)
always @ (posedge clk or negedge rst_n)
if(!rst_n)
dout <= 3'b0; //一开始复位让dout为0
else
dout <= {dout[2:0],din} //舍弃dout的最高位,把串行数据din放入低位,三拍后dout装满din
9、积分器
reg [63:0] d;
wire [63:0] Idout;
always @(posedge clk or negedge rst_n)
if (!rst_n) begin
cnt <= 0;
tlast <= 1'b0;
end
else if (cnt == N) begin
cnt <= 0;
tlast <= 1'b1;
end
else begin
cnt <= cnt + 1'b1;
tlast <= 1'b0;
end
always @(posedge clk or negedge rst_n)
if(!rst_n)
d <= 63'd0;
else
d <= Idout;
assign Idout= d + Idin;
assign Idata = (tlast) ? I_dout : 0;
Idin是积分输入,Idout是积分的值,idata是从0积到N的值。
10、触发器
…D触发器
always @(posedge clk or negedge rst_n)
if(!rst_n) begin
sel <= 1'b0'
end
else begin
sel <= sel_a & sel_b;
end
always @(posedge clk or negedge rst_n)
if(!rst_n)
data <= 0;
else if(data _valid)
data <= data ;
else
data <= data ;
11、(内部)自动复位
//此复位的触发条件只有clk,没有rst_n,所以不能在仿真中运行会报错,只能上板调试使用。
localparam COUNT = xxxxx;//复位延迟时间
reg [15:0]count_ctrl = 0; //这里也可以先定义count_ctrl,再initial count_ctrl = 0;两种写法都是属于上电初始化
always @(posedge clk )begin
if(count_ctrl==COUNT)begin
count_ctrl <= count_ctrl;
end
else begin
count_ctrl <= count_ctrl + 1'b1;
end
end
assign sys_rst_n = (count_ctrl==COUNT)? 1:0;
//用FPGA描述上述三态门,固定写法:
inout Y, //define three-state gate Y in your module
reg A; //intermediate driver registers
wire C; //Enable signal
assign Y = (C == 1’b0) ? A : 1’bz; //C control Y on-off
module dual_port (
....
inout wire data;,
....
);
wire rd_data;;
wire wr_data;
wire wr_en;
assign rd_data = data;
assign data = wr_en? wr_data : 1'bz; //1'bz表示三态门的第三种状态:高阻
endmodule
对于双向端口而言,它包含输入端口,输出端口。首先当输出使能有效的时候,把缓存的数据赋值给总线上的输出端口,无效时拉高阻状态清空总线。当总线端口有数据时(清空总线前),数据赋值给输入(输入输出端口在一条总线上)。
需要注意的是三态门必须是管脚,不能是内部信号,这里的data必须连接管脚。
13、ADC与DAC模块分析
ADC与DAC的驱动都是用的SPI接口——其数据传输都是串行的。由于模拟信号是无法在FPGA中传输的,所以在ADC中,上游模块给了驱动模块一个通道的选择,驱动了ADC读取了其中某个通道的数据;在DAC中把数字信号传输给DAC,产生的模拟信号无法传回FPGA所以通过其他形式传出去了。
SPI——串行外设接口,主要有四个数据线(均是1bit)即数据的传输是串行的(一般为三个数据驱动外设,外设传回一个数据)。其中数据线包括:1、片选 2、时钟 3、输入外设的数据 4、外设输出的数据。
DAC 数模转换器
FPGA在驱动ADC和DAC时,由于内部是数字电路,所以FPGA内是没有模拟信号的,切记FPGA内部数据全是数字信号!
在spi接口上是串行的数据,但是ADC和DAC可以多通道并行传输数据。
14、矩阵按键扫描
如一个4*4的矩阵扫描键盘驱动,输入为(4位)行,输出为(4位)列。接口信息如图所示。
状态机思路:先初始化一个列值,判断行是否有值(!=1111),若有则进入滤波,无则这个列的值左移(右移也可以)1位再判断是行是否有值。在滤波状态下,计数20ms且行值有值(!=1111)则成功,其余同普通按键消抖。
代码:https://download.csdn.net/download/weixin_44790601/11824469