FPGA模块总结

8 篇文章 0 订阅
5 篇文章 0 订阅

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 pedge = a & (!b);
    assign nedge = (!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	

在这里插入图片描述
…边沿D触发器

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;

//正常工程中,都会有锁相环,可以使用锁相环的锁定状态locked作为复位信号

	localparam COUNT = xxxxx;//复位延迟时间
	reg	[15:0]count_ctrl;	//这里也可以先定义count_ctrl,再initial count_ctrl = 0;两种写法都是属于上电初始化
	always @(posedge clk )begin	
	    if(pll_lock==1'b0)begin
	        count_ctrl <= 'd0;
        end	
		else 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;

12、三态门

固定写法:
在这里插入图片描述

 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、外设输出的数据。

ADC 模数转换器
在这里插入图片描述

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

  • 8
    点赞
  • 74
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值