모듈 설계 중 AXI 인터페이스 처음 접했을 때 단순화된 플로우 제어 방식에 놀랐습니다. 기존 프로토콜들이 busy/valid, full/valid, empty/valid 쌍에 의존하는 것과 달리 AXI는 ready와 valid 두 신호만 사용합니다. 좌절로 시작된 것이 곧 매혹으로 바뀌었습니다.
AXI는 플로우 제어를 핸드셰이크에 직접 내장해 추가 상태 라인 불필요. 기본 규칙은 단순하지만 FPGA 하드웨어에 프로토콜 매핑 시 세심한 주의 필요. 이 튜토리얼에서 블록 RAM에서 동작하는 AXI FIFO의 VHDL 구현을 안내합니다.
AXI가 한 사이클 지연 문제 해결
두 클럭 동기화 모듈이 데이터 스트림 교환 시 과도 읽기·쓰기 방지는 공통 과제. 핵심은 각 모듈이 상대방 출력을 한 클럭 사이클 후에만 관찰 가능하다는 점.
위 다이어그램은 클래식 라이트 인에이블/풀 시그널링 쓰는 FIFO에 시퀀셜 모듈 쓰는 상황. 작성자가 wr_en 어설트해 데이터 FIFO에 푸시, FIFO는 공간 없으면 full 어설트. 문제는 full이 클럭 라이징 에지에서 정확히 어설트되므로 작성자가 같은 사이클에 반응 불가. 해결책은 almost_empty 같은 룩어헤드 신호로 업스트림 로직에 선행 시간 제공.
Ready/Valid 핸드셰이크
AXI 스트림 인터페이스는 두 신호 핸드셰이크로 이 타이밍 딜레마 해결: ready와 valid. 수신측이 ready 구동; 하이 값은 새 데이터 수용 준비 완료 의미. 송신측이 valid 구동; 하이 값은 버스 상 데이터 안정적이라 샘플링 가능 의미. 전송은 두 신호가 같은 라이징 에지에서 하이일 때만 발생.
파형은 단일 데이터 항목 전송 시연. 데이터는 통상 동기 설계처럼 라이징 클럭 에지에서 샘플링.
구현
AXI FIFO를 VHDL로 구축하는 방법 여러 가지. 시프트 레지스터도 가능하지만 링 버퍼에 블록 RAM 백업이 가장 효율적이고 확장성 좋음. 아래서 로직을 전용 프로세스들로 분리해 명확성과 합성기 친화성 확보한 모듈러 설계 제시.
엔티티
엔티티는 두 제네릭 파라미터 ram_width와 ram_depth 정의로 워드 폭과 FIFO 깊이 설정. 용량은 사실상 ram_depth - 1인데, 풀 FIFO와 빈 FIFO 구별 위해 슬롯 하나 예약.
entity axi_fifo is generic ( ram_width : natural; ram_depth : natural ); port ( clk : in std_logic; rst : in std_logic; -- AXI input interface in_ready : out std_logic; in_valid : in std_logic; in_data : in std_logic_vector(ram_width - 1 downto 0); -- AXI output interface out_ready : in std_logic; out_valid : out std_logic; out_data : out std_logic_vector(ram_width - 1 downto 0) ); end axi_fifo;
인터페이스는 AXI 관례 따름: 입력 측 in_ready, in_valid 사용, 출력 측은 방향 반전된 신호들 미러링. 이 대칭성으로 다중 FIFO 체인 연결 용이.
신호 선언
먼저 제네릭 폭과 깊이에 맞는 RAM 타입 정의 후 배열 인스턴스화:
-- The FIFO is full when the RAM contains ram_depth - 1 elements type ram_type is array (0 to ram_depth - 1) of std_logic_vector(in_data'range); signal ram : ram_type;
RAM 내 아무 주소나 표현 가능한 인덱스 서브타입 선언, 네 핵심 신호: head, tail, count, 지연 복사본 count_p1:
subtype index_type is natural range ram_type'range; signal head : index_type; signal tail : index_type; signal count : index_type; signal count_p1 : index_type;
아키텍처 내부에서 out 신호 읽기 피하려 ready/valid 출력의 내부 버전도 유지:
signal in_ready_i : std_logic; signal out_valid_i : std_logic;
헬퍼 신호로 이전 사이클에 동시 읽기·쓰기 발생 추적, FIFO가 거의 빈 상태일 때 out_valid 올바르게 설정 위해 필수:
signal read_while_write_p1 : std_logic;
서브프로그램
next_index 함수는 읽기 또는 쓰기 핸드셰이크 완료 여부에 따라 다음 주소 결정. 배열 끝 도달 시 랩어라운드.
function next_index( index : index_type; ready : std_logic; valid : std_logic) return index_type is begin if ready = '1' and valid = '1' then if index = index_type'high then return index_type'low; else return index + 1; end if; end if; return index; end function;
index_proc 프로시저가 head 또는 tail 포인터 업데이트하는 동기 로직 캡슐화. 중복 코드 방지, 아키텍처 깔끔 유지.
procedure index_proc( signal clk : in std_logic; signal rst : in std_logic; signal index : inout index_type; signal ready : in std_logic; signal valid : in std_logic) is begin if rising_edge(clk) then if rst = '1' then index <= index_type'low; else index <= next_index(index, ready, valid); end if; end if; end procedure;
내부 신호를 출력에 복사
in_ready <= in_ready_i; out_valid <= out_valid_i;
Head와 Tail 업데이트
헬퍼 프로시저 두 번 인스턴스화: 쓰기 포인터(head)용 한 번, 읽기 포인터(tail)용 한 번:
대부분 FPGA 블록 RAM은 동기식. 합성기가 RAM 프리미티브 추론하게 하려면 읽기·쓰기 연산 클럭 프로세스 내에 두고 명시적 인에이블 신호 피함:
PROC_RAM : process(clk) begin if rising_edge(clk) then ram(head) <= in_data; out_data <= ram(next_index(tail, out_ready, out_valid_i)); end if; end process;
쓰기는 in_valid 하이인 사이클에 무조건 발생, 읽기 데이터는 매 사이클 업데이트되어 컨슈머가 FIFO에 데이터 있을 때 유효 워드 관찰 가능.
FIFO 내 요소 수 카운트
count 시그널이 현재 점유율 반영. head가 tail보다 앞서면 카운트는 단순 차이, 헤드 랩어라운드면 전체 깊이 더함:
PROC_COUNT : process(head, tail) begin if head < tail then count <= head - tail + ram_depth; else count <= head - tail; end if; end process;
지연 복사본 count_p1은 이전 사이클에 FIFO가 비었었는지 감지해 out_valid 영향 주기 위해 필요:
PROC_COUNT_P1 : process(clk) begin if rising_edge(clk) then if rst = '1' then count_p1 <= 0; else count_p1 <= count; end if; end if; end process;
Ready 출력 업데이트
작성자에게 FIFO가 더 많은 데이터 수용 가능할 때 알림. 카운트가 ram_depth - 1 미만일 때 ready 신호 하이:
PROC_IN_READY : process(count) begin if count < ram_depth - 1 then in_ready_i <= '1'; else in_ready_i <= '0'; end if; end process;
동시 읽기·쓰기 감지
같은 사이클에 읽기와 쓰기 모두 발생하면 FIFO 점유율 안 변하지만 내부 상태는 FIFO가 거의 빈 상태일 때 값이 두 사이클 후에야 사용 가능해짐 반영해야 함. 이 이벤트 read_while_write_p1에 캡처:
PROC_READ_WHILE_WRITE_P1: process(clk) begin if rising_edge(clk) then if rst = '1' then read_while_write_p1 <= '0'; else read_while_write_p1 <= '0'; if in_ready_i = '1' and in_valid = '1' and out_ready = '1' and out_valid_i = '1' then read_while_write_p1 <= '1'; end if; end if; end if; end process;
Valid 출력 업데이트
out_valid 신호가 하위 로직에 out_data가 사용 가능 워드 보유 중일 때 알림. 아래 조합 프로세스는 기본적으로 하이 설정하지만 두 코너 케이스에서 클리어:
PROC_OUT_VALID : process(count, count_p1, read_while_write_p1) begin out_valid_i <= '1'; -- If the FIFO is empty or was empty in the previous cycle if count = 0 or count_p1 = 0 then out_valid_i <= '0'; end if; -- If a simultaneous read/write occurred when the FIFO had only one item if count = 1 and read_while_write_p1 = '1' then out_valid_i <= '0'; end if; end process;
첫 조건으로 첫 쓰기 후 전파 지연 동안 out_valid 로우 유지 보장. 두 번째 조건으로 FIFO가 한 사이클 동안 풀인 듯 보여도 실제 출력할 데이터 없음 상황 방지.
Vivado에서 합성
Xilinx Vivado에서 이 설계 합성하려면 제네릭에 구체적 값 제공. 설정 대화상자(General → Generics/Parameters)에서 타겟 Zynq 디바이스용 RAMB36E1 프리미티브 매칭 깊이 선택. 구현 후 리소스 사용량 적당 - 블록 RAM 1개 + 약간의 LUT와 FF.
AXI는 ready/valid 이상
AXI(Advanced eXtensible Interface)는 ARM AMBA 버스 아키텍처 구성요소로 단순 ready/valid 핸드셰이크 넘어 풍부한 기능 세트 제공. 깊은 이해 위해 다음 자료 참고: