什么时候使用 VECTOR 表示与 INTEGER 相比更简洁?

电器工程 FPGA 视频文件
2022-01-31 07:22:30

在关于这个问题的答案的评论线程中:VHDL实体中的错误输出指出:

“对于整数,您无法控制或访问 FPGA 中的内部逻辑表示,而 SLV 可让您执行诸如有效利用进位链之类的技巧”

那么,在什么情况下,您发现使用位向量表示比使用整数s 来访问内部表示更简洁?测量了哪些优势(在芯片面积、时钟频率、延迟或其他方面)?

4个回答

在编写 VHDL 时,我强烈建议对SIGNALS使用 std_logic_vector (slv) 而不是 integer (int) (另一方面,将 int 用于泛型、一些常量和一些变量可能非常有用。)简单地说,如果你声明一个 int 类型的信号,或者必须为一个整数指定一个范围,那么你可能正在做有事吗。

int 的问题在于 VHDL 程序员不知道 int 的内部逻辑表示是什么,因此我们无法利用它。例如,如果我定义一个范围为 1 到 10 的 int,我不知道编译器如何编码这些值。希望它会被编码为 4 位,但除此之外我们知之甚少。如果您可以探测 FPGA 内部的信号,它可能被编码为“0001”到“1010”,或编码为“0000”到“1001”。它也有可能以对我们人类完全没有意义的方式编码。

相反,我们应该只使用 slv 而不是 int,因为这样我们就可以控制编码并且还可以直接访问各个位。正如您稍后将看到的,拥有直接访问权限很重要。

每当我们需要访问各个位时,我们都可以将一个 int 转换为 slv,但这会变得非常混乱,非常快。这就像两全其美,而不是两全其美。你的代码很难让编译器优化,你几乎不可能阅读。我不推荐这个。

因此,正如我所说,使用 slv 您可以控制位编码并直接访问位。那么你能用这个做什么呢?我给你看几个例子。假设您需要每 4,294,000,000 个时钟输出一次脉冲。以下是使用 int 执行此操作的方法:

signal count :integer range 0 to 4293999999;  -- a 32 bit integer

process (clk)
begin
  if rising_edge(clk) then
    if count = 4293999999 then  -- The important line!
      count <= 0;
      pulse <= '1';
    else
      count <= count + 1;
      pulse <= '0';
    end if;
  end if;
end process;

以及使用 slv 的相同代码:

use ieee.numeric_std.all;
signal count :std_logic_vector (32 downto 0);  -- a 33 bit integer, one extra bit!

