亿迅智能制造网
工业4.0先进制造技术信息网站!
首页 | 制造技术 | 制造设备 | 工业物联网 | 工业材料 | 设备保养维修 | 工业编程 |
home  MfgRobots >> 亿迅智能制造网 >  >> Industrial programming >> 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 的 list 类

我们以广为人知的 Python 列表实现为蓝本。我们的 VHDL 动态字符串列表将模仿 Python 内置 list 类的行为,采用其 append()、insert() 和 pop() 方法。

我将直接打开交互式 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") # 插入到倒数第二个位置 

遍历可见「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) # 从最后位置弹出 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()、insert() 和 clear() 过程与 Python 对应方法一致,但无法直接将 pop() 移植到 VHDL。问题在于难以从保护类型传出动态对象。

为克服此限制,我将 pop() 拆分为两个子程序:get() 和 delete()。这样可先像数组一样索引元素,打印到仿真控制台后再删除。

length() 非纯函数类似 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_ptr 时引用了 item。

最后给出 item 完整声明,包含字符串指针和指向下一元素的指针。item->item_ptr->item 存在循环依赖,先声明不完整 item 类型可避免编译错误。

保护类型包含两个变量:root 和 length_i。root 指向的项为列表第一个元素(数组索引 0),length_i 始终反映列表中的字符串数量。

 variable root : item_ptr; variable length_i : integer := 0; 

Append 过程

下面的 append() 过程是将字符串插入列表最后位置的简写:

 procedure append(str : string) is begin insert(length_i, str); end procedure; 

如 Python 示例所述,用索引 -1 易于插入到倒数第二个位置:insert(-1, str)。但插入到最后位置需要列表长度作为索引参数。这可能是 Python 列表设有专门 append() 方法的原因,我们也将提供。

Insert 过程

insert 过程分四步:

第一步,用 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 函数

得益于私有 get_node() 函数,公共 get() 函数变得很简单。它是单行代码:获取正确节点,解包字符串内容并返回给调用者。

 impure function get(index : integer) return string is begin return get_node(index).str.all; end function; 

Delete 过程

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 过程

clear() 过程用 While 循环遍历列表,对每个元素调用 delete() 直到列表为空。

 procedure clear is begin while length_i > 0 loop delete(0); end loop; end procedure; 

Length 函数

遵循良好编程实践,提供 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; ... 

保护类型是 VHDL 的类式构造,通过声明 string_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 示例。代替 Python 的 pop() 方法,我们用 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

  1. 基于 Tcl 驱动的 VHDL 密码锁模块测试平台
  2. 优雅结束 VHDL 测试平台仿真
  3. VHDL 实现 PWM 控制器:设计、仿真与 FPGA 演示
  4. VHDL 生成随机数:从均匀分布到 OSVVM
  5. 在块RAM中设计健壮的VHDL环形缓冲区FIFO
  6. 构建VHDL自检测试平台实战指南
  7. 用VHDL受保护类型和访问指针实现动态链表
  8. 利用进程内过程简化VHDL状态机设计
  9. 使用VHDL不纯函数提升FSM可读性与可维护性
  10. 掌握VHDL函数:高效设计实战指南
  11. 用VHDL设计有限状态机:红绿灯实战范例
  12. 在VHDL中使用过程:用可复用代码简化设计