01signal.com

FPGA 설계자를 위한 실전 역량

이 페이지는 전문 FPGA 설계자가 되는 방법을 다루는 다섯 편짜리 연재의 세 번째 글입니다. 이번 글에서는 FPGA 설계자가 되기 위해 필요한 가장 중요한 실전 역량들을 정리해 보겠습니다. 각 역량이 왜 중요한지도 설명하려고 합니다. Verilog 부분은 뻔해 보이더라도 건너뛰지 마세요. 뻔하지 않은 이야기가 몇 가지 있을 수 있습니다.

시작하기 전에, 왜 VHDL이 아니라 Verilog만 언급하는지 설명하겠습니다. 간단합니다. Verilog를 권하기 때문입니다. 앞으로 Verilog가 HDL 언어의 주류가 될 것 같기 때문이죠. 무엇보다 중국에서는 VHDL 이야기를 거의 듣기 어렵습니다. 하지만 아래에서 하는 이야기는 VHDL에도 모두 해당됩니다.

Verilog의 두 가지 측면

Verilog를 알아야 한다는 것이 왜 필요한지는 설명이 거의 필요 없습니다. 사실 많은 사람이 Verilog를 아는 것 = FPGA 설계자라고 생각합니다. 주니어 프로그래머를 Verilog 강좌에 보내 놓고, FPGA 챔피언이 되어 돌아오지 않았다며 실망한 프로젝트 매니저도 한두 명이 아닙니다.

우선 Verilog의 문법을 아는 것과 이 언어를 쓰는 방법을 아는 것은 전혀 다릅니다. 어떤 언어든 마찬가지지만, Verilog에서는 그 간극이 훨씬 큽니다. 둘째, Verilog 코드는 컴퓨터 프로그램이 아니라는 점을 이해하는 것이 중요합니다. 그것은 하드웨어를 기술합니다. 따라서 원칙적으로 모든 것이 병렬로 일어납니다. 멀티스레드 프로그래밍에 익숙하지 않은 사람, 특히 소프트웨어 개발자들이 어려워하는 사고의 전환이 바로 이것입니다.

'프로그래밍 언어냐 아니냐'라는 질문은 사실 조금 더 복잡합니다. Verilog는 두 가지 다른 목적으로 쓰이기 때문입니다. 하드웨어의 시뮬레이션과, FPGA나 ASIC용 로직의 생성(합성)입니다.

시뮬레이션의 목적은 대개 Verilog 코드를 FPGA의 로직으로 합성하기 전에 점검하는 것입니다. 이를 위해 테스트벤치 (testbench)를 작성합니다. 테스트벤치는 본질적으로 컴퓨터 프로그램으로, 자극 신호를 인위적으로 만들어 내고, 테스트 대상 로직에서 나오는 출력 신호로 무언가를 합니다. 보통은 신호의 값을 파일에 기록하거나, 값이 예상대로인지 확인합니다.

Verilog를 시뮬레이션에 사용할 때, 그것은 실제로 프로그래밍 언어입니다. 다만 C나 Python, JavaScript와는 전혀 다른 부류죠. 그래도 컴퓨터는 우리가 요청한 것을 정확히 수행합니다. 시뮬레이션이 돌면 테스트벤치와 합성하려는 코드 모두 문법이 정의한 그대로 동작합니다.

그런데 여기서 큰 함정이 나옵니다. 우리가 테스트한 바로 그 Verilog 코드를 합성에 사용하면, Verilog는 더 이상 프로그래밍 언어가 아닙니다. HTML이 웹 페이지에 무엇을 표시할지 기술하는 것과 같은 방식으로 로직을 기술합니다. 더 나쁜 것은, HTML과 달리 Verilog 코드의 해석이 꽤 애매할 수 있다는 점입니다.

Verilog 코드를 합성하는 것은 일종의 마법이라는 점을 이해하는 것이 중요합니다. 우리 인간은 원하는 동작을 기술하고, 합성기 (synthesizer)는 로직 자원의 도움을 받아 그것을 어떻게든 구현합니다. 따라서 우리는 합성기가 인식하고, 우리가 의도한 로직을 만들어 내는 특정 코딩 패턴으로 이 동작을 정의해야 합니다. 일반 프로그래밍과 달리, 하고 싶은 대로 아무렇게나 쓸 수 없습니다. 우리의 요청에 따라 합성기가 만들어 낼 로직 소자를 생각해야 합니다. Verilog 코드는 합성기에게 어떤 로직을 만들지 알려 주는 힌트일 뿐입니다.

AI 시대이니 이것도 덧붙여야겠습니다. Verilog는 바이브 코딩 (vibe coding)이 아닙니다. 형식 언어 (formal language)이고, 합성기 (synthesizer)는 우리가 달래서 원하는 대로 시키는 LLM이 아닙니다. 합성기는 LLM이 등장하기 훨씬 전부터 있었고, 훈련된 신경망이 아니라 엄격한 규칙에 따라 우리 코드를 해석합니다.

