FPGA智慧(45)变量访问的明智之道

变量访问思路概述

访问思路简述

变量的访问思路,简而言之就是“一写多读”,即如果有多个并行语句需要操作一个变量时,有且只能有一个固定的并行语句可以对变量进行写操作,而所有的并行语句都可以对变量进行读操作,因为一个变量只能有一个驱动源,如果被多个驱动源驱动,就会产生冲突。 这就好比在教室里上课一样,学生可以有很多,但是老师只能有一个,否则讲台上站着数、理、化、史、地、生一干老师,同时开讲,那学生们该听谁的呢?例如如下写变量的代码会造成变量访问冲突,从而导致编译出错:


-- VHDL example	
process(a)
begin
	c <= a;
end
process(b)
begin
	c <= b; -- multi-driver
end

// Verilog example
always@(a)
	c = a;
always@(b)
	c = b; // multi-driver

而如下的读变量代码则没有任何问题:


process(a)
begin
	c <= a;
end
process(a)
begin
	d <= not a; -- OK
end

// Verilog example
always@(a)
	c = a;
always@(a)
	d = ~a; // OK

写变量注意事项

关于“一写”,要把握一个原则,就是每次动作只能修改一次变量的值。动作可分为组合动作和时序动作,组合逻辑输入信号的每一次变化称为一个组合动作,而时钟信号的每一次有效沿称为一个时序动作。所谓“每次动作只能修改一次变量的值”,对于组合动作来说,由于输入唯一确定输出,因此组合动作发生后,输出只可能变化一次;对于时序动作来说,由于输出取决于时钟有效沿到来时刻的输入值,因此输出也只可能变化一次。在进行代码设计时,凡是与上述思路不符合的写变量操作都是无法实现的,例如下述的代码缔造者就想在一次动作发生后改变两次变量的值,但这是无法实现的:


-- VHDL example	
process(a,b) -- 错误的组合动作思路
begin
	c <= a;
	c <= b;
