
개발 환경: Vivado 2024.2 / Zybo Z7-20 (xc7z020clg400-1) / Ubuntu
대상 IP: fir_decimator_n43_axis (N=43 transposed FIR + M=2 데시메이터, 16-bit AXI-Stream)
입력 8192 샘플 → 출력 4096 샘플
시뮬레이션 골든은 4117샘플이다. 테스트벤치는 마지막 입력 뒤에도 0을 밀어 넣어 파이프라인에 남은 42샘플까지 전부 flush하므로 FIR 출력이 8192+43−1 = 8234, 여기서 M=2 데시메이션하면 4117이 된다. 보드는 DMA가 8192샘플만 밀어 넣고 끝내므로 테일이 나오지 않고 정확히 8192/2 = 4096이다. 이 글부터는 보드 기준 4096을 쓴다.
① PS(C코드): 멀티톤 신호 합성
↓
② PS: DDR에 입력 배열 저장
↓
③ PS: DMA에 전송 명령 (시작 주소, 길이)
↓
④ DMA MM2S: DDR → AXI-Stream → FIR S_AXIS (입력)
↓
⑤ FIR IP: 필터링 + 데시메이션 (M=2)
↓
⑥ DMA S2MM: FIR M_AXIS → AXI-Stream → DDR (출력 배열 저장)
↓
⑦ PS(C코드): DDR에서 결과 읽기 → UART 출력
③~⑥ 구간에서 PS는 명령만 내리고, 실제 데이터 이동은 DMA가 CPU 개입 없이 처리한다. 이것이 DMA를 쓰는 이유다.
DMA는 채널이 2개
bd_fir_dma: custom fir ip와 PS ip, DMA ip를 붙인 block design
Block Design: bd_fir_dma
├── ZYNQ7 Processing System ← IP 1
├── AXI DMA ← IP 2
└── fir_decimator_n43_axis ← IP 3 (우리 것)
| # | IP 이름 | 출처 | 역할 |
|---|---|---|---|
| 1 | ZYNQ7 Processing System | Xilinx 기본 제공 | ARM 코어 + DDR + HP0 포트 |
| 2 | AXI Direct Memory Access | Xilinx 기본 제공 | DDR ↔ AXI-Stream 브리지 |
| 3 | fir_decimator_n43_axis | 우리가 만든 RTL | FIR 필터 + 데시메이터 |
Vivado가 "Run Connection Automation" 시 자동으로 추가하는 IP 3개:
| # | IP 이름 | 역할 |
|---|---|---|
| 4 | Processor System Reset | PS 리셋 신호 동기화 |
| 5 | AXI SmartConnect | PS GP0 → DMA AXI-Lite 연결 중계 |
| 6 | AXI Interconnect | DMA MM2S/S2MM → PS HP0 메모리 경로 병합 |
GUI에서 IP들을 배치하고 연결하는 것:
fir_decimator_n43_axis) — Module Reference로 삽입연결:
AXI DMA MM2S → (AXI-Stream) → FIR S_AXIS (입력)
AXI DMA S2MM ← (AXI-Stream) ← FIR M_AXIS (출력)
AXI DMA ↔ PS7 HP0 (DDR 직접 접근)
Block Design → Synthesize → Implement → Generate Bitstream → Export Hardware (.xsa)
진행 순서
1. 새 프로젝트 생성 (Board: Zybo Z7-20)
2. Create Block Design
3. IP 추가: ZYNQ7 Processing System
4. IP 추가: AXI DMA
5. IP 추가: 우리 FIR (Module Reference)
6. Create HDL Wrapper
7. Generate Bitstream
8. Export Hardware
Create Project 클릭fir_decimator_trans_n43원하는 경로 설정
rtl/transposed_form/n43/fir_decimator_n43_axis.v ← 최상위 toprtl/transposed_form/decimator_m2_phase0.vrtl/transposed_form/n43/fir_decimator_n43.vrtl/transposed_form/n43/fir_n43.v

Zybo Z7-20 선택

Vivado 좌측 Flow Navigator에서:
IP INTEGRATOR → Create Block Design 클릭bd_fir_dma
Add IP → 검색창에 zynq 입력 → ZYNQ7 Processing System 더블클릭

