[Verilog] UART 컨트롤러를 만들어 보자 (3) - 수신기 구현

pikamon·2022년 4월 28일
0

Verilog

목록 보기
9/12

Verilog HDL로 각 모듈을 하나씩 만들어 보자.


두 번째로 수신기를 만들어 보자.

0. 매크로 정의

아래 글에서 구현한 매크로 파일을 이용한다.

https://velog.io/@pikamon/Verilog-10#0-매크로-정의

1. RTL 구현

RTL 모듈을 만들어 보자.

  • Uart8Receiver.v
`ifndef __UART_RECEIVER_V__
`define __UART_RECEIVER_V__

`include "inc/Defines.vh"
`include "src/Uart8Receiver.v"

module Uart8Receiver #(
        parameter OVERSAMPLING  = `OVERSAMPLING_16, // use 16x as default
        parameter DATA_BITS     = `DATA_BITS_8      // use 8bit as default
    )(
        input wire                   clk,  // baud rate
        input wire                   en,   // chip enable
        input wire                   in,   // rx line
        output reg [DATA_BITS - 1:0] out,  // received data
        output reg                   done, // end on transaction
        output reg                   busy, // transaction is in process
        output reg                   err   // error while receiving data
    );
    
    reg [2:0] state        = `STATE_RESET;
    reg [1:0] shiftReg     = 0; // shift reg for input signal state
    reg [7:0] receivedData = 0; // temporary storage for input data

    localparam CLK_CNT_WIDTH   = $clog2(OVERSAMPLING);
    localparam DATA_BITS_WIDTH = $clog2(DATA_BITS);

    reg [CLK_CNT_WIDTH - 1:0]   clockCount = 0; // count clocks for oversampling
    reg [DATA_BITS_WIDTH - 1:0] bitIdx     = 0; // index

    initial begin
        out  <= 0;
        err  <= 0;
        done <= 0;
        busy <= 0;
    end

    always @ (posedge clk) begin
        shiftReg = { shiftReg[0], in }; // shift left and append in

        if (!en) begin
            state <= `STATE_RESET;
        end

        case (state)
            `STATE_RESET: begin
                out  <= 0;
                done <= 0;
                busy <= 0;
                err  <= 0;
                bitIdx       <= 0;
                clockCount   <= 0;
                receivedData <= 0;
                if (en) begin
                    state <= `STATE_IDLE;
                end
            end

            `STATE_IDLE: begin
                done <= 1'b0;
                if (&clockCount) begin
                    out  <= 8'b0;
                    busy <= 1'b1;
                    err  <= 1'b0;
                    bitIdx       <= 3'b0;
                    clockCount   <= 4'b0;
                    receivedData <= 8'b0;
                    state <= `STATE_DATA_BITS;
                end else if (!(&shiftReg) || |clockCount) begin
                    // Check bit to make sure it's still low
                    if (shiftReg == 2'b11) begin
                        err <= 1'b1;
                        state <= `STATE_RESET;
                    end
                    clockCount <= clockCount + 1;
                end
            end

            // Wait 8 full cycles to receive serial data
            `STATE_DATA_BITS: begin
                if (&clockCount) begin // save one bit of received data
                    clockCount <= 4'b0;
                    // TODO: check the most popular value
                    receivedData[bitIdx] <= shiftReg[0];
                    if (&bitIdx) begin
                        bitIdx <= 3'b0;
                        state <= `STATE_STOP_BIT;
                    end else begin
                        bitIdx <= bitIdx + 3'b1;
                    end
                end else begin
                    clockCount <= clockCount + 1;
                end
            end

            /*
            * Baud clock may not be running at exactly the same rate as the
            * transmitter. Next start bit is allowed on at least half of stop bit.
            */
            `STATE_STOP_BIT: begin
                if (&clockCount || (clockCount >= 4'h8 && !(|shiftReg))) begin
                    state <= `STATE_IDLE;
                    done <= 1'b1;
                    busy <= 1'b0;
                    out <= receivedData;
                    clockCount <= 4'b0;
                end else begin
                    clockCount <= clockCount + 1;
                    // Check bit to make sure it's still high
                    if (!(|shiftReg)) begin
                        err <= 1'b1;
                        state <= `STATE_RESET;
                    end
                end
            end

            default:
                state <= `STATE_IDLE;
        endcase
    end

endmodule

`endif /*__UART_RECEIVER_V__*/

state machine을 이용하여 동작을 관리한다. in 라인에 LOW가 들어올 때까지 대기하다가 clockCount가 한 바퀴 돌 때까지 LOW로 읽히면 정상적인 LOW 신호로 간주하고 데이터를 읽는다. 그리고 stop bit까지 읽은 후, 바이트 형태로 변환된 데이터를 출력으로 내보낸다.

(코드를 다시 확인해보니 데이터 비트를 8비트로 간주하고 구현한 부분이 있어서, 추후에 수정이 필요함.)

2. Testbench 구현

위 모듈을 테스트하기 위한 테스트벤치를 만들어 보자.

  • Uart8Receiver_tb.v
`timescale 10ns/1ns

`include "inc/Defines.vh"
`include "src/Uart8Receiver.v"

module tbUart8Receiver();

	parameter CLOCK_RATE   = `CLOCK_RATE_100MHZ;
	parameter BAUD_RATE    = `BAUD_RATE_115200;
	parameter OVERSAMPLING = `OVERSAMPLING_16;
	parameter DATA_BITS    = `DATA_BITS_8;

    localparam MAX_RATE_RX = CLOCK_RATE / (2 * BAUD_RATE * OVERSAMPLING);
	localparam CLOCK_RX = MAX_RATE_RX * OVERSAMPLING * 2;

	reg rxClk = 0;
	reg en	  = 1;
	reg in    = 1;

	wire [DATA_BITS - 1:0] out;
	wire                   done;
	wire                   busy;
	wire                   err;

	Uart8Receiver #(
        .OVERSAMPLING(OVERSAMPLING),
        .DATA_BITS(DATA_BITS)
	) test (
		.clk(rxClk),
        .en(en),
        .in(in),
        .out(out),
        .done(done),
        .busy(busy),
        .err(err)
    );

	initial begin
		$dumpfile("test.vcd");
		$dumpvars(-1, test);
    end

	initial begin
		begin // (0x55)
			#CLOCK_RX in = 0; // start bit

			#CLOCK_RX in = 1; // data bit (0x55)
			#CLOCK_RX in = 0;
			#CLOCK_RX in = 1;
			#CLOCK_RX in = 0;
			#CLOCK_RX in = 1;
			#CLOCK_RX in = 0;
			#CLOCK_RX in = 1;
			#CLOCK_RX in = 0;

			#CLOCK_RX in = 1; // stop bit
		end

		begin // (0x96)
			#CLOCK_RX in = 0; // start bit

			#CLOCK_RX in = 0; // data bit (0x96)
			#CLOCK_RX in = 1;
			#CLOCK_RX in = 1;
			#CLOCK_RX in = 0;
			#CLOCK_RX in = 1;
			#CLOCK_RX in = 0;
			#CLOCK_RX in = 0;
			#CLOCK_RX in = 1;

			#CLOCK_RX in = 1; // stop bit
		end
	end

	always begin
		#MAX_RATE_RX rxClk = ~rxClk;
//		#0.5 rxClk = ~rxClk;
	end

	initial begin
		#25000 $finish;
	end

endmodule

in 라인으로 0x55, 0x96에 해당되는 UART 신호를 입력했을 때 이를 원래의 데이터로 잘 변환하는지를 보면 된다.

원래는 보레이트 생성기를 같이 생성해서 검증해야 하는데, 타 모듈 종속성 없이 수신기만 검증하고 싶어서 rx 클럭을 테스트벤치에서 직접 만들도록 했다.

3. Makefile 정의

개발 편의를 위해 Makefile을 생성하였다.

  • Makefile
all: Uart8Receiver

Uart8Receiver: clean
	iverilog -o test.vvp testbench/Uart8Receiver_tb.v
	vvp test.vvp
	gtkwave test.vcd

clean:
	rm -f test.vcd
	rm -f test.vvp

make만 입력하면 자동으로 clean 후 시뮬레이션을 돌릴 수 있다.

4. 실행 및 결과

make를 입력한다.

make

그러면 gtkwave가 실행되며, 아래와 같이 입출력을 확인할 수 있다.

out 라인을 보면 0x55와 0x96이 출력되는 것으로 보아 수신기가 정상 동작했다고 볼 수 있다.

busy 라인을 중점적으로 보면, 최초에 in 라인으로 start bit가 들어오고 data bit가 시작되는 시점에서 busy 라인이 HIGH로 올라가는 것을 볼 수 있다. 그리고 데이터 여덟 비트 및 stop bit를 수신하면서 다시 LOW로 내려간다. 이 때 done 라인이 HIGH로 올라가며 out 라인에 데이터가 출력되는 것을 볼 수 있다.

profile
개발자입니당 *^^* 깃허브 https://github.com/pikamonvvs

0개의 댓글