그리고 무엇보다 중요한 것. 형편없는 Verilog 코드를 쓰면, 로직이 시뮬레이션처럼 동작하지 않습니다. 시뮬레이터는 우리가 원하는 것을 정확히 수행합니다. 시뮬레이터는 코드가 말하는 대로 하기 때문입니다. 반면 합성기 (synthesizer)는, 우리가 Verilog 코드를 제대로 쓰는 데 주의하지 않으면, 조용히 다른 일을 하는 로직을 만들어 낼 수 있습니다.

이 점은 결정적이고 반복해서 말할 가치가 있습니다. Verilog를 시뮬레이션에 쓸 때, 그것은 — 다소 시시하긴 해도 — 프로그래밍 언어입니다. 문법만 제대로 맞추면 시뮬레이터는 원하는 대로 정확히 해 줍니다. 반면 합성기 (synthesizer)는 Verilog 문법이 완벽하게 맞더라도 시뮬레이션과 완전히 다른 로직을 만들어 낼 수 있습니다. 운이 좋으면 합성기가 예상과 다른 결과를 얻을 수 있다는 경고를 내줍니다. 그 경고가 수백 개 속에 파묻혀 있어도 보시길 바랍니다. 운이 더 좋으면 합성기가 에러를 내고 멈춥니다. 하지만 조용한 버그가 생기는 경우가 너무나 흔합니다. 이를 피하려면 어떤 코딩 패턴이 합성에 괜찮은지 알아야 합니다. 그리고 다른 것은 시도하지 말아야 합니다.

그러면 이제 문제입니다. 올바른 Verilog는 어떻게 배워야 할까요?

기본 Verilog 역량

물론 첫 단계는 문법을 배우는 것입니다. Verilog 책과 튜토리얼은 정말 많습니다. 불행히도 그중 상당수는 하나부터 열까지 다 다룹니다. 그것이 불필요할 뿐만 아니라, 합성기 (synthesizer)를 망가뜨리는 코딩 패턴을 쓰도록 유혹할 수 있습니다.

저는 다음의 최소한의 주제 집합을 권합니다. 구할 수 있는 아무 자료에서나 이것들을 익히면, 문법 면에서는 충분합니다. 마주치는 Verilog 코드에서 모르는 것이 나오면, 즐겨 쓰는 AI 프롬프트에 그것이 무슨 뜻인지 물어보세요. 그 이상 어렵지 않습니다. 그러면 제 쇼핑 목록입니다.

여기까지는 쉬운 부분입니다. 진짜 문제는 Verilog 코드를 올바르게 작성해서 합성기 (synthesizer)가 우리가 실제로 원하는 로직을 만들어 내게 하는 것입니다. 사실, 세상 어떤 합성기든 똑같은 그 로직을 만들어 내게 하는 것입니다.

저는 간단한 원칙을 따르는데, 제 황금률 목록의 4번 규칙입니다. 아주 흔하게 쓰이는 코딩 패턴을 반드시 사용하라는 것이죠. 생각은 이렇습니다. 합성기 (synthesizer)가 내 코드를 잘못 해석한다면, 다른 많은 사람의 코드에도 똑같은 실수를 할 것입니다. 그러면 그런 일은 일어나지 않습니다. 그 많은 사람들이 곧 다른 합성기로 갈아탈 테니까요. 그래서 저는 쓴 코드를 보면서 묻습니다. '합성기가 이걸 망가뜨리면 다른 사람 몇 명이 영향을 받을까?' 답이 '많다'라면 아마 안전한 쪽입니다. '아마'에 강조를 두고요.

그런데 이 다른 사람들은 대체 Verilog를 어떻게 쓰는 걸까요? 사실 이것은 풀기 어려운 문제입니다. Verilog의 대부분은 회사 안에서 작성되고 공개되지 않기 때문입니다. 게다가 사람마다 다른 코딩 스타일로 씁니다. 설상가상으로 인터넷의 예제는 경험 없는 사람이 쓴 경우가 많습니다. 그 코드가 동작하더라도 반드시 따라 할 만한 것은 아닙니다. 다른 합성기가 같은 코드를 망가뜨릴 수도 있는데, 어쩌면 합성 요령을 한계까지 밀어붙였기 때문일 수도 있습니다. OpenCores의 Verilog 코드는 훌륭한 것부터 시뮬레이션에서만 동작하는 것까지 품질이 천차만별입니다.

그러면 어떻게 알 수 있을까요? 제가 떠올릴 수 있는 가장 좋은 제안은 AMD의 IP 도구가 생성한 Verilog 코드를 읽어 보라는 것입니다. 예를 들어 DDR 메모리 컨트롤러 같은 일부 IP는 합성 가능한 Verilog를 생성하는데, 자기 일을 아는 사람들이 작성한 것입니다. 다만 자동 생성된 Verilog 코드에는 쓸모없고 쓰이지 않는 코드가 많이 들어 있는 경우가 많다는 점에 유의하세요. 스크립트 (script)가 생성한 코드의 전형입니다. 다른 모듈을 인스턴스화하고, 그것이 다시 다른 모듈을 인스턴스화하는 식의 모듈만 있는 경우도 많습니다. 이렇게 끝없이 모듈을 감싸는 것은 따라 할 것이 아닙니다. 같은 코드를 다양한 구성으로 쓸 수 있도록 계층 구조를 필요한 만큼 정리하는 방법일 뿐입니다. 자동 생성된 Verilog 코드에는 파라미터도 많은 경향이 있는데, 이것도 반드시 따라 할 필요는 없습니다. 여러분의 목표는 그들이 기능을 표현하는 방식을 모방하는 것이지, 지저분한 계층 구조가 아니라는 점을 기억하세요.