1. 포트 활성화
PS-PL Configuration → HP Slave AXI Interface → S AXI HP0 Interface 체크

2. 클럭 확인
Clock Configuration → PL Fabric Clocks → FCLK_CLK0 가 100MHz인지 확인

Add IP → 검색창에 axi dma 입력 → AXI Direct Memory Access 더블클릭
블록 더블클릭 이후 확인/수정할 항목 확인

상단
| 항목 | 값 | 설명 |
|---|---|---|
| Enable Scatter Gather Engine | 해제 ✓ | Simple DMA 모드. 연속 배열 하나를 옮기는 구조라 시작 주소와 길이만 넘기는 Simple DMA로 충분 |
| Enable Micro DMA | 해제 | 기능 제한된 경량 DMA. 해당 없음 |
| Enable Multi Channel Support | 비활성 | Scatter Gather 전용. 해당 없음 |
| Enable Control/Status Stream | 비활성 | Scatter Gather 전용. 해당 없음 |
| Width of Buffer Length Register | 14 bits | 최대 전송 크기 2¹⁴ -1 = 16383 bytes. 출력 4117샘플 × 2bytes ≈ 8KB이므로 충분 |
| Address Width | 32 bits | Zynq-7000은 32비트 주소 체계. 맞음 |
⚠️ 이 판단이 나중에 MM2S timeout의 원인이 된다. 여기서 내가 계산에 쓴 건 출력 쪽 길이였다. 그런데 이 설정은 채널별이 아니라 MM2S와 S2MM에 함께 적용되는 파라미터다. 실제 제약은 둘 중 큰 쪽이 건다.
S2MM (출력) : 4096샘플 × 2B = 8192 bytes → 16383 이내, 문제 없음
MM2S (입력) : 8192샘플 × 2B = 16384 bytes → 16383을 정확히 1 byte 초과
FIR는 M=2 데시메이터라 입력이 출력의 두 배인데, 하필 여유를 확인한 쪽이 작은 출력이었다.
이 시점의 나는 몰랐고, 보드를 올린 뒤에야 알게 된다. 상세는 3편.
Read Channel (MM2S) — DDR → FIR 방향
| 항목 | 값 | 설명 |
|---|---|---|
| Number of Channels | 1 | 채널 하나면 충분 |
| Memory Map Data Width | 32 | DDR(HP0) 쪽 버스 폭. 32비트 표준 |
| Stream Data Width | 16 ✓ | FIR s_axis_tdata 폭과 일치 |
| Max Burst Size | 16 | DDR 읽기 시 한 번에 16 beat. 기본값으로 충분 |
| Allow Unaligned Transfers | 해제 | 16비트 샘플은 정렬 보장됨 |
Write Channel (S2MM) — FIR → DDR 방향
| 항목 | 값 | 설명 |
|---|---|---|
| Number of Channels | 1 | 채널 하나면 충분 |
| Memory Map Data Width | 32 (AUTO) | DDR 쪽 버스 폭. 동일 |
| Stream Data Width | 16 (MANUAL) ✓ | FIR m_axis_tdata 폭과 일치 |
| Max Burst Size | 16 | DDR 쓰기 시 한 번에 16 beat |
| Allow Unaligned Transfers | 해제 | 정렬 보장됨 |
하단
| 항목 | 값 | 설명 |
|---|---|---|
| Enable Single AXI4 Data Interface | AUTO/해제 | MM2S와 S2MM이 DDR 포트를 각각 따로 씀. 동시 읽기+쓰기 가능 |
커스텀 RTL을 BD에 넣는 방법은 IP Packaging과 Module Reference 두 가지다. IP Packaging 방식은 재사용할 때 이득이 있다. 해당 FIR IP는 이 Block Design 하나에서만 쓰이고 AXI-Stream 인터페이스도 이미 RTL에 있어서, Module Reference 방식을 사용했다.
Add Module → fir_decimator_n43_axis 선택
캔버스 상단 초록색 배너 "Run Connection Automation" 클릭

