마이크로컨트롤러와 FPGA 간 견고한 SPI 인터페이스 구현하기: 1부 - FPGA의 도전 과제

이 연속 게시글에서는 필드 프로그래머블 게이트 어레이(FPGA)의 직렬 주변기기 인터페이스(SPI)에 마이크로컨트롤러(uC)를 연결하는 것과 관련하여 살펴볼 것입니다. 주된 목표는 FPGA를 더 쉽게 제어할 수 있도록 만드는 것입니다. Digilent의 Basys 3 Artix-7 FPGA 개발 기판에서 개발된 베릴로그 구현과 이에 해당하는 아두이노 나노 에브리에서 개발된 마이크로컨트롤러 코드를 살펴볼 것입니다. 둘 모두, 검증된 16MHz의 SPI 클럭으로 동작하는 16비트 순환 중복 검사(Cyclic Redundancy Check, CRC)로 패킷화된 데이터를 전송합니다. 베릴로그 구현은 최소한의 수정으로 다른 플랫폼에도 이식이 가능하도록 Xilinx에 특정된 종속성을 피해야 합니다. 그러나 이 플랫폼 간의 이식 가능성은 아직 테스트되지는 않았습니다.

그림 1: Digilent Basys 3와 아두이노 나노 에브리를 사용해 uC를 FPGA의 SPI 인터페이스에 연결한 실험실에서의 실험. uC는 Basys 3 기판의 SSD(Seven Segment Display)와 16개의 LED를 멀티플렉싱하고 있습니다.

많은 사람들이 디지털 논리 수업의 일부로 FPGA를 접하게 되며, 회로도 입력 도구를 사용하여 논리 구성 요소를 사용한 간단한 조합 회로를 개발합니다. 대게는 베릴로그나 VHDL을 소개하는 FPGA 전용 수업으로 학업을 이어갑니다. 일부는 고급 논리 수업을 듣거나 캡스톤 프로젝트에 FPGA를 적용합니다. 그러나 안타깝게도, 독립적인 모듈을 넘어 여러 모듈을 하나의 큰 시스템에 통합하는 단계로 나아가는 사람은 거의 없습니다. 거기에는 이유가 있습니다.

이 연재글은 여러분이 FPGA의 시스템 설계와 가까워 지는 데 도움을 주기 위해 작성되었습니다. 속도가 요구되는 시간 민감적, 병렬적, 결정론적 회로에는 FPGA의 최적의 속성 사용을 제안합니다. 그런 다음 마이크로컨트롤러의 강점인 유연하고, 프로그래밍이 상대적으로 쉬우며, 무선 및 클라우드 기능을 포함하는 통신 스택을 활용합니다. 또다른 관점에서 이 상황을 보면, FPGA를 적당히 고속인 데이터 버스를 통해 연결된 강력한 마이크로컨트롤러 주변 장치라고 생각할 수 있습니다.

대상으로 하는 독자는 베릴로그로 FPGA를 프로그래밍할 수 있을 뿐만 아니라 마이크로컨트롤러도 프로그래밍할 수 있는 개인 또는 팀입니다. FPGA와 uC에서의 동시 작업이 가능하도록 팀을 짤 수 있는 캡스톤 프로젝트를 하는 학생들에게 적합할 수 있습니다.

우리는 FPGA에 중점을 두고 있기 때문에, 마이크로컨트롤러 관련 내용을 최소화하는 것이 바람직합니다. 아두이노 나노 에브리는 일반적이고 널리 알려진 마이크로컨트롤러이기에 선택하였습니다. 이 글의 독자 대부분은 마이크로컨트롤러와 C 프로그래밍에 아주 익숙할 것입니다.

단순히 베릴로그와 uC 코드를 제시하는 대신, 설계 과정에서 발생하는 추론과 도전 과제에 대해 살펴볼 것입니다. 그 결과 교육적인 게시글이 연속적으로 작성되겠지만, 유익할 것이라 믿습니다.

시스템 정의 예시

시스템이란 함께 동작하는 부품의 모음입니다. FPGA 애플리케이션에서는 데이터 수집, 필터링 및 제어용 모듈과 정보를 사용자에게 보여주는 방법 또는 대형 자동화 시스템으로의 통합이 포함될 수 있습니다.

이 용어를 보다 잘 정의하기 위해, FPGA와 uC를 기반으로 하는 도전적인 예시 하나를 고려해 보겠습니다. 3상 400Hz 파형에 대한 유효 전력과 무효 전력을 측정하는 시스템을 구축하려 한다고 가정해 보겠습니다. 설계 요구 사항은 신호 간 위상 차의 정확한 측정 뿐만 아니라 RMS 전압, RMS 전류를 제공하는 것입니다. 또한 최대 20kHz의 라인 고조파도 측정해야 한다고 가정해 보겠습니다.

