베릴로그로 클럭 경계 동기화기(Clock Boundary Synchronizer) 구현하기

이번 게시글에서는 클럭 동기화기의 베릴로그 기술(description)에 대해 살펴볼 것입니다. 디지털 전자공학의 기초로부터 준안정성(metastability) 개념을 떠올릴 수 있을 것입니다. 이는 디지털 신호가 1도 0도 아닌 미확정(indeterminate) 상태에 들어가는 달갑지 않은 상태입니다. 클럭 경계 동기화기의 목적은 이 준안정 상태를 줄이는 것입니다.

동기화기 논리를 더 잘 이해하기 위해, 몇 가지 일반적인 질문에 답하는 것으로부터 시작하겠습니다:

클럭 경계란 무엇인가요?

많은 논리 설계들이 동기식으로 설계되며, 이는 모든 논리가 동일한 주기로 동작함을 의미합니다. FPGA에서 이는 일련의 레지스터로 구현됩니다. 데이터는 흔히 클럭의 상승 에지에서 레지스터 간에 전송됩니다. 이 단일 클럭에 동기화된 모든 논리는 동일한 클럭 도메인에 있다고 일컬어집니다. FPGA 내에는 여러 개의 도메인이 있을 수 있으며, 외부 마이크로컨트롤러와 같은 FPGA 외부 도메인도 존재할 수 있습니다.

준안정성이란 무엇인가요?

클럭 경계를 명확하게 유지해야 하는 주요 이유 중 하나는 레지스터(플립플롭)가 중간 상태에 들어가는 상황인 준안정 상태를 방지하기 위함입니다. 모든 구성요소들이 동시에, 예를 들면 클럭의 상승 에지에서 전환하기 때문에 단일 클럭 도메인의 경우 이를 완화할 수 있습니다. 이때, 모든 구성요소들은 다음 상승 에지가 발생하기 전까지 안정화될 시간이 있습니다. 게이트 전파 지연이 지나치지만 않으면, 준안정 상태가 발생할 가능성은 줄어듭니다.

동기화기란 무엇인가요?

신호가 클럭 경계를 교차할 때 문제의 상황이 발생합니다. 이러한 경계는 경미하게 음이 맞지 않는 두 개의 악기와 같습니다. 에지는 서로 상대적으로 움직입니다. 때로는 상승 에지가 일치하지만 어떨 때는 멀리 떨어져 있습니다. 경계 사이에 있는 신호가 (시간적으로) 서로 가까우면 준안정성 문제가 발생합니다. 이는 수신 클럭 도메인의 레지스터를 읽기 전 안정화할 시간이 부족하기 때문에 위험합니다. 극단적인 경우, 이 불안정성이 클럭 도메인 전반에 걸쳐 전파되어 예측 불가능한 동작을 초래할 수 있습니다.

동기화기의 블록도

동기화기의 베릴로그 구현을 나타낸 블록도가 그림 1에 있습니다. 이는 비동기 신호가 레지스터를 통해 전달되는 고전적 설계입니다. 각 레지스터는 새로운 도메인의 마스터 클럭에 의해 동기화됩니다. 다시 말해, 비동기 신호는 새로운 클럭 도메인에서 동기화된 신호로 변합니다. 여기에서 레지스터의 개수는 설계 상의 절충점으로 이는 통계적인 문제입니다. 동기화 레지스터가 많으면 준안정성이 발생할 가능성을 줄일 수 있습니다. 그러나 지나치게 복잡한 설계는 소중한 FPGA 자원을 낭비하고 속도를 저하시킬 수 있습니다.

그림 1: 신호가 클럭 경계를 넘을 때 동기화 체인이 이를 조정함.

동기화기 모듈의 베릴로그 파라미터

이제 이 게시글의 끝부분에 첨부된 베릴로그 코드의 주요 내용을 살펴보겠습니다. 먼저 그림 2에 있는 동기화기의 아스키 아트(ASCII art) 표현이 눈에 띌 것입니다. 이어서 인스턴스화 예제와 모든 입력 및 출력 신호에 대해 간단한 설명을 제공하는 헤더가 이어집니다.

그림 2: 동기화기의 아스키 아트(ASCII art) 표현.