모든 항목 체크 : Vivado가 SmartConnect, Processor System Reset, AXI Interconnect를 자동 생성하고 연결한다.


| 항목 | 연결되는 경로 | 설명 |
|---|---|---|
axi_dma_0 → S_AXI_LITE | PS GP0 → SmartConnect → DMA S_AXI_LITE | PS가 DMA 레지스터에 명령을 쓰는 제어 경로 |
processing_system7_0 → S_AXI_HP0 | DMA M_AXI_MM2S/S2MM → PS HP0 | DMA가 DDR을 읽고 쓰는 데이터 경로 |
FIR의 AXI-Stream 포트(s_axis, m_axis)는 이 목록에 나오지 않는다. Vivado가 DMA의 어느 채널을 FIR의 어느 포트에 연결할지 판단하지 못하기 때문이다. 아래의 7번에서 수동 연결한다.
Validate Design에서 이런 에러가 났다.
[BD 41-758] /axi_dma_0/m_axi_s2mm_aclk is not connected to a valid clock source
DMA의 클럭 포트는 s_axi_lite_aclk, m_axi_mm2s_aclk, m_axi_s2mm_aclk 세 개인데, Run Connection Automation이 이걸 전부 연결해주지는 않는다. 셋 다 연결돼 있는지 확인하고, 빠진 것은 PS의 FCLK_CLK0에 수동으로 연결한다.
"Run Connection Automation"으로 자동 생성된 axi_mem_intercon(AXI Interconnect) 더블클릭
DMA는 DDR 쪽으로 M_AXI_MM2S(읽기)와 M_AXI_S2MM(쓰기) 두 개의 마스터 포트를 낸다.
이 둘이 PS의 HP0 하나로 들어가야 하므로 Interconnect의 slave interface가 2개 필요하다.
MM2S/S2MM을 한 번에 자동 연결했다면 Vivado가 이미 2로 잡아준다.

포트 연결 요약:
PS GP0 → SmartConnect → DMA S_AXI_LITE (PS가 DMA 제어)
DMA M_AXI_MM2S → PS HP0 (DMA가 DDR 읽기)
DMA M_AXI_S2MM → PS HP0 (DMA가 DDR 쓰기)
DMA M_AXIS_MM2S → FIR S_AXIS (DDR → FIR)
FIR M_AXIS → DMA S_AXIS_S2MM (FIR → DDR)
1. AXI-Stream은 번들을 통째로 연결한다.
BD에서 AXI-Stream 포트는 wire 묶음이 아니라 인터페이스로 관리된다. 펼쳐서 tdata/tvalid/tready/tlast를 하나씩 연결하면 Vivado가 AXI-Stream 프로토콜 연결로 인식하지 못하고, DMA도 S_AXIS_S2MM 인터페이스 단위로만 인식해서 핸드셰이크가 보장되지 않는다.
2. 빈 캔버스에 drop하면 외부 포트가 생긴다.
대상 포트 위가 아닌 곳에 놓으면 Vivado가 Make External을 실행해 FPGA 핀으로 노출시키고, 비트스트림 단계에서 [DRC NSTD-1] 39 out of 39 logical ports use I/O standard 'DEFAULT'로 터진다. get_bd_ports *로 확인·삭제하고 초록 체크가 뜰 때 놓아 재연결한다.
첫 번째: aclk 연결
FIR 블록의 aclk 포트 → PS 블록의 FCLK_CLK0 포트로 드래그

두 번째: aresetn 연결
FIR 블록의 aresetn 포트 → rst_ps7_0_100M 블록의 peripheral_aresetn 포트로 드래그

aresetn : FIR IP의 active-low 리셋 포트
세 번째: DMA → FIR 입력 연결
axi_dma_0 블록의 M_AXIS_MM2S 포트 → FIR 블록의 s_axis 포트로 드래그

네 번째: FIR → DMA 출력 연결
FIR 블록의 m_axis 포트 → axi_dma_0 블록의 S_AXIS_S2MM 포트로 드래그