최상의 성능을 보장하기 위해 전압과 전류 측정을 동시에 수행한다고 가정한다면, 이는 6개의 독립적인 아날로그-디지털 변환기(Analog to Digital Converter, ADC)가 필요함을 의미합니다. 나이퀴스트 샘플링은 최소 초당 40,000 샘플의 속도로 측정해야 합니다. 따라서 초당 240,000 샘플의 속도가 필요합니다. 이 외에도, 시스템은 RMS 및 위상각 계산도 수행해야 합니다. RMS 계산 방식은 장기적인 안정성을 유지하면서도 과도 상태에 빠르게 응답할 수 있도록 단기 및 장기 적분을 제공하는 필터링을 포함해야 합니다. 여기에 고조파를 결정하기 위한 고속 푸리에 변환(Fast Fourier Transform, FFT)을 추가하면 금상첨화입니다.

이러한 시스템을 설계하는 방법에는 여러 가지가 있습니다. 하나의 고성능 uC 또는 조직화된 여러 개의 uC들로 이 작업을 수행할 수 있습니다. 하지만 이는 이 게시글이 중점을 두는 바가 아닙니다. 대신, 데이터 수집 및 필터 부분은 입문자용 FPGA로도 할 수 있다는 것을 알 수 있습니다.

병렬 구조의 FPGA 기반 시스템은 이러한 작업을 쉽게 수행할 수 있습니다. 개별 모듈을 설계하는 것은 그리 어렵지 않습니다. 실제로, 많은 분들은 이미 FPGA에 ADC 하나는 통합해 보셨을 것입니다. 진정한 도전 과제는 모든 모듈을 함께 연결해 데이터를 필요할 때 필요한 곳으로 이동하는 것입니다.

FPGA 글루(glue)

개인적으로, 제가 경험한 FPGA 학습에 있어 가장 어려웠던 점은 uC 프로그래밍 기법을 잊기 어렵다는 것이었습니다. 베릴로그와 VHDL은 하드웨어 기술 언어인 반면, C와 같은 언어는 하드웨어 의존성을 없애기 위해 추상화를 사용하는 절차식 언어입니다. FPGA 하드웨어 기술에 내재된 '모든 것을 한 번에’라는 유사성을 파악하는 데 예상보다 더 오랜 시간이 걸렸습니다.

예제를 시작하기 위해, 6개의 ADC를 시각화할 필요가 있습니다. 이들을 서로 연결하는 데 사용되는 다양한 제어 및 데이터 라인도 시각화해야 합니다. 다음으로, 중간 결과를 저장하는 레지스터, RMS 계산의 제곱 및 합산 연산을 수행하는 곱셈기과 덧셈기, 그리고 이러한 작업들을 조정하는 다른 많은 상태 머신(state machine) 하드웨어를 시각화해야 합니다.

이 FPGA 기반 하드웨어의 글루는 레지스터 전달 레벨(Register Transfer Level, RTL) 설계 방법론입니다. 레지스터 전달(RT)이라는 용어는 데이터를 한 레지스터에서 다른 레지스터로 전송하기 위해 보통 레지스터 사이에 많은 조합 논리(combinational logic)를 삽입하여 사용하는 제어 메커니즘을 의미합니다. 이는 마이크로프로세서에서 사용되는 파이프라인 처리 절차와 관련이 있습니다. 예를 들어, 첫 번째 클럭 주기에 데이터가 덧셈기에 전달되면, 두 번째 클럭 주기에 덧셈기가 연산을 수행하고, 세 번째 주기에 데이터는 메모리로 전송됩니다. 이 예시에서의 컨트롤러는 RT 처리 절차를 개시해야 하는 상태 머신입니다. 우리 목적상, 모든 레지스터가 단일 클럭 도메인 내에 있다고 가정하겠습니다.

이는 반복할 가치가 있는 아이디어입니다.

우리의 RTL 설계에서는 하나의 상태 머신 또는 상태 머신 집합이 레지스터 간 데이터 전송을 제어 및 조정할 것입니다. 모든 연산은 동일한 클럭 도메인 내에 있다고 가정하겠습니다. 클럭 도메인을 교차하기 위해 동기화기(synchronizers)를 사용하는 것에 대한 관련 게시글을 살펴볼 좋은 시점일 수 있습니다.

각 레지스터는 메모리라는 점을 기억하십시오. 이 용어는 단일 D형 플립플롭에서 FPGA의 대형 블록 메모리 중 하나를 인스턴스화하는 것에 이르기까지 모든 것에 적용됩니다. 간단한 8비트 레지스터를 사용해 이 개념을 살펴보겠습니다.

