diff --git a/hectare/_HectareVerilogGen.py b/hectare/_HectareVerilogGen.py new file mode 100644 index 0000000..7cf0465 --- /dev/null +++ b/hectare/_HectareVerilogGen.py @@ -0,0 +1,521 @@ +""" +Copyright (c) 2020 Deutsches Elektronen-Synchrotron DESY. + +See LICENSE.txt for license details. +""" + +import datetime +import getpass +import os +import socket +from typing import Iterator, List, Optional + +from systemrdl.rdltypes import AccessType + +import hectare._verilog_templates as _verilogt +from hectare._hectare_types import AddressMap, Field, Register + + +def indent_lines(ls: List[str], ident_level: int) -> Iterator[str]: + for l in ls: + yield " " * ident_level + l + + +class HectareVerilogGen: + def __init__(self, addrmap, input_filename=""): + self.addrmap = addrmap + self.cur_indent = 0 + self.data_w_bytes = 4 # 32 / 8 # TODO check regwidth + self.input_filename = input_filename + + def generate_package(self) -> Optional[str]: + """ generates package with VERILOG enums if the register description + contains enums + """ + + print("generate_package") + + # first check if any fields are enum, otherwise package is not generated + lines = [] + generated_enums = set() + + for reg in self.addrmap.regs: + for field in reg.fields: + if field.encode is not None and field.encode not in generated_enums: + lines.extend(self._gen_single_enum_type(field)) + lines.append("") + generated_enums.add(field.encode) + + if lines: + # at least one enum was found, generated package string + + s = "" + s += self._gen_header(self.input_filename) + s += _verilog.VERILOG_LIBS + s += "\n" + + s += "package {entity_name}_pkg is\n".format(entity_name=self.addrmap.name) + s += "\n" + s += " // attributes\n" + s += " attribute enum_encoding: string;\n" + s += "\n" + + for line in lines: + s += " " + line + "\n" + + s += "\n" + s += "end package;\n" + + return s + + def generate_string(self) -> str: + s = "" + + s += self._gen_header(self.input_filename) + + s += _verilogt.VERILOG_LIBS + s += "\n" + + # check if there is package being generated, add package to the includes + contains_enums = False + for reg in self.addrmap.regs: + for field in reg.fields: + if field.encode is not None: + contains_enums = True + + if contains_enums: + s += "use work.{entity_name}_pkg.all;\n".format( + entity_name=self.addrmap.name + ) + s += "\n" + + s += "module {entity_name} #(\n".format(entity_name=self.addrmap.name) + s += " parameter G_ADDR_W = 8\n" + s += ") (\n" + s += "\n".join(indent_lines(self._gen_ports(), 4)) + s += "\n" + s += _verilogt.VERILOG_PORTS_AXI + s += "\n);\n" + + s += "\n\n // address constants\n" + s += "\n".join(indent_lines(self._gen_reg_addr(), 2)) + + s += "\n\n // field ranges constants\n" + s += "\n".join(indent_lines(self._gen_field_ranges(), 2)) + + s += "\n\n // registers\n" + s += "\n".join(indent_lines(self._gen_regs(), 2)) + s += "\n\n" + + s += _verilogt.VERILOG_INTERNAL_SIG_DEFS + + s += "\n".join(indent_lines(self._gen_hw_access(), 2)) + + s += "\n\n\n" + s += _verilogt.VERILOG_FSM_READ + + s += "\n\n // ### read logic\n\n" + s += "\n".join(self._gen_read_logic()) + s += "\n" + + s += _verilogt.VERILOG_FSM_WRITE + + s += " // ### write logic (use waddr_word and wdata_reg)\n\n" + s += "\n".join(self._gen_write_logic()) + + s += _verilogt.VERILOG_WRITE_OUTPUT + s += _verilogt.VERILOG_END_ARCH + + return s + + @staticmethod + def _gen_header(input_filename: str, verbose: bool = False) -> str: + s = "// This file was automatically generated with HECTARE\n" + s += "//\n" + s += "// DO NOT EDIT\n" + s += "//\n" + s += "// input_filename = {0}\n".format(input_filename) + if verbose: + s += "// date = {0}\n".format(datetime.datetime.now().ctime()) + s += "// hostname = {0}\n".format(socket.gethostname()) + s += "// user = {0}\n".format(getpass.getuser()) + s += "\n" + return s + + def _gen_ports(self) -> List[str]: + ports = [] + for reg in self.addrmap.regs: + for field in reg.fields: + ports.extend(self._gen_single_port(reg.name, field)) + return ports + + def _gen_reg_addr(self) -> List[str]: + return [ + self._gen_single_addr(reg, self.data_w_bytes) for reg in self.addrmap.regs + ] + + def _gen_field_ranges(self) -> List[str]: + field_ranges = [] + for reg in self.addrmap.regs: + for field in reg.fields: + field_ranges.extend(self._gen_single_field_range(reg.name, field)) + + return field_ranges + + def _gen_regs(self) -> List[str]: + ls = [self._gen_single_reg(reg, self.data_w_bytes) for reg in self.addrmap.regs] + for reg in self.addrmap.regs: + swmod_reg = self._gen_single_reg_swmod(reg, self.data_w_bytes) + if swmod_reg is not None: + ls.append(swmod_reg) + return ls + + def _gen_hw_access(self) -> List[str]: + hw_access_exprs = [] + for reg in self.addrmap.regs: + for field in reg.fields: + hw_access_exprs.extend(self._gen_single_hw_access(reg.name, field)) + + return hw_access_exprs + + def _gen_read_logic(self) -> List[str]: + lines = [] + + lines.append(" always @(posedge clk)") + lines.append(" begin") + lines.append(" rdata_reg <= 32'b0;") + lines.append(" case (raddr_word)") + for reg in self.addrmap.regs: + lines.append(" `C_ADDR_{0}:\n begin".format(reg.name.upper())) + reg_has_assign = False + for field in reg.fields: + line = self._gen_single_sw_rd_access(reg.name, field) + if line is not None: + lines.append(" " + line) + reg_has_assign = True + lines.append(" end") + + if not reg_has_assign: + lines.append(" ;") + + lines.append(" default:") + lines.append(" ;") + lines.append(" endcase") + lines.append(" end") + return lines + + def _gen_write_logic(self) -> List[str]: + lines = [] + lines.append("always @ (posedge clk)") + lines.append(" if (reset)\n begin") + + # generate reset assignments + for reg in self.addrmap.regs: + for field in reg.fields: + line = self._gen_single_reset_assignment(reg.name, field) + if line is not None: + lines.append(" " + line) + + lines.append(" end") + lines.append(" else") + lines.append("") + lines.append(" // default (pulse)") + lines.append(" // TODO") + # TODO: handle here pulses + lines.append("") + lines.append(" // default (swmod)") + for reg in self.addrmap.regs: + has_swmod = any(map(lambda f: f.swmod, reg.fields)) + if has_swmod: + lines.append( + " reg_{name}_swmod <= '0';".format(name=reg.name.lower()) + ) + + lines.append("") + + lines.append( + " if ((state_write == `sWriteResp) && (state_write_prev != `sWriteResp))" + ) + lines.append(" case (waddr_word)") + + for reg in self.addrmap.regs: + lines.append(" `C_ADDR_{0}:\n begin".format(reg.name.upper())) + reg_has_assign = False + for field in reg.fields: + line = self._gen_single_sw_wr_access(reg.name, field) + if line is not None: + lines.append(" " + line) + reg_has_assign = True + # swmod + has_swmod = any(map(lambda f: f.swmod, reg.fields)) + if has_swmod: + lines.append( + " reg_{name}_swmod <= 1'b1;".format( + name=reg.name.lower() + ) + ) + if not reg_has_assign: + lines.append(" ;") + lines.append(" end") + + lines.append(" default:") + lines.append(" ;") + lines.append(" endcase") + + return lines + + @staticmethod + def _gen_single_enum_type(field: Field) -> str: + assert ( + field.encode is not None + ), "_gen_single_enum_type should only be called on enums" + + # right now we only support enums which start at 0 and are sequential + do_vals_start_at_zero_and_inc_by_1 = all( + map( + lambda ab: ab[0] == ab[1], + enumerate(map(lambda it: it.value, field.encode)), + ) + ) + assert ( + do_vals_start_at_zero_and_inc_by_1 + ), "only supported encoding are those who start at 0 and increment by 1" + + lines = [] + lines.append( + "type {encode_name}_t is (".format(encode_name=field.encode.__name__) + ) + for item in field.encode: + lines.append(" " + item.name + ",") + + # last line has also a ",", which needs to be removed + lines[-1] = lines[-1][:-1] + + lines.append(");") + return lines + + @staticmethod + def _gen_single_addr(reg: Register, data_w_bytes: int) -> str: + """ Generate an address constant for a single register + + E.g. constant C_ADDR_SCRATCH : integer := 3; + """ + + word_addr = reg.addr / data_w_bytes + assert word_addr.is_integer(), ( + "Address should be aligned to data width (%d bytes)" % data_w_bytes + ) + word_addr = int(word_addr) + + return "`define C_ADDR_{name} {word_addr}".format( + name=reg.name.upper(), word_addr=word_addr + ) + + @staticmethod + def _gen_single_field_range(reg_name: str, field: Field) -> List[str]: + return [ + "`define C_FIELD_{reg_name}_{field_name}_MSB {msb}".format( + reg_name=reg_name.upper(), field_name=field.name.upper(), msb=field.msb + ), + "`define C_FIELD_{reg_name}_{field_name}_LSB {lsb}".format( + reg_name=reg_name.upper(), field_name=field.name.upper(), lsb=field.lsb + ), + ] + + @staticmethod + def _gen_single_reg(reg: Register, data_w_bytes: int) -> str: + """ signal reg_scratch : std_logic_vector(31 downto 0); """ + + return "logic [{w}-1:0]reg_{name};".format( + name=reg.name.lower(), w=data_w_bytes * 8 + ) + + @staticmethod + def _gen_single_reg_swmod(reg: Register, data_w_bytes: int) -> Optional[str]: + """ generates swmod reg is at least one field in the register has swmod attribute """ + + has_swmod = any(map(lambda f: f.swmod, reg.fields)) + if has_swmod: + return "logic reg_{name}_swmod;".format(name=reg.name.lower()) + else: + return None + + @staticmethod + def _gen_single_port(reg_name: str, field: Field) -> List[str]: + """ Generate output and input ports for a single field + + Several possible cases: no access, HW only read, HW only write, HW r/w. + Also handles swmod attribute, by generating additional _swmod output + """ + + l = [] + + assert ( + field.hw_acc_type != AccessType.rw1 or field.hw_acc_type != AccessType.w1 + ), '"rw1" and "w1" are not supported for HW access' + + if field.encode is not None: + port_type = "{encode_name}_t".format(encode_name=field.encode.__name__) + elif field.msb == field.lsb: + port_type = "" + else: + port_type = "[{msb}:{lsb}]".format( + msb=field.msb - field.lsb, lsb=0 + ) + + if field.hw_acc_type == AccessType.r or field.hw_acc_type == AccessType.rw: + out_str = "output {indent}{port_type}{reg_name}_{field_name}_o,".format( + field_name=field.name.lower(), + reg_name=reg_name.lower(), + port_type=port_type, + indent=(13-len(port_type))*" ", + ) + l.append(out_str) + + if field.hw_acc_type == AccessType.w or field.hw_acc_type == AccessType.rw: + in_str = "input {indent}{port_type}{reg_name}_{field_name}_i,".format( + field_name=field.name.lower(), + reg_name=reg_name.lower(), + port_type=port_type, + indent=(13-len(port_type))*" ", + ) + l.append(in_str) + + if field.swmod: + swmod_str = "output logic {reg_name}_{field_name}_swmod;".format( + field_name=field.name.lower(), reg_name=reg_name.lower() + ) + l.append(swmod_str) + + return l + + @staticmethod + def _gen_single_hw_access(reg_name: str, field: Field, in_reg=True) -> List[str]: + """ + + - Several possible cases: no access, HW only read, HW only write, HW r/w + - if the field is enum, convert from slv to enum for outputs, and from + enum to slv for inputs + + """ + + # TODO: somewhere handle write enable + + l = [] + + assert ( + field.hw_acc_type != AccessType.rw1 or field.hw_acc_type != AccessType.w1 + ), '"rw1" and "w1" are not supported for HW access' + + reg_slice = ( + "{msb}".format(msb=field.msb) + if field.msb == field.lsb + else "{msb}:{lsb}".format(msb=field.msb, lsb=field.lsb,) + ) + + if field.encode is None: + enum_conv_out_left = "" + enum_conv_out_right = "" + enum_conv_in_left = "" + enum_conv_in_right = "" + else: + enum_conv_out_left = "{encode_name}_t'val(to_integer(unsigned(".format( + encode_name=field.encode.__name__ + ) + enum_conv_out_right = ")))" + enum_conv_in_left = "std_logic_vector(to_unsigned({encode_name}_t'pos(".format( + encode_name=field.encode.__name__ + ) + enum_conv_in_right = "), {field_length}))".format( + field_length=field.msb - field.lsb + 1 + ) + + if field.hw_acc_type == AccessType.r or field.hw_acc_type == AccessType.rw: + out_str = "assign {reg_name}_{field_name}_o = {enum_conv_out_left}reg_{reg_name}[{reg_slice}]{enum_conv_out_right};".format( + field_name=field.name.lower(), + reg_name=reg_name.lower(), + reg_slice=reg_slice, + enum_conv_out_left=enum_conv_out_left, + enum_conv_out_right=enum_conv_out_right, + ) + l.append(out_str) + + if field.hw_acc_type == AccessType.w or field.hw_acc_type == AccessType.rw: + in_str = "reg_{reg_name}[{reg_slice}] <= {enum_conv_in_left}{reg_name}_{field_name}_i{enum_conv_in_right};".format( + field_name=field.name.lower(), + reg_name=reg_name.lower(), + reg_slice=reg_slice, + enum_conv_in_left=enum_conv_in_left, + enum_conv_in_right=enum_conv_in_right, + ) + if in_reg: + in_str = "always @(posedge clk)\n " + in_str; + l.append(in_str) + + if field.swmod: + swmod_str = "{reg_name}_{field_name}_swmod <= reg_{reg_name}_swmod;".format( + field_name=field.name.lower(), reg_name=reg_name.lower(), + ) + l.append(swmod_str) + + return l + + @staticmethod + def _gen_single_sw_rd_access(reg_name: str, field: Field) -> Optional[str]: + """ + + Several possible cases: no access, SW only read, SW only write, SW r/w + + reg_idelay_inc(8 downto 0) <= wdata_reg(8 downto 0); + """ + + assert ( + field.sw_acc_type != AccessType.rw1 or field.sw_acc_type != AccessType.w1 + ), '"rw1" and "w1" are not supported for HW access' + + if field.sw_acc_type == AccessType.r or field.sw_acc_type == AccessType.rw: + out_str = "rdata_reg[{msb}:{lsb}] <= reg_{reg_name}[{msb}:{lsb}];".format( + reg_name=reg_name.lower(), msb=field.msb, lsb=field.lsb, + ) + return out_str + + return None + + @staticmethod + def _gen_single_sw_wr_access(reg_name: str, field: Field) -> Optional[str]: + """ + + Several possible cases: no access, SW only read, SW only write, SW r/w + + reg_idelay_inc(8 downto 0) <= wdata_reg(8 downto 0); + """ + + assert ( + field.sw_acc_type != AccessType.rw1 or field.sw_acc_type != AccessType.w1 + ), '"rw1" and "w1" are not supported for HW access' + + if field.sw_acc_type == AccessType.w or field.sw_acc_type == AccessType.rw: + in_str = "reg_{reg_name}[{msb}:{lsb}] <= wdata_reg[{msb}:{lsb}];".format( + reg_name=reg_name.lower(), msb=field.msb, lsb=field.lsb, + ) + return in_str + + return None + + @staticmethod + def _gen_single_reset_assignment(reg_name: str, field: Field) -> Optional[str]: + """ Generate reset assignment if the field has a reset value """ + + if field.reset is not None: + msb = field.msb + lsb = field.lsb + + # we always assign to a vector, even for single-bit signals + assign_val = "{l}'b{val}".format(val=field.reset, l=msb - lsb + 1) + + assign_str = "reg_{reg_name}[{msb}:{lsb}] <= {assign_val};".format( + reg_name=reg_name.lower(), msb=msb, lsb=lsb, assign_val=assign_val + ) + return assign_str + + return None diff --git a/hectare/_verilog_templates.py b/hectare/_verilog_templates.py new file mode 100644 index 0000000..8ed3354 --- /dev/null +++ b/hectare/_verilog_templates.py @@ -0,0 +1,210 @@ +""" +Copyright (c) 2020 Deutsches Elektronen-Synchrotron DESY. + +See LICENSE.txt for license details. +""" +# +# Given that this is machine-generated code, we use the lowest common subset, +# i.e. Verilog 2001. +# We could use nicer SV but there are enough tools not supporting it, that using +# if for machine generated code is pointless +# +VERILOG_LIBS = """ +//library ieee; +//use ieee.std_logic_1164.all; +//use ieee.numeric_std.all; +""" + +VERILOG_PORTS_AXI = """ + input clk, + input reset, + input [G_ADDR_W-1:0]S_AXI_AWADDR, + input [2:0]S_AXI_AWPROT, + input S_AXI_AWVALID, + output S_AXI_AWREADY, + input [31:0]S_AXI_WDATA, + input [32/8-1:0]S_AXI_WSTRB, + input S_AXI_WVALID, + output S_AXI_WREADY, + output [1:0]S_AXI_BRESP, + output S_AXI_BVALID, + input S_AXI_BREADY, + input [G_ADDR_W-1:0]S_AXI_ARADDR, + input [2:0]S_AXI_ARPROT, + input S_AXI_ARVALID, + output S_AXI_ARREADY, + output [31:0]S_AXI_RDATA, + output [1:0]S_AXI_RRESP, + output S_AXI_RVALID, + input S_AXI_RREADY +""" + +VERILOG_INTERNAL_SIG_DEFS = """ + // read + `define sReadIdle 1'b0 + `define sReadValid 1'b1 + logic state_read; + + logic [31:0]rdata_reg; + logic [31:0]raddr_word; + + logic arready_wire; + logic rvalid_wire; + + // write + `define sWriteIdle 2'b00 + `define sWriteWaitData 2'b01 + `define sWriteWaitAddr 2'b10 + `define sWriteResp 2'b11 + logic [1:0]state_write; + logic [1:0]state_write_prev; + + logic [G_ADDR_W-1:0]waddr_reg; + logic [31:0]wdata_reg; + + logic [31:0]waddr_word; + + logic awready_wire; + logic wready_wire; + logic bvalid_wire; +""" + +VERILOG_FSM_READ = """ + always @(posedge clk) + if (reset) + state_read <= `sReadIdle; + else + case (state_read) + `sReadIdle: + if (S_AXI_ARVALID) + state_read <= `sReadValid; + + `sReadValid: + if (S_AXI_RREADY) + state_read <= `sReadIdle; + endcase + + assign raddr_word = {{(32-(G_ADDR_W-2)){1'b0}}, S_AXI_ARADDR[G_ADDR_W-1:2]}; +""" + +VERILOG_FSM_WRITE = """ + always @(*) + case (state_read) + `sReadIdle: + begin + arready_wire = 1'b1; + rvalid_wire = 1'b0; + end + `sReadValid: + begin + arready_wire = 1'b0; + rvalid_wire = 1'b1; + end + default: + begin + arready_wire = 1'b0; + rvalid_wire = 1'b0; + end + endcase + + assign S_AXI_ARREADY = arready_wire; + assign S_AXI_RVALID = rvalid_wire; + assign S_AXI_RDATA = rdata_reg; + assign S_AXI_RRESP = 2'b00; + + always @(posedge clk) + state_write_prev <= state_write; + + always @(posedge clk) + if (reset) + state_write <= `sWriteIdle; + else + case (state_write) + `sWriteIdle: + if (S_AXI_AWVALID && S_AXI_WVALID) + begin + state_write <= `sWriteResp; + waddr_reg <= S_AXI_AWADDR; + wdata_reg <= S_AXI_WDATA; + end + else if (S_AXI_AWVALID && !S_AXI_WVALID) + begin + state_write <= `sWriteWaitData; + waddr_reg <= S_AXI_AWADDR; + end + else if (!S_AXI_AWVALID && S_AXI_WVALID) + begin + state_write <= `sWriteWaitAddr; + wdata_reg <= S_AXI_WDATA; + end + + `sWriteWaitData: + if (S_AXI_WVALID) + begin + state_write <= `sWriteResp; + wdata_reg <= S_AXI_WDATA; + end + + `sWriteWaitAddr: + if (S_AXI_AWVALID) + begin + state_write <= `sWriteResp; + waddr_reg <= S_AXI_AWADDR; + end + + `sWriteResp: + if (S_AXI_BREADY) + state_write <= `sWriteIdle; + endcase + + assign waddr_word = {{(32-(G_ADDR_W-2)){1'b0}}, waddr_reg[G_ADDR_W-1:2]}; +""" + +VERILOG_WRITE_OUTPUT = """ + + always @(*) + case (state_write) + `sWriteIdle: + begin + awready_wire = 1'b1; + wready_wire = 1'b1; + bvalid_wire = 1'b0; + end + `sWriteWaitData: + begin + awready_wire = 1'b0; + wready_wire = 1'b1; + bvalid_wire = 1'b0; + end + `sWriteWaitAddr: + begin + awready_wire = 1'b1; + wready_wire = 1'b0; + bvalid_wire = 1'b0; + end + `sWriteResp: + begin + awready_wire = 1'b0; + wready_wire = 1'b0; + bvalid_wire = 1'b1; + end + default: + begin + awready_wire = 1'b0; + wready_wire = 1'b0; + bvalid_wire = 1'b0; + end + endcase + + assign S_AXI_AWREADY = awready_wire; + assign S_AXI_WREADY = wready_wire; + assign S_AXI_BRESP = 2'b00; + assign S_AXI_BVALID = bvalid_wire; +""" + +VERILOG_BEGIN_ARCH = """ +""" + +VERILOG_END_ARCH = """ +endmodule +""" diff --git a/hectare/hectare.py b/hectare/hectare.py index a359d43..0ea6410 100755 --- a/hectare/hectare.py +++ b/hectare/hectare.py @@ -13,6 +13,7 @@ from hectare._HectareListener import HectareListener from hectare._HectareVhdlGen import HectareVhdlGen +from hectare._HectareVerilogGen import HectareVerilogGen from hectare.__init__ import __version__ as hectare_version @@ -47,6 +48,37 @@ def gen_vhdl_axi(in_filename, out_filename): out_file.close() +def gen_verilog_axi(in_filename, out_filename): + + if out_filename[-2:] != ".v": + raise ValueError("output filename is expected to have .v extension") + + rdlc = RDLCompiler() + rdlc.compile_file(in_filename) + root = rdlc.elaborate() + + walker = RDLWalker(unroll=True) + listener = HectareListener() + walker.walk(root, listener) + print("Parsing finished.") + + verilog_gen = HectareVerilogGen(listener.addrmaps[0], input_filename=in_filename) + s_pkg = verilog_gen.generate_package() + s_verilog = verilog_gen.generate_string() + + print("Generating {0} ...".format(out_filename)) + out_file = open(out_filename, "w") + out_file.write(s_verilog) + out_file.close() + + if s_pkg is not None: + pkg_filename = out_filename.replace(".v", "_pkg.v") + print("Generating {0} ...".format(pkg_filename)) + out_file = open(pkg_filename, "w") + out_file.write(s_pkg) + out_file.close() + + def main(): parser = argparse.ArgumentParser( description="HECTARE - Hamburg Elegant CreaTor from Accelera systemrdl to REgisters" @@ -65,6 +97,13 @@ def main(): type=str, help="generate AXI4-Lite slave", ) + parser.add_argument( + "--axi-verilog", + nargs=1, + dest="verilog_name", + type=str, + help="generate AXI4-Lite slave", + ) args = parser.parse_args() if args.debug: @@ -73,6 +112,8 @@ def main(): try: if args.vhdl_name is not None: gen_vhdl_axi(args.filename, args.vhdl_name[0]) + if args.verilog_name is not None: + gen_verilog_axi(args.filename, args.verilog_name[0]) except RDLCompileError as err: print(err) sys.exit(1)