| #!/usr/bin/env python3 | |
| """ | |
| This script allows to convert HEX file with firmware to a verilog ROM | |
| implementation. The HEX file must be generated using the following command: | |
| "objcopy -O verilog <input ELF> <output HEX>". | |
| The script generates a series of verilog case statements. Those statements are | |
| then injected to a template file and as a result a new verilog file is written. | |
| The verilog template file name is hard coded as "progmem.v.template". The | |
| output file with the ROM is named "progmem.v". | |
| Important! The script assumes that the firmware is placed at 0x00100000 address | |
| and will convert to verilog content beginning at that address only ! | |
| """ | |
| import argparse | |
| import sys | |
| # ============================================================================= | |
| class Templates: | |
| memory_case = """ | |
| module progmem | |
| ( | |
| // Closk & reset | |
| input wire clk, | |
| input wire rstn, | |
| // PicoRV32 bus interface | |
| input wire valid, | |
| output wire ready, | |
| input wire [31:0] addr, | |
| output wire [31:0] rdata | |
| ); | |
| // ============================================================================ | |
| localparam MEM_SIZE_BITS = {mem_size}; // In 32-bit words | |
| localparam MEM_SIZE = 1 << MEM_SIZE_BITS; | |
| localparam MEM_ADDR_MASK = 32'h0010_0000; | |
| // ============================================================================ | |
| wire [MEM_SIZE_BITS-1:0] mem_addr; | |
| reg [31:0] mem_data; | |
| always @(posedge clk) | |
| case (mem_addr) | |
| {mem_data} | |
| default: mem_data <= 32'hDEADBEEF; | |
| endcase | |
| // ============================================================================ | |
| reg o_ready; | |
| always @(posedge clk or negedge rstn) | |
| if (!rstn) o_ready <= 1'd0; | |
| else o_ready <= valid && ((addr & MEM_ADDR_MASK) != 0); | |
| // Output connectins | |
| assign ready = o_ready; | |
| assign rdata = mem_data; | |
| assign mem_addr = addr[MEM_SIZE_BITS+1:2]; | |
| endmodule | |
| """ | |
| memory_initial = """ | |
| module progmem | |
| ( | |
| // Closk & reset | |
| input wire clk, | |
| input wire rstn, | |
| // PicoRV32 bus interface | |
| input wire valid, | |
| output wire ready, | |
| input wire [31:0] addr, | |
| output wire [31:0] rdata | |
| ); | |
| // ============================================================================ | |
| localparam MEM_SIZE_BITS = {mem_size}; // In 32-bit words | |
| localparam MEM_SIZE = 1 << MEM_SIZE_BITS; | |
| localparam MEM_ADDR_MASK = 32'h0010_0000; | |
| // ============================================================================ | |
| wire [MEM_SIZE_BITS-1:0] mem_addr; | |
| reg [31:0] mem_data; | |
| reg [31:0] mem[0:MEM_SIZE]; | |
| initial begin | |
| {mem_data} | |
| end | |
| always @(posedge clk) | |
| mem_data <= mem[mem_addr]; | |
| // ============================================================================ | |
| reg o_ready; | |
| always @(posedge clk or negedge rstn) | |
| if (!rstn) o_ready <= 1'd0; | |
| else o_ready <= valid && ((addr & MEM_ADDR_MASK) != 0); | |
| // Output connectins | |
| assign ready = o_ready; | |
| assign rdata = mem_data; | |
| assign mem_addr = addr[MEM_SIZE_BITS+1:2]; | |
| endmodule | |
| """ | |
| # ============================================================================= | |
| def load_hex_file(file_name): | |
| """ | |
| Loads a "HEX" file generated using the command: | |
| 'objcopy -O verilog firmware.elf firmware.hex' | |
| """ | |
| sys.stderr.write("Loading 'HEX' from: " + file_name + "\n") | |
| # Load and parse HEX data | |
| sections = {} | |
| section = 0 # If no '@' is specified the code will end up at addr. 0 | |
| hex_data = [] | |
| with open(file_name, "r") as fp: | |
| for line in fp.readlines(): | |
| # Address, create new section | |
| if line.startswith("@"): | |
| section = int(line[1:], 16) | |
| hex_data = [] | |
| sections[section] = hex_data | |
| continue | |
| # Data, append to current section | |
| else: | |
| hex_data += line.split() | |
| # Convert section data to bytes | |
| for section in sections.keys(): | |
| sections[section] = bytes([int(s, 16) for s in sections[section]]) | |
| # Dump sections | |
| sys.stderr.write("Sections:\n") | |
| for section in sections.keys(): | |
| length = len(sections[section]) | |
| sys.stderr.write( | |
| " @%08X - @%08X, %d bytes\n" % (section, section + length, length) | |
| ) | |
| return sections | |
| def modify_code_templte(sections, rom_style): | |
| """ | |
| Modifies verilog ROM template by inserting case statements with the | |
| ROM content. Requires the sections dict to contain a section beginning | |
| at 0x00100000 address. | |
| Returns a string with the verilog code | |
| """ | |
| # Get section at 0x00100000 | |
| data = sections[0x00100000] | |
| # Pad to make length a multiply of 4 | |
| if len(data) % 4: | |
| dummy_cnt = ((len(data) // 4) + 1) * 4 - len(data) | |
| data += bytes(dummy_cnt) | |
| # Determine memory size bits (in words) | |
| mem_size_bits = len(data).bit_length() - 2 | |
| sys.stderr.write("ROM size (words): %d bits\n" % mem_size_bits) | |
| # Encode verilog case statements | |
| if rom_style == "case": | |
| # Generate statements | |
| case_statements = "" | |
| for i in range(len(data) // 4): | |
| # Little endian | |
| data_word = data[4 * i + 0] | |
| data_word |= data[4 * i + 1] << 8 | |
| data_word |= data[4 * i + 2] << 16 | |
| data_word |= data[4 * i + 3] << 24 | |
| statement = " 'h%04X: mem_data <= 32'h%08X;\n" % (i, data_word) | |
| case_statements += statement | |
| # Return the code | |
| return Templates.memory_case.format( | |
| mem_size=mem_size_bits, mem_data=case_statements | |
| ) | |
| # Encode data as initial statements for a verilog array | |
| if rom_style == "initial": | |
| # Generate statements | |
| initial_statements = "" | |
| for i in range(len(data) // 4): | |
| # Little endian | |
| data_word = data[4 * i + 0] | |
| data_word |= data[4 * i + 1] << 8 | |
| data_word |= data[4 * i + 2] << 16 | |
| data_word |= data[4 * i + 3] << 24 | |
| statement = " mem['h%04X] <= 32'h%08X;\n" % (i, data_word) | |
| initial_statements += statement | |
| # Return the code | |
| return Templates.memory_initial.format( | |
| mem_size=mem_size_bits, mem_data=initial_statements | |
| ) | |
| # Error | |
| sys.stdout.write("Invalid ROM style '%s'\n" % rom_style) | |
| return "" | |
| # ============================================================================= | |
| def main(): | |
| # Argument parser | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument("hex", type=str, help="Input HEX file") | |
| parser.add_argument( | |
| "--rom-style", type=str, default="case", help="ROM style" | |
| ) | |
| args = parser.parse_args() | |
| # Load HEX | |
| sections = load_hex_file(args.hex) | |
| # Generate verilog code | |
| code = modify_code_templte(sections, args.rom_style) | |
| # Output verilog code | |
| sys.stdout.write(code) | |
| sys.stdout.flush() | |
| # ============================================================================= | |
| if __name__ == "__main__": | |
| main() |