SystemVerilog에 대해 한마디. 꽤 인기가 많지만, 저는 개인적으로 이 언어 방언으로 코드를 써 본 적이 없습니다. 주로 제 Verilog를 최대한 단순하게 유지하고 싶기 때문입니다. 합성기 (synthesizer)를 덜 믿어야 할수록 좋습니다. 복잡한 구조가 필요하면 간단한 Verilog를 만들어 내는 Perl 스크립트 (script)를 작성합니다. 합성기에게는 한 티스푼씩 떠먹이세요. 그러지 않으면 합성기가 여러분에게 토해 냅니다.

구현 도구

이 역량을 익힌다는 것은 도구를 효율적으로 다루고, 잘 통제하고, 경고 메시지를 이해하고, 재현 가능한 비트스트림 (bitstream) 생성을 보장하고, 프로젝트가 커져도 관리 가능하게 유지한다는 뜻입니다. 요컨대 프로젝트에서의 작업을 통제하는 것입니다.

Vivado가 가장 선호되는 도구입니다. AMD가 시장을 지배할 뿐 아니라, 다른 벤더, 특히 새로 등장한 중국 FPGA 벤더들이 호환되는 방식으로 도구를 설계하는 경향이 있기 때문입니다. 이 도구들을 IDE처럼 쓰는 법을 아는 것 외에도, Verilog 코드와 IP를 비트스트림 (bitstream)으로 바꾸는 과정을 이해해야 합니다. 합성으로 시작해, 벤더에 따라 달라지는 여러 단계가 이어지지만, 원칙적으로는 모두 같은 일을 합니다. 합성기의 출력을 FPGA에 로드할 수 있는 비트스트림으로 점진적으로 변환하는 것이죠. 이 실행 단계의 연속은 블랙박스가 아니고, 블랙박스로 취급해서도 안 됩니다.

도구가 어떻게 동작하는지 이해해야 하는 주된 이유는 에러와 경고에 올바르게 대응하기 위해서입니다. 프로젝트 구현 중에는 경고가 아주 많이 나오는데, 어느 것이 중요하고 어느 것이 무시해도 되는지 구분하는 것이 중요합니다. 그리고 에러가 나면 과정이 실패하고, 문제를 해결해야 합니다. 그런 문제를 푸는 능력은 도구의 동작 뒤에 있는 이론을 이해하고 경험을 쌓는 데서 나옵니다.

AI에게 문제 해결법을 물어보는 것이 통할 때도 있지만, AI가 무의미한 디버깅의 긴 여정으로 끌고 가는 경우가 많습니다. 그리고 자기 판단 없이 AI 말을 들으면, 문제를 겉보기엔 해결한 것 같은데 실제로는 에러 메시지만 없애고 진짜 문제를 만든 셈이 될 수도 있습니다. 요컨대, 자기 머리를 대신할 것은 없고, 앞으로도 없을 것입니다.

또 중요한 점은 도구마다 특유의 버릇이 있다는 것입니다. 예를 들어 오해를 부르는 에러 메시지 같은 것이죠. 더 나쁜 경우도 있습니다. 도구가 코드나 설정, 제약 조건을 조용히 무시하는 것입니다. 이런 것들은 오로지 경험으로 배우게 되고, 그 경험의 일부는 리포트를 실제로 읽고, 이런 메시지들이 구체적인 관련이 없더라도 그것이 무엇을 뜻하는지 파악하는 것입니다.

다음은 제가 체크리스트로 권하는 개념과 절차의 목록입니다. 합성과 비트스트림 (bitstream) 확보에만 관련된 것입니다. 시뮬레이션, 검증, 디버깅은 나중으로 미루겠습니다.

예제 프로젝트를 시험해 보면, 마지막 항목을 빼고 이 목록의 모든 것을 거치게 될 가능성이 큽니다. 누군가 예제 프로젝트를 이미 준비해 준 상태에서 도구를 쓰는 것은 쉽습니다. 실제로는 프로젝트가 그렇게 깔끔하게 정리된 경우가 드물고, 도구가 제대로 동작하게 하고 가능한 최선의 결과를 얻는 일은 여러분 몫입니다. 기계가 어떻게 돌아가는지 이해하지 못하면, 기계가 멈추거나 원하는 대로 동작하지 않을 때 고치기가 어렵습니다. 뭐든 다 제대로 해도 이상한 일은 늘 일어난다는 것을 기억하세요.

파일의 바다

개발 도구는 파일을 만들고, 아주 많이 만듭니다. 합성부터 최종 비트스트림 (bitstream)까지 도구가 실행하는 각 단계는, 파일 몇 개를 읽고 결과를 다른 파일로 출력하는 컴퓨터 프로그램과 같습니다.

