
1편에서 Block Design을 만들고 비트스트림과 .xsa까지 뽑았다. 이제 Vitis에서 C 코드를 짜서 보드에 올리면 끝날 줄 알았다.
보드에 올리는 것부터 막혔다. 16일이 걸렸고, 끝내 원인을 못 밝혔다.
| 항목 | 내용 |
|---|---|
| 증상 | JTAG로 ELF를 DDR에 올리면 32비트 워드의 상위 1바이트가 오염된다 |
| 기간 | 05-08 ~ 05-24 (16일) |
| 결말 | 완전 해결 실패. JTAG를 버리고 SD 부팅으로 우회 |
원인을 못 밝힌 이야기를 굳이 쓰는 이유는, 그 과정에서 배운 게 원인 자체보다 많았기 때문이다. 특히 답을 이미 적어놓고도 못 찾은 경험이 그렇다.
xsdb는 Xilinx의 JTAG 디버거다. PC에서 JTAG 케이블을 통해 보드의 메모리를 직접 읽고 쓰거나, CPU를 세우고 실행시킬 수 있다. 이 글에서 계속 나오는 명령은 여섯 개다.
| 명령 | 하는 일 |
|---|---|
fpga | 비트스트림을 PL에 프로그래밍 (PS는 안 건드림) |
dow | ELF를 메모리에 다운로드 |
dow -data <bin> <addr> | 바이너리를 특정 주소에 그대로 적재 |
mwr / mrd | 메모리 워드 단위 쓰기 / 읽기 |
con / stop | 실행 / 정지 |
rst | 리셋 |
dow로 ELF를 올리려 하자 첫 에러가 이거였다.
Memory write error at 0x100000. Cannot access DDR: the controller is held in reset.
fpga 명령은 PL만 프로그래밍하고 PS는 건드리지 않는다. SD 부팅이라면 FSBL이 알아서 DDR 컨트롤러를 깨우지만, JTAG 직접 부팅에는 FSBL이 없다. 그러니 DDR은 리셋 상태 그대로다.
비트스트림을 올린 뒤, CPU 리셋 다음에 ps7_init을 직접 돌려야 한다.
targets -set -filter {name =~ "*A9*#0"}
rst -processor -clear-registers
source $PS7_INIT
ps7_init
ps7_post_config
이걸 통과하고 나서 진짜 문제가 시작됐다.
xsdb로 비트스트림을 올리고 ELF를 DDR(0x100000)에 다운로드한 뒤 읽어보면 이랬다.
mwr 0x100000 0x11111111
mwr 0x100004 0x22222222
mwr 0x100008 0x33333333
mwr 0x10000C 0x44444444
mrd 0x100000 4
# 100000: F2111111 ← byte[3]=F2, 비정상
# 100004: F2222222 ← byte[3]=F2, 비정상
# 100008: F2333333 ← byte[3]=F2, 비정상
# 10000C: 44444444 ← 정상
하위 3바이트는 멀쩡한데 최상위 1바이트만 다른 값으로 덮인다. 그리고 마지막 워드만 정상이다.
ELF가 이렇게 깨지면 CPU는 부팅 즉시 죽는다. 이 ELF는 맨 앞에 예외 벡터 테이블이 놓이기 때문에, 0x100000부터 오염된 워드가 그대로 잘못된 명령어가 된다. 실제로 dow가 "성공"으로 끝난 뒤 con을 쳐도 UART에 한 글자도 안 나왔고, PC를 읽어보면 이랬다.
rrd pc
→ 0x00000010 # Data Abort 벡터
부팅한 게 아니라 예외 핸들러로 떨어진 것이다. 세션에 따라 SVC exception(PC=0x8)으로 죽기도 했다.
"마지막 워드만 정상"이라는 지문이 결정적이었다. 오염이 무작위였다면 마지막 워드라고 정상일 이유가 없다. 순서와 관련이 있다는 뜻이다.
명령을 주는 방식을 바꿔가며 관찰을 모았다.
| 명령을 주는 방식 | 결과 |
|---|---|
| 터미널에서 한 줄씩 손으로 타이핑 | 전부 정상 |
버스트 mwr {w0 w1 w2 w3} 한 번 | 마지막 워드만 정상 |
source script.tcl / dow | 전부 오염 |
이 셋을 동시에 설명하는 가장 단순한 가설은 이거였다.
xsdb의 JTAG AXI 마스터는
mwr실행 즉시 byte[0~2]를 전송하지만, byte[3]은 내부
파이프라인 버퍼에 hold한다. byte[3]이 commit되는 조건은 "xsdb가 stdin에서 다음 명령을
기다리며 블로킹할 때"다.
손으로 칠 때는 Enter를 누르기 전까지 xsdb가 stdin에서 멈춰 있으니 매 워드가 커밋된다.
버스트는 명령 하나라 끝에서 딱 한 번 블로킹하고, 그래서 마지막 워드만 제대로 쓰여진다. dow는 내부 bulk 전송이고 source는 파서가 파일에서 곧장 읽으므로, 워드 사이에 stdin 블로킹이 아예 없다.
Xilinx 문서에 이렇게 적혀 있는 건 아니다. 관찰 세 개를 동시에 설명하려고 내가 세운 동작 모델이다. 이 구분이 나중에 중요해진다.
그리고 이건 DDR도, ps7_init도, 보드도 아니라 xsdb 툴 레이어의 문제라는 뜻이었다.
xsdb한테 "이 주소에 이 값 써라"라고 시키면, 4바이트 중 앞의 3바이트는 바로 써진다.
그런데 맨 위 1바이트는 안 나가고 xsdb 안에 잠깐 hold되어 있다. 이 데이터는 xsdb가 할 일을 다 하고 다음 명령어를 기다릴 때 써진다.
그러니까 터미널에 한 줄 치고 엔터 누르고, 또 한 줄 치고 엔터 누르면 매번 xsdb가
명령어를 기다리느라 멈추니까 그때마다 마지막 1바이트가 잘 나간다. 그래서 손으로 치면 멀쩡했던 거다.
문제는 ELF를 올릴 때인데, 이건 워드가 만 개가 넘어서 손으로 칠 수 없기에 스크립트로 한 번에 입력하는데, 그러면 xsdb가 중간에 멈출 필요가 없다. 그래서 워드마다 맨 위 1바이트가 계속 안 나간 채로 밀리고, 다음 워드 쓸 때 그 자리에 이전 값이 남아 있는 게 보인다. 스크립트가 끝나면 그제서야 xsdb가 멈추니까 딱 하나, 제일 마지막 워드만 살아남는다.
mwr 뒤에 mrd를 붙이는 것이었다. mrd가 "다음 JTAG 명령" 역할을 하면서 직전 mwr의 byte[3]을 밀어낼 거라고 생각했다.
cmds.append(f"mwr 0x{addr:08X} 0x{word:08X}")
cmds.append(f"mrd 0x{addr:08X} 1") # JTAG write buffer flush 유도
ELF의 PT_LOAD 세그먼트를 워드 단위로 쪼개 이 쌍을 13,769개 생성했다.
그런데 해결이 되지 않았다.
sourced 모드에서는 mrd도 같은 실행 컨텍스트에서 연속 파싱될 뿐이라 stdin 블로킹이 생기지 않기 때문이다.
| 삽입한 것 | 결과 |
|---|---|
mrd 0x{same_addr} 1 | byte[3] 여전히 오염 |
after 1 (1 ms) | byte[3]이 데이터가 아니라 0x00으로 flush됨 |
after 0 | byte[3]뿐 아니라 byte[0~2]까지 전부 0x00 |
mrd 0xF8000000 1 (다른 주소) | sourced 모드 배치 처리라 효과 없음 |
after 1은 오히려 xsdb의 idle timeout을 트리거해서, byte[3]을 실제 데이터가 아닌 0x00으로 밀어내버렸다.
그래서 pexpect로 PTY를 에뮬레이션해 xsdb가 자신을 인터랙티브 터미널로 착각하게 만들었다.
xsdb = pexpect.spawn('xsdb', encoding='utf-8', timeout=60)
for addr, word in words:
xsdb.sendline(f"mwr 0x{addr:08X} 0x{word:08X}")
xsdb.expect('xsdb%') # ← 이 대기가 인터랙티브 REPL과 같은 barrier
xsdb.buffer = ''
이때는 성공했다고 기록했다. 나중에 이 기록이 틀렸다는 걸 알게 된다.
손으로 치면 정상이라는 걸 알았으니, 손으로 치는 걸 그대로 흉내내면 될 것이라고 생각했다.
1. — subprocess.PIPE로 붙였더니 초기화 단계에서 무한 대기.
xsdb가 자기가 비대화형 환경에 있다는 걸 감지하고 블록 버퍼링을 해버려서 프롬프트를 안 내보냈다. 프로그램은 자기 출력이 어디로 나가는지 확인하고 버퍼링 방식을 바꾼다. 터미널이면 사람이 보고 있다는 뜻이니 한 줄 나올 때마다 바로 내보내고, 파이프면 다른 프로그램이 받는다는 뜻이니 몇 KB 모일 때까지 쌓아뒀다가 한 번에 내보낸다. 후자가 훨씬 효율적이기 때문이다.
문제는 xsdb% 프롬프트가 짧다는 것이다. 몇 바이트짜리라 버퍼가 찰 리가 없다. Python은 xsdb%가 오기를 기다리고, xsdb는 버퍼가 찰 때까지 안 내보낸다. 둘 다 상대를 기다리며 영원히 멈춘다. 명령을 한 줄도 못 보내고 초기화 단계에서 죽은 이유가 이거였다.
→ pexpect로 가상 터미널(PTY)을 에뮬레이션해서 대화형 환경을 강제했다.
2. — 되긴 되는데, 너무 느렸다.
약 19 words/s × 13,000 words = 10분 이상
워드 하나마다 Python과 xsdb 사이에 문자열을 주고받고 컨텍스트 스위칭이 일어난다. 그 오버헤드가 실제 JTAG 전송 시간보다 수백 배 컸다. 진행률 숫자가 기어가는 걸 그냥 쳐다보고 있었다. 느리기만 한 것도 아니었다. 중간중간 프롬프트가 안 돌아와서 그대로 멈췄다. 결국 워드마다 타임아웃을 걸고 재시도하는 루프를 씌워야 했다.
for i, (addr, word) in enumerate(words):
while True:
try:
xsdb.sendline(f"mwr -force 0x{addr:08X} 0x{word:08X}")
xsdb.expect('xsdb%', timeout=30)
break
except pexpect.TIMEOUT:
print(f"\n[{i+1}] timeout, 재시도...", flush=True)
한 번 돌릴 때마다 10분이 넘는데 거기에 멈춤과 재시도가 얹혔다. 설정을 바꿔가며 몇 번 돌리다 보면 30분이 훌쩍 갔다.
→ 128워드씩 묶어서 보내는 chunking을 적용했다.
3. — chunking을 적용했더니, 14% 지점(2048워드)에서 전송이 멈췄다.
리눅스 커널의 PTY 입력 큐(4~8KB)가 가득 찼는데, xsdb 파서가 소화하는 속도가 파이썬이 타이핑하는 속도를 못 따라간다. 커널 레벨에서 막힌 것이다.
→ 텍스트 통신 자체를 줄이려고, 모든 mwr 명령을 담은 임시 .tcl 파일을 만들어 xsdb에서 source로 한 번에 실행하도록 구조를 바꿨다.
4. — .tcl 방식으로 바꿨더니, 이번엔 Cannot access DDR.
새로 짠 시퀀스에 들어 있던 rst -system이 Zynq PS 레지스터를 전부 초기화하면서 DDR 컨트롤러가 리셋 상태에 묶였다.
→ 메모리 접근 전에 ps7_init을 반드시 재실행하도록 시퀀스를 고쳤다.
여기까지 고치고 나서야 로딩이 끝까지 완주했다. 그런데 읽어보니 byte[3]은 여전히 오염돼 있었다.
하루를 꼬박 쓴 결과가 "처음 증상 그대로"였다.
그리고 이 4연속 에러에는 내가 그때 못 본 게 하나 있었다.
3번의 해결책이 원래 문제를 되살렸다는 것이다.
source script.tcl은 xsdb 파서가 파일 버퍼에서 명령을 곧장 읽는다. 그러면 워드 사이에 stdin 블로킹이 없다. 내가 그토록 흉내내려던 "한 줄씩 치는 상태"를, 속도 문제를 풀려다 스스로 없애버린 것이다.
성능을 위한 우회가 정확성 조건을 깨뜨렸는데, 두 문제를 다른 날 다른 이름으로 겪는 바람에 연결이 안 됐다.
원인을 쫓는 것과 별개로, 관측 자체를 못 믿게 만드는 문제들이 계속 끼어들었다.
flush를 적용하고 나서도 mrd 결과가 이상하게 보이는 경우가 있었다. DDR을 읽었다고 생각했는데 실제로는 L2 캐시(PL310)에 남아 있던 이전 펌웨어의 데이터를 읽고 있었다.
그래서 ELF 로드 직전에 L2를 통째로 무효화했다.
mwr 0xF8F0277C 0x0000FFFF ;# PL310 Invalidate by Way
after 200
쓰는 쪽만 의심하고 있었는데, 읽는 쪽도 의심 대상이었다.
전원을 안 끄고 다시 다운로드하면 이렇게 났다.
Memory write error at 0xE0001034. AP transaction timeout
이전 세션에서 con으로 실행된 펌웨어가 DMA/UART 레지스터를 점유한 채 무한 루프를 돌고 있어서다. rst -processor로 halt를 시도해도 Cannot halt processor core, timeout이 뜬다. 이미 멈춰 세울 수 없는 상태다.
전원 재공급 말고는 방법이 없었다. 소프트 리셋으로는 복구가 안 된다.
이게 제일 악질이었다. pexpect 방식으로 13,769워드를 다 밀어넣고 "완료" 메시지까지 봤는데, 읽어보니 이랬다.
100000: 00000000
100004: 00000000
100008: 00000000
10000C: FF000000
byte[3] 버그의 패턴(00000031처럼 상위 1바이트만 이상)과 완전히 다르다. 전부 0이다. 쓰기 자체가 처음부터 끝까지 실패한 것이다.
두 실패 모드를 구별하는 기준을 이때 정리했다.
| 읽은 패턴 | 의미 |
|---|---|
0x000000XX — 하위 3바이트만 정상 | 쓰기는 성공, byte[3] flush 실패 |
0x00000000 — 전체 0 | 쓰기 자체가 실패, DDR 접근 불가 |
원인은 함정 2였다. 이전 con 세션이 살아 있어서 ps7_init 중 UART1 레지스터 접근이 timeout으로 죽었고, 그 뒤 모든 mwr이 실패하고 있었다. 그런데 왜 몰랐냐면 —
xsdb는 mwr이 성공하면 아무 출력도 하지 않는다. 출력이 있으면 그게 에러다. 그리고 내 스크립트는 매 반복마다 xsdb.buffer = ''로 버퍼를 비우고 있었다. 에러 메시지를 13,769번 받아놓고 13,769번 버린 것이다.
두 가지를 넣었다.
# 1. 로딩 시작 전에 DDR이 살아 있는지부터 확인
cmd(xsdb, "mwr 0x100000 0xDEADBEEF")
out = cmd(xsdb, "mrd 0x100000 1")
if "DEADBEEF" not in out.upper():
sys.exit("DDR 접근 실패 — 전원 재공급 후 재시도")
# 2. mwr이 뭔가 출력하면 그건 에러다
out = xsdb.before.strip()
if out:
print(f"[경고] mwr 0x{addr:08X}: {out}")
앞의 4줄이 13,769번의 헛수고를 막는다. "성공했다는 신호가 없는 것"과 "성공한 것"은 다르다는 걸 여기서 배웠다.
그리고 로딩이 끝난 뒤에도 스스로 확인하게 만들었다. 벡터 테이블 첫 4워드를 읽어서 기대값과 대조하고, 틀리면 거기서 죽는다.
xsdb.sendline("mrd -force 0x100000 4")
xsdb.expect('xsdb%', timeout=10)
out = xsdb.before.strip()
expected = {0x100000: 0xEA000031, 0x100004: 0xEA00000D,
0x100008: 0xEA000013, 0x10000C: 0xEA000023}
ok = all(f"{v:08X}".upper() in out.upper() for v in expected.values())
if not ok:
sys.exit("ELF 로딩 실패 — 전원 재공급 후 재시도")
없던 성공 신호를 직접 만들어 붙인 셈이다. 이걸 넣고 나서야 "올렸는데 왜 안 돌지"를 고민하는 시간이 사라졌다.
DDR sanity check가 계속 실패했다. mwr로 0xDEADBEEF를 쓰고 mrd로 읽었는데 빈 문자열이 돌아왔다. DDR 접근이 아예 안 되는 줄 알고 전원을 껐다 켜고, ps7_init을 다시 돌리고, 한참을 헤맸다.
원인은 내가 만든 헬퍼 함수였다. 긴 명령이 여러 줄로 표시될 때 xsdb%가 중복 출력되는 걸 막으려고, 남은 프롬프트를 전부 소진하는 while 루프를 넣어뒀었다.
while True:
try:
xsdb.expect('xsdb%', timeout=0.05)
last_before = xsdb.before # ← mrd 출력이 여기서 덮어써진다
except pexpect.TIMEOUT:
break
mwr은 성공 시 출력이 없다. 그래서 이 루프가 mwr 직후 남은 프롬프트를 먹으면서, 바로 뒤 mrd가 뱉은 출력까지 같이 삼켰다. 하드웨어는 멀쩡했고 값도 제대로 써지고 있었는데, 읽어오는 코드가 결과를 버리고 있었던 것이다.
sanity check만 헬퍼를 안 쓰고 직접 sendline/expect로 바꿨더니 바로 정상이 나왔다.
함정 1이 "읽는 쪽(캐시)이 거짓말한다"였다면, 이건 "읽는 코드가 거짓말한다"였다. 디버깅 도구를 직접 만들어 쓸 때는 그 도구도 디버깅 대상이다.
문제는 그다음이었다. 다시 오염이 나타났는데, 이번엔 내가 위 진단을 떠올리지 못했다. 그리고 완전히 다른 방향을 파기 시작했다.
jtag frequency 1000000 ;# 1MHz로 대폭 하향
dow build/output/fir_decimator_demo.elf
전송 속도는 0.1MB/s로 정직하게 느려졌다. 오염은 그대로.
mwr -force 0x100000 0x12345678 → 12345678 정상
mwr 0x100000 {0x11111111 0x22222222 0x33333333 0x44444444}
→ 11222222 ... byte[3]에 이전 값 잔류
단일은 되고 버스트는 깨진다. 여기서 "버스트 FIFO 오버플로우"를 의심했다.
같은 JTAG, 같은 명령, 목적지만 바꿨다. 외부 DDR 대신 칩 내부 메모리 OCM으로.
# OCM 버스트
mwr -force 0x00000000 {0x11111111 0x22222222 0x33333333 0x44444444}
mrd -force 0x00000000 4
→ 11111111 22222222 33333333 44444444 ✅ 완벽
# DDR 버스트
mwr -force 0x00100000 {0x11111111 0x22222222 0x33333333 0x44444444}
mrd -force 0x00100000 4
→ 00111111 00222222 00333333 44444444 ❌ byte[3] 소실
OCM은 멀쩡하고 DDR만 깨진다. 이 결과가 나를 완전히 DDR 하드웨어 쪽으로 몰아갔다.
Vivado에서 PS7 IP의 DDR Configuration을 열었더니 실제로 이상한 게 나왔다. 프로젝트 타겟은 Zybo Z7-20인데 IP 내부 Presets가 None이었고, DDR 배선 길이 차를 보정하는 Board Delay가 전부 0ns였다.
말이 됐다. 모든 데이터 선의 길이가 같다고 착각하면, 실제로 조금 긴 배선의 타이밍 마진이 먼저 무너진다. 증상이 특정 바이트 레인에만 몰린 것과 맞아떨어졌다.
BD를 새로 만들고 Run Block Automation으로 Zybo 보드 파일을 강제 주입해 DQ[31:24]의 Board Delay가 0.244ns 같은 실제 값으로 채워진 걸 확인했다. 비트스트림을 다시 굽고 같은 테스트를 돌렸다.
똑같이 깨졌다. 오염되는 값의 패턴만 11(잔류)에서 00(소실)로 바뀌었을 뿐이다.
ps7_init.tcl은 DDR 컨트롤러 레지스터를 세팅하는 스크립트다. 이게 틀리면 DDR 타이밍이 틀어진다. Digilent 공식 Z7-20 레퍼런스와 diff를 떠봤더니 실제로 차이가 있었다.
레퍼런스에는 있는데 내 파일에는 없는 것
0xF8000180 0x00100A20 ← DDRIOB 설정 (DDR I/O 버퍼)
0xF8000190 0x00100500 ← DDRIOB 설정 (DDR I/O 버퍼)
mask_delay 0xF8F00200 × 5 ← DDR PHY 초기화 타이밍 딜레이
DDRC 타이밍 레지스터 값도 달랐다
| 레지스터 | 레퍼런스 (Z7-20) | 내 프로젝트 |
|---|---|---|
0xF8006004 | 0x00001081 | 0x00001082 |
0xF8006014 | 0x0004281A | 0x0004285B |
0xF8006018 | 0x44E458D2 | 0x44E458D3 |
0xF800601C | 0x720238E5 | 0x7282BCE5 |
0xF8006030 | 0x00040930 | 0x00040B30 |
DDR PHY 딜레이 설정이 통째로 빠져 있고 타이밍 값도 다르다. 이제 확실하다고 생각했다.
레퍼런스 ps7_init.tcl로 교체했다. word 0만 정상화되고 나머지는 그대로였다. 게다가 실행할 때마다 오염값이 달라졌다.
실행 1: F9000031 F900000D F9000013 EA000023
실행 2: F2000031 F200000D F2000013 F2000023
실행 3: F4000031 F400000D F4000013 F4000023
기대값은 EA000031 EA00000D EA000013 EA000023이다. F9, F2, F4 — 비결정적이다. "마지막 워드만 정상"이라는 규칙마저 3차에서 무너졌다.
버스트 테스트로 정리하면 이렇다. word 0은 살아났지만 나머지는 실행할 때마다 다른 값이 박혔다.
| ps7_init 버전 | word0 | word1 | word2 | word3 |
|---|---|---|---|---|
| 원본 (BD 수정 전) | 00 | 00 | 00 | 44 ✅ |
| 원본 (BD 수정 후) | 00 | 00 | 00 | 44 ✅ |
| 레퍼런스 (잔류 있음) | 11 ✅ | D0 | EC | 44 ✅ |
| 레퍼런스 (클리어 후) | 11 ✅ | 11 | F9 | 44 ✅ |
패턴이 바뀌기는 하는데 사라지지는 않는다. 이게 사람을 제일 헷갈리게 했다. 뭔가 건드릴 때마다 결과가 달라지니 "방향은 맞다"고 믿게 된다.
여기서 막혀서, 이전 디버깅 로그를 처음부터 다시 읽었다. 그리고 며칠 전 문서에서 이 문장을 찾았다.
xsdb의 JTAG AXI 마스터는 byte[3]을 파이프라인 버퍼에 hold한다. commit 조건은 다음 명령 대기 시 REPL 블로킹이다.
이미 진단해놨고, 해결책까지 만들어놨던 문제였다. 가설 1~5가 전부 잘못된 방향이었다. ps7_init 교체로 word 0이 정상화된 것도 DDR이 좋아져서가 아니라, ps7_init 시퀀스의 레지스터 write들이 파이프라인 버퍼 상태를 우연히 흔든 부수 효과였을 뿐이다.
이 사실을 문서에 그대로 적었다. "ps7_init.tcl / DDR PHY 방향 조사는 잘못된 가설 추적이었다."
이제 알았으니 고치면 될 일이었다. pexpect로 xsdb REPL을 붙잡고, 워드마다 프롬프트를 확실히 기다리며 단일 mwr을 13,769번 실행했다. 진단이 맞다면 매 워드가 다음 명령을 만나므로 전부 커밋돼야 한다.
mrd -force 0x100000 4
→ F9000031 F900000D F9000013 ...
비결정적으로 재발했다. 이전 문서가 "검증된 방법"으로 기록해둔 방식인데도 그랬다.
열흘쯤 뒤에 혹시나 해서 한 번 더 시도했다. 아직 완전히 검증되지 않은 가설이 하나 남아 있었기 때문이다.
"내 Block Design이 잘못돼서 ps7_init이 틀리게 생성된 것 아닌가?"
앞에서 본 것처럼 내 XSA의 ps7_init은 레퍼런스와 값이 달랐다(0x1082 vs 0x1081). FIR과 DMA를 붙인 BD가 PS7 설정을 어딘가 건드렸을 수 있다. 그럴듯했다.
이번엔 가설을 쫓는 대신 통제 실험을 짰다. 확인해야 할 건 "BD가 원인인가"이므로, BD를 없애면 된다.
| 항목 | 설정 |
|---|---|
| Project type | RTL Project |
| Sources / constraints | 추가 없음 |
| Board | Zybo Z7-20 |
| Block Design | ZYNQ7 Processing System 단독 |
| 적용 | Run Block Automation |
| Export | bitstream 없이 XSA만 |
FIR도, DMA도, 내가 쓴 RTL도 한 줄 없는 최소 프로젝트다. 여기서 나온 ps7_init이 레퍼런스와 같으면 내 BD가 범인이고, 다르면 내 BD는 무죄다.
결과는 이랬다.
최소 PS7-only XSA의 ps7_init:
0xF8006004 -> 0x00001082 / 0x00081082
0xF8006014 -> 0x0004285B
0xF8006018 -> 0x44E458D3
0xF800601C -> 0x7282BCE5
0xF8006030 -> 0x00040B30
내 FIR DMA XSA와 완전히 같았다. PS7 하나만 올려도 같은 값이 나온다는 건, 이게 내 설계 탓이 아니라 현재 Vivado 2024.2 + 현재 Zybo 보드 파일 조합의 기본 생성 결과라는 뜻이다. 가설 하나가 깨끗하게 사라졌다.
그다음, 두 종류의 ps7_init으로 각각 DDR 버스트 쓰기를 돌렸다.
# (A) 레퍼런스 계열 ps7_init (0x1081 계열)
source .../fir_dma_bringup_demo/_ide/psinit/ps7_init.tcl
ps7_init; ps7_post_config
mwr -force 0x00100000 {0x11111111 0x22222222 0x33333333 0x44444444}
mrd -force 0x00100000 4
→ D7111111 E9222222 4D333333 EF444444
# (B) 최소 XSA에서 꺼낸 ps7_init (0x1082 계열)
source /tmp/zybo_ps7_bringup_min_ps7_init.tcl
ps7_init; ps7_post_config
mwr -force 0x00100000 {0x11111111 0x22222222 0x33333333 0x44444444}
mrd -force 0x00100000 4
→ 00111111 00222222 00333333 44444444
둘 다 오염됐다. 값의 모양만 다르고 byte[3]이 깨지는 건 똑같다.
같은 라운드에서 두 가지를 더 확인했다.
XSA의 하드웨어 설정 자체는 정상이었다. hwh를 열어보니 DDR 파라미터가 전부 제대로 들어가 있었다.
| 항목 | 값 |
|---|---|
| DDR part | MT41K256M16 RE-125 |
| DDR bus width | 32 Bit |
| Board delay | 0.221 / 0.222 / 0.217 / 0.244 |
| CL / CWL | 7 / 6 |
| Speed bin | DDR3_1066F |
가설 4에서 0ns였던 Board Delay가 이제 실제 값으로 채워져 있다. 그걸 고쳐놨는데도 오염이 남아 있다는 뜻이다.
FIR RTL도 다시 의심했다가 배제했다. 시뮬레이션을 전부 다시 돌렸다.
make run_all
PASS [S1] TREADY=1 data+TLAST: 4096 samples
PASS [S2] Random Backpressure + Bubble: 4096 samples
PASS [S3] Reset Recovery: 4096 samples
PASS tb_fir_decimator_n43_axis: all scenarios
5개 TB 전부 PASS
애초에 dow/mwr/mrd 경로는 PL을 거치지도 않는다. 그래도 확인해둔 이유는, 그때쯤 되면 모든 게 의심스러워지기 때문이다. 근거 없이 의심하는 걸 끊으려면 근거를 만들어야 했다.
이 시도의 결론은 짧다.
ps7_init값이 무엇이든 byte[3] 오염은 재현된다. 이건ps7_init문제가 아니다.
여기서 확실해진 게 하나 더 있다. ps7_init 값이 레퍼런스와 다르다는 사실과, DDR write가 오염된다는 사실은 별개의 문제였다. 둘 다 "DDR이 이상하다"로 묶여 있었을 뿐이다. 둘을 한 덩어리로 보고 있는 동안은 어느 쪽도 못 풀었다.
16일 동안 지운 변수는 이렇다.
| 시도 | 결과 |
|---|---|
jtag frequency 1000000 + dow | 속도만 감소, 오염 동일 |
단일 mwr 수동 | 정상 |
단일 mwr × 13,769 (REPL 배리어) | 비결정적 오염 재발 |
버스트 mwr | byte[3] 잔류/소실 |
mwr + after 1 | byte[3] → 0x00 null flush |
| OCM 버스트 | 완전 정상 |
| Board Delay 수정 비트스트림 | 오염 동일 |
레퍼런스 ps7_init.tcl | 오염 패턴만 변화 |
최소 PS7-only XSA의 ps7_init | 오염 동일 |
| FIR RTL 시뮬레이션 (TB 5종) | 전부 PASS — 애초에 이 경로와 무관 |
이걸 다 지우고 남은 건 둘이었다.
source 모드에서는 아예 재현조차 안 된다.둘이 겹쳐 있을 가능성이 높다고 봤다. 그리고 둘 다 내가 만든 게 아니다. 하나는 툴 레이어, 하나는 하드웨어다. 내가 코드나 설계로 고칠 수 있는 대상이 남아 있지 않았다.
원인을 특정하진 못했지만, 내가 고칠 수 있는 게 없다는 건 확실했다. 그래서 여기서 멈췄다.
이 시점에 내가 하려던 건 "JTAG를 고치는 것"이 아니라 "보드에서 FIR을 돌리는 것"이었다. JTAG 로딩은 목표로 가는 여러 경로 중 하나일 뿐이었다.
그래서 경로를 바꿨다. SD카드 부팅이다.
BOOT.bin(비트스트림 + FSBL + ELF)을 SD에 넣고 전원을 넣으면 자동 실행된다이후 이 프로젝트의 모든 보드 검증은 SD boot + UART를 기준 경로로 고정했다. 규칙을 다섯 줄로 적어놓고 다시는 흔들리지 않기로 했다.
dow / 버스트 mwr 기반 부팅 경로는 폐기한다.BOOT.bin 부팅 경로로 진행한다.ps7_init이 0x1082 계열이면 XSA가 잘못된 것"이라는 이전 판정 기준은 철회한다.ps7_init.c를 그대로 쓰고, 판정은 DONE LED / UART READY / Python FFT로 한다.덧붙이면, 새 보드로 교체한 뒤 같은 SD 부팅에서 DONE LED가 켜지고 READY까지 도달했다. 이전 보드에서는 SD 부팅조차 실패했었다. 그래서 이 증상에 보드 개체 불량이 섞여 있었을 가능성도 배제하지 못한다. 이것도 미해결로 남겨뒀다.
16일을 쓰고 원인을 못 밝힌 채 우회했다. 그런데 진짜 손해는 그게 아니었다.
답을 이미 적어놨는데 안 읽은 것. 로그를 남기는 이유는 나중에 읽기 위해서인데, 정작 같은 증상이 다른 얼굴로 돌아왔을 때 나는 그걸 새 문제로 취급했다. 그래서 그 뒤로는 증상이 재발하면 코드보다 내 문서를 먼저 grep한다.
그리고 하나 더. OCM 정상 / DDR 오염이라는 관찰은 그 자체로는 정확했지만, 나를 하드웨어 쪽으로 몰아갔다. 깨끗한 실험 결과가 항상 올바른 방향을 가리키지는 않는다.
세 번째는 증상이 같은 범주에 있다고 원인을 묶지 말 것이다. ps7_init 값이 레퍼런스와 다른 것과 DDR write가 오염되는 것은 둘 다 "DDR이 이상하다"로 보였지만 별개의 문제였다. 묶어놓고 보는 동안은 어느 쪽도 안 풀렸고, 게다가 그 오해 때문에 "ps7_init 값이 다르면 XSA가 잘못된 것"이라는 틀린 판정 기준까지 만들어서 한동안 그걸로 다른 실험들을 채점하고 있었다.
마지막으로, 이 글에서 얻은 것 중 제일 오래 쓰게 된 건 원인이 아니라 습관 쪽이었다.
SD 부팅으로 바꾸니 UART에 READY가 떴다. 부트 체인은 통과다.
그런데 PC에서 데모를 돌리면 여전히 timeout이었다. 이번엔 DMA가 시작은 되는데 끝나지 않았다. 다음 글은 그 원인을 하루 만에 잡은 이야기다. 원인은 1편에서 내가 "충분함"이라고 적고 넘어간 설정 한 줄이었다.