이전 글에 이어서 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가 결정됩니다.
이렇게 생성된 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_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에 사용하면 됩니다.
Tx controller RTL design
Tx controller를 설계하기 전에, 먼저 FSM(finite-state machine)에 대해 알아봅시다.
통신 Protocol은 ‘약속’입니다. 상호 합의된 순서대로 데이터를 주고받기 때문에 신뢰성 있게 통신할 수 있고, 그렇기 때문에 controller는 protocol대로 통신을 진행해야 합니다.
FSM은 유한한 state로 통신을 진행할 때, 각각의 state의 상태나 입/출력, 다음 state로 넘어가기 위한 조건 등을 정의한 모델입니다. Protocol의 state를 확인해 볼까요?
통신하지 않을 때는 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
위 시뮬레이션 결과는 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