아래 나와 있는 예시는 우리의 타이밍 조건을 포함한 모든 RTL 구성 요소를 담고 있습니다. Q 출력은 레지스터입니다. @(posedge clk) 구문과 베릴로그의 논블록킹 <= 연산자 사용으로 명확히 알 수 있듯이 레지스터 업데이트는 클럭의 양의 에지에 동기화됩니다.

module reg_8bit(
    input clk,
    input load,
    input wire [7:0] D, 
    output reg [7:0] Q 
);
always @(posedge clk) begin
    if (load) 
        Q <= D;      
end
endmodule

load 신호의 특성을 고려하십시오. load 신호는 클럭의 상승 에지 이전에 안정되어 있어야만 합니다. 모든 신호가 동일한 클럭 도메인에 속해 있고, load 신호 자체는 상태 머신의 레지스터된 출력에 의해 구동된다고 가정하면, 합성(Synthesis) 툴은 이러한 극적인 타이밍 안정성을 충족하기 위해 최선을 다할 것입니다.

“load” 명령어 라인은 클럭의 상승 에지에서 어써트(assert)된다는 점에 유의하십시오. D에 존재하는 데이터는 클럭의 다음 상승 에지에서 Q가 될 것입니다. 이는 초보 프로그래머들에게 있어 함정으로 예상치 못한 한 클럭의 지연이 발생합니다. 이러한 RTL 동작을 계속 추적하기 위해서는 상태와 다음 상태 관점에서 생각하는 것이 중요합니다. 마지막으로, load 신호의 폭(주기)에 주의하십시오.

동기식 RTL 시스템에서, 신호가 온(on, 활성화)되는 시간은 클럭의 주기(상승 에지에서 상승 에지까지)와 같습니다. 이는 클럭의 상승 에지에서 관련 상태 머신이 업데이트되는 것을 이해하면 설명됩니다.

프로그래밍 팁: 본 게시글에 언급된 RTL 설계 제약은 FPGA 성능에 제한을 가하여 불필요한 FPGA 자원의 사용을 초래할 수 있습니다. 그러나 모든 신호를 @(posedge clk) 조건으로 레지스터화 하면 시스템 안정성은 일반적으로 향상됩니다. 이렇게 시작하는 것이 좋으며 나중에 필요에 맞게 수정할 수 있습니다.

스트로브된 레지스터 전송

이전 예시에서 “load” 신호의 온 시간(폭)은 달라질 수 있다는 것을 알았습니다. 동기식 시스템이므로, 폭은 항상 클럭과 상관관계에 있습니다. 최소 온 시간은 시스템 클럭의 한 주기입니다. 이 짧은 신호에는 스트로브, 틱, 또는 펄스를 포함한 이러한 유형의 신호에 대한 여러 다른 이름이 있습니다. 이 시리즈 게시글에서는 스트로브(strobe)라는 용어를 사용하겠습니다.

앞서 우리는 RTL을 일련의 레지스터를 사용하는 설계 방법론으로 정의하였습니다. 동일 클럭 도메인에 있는 것으로 여겨지는 레지스터들 간에 데이터가 전송됩니다. 모든 논리 연산은 도메인의 클럭 주기 내에 완료되어야 한다는 조건하에 조합 논리는 레지스터들 사이에 배치되어 데이터를 수정합니다. 예를 들어, 100MHz 클럭의 경우, 모든 FPGA 신호들이 10ns 이내에 정리되어야 다음 클럭 이벤트를 대비합니다.

레지스터를 제어하기 위해 RTL 프로세스에는 컨트롤러 또는 조직화된 컨트롤러들의 집합이 필요합니다. 레지스터를 제어하는 한 가지 방법은 각 컨트롤러가 스트로브 신호를 발생시켜 해당 레지스터를 진행시키는 것입니다.

아래는 간단한 스트로브 기반 컨트롤러의 예시입니다. 이 RTL은 100MHz 클럭으로부터 20kHz 속도의 스트로브 신호를 발생시킬 것이며, 이는 모듈러-5000 카운터입니다. 스트로브를 초당 20,000회 반복하는 ADC와 같은 프로세스의 실행에 사용할 수 있다는 점에서 컨트롤러입니다.

module pulse_20k (                          // mod 5000 for a 100 MHz clock
    input clk,
    output reg zero_strobe,
    output reg [12:0] count                 // 13 bits to hold numbers from 0 to 4999
);
always @(posedge clk) begin
    zero_strobe <= 1'b0; 			        // default
    count <= count + 1;
    if (count >= 4999) begin                // Count starts at 0
        count <= 13'd0;  
        zero_strobe <= 1'b1;
    end 
end
endmodule