다섯 번째: Run Block Automation 클릭
상단 녹색 배너의 Run Block Automation 클릭
※ Block Automation은 PS7 IP를 추가한 직후(3번)에 실행하는 게 일반적이다. DDR / FIXED_IO를 외부 포트로 빼주는 단계라 언제 눌러도 결과는 같지만, 따라 하는 입장이라면 PS 추가 직후에 눌러도 된다. 나는 배선을 다 한 뒤에 눌렀다.


여섯 번째: 검증하기
단축키 F6를 눌러 Validate Design 진행

Digilent 보드 파일을 쓰는 Zybo 설계에서는 흔히 뜨는 경고라고 한다.
다만 넘기기 전에 DDR 파트는 확인해야 한다. 이 시점의 BD에는 PS7 DDR 파트가 Zybo Z7-20이 아니라 Z7-10용 파트로 들어가 있었다. 보드 파일이 제대로 적용되지 않은 채로 BD가 만들어졌기 때문이고, 그대로 아래 11번에서 TCL로 추출까지 됐다. ps7_init은 DDR 파트를 기준으로 CAS latency, tRCD, tRP 같은 타이밍 레지스터를 설정하므로, 파트가 틀리면 DDR이 불안정해지고 DMA 전송 실패로 이어질 수 있다.
get_property CONFIG.PCW_UIPARAM_DDR_PARTNO [get_bd_cells /processing_system7_0]
# MT41K256M16 RE-125 → 정상 (Z7-20)
# MT41J128M8 JP-125 → 잘못됨 (Z7-10). BD 상단 녹색 배너에서 보드 파일 재적용
Sources 탭에서 Block Design 파일(bd_fir_dma) 우클릭 → Create HDL Wrapper → Let Vivado manage wrapper and auto-update 선택

Synthesis → Implementation → Generate Bitstream
로그 첫 줄: synth_design -top fir_decimator_n43_axis
두 가지 문제가 동시에 존재했다.
문제 1: 합성 Top 모듈 오설정
합성 Top 모듈이 bd_fir_dma_wrapper가 아닌 fir_decimator_n43_axis로 설정되어 있었다.
이는 Block Design 생성 이전에 FIR IP 단독 검증을 위해 fir_decimator_n43_axis를 Top으로 설정해 두었던 상태가 그대로 남아 있었기 때문이다. Block Design Wrapper가 아닌 FIR 모듈 단독으로 합성을 시도하니 DMA, PS 등 나머지 연결이 전혀 없는 불완전한 설계가 합성 대상이 되었다.
→ Sources 패널에서 bd_fir_dma_wrapper를 우클릭 → Set as Top으로 변경
문제 2: 불필요한 XDC 파일 활성화
로그에서 zybo_n43.xdc가 포함되어 있었다. 이 XDC 파일은 FIR IP를 단독으로 합성할 때 사용하던 핀 제약 파일로, fir_decimator_n43_axis의 포트를 물리 핀에 직접 매핑하는 내용이 담겨있었다. Block Design 기반 설계에서는 FIR 포트가 내부적으로 DMA에 연결되므로 물리 핀 매핑이 필요 없는데, 이 XDC가 여전히 활성화되어 있어 잘못된 핀 제약이 적용되고 있었다.
→ Sources 패널 → Constraints 폴더 → zybo_n43.xdc 우클릭 → Disable File
두 문제를 모두 수정한 뒤 다시 시도했다.
드디어 성공

IMPLEMENTATION → Open Implemented Design → Report Timing Summary

Block Design을 만들기 전, FIR 코어만 단독으로 합성해 100MHz 타이밍 클로저를 확인했을 때는 WNS가 +0.278ns였다. 여유가 더 커진 건 조건이 달라서다. 단독 합성 때는 FIR 포트가 zybo_n43.xdc로 물리 핀에 묶여 I/O 경로가 크리티컬 패스에 들어갔지만, Block Design에서는 같은 포트가 DMA로 가는 내부 연결이라 그 경로가 사라진다.
File → Export → Export Hardware → Include bitstream 체크
지정한 디렉토리에 xsa 파일을 export하는 과정입니다.
XSA (Xilinx Support Archive):
Vivado가 만드는 하드웨어 설명 패키지 파일