문제를 더 어렵게 만드는 것은, 도구가 소스 파일의 복사본을 만들어 원본 대신 그 복사본에 의존하는 경우가 많다는 것입니다. 도구는 중간 파일도 만들어, 소스 파일처럼 보이는 어떤 것 대신 그것에 의존합니다. 소스를 바꿨는데 결과가 바뀌지 않을 때 특히 이 점을 명심하세요.

학습의 첫 단계로 파일 하나하나가 무슨 일을 하고 무엇을 위한 것인지 이해하는 데 우선순위를 두지는 않겠습니다. 그것들을 전부 익히려는 것은 무의미하지만, 어떤 파일을 '소스'로, 어떤 파일을 '생성된 것'으로 봐야 하는지 아는 것은 중요합니다. 이 파일의 바다에서 수영을 잘할수록 머리를 물 위에 유지할 가능성이 커집니다. 프로젝트를 다른 방향으로 발전시키려고 전체 프로젝트의 독립적인 복사본을 만들거나, 다른 컴퓨터로 옮기려 할 때 제 말이 무슨 뜻인지 이해하게 될 것입니다.

가장 좋은 방법은 FPGA 프로젝트를 정의하는 최소한의 파일 집합을 Git 저장소에 유지하는 것입니다. 때때로 다른 파일은 모두 지우고, 이 최소 집합에서 프로젝트를 다시 빌드하세요. 최소한의 파일 집합에서 프로젝트를 부트스트랩하는 예를 보려면, Xillybus의 데모 번들 (demo bundle) 중 하나를 내려받아 구현해 보세요(Vivado와 Quartus 모두 사용 가능). 시작할 때 소스를 Vivado에 평소처럼 임포트하는 것이 아니라 Tcl 스크립트 (script)를 실행한다는 점에 유의하세요. 무서운 해결책처럼 들릴 수 있지만, 이 스크립트는 Tcl을 전혀 몰라도 수정하기 쉽습니다.

Vivado에 대해 알아 둘 만한 것 하나는 DCP가 압축 파일이라는 점입니다. edif 형식의 넷리스트 (netlist), 제약 조건, 그리고 다른 정보를 담고 있습니다. 예를 들어 DCP가 배치 및 배선 (place and route) 이후 단계의 결과라면, 로직 소자가 정확히 어디에 배치되었는지를 담고 있습니다. 과정의 특정 단계를 마친 시점의 설계 스냅샷인 셈입니다. 심심풀이 삼아 DCP의 압축을 풀어 들여다보시길 권합니다.

검증과 시뮬레이션

완벽한 세상에서는 로직 설계가 첫 시도부터 동작하고, 고칠 버그가 없습니다. 물론 현실은 거의 항상 다릅니다.

모든 Verilog 입문 과정에서 이렇게 말할 것입니다. 먼저 FPGA에서 로직으로 쓰고 싶은 Verilog 코드를 작성하세요. 이를 '합성용 코드' 또는 '합성 가능한 코드'라고 부릅니다. 그다음 Verilog로 테스트벤치 (testbench)를 작성하고, 시뮬레이션에 사용해서 합성용 코드가 제대로 동작하는지 확인합니다. 아니, 사실은 제대로 동작하지 않는 모습을 보고 버그를 고치는 것이죠. 테스트벤치 (testbench)는 오로지 시뮬레이션 목적으로만 작성되는 Verilog 코드이고, 합성기 (synthesizer) 근처에는 가지 않습니다.

실제로 일반적인 작업 방식은 Verilog 모듈 하나, 혹은 몇 개를 작성한 뒤 그것들이 제대로 동작하는지 검증할 테스트벤치 (testbench)를 작성하는 것입니다. 프로젝트가 커지면서 이것을 반복하고, 결국에는 프로젝트 전체를 시뮬레이션하기도 합니다. 그렇게 큰 시뮬레이션에는 보통 여러 개의 서로 다른 테스트벤치가 있고, 각각 다른 기능을 확인하는 것을 목적으로 합니다.

그런데 모든 시뮬레이션이 같은 방식으로 이루어지지는 않습니다. 시뮬레이션에는 크게 세 가지 접근법이 있습니다.

첫 번째 접근법은 파형을 얻기 위한 시뮬레이션입니다. 이 방식에서 테스트벤치 (testbench)는 시뮬레이션하려는 모듈의 입력을 먹여 주는 신호만 만들어 냅니다. 클록과 리셋, 그리고 실제 물리적 신호나 다른 모듈이 만들어 내는 신호의 동작을 흉내 내는 다른 신호들이 포함됩니다. 시뮬레이션이 끝난 뒤 GUI 인터페이스로 테스트 대상 로직이 만들어 낸 파형을 보면서, 제대로 동작하는지, 아니라면 왜 그런지 확인합니다. 이 방법은 비교적 단순한 로직에 적합합니다. 예를 들어 비디오 출력 패턴을 구현하는 상태 머신 (state machine)은 이렇게 시뮬레이션할 수 있습니다. 올바른 반복 출력 패턴은 파형만 봐도 쉽게 확인되니까요.