process (clk)
begin
  if rising_edge(clk) then
    if count(count'high)='1' then   -- The important line!
      count <= std_logic_vector(4293999999-1,count'length);
      pulse <= '1';
    else
      count <= count - 1;
      pulse <= '0';
    end if;
  end if;
end process;

大多数代码在 int 和 slv 之间是相同的,至少在结果逻辑的大小和速度方面是相同的。当然,一个是向上计数,另一个是向下计数,但这对于这个例子并不重要。

不同之处在于“重要线”。

对于 int 示例,这将产生一个 32 输入比较器。使用 Xilinx Spartan-3 使用的 4 输入 LUT,这将需要 11 个 LUT 和 3 级逻辑。一些编译器可能会将其转换为减法,该减法将使用进位链并跨越相当于 32 个 LUT,但运行速度可能比 3 级逻辑快。

在 slv 示例中,没有 32 位比较,因此它是“零 LUT,零级逻辑”。唯一的惩罚是我们的计数器是一个额外的位。因为这个额外的计数器位的额外时序都在进位链中,所以有“几乎为零”的额外时序延迟。

当然这是一个极端的例子,因为大多数人不会以这种方式使用 32 位计数器。它确实适用于较小的柜台,但差异将不那么显着,尽管仍然很重要。

这只是如何利用 slv 而不是 int 来获得更快时序的一个示例。还有很多其他方法可以利用 slv——它只需要一些想象力。

更新:添加了一些东西来解决 Martin Thompson 关于将 int 与“if (count-1) < 0”一起使用的评论

(注意:我假设您的意思是“如果 count<0”,因为这会使它更等同于我的 slv 版本,并且不需要额外的减法。)

在某些情况下,这可能会生成预期的逻辑实现,但不能保证始终有效。这将取决于您的代码以及编译器如何编码 int 值。

根据您的编译器以及您指定 int 范围的方式,当 int 值进入 FPGA 逻辑时,它完全有可能不会编码为“0000...0000”的位向量。为了使您的变体起作用,它必须编码为“0000...0000”。

例如,假设您将 int 定义为范围为 -5 到 +5。您期望将值 0 编码为 4 位,如“0000”,+5 为“0101”,-5 为“1011”。这是典型的二进制补码编码方案。

但不要假设编译器将使用二进制补码。虽然不寻常,但补码可能会导致“更好”的逻辑。或者,编译器可以使用一种“有偏”编码,其中 -5 编码为“0000”,0 编码为“0101”,+5 编码为“1010”。

如果 int 的编码是“正确的”,那么编译器可能会推断出如何处理进位位。但如果它不正确,那么由此产生的逻辑将是可怕的。

以这种方式使用 int 可能会导致合理的逻辑大小和速度,但这不是保证。切换到不同的编译器(例如 XST 到 Synopsis),或使用不同的 FPGA 架构可能会导致完全错误的事情发生。

Unsigned/Signed vs. slv 是另一个争论。您可以感谢美国政府委员会在 VHDL 中为我们提供了如此多的选择。:) 我使用 slv 因为这是模块和内核之间的接口标准。除此之外,以及模拟中的其他一些情况,我认为使用 slv 而不是签名/未签名有很大的好处。我也不确定有符号/无符号是否支持三态信号。

我已经以两种形式编写了其他两个海报建议的代码,vectorinteger注意让两个版本以尽可能相似的方式运行。

我比较了仿真结果,然后使用面向 Xilinx Spartan 6 的 Synplify Pro 进行了合成。下面的代码示例是从工作代码中粘贴的,因此您应该能够将它们与您最喜欢的合成器一起使用,看看它的行为是否相同。


降价柜台

首先,正如大卫凯斯纳所建议的那样:

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity downcounter is
    generic (top : integer);
    port (clk, reset, enable : in  std_logic; 
         tick   : out std_logic);
end entity downcounter;

矢量架构:

architecture vec of downcounter is
begin
    count: process (clk) is
        variable c : unsigned(32 downto 0);  -- don't inadvertently not allocate enough bits here... eg if "integer" becomes 64 bits wide
    begin  -- process count
        if rising_edge(clk) then  
            tick <= '0';
            if reset = '1' then
                c := to_unsigned(top-1, c'length);
            elsif enable = '1' then
                if c(c'high) = '1' then
                    tick <= '1';
                    c := to_unsigned(top-1, c'length);
                else
                    c := c - 1;
                end if;
            end if;
        end if;
    end process count;
end architecture vec;

整数架构

architecture int of downcounter is
begin
    count: process (clk) is
        variable c : integer;
    begin  -- process count
        if rising_edge(clk) then  
            tick <= '0';
            if reset = '1' then
                c := top-1;
            elsif enable = '1' then
                if c < 0 then
                    tick <= '1';
                    c := top-1;
                else
                    c := c - 1;
                end if;
            end if;
        end if;
    end process count;
end architecture int;

结果

在代码方面,整数 1 对我来说似乎更可取,因为它避免了to_unsigned()调用。否则,没有太多选择。

通过 Synplify Pro 运行它会为该版本top := 16#7fff_fffe#生成66 个 LUT为该vector版本生成64 个 LUTinteger两个版本都大量使用了进位链。两者都报告超过280MHz的时钟速度。合成器非常有能力很好地利用进位链——我用 RTL 查看器直观地验证了两者都产生了相似的逻辑。显然,带有比较器的递增计数器会更大,但整数和向量再次相同。


除以 2**n 个计数器

ajs410 建议:

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity clkdiv is
    port (clk, reset : in     std_logic;
        clk_2, clk_4, clk_8, clk_16  : buffer std_logic);
end entity clkdiv;

矢量架构

architecture vec of clkdiv is

begin  -- architecture a1

    process (clk) is
        variable count : unsigned(4 downto 0);
    begin  -- process
        if rising_edge(clk) then  
            if reset = '1' then
                count  := (others => '0');
            else
                count := count + 1;
            end if;
        end if;
        clk_2 <= count(0);
        clk_4 <= count(1);
        clk_8 <= count(2);
        clk_16 <= count(3);
    end process;

end architecture vec;

整数架构

您必须跳过一些障碍,以避免只使用to_unsigned然后挑选位,这显然会产生与上述相同的效果:

architecture int of clkdiv is
begin
    process (clk) is
        variable count : integer := 0;
    begin  -- process
        if rising_edge(clk) then  
            if reset = '1' then
                count  := 0;
                clk_2  <= '0';
                clk_4  <= '0';
                clk_8  <= '0';
                clk_16 <= '0';
            else
                if count < 15 then
                    count := count + 1;
                else
                    count := 0;
                end if;
                clk_2 <= not clk_2;
                for c4 in 0 to 7 loop
                    if count = 2*c4+1 then
                        clk_4 <= not clk_4;
                    end if;
                end loop; 
                for c8 in 0 to 3 loop
                    if count = 4*c8+1 then
                        clk_8 <= not clk_8;
                    end if;
                end loop; 
                for c16 in 0 to 1 loop
                    if count = 8*c16+1 then
                        clk_16 <= not clk_16;
                    end if;
                end loop; 
            end if;
        end if;
    end process;
end architecture int;

结果

代码方面,在这种情况下,vector版本显然更好!

在综合结果方面,对于这个小例子,整数版本(如 ajs410 预测的那样)确实会产生 3 个额外的 LUT 作为比较器的一部分,我对综合器过于乐观,尽管它正在处理一段非常模糊的代码!


其他用途

当您希望算术环绕时,向量是一个明显的胜利(计数器甚至可以作为单行完成):

vec <= vec + 1 when rising_edge(clk);

对比

if int < int'high then 
   int := int + 1;
else
   int := 0;
end if;

尽管至少从该代码中可以清楚地看出作者打算进行环绕。


我没有在实际代码中使用过的东西,但思考过:

“自然环绕”功能也可用于“通过溢出计算”。当您知道加法/减法和乘法链的输出是有界的时,您不必将中间计算的高位存储为(在 2-s 补码中)它会“在洗涤中”出现当你到达输出时。有人告诉我,这篇论文包含一个证明,但它看起来有点密集,我无法快速评估!计算机加法和溢出理论 - HL Garner

在这种情况下使用integers 会在包裹时导致模拟错误,即使我们知道它们最终会解包裹。


正如菲利普指出的那样,当你需要一个大于 2**31 的数字时,你别无选择,只能使用向量。

我的建议是两者都尝试,然后查看综合、地图和布局布线报告。这些报告将准确告诉您每种方法消耗了多少 LUT,它们还将告诉您逻辑可以运行的最大速度。

我同意 David Kessner 的观点,即您受制于工具链,并且没有“正确”的答案。综合是黑魔法,知道发生了什么的最好方法是仔细彻底地阅读生成的报告。Xilinx 工具甚至允许您查看 FPGA 内部,包括每个 LUT 的编程方式、进位链的连接方式、交换结构如何连接所有 LUT 等。

对于 Kessner 先生方法的另一个戏剧性示例,假设您希望在 1/2、1/4、1/8、1/16 等处拥有多个时钟频率。您可以使用一个整数,每个周期都不断计数,然后针对该整数值有多个比较器,每个比较器输出形成不同的时钟分频。根据比较器的数量,扇出可能会变得异常大,并开始消耗额外的 LUT 来进行缓冲。SLV 方法只会将向量的每个单独的位作为输出。

一个明显的原因是有符号和无符号允许比 32 位整数更大的值。这是 VHDL 语言设计中的一个缺陷,这不是必需的。新版本的 VHDL 可以解决这个问题,需要整数值来支持任意大小(类似于 Java 的 BigInt)。

除此之外,我很想听听关于整数与向量相比表现不同的基准。

顺便说一句,Jan Decaluwe 写了一篇很好的文章:这些 Ints are made for Countin'