[Verilog] UART RTL design 2

이전 글에 이어서 UART RTL design을 계속해 보겠습니다.

UART RTL design

CLK gen RTL design

다음으로 baud rate를 설정해 주는 clk gen을 설계해 봅시다. Block diagram을 보면 Tx/Rx에 각각의 clk gen이 있는데, 기능은 비슷하기 때문에 tx clk gen을 먼저 설계해 보겠습니다.

module uart_tx_clkgen (
     input  wire        presetn
    ,input  wire        pclk

    ,input  wire [ 8:0] bit_div
    ,input  wire [ 2:0] pre_scale
    ,input  wire [ 4:0] bit_mult

    ,output wire        tx_uclk
);

    reg         r_uclk;
    reg  [ 8:0] r_prescale;
    reg  [13:0] r_divisor_1;
    reg  [22:0] r_divisor_2;
    reg  [21:0] r_dividercnt;

    always @(negedge presetn or posedge pclk) begin
        if (!presetn)    r_prescale  <= 9'd0;
        else begin
            case (pre_scale)
                3'd0   : r_prescale <= 9'd2  ;
                3'd1   : r_prescale <= 9'd4  ;
                3'd2   : r_prescale <= 9'd8  ;
                3'd3   : r_prescale <= 9'd16 ;
                3'd4   : r_prescale <= 9'd32 ;
                3'd5   : r_prescale <= 9'd64 ;
                3'd6   : r_prescale <= 9'd128;
                3'd7   : r_prescale <= 9'd256;
                default: r_prescale <= r_prescale;
            endcase
        end
    end

    //r_divisor_1  =  { 2^(pre_scale+1) x (bit_mult + 1) } ]
    always @(negedge presetn or posedge pclk) begin
        if (!presetn) r_divisor_1 <= 14'd0; 
        else          r_divisor_1 <= r_prescale * (bit_mult + 'b1);
    end

    //r_divisor_2  =  [ (bit_div) x  { 2^(pre_scale+1) x (bit_mult + 1) } ]
    always @(negedge presetn or posedge pclk) begin
        if (!presetn) r_divisor_2 <= 23'd0; 
        else          r_divisor_2 <= r_divisor_1 * bit_div;
    end

    //half cycle of serial clock = r_divisor_2 / 2
    always @(negedge presetn or posedge pclk) begin
        if (!presetn)                          r_dividercnt <= 22'd0; 
        else if (r_dividercnt !=  r_divisor_2) r_dividercnt <= (r_dividercnt + 'b1);
        else                                   r_dividercnt <= 22'd0; 
    end

    wire [22:0] r_divisor_2_half;
    assign      r_divisor_2_half = r_divisor_2 >> 1;

    //serial clock generation
    always @(negedge presetn or posedge pclk) begin
        if (!presetn)                             r_uclk <= 'b0;
        else if (r_dividercnt < r_divisor_2_half) r_uclk <= 'b0;
        else                                      r_uclk <= 'b1;
    end

    assign tx_uclk = r_uclk;

endmodule

Baud rate의 설정 수식은 다음과 같습니다.

Fbaud = Fpclk / [ 2 ^ ( pre_scale + 1 ) x { bit_div x ( bit_mult + 1 ) } ]

Module에 사용되는 pclk의 주기와 register setting에 따라 baud rate가 결정됩니다.

Baud rate setting
Baud rate setting

이렇게 생성된 clock(uclk)을 Tx controller에서 사용하게 됩니다.

    always @(negedge presetn or posedge uclk) begin
        if   ~~~~
        else ~~~~
    end

하지만 이렇게 설계하면 한 IP에 두 clock domain으로 나뉘기 때문에 SDC(Synopsys Design Constraints) setting 하는 데 어려움이 있습니다. 합성을 위해서는 constraint 파일에 clock에 대한 정보를 모두 입력해야 하는데, uclk을 사용하면 그에 대한 설정을 추가로 입력해야 합니다. 결론적으로, 한 IP에는 한 clock으로만 작동하는 것이 좋습니다.

uclk 사용 시 clock domain
uclk 사용 시 clock domain

uclk_en

그래서 uclk enable signal(uclk posedge signal)을 만들어 설계를 진행합니다.

    wire        tx_uclk;
    wire        tx_uclk_en;
    
    reg         r_tx_uclk;
    always @(negedge presetn or posedge pclk) begin
        if (!presetn) begin 
            r_tx_uclk <= 1'b0;
        end
        else begin
            r_tx_uclk <= tx_uclk;
        end
    end

    assign tx_uclk_en =  tx_uclk & ~r_tx_uclk;

이 logic을 통해 1 pclk의 uclk_en signal을 만들 수 있고, 이를 Tx controller에 사용하면 됩니다.

uclk_en signal
uclk_en signal

Tx controller RTL design

Tx controller를 설계하기 전에, 먼저 FSM(finite-state machine)에 대해 알아봅시다.

통신 Protocol은 ‘약속’입니다. 상호 합의된 순서대로 데이터를 주고받기 때문에 신뢰성 있게 통신할 수 있고, 그렇기 때문에 controller는 protocol대로 통신을 진행해야 합니다.

FSM은 유한한 state로 통신을 진행할 때, 각각의 state의 상태나 입/출력, 다음 state로 넘어가기 위한 조건 등을 정의한 모델입니다. Protocol의 state를 확인해 볼까요?

Tx controller FSM
Tx controller FSM

통신하지 않을 때는 IDLE 상태입니다. Tx controller가 통신을 시작하면 START 상태가 되고 1 uclk 뒤에 8-bit의 데이터를 내보냅니다. 만약 parity를 설정했다면 1-bit의 parity bit가 나가고, 그렇지 않으면 바로 STOP_1로 넘어갑니다. 해당 통신을 2-stop으로 설정했다면 STOP_1에서 STOP_2를 거쳐 IDLE 상태가 되고, 설정하지 않았다면 그냥 IDLE 상태가 됩니다.

그러면 이를 기반으로 Tx controller를 설계해 봅시다. 먼저 input output port 선업입니다.

module uart_tx_ctrl (
     input  wire        pclk
    ,input  wire        presetn

    ,input  wire        uclk_en

    ,input  wire [ 7:0] uart_data
    ,input  wire        uart_en
    ,input  wire        complete_clr
    ,input  wire        parity_en
    ,input  wire        stop_en

    ,output wire        complete

    ,output wire        uart_out
);

여기서 complete는 1-byte transfer가 끝나면 CPU에 intr로 알리기 위한 signal입니다. 다음으로 자료형 선언입니다.

    //===================================================================
    // Local Parameters
    //===================================================================
    localparam IDLE        = 3'h0,
               START       = 3'h1,
               DATA        = 3'h2,
               PARITY      = 3'h3,
               STOP        = 3'h4,
               TRANSFINISH = 3'h5;

    reg  [ 2:0] r_cur_st;
    reg  [ 2:0] r_nxt_st;
    reg  [ 7:0] r_bitcnt;

    reg         r_uart;
    reg         r_complete;
    reg  [ 7:0] r_shift;    //shift register

    wire        data_end;   //DATA state end
    assign data_end = (r_bitcnt == 8'h8);

각각의 자료형에 대한 설명은 이어서 계속하겠습니다. 다음으로 FSM 설계입니다.

    //cur_st
    always @(posedge pclk or negedge presetn) begin
        if (!presetn)          r_cur_st <= IDLE;
        else if (complete_clr) r_cur_st <= IDLE;
        else                   r_cur_st <= r_nxt_st;
    end

    //FSM
    always @(*) begin
        case (r_cur_st)
            IDLE        : begin
                if (uart_en & uclk_en) begin
                     r_nxt_st <= START;
                end
                else r_nxt_st <= IDLE;
            end
            START       : begin
                if (uclk_en) begin
                     r_nxt_st <= DATA;
                end
                else r_nxt_st <= START;
            end
            DATA        : begin
                if (data_end & parity_en) begin
                     r_nxt_st <= PARITY;
                end
                else if (data_end & !parity_en & stop_en) begin
                     r_nxt_st <= STOP;
                end
                else if (data_end & !parity_en & !stop_en) begin
                     r_nxt_st <= TRANSFINISH;
                end
                else r_nxt_st <= DATA;
            end
            PARITY      : begin
                if (stop_en & uclk_en) begin
                     r_nxt_st <= STOP;
                end
                else if (uclk_en) begin
                     r_nxt_st <= TRANSFINISH;
                end
                else r_nxt_st <= PARITY;
            end
            STOP        : begin
                if (uclk_en) begin
                     r_nxt_st <= TRANSFINISH;
                end
                else r_nxt_st <= STOP;
            end
            TRANSFINISH : begin
                if (complete_clr) begin
                     r_nxt_st <= IDLE;
                end
                else r_nxt_st <= TRANSFINISH;
            end
            default     : begin
                r_nxt_st <= IDLE;
            end
        endcase
    end

여기서 TRANSFINISH가 stop_1 state이고, STOP이 stop_2 state라고 이해하시면 되겠습니다.

왜 그런지 모르겠는데, 대부분의 IP은 nxt_st(next state)를 1 clock 밀어서 cur_st(current state)로 쓰더라고요;;; 이유를 아시는 분 메일로 알려주시면 ㅠㅠ.

다음으로 bitcnt(bit counter) 설계입니다.

    //BITCNT
    wire data_st;
    assign data_st = (r_cur_st == DATA);
    always @(posedge pclk or negedge presetn) begin
        if (!presetn)               r_bitcnt <= 8'h0;
        else if (complete_clr)      r_bitcnt <= 8'h0;
        else if (data_st & uclk_en) r_bitcnt <= (r_bitcnt + 1);
        else                        r_bitcnt <= r_bitcnt;
    end

bitcnt는 DATA state에서 8-bit 동안 tx 할 data를 shift 하면서 1-bit 씩 transfer 하기 위해 사용합니다. 그러면 바로 shift register 설계를 확인해 볼까요?

    //shift reg
    always @(posedge pclk or negedge presetn) begin
        if (!presetn)               r_shift <= 8'h0;
        else if (data_st) begin
            if (uclk_en)            r_shift <= {1'b0,r_shift[7:1]};
            else                    r_shift <= r_shift;
        end
        else                        r_shift <= uart_data;
    end

이렇게 설계하면 APB register에서 들어온 data가 1-bit 씩 shift 되면서 r_shift에 저장됩니다. 그러면 r_shift가 어떻게 output으로 나가는 걸까요?

    //UART tx
    always @(posedge pclk or negedge presetn) begin
        if (!presetn) r_uart <= 1'b1;
        else begin
            case (r_cur_st)
                IDLE        : begin
                    if (uart_en & uclk_en) begin
                         r_uart <= 1'b0;
                    end
                    else r_uart <= 1'b1;
                end
                START       : begin
                    if (uclk_en) begin
                         r_uart <= r_uart;
                    end
                    else r_uart <= r_uart;
                end
                DATA        : begin
                    r_uart <= r_shift[0]; //shift register
                end
                PARITY      : begin
                    r_uart <= 1'b0;       //수정 필요 (parity)
                end
                STOP        : begin
                    r_uart <= 1'b1;
                end
                TRANSFINISH : begin
                    r_uart <= 1'b1;
                end
                default     : begin
                    r_uart <= 1'b1;
                end
            endcase
        end
    end

수정이 필요한 곳이 있는데, parity가 enable 돼 있으면 계산된 parity가 output으로 나가는 겁니다. parity 계산은 직접 해보시는 것도 좋을 것 같네요!! (귀찮아서 하지 않은 건 안 비밀;;;;;)

마지막으로 complete intr와 output port assign입니다.

    //complete intr
    always @(posedge pclk or negedge presetn) begin
        if (!presetn)               r_complete <= 1'b0;
        else if (complete_clr)      r_complete <= 1'b0;
        else if ((r_cur_st == TRANSFINISH) & uclk_en) begin
                                    r_complete <= 1'b1;
        end
        else                        r_complete <= r_complete;
    end

    assign complete = r_complete;
    assign uart_out = r_uart;

한 번 전체적으로 확인해 볼까요?

module uart_tx_ctrl (
     input  wire        pclk
    ,input  wire        presetn

    ,input  wire        uclk_en

    ,input  wire [ 7:0] uart_data
    ,input  wire        uart_en
    ,input  wire        complete_clr
    ,input  wire        parity_en
    ,input  wire        stop_en

    ,output wire        complete

    ,output wire        uart_out
);

    //===================================================================
    // Local Parameters
    //===================================================================
    localparam IDLE        = 3'h0,
               START       = 3'h1,
               DATA        = 3'h2,
               PARITY      = 3'h3,
               STOP        = 3'h4,
               TRANSFINISH = 3'h5;

    reg  [ 2:0] r_cur_st;
    reg  [ 2:0] r_nxt_st;
    reg  [ 7:0] r_bitcnt;

    reg         r_uart;
    reg         r_complete;
    reg  [ 7:0] r_shift;    //shift register

    wire        data_end;   //DATA state end
	
	//cur_st
    always @(posedge pclk or negedge presetn) begin
        if (!presetn)          r_cur_st <= IDLE;
        else if (complete_clr) r_cur_st <= IDLE;
        else                   r_cur_st <= r_nxt_st;
    end

    //FSM
    always @(*) begin
        case (r_cur_st)
            IDLE        : begin
                if (uart_en & uclk_en) begin
                     r_nxt_st <= START;
                end
                else r_nxt_st <= IDLE;
            end
            START       : begin
                if (uclk_en) begin
                     r_nxt_st <= DATA;
                end
                else r_nxt_st <= START;
            end
            DATA        : begin
                if (data_end & parity_en) begin
                     r_nxt_st <= PARITY;
                end
                else if (data_end & !parity_en & stop_en) begin
                     r_nxt_st <= STOP;
                end
                else if (data_end & !parity_en & !stop_en) begin
                     r_nxt_st <= TRANSFINISH;
                end
                else r_nxt_st <= DATA;
            end
            PARITY      : begin
                if (stop_en & uclk_en) begin
                     r_nxt_st <= STOP;
                end
                else if (uclk_en) begin
                     r_nxt_st <= TRANSFINISH;
                end
                else r_nxt_st <= PARITY;
            end
            STOP        : begin
                if (uclk_en) begin
                     r_nxt_st <= TRANSFINISH;
                end
                else r_nxt_st <= STOP;
            end
            TRANSFINISH : begin
                if (complete_clr) begin
                     r_nxt_st <= IDLE;
                end
                else r_nxt_st <= TRANSFINISH;
            end
            default     : begin
                r_nxt_st <= IDLE;
            end
        endcase
    end

    //BITCNT
    wire data_st;
    assign data_st = (r_cur_st == DATA);
    always @(posedge pclk or negedge presetn) begin
        if (!presetn)               r_bitcnt <= 8'h0;
        else if (complete_clr)      r_bitcnt <= 8'h0;
        else if (data_st & uclk_en) r_bitcnt <= (r_bitcnt + 1);
        else                        r_bitcnt <= r_bitcnt;
    end

    //shift reg
    always @(posedge pclk or negedge presetn) begin
        if (!presetn)               r_shift <= 8'h0;
        else if (data_st) begin
            if (uclk_en)            r_shift <= {1'b0,r_shift[7:1]};
            else                    r_shift <= r_shift;
        end
        else                        r_shift <= uart_data;
    end

    //UART tx
    always @(posedge pclk or negedge presetn) begin
        if (!presetn) r_uart <= 1'b1;
        else begin
            case (r_cur_st)
                IDLE        : begin
                    if (uart_en & uclk_en) begin
                         r_uart <= 1'b0;
                    end
                    else r_uart <= 1'b1;
                end
                START       : begin
                    if (uclk_en) begin
                         r_uart <= r_uart;
                    end
                    else r_uart <= r_uart;
                end
                DATA        : begin
                    r_uart <= r_shift[0]; //shift register
                end
                PARITY      : begin
                    r_uart <= 1'b0;       //수정 필요 (parity)
                end
                STOP        : begin
                    r_uart <= 1'b1;
                end
                TRANSFINISH : begin
                    r_uart <= 1'b1;
                end
                default     : begin
                    r_uart <= 1'b1;
                end
            endcase
        end
    end

    //complete intr
    always @(posedge pclk or negedge presetn) begin
        if (!presetn)               r_complete <= 1'b0;
        else if (complete_clr)      r_complete <= 1'b0;
        else if ((r_cur_st == TRANSFINISH) & uclk_en) begin
                                    r_complete <= 1'b1;
        end
        else                        r_complete <= r_complete;
    end

    assign data_end = (r_bitcnt == 8'h8);
    assign complete = r_complete;
    assign uart_out = r_uart;

endmodule
Simulation result
Simulation result

위 시뮬레이션 결과는 Tx data가 0xA, stop_en과 parity_en이 둘 다 설정되지 않았을 때의 결과입니다. uclk_en마다 state가 넘어가는 것을 확인할 수 있습니다.

0x0(IDLE) -> 0x1(START) -> 0x2(DATA) -> 0x5(TRANSFINISH)

DATA state일 때 data가 1-bit 씩 shift 되어 r_shift에 저장되고 그에 따라 tx signal이 변하는 것을 확인할 수 있습니다. 마지막으로 transfer가 끝나면 complete intr가 켜집니다.

문제점

물론, 이 controller는 완전한 IP가 아닙니다. 왜냐하면 1-byte 씩 보낼 때마다 intr를 내보내기 때문입니다. 일반적인 module의 경우, FIFO(First In, First Out) 메모리가 달려있어서 enable을 하면 사용자가 일부러 disable 하지 않는 이상 FIFO 메모리가 비워질 때까지 데이터를 계속 보냅니다. FIFO가 비워지면 CPU에 intr로 데이터를 요청하거나 DMA(Direct Memory Access)에 데이터를 요청하는 신호를 보냅니다.

참고: Realtek UART

Similar Posts