두 번째 접근법은 파일에서 입력을 읽고 파일에 출력을 쓰는 것입니다. 여기서 테스트벤치 (testbench)는 간단한 신호 몇 개를 만들어 내지만, 중요한 신호는 파일에서 읽어 옵니다. 테스트 대상 모듈의 출력, 혹은 선택된 출력 몇 개의 값은 다른 파일에 기록됩니다. 테스트벤치가 읽는 파일과, 테스트벤치가 내놓아야 할 기대 출력을 담은 파일을 만들어 내는 컴퓨터 프로그램이나 스크립트 (script)를 작성하는 일이 꽤 흔합니다. 시뮬레이션을 돌린 뒤, 기대 출력과 테스트벤치가 실제로 쓴 것 사이에 간단한 텍스트 diff를 수행할 수 있습니다. 물론 여기에는 끝없는 변형이 있습니다. 테스트벤치가 기대 출력과 비교할 수도 있고, 반대로 컴퓨터 소프트웨어가 테스트벤치의 출력을 읽어서 분석할 수도 있습니다.

이 시뮬레이션 방법은 제가 '처리 로직'이라고 부른 것, 즉 어떤 종류의 데이터 처리를 구현하는 로직에 특히 적합합니다. 회귀 테스트 (regression test), 즉 코드의 한 버전에서 다른 버전으로 넘어갈 때 기능적으로 달라진 것이 없는지 확인하는 시뮬레이션에도 유용합니다.

세 번째 접근법은 테스트벤치 (testbench)가 스스로 정확성을 확인하고, 뭔가 잘못되면 에러를 내고 멈추게 하는 것입니다. 이 방법은 의미 있는 테스트 패턴을 Verilog로 작성해야 하는데, 스크립트 언어로 하는 것보다 어려운 경우가 많습니다. 반면, 통과 또는 실패를 알려 주는 자기 완결적인 테스트벤치가 있으면 FPGA 프로젝트를 유지 관리하기가 더 쉽습니다. 회귀 테스트 스위트를 돌리는 사람은 이 방식으로 일하는 것을 반길 것입니다.

이것이 세 가지 주요 접근법입니다. 마치 우리 인간이 쓴 Verilog만 시뮬레이션하는 것처럼 말했지만, 사실은 그렇지 않습니다.

합성 후 시뮬레이션

앞서 언급했듯이, 합성기 (synthesizer)는 Verilog 코드가 기술한 동작과 다른 로직을 만들어 낼 수 있습니다. 이 문제를 다루는 한 가지 방법은 합성기가 만들어 낸 출력, 즉 넷리스트 (netlist)를 시뮬레이션하는 것입니다. 이를 합성 후 시뮬레이션이라고 부릅니다.

합성 후 시뮬레이션을 돌리려면, 개발 도구에 합성된 코드의 Verilog 모델을 만들어 달라고 요청합니다. 이것은 우리가 합성용으로 작성한 모듈과 같은 포트를 가진 거대한 Verilog 모듈입니다. 하지만 내부는 타깃 FPGA의 실제 로직 소자를 나타내는 작은 모듈들(시뮬레이션 프리미티브 (primitive)라고 부릅니다)로 이루어져 있습니다. 그래서 정말 지저분한 Verilog 파일이지만, 테스트벤치 (testbench)에서 우리가 작성한 원래 Verilog 모듈 대신 이것을 참조할 수 있습니다.

고전적인 방법은 사람이 작성한 Verilog 코드로 시뮬레이션을 돌리고, 그다음 합성 후 모델로 돌려서 출력을 비교하는 것입니다. 위에서 말한 두 번째 방법(파일을 입력과 출력으로 쓰는)이 가장 좋습니다. 합성 후에도 로직 설계가 정확히 같게 동작하기를 기대하기 때문입니다. 그렇지 않다면, Verilog 코딩 스타일에 큰 타격이라고 봐야 합니다. 사실 Verilog를 올바르게 쓰고 있다면 합성 후 시뮬레이션은 필요 없어야 합니다. 이 종류의 시뮬레이션은 ASIC 업계에서 더 흔한데, 최종 칩에 버그가 남는 것을 극도로 경계하기 때문입니다. 또한 참고할 점: 합성 후 시뮬레이션이 원래와 정확히 같은 출력을 내더라도, 이것만으로 합성기 (synthesizer)가 예상대로 했다는 것이 보장되지는 않습니다.

배치 및 배선 (place and route) 후 시뮬레이션도 있습니다. 이름에서 알 수 있듯이, 로직 소자가 FPGA 안에서 배치되고 연결된 상태를 시뮬레이션합니다. FPGA 내부의 전파 지연 (propagation delay)을 고려하지만, 아주 제한적인 방식으로만 그렇습니다. 시뮬레이션과 실제 일어나는 일 사이에는 여전히 큰 차이가 있습니다. FPGA 내부의 실제 지연은 온도나 공급 전압 같은 많은 물리적 요인에 달려 있기 때문입니다. 또 이 지연은 어느 정도 무작위적입니다. 칩 실리콘에 온 사방에 흩어진 오염 물질 때문입니다. 이런 오염이 물리적 특성을 바꾸므로 전기적 지연이 조금씩 어긋납니다. 따라서 물리적인 FPGA마다 지연이 다르지만, 규격 범위 안에는 있습니다. 시뮬레이션이 돌면 각 경로에 특정 지연 하나만 적용되는데, 보통 허용되는 최대 지연입니다. 배치 및 배선 후 시뮬레이션에서 설계가 완벽하게 동작하더라도, 실제 물리적 FPGA에서 동작한다는 보장은 여전히 없습니다.