스트로브와 0 카운트는 동일한 클럭 사이클에 발생한다는 점에 유의하십시오. 상태 머신을 상태 / 다음 상태의 관점에서 바라보지 않으면 직관적이지 않을 수 있습니다. (count >= 4999) 조건은 카운터가 4999 상태에 있으면 참일 것입니다. 이 예시에서 4999는 mod-5000 카운터의 최대 카운트입니다. 클럭의 다음 상승 에지에서 카운터는 다시 0으로 변경되는 동시에 zero_strobe를 어써트시킵니다. 이는 60분이 최대인 시계와 유사합니다.

이제 우리는 좀 더 복잡한 컨트롤러의 설계를 시작할 수 있습니다. 이 기사의 후속 편에서 필시 그렇게 할 것입니다. 우선, 이 간단한 시간에 기반한 컨트롤러는 그 목적을 달성했습니다. 이 컨트롤러가 클락 도메인에 동기화된 스트로브를 생성할 것이라는 것을 알고 있으며, 해당 스트로브는 이후 더 큰 RTL 시스템의 일부로 사용될 수도 있습니다.

더블 버퍼

현재 우리는 단일 클럭 도메인 내에서 동기식 레지스터 전송을 유지해야 한다는 점에 유의하면서 간단히 RTL 연산을 살펴보았습니다. 이제 레지스터의 폭이 다른 경우의 RTL 연산을 살펴보겠습니다. 이는 특히 SPI와 같은 통신 프로토콜을 사용할 때 흔하게 발생합니다. 이 경우, SPI는 일반적으로 연속적인 바이트로 데이터를 처리하는 반면, 관련된 FPGA 하드웨어는 2에서 4바이트 폭으로 처리할 것입니다.

10비트 PWM(Pulse Width Modulator, 펄스 폭 변조기)를 예로 들어보겠습니다. reg_B1과 reg_B0가 주어지면, 다음과 같은 연산을 수행할 수 있습니다:

assign reg_PWM = {reg_B1, reg_B0}[9:0];

이는 적절한 결합(concatenation) 연산이지만, 잘못될 가능성도 큽니다. 문제는 reg_B1과 reg_B0의 업데이트 시간입니다. 둘의 업데이트간에 시간 지연이 있으면 reg_PWM의 값이 잘못될 수 있습니다.

reg_B1과 reg_B0를 SPI 인터페이스로 가져온다고 가정해 보겠습니다. 이 경우 두 레지스터는 서로 다른 시점에 업데이트됩니다. 최악의 경우로, PWM 명령이 255에서 256으로 증가하는 상황을 가정해 보겠습니다. 어느 한 순간 PWM이 25%(10비트 PWM에서는 255)의 듀티 사이클로 동작하고 있습니다. 이때 SPI에 의해 reg_B1은 업데이트되었는데 reg_B0은 그대로라고 가정해 보겠습니다. 이제 듀티 사이클이 50%(10비트 PWM에서는 511)로 급격히 증가합니다. SPI가 reg_B0를 업데이트할 때까지 이 잘못된 값은 유지됩니다. 잠깐 동안의 명령이더라도 이런 급격한 PWM 변화는 시스템 안정성에 원치 않는 영향을 줄 수 있습니다. 폐쇄 루프 시스템에서 모터를 PWM으로 제어하고 있다면 발생할 수 있는 불안정성을 고려해야 합니다.

해결책은 그림 2와 같이 더블 버퍼라고 알려진 중간 레지스터를 추가하는 것입니다. 이를 통해 reg_B1과 reg_B0가 자연스럽게 업데이트될 수 있습니다. 그 후, 두 레지스터가 모두 업데이트된 것이 확인되면, 컨트롤러는 그 내용을 더블 버퍼로 알려진 2바이트 레지스터에 전송할 수 있습니다. 이렇게 하면 PWM과 같은 장치는 중간 값이 아닌 알고 있는 전체 레지스터로 업데이트될 수 있습니다.

그림 2: 더블 버퍼 RTL을 나타내는 블록도

1부 마무리

1부에서는 FPGA 설계를 위한 몇 가지 시스템 수준의 고려 사항을 살펴보았습니다. 비록 전체 목록은 아니지만, 필수적인 RTL 설계 방법론, 단일 클럭 도메인을 가지는 동기식 설계, 그리고 스트로브 사용에 대해 명확하게 이해하셨기를 바랍니다. 이 정보는 다은 2부에서 소개할 SPI 모듈을 이해하는 데 도움이 될 것입니다. 해당 게시글은 SPI 모듈의 출력 스트로브가 마스터 마이크로컨트롤러에서 FPGA로의 데이터 흐름을 제어하는 데 어떻게 사용될 수 있는지에 대한 실마리를 제공합니다.

2부가 게시되었습니다.

여러분의 의견과 제안을 환영합니다. 특히 고급 RTL 시스템 설계 방법론에 대한 추가 논의를 환영합니다.

감사합니다,

APDahlen



영문 원본: Implementing a Robust Microcontroller to FPGA SPI Interface: Part 1 - FPGA Challenges