Add AXIS-Wishbone bridge

This adds the core of the UART-Wishbone bridge. The protocol has
a variable-length address phase to help reduce overhead. Multiple
in-flight commands are not supported, although this could be resolved
with some FIFOs.

Signed-off-by: Sean Anderson <seanga2@gmail.com>
This commit is contained in:
Sean Anderson 2023-03-01 19:14:59 -05:00
parent 0f6d4b166f
commit 75142311f2
7 changed files with 565 additions and 0 deletions

3
.gitignore vendored
View File

@ -18,3 +18,6 @@ __pycache__
*.fst *.fst
results.xml results.xml
test_profile.pstat test_profile.pstat
# doc artifacts
*.html

View File

@ -2,6 +2,7 @@
# Copyright (C) 2022 Sean Anderson <seanga2@gmail.com> # Copyright (C) 2022 Sean Anderson <seanga2@gmail.com>
Q = 1 Q = 1
ADOC = asciidoctor
SYNTH = yosys SYNTH = yosys
PNR = nextpnr-ice40 PNR = nextpnr-ice40
ICARUS = iverilog ICARUS = iverilog
@ -124,6 +125,7 @@ endef
MODULES += axis_replay_buffer MODULES += axis_replay_buffer
MODULES += axis_mii_tx MODULES += axis_mii_tx
MODULES += axis_wb_bridge
MODULES += descramble MODULES += descramble
MODULES += hub MODULES += hub
MODULES += hub_core MODULES += hub_core
@ -153,6 +155,17 @@ test: $(addsuffix .fst,$(MODULES)) $(addsuffix .synth.fst,$(MODULES))
.PHONY: asc .PHONY: asc
asc: $(addprefix rtl/,$(addsuffix .asc,$(MODULES))) 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 CLEAN_EXT := .json .asc .pre .vvp .d .synth.v .place.v .sdf .bin
.PHONY: clean .PHONY: clean
@ -161,3 +174,4 @@ clean:
rm -rf log rm -rf log
rm -f $(addprefix rtl/*,$(CLEAN_EXT)) rm -f $(addprefix rtl/*,$(CLEAN_EXT))
rm -f $(addprefix examples/*/*,$(CLEAN_EXT)) rm -f $(addprefix examples/*/*,$(CLEAN_EXT))
rm -rf doc/output

View File

@ -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 100M ethernet, although 10M would be easy to add. I have no plans to support
1000M. 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` === `descramble`
This implements a descrambler as specified in ANSI X3.264-1995 section 7.2.3. It This implements a descrambler as specified in ANSI X3.264-1995 section 7.2.3. It

4
doc/docinfo.html Normal file
View File

@ -0,0 +1,4 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/wavedrom/3.1.0/skins/default.js" type="text/javascript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/wavedrom/3.1.0/wavedrom.min.js" type="text/javascript"></script>
<script type="text/javascript">document.addEventListener('DOMContentLoaded', WaveDrom.ProcessAll)</script>

159
doc/uart_wb_bridge.adoc Normal file
View File

@ -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:
++++
<script type="WaveDrom">
{ signal : [
{ name: "rx", wave: "z34444z....", data: "CMD ADDR0 ADDR1 ADDR2 ADDR3" },
{ name: "tx", wave: "z......655z", data: "STATUS DATA0 DATA1" },
],
config: { hscale: 2 },
}
</script>
++++
Similarly, this diagram shows a successful write:
++++
<script type="WaveDrom">
{ signal : [
{ name: "rx", wave: "z3444455z..", data: "CMD ADDR0 ADDR1 ADDR2 ADDR3 DATA0 DATA1" },
{ name: "tx", wave: "z........6z", data: "RESP" },
],
config: { hscale: 2 },
}
</script>
++++
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:
++++
<script type="WaveDrom">
{ signal : [
{ name: "rx", wave: "z344z....355z..", data: "11 01 23 02 ba be" },
{ name: "tx", wave: "z....655z....6z", data: "00 ca fe 01" },
]}
</script>
++++
Reading from `0x80001000`, `0x80002000`, and `0x80002001`:
++++
<script type="WaveDrom">
{ signal : [
{ name: "rx", wave: "z34444z....344z....3z....", data: "18 80 00 10 00 14 20 00 00" },
{ name: "tx", wave: "z......655z....655z..655z", data: "00 d0 0d 00 fe ed 00 fa ce" },
]}
</script>
++++

210
rtl/axis_wb_bridge.v Normal file
View File

@ -0,0 +1,210 @@
// SPDX-License-Identifier: AGPL-3.0-Only
/*
* Copyright (C) 2022 Sean Anderson <seanga2@gmail.com>
*/
`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

166
tb/axis_wb_bridge.py Normal file
View File

@ -0,0 +1,166 @@
# 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 .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)