end
process(clk) -- 错误的时序动作思路
begin
	if (clk'event and clk = '1') then
		d <= a;
		d <= b;
	end if;
end process;

// Verilog example
always@(a,b) // 错误的组合动作思路
begin 
	c = a;
	c = b;
end
always@(posedge clk) // 错误的时序动作思路
begin 
	d <= a;
	d <= b;
end

上述代码并不会造成编译错误,事实上这属于赋值冲突问题,我们将在后面的【赋值冲突】小节进行详细介绍。
在实际中,由于路径延迟等的原因,每一次动作后,输出的确有可能发生多次反转,但这绝不是FPGA设计的初衷,而是由于组合逻辑中的“竞争与险象”。

读变量注意事项

关于“多读”,其实就是说该变量可以作为多个并行语句的输入变量,无论这些语句是描述时序逻辑还是组合逻辑。不过当读变量的并行语句同时也是那唯一的一条写变量并行语句时,一定要注意避免引入反馈,否则要不然不能综合,要不然不能仿真。例如如下的代码就存在着问题:


-- VHDL example	
	process(a) -- 错误代码
	begin
		c <= a xor c;
	end process;
	
	// Verilog example
	always@(a) // 错误的代码
	begin 
		c = a ^ c;
	end

但如下的代码就是完全没有问题的:
– VHDL example


process(clk) -- 正确的代码
begin
	if (clk'event and clk = '1') then
		c <= a xor c;
	end if;
end process;

// Verilog example
always@(posedge clk) // 正确的代码
begin 
	c <= a ^ c;
end

这是因为时序逻辑是在时钟有效沿才将输入搬移到输出的,其反馈路径实际上是被触发器截断了的,因此确切的说上述时序逻辑代码中的不应该叫做反馈。
需要注意的是,数字电路中其实是可以存在反馈的,例如锁存器、触发器等时序单元就可以利用组合逻辑单元的正反馈实现,但这是更底层的细节,在FPGA中,HDL描述语言可以直接描述或调用锁存器、触发器等基本时序单元,而无需甚至无法创造它们。

赋值冲突

赋值冲突,是写变量时常碰到的一类问题,其主要可分为两类,分别介绍如下:

两个以上并行语句赋值冲突

这类赋值冲突对FPGA设计来说是致命的,因为它违背了变量操作中“一写”的原则。因此无论起冲突的并行语句情况是组合与组合、时序与时序、组合与时序,其都会造成编译器的报错,所以此类冲突必须禁止。

两个以上串行语句赋值冲突

这类冲突对编译器并不一定是致命的,但是由于其违背了“一写”中“每次动作只能修改一次变量的值”,因此当出现此类冲突时,FPGA设计的行为很可能与我们预期的不一致,从而造成问题。经过【本篇->编程思路->编写纯净的组合或时序逻辑】章节的学习,我们已经知道,为了避免不必要的麻烦,要尽量编写纯净的组合或时序逻辑,那么在本小节的讨论中,我们就主要针对纯净的组合逻辑并行语句和时序逻辑并行语句来进行赋值冲突讨论。
这里需要补充一点,在Verilog语言中,如果在一个always中对同一变量同时应用阻塞和非阻塞赋值,那么这类赋值冲突编译器是会报错的。

组合并行语句内串行语句的赋值冲突

这类赋值冲突可分为三种:
第一种,无反馈的组合串行赋值冲突。这类冲突发生时,按照HDL串行语句的执行思路,可知写在最后面的一条语句才是有效的。例如下述代码对应的都是一个逻辑或的功能:


	-- VHDL example	
	process(a, b)
	begin
		t <= a and b; -- 无效赋值
		t <= a or b; -- 有效赋值
	end process;
	
	// Verilog example
	always@(a, b)
begin
		t = a & b; //无效赋值
		t = a | b;	//有效赋值
end

第二种:反馈在前的组合串行赋值冲突。这种赋值冲突的分析结果与上一种一致,尽管反馈现象是非常不好的,但是由于后续的无反馈代码对信号的值进行了重新设置,所以使得反馈代码相当于无效。例如下述代码对应的仍是一个逻辑或的功能:


	-- VHDL example	
	process(a, b)
	begin
		t <= not t; -- 无效赋值
		t <= a or b; -- 有效赋值
	end process;
	
	// Verilog example
	always@(a, b)
begin
		t = ~t; //无效赋值
		t = a | b;	//有效赋值
end

第三种,反馈在后的组合串行赋值冲突。这类赋值冲突的分析结果与前两种截然不同,它不在是简单的忽略掉之前代码对变量的赋值操作,而是将之前代码得到的变量值代入到最后一个具有反馈的赋值语句中。例如,如下代码对应的是一个三输入与门的功能:


-- VHDL example	
	process(a, b, c)
	begin
		t <= a and b; 
		t <= t and c; -- equal to : t <= a and b and c;
	end process;
	
	// Verilog example
	always@(a, b, c)
begin
		t = a & b; 
		t = t & c;	//equal to : t = a & b & c;
end

时序并行语句内串行语句的赋值冲突

在【变量访问思路概述】小节中,我们已经分析过,时序逻辑中,实际上是不存在真正的反馈的,因此当位于描述时序逻辑的串行语句中出现赋值冲突时,按照HDL串行语句的执行思路,可知写在最后面的一条语句才是有效的。例如下述代码对应的都是一个逻辑或加一个触发器的功能:


	-- VHDL example	
	process(clk)
	begin
		if (clk’event and clk = ‘1’)then
			t <= a and b; -- 无效赋值
			t <= a or b; -- 有效赋值
		end if;
	end process;
	
	// Verilog example
	always@(posedge clk)
begin
		t <= a & b; //无效赋值
		t <= a | b;	//有效赋值
end

利用赋值冲突编写代码

一个规范且合理的HDL代码中,最好不要出现赋值冲突的情况,因为变量的驱动只能有一个。虽然出现了赋值冲突的代码仍然能够被编译,但总有一部分赋值冲突代码的存在是没有任何意义的,而且会令代码的阅读变得更加困难。不过赋值冲突现象也不是一无是处,有一种利用无反馈的组合串行赋值冲突来简化HDL代码的技巧,举例介绍如下。先看一段代码:


	-- VHDL example	
	process(sel)
	begin
		case (sel) is 
			when "00" =>
				flaga <= 1'b1; 
				flagb <= 1'b0;
				flagd <= 1'b0; 
				flagd <= 1'b0;
			when "01" =>
				flaga <= 1'b0; 
				flagb <= 1'b1;
				flagd <= 1'b0; 
				flagd <= 1'b0;
			when "10" =>
				flaga <= 1'b0; 
				flagb <= 1'b0;
				flagd <= 1'b1; 
				flagd <= 1'b0;
			when "11" =>
				flaga <= 1'b0; 
				flagb <= 1'b0;
				flagd <= 1'b0; 
				flagd <= 1'b1;
			when others =>
				flaga <= 1'b0; 
				flagb <= 1'b0;
				flagd <= 1'b0; 
				flagd <= 1'b0;
		end case;
	end process;
	
	// Verilog example
	always@(sel)
	begin
			case (sel)
			2'b00 : 	
begin
				flaga = 1'b1; 
				flagb = 1'b0;
				flagd = 1'b0; 
				flagd = 1'b0;
			end
			2'b01 : 	
begin
				flaga = 1'b0; 
				flagb = 1'b1;
				flagd = 1'b0; 
				flagd = 1'b0;
			end
			2'b10 : 
begin
				flaga = 1'b0; 
				flagb = 1'b0;
				flagd = 1'b1; 
				flagd = 1'b0;
			end
			2'b11 : 	
begin
				flaga = 1'b0; 
				flagb = 1'b0;
				flagd = 1'b0; 
				flagd = 1'b1;
			end
			default : 		
begin
				flaga = 1'b0; 
				flagb = 1'b0;
				flagd = 1'b0; 
				flagd = 1'b0;
			end
		endcase	
	end

在上例中,由于描述的是一个组合逻辑电路,依据在【本篇->编程思路->编写纯净的组合或时序逻辑->组合逻辑描述方法】小节中的分析,必须在每一个条件分支中都保证flaga~flagd具有一个确定的输出值,否则将会在设计中引入锁存。因此上述代码会显得比较冗长。此时,只要稍稍利用一下无反馈的组合串行赋值冲突的分析结果,就可以在保证代码原有功能不变的前提下,极大的简化代码的书写,例如上例可以修改如下:


-- VHDL example	
process(sel)
begin
	flaga <= 1'b0; 
	flagb <= 1'b0;
	flagd <= 1'b0; 
	flagd <= 1'b0;

	case (sel) is 
		when "00" =>
			flaga <= 1'b1;
		when "01" => 
			flagb <= 1'b1;
		when "10" =>
			flagd <= 1'b1; 
		when "11" => 
			flagd <= 1'b1;
	end case;
end process;

// Verilog example
always@(sel)
begin
	flaga = 1'b0; 
	flagb = 1'b0;
	flagd = 1'b0; 
	flagd = 1'b0;

	case (sel)
		2'b00 : flaga = 1'b1;
		2'b01 : flagb = 1'b1;
		2'b10 : flagc = 1'b1; 
		2'b11 : flagd = 1'b1;
	endcase	
end

对比上述两个例子,可以发现利用了无反馈的组合串行赋值冲突,对HDL代码的简化效果还是非常明显的。注意,以上例子是赋值冲突现象的一个正面应用案例,而在其他情况下,强烈建议大家一定要避免在代码中出现赋值冲突的情况。

总线是怎么回事?

关于“一写多读”的变量访问思路,有的人可能会质疑并举出反例——总线。你可能会觉得总线好像就是一个真正可以“多写多读”的对象,其实不然!在这一小节中,我们将详细介绍一下总线的概念及其实现方式,大家也可以利用与总线类似的思路,来完成一些“多写多读”的事情。

总线的基本概念介绍

总线,英文为Bus,是系统各种功能部件之间传送信息的公共通信干线。例如,SPI、C、PCI、PCIe、PXI、PXIe、USB、SATA、1553B、CAN等等,都是一些常用的总线规范。
假设系统现在有5个部件——A、B、C、D、E,互相之间都需要传递数据,如果没有总线,那么它们之间的连接关系很可能如下图所示。如果此时系统中又加入一个部件F,那么系统新增的部分如图中虚线所示:
FPGA之道(45)正确的变量访问思路_选择器
(注意,为了能够互相传递数据,上图中的每根连线实际对应输入、输出两根连线)
如果在系统中引入总线结构,那么5个部件之间的连接关系很可能如下图所示。如果此时系统中又加入一个部件F,那么系统新增的部分如图中虚线所示:
FPGA之道(45)正确的变量访问思路_三态门_02
对比上述两种情况,我们可以明显感受到总线为系统设计带来的好处,即:简化了系统的结构和硬件设计;便利了系统的扩充和更新;简化了系统的调试和维修。
当然了,总线也有一定的缺点,那就是系统中的各个部件只能分时使用总线,而无法同时使用。因此从微观上来说,在某一固定时刻,总线都是“一写多读”的,只不过通过一些控制电路,我们貌似可以在不同时刻为总线分配不同的驱动源,以达到宏观上的“多写多读”。由于总线是被大家分时使用的,那么对于一些实时性要求很高的通信要求,就比较难以胜任。除此以外,必须引入额外的控制和筛选机制,以保证连接到总线的各个部件都知道什么时候可以写总线以及什么时候总线上的数据是发给自己的。
了解了总线的优缺点之后,接下来就让我们来看一下如何在FPGA中实现总线吧。

总线实现形式之选择器

利用多路选择器的选通特性,可以实现总线的功能,这也是在FPGA芯片内部设计总线时最常用的方法,因为FPGA芯片内部有着非常丰富的MUX资源。

原理简介

选择器式总线的原理图如下:
FPGA之道(45)正确的变量访问思路_三态门_03
从图中我们可以看出,总线仍然是“一写多读”的,其驱动源只有一个,那就是MUX的输出。而由于MUX具有选通功能,所以可以通过改变选通信号的值来改变写入总线数据的来源,因此给我们以一种“多写多读”的错觉。

示例代码

本小节为大家提供了一个非常简单的选择器式总线的FPGA实现方式。功能代码主要分为三个部分:
1、选择器控制信号产生部分,采用抢占式优先级译码器的思路,根据四个外围器件的请求信号产生多路选择器的选择控制信号。
2、写总线部分,即将选通通道的数据写到总线上。
3、读总线部分,各个外围器件根据自己的情况将总线数据读入器件内部进行运算和处理。
示例代码如下:


-- VHDL example	
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity SimplestMuxBus is
port (
	request0, request1, request2, request3 : in std_logic;
	device0Out, device1Out, device2Out, device3Out : in  std_logic_vector(7 downto 0);
	device0In, device1In, device2In, device3In : out std_logic_vector(7 downto 0)
);
end SimplestMuxBus;

architecture Behavioral of SimplestMuxBus is

	signal requestAll : std_logic_vector(3 downto 0);
	signal sel : std_logic_vector(1 downto 0);
	signal sysBus : std_logic_vector(7 downto 0);

begin

-- generate sel
requestAll <= request0 & request1 & request2 & request3;
process(requestAll)
begin
	case (requestAll) is 
		when "0001" =>
			sel <= "11";
		when "0010"|"0011" =>
			sel <= "10";
		when "0100"|"0101"|"0110"|"0111" =>				
			sel <= "01";
		when others =>				
			sel <= "00";
	end case;
end process;

-- write sysBus
process(sel, device0Out, device1Out, device2Out, device3Out)
begin
	case (sel) is 
		when "00" =>
			sysBus <= device0Out;
		when "01" =>
			sysBus <= device1Out;
		when "10" =>				
			sysBus <= device2Out;
		when "11" =>				
			sysBus <= device3Out;
		when others =>				
			sysBus <= device0Out;
	end case;
end process;

-- read sysBus
device0In <= sysBus;
device1In <= sysBus;
device2In <= sysBus;
device3In <= sysBus;	

end Behavioral;

// Verilog example
module SimplestMuxBus (
input request0, request1, request2, request3,
input [7:0] device0Out, device1Out, device2Out, device3Out,
output [7:0] device0In, device1In, device2In, device3In
);

reg [1:0] sel;
reg [7:0] bus;

//generate sel
always@(request0, request1, request2, request3)
begin
	//Priority decoder
	case ({request0, request1, request2, request3})
  			4'b0001 : sel = 2'b11;
 			4'b0010 - 4'b0011 : sel = 2'b10;
  			4'b0100 - 4'b0111 : sel = 2'b01;
  			default : sel = 2'b00;
	endcase
end

//write bus
always@(sel, device0Out, device1Out, device2Out, device3Out)
begin 
	case (sel)
  			2'b00 : bus = device0Out;
  			2'b01 : bus = device1Out;
  			2'b10 : bus = device2Out;
  			2'b11 : bus = device3Out;
  			default : bus = device0Out;
	endcase
end

//read bus
assign device0In = bus;
assign device1In = bus;
assign device2In = bus;
assign device3In = bus;
	
endmodule

总线实现形式之三态门

利用三态门的高阻特性,可以实现总线的功能,这也是硬件电路系统中设计总线时最常用的方法。由于三态门资源大多存在于FPGA芯片的接口资源中而非FPGA芯片内部,因此一般当总线位于FPGA芯片的外部时,常采用这种方式来实现。

原理简介

三态门式总线的原理图如下:
FPGA之道(45)正确的变量访问思路_赋值_04
从图中我们可以看出,总线正常工作时,最多只能有一个三态门被选通,否则总线上将会发生数据冲突,从而导致系统运转不正常。因此三态门式总线仍然是“一写多读”的,并且其驱动源只有一个,那就是被选通的那个三态门的输出。也许你会说,此时总线的驱动源就不是固定的了,因为每次被选通的三态门可以不同。不过这里请大家注意,三态门的高阻实际上就相当于电路的断路,因此选通不同的三态门,其实相当于对数字电路从结构上做了动态的调整与修改,因此改变选通情况的前后两种状态实际上是对应两种不同的数字电路,而要求驱动源固定是针对同一个数字电路来说的,因此此类总线结构并不和我们之前所介绍的概念冲突。而由于三态门的高阻特性,可以通过改变选通信号的值来动态改变各个输入源于总线之间的电路连接关系,因此带给我们一种“多写多读”的错觉。

赋值冲突?

经过【赋值冲突】章节的学习,我们知道如下代码的写法是错误的,也是无法通过编译的。


-- VHDL example	
c <= a when sel(0) = '1' else '0';
c <= b when sel(1) = '1' else '1';	

// Verilog example
assign c = (sel[0] == 1'b1) ? a : 1'b0;
assign c = (sel[1] == 1'b1) ? b : 1'b1;
但是,以下的写法却是正确的,且不会产生赋值冲突。
-- VHDL example	
c <= a when sel(0) = '1' else 'Z';
c <= b when sel(1) = '1' else 'Z';	

// Verilog example
assign c = (sel[0] == 1'b1) ? a : 1'bz;
assign c = (sel[1] == 1'b1) ? b : 1'bz;

对比以上两种写法,可以发现唯一的区别就是高阻态的使用。为什么使用了高阻态,代码就是正确的呢?这是因为高阻态实际上就是电路中的断路,通过合理的切换到高阻态,可以让上述有高阻态例子中的两条赋值语句不会在同一个时刻生效,因此不会产生赋值冲突。而没有高阻态的例子代码,两条赋值语句肯定都是一直生效的,因此必然会产生赋值冲突从而导致出错。

示例代码

本小节为大家提供了一个非常简单的三态门式总线的FPGA实现方式。功能代码主要分为三个部分:
1、三态门控制信号产生部分,采用抢占式优先级译码器的思路,根据四个外围器件的请求信号产生三态门的选通控制信号。
2、写总线部分,即将四个三态门的输出连接到总线上。
3、读总线部分,各个外围器件根据自己的情况将总线数据读入器件内部进行运算和处理。


	示例代码如下:
-- VHDL example	
	library IEEE;
	use IEEE.STD_LOGIC_1164.ALL;
	use IEEE.STD_LOGIC_ARITH.ALL;
	use IEEE.STD_LOGIC_UNSIGNED.ALL;

	entity simplestZBus is
	port (
		request0, request1, request2, request3 : in std_logic;
		device0Out, device1Out, device2Out, device3Out : in  std_logic_vector(7 downto 0);
		device0In, device1In, device2In, device3In : out std_logic_vector(7 downto 0)
	);
	end simplestZBus;

	architecture Behavioral of simplestZBus is

		signal requestAll : std_logic_vector(3 downto 0);
		signal en : std_logic_vector(3 downto 0);
		signal sysBus : std_logic_vector(7 downto 0);

	begin

		-- generate sel
		requestAll <= request0 & request1 & request2 & request3;
		process(requestAll)
		begin
			case (requestAll) is 
				when "0000" =>
					en <= "0000";
				when "0001" =>
					en <= "1000";
				when "0010"|"0011" =>
					en <= "0100";
				when "0100"|"0101"|"0110"|"0111" =>				
					en <= "0010";
				when others =>				
					en <= "0001";
			end case;
		end process;
		
		-- write sysBus
		sysBus <= device0Out when en(0) = '1' else "ZZZZZZZZ";
		sysBus <= device1Out when en(1) = '1' else "ZZZZZZZZ";
		sysBus <= device2Out when en(2) = '1' else "ZZZZZZZZ";
		sysBus <= device3Out when en(3) = '1' else "ZZZZZZZZ";
		
		-- read sysBus
		device0In <= sysBus;
		device1In <= sysBus;
		device2In <= sysBus;
		device3In <= sysBus;	

	end Behavioral;
	
	// Verilog example
	module simplestZBus(
	input request0, request1, request2, request3,
	input [7:0] device0Out, device1Out, device2Out, device3Out,
	output [7:0] device0In, device1In, device2In, device3In
	);

	reg [3:0] en;
	wire [7:0] bus;

	//generate sel
	always@(request0, request1, request2, request3)
	begin
		//Priority decoder
		case ({request0, request1, request2, request3})
			4'b0000 : en = 4'b0000;
			4'b0001 : en = 4'b1000;
			4'b0010, 4'b0011 : en = 4'b0100;
			4'b0100,4'b0101,4'b0110, 4'b0111 : en = 4'b0010;
			default : en = 4'b0001;
		endcase
	end

	//write bus
	assign bus = (en[0] == 1'b1) ? device0Out : 8'hzz;
	assign bus = (en[1] == 1'b1) ? device1Out : 8'hzz;
	assign bus = (en[2] == 1'b1) ? device2Out : 8'hzz;
	assign bus = (en[3] == 1'b1) ? device3Out : 8'hzz;

	//read bus
	assign device0In = bus;
	assign device1In = bus;
	assign device2In = bus;
	assign device3In = bus;
			
	endmodule




免责声明:本文系网络转载或改编,未找到原创作者,版权归原作者所有。如涉及版权,请联系删

QR Code
微信扫一扫,欢迎咨询~

联系我们
武汉格发信息技术有限公司
湖北省武汉市经开区科技园西路6号103孵化器
电话:155-2731-8020 座机:027-59821821
邮件:tanzw@gofarlic.com
Copyright © 2023 Gofarsoft Co.,Ltd. 保留所有权利
遇到许可问题?该如何解决!?
评估许可证实际采购量? 
不清楚软件许可证使用数据? 
收到软件厂商律师函!?  
想要少购买点许可证,节省费用? 
收到软件厂商侵权通告!?  
有正版license,但许可证不够用,需要新购? 
联系方式 155-2731-8020
预留信息,一起解决您的问题
* 姓名:
* 手机:

* 公司名称:

姓名不为空

手机不正确

公司不为空