그러니까 종합하면, 시뮬레이션에는 꽤 심각한 한계가 있습니다. 하나는, 합성기 (synthesizer)가 따르지 않을 우리의 바람을 시뮬레이션이 따른다는 점입니다. 그리고 합성 후 시뮬레이션조차 FPGA 내부의 많은 실제 효과를 담아내지 못합니다. 글리치 (glitch), 온도나 공급 전압, 제조 허용 오차로 인한 물리적 로직 소자의 편차 같은 것들입니다. 시뮬레이션은 타이밍 위반의 결과로 나타나는 로직의 동작도 재현하지 못합니다. 예를 들어 클록 도메인 (clock domain)을 안전하지 않게 넘을 때 같은 경우죠.

설계가 올바르게 작성되고 제약이 제대로 주어졌다면, 시뮬레이션과 실제 동작이 같습니다. 그렇지 않다면 시뮬레이션은 무가치합니다. 이 점을 명심하는 것이 중요합니다.

또 다른 한계는 시뮬레이션이 아주 짧은 시간 구간만 다룰 수 있다는 것입니다. 시뮬레이션이 백만 클록 사이클 동안 돈다면 — 오래 걸릴 수 있지만 — 클록이 100 MHz로 돌 때 실제 시간으로는 10 ms에 불과합니다. 드문 문제나 코너 케이스는 쉽게 놓칩니다. 시뮬레이션 창 안에 우연히 들어오지 않았기 때문입니다.

시뮬레이션에 대한 제안

초보자에게 무엇을 권할까요? 시뮬레이션하는 법을 아는 것은 필수이고, 다른 Verilog 관련 역량을 배우면서 함께 익혀야 할 것입니다. 특히 파일을 입력과 출력으로 쓰는 두 번째 시뮬레이션 방법에 익숙해지세요. 이것이 진지한 방법으로 여겨집니다. 무엇보다 회귀 테스트 (regression test)에서 흔히 쓰이기 때문입니다. 합성 후 시뮬레이션과 배치 및 배선 (place and route) 후 시뮬레이션으로도 시험해 보세요. 적어도 취업 면접을 위해서라도요.

그런데 시뮬레이션에 중독되지는 마세요. 시뮬레이션으로 버그를 찾을 때마다 자기 손등을 때리세요. 첫 번째 방법(시뮬레이션 파형을 보는 것)으로 버그를 찾았다면 더 세게 때리세요. 궁극적인 목표는 첫 시도에 동작하는 코드를 쓰는 것입니다. 시뮬레이션은 단순한 버그를 잡아 줄 뿐, 수십억 클록 사이클마다 뭔가 이상한 일을 일으키는 버그는 잡지 못합니다. 그런 버그는 영원히 쫓고 싶지 않을 것입니다.

마지막으로, 저 자신에 대한 작은 비밀 하나. 저는 시뮬레이션을 거의 돌리지 않습니다. Verilog 코드를 신중하고 사려 깊게 작성해서, 바로, 적어도 FPGA에서 직접 고칠 수 있을 만큼 적은 버그로 제대로 동작하게 만듭니다. 하지만 이렇게 일하는 다른 사람은 모르겠고, 이것을 일반적인 방법으로 권할지는 잘 모르겠습니다.

사실 나중을 위해 기억해 둘 고급 보너스 팁을 하나 덧붙이고 싶습니다. 사람들이 시뮬레이션을 돌릴 때, 로직에 리셋을 넣는 경우가 많습니다. 시뮬레이션에서는 모든 레지스터가 'X'로 표시되는 미지의 상태에서 시작하기 때문입니다. 그 결과 아무것도 제대로 나오지 않습니다. 시스템 전체가 'X' 상태에 머물기 때문이죠. 흔한 실수는 이 X를 없애려고 비동기 리셋 (asynchronous reset)을 여기저기 급하게 넣는 것입니다. 그러고는 이것이 아주 나쁜 생각일 수 있다는 것을 잊어버립니다. 별도 페이지에서 설명한 것처럼요. 그러니 X 문제는 반드시 해결하되, 현명하게 하세요. 동기 리셋 (synchronous reset)을 쓰거나, 아니면 합성 가능한 코드에 "initial" 블록을 쓰는 것은 어떨까요? 원래는 시뮬레이션용으로만 의도된 블록이지만, 대부분의 합성기 (synthesizer)가 이 블록을 구현합니다. 시뮬레이션이 코드를 쓰는 방식을 좌우하게 두지 마세요.

테스트와 디버깅

이런저런 이야기를 다 하고 나면, 설계를 테스트하고 예상대로 동작하지 않는 이유를 찾아내게 됩니다. 디버깅은 피해자, 탐정, 범인이 모두 같은 사람인 추리 소설과 같다는 말이 있습니다.

