diff --git a/.gitignore b/.gitignore index 3d0dba2..4a0f966 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,6 @@ __pycache__ *.fst results.xml test_profile.pstat + +# doc artifacts +*.html diff --git a/Makefile b/Makefile index d3cd15e..328d2af 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ # Copyright (C) 2022 Sean Anderson Q = 1 +ADOC = asciidoctor SYNTH = yosys PNR = nextpnr-ice40 ICARUS = iverilog @@ -124,6 +125,7 @@ endef MODULES += axis_replay_buffer MODULES += axis_mii_tx +MODULES += axis_wb_bridge MODULES += descramble MODULES += hub MODULES += hub_core @@ -153,6 +155,17 @@ test: $(addsuffix .fst,$(MODULES)) $(addsuffix .synth.fst,$(MODULES)) .PHONY: asc asc: $(addprefix rtl/,$(addsuffix .asc,$(MODULES))) +doc/output: + mkdir -p $@ + +doc/output/%.html: doc/%.adoc doc/docinfo.html | doc/output + $(ADOC) -o $@ $< + +DOCS += uart_wb_bridge + +.PHONY: htmldocs +htmldocs: $(addprefix doc/output/,$(addsuffix .html,$(DOCS))) + CLEAN_EXT := .json .asc .pre .vvp .d .synth.v .place.v .sdf .bin .PHONY: clean @@ -161,3 +174,4 @@ clean: rm -rf log rm -f $(addprefix rtl/*,$(CLEAN_EXT)) rm -f $(addprefix examples/*/*,$(CLEAN_EXT)) + rm -rf doc/output diff --git a/README.adoc b/README.adoc index 28d2ef5..91b1dd6 100644 --- a/README.adoc +++ b/README.adoc @@ -77,6 +77,15 @@ preamble/SFD and appends a 4-byte FCS to the data. It currently only supports 100M ethernet, although 10M would be easy to add. I have no plans to support 1000M. +=== `axis_wb_bridge` + +This module implements an AXI Stream to Wishbone bridge. This is not a +more-typical DMA bridge, where streaming data is written in a fixed pattern. +Rather, this module allows interactive or scripted examination and readout +of a Wishbone bus. For more details on the protocol implemented by this bridge, +refer to the xref:doc/uart_wb_bridge.adoc#protocol[UART-Wishbone Bridge +documentation]. + === `descramble` This implements a descrambler as specified in ANSI X3.264-1995 section 7.2.3. It diff --git a/doc/docinfo.html b/doc/docinfo.html new file mode 100644 index 0000000..913627c --- /dev/null +++ b/doc/docinfo.html @@ -0,0 +1,4 @@ + + + + diff --git a/doc/uart_wb_bridge.adoc b/doc/uart_wb_bridge.adoc new file mode 100644 index 0000000..dbb570d --- /dev/null +++ b/doc/uart_wb_bridge.adoc @@ -0,0 +1,159 @@ += UART-Wishbone Bridge +:docinfo: shared + +[[protocol]] +== Protocol + +The following sections outline the protocol used to communicate with the UART +half of the bridge. + +=== Overview + +The UART protocol uses a request/response format. Each wishbone transaction +corresponds to one request and one response. Each request begins with a command +byte; an optional, variable-length address; and some data if the request is a +write. Each response begins with a status byte, followed by some data if the +request was a read. The following diagram shows a successful read: + +++++ + +++++ + +Similarly, this diagram shows a successful write: + +++++ + +++++ + +The bridge contains an internal address register that retains its state between +different transactions. It possible to reduce the length of requests by +partially modifying the address register. + +=== Requests + +Each request begins with a command byte. The format of the command byte is as +follows: + +.Command byte +[cols="1,1,4a"] +|=== +| Bit | Name | Description + +| 0 | Clear | Setting this bit clears the address register before modifying + it. The address register should always be cleared during the + first transaction following a reset. +| 1 | Write-Enable | If this bit is set, this request is a write, and a data + phase follows the address phase. Otherwise, this request + is a read, and there is no data phase. +| 2 | Post-Increment | If this bit is set, the address register will be + incremented when the transaction completes. +| 4:3 | Address length +| This field indicates the number of bytes in the subsequent address phase. +!=== +! Value ! Address bytes + +! 0 ! 0 (no address phase) +! 1 ! 1 +! 2 ! 2 +! 3 ! 4 +!=== +| 7:5 | Reserved | Reserved, set to 0. +|=== + +Following the command byte, there is an optional address phase. The length of +the address phase is determined by the command byte. Bytes in the address phase +are loaded into the address register. The address is transmitted in big-endian +byte order (most-significant byte first). If number of bytes in the address +phase is smaller than the size of the address register, the lower bytes in the +address register will be replaced, and the upper bytes will not be modified. + +The following table shows the value of each byte in the address register after a +particular address phase. Bytes are numbered by the order they are transmitted: + +.Address phase +|=== +| Address bytes | Address[31:24] | Address[23:16] | Address[15:8] | Address[7:0] + +| 0 | Unmodified | Unmodified | Unmodified | Unmodifed +| 1 | Unmodified | Unmodified | Unmodified | Byte 0 +| 2 | Unmodified | Unmodified | Byte 0 | Byte 1 +| 4 | Byte 0 | Byte 1 | Byte 2 | Byte 3 +|=== + +Finally, there is a data phase if the request is a write. Data is transmitted +in big-endian byte order (most-significant byte first). + +Any requests transmitted while the bridge is processing another request will not +be handled correctly. This condition is indicated by an overflow status in +response to the initial request. + +=== Responses + +Each response begins with a status byte. The format of the status byte is as +follows: + +.Status byte +[cols="1,1,4a"] +|=== +| Bit | Name | Description + +| 0 | Write Response | If set, the response is for a write and no data phase + follows. Otherwise, the response is for a read and a data + phase will follow. +| 1 | Bus Error | There was bus error when servicing the request, and no data + phase will follow. This bit has priority over any data phase + implied by the Write Response bit. +| 2 | Reserved | Reserved, do not use. +| 3 | Overflow | While processing this request, the receive UART overflowed, and + one or more request bytes were dropped. The bridge must be + reset before issuing the next command. +| 7:4 | Reserved | Reserved, do not use. +|=== + +Finally, there is a data phase if the request was a read. Data is transmitted +in big-endian byte order (most-significant byte first). + +=== Resetting + +The bridge and wishbone bus may be reset by sending a character with a framing +error (a break) over the serial line. The bridge should be reset before each +session in order to bring the bridge into a known state. + +=== Examples + +A read of `0xcafe` from `0x00000123` followed by a write of `0xbabe` to the same +address: + +++++ + +++++ + +Reading from `0x80001000`, `0x80002000`, and `0x80002001`: + +++++ + +++++ diff --git a/rtl/axis_wb_bridge.v b/rtl/axis_wb_bridge.v new file mode 100644 index 0000000..69f2b08 --- /dev/null +++ b/rtl/axis_wb_bridge.v @@ -0,0 +1,210 @@ +// SPDX-License-Identifier: AGPL-3.0-Only +/* + * Copyright (C) 2022 Sean Anderson + */ + +`include "common.vh" + +module axis_wb_bridge ( + input clk, + input rst, + + output reg s_axis_ready, + input s_axis_valid, + input [7:0] s_axis_data, + + input m_axis_ready, + output reg m_axis_valid, + output reg [7:0] m_axis_data, + + /* Wishbone */ + input wb_ack, wb_err, + output reg wb_cyc, wb_stb, wb_we, + output reg [ADDR_WIDTH - 1:0] wb_addr, + output reg [15:0] wb_data_write, + input [15:0] wb_data_read, + + input overflow +); + + parameter ADDR_WIDTH = 32; + generate if (ADDR_WIDTH % 8) + $error("Unsupported ADDR_WIDTH"); + endgenerate + /* The data width is not parametric for now */ + localparam DATA_WIDTH = 16; + + localparam IDLE = 0; + localparam ADDR3 = 1; + localparam ADDR2 = 2; + localparam ADDR1 = 3; + localparam ADDR0 = 4; + localparam DATA1 = 5; + localparam DATA0 = 6; + localparam BUS = 7; + localparam RESP2 = 8; + localparam RESP1 = 9; + localparam RESP0 = 10; + + reg s_axis_ready_next, s_axis_valid_last, m_axis_ready_last, m_axis_valid_next; + reg [7:0] s_axis_data_last, m_axis_data_next; + reg wb_ack_last, wb_err_last; + reg wb_stb_next, wb_we_next; + reg [ADDR_WIDTH - 1:0] wb_addr_next; + reg [15:0] wb_data_write_next, wb_data_latch, wb_data_latch_next; + reg [3:0] state, state_next; + reg overflow_latch, overflow_latch_next, postinc, postinc_next; + + always @(*) begin + s_axis_ready_next = s_axis_ready; + m_axis_valid_next = m_axis_valid; + m_axis_data_next = 8'bX; + + wb_cyc = wb_stb; + wb_stb_next = wb_stb; + wb_we_next = wb_we; + wb_addr_next = wb_addr; + wb_data_write_next = wb_data_write; + if (wb_stb && (wb_err || wb_ack)) + wb_data_latch_next = wb_data_read; + else + wb_data_latch_next = wb_data_latch; + + state_next = state; + postinc_next = postinc; + overflow_latch_next = overflow_latch || overflow; + + case (state) + IDLE: if (s_axis_valid_last) begin + if (s_axis_data_last[0]) + wb_addr_next = {ADDR_WIDTH{1'b0}}; + wb_we_next = s_axis_data_last[1]; + postinc_next = s_axis_data_last[2]; + case (s_axis_data_last[4:3]) + 2'd3: state_next = ADDR3; + 2'd2: state_next = ADDR1; + 2'd1: state_next = ADDR0; + 2'd0: if (wb_we_next) begin + state_next = DATA1; + end else begin + state_next = BUS; + wb_stb_next = 1; + s_axis_ready_next = 0; + end + endcase + end + ADDR3: if (s_axis_valid_last) begin + if (ADDR_WIDTH >= 32) + wb_addr_next[31:24] = s_axis_data_last; + state_next = ADDR2; + end + ADDR2: if (s_axis_valid_last) begin + if (ADDR_WIDTH >= 24) + wb_addr_next[23:16] = s_axis_data_last; + state_next = ADDR1; + end + ADDR1: if (s_axis_valid_last) begin + if (ADDR_WIDTH >= 16) + wb_addr_next[15:8] = s_axis_data_last; + state_next = ADDR0; + end + ADDR0: if (s_axis_valid_last) begin + if (ADDR_WIDTH >= 8) + wb_addr_next[7:0] = s_axis_data_last; + if (wb_we) begin + state_next = DATA1; + end else begin + state_next = BUS; + wb_stb_next = 1; + s_axis_ready_next = 0; + end + end + DATA1: if (s_axis_valid_last) begin + wb_data_write_next = { wb_data_write[7:0], s_axis_data_last }; + state_next = DATA0; + end + DATA0: if(s_axis_valid_last) begin + wb_data_write_next = { wb_data_write[7:0], s_axis_data_last }; + state_next = BUS; + wb_stb_next = 1; + s_axis_ready_next = 0; + end + BUS: if (wb_ack || wb_err) begin + wb_stb_next = 0; + wb_addr_next[7:0] = wb_addr[7:0] + postinc; + m_axis_valid_next = 1; + m_axis_data_next = { 4'b0, overflow_latch_next, 1'b0, wb_err, wb_we }; + overflow_latch_next = 0; + state_next = wb_we || wb_err ? RESP0 : RESP2; + end + RESP2: if (m_axis_ready_last) begin + m_axis_data_next = wb_data_latch[15:8]; + state_next = RESP1; + end + RESP1: if (m_axis_ready_last) begin + m_axis_data_next = wb_data_latch[7:0]; + state_next = RESP0; + end + RESP0: if (m_axis_ready_last) begin + m_axis_valid_next = 0; + s_axis_ready_next = 1; + state_next = IDLE; + end + endcase + end + + always @(posedge clk) begin + s_axis_data_last <= s_axis_data; + m_axis_data <= m_axis_data_next; + wb_we <= wb_we_next; + wb_addr <= wb_addr_next; + wb_data_write <= wb_data_write_next; + wb_data_latch <= wb_data_latch_next; + postinc <= postinc_next; + end + + always @(posedge clk, posedge rst) begin + if (rst) begin + s_axis_ready <= 1; + s_axis_valid_last <= 0; + m_axis_ready_last <= 0; + m_axis_valid <= 0; + wb_ack_last <= 0; + wb_err_last <= 0; + wb_stb <= 0; + state <= IDLE; + overflow_latch <= 0; + end else begin + s_axis_ready <= s_axis_ready_next; + s_axis_valid_last <= s_axis_valid; + m_axis_ready_last <= m_axis_ready; + m_axis_valid <= m_axis_valid_next; + wb_ack_last <= wb_ack; + wb_err_last <= wb_err; + wb_stb <= wb_stb_next; + state <= state_next; + overflow_latch <= overflow_latch_next; + end + end + +`ifndef SYNTHESIS + reg [255:0] state_text; + + always @(*) begin + case (state) + IDLE: state_text = "IDLE"; + ADDR3: state_text = "ADDR3"; + ADDR2: state_text = "ADDR2"; + ADDR1: state_text = "ADDR1"; + ADDR0: state_text = "ADDR0"; + DATA1: state_text = "DATA1"; + DATA0: state_text = "DATA0"; + BUS: state_text = "BUS"; + RESP2: state_text = "RESP2"; + RESP1: state_text = "RESP1"; + RESP0: state_text = "RESP0"; + endcase + end +`endif + +endmodule diff --git a/tb/axis_wb_bridge.py b/tb/axis_wb_bridge.py new file mode 100644 index 0000000..3835f35 --- /dev/null +++ b/tb/axis_wb_bridge.py @@ -0,0 +1,166 @@ +# SPDX-License-Identifier: AGPL-3.0-Only +# Copyright (C) 2023 Sean Anderson + +import cocotb +from cocotb.binary import BinaryValue +from cocotb.clock import Clock +from cocotb.triggers import FallingEdge, Timer + +from .axis_replay_buffer import send_packet, recv_packet +from .mdio import wb_read, wb_write, wb_err +from .util import BIT, GENMASK + +CMD_CLEAR = BIT(0) +CMD_WE = BIT(1) +CMD_POSTINC = BIT(2) +CMD_ADDR0 = 0x00 +CMD_ADDR8 = 0x08 +CMD_ADDR16 = 0x10 +CMD_ADDR32 = 0x18 + +STATUS_WE = BIT(0) +STATUS_ERR = BIT(1) +STATUS_OVERFLOW = BIT(3) + +class Encoder: + def __init__(self): + self.last_addr = None + + def encode(self, addr, data=None, postinc=False): + cmd = CMD_POSTINC if postinc else 0 + if data is None: + data_bytes = () + else: + cmd |= CMD_WE + data_bytes = data.to_bytes(2, 'big') + + if self.last_addr is None: + cmd |= CMD_CLEAR + self.last_addr = 0 + + def addr_len(last): + if (addr ^ last) & ~GENMASK(15, 0): + return 4 + if (addr ^ last) & GENMASK(15, 8): + return 2 + if (addr ^ last) & GENMASK(7, 0): + return 1 + return 0 + + len_zero = addr_len(0) + len_last = addr_len(self.last_addr) + if len_zero < len_last: + addr_len = len_zero + cmd |= CMD_CLEAR + else: + addr_len = len_last + + addr_bytes = (addr & GENMASK(addr_len * 8 - 1, 0)).to_bytes(addr_len, 'big') + if addr_len == 4: + cmd |= CMD_ADDR32 + elif addr_len == 2: + cmd |= CMD_ADDR16 + elif addr_len == 1: + cmd |= CMD_ADDR8 + else: + cmd |= CMD_ADDR0 + + self.last_addr = (addr & ~GENMASK(7, 0)) | ((addr + postinc) & GENMASK(7, 0)) + return (cmd, *addr_bytes, *data_bytes) + +@cocotb.test(timeout_time=10, timeout_unit='us') +async def test_bridge(bridge): + bridge.clk.value = BinaryValue('Z') + bridge.rst.value = 1 + bridge.s_axis_valid.value = 0 + bridge.m_axis_ready.value = 1 + bridge.wb_ack.value = 0 + bridge.wb_err.value = 0 + bridge.overflow.value = 0 + + await Timer(1) + bridge.rst.value = 0 + await cocotb.start(Clock(bridge.clk, 8, units='ns').start()) + await FallingEdge(bridge.clk) + + s_axis = { + 'clk': bridge.clk, + 'ready': bridge.s_axis_ready, + 'valid': bridge.s_axis_valid, + 'data': bridge.s_axis_data, + } + + m_axis = { + 'clk': bridge.clk, + 'ready': bridge.m_axis_ready, + 'valid': bridge.m_axis_valid, + 'data': bridge.m_axis_data, + } + + wb = { + 'clk': bridge.clk, + 'ack': bridge.wb_ack, + 'err': bridge.wb_err, + 'cyc': bridge.wb_cyc, + 'stb': bridge.wb_stb, + 'we': bridge.wb_we, + 'addr': bridge.wb_addr, + 'data_write': bridge.wb_data_write, + 'data_read': bridge.wb_data_read, + } + + e = Encoder() + + async def read(addr, data, postinc=False, resp=0): + await send_packet(s_axis, e.encode(addr, None, postinc)) + + bridge.overflow.value = bool(resp & STATUS_OVERFLOW) + if resp & STATUS_ERR: + await wb_err(wb) + bridge.overflow.value = 0 + await recv_packet(m_axis, (resp,)) + else: + await wb_read(wb, addr, data) + bridge.overflow.value = 0 + await recv_packet(m_axis, (resp, *data.to_bytes(2, 'big'))) + + async def write(addr, data, postinc=False, resp=STATUS_WE): + await send_packet(s_axis, e.encode(addr, data, postinc)) + + bridge.overflow.value = bool(resp & STATUS_OVERFLOW) + if resp & STATUS_ERR: + await wb_err(wb) + else: + await wb_write(wb, addr, data) + bridge.overflow.value = 0 + + await recv_packet(m_axis, (resp,)) + + for f in read, write: + await f(0x01234567, 0x89ab) + await f(0x01234567, 0xcdef) + await f(0x012345fe, 1, True) + await f(0x012345ff, 2, True) + await f(0x01234500, 3) + await f(0x012345ff, 4, True) + await f(0x01234600, 5) + await f(0x0123ffff, 6) + await f(0x01ffffff, 7) + await f(0xffffffff, 8) + await f(0x0000ffff, 9) + await f(0x000000ff, 10) + await f(0x00000000, 11) + + # fast back-to-back + recv = await cocotb.start(recv_packet(m_axis, (STATUS_WE, 0, 0, 4))) + await send_packet(s_axis, e.encode(1, 2)) + await wb_write(wb, 1, 2) + await send_packet(s_axis, e.encode(3)) + await wb_read(wb, 3, 4) + await recv + + # bus error/overflow + await write(5, 6, resp=STATUS_WE | STATUS_ERR | STATUS_OVERFLOW) + await read(7, 8, resp=STATUS_ERR) + await read(9, 10, resp=STATUS_OVERFLOW) + await write(11, 12)