Block Design은 Vivado 프로젝트 파일(.xpr) 안에만 존재합니다. 프로젝트가 유실되거나 다른 환경에서 재현해야 할 때를 대비해 write_bd_tcl로 Block Design 전체를 TCL 파일로 추출해 레포에 보관합니다.
Block Design이 열린 상태에서 Vivado TCL Console에서 실행:
write_bd_tcl -force <repo>/vivado/bd_fir_dma.tcl
추출 결과: <repo>/vivado 디렉토리안에 bd_fir_dma.tcl 스크립트 저장
현재 레포에서는 이 파일이 vivado/fir_n43/bd_fir_dma.tcl로 옮겨져 있고, 2편에서 수정하는 c_sg_length_width {23}과 위의 DDR 파트 수정(MT41K256M16 RE-125)이 이미 반영돼 있다. 지금 레포의 TCL은 이 글 시점의 파일이 아니다.
write_bd_tcl이 추출하는 정보:
assign_bd_address)validate_bd_design 자동 호출다른 머신에서 Block Design을 재현하려면:
source vivado/fir_n43/bd_fir_dma.tcl
RTL 소스 없이 sourcing하면 fir_decimator_n43_axis 모듈을 찾지 못해 실패한다.
지금 레포에는 프로젝트 생성부터 BD 구성, 비트스트림, XSA까지 한 번에 도는 배치 스크립트도 있다. 보드 파일도 레포 boards/에 포함돼 별도 설치가 필요 없다.
vivado -mode batch -source vivado/fir_n43/build_bd_fir_dma.tcl
| # | 증상 | 원인 | 해결 |
|---|---|---|---|
| 1 | [BD 41-758] m_axi_s2mm_aclk is not connected to a valid clock source | Run Connection Automation이 DMA 클럭 포트를 전부 연결하지 않음 | s_axi_lite_aclk / m_axi_mm2s_aclk / m_axi_s2mm_aclk를 FCLK_CLK0에 수동 연결 |
| 2 | [DRC NSTD-1] 39 out of 39 logical ports use I/O standard 'DEFAULT' | 포트를 빈 캔버스에 drop → Vivado가 Make External 실행, AXI-Stream 포트가 FPGA 물리 핀으로 노출 | get_bd_ports *로 확인 후 외부 포트 삭제, 대상 포트 위에서 초록 체크가 뜰 때 재연결 |
| 3 | 비트스트림 생성 실패, 로그에 synth_design -top fir_decimator_n43_axis | 합성 Top이 bd_fir_dma_wrapper가 아니고, 단독 합성용 zybo_n43.xdc가 활성화되어 잘못된 핀 제약 적용 | Top을 wrapper로 변경 + XDC Disable File |
| 4 | (이후에 발견) PS7 DDR 파트가 MT41J128M8 JP-125 (Zybo Z7-10용) | BD 생성 시 보드 파일이 제대로 적용되지 않음 | 보드 파일 재적용 후 PCW_UIPARAM_DDR_PARTNO가 MT41K256M16 RE-125인지 확인 |
Validate Design 성공과 비트스트림 생성 성공은 다르다는 걸 이번 작업에서 체감했습니다. Vivado가 "연결에 문제없다"고 판정해도, 합성 Top이 잘못 설정되어 있거나 프로젝트 초기에 넣어둔 XDC 파일이 남아있으면 비트스트림 단계에서 막힙니다. 에러 메세지를 읽고 원인을 역추적하는 과정이 번거롭긴 했지만, 덕분에 Block Design의 외부 포트 개념과 Vivado 프로젝트 상태 관리 방식을 제대로 이해하게 됐습니다. 다음 단계는 생성된 .xsa를 Vitis에서 열어 DMA를 제어하는 C 코드를 작성하는 것입니다. 코드는 금방 나왔는데, 정작 그걸 보드에 올리는 데서 16일이 갔습니다. JTAG로 ELF를 DDR에 올리면 32비트 워드의 상위 1바이트가 계속 오염됐고, 끝내 원인을 못 밝힌 채 부팅 경로를 바꿔야 했습니다. 다음 글은 그 이야기입니다.