Add UART receive module

Add the recieve half of the UART. It's more or less the inverse of the
transmit half, except we manage the state explicitly. I originally did
this in hopes that yosys would recode the FSM, but it doesn't like the
subtraction in the D* states. I left in the async reset anyway since it
reduces the LUT count.

Signed-off-by: Sean Anderson <seanga2@gmail.com>
This commit is contained in:
Sean Anderson 2023-02-28 22:19:22 -05:00
parent 3f61f85a1f
commit a549fca957
4 changed files with 208 additions and 0 deletions

View File

@ -143,6 +143,7 @@ MODULES += pmd_dp83223
MODULES += pmd_dp83223_rx
MODULES += scramble
MODULES += uart_tx
MODULES += uart_rx
MODULES += wb_mux
.PHONY: test

View File

@ -205,6 +205,12 @@ This module implements a scrambler as described in ANSI X3.264-1995 section
A standard UART transmit module, accepting AXI-stream. 8n1 only. Supports
115,200 and 4,000,000 baud.
=== `uart_rx`
A standard UART receive module, outputting AXI-stream. 8n1 only. Supports
115,200 and 4,000,000 baud. Properly detects breaks as (single) frame errors,
and ignores runt start bits.
=== `wb_mux`
This implements a simple Wishbone mux, allowing a single master to access

121
rtl/uart_rx.v Normal file
View File

@ -0,0 +1,121 @@
// SPDX-License-Identifier: AGPL-3.0-Only
/*
* Copyright (C) 2022 Sean Anderson <seanga2@gmail.com>
*
* 8n1@115200; no one uses anything else (and neither do I)
*/
`include "common.vh"
module uart_rx (
input clk,
input rst,
output reg [7:0] data,
input ready,
output reg valid,
input rx,
/* Run at 4M for testing */
input high_speed,
/* No one ready */
output reg overflow,
/* Missing stop bit */
output reg frame_error
);
/* 1085 cycles, for 115200 baud with a 125 MHz clock */
parameter SLOW_FULL = 11'h78c;
parameter SLOW_HALF = 11'h202;
/* 31 cycles, for 4M baud with a 125 MHz clock */
parameter FAST_FULL = 11'h68e;
parameter FAST_HALF = 11'h34c;
localparam ERROR = 11;
localparam IDLE = 10;
localparam START = 9;
localparam D0 = 8;
localparam D1 = 7;
localparam D2 = 6;
localparam D3 = 5;
localparam D4 = 4;
localparam D5 = 3;
localparam D6 = 2;
localparam D7 = 1;
localparam STOP = 0;
reg [3:0] state, state_next;
reg [10:0] lfsr, lfsr_next;
reg [7:0] bits, bits_next, data_next;
reg ready_last;
reg valid_next, overflow_next, frame_error_next;
always @(*) begin
state_next = state;
lfsr_next = { lfsr[9:0], lfsr[10] ^ lfsr[8] };
bits_next = bits;
data_next = data;
valid_next = valid && !ready_last;
overflow_next = 0;
frame_error_next = 0;
case (state)
IDLE: if (!rx) begin
state_next = START;
lfsr_next = high_speed ? FAST_HALF : SLOW_HALF;
end
START: if (&lfsr) begin
state_next = rx ? IDLE : D0;
lfsr_next = high_speed ? FAST_FULL : SLOW_FULL;
end
D0, D1, D2, D3, D4, D5, D6, D7: if (&lfsr) begin
lfsr_next = high_speed ? FAST_FULL : SLOW_FULL;
bits_next = { rx, bits[7:1] };
state_next = state - 1;
end
STOP: if (&lfsr) begin
lfsr_next = high_speed ? FAST_FULL : SLOW_FULL;
if (rx) begin
state_next = IDLE;
if (valid_next)
overflow_next = 1;
else
data_next = bits;
valid_next = 1;
end else begin
frame_error_next = 1;
state_next = ERROR;
end
end
ERROR: if (&lfsr) begin
lfsr_next = high_speed ? FAST_FULL : SLOW_FULL;
if (rx)
state_next = IDLE;
end
endcase
end
always @(posedge clk) begin
lfsr <= lfsr_next;
bits <= bits_next;
data <= data_next;
end
always @(posedge clk, posedge rst) begin
if (rst) begin
state <= IDLE;
valid <= 0;
ready_last <= 0;
overflow <= 0;
frame_error <= 0;
end else begin
state <= state_next;
valid <= valid_next;
ready_last <= ready;
overflow <= overflow_next;
frame_error <= frame_error_next;
end
end
endmodule

80
tb/uart_rx.py Normal file
View File

@ -0,0 +1,80 @@
# SPDX-License-Identifier: AGPL-3.0-Only
# Copyright (C) 2023 Sean Anderson <seanga2@gmail.com>
import cocotb
from cocotb.binary import BinaryValue
from cocotb.clock import Clock
from cocotb.triggers import FallingEdge, Timer
from cocotb.utils import get_sim_time, get_sim_steps
from .axis_replay_buffer import recv_packet
BAUD = 4e6
BIT_STEPS = get_sim_steps(1 / BAUD, 'sec', round_mode='round')
def as_bits(c):
for _ in range(8):
yield c & 1
c >>= 1
@cocotb.test(timeout_time=1, timeout_unit='ms')
async def test_rx(uart):
uart.clk.value = BinaryValue('Z')
uart.rst.value = 1
uart.ready.value = 1
uart.rx.value = 1
uart.high_speed.value = 1
await Timer(1)
uart.rst.value = 0
await cocotb.start(Clock(uart.clk, 8, units='ns').start())
await FallingEdge(uart.clk)
async def putchar(c):
for bit in (0, *as_bits(c), 1):
uart.rx.value = bit
await Timer(BIT_STEPS)
msg = b"Hell\0"
signals = {
'clk': uart.clk,
'data': uart.data,
'valid': uart.valid,
'ready': uart.ready,
}
await cocotb.start(recv_packet(signals, msg))
for c in msg:
await putchar(c)
overflows = 0
frame_errors = 0
async def count_errors():
nonlocal overflows
nonlocal frame_errors
while True:
await FallingEdge(uart.clk)
overflows += uart.overflow.value
frame_errors += uart.frame_error.value
monitor = await cocotb.start(count_errors())
uart.rx.value = 0
await Timer(BIT_STEPS * 20)
uart.rx.value = 1
await Timer(BIT_STEPS)
assert frame_errors == 1
uart.ready.value = 0
await putchar(0xFF)
await putchar(0)
assert overflows == 1
uart.ready.value = 1
await recv_packet(signals, (0xFF,))
monitor.kill()