如何在 VHDL 中创建字符串列表
VHDL 中的文本字符串通常仅限于固定长度的字符数组。这是有道理的,因为 VHDL 描述了硬件,而通用长度的字符串需要动态内存。
要定义字符串数组,您必须在编译时为要存储的最大字符串数分配空间。更糟糕的是,您必须确定字符串的最大长度,并将每次出现的字符都填充到该字符数。下面的代码显示了这种构造的示例用法。
type arr_type is array (0 to 3) of string(1 to 10); signal arr : arr_type; begin arr(0) <= "Amsterdam "; arr(1) <= "Bangkok "; arr(2) <= "Copenhagen"; arr(3) <= "Damascus ";
虽然从硬件的角度来看这是有道理的,但在 VHDL 测试平台中使用字符串数组变得很麻烦。因此,我决定创建一个动态字符串列表包,我将在本文中进行说明。
您可以使用下面的表格下载完整的代码。
Python 的列表类
让我们在一个众所周知的列表实现之后为我们的动态 VHDL 列表建模。我们的 VHDL 字符串列表将模仿 Python 内置列表类的行为。我们将采用 append() , 插入() , 和 pop() Python 列表中的方法。
为了向你展示我的意思,我将直接进入并打开一个交互式 python shell 来运行一些实验。
首先,我们先声明一个列表,并在其中附加四个字符串,如下所示。
IPython 7.19.0 -- An enhanced Interactive Python. Type '?' for help. In [1]: l = [] In [2]: l.append("Amsterdam") In [3]: l.append("Bangkok") In [4]: l.append("Copenhagen") In [5]: l.append("Damascus")
append() 方法简单;它将一个对象附加到列表的末尾。
我们可以用 pop() 来验证 方法,该方法删除一个元素并将其返回给调用者。参数指定要检索的元素的位置。通过弹出 0 直到列表为空,我们得到从最低到最高索引排序的内容:
In [6]: for _ in range(len(l)): print(l.pop(0)) Amsterdam Bangkok Copenhagen Damascus
好的,让我们重新填写清单。而这一次,我们将使用 insert() 乱序添加列表元素的方法:
In [7]: l.insert(0, "Bangkok") In [8]: l.insert(1, "Copenhagen") In [9]: l.insert(0, "Amsterdam") In [10]: l.insert(3, "Damascus")
insert() 函数让您指定在哪个索引处插入新项目。在上面的示例中,我们创建了与之前相同的列表。让我们通过像数组一样遍历列表来检查:
In [11]: for i in range(len(l)): print(l[i]) Amsterdam Bangkok Copenhagen Damascus
Python 括号 [] 列表运算符不会删除项目;它使列表的行为类似于数组。从上面的清单中可以看出,您可以通过括号内的数字获得插槽内容。
让我们通过弹出来清空列表,但这次是从列表的末尾。 Python 列表的一个特点是您可以使用负索引从最后一项而不是列表的开头开始计数。它适用于括号运算符和 insert() 或 pop() 方法。
通过弹出索引 -1,您始终可以从列表中获取最后一项。当我们把它放在一个 For 循环中时,它会以相反的顺序清空列表:
In [12]: for _ in range(len(l)): print(l.pop(-1)) Damascus Copenhagen Bangkok Amsterdam
您也可以使用负索引来插入。在下面示例的最后一行中,我们在索引 -1 处插入“Copenhagen”:
In [13]: l.append("Amsterdam") In [14]: l.append("Bangkok") In [15]: l.append("Damascus") In [16]: l.insert(-1, "Copenhagen") # insert at the second last position
当我们遍历列表时,我们可以看到“Copenhagen”现在是倒数第二个元素:
In [17]: for i in range(len(l)): print(l[i]) Amsterdam Bangkok Copenhagen Damascus
现在,关键来了(但这是有道理的)。
当插入到 -1 时,新项目成为倒数第二个,但是当从 -1 弹出时,我们得到最后一个项目。
这是有道理的,因为 -1 指的是当前列表中最后一个元素的位置。当我们弹出时,我们要求最后一个元素。但是当我们插入时,我们要求将新项目插入到当前列表中最后一个元素的位置。因此,新项目将最后一个元素替换了一个位置。
我们可以通过弹出元素 -1 来确认这一点,它返回“Damascus”,而不是“Copenhagen”:
In [18]: l.pop(-1) # pop from the last position Out[18]: 'Damascus'
该列表现在包含三个元素:
In [19]: for i in range(len(l)): print(l[i]) Amsterdam Bangkok Copenhagen
也可以这样计算列表长度:
In [20]: len(l) Out[20]: 3
我们可以通过调用 clear() 清空列表 :
In [21]: l.clear() In [22]: len(l) Out[22]: 0
如您所见,Python 列表用途广泛,许多程序员都理解它们。这就是为什么我会根据这个成功公式来实现我的 VHDL 列表。
字符串列表VHDL子程序原型
为了让我们能够像使用具有成员方法的对象一样使用字符串列表,我们必须将其声明为受保护类型。我们会将受保护的类型放在同名的包中:string_list .
下面的代码显示了列出子程序原型的受保护类型的“公共”部分。
package string_list is type string_list is protected procedure append(str : string); procedure insert(index : integer; str : string); impure function get(index : integer) return string; procedure delete(index : integer); procedure clear; impure function length return integer; end protected; end package;
而 append() , 插入() , 和 clear() 程序与它们的 Python 对应程序相同,我们不能移植 pop() 功能直接到VHDL。问题是我们不能轻易地将动态对象从 VHDL 中的受保护类型中传递出去。
为了克服这个限制,我拆分了 pop() 功能分为两个子程序:get() 和 delete() .这将允许我们首先索引元素,就像一个数组,然后在我们不再需要它时删除它。例如,在我们将字符串打印到模拟器控制台之后。
长度() impure 函数的行为类似于 Python 的内置 len() 功能。它将返回列表中的字符串数。
字符串列表VHDL实现
受保护的类型由两部分组成:声明部分和正文。虽然声明部分对用户可见,但主体包含子程序实现和任何私有变量。现在是时候揭示字符串列表的内部工作原理了。
在下面的表格中留下您的电子邮件地址,以便在您的收件箱中接收完整的代码和 ModelSim 项目!
我们将使用单链表作为内部数据结构。
另请阅读:如何在 VHDL 中创建链接列表
因为后面的所有代码都在受保护类型的主体中,所以这些构造在此包之外不能直接访问。所有的通信都必须通过我们在上一节讨论的声明区域中列出的子程序。
数据存储类型和变量
从下面的代码可以看出,我们首先声明了一个访问类型,一个指向动态内存中字符串的 VHDL 指针。当我们谈论动态内存时,它不是 FPGA 上的 DRAM,因为这段代码是不可合成的。字符串列表纯粹是一个模拟组件,它会使用运行模拟的计算机的动态内存。
type str_ptr is access string; type item; type item_ptr is access item; type item is record str : str_ptr; next_item : item_ptr; end record;
在 str_ptr 之后 , 我们声明 item 作为不完整的类型。我们必须这样做,因为在下一行,我们引用 item 创建 item_ptr 时 .
最后,我们指定 item 的完整声明 类型,包含字符串指针和指向下一个元素的指针的记录。 item->item_ptr->item 类型之间存在循环依赖关系 ,并首先声明不完整的 item 类型,我们避免了编译错误。
受保护类型包含两个变量,如下所示: root 和 length_i . root 指向的项目 将是列表的第一个元素,数组索引为零。而 length_i 变量将始终反映列表中的字符串数。
variable root : item_ptr; variable length_i : integer := 0;
附加过程
append() 下面显示的过程是在列表的最后一个位置插入字符串的简写符号。
procedure append(str : string) is begin insert(length_i, str); end procedure;
正如 Python 示例中所讨论的,使用索引 -1 很容易在倒数第二个位置插入:insert(-1, str)
.但是在最后一个位置插入需要列表的长度作为索引参数。这可能就是为什么 Python 的列表有一个专门的 append() 方法,我们也会有一个。
插入过程
插入过程如下图所示,分四步进行。
首先,我们使用 VHDL new 创建一个动态项目对象 关键词。我们首先创建一个列表项对象,然后创建一个动态字符串对象来存储在其中。
procedure insert(index : integer; str : string) is variable new_item : item_ptr; variable node : item_ptr; variable index_v : integer; begin -- Create the new object new_item := new item; new_item.str := new string'(str); -- Restrict the index to the list range if index >= length_i then index_v := length_i; elsif index <= -length_i then index_v := 0; else index_v := index mod length_i; end if; if index_v = 0 then -- The new object becomes root when inserting at position 0 new_item.next_item := root; root := new_item; else -- Find the node to insert after node := root; for i in 2 to index_v loop node := node.next_item; end loop; -- Insert the new item new_item.next_item := node.next_item; node.next_item := new_item; end if; length_i := length_i + 1; end procedure;
第二步是将索引参数转换为符合列表范围的索引。 Python 的 list.insert() 实现允许越界索引,我们的 VHDL 列表也将允许它。如果用户引用了过高或过低的索引,它将默认为最高索引或元素 0。此外,我们使用模运算符将任何边界内的负索引转换为正数组位置。
在第三步中,我们遍历列表以找到要插入的节点。与往常一样,对于链表,我们必须明确处理在根处插入的特殊情况。
第四步也是最后一步是增加 length_i 变量以确保簿记是最新的。
内部 get_index 和 get_node 函数
由于 VHDL 的对象传递限制,我们决定拆分 pop() 分成两个子程序:get() 和 delete() .第一个函数将获取该项目,第二个过程将其从列表中删除。
但是查找索引或对象的算法对于 get() 是相同的 和 delete() ,所以我们可以在两个私有函数中分别实现它:get_index() 和 get_node() .
不像 insert() , Python 的 pop() 函数不允许越界索引,我们的 get_index() 也不允许 功能。为了防止用户错误,如果请求的索引超出范围,我们将引发断言失败,如下所示。
impure function get_index(index : integer) return integer is begin assert index >= -length_i and index < length_i report "get index out of list range" severity failure; return index mod length_i; end function;
get_node() 如下所示的函数更进一步,并在指定索引处找到实际对象。它使用 get_index() 查找正确的节点并返回一个指向 item 的指针 对象。
impure function get_node(index : integer) return item_ptr is variable node : item_ptr; begin node := root; for i in 1 to get_index(index) loop node := node.next_item; end loop; return node; end function;
获取函数
因为私有 get_node() 函数,公共 get() 功能变得相当简单。它是一个单行器,它获取正确的节点,解包字符串内容,并将其返回给调用者。
impure function get(index : integer) return string is begin return get_node(index).str.all; end function;
删除程序
delete() 过程也使用 get_index() 和 get_node() 来简化算法。首先,我们使用 get_index() 找到要删除的对象的索引,如 index_c 所示 常量声明如下。
procedure delete(index : integer) is constant index_c : integer := get_index(index); variable node : item_ptr; variable parent_node : item_ptr; begin if index_c = 0 then node := root; root := root.next_item; else parent_node := get_node(index_c - 1); node := parent_node.next_item; parent_node.next_item := node.next_item; end if; deallocate(node.str); deallocate(node); length_i := length_i - 1; end procedure;
然后,我们从列表中取消链接节点。如果是根对象,我们将下一项设置为根。否则,我们使用 get_node() 找到父项并重新链接列表以分离手头的项目。
最后,我们通过调用 VHDL 关键字 deallocate 释放内存 并更新 length_i 记账变量。
清除程序
要删除所有项目,clear() 过程使用 While 循环遍历列表,调用 delete() 在每个元素上,直到一个都没有。
procedure clear is begin while length_i > 0 loop delete(0); end loop; end procedure;
长度函数
为了符合良好的编程习惯,我们提供了一个 getter 函数,而不是让用户直接访问 length_i 变量。
impure function length return integer is begin return length_i; end function;
用户不会注意到差异,因为您不需要括号来调用没有参数的函数 (my_list.length
)。但是用户不能更改内部记账变量,这是防止误用的保障。
在测试台中使用字符串列表
现在列表实现已经完成,是时候在测试台上运行它了。首先,我们必须从包中导入受保护的类型,如下面代码的第一行所示。
use work.string_list.string_list; entity string_list_tb is end string_list_tb; architecture sim of string_list_tb is shared variable l : string_list; ...
protected 类型是 VHDL 的类类构造,我们可以通过声明 string_list 类型的共享变量来创建它的对象 ,如上面最后一行所示。我们将“list”命名为“l”,以复制我在本文开头介绍的 Python 示例。
从现在开始,我们可以使用软件方法来访问列表的数据。如下图测试台流程所示,我们可以通过共享变量(l.append("Amsterdam")
)。
begin SEQUENCER_PROC : process begin print("* Append four strings"); print(" l.append(Amsterdam)"); l.append("Amsterdam"); print(" l.append(Bangkok)"); l.append("Bangkok"); print(" l.append(Copenhagen)"); l.append("Copenhagen"); print(" l.append(Damascus)"); l.append("Damascus"); ...
我省略了完整的测试平台和运行脚本以减少本文的长度,但您可以通过在下面的表格中留下您的电子邮件地址来请求它。您将在几分钟内在收件箱中收到一个包含完整 VHDL 代码和 ModelSim 项目的 Zip 文件。
运行测试台
如果您使用上面的表格下载了示例项目,您应该能够复制以下输出。详细说明请参阅 Zip 文件中的“How to run.txt”。
这是一个手动检查的测试平台,我让测试用例尽可能地类似于我的 Python 示例。而不是 pop() Python方法,我们使用VHDL列表的get() 函数,然后调用 delete() .这样做也是一样的。
正如我们从下面显示的 ModelSim 控制台的打印输出中看到的那样,VHDL 列表的行为与其 Python 对应项类似。
# * Append four strings # l.append(Amsterdam) # l.append(Bangkok) # l.append(Copenhagen) # l.append(Damascus) # * Pop all strings from the beginning of the list # l.get(0): Amsterdam # l.get(1): Bangkok # l.get(2): Copenhagen # l.get(3): Damascus # * Insert four strings in shuffled order # l.insert(0, Bangkok) # l.insert(1, Copenhagen) # l.insert(0, Amsterdam) # l.insert(3, Damascus) # * Traverse the list like an array # l.get(0): Amsterdam # l.get(1): Bangkok # l.get(2): Copenhagen # l.get(3): Damascus # * Pop all strings from the end of the list # l.get(0): Damascus # l.get(1): Copenhagen # l.get(2): Bangkok # l.get(3): Amsterdam # * Append and insert at the second last position # l.append(Amsterdam) # l.append(Bangkok) # l.append(Damascus) # l.insert(-1, Copenhagen) # * Pop from the last position # l.get(-1): Damascus # * Traverse the list like an array # l.get(0): Amsterdam # l.get(1): Bangkok # l.get(2): Copenhagen # * Check the list length # l.length: 3 # * Clear the list # * Check the list length # l.length: 0 # * Done
最后的想法
我认为 VHDL 的高级编程特性被低估了。虽然它对 RTL 设计没有用处,因为它不可综合,但它对验证目的很有用。
尽管实现起来可能很复杂,但对于最终用户来说会更简单,即编写使用受保护类型的测试平台的人。受保护的类型对用户隐藏了所有复杂性。
使用受保护类型只需要三行:导入包、声明共享变量、调用测试台进程中的子程序。这比实例化 VHDL 组件要简单。
另请参阅:如何在 VHDL 中创建链接列表
在文章下方的评论部分告诉我你的想法!
VHDL