그리고 이 추리 소설에는 미스터리를 푸는 단 하나의 최선의 방법이 없다는 것이 사실입니다. 매번 올바른 접근법과 도구, 방법을 창의적으로 찾아내서 버그의 근원에 다가가야 합니다. 어떤 사람들은 매번 같은 디버깅 방법만 고집합니다. 때로는 꽤 빨리 문제를 찾아내기도 하고, 때로는 끝없이 걸리기도 합니다.

특정 프로젝트를 위한 전용 디버깅 도구와 방법의 집합을 개발하는 일도 꽤 흔합니다. 큰 소프트웨어 프로젝트에서도 그런 일이 있지만, FPGA 설계에서는 피할 수 없는 경우가 많습니다. 테스트를 위한 테스트벤치 (testbench)를 개발하는 것과 비슷하지만, 하드웨어에서 하는 셈입니다.

그런데 버그를 찾는 단 하나의 최적 방법은 없어도, 흔히 쓰이는 도구들은 있습니다. 그중 몇 가지를 언급하겠습니다.

대부분의 사람이 가장 먼저 쓰는 도구는 온칩 로직 애널라이저 (on-chip logic analyzer)입니다. 어떤 개발 스위트를 쓰는지에 따라 ILA, ChipScope, SignalTap 혹은 비슷한 이름으로 불리며, 모두 같은 일을 합니다. 컴퓨터를 로직 애널라이저로 바꿔 주는 것이죠. 이 도구를 쓰면 FPGA 내부에서 캡처한 파형을 시뮬레이션 파형을 보듯이 볼 수 있습니다. 어떤 신호를 지켜볼지, 그리고 그 신호의 캡처를 트리거할 조건을 고르면 됩니다.

컴퓨터와 FPGA 사이의 통신은 JTAG 인터페이스로 이루어지는데, 비트스트림 (bitstream) 파일을 FPGA로 보낼 때 쓰는 바로 그 인터페이스입니다. 그래서 추가 하드웨어가 필요 없고, 전자 부품을 좋아하지 않는 많은 FPGA 설계자가 이 점을 반깁니다. FPGA와 이미 존재하는 JTAG 링크가 곧 디버깅 도구인 셈입니다.

이 방법은 너무 편리해서 많은 FPGA 설계자가 여기에 중독되고, 어떤 상황에서는 더 적합할 수 있는 다른 방법들을 잊어버립니다.

이 방법의 주된 단점은 JTAG의 데이터 대역폭이 비교적 낮다는 것입니다. 그래서 조사에 대량의 데이터가 필요할 때는 ILA 같은 도구가 적합하지 않습니다. 또한 JTAG 링크를 통해 데이터가 도착하기까지 시간이 조금 걸립니다. 이벤트에 즉시 반응해서 다른 일어나는 일들과 연관 지어 보고 싶을 때 문제가 될 수 있습니다.

JTAG 링크의 한계가 문제가 될 때는 더 빠른 인터페이스가 필요합니다. FPGA 보드에 PCIe 인터페이스가 있으면, 컴퓨터와 사이에서 높은 데이터 레이트로 데이터를 전송하는 데 쓸 수 있습니다. PCIe 인터페이스와 컴퓨터 드라이버를 구현하는 일은 그 자체로 하나의 프로젝트가 될 수 있지만, Xillybus를 쓰면 그런 링크를 빠르고 쉽게 띄울 수 있습니다. Xillinux와 함께 동작하는 Zynq-7000 보드에서도 Xillybus로 비슷한 해법이 가능합니다.

많은 데이터 속에 숨어 있는 핀포인트 문제가 있을 때 고대역폭 연결이 필요할 때가 많습니다. 예를 들어 영상 처리에서는 이미지를 Xillybus를 통해 컴퓨터로 전송하고, 전용 컴퓨터 프로그램의 도움을 받아 컴퓨터 모니터에서 봅니다.

온칩 로직 애널라이저 (on-chip logic analyzer)를 고르든 Xillybus를 고르든, 이것들은 하드웨어를 건드리지 않는, 말하자면 무균 상태의 디버깅 도구입니다. 하지만 때로는 무슨 일이 벌어지는지 알아내려면 하드웨어에서 오는 단순하고 즉각적인 반응이 필요합니다.

그래서 우선, 가장 단순한 디버깅 도구를 하나 권하겠습니다. LED입니다. 놀랄 만큼 자주, 버그를 찾는 가장 빠른 방법은 여러 로직 조건을 정의하고, 그 조건이 참일 때 LED가 깜빡이도록 하는 것입니다. 특히 절대 일어나면 안 되는 일이 실제로 일어날 때 LED가 깜빡이도록 하세요. LED가 많을수록 좋습니다. C 코드에서 디버깅용 "printf"를 넣는 것과 조금 비슷합니다. 같은 요령으로, 디버깅용 Verilog 코드를 조금 추가하세요. 비트스트림 (bitstream)을 FPGA에 로드하고, 아무 엉뚱한 시도를 해 보세요. 그러면 이 LED들이 버그가 무엇과 관련됐는지 드러내 줄지도 모릅니다.

