refactor IO core read/write to be less ugly

This commit is contained in:
Fischer Moseley 2023-03-17 20:12:57 -04:00
parent 3cf5164d23
commit edd50168e2
3 changed files with 112 additions and 156 deletions

View File

@ -1,6 +1,5 @@
## IO Core
- test examples that build
- update IO core read/write memory handling to be less ugly
- add logic for ports >16 bits in width
- clock domain crossing
- figure out what happens for module naming - it's possible we could have two modules that have the same ports but have different names
@ -21,6 +20,7 @@
- write interface
## Python API
- implement global address assignment
- find a better way of handling tabs
- make finding a serial port possible even if no cores are configured
- make autodetecting and automatically selecting a serial device possible

View File

@ -2,7 +2,7 @@
`timescale 1ns/1ps
/*
This manta definition was generated on 16 Mar 2023 at 12:07:39 by fischerm
This manta definition was generated on 17 Mar 2023 at 20:02:55 by fischerm
If this breaks or if you've got dank formal verification memes,
please contact fischerm [at] mit.edu
@ -418,31 +418,31 @@ always @(posedge clk) begin
if(!rw_i) begin // reads
case (addr_i)
BASE_ADDR + 0: rdata_o <= {15'b0, btnu};
BASE_ADDR + 1: rdata_o <= {15'b0, btnd};
BASE_ADDR + 2: rdata_o <= {15'b0, btnl};
BASE_ADDR + 3: rdata_o <= {15'b0, btnr};
BASE_ADDR + 4: rdata_o <= {15'b0, btnc};
BASE_ADDR + 5: rdata_o <= sw;
BASE_ADDR + 6: rdata_o <= led;
BASE_ADDR + 7: rdata_o <= {15'b0, led16_b};
BASE_ADDR + 8: rdata_o <= {15'b0, led16_g};
BASE_ADDR + 9: rdata_o <= {15'b0, led16_r};
BASE_ADDR + 10: rdata_o <= {15'b0, led17_b};
BASE_ADDR + 11: rdata_o <= {15'b0, led17_g};
BASE_ADDR + 12: rdata_o <= {15'b0, led17_r};
0: rdata_o <= {15'b0, btnu};
1: rdata_o <= {15'b0, btnd};
2: rdata_o <= {15'b0, btnl};
3: rdata_o <= {15'b0, btnr};
4: rdata_o <= {15'b0, btnc};
5: rdata_o <= sw;
6: rdata_o <= led;
7: rdata_o <= {15'b0, led16_b};
8: rdata_o <= {15'b0, led16_g};
9: rdata_o <= {15'b0, led16_r};
10: rdata_o <= {15'b0, led17_b};
11: rdata_o <= {15'b0, led17_g};
12: rdata_o <= {15'b0, led17_r};
endcase
end
else begin // writes
case (addr_i)
BASE_ADDR + 6: led <= wdata_i;
BASE_ADDR + 7: led16_b <= wdata_i[0];
BASE_ADDR + 8: led16_g <= wdata_i[0];
BASE_ADDR + 9: led16_r <= wdata_i[0];
BASE_ADDR + 10: led17_b <= wdata_i[0];
BASE_ADDR + 11: led17_g <= wdata_i[0];
BASE_ADDR + 12: led17_r <= wdata_i[0];
6: led <= wdata_i;
7: led16_b <= wdata_i[0];
8: led16_g <= wdata_i[0];
9: led16_r <= wdata_i[0];
10: led17_b <= wdata_i[0];
11: led17_g <= wdata_i[0];
12: led17_r <= wdata_i[0];
endcase
end
end

View File

@ -131,91 +131,80 @@ class UARTInterface:
.tx(tx));\n"""
class IOCoreProbe:
def __init__(self, name, width, direction, base_addr, interface):
self.name = name
self.width = width
self.direction = direction
self.base_addr = base_addr
self.interface = interface
def set(self, data):
# check that value is within range for the width of the probe
assert isinstance(data, int), "Data must be an integer."
if data > 0:
assert data <= (2**self.width) - 1, f"Unsigned value too large for probe of width {self.width}"
elif data < 0:
assert data >= -(2**(self.width-1))-1, f"Signed value too large for probe of width {self.width}"
assert data <= (2**(self.width-1))-1, f"Signed value too large for probe of width {self.width}"
self.interface.write_register(self.base_addr, data)
def get(self, probe):
return self.interface.read_register(self.base_addr)
class IOCore:
def __init__(self, config, interface):
def __init__(self, config, name, base_addr, interface):
self.name = name
self.base_addr = base_addr
self.interface = interface
# make sure we have ports defined
assert ('inputs' in config) or ('outputs' in config), "No input or output ports specified."
# add inputs to core
address = 0
self.inputs = []
# add input probes to core
self.probes = []
probe_base_addr = self.base_addr
if 'inputs' in config:
for name, width in config["inputs"].items():
for name, width in config["inputs"].items():
# make sure inputs are of reasonable width
assert width > 0, f"Input {name} must have width greater than zero."
assert isinstance(width, int), f"Probe {name} must have integer width."
assert width > 0, f"Probe {name} must have positive width."
self.inputs.append( {"name": name, "width": width, "address": address} )
self.max_rel_addr = address
address += 1
probe = IOCoreProbe(name, width, "input", probe_base_addr, self.interface)
# add friendly name, so users can do Manta.my_io_core.my_probe.set() for example
setattr(self, name, probe)
self.probes.append(probe)
# add outputs to core
self.outputs = []
self.max_probe_addr = probe_base_addr
probe_base_addr += 1
# add output probes to core
if 'outputs' in config:
for name, width in config["outputs"].items():
# make sure inputs are of reasonable width
assert width > 0, f"Output {name} must have width greater than zero."
assert isinstance(width, int), f"Probe {name} must have integer width."
assert width > 0, f"Probe {name} must have positive width."
probe = IOCoreProbe(name, width, "output", probe_base_addr, self.interface)
# add friendly name, so users can do Manta.my_io_core.my_probe.set() for example
setattr(self, name, probe)
self.probes.append(probe)
self.outputs.append( {"name": name, "width": width, "address": address} )
self.max_rel_addr = address
address += 1
self.max_probe_addr = probe_base_addr
probe_base_addr += 1
def set(self, probe, data):
# check that probe actually exists
assert (probe in self.inputs) or (probe in self.outputs), "Probe {probe} not found."
if probe in self.inputs:
probe_def = self.inputs[probe]
elif probe in self.outputs:
probe_def = self.outputs[probe]
# check that value is reasonable
# (should be an integer between 0 and 2^width - 1)
# send message
addr = probe_def["address"] + self.base_addr
assert isinstance(data, int), "Data must be an integer."
if data > 0:
assert data <= (2**probe_def["width"]) - 1, f"Unsigned value too large for probe of width {probe_def['width']}"
elif data < 0:
assert data >= -(2**(probe_def["width"]-1))-1, f"Signed value too large for probe of width {probe_def['width']}"
assert data <= (2**(probe_def["width"]-1))-1, f"Signed value too large for probe of width {probe_def['width']}"
self.interface.write_register(addr, data)
def get(self, probe):
# check that probe actually exists
assert (probe in self.inputs) or (probe in self.outputs), "Probe {probe} not found."
if probe in self.inputs:
probe_def = self.inputs[probe]
elif probe in self.outputs:
probe_def = self.outputs[probe]
addr = self.base_addr + probe_def["address"]
return self.interface.read_register(addr)
def hdl_inst(self):
inst_ports = ""
for input in self.inputs:
name = input["name"]
inst_ports += f".{name}({name}),\n "
for output in self.outputs:
name = output["name"]
inst_ports += f".{name}({name}),\n "
# TODO: make this a string comprehension
inst_ports = [f".{probe.name}({probe.name}),\n " for probe in self.probes]
inst_ports = "".join(inst_ports)
inst_ports = inst_ports.rstrip()
inst = f"""
return f"""
{self.name} {self.name}_inst(
.clk(clk),
@ -237,12 +226,9 @@ class IOCore:
.valid_o()
);
"""
return inst
def hdl_def(self):
# generate declaration
top_level_ports = ',\n '.join(self.hdl_top_level_ports())
top_level_ports += ','
declaration = f"""
@ -269,43 +255,29 @@ module {self.name} (
"""
# generate memory handling
# TODO: clean this up, should just do all the probes at once
# instead of splitting by input/output
read_case_statement_body = ""
for input in self.inputs:
name = input["name"]
width = input["width"]
address = input["address"]
if width == 16:
read_case_statement_body += f"\t\t\t\t\tBASE_ADDR + {address}: rdata_o <= {name};\n"
else:
read_case_statement_body += f"\t\t\t\t\tBASE_ADDR + {address}: rdata_o <= {{{16-width}'b0, {name}}};\n"
write_case_statement_body = ""
for output in self.outputs:
name = output["name"]
width = output["width"]
address = output["address"]
for probe in self.probes:
if width == 16:
read_case_statement_body += f"\t\t\t\t\tBASE_ADDR + {address}: rdata_o <= {name};\n"
else:
read_case_statement_body += f"\t\t\t\t\tBASE_ADDR + {address}: rdata_o <= {{{16-width}'b0, {name}}};\n"
if width == 1:
write_case_statement_body += f"\t\t\t\t\tBASE_ADDR + {address}: {name} <= wdata_i[0];\n"
elif width == 16:
write_case_statement_body += f"\t\t\t\t\tBASE_ADDR + {address}: {name} <= wdata_i;\n"
# add to read block
if probe.width == 16:
read_case_statement_body += f"\t\t\t\t\t{probe.base_addr}: rdata_o <= {probe.name};\n"
else:
write_case_statement_body += f"\t\t\t\t\tBASE_ADDR + {address}: {name} <= wdata_i[{width-1}:0];\n"
read_case_statement_body += f"\t\t\t\t\t{probe.base_addr}: rdata_o <= {{{16-probe.width}'b0, {probe.name}}};\n"
# if output, add to write block
if probe.direction == "output":
if probe.width == 1:
write_case_statement_body += f"\t\t\t\t\t{probe.base_addr}: {probe.name} <= wdata_i[0];\n"
elif probe.width == 16:
write_case_statement_body += f"\t\t\t\t\t{probe.base_addr}: {probe.name} <= wdata_i;\n"
else:
write_case_statement_body += f"\t\t\t\t\t{probe.base_addr}: {probe.name} <= wdata_i[{probe.width-1}:0];\n"
# remove trailing newline
read_case_statement_body = read_case_statement_body.rstrip()
write_case_statement_body = write_case_statement_body.rstrip()
@ -322,7 +294,7 @@ always @(posedge clk) begin
// check if address is valid
if( (valid_i) && (addr_i >= BASE_ADDR) && (addr_i <= BASE_ADDR + {self.max_rel_addr})) begin
if( (valid_i) && (addr_i >= BASE_ADDR) && (addr_i <= BASE_ADDR + {self.max_probe_addr})) begin
if(!rw_i) begin // reads
case (addr_i)
@ -343,34 +315,18 @@ always @(posedge clk) begin
return hdl
def hdl_top_level_ports(self):
probes = []
# generate inputs
for input in self.inputs:
name = input["name"]
width = input["width"]
if width == 1:
probes.append(f"input wire {name}")
else:
probes.append(f"input wire [{width-1}:0] {name}")
# generate outputs
for output in self.outputs:
name = output["name"]
width = output["width"]
if width == 1:
probes.append(f"output reg {name}")
else:
probes.append(f"output reg [{width-1}:0] {name}")
return probes
ports = []
for probe in self.probes:
net_type = "input wire " if probe.direction == "input" else "output reg "
name_def = probe.name if probe.width == 1 else f"[{probe.width-1}:0] {probe.name}"
ports.append(net_type + name_def)
return ports
class LUTRAMCore:
def __init__(self, config, interface):
def __init__(self, config, name, base_addr, interface):
self.name = name
self.base_addr = base_addr
self.interface = interface
assert "size" in config, "Size not specified for LUT RAM core."
@ -404,7 +360,9 @@ class LUTRAMCore:
return []
class LogicAnalyzerCore:
def __init__(self, config, interface):
def __init__(self, config, name, base_addr, interface):
self.name = name
self.base_addr = base_addr
self.interface = interface
# load config
@ -587,6 +545,7 @@ class Manta:
assert len(config["cores"]) > 0, "Must specify at least one core."
# add cores to self
base_addr = 0 # TODO: implement address assignment
self.cores = []
for i, core_name in enumerate(config["cores"]):
core = config["cores"][core_name]
@ -596,20 +555,17 @@ class Manta:
# add the core to ourself
if core["type"] == "logic_analyzer":
new_core = LogicAnalyzerCore(core, self.interface)
new_core = LogicAnalyzerCore(core, core_name, base_addr, self.interface)
elif core["type"] == "io":
new_core = IOCore(core, self.interface)
new_core = IOCore(core, core_name, base_addr, self.interface)
elif core["type"] == "lut_ram":
new_core = LUTRAMCore(core, self.interface)
new_core = LUTRAMCore(core, core_name, base_addr, self.interface)
else:
raise ValueError(f"Unrecognized core type specified for {core_name}.")
# TODO: update class defs so that we don't monkey-patch like this. this is not good. i am lazy
setattr(new_core, 'name', core_name)
# add friendly name, so users can do Manta.my_logic_analyzer.read() for example
setattr(self, core_name, new_core)
self.cores.append(new_core)