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 ## IO Core
- test examples that build - test examples that build
- update IO core read/write memory handling to be less ugly
- add logic for ports >16 bits in width - add logic for ports >16 bits in width
- clock domain crossing - 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 - 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 - write interface
## Python API ## Python API
- implement global address assignment
- find a better way of handling tabs - find a better way of handling tabs
- make finding a serial port possible even if no cores are configured - make finding a serial port possible even if no cores are configured
- make autodetecting and automatically selecting a serial device possible - make autodetecting and automatically selecting a serial device possible

View File

@ -2,7 +2,7 @@
`timescale 1ns/1ps `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, If this breaks or if you've got dank formal verification memes,
please contact fischerm [at] mit.edu please contact fischerm [at] mit.edu
@ -418,31 +418,31 @@ always @(posedge clk) begin
if(!rw_i) begin // reads if(!rw_i) begin // reads
case (addr_i) case (addr_i)
BASE_ADDR + 0: rdata_o <= {15'b0, btnu}; 0: rdata_o <= {15'b0, btnu};
BASE_ADDR + 1: rdata_o <= {15'b0, btnd}; 1: rdata_o <= {15'b0, btnd};
BASE_ADDR + 2: rdata_o <= {15'b0, btnl}; 2: rdata_o <= {15'b0, btnl};
BASE_ADDR + 3: rdata_o <= {15'b0, btnr}; 3: rdata_o <= {15'b0, btnr};
BASE_ADDR + 4: rdata_o <= {15'b0, btnc}; 4: rdata_o <= {15'b0, btnc};
BASE_ADDR + 5: rdata_o <= sw; 5: rdata_o <= sw;
BASE_ADDR + 6: rdata_o <= led; 6: rdata_o <= led;
BASE_ADDR + 7: rdata_o <= {15'b0, led16_b}; 7: rdata_o <= {15'b0, led16_b};
BASE_ADDR + 8: rdata_o <= {15'b0, led16_g}; 8: rdata_o <= {15'b0, led16_g};
BASE_ADDR + 9: rdata_o <= {15'b0, led16_r}; 9: rdata_o <= {15'b0, led16_r};
BASE_ADDR + 10: rdata_o <= {15'b0, led17_b}; 10: rdata_o <= {15'b0, led17_b};
BASE_ADDR + 11: rdata_o <= {15'b0, led17_g}; 11: rdata_o <= {15'b0, led17_g};
BASE_ADDR + 12: rdata_o <= {15'b0, led17_r}; 12: rdata_o <= {15'b0, led17_r};
endcase endcase
end end
else begin // writes else begin // writes
case (addr_i) case (addr_i)
BASE_ADDR + 6: led <= wdata_i; 6: led <= wdata_i;
BASE_ADDR + 7: led16_b <= wdata_i[0]; 7: led16_b <= wdata_i[0];
BASE_ADDR + 8: led16_g <= wdata_i[0]; 8: led16_g <= wdata_i[0];
BASE_ADDR + 9: led16_r <= wdata_i[0]; 9: led16_r <= wdata_i[0];
BASE_ADDR + 10: led17_b <= wdata_i[0]; 10: led17_b <= wdata_i[0];
BASE_ADDR + 11: led17_g <= wdata_i[0]; 11: led17_g <= wdata_i[0];
BASE_ADDR + 12: led17_r <= wdata_i[0]; 12: led17_r <= wdata_i[0];
endcase endcase
end end
end end

View File

