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의 리스트 클래스를 본떠 동적 문자열 리스트 패키지를 만들기로 했습니다. 이 글에서 설명하겠습니다.
아래 양식으로 전체 코드를 다운로드하세요.
Python의 리스트 클래스
잘 알려진 리스트 구현을 모델로 삼겠습니다. 우리 VHDL 문자열 리스트는 Python 내장 리스트 클래스의 동작을 모방합니다. Python 리스트의 append(), insert(), pop() 메서드를 채택합니다.
먼저 대화형 Python 쉘을 열어 실험해 보겠습니다.
리스트를 선언하고 네 문자열을 추가합니다:
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
삽입에도 음수 인덱스 사용 가능합니다. 아래 예제 마지막 줄에서 "Copenhagen"을 인덱스 -1에 삽입합니다:
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은 현재 리스트의 마지막 요소 위치를 가리킵니다. 팝할 땐 마지막 요소를 요청하는 것이고, 삽입할 땐 현재 마지막 요소 위치에 새 항목을 넣으라는 뜻이므로 마지막 요소가 한 칸 밀려납니다.
l.pop(-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 서브프로그램 프로토타입
문자열 리스트를 멤버 메서드를 가진 객체처럼 다루려면 보호 타입(protected type)으로 선언해야 합니다. 패키지 이름도 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에서 연결 리스트 만들기
아래 모든 코드는 보호 타입 본문에 있으므로 외부에서 직접 접근 불가합니다. 모든 통신은 앞서 본 선언부의 서브프로그램을 통해야 합니다.
데이터 저장 타입과 변수
아래 코드에서 볼 수 있듯, 먼저 접근 타입(access type)을 선언합니다. 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 타입을 먼저 선언해 컴파일 오류를 피합니다.
보호 타입엔 두 변수가 있습니다:
variable root : item_ptr; variable length_i : integer := 0;
root가 가리키는 항목이 리스트의 첫 번째 요소(배열 인덱스 0)입니다. length_i 변수는 항상 리스트 내 문자열 수를 반영합니다.
Append 프로시저
아래 append() 프로시저는 리스트 마지막 위치에 문자열을 삽입하는 축약 표기입니다.
procedure append(str : string) is begin insert(length_i, str); end procedure;
Python 예제에서 논의했듯, 인덱스 -1로 마지막에서 두 번째 위치에 삽입하기 쉽습니다: insert(-1, str). 하지만 마지막 위치에 삽입하려면 리스트 길이를 인덱스 인자로 써야 합니다. 그래서 Python 리스트에 전용 append() 메서드가 있고, 우리도 하나 만듭니다.
Insert 프로시저
아래 insert 프로시저는 네 단계로 작동합니다.
1단계: 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;
2단계: 인덱스 인자를 리스트 범위에 맞게 변환. Python의 list.insert()는 범위 밖 인덱스를 허용하며, 우리 VHDL 리스트도 허용합니다. 사용자가 너무 높거나 낮은 인덱스를 참조하면 최고 인덱스나 요소 0으로 기본 설정. 모듈로 연산으로 범위 내 음수 인덱스를 양수 배열 위치로 변환.
3단계: 리스트를 순회하며 삽입 후 노드 찾기. 연결 리스트에서 늘 그렇듯 루트 위치 삽입의 특수한 경우를 명시적으로 처리.
4단계: 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 함수
좋은 프로그래밍 관행 준수 위해 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 타입의 공유 변수 선언으로 객체 생성(위 코드 마지막 줄). Python 예제 따라 '리스트' 의미로 'l'이라 명명.
이제 소프트웨어 방식처럼 리스트 데이터 접근 가능. 아래 테스트벤치 프로세스에서 보듯, 공유 변수에 점 표기법으로 공개 서브프로그램 참조(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에서 연결 리스트 만들기
아래 댓글로 의견 남겨주세요!