그림 2의 중간 부분에서 파라미터와 기본값을 볼 수 있습니다. 파라미터는 베릴로그의 숨겨진 보석 중 하나로, 파라미터가 인스턴스화될 때 RTL(Register Transfer Level) 설계의 동작을 수정할 수 있습니다. 모든 파라미터는 기본값이 정해지며, 필요에 따라 이를 재정의할 수 있습니다. 이 예제에서 파라미터는 동기화기에 포함된 플립플롭의 개수를 결정합니다. 기본값은 2이며, 인스턴스화 예제에서 볼 수 있듯이 2를 유지하고 있습니다.

//** Sample Instantiation ******************************************************
//
//    synchronizer #( .SYNC_STAGES(2) )    // Example with 2 stage flip-flop
//    synchronizer(                        // Instance name
//        .clk(clk),                       // Your clock signal
//        .async_in(async_in),             // Asynchronous input signal
//        .sync_out(sync_out),             // Safe synchronized signal
//        .rise_edge_tick(rise_edge_tick), // Pulse on rise edge of safe signal
//        .fall_edge_tick(fall_edge_tick)  // Pulse on fall edge of safe signal
//    );
//

이 베릴로그 코드 라인이 동기화 플립플롭을 선언하고 초기화합니다:


reg [SYNC_STAGES-1:0] sync_regs = {SYNC_STAGES{1'b0}}; // init with zeros

이는 [SYNC_STAGES-1:0]에서 (벡터) 폭이 결정되는 단일 레지스터입니다. 예를 들어, SYNC_STAGES 파라미터를 2로 설정하면 해당 벡터는 [1:0]인 것으로 이해할 수 있습니다. {SYNC_STAGES{0’b0}}; 문장은 베릴로그 복제 연산자입니다. 이는 sync_reg 플립플롭의 개별 플립플롭을 이진 0으로 초기화하는 효과적인 방법입니다.

플립플롭처럼 보이지 않을 수 있지만, FPGA 합성 도구는 이 기술(description)을 그림 3에 있는 동기화기의 VIVADO 회로 표현에서 볼 수 있듯이 플립플롭의 계단식 연결로 해석할 것입니다.

이 특정 예제에서는, 다음 코드 예제에서 볼 수 있듯이 레지스터를 정의한 다음 시프트 연산을 사용하면 회로를 쉽게 확장할 수 있습니다. 파라미터를 사용하여 플립플롭의 개수를 결정합니다.

그림 3: VIVADO에서 생성된 동기화기 회로의 회로도.

동기화 체인과 상승/하강 에지 검출기는 always @(posedge clk) 블록 안에 구현되어 있습니다. 논블로킹(non-blocking) 코드의 사용에 유의하십시오. always 블록 내의 모든 작업은 동일한 시간에 발생하며, 이것이 해당 설계에서 매우 중요하게 고려하는 동기식 동작입니다.

    always @(posedge clk) begin
        sync_regs <= {sync_regs[SYNC_STAGES-2:0], async_in};    // shift left
        sync_out <= sync_regs[SYNC_STAGES-1];
        rise_edge_tick <= (sync_out != sync_regs[SYNC_STAGES-1]) & (sync_regs[SYNC_STAGES - 1] == T) ? T : F; 
        fall_edge_tick <= (sync_out != sync_regs[SYNC_STAGES-1]) & (sync_regs[SYNC_STAGES - 1] == F) ? T : F; 
    end

동작(민감도)는 수신 도메인과 관련된 마스터 클럭의 상승 에지에 의해 제어됩니다. 수신 도메인과 관련된 마스터 클럭의 상승 에지가 동작(민감도)룰 제어합니다. 베릴로그 결합 연산자는 마이크로컨트롤러의 왼쪽 시프트 연산과 동일한 작업을 수행합니다. 왼쪽 최상위 비트를 버리고, 최하위 비트에 최신 비동기 입력을 넣습니다.

각 벡터 및 비트 연산은 SYNC_STAGES 파라미터의 값에 따라 달라집니다. 따라서 동기화 플립플롭의 개수는 모듈을 수정하지 않고도 쉽게 변경됩니다. 이러한 변경은 모듈이 인스턴스화되면 이루어지며, 설계된 바와 같이 런타임에는 변경할 수 없습니다.

모듈의 sync_out은 시프트 레지스터의 왼쪽 최상위 비트에서 가져옵니다. 이 부분도 논블로킹 코드이니 유의하십시오. 이 예제에는 두 개의 동기화기와 하나의 레지스터가 있으며, sync_out 값은 3번째 클럭 펄스의 상승 에지에 고정됩니다.

코드의 마지막 두 줄을 사용하여 상승 및 하강 에지 검출기를 만듭니다. 베릴로그에서 if else 구문을 간략하게 구현하는 방식인, 삼항 연산자(ternary operator) ? :를 사용하였습니다. 그림 1을 참조하면 해당 구문을 이해할 수 있을 것입니다. 에지 트리거 블록은 Output Reg(직전 출력)와 곧 출력될 값을 모니터링합니다. 클럭의 상승 에지에서 해당 블록은 Output Reg의 값이 변경되었는지 판단합니다. 값이 서로 다르고 새로운 출력이 양이면 상승 에지가 발생한 것입니다. 하강 에지도 동일한 방식입니다.

테스트벤치(Testbench)

초점을 전환해 RTL의 성능에 대해 알아보겠습니다. 성능에는 테스트벤치 결과 뿐만 아니라 실제 성능이 포함되어 있습니다. 하드웨어는 Digilent Basys 3에 설치된 Xilinx Artix FPGA 용 VIVADO 플랫폼 위에 개발되었습니다. 이와 관련해서는 추후에 더 다루겠습니다.

테스트벤치 결과가 그림 4에 나와있습니다. 클럭 1은 동기 클럭, 클럭 2는 비동기 클럭으로 각각의 시뮬레이션 주파수는 100MHz와 59MHz입니다. sync_out이 clk_1의 상승 에지 세 번 후에 in 신호를 따르는 것을 관찰할 수 있습니다. sync_out은 복제이긴 하지만, clk_1의 상승 에지 세 번 만큼 늘어진 지연된 복제 신호입니다. 또한 rise_edge_tick과 fall_edge_tick도 clk_1의 상승 에지와 동기화되어 있는 것을 관찰할 수 있습니다. "tick"이라는 용어는 한 클럭 주기 동안 하이를 유지하는 펄스 신호를 의미합니다. 상승 에지 트리거 또는 하강 에지 트리거는 뒤에 오는 에지 트리거형(edge triggered) 회로에 사용될 수 있으며, sync_out 신호는 레벨 민감 회로에 사용될 수 있습니다.

그림 4: 동기화기 회로의 테스트벤치 결과.

그림 5는 (구형) Digilent Analog Discovery를 사용하여 실제 신호를 포착한 것입니다. 100MHz 신호는 순식간에 지나가서 포착하기 어렵기 때문에 확인이 쉽지 않습니다. 그러나 여러 다른 지연 및 펄스 신호는 확인할 수 있습니다. 이 정지 화면에서는 확인할 수 없지만, 신호 간에 관찰 가능한 지터가 있습니다. 이는 테스트 펄스를 생성하는 Analog Discovery의 신호 발생기가 BASYS 3 기판의 100MHz 클럭과 동기화되지 않았기 때문입니다. 하지만 이것이 핵심으로, 우리는 클럭 경계를 교차하였으며, 동기화기는 제대로 작동하였습니다.

그림 5: Digilent BASYS 3 FPGA에 구현된 동기화기의 실제 동작.

마무리 소감

최신 FPGA "컴파일러"는 전력 소비, 속도 및 리소스 소비에 대해 사용자가 할당한 가중치에 따라 코드를 최적화하도록 설계되었습니다. 합성 도구가 로직의 일부를 제거하는 것은 흔한 일입니다. 예를 들어, rise_edge_tick과 fall_edge_tick이 필요할 수도 있지만, 이 출력들을 연결하지 않은 채 동기화기를 인스턴스화하면, 자동으로 제거되어 사용하지 않는 로직(unused logic) 경고를 보게 될 것입니다.

이런 종류의 경고는 일반적으로 무시해도 되지만, 로직을 개발할 때는 이런 경고의 원인을 찾고 해결하기 위해 최선을 다해야 합니다. 이 동기화기의 상승 및 하강 에지 검출기를 만드는 동안 이 문제에 부딪혔었습니다. 원래는, sync_out의 마지막 값을 기억하기 위해 레지스터를 사용했었습니다. 이는 마이크로컨트롤러 세계에서는 지극히 자연스러운 코딩 방식입니다:

If (present != last) do something

이는 좋은 해결책이었고 작동했지만, 합성 도구는 계속해서 로직이 제거되었다는 경고를 표시했습니다. 알고 보니 제가 잘못된 관점에서 생각하고 있었기 때문입니다. 이를 이해하기 위해 그림 1을 다시 보겠습니다. 마지막 플립플롭이 특별합니다. 이 플립플롭은 그 자체로는 동기화기의 일부라기보다 모듈의 출력 레지스터로, 다른 FPGA 로직에 동기 안정성을 제공하는 원천입니다. 여기서 안정적이라는 것은 레지스터가 always @(posedge clk) 블록에 포함된 마스터 클럭에 의해 제어된다는 것을 의미합니다.

생각해 보면, 제가 "직전 출력"을 기록하는 데 사용했던 레지스터는 중복이었습니다. 해당 신호는 sync_out 레지스터에 저장되어 있었습니다. 합성 도구는 이를 관측하고 중복(redundancy)에 대해 경고한 것이었습니다.

이 관측이 그림 1에서 보여주는 상승 및 하강 블록 입력의 위치를 설명하는 데 도움이 될 수도 있습니다. 이 블록이 마지막 플립플롭의 입력과 출력을 모니터링합니다. 만약 신호가 다르면, 블록은 적절한 펄스를 송신합니다.

감사합니다,

APDahlen

저자 정보

미합중국 해안경비대(USCG) 소령(LCDR)으로 전역한 Aaron Dahlen은 DigiKey에서 애플리케이션 엔지니어로 근무하고 있습니다. 27년간의 군 복무 동안 기술자 및 엔지니어로서 쌓아온 그 만의 전자 및 자동화에 대한 지식은 12년간의 교단을 통해 (상호 연계되어) 더욱 향상되었습니다. 미네소타 주립대학, Mankato에서 전기공학 석사(MSEE) 학위를 받은 Dahlen은 ABET(Accreditation Board for Engineering and Technology, 미국 공학 기술 인증 위원회) 공인 전기공학 과정을 가르치고, EET(Electrical Engineering Technology, 전기공학 기술) 과정의 프로그램 조정관으로 일했으며, 군 전자 기술자에게 부품 수준의 수리에 대해 가르쳤습니다. 미네소타 주 북부의 집으로 돌아와 이런 류의 연구와 글쓰기를 즐기고 있습니다.




synchronizer.v

//******************************************************************************
//
// Module: synchronizer
//
//  This code is derived from Xilinx UltraFast Design Methodology Guide for
//  the Vivado Design Suite UG949 (v2014.1) April 2, 2014
//
//  This RTL is subject to Term and Conditions associated with the 
//  DigiKey TechForum. Refer to:
//  https://www.digikey.com/en/terms-and-conditions?_ga=2.92992980.817147340.1696698685-139567995.1695066003
//
//  Should you find an error, please leave a comment in the forum space below. 
//  If you are able, please provide a recommendation for improvement.
//
//******************************************************************************
//           ______________________________________________
//          |                                              |
//          | synchronizer                                 |
//          |______________________________________________|
//          |                                              |
//          |    Parameters and defaults                   |
//          |        SYNC_STAGES = 2                       |
//          |                                              |
//      ----| async_in                            sync_out |----
//          |                               rise_edge_tick |----
//      ----| clk                           fall_edge_tick |----
//          |______________________________________________|
//
//** Description ***************************************************************
//
//  A synchronizer used to safely transition a signal across a clock domain.
//
//** Sample Instantiation ******************************************************
//
//    synchronizer #( .SYNC_STAGES(2) )    // Example with 2 stage flip flop
//    synchronizer(                        // Instance name
//        .clk(clk),                       // Your clock signal
//        .async_in(async_in),             // Asynchronous input signal
//        .sync_out(sync_out),             // Safe synchronized signal
//        .rise_edge_tick(rise_edge_tick), // Pulse on rise edge of safe signal
//        .fall_edge_tick(fall_edge_tick)  // Pulse on fall edge of safe signal
//    );
//
//** Parameters ****************************************************************
//
//  Parameter SYNC_STAGES identifies the number of Flip-Flops to be used in
//  the synchronizer. The default value is set to 2 which is generally 
//  considered adequate for most applications.
//
//** Signal Inputs: ************************************************************
//
//  1) clk: high speed clock (typically 100 MHz) for synchronous operation
//
//  2) async_in: The input signal to be synchronized to clk
//
//** Signal Outputs ************************************************************
//
//  1) sync_out: this is a "safe" signal synchronized to clk
//
//  2) rise_edge_tick: pulse on rising edge of synchronized signal
//
//  3) fall_edge_tick: pulse on falling edge of synchronized signal
//
//** Comments ******************************************************************
//
//  N/A
//
//******************************************************************************

module synchronizer #(parameter SYNC_STAGES = 2)(
    input async_in,
    input clk,
    output reg sync_out,
    output reg rise_edge_tick,
    output reg fall_edge_tick
);

//** CONSTANT DECLARATIONS *****************************************************

    /* General shortcuts */
        localparam T = 1'b1;
        localparam F = 1'b0;
        
//** BODY **********************************************************************

    reg [SYNC_STAGES-1:0] sync_regs = {SYNC_STAGES{1'b0}};  // init with zeros

    always @(posedge clk) begin
        sync_regs <= {sync_regs[SYNC_STAGES-2:0], async_in};    // shift left
        sync_out <= sync_regs[SYNC_STAGES-1];

        rise_edge_tick <= (sync_out != sync_regs[SYNC_STAGES-1]) & (sync_regs[SYNC_STAGES - 1] == T) ? T : F; 
        fall_edge_tick <= (sync_out != sync_regs[SYNC_STAGES-1]) & (sync_regs[SYNC_STAGES - 1] == F) ? T : F; 
  
    end
endmodule

tb_synchronizer.v

//`timescale 1ns / 1ps
//*************************************************************
//
// Description:
//
//    This testbench provides a stimulus to the synchronizer module.
//    The graphical signal timing diagram serves as the main
//    debugging tool.
//
//*************************************************************
module tb_synchronizer();

    /* Module Inputs */
        reg clk_1;
        reg clk_2;
        reg in;

    /* Module Outputs */
        wire sync_out;
        wire rise_edge_tick;
        wire fall_edge_tick;

    /* Testbench Specific */


//** CONSTANT DECLARATION ************************************

    /* Local */

    /* Clock simulation */
        localparam clock_1_T_ns = 10;     // 100 MHz
        localparam clock_2_T_ns = 17;     // 59 MHz
    /* General shortcuts */
        localparam T = 1'b1;
        localparam F = 1'b0;

//** SYMBOLIC STATE DECLARATIONS ******************************

//** SIGNAL DECLARATIONS **************************************

//** INSTANTIATE THE UNIT UNDER TEST (UUT)*********************

    synchronizer #( .SYNC_STAGES(2) )    // Example with 2 stage flip flop
    synchronizer(                        // Instance name
        .clk(clk_1),                     // Your clock signal
        .async_in(in),                   // Asynchronous input signal
        .sync_out(sync_out),             // Safe synchronized signal
        .rise_edge_tick(rise_edge_tick), // Pulse on rise edge of safe signal
        .fall_edge_tick(fall_edge_tick)  // Pulse on fall edge of safe signal
    );


//** ASSIGN STATEMENTS ****************************************

//** CLOCK ****************************************************

    always begin
        clk_1 = T;
        #(clock_1_T_ns/2);
        clk_1 = F;
        #(clock_1_T_ns/2);
    end

    always begin
        clk_2 = T;
        #(clock_2_T_ns/2);
        clk_2 = F;
        #(clock_2_T_ns/2);
    end

//** UUT Tests ************************************************ 

    initial begin

        initial_conditions();

    /* Begin tests */
        delay_N_clk_2_cycles(5);
        in = T;
        delay_N_clk_2_cycles(5);
        in = F;
        delay_N_clk_2_cycles(5);
        in = T;
        delay_N_clk_2_cycles(5);
        in = F;
        delay_N_clk_2_cycles(5);
        
//        $monitor($realtime, " count = %d", cnt);

        $finish;
    end

//** FUNCTIONS ************************************************ 

//** Tasks **************************************************** 

    task initial_conditions; begin
        repeat(5) @(posedge clk_1)
        in = F;
        end
    endtask


    task delay_N_clk_1_cycles(input integer N); begin
        repeat(N) @(posedge clk_1);
        end
    endtask
    
       task delay_N_clk_2_cycles(input integer N); begin
        repeat(N) @(posedge clk_2);
        end
    endtask
    
endmodule



영문 원본: Implementing a Clock Boundary Synchronizer in Verilog