@ -131,91 +131,80 @@ class UARTInterface:
.tx(tx));\n""" .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: 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 self.interface = interface
# make sure we have ports defined # make sure we have ports defined
assert ('inputs' in config) or ('outputs' in config), "No input or output ports specified." assert ('inputs' in config) or ('outputs' in config), "No input or output ports specified."
# add inputs to core # add input probes to core
address = 0 self.probes = []
self.inputs = [] probe_base_addr = self.base_addr
if 'inputs' in config: 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 # 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} ) probe = IOCoreProbe(name, width, "input", probe_base_addr, self.interface)
self.max_rel_addr = address
address += 1 # 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.max_probe_addr = probe_base_addr
self.outputs = [] probe_base_addr += 1
# add output probes to core
if 'outputs' in config: if 'outputs' in config:
for name, width in config["outputs"].items(): for name, width in config["outputs"].items():
# make sure inputs are of reasonable width # 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_probe_addr = probe_base_addr
self.max_rel_addr = address probe_base_addr += 1
address += 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): def hdl_inst(self):
inst_ports = "" # TODO: make this a string comprehension
for input in self.inputs: inst_ports = [f".{probe.name}({probe.name}),\n " for probe in self.probes]
name = input["name"] inst_ports = "".join(inst_ports)
inst_ports += f".{name}({name}),\n "
for output in self.outputs:
name = output["name"]
inst_ports += f".{name}({name}),\n "
inst_ports = inst_ports.rstrip() inst_ports = inst_ports.rstrip()
inst = f""" return f"""
{self.name} {self.name}_inst( {self.name} {self.name}_inst(
.clk(clk), .clk(clk),
@ -237,12 +226,9 @@ class IOCore:
.valid_o() .valid_o()
); );
""" """
return inst
def hdl_def(self): def hdl_def(self):
# generate declaration # generate declaration
top_level_ports = ',\n '.join(self.hdl_top_level_ports()) top_level_ports = ',\n '.join(self.hdl_top_level_ports())
top_level_ports += ',' top_level_ports += ','
declaration = f""" declaration = f"""
@ -269,43 +255,29 @@ module {self.name} (
""" """
# generate memory handling # 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 = "" 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 = "" write_case_statement_body = ""
for output in self.outputs: for probe in self.probes:
name = output["name"]
width = output["width"]
address = output["address"]
if width == 16: # add to read block
read_case_statement_body += f"\t\t\t\t\tBASE_ADDR + {address}: rdata_o <= {name};\n" if probe.width == 16:
read_case_statement_body += f"\t\t\t\t\t{probe.base_addr}: rdata_o <= {probe.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"
else: 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 # remove trailing newline
read_case_statement_body = read_case_statement_body.rstrip() read_case_statement_body = read_case_statement_body.rstrip()
write_case_statement_body = write_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 // 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 if(!rw_i) begin // reads
case (addr_i) case (addr_i)
@ -343,34 +315,18 @@ always @(posedge clk) begin
return hdl return hdl
def hdl_top_level_ports(self): def hdl_top_level_ports(self):
probes = [] ports = []
for probe in self.probes:
# generate inputs net_type = "input wire " if probe.direction == "input" else "output reg "
for input in self.inputs: name_def = probe.name if probe.width == 1 else f"[{probe.width-1}:0] {probe.name}"
name = input["name"] ports.append(net_type + name_def)
width = input["width"]
return ports
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
class LUTRAMCore: 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 self.interface = interface
assert "size" in config, "Size not specified for LUT RAM core." assert "size" in config, "Size not specified for LUT RAM core."
@ -404,7 +360,9 @@ class LUTRAMCore:
return [] return []
class LogicAnalyzerCore: 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 self.interface = interface
# load config # load config
@ -587,6 +545,7 @@ class Manta:
assert len(config["cores"]) > 0, "Must specify at least one core." assert len(config["cores"]) > 0, "Must specify at least one core."
# add cores to self # add cores to self
base_addr = 0 # TODO: implement address assignment
self.cores = [] self.cores = []
for i, core_name in enumerate(config["cores"]): for i, core_name in enumerate(config["cores"]):
core = config["cores"][core_name] core = config["cores"][core_name]
@ -596,20 +555,17 @@ class Manta:
# add the core to ourself # add the core to ourself
if core["type"] == "logic_analyzer": 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": 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": elif core["type"] == "lut_ram":
new_core = LUTRAMCore(core, self.interface) new_core = LUTRAMCore(core, core_name, base_addr, self.interface)
else: else:
raise ValueError(f"Unrecognized core type specified for {core_name}.") 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 # add friendly name, so users can do Manta.my_logic_analyzer.read() for example
setattr(self, core_name, new_core) setattr(self, core_name, new_core)
self.cores.append(new_core) self.cores.append(new_core)