이 방법을 쓸 때 기억할 중요한 점 하나. 조건이 충족되면 LED가 그에 반응해 적어도 20 ms는 켜져 있도록 하세요. 그러지 않으면 사람 눈에 보이지 않습니다.

LED 방법의 더 정교한 버전은 오실로스코프를 사용하는 것입니다. 이 연재의 첫 페이지에서 오실로스코프를 사서 익숙해지라고 권한 이유가 여기 있습니다. 아이디어는 FPGA 내부의 여러 신호를 오실로스코프 프로브로 닿을 수 있는 출력 핀에 연결하는 것입니다. 실제 프로젝트에서는 PCB에 전용 커넥터를 두고 보드 설계자가 FPGA의 쓰이지 않는 핀을 많이 모아 두는 일도 드물지 않습니다. 디버깅에 쓸 수 있도록요.

온칩 로직 애널라이저 (on-chip logic analyzer)와 비교하면 오실로스코프가 시시해 보일 수 있습니다. 한 번에 두 신호를 관찰할 수 있고, 더 비싼 오실로스코프라면 몇 개 더 볼 수 있습니다. 대역폭도 제한적이라 아주 빠른 신호는 놓칠 수 있습니다. 그런데도 때로는 손에 있는 가장 강력한 도구일 때가 있습니다. 특히 반복되는 신호 패턴이 있을 때, 오실로스코프로 지켜보면 무엇이 잘못되는지 잡아내는 데 도움이 됩니다.

또 때로는 순수한 하드웨어 수준에서 무슨 일이 일어나는지 이해하기 위해 오실로스코프가 필요합니다. 전압이 올바른가? 디지털 신호가 정상으로 보이는가? 전압 신호에 이상한 노이즈가 있는가?

그리고 여기서 가장 현실적인 종류의 디버깅으로 이어집니다. 우리가 믿고 싶은 것보다 더 자주, 문제는 정말 시시한 하드웨어 문제입니다. 특히 특정 프로젝트를 위해 개발된 PCB에서요. 공급 전압 하나가 불안정할 수 있고, 이따금 짧은 스파이크나 전압 강하가 있을 수 있습니다. 클록 발진기가 예상과 다르게 동작해서 부적절한 클록 신호를 만들어 낼 수도 있습니다. 정교한 이론을 세우기 전에 이런 것들을 먼저 살펴보는 것이 언제나 좋습니다.

그래서 FPGA 설계를 잘하고 싶다면, 둘 다 조금씩 필요합니다. FPGA 내부에서 데이터를 가져오는 무균적인 방법과, 하드웨어를 직접 만지며 손을 더럽히는 방법 모두요.

그리고 잊지 마세요. 올바르게 사용하기만 한다면 정말로 효율적인 디버깅 도구는 단 하나뿐입니다. 바로 여러분 자신의 머리입니다.

프로그래밍 언어

프로그래밍이 FPGA 설계와 직접 관련은 없지만, 일을 끝내려면 최소한의 프로그래밍 능력이 필요합니다. 예를 들어 위에서 시뮬레이션용 테스트 데이터를 특별히 작성한 프로그램이나 스크립트 (script)로 만드는 경우가 많다고 말했습니다.

익혀 둘 만한 프로그래밍 언어 몇 가지를 언급하겠습니다. 제가 권하는 모든 것 중에서 이 부분이 사실 더 쉬운 편이고, 이것을 첫날부터 다 알아야 할 필요는 전혀 없습니다. 아니면 평생 그럴 필요가 없을 수도 있습니다.

MATLAB 같은 다른 언어들도 유용할 수 있습니다. 특히 시뮬레이션용 테스트 데이터를 만드는 데요.

프로그래밍 언어를 아는 것의 또 다른 측면은 소프트웨어 팀과 공통 언어를 만들어 준다는 것입니다. 임베디드 프로세서나 컴퓨터가 관련된 FPGA 프로젝트에서 이는 중요합니다. 어느 정도 프로그래머인 것 자체가 소프트웨어 담당자들과 소통하는 데 도움이 됩니다. 또한 FPGA 설계와 통신하는 저수준 프로그램을 직접 작성하는 것이, 명세를 주고 다른 사람에게 맡기는 것보다 흔히 더 쉽습니다.

그리고 일반적인 권고로, Git을 사용하는 법을 배우고 쓰세요. 혼자 프로젝트를 하더라도요. 버전 관리는 관리자를 기쁘게 하려는 것만이 아니라, 뭔가를 시도했다가 돌아가고, 돌아간 것을 후회하게 해 주는 값진 도구입니다. 그리고 장난을 다 치고 나면 그 지저분한 흔적이 하나도 보이지 않습니다.

저는 버전 트리를 그래픽으로 보기 위해 gitk를 씁니다.

이것으로 이 연재의 세 번째 페이지를 마칩니다. 다음 페이지에서도 실전 역량을 다루지만, 처음 시작하기에는 덜 중요한 것들입니다. 핵심은 그것들이 언제, 어떻게 중요해질 수 있는지 설명하는 것입니다.

이 페이지는 영어 원문을 기계 번역한 것입니다. 의문이 드는 부분이 있으면 원문을 참조하시기 바랍니다.
Copyright © 2021-2026. All rights reserved. (852e87d4)