[dv, flow] Add ELF loader feature Adds a Python script (`utils/elf_to_mem.py`) to parse the test ELF, generate separate ITCM/DTCM memory files, and extract the 'tohost' symbol address. Change-Id: I1fcd97e523206604b1fcb7e6007073263eb9ee14
diff --git a/tests/uvm/Makefile b/tests/uvm/Makefile index 924fe8f..d363942 100644 --- a/tests/uvm/Makefile +++ b/tests/uvm/Makefile
@@ -30,7 +30,7 @@ ENV_DIR = ./env TESTS_DIR = ./tests BIN_DIR = ./bin -UTILS_DIR = $(ROOTDIR)/hw/kelvin/utils +UTILS_DIR = $(ROOTDIR)/hw/kelvin/tests/uvm/utils # Simulation Work Directory SIM_DIR = ./sim_work
diff --git a/tests/uvm/utils/elf_to_mem.py b/tests/uvm/utils/elf_to_mem.py new file mode 100644 index 0000000..58afeb6 --- /dev/null +++ b/tests/uvm/utils/elf_to_mem.py
@@ -0,0 +1,167 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +A script to parse an ELF file and generate Verilog hex-formatted memory files +for ITCM and DTCM, and a simulator arguments file. +""" + +import argparse +import os +import logging +from elftools.elf.elffile import ELFFile + +# Define the memory map for the Kelvin core +MEM_MAP = { + 'itcm': {'base': 0x00000000, 'size': 8 * 1024, 'file': 'itcm.mem'}, + 'dtcm': {'base': 0x00010000, 'size': 32 * 1024, 'file': 'dtcm.mem'}, +} + +def contains(mem_info, addr): + """Checks if the given address is within the memory region.""" + return mem_info['base'] <= addr < mem_info['base'] + mem_info['size'] + +def process_and_dump_segments(segments, mem_info, out_dir, word_size_bytes=16): + """ + Processes a list of memory segments, sorts them, and dumps them to a + Verilog hex file, handling memory locations. Returns True if data was + written. + """ + file_path = os.path.join(out_dir, mem_info['file']) + mem_base = mem_info['base'] + + with open(file_path, 'w') as f: + if not segments: + logging.error("No segments found for %s. Created empty file.", + mem_info['file']) + return False + + segments.sort(key=lambda s: s['p_addr']) + last_addr_written = -1 + + for seg in segments: + p_addr = seg['p_addr'] + data = seg['data'] + data_len = len(data) + current_addr_offset = p_addr - mem_base + + # Check if the segment start address is aligned to the memory word size. + if current_addr_offset % word_size_bytes != 0: + logging.error( + f"ELF segment at address 0x{p_addr:08x} has an unaligned " + f"offset of 0x{current_addr_offset:08x}. Exiting. " + ) + sys.exit(1) + + if current_addr_offset > last_addr_written: + aligned_addr = (current_addr_offset // word_size_bytes) + f.write(f"@{aligned_addr:08x}\n") + + for i in range(0, data_len, word_size_bytes): + chunk = data[i:i + word_size_bytes] + while len(chunk) < word_size_bytes: + chunk += b'\x00' + word = int.from_bytes(chunk, byteorder='little') + f.write(f"{word:0{word_size_bytes*2}x}\n") + + last_addr_written = current_addr_offset + data_len - 1 + return True + +def find_tohost_addr(elf): + """Finds the 'tohost' symbol in the ELF file's symbol table.""" + symtab = elf.get_section_by_name('.symtab') + if not symtab: + logging.warning("No symbol table found in ELF file.") + return None + for symbol in symtab.iter_symbols(): + if symbol.name == 'tohost': + logging.info("Found 'tohost' symbol at 0x%08x", symbol['st_value']) + return symbol['st_value'] + logging.warning("'tohost' symbol not found in ELF file.") + return None + +def main(): + """Main function to parse ELF and generate files.""" + logging.basicConfig(level=logging.INFO, + format='%(levelname)s: %(message)s') + + parser = argparse.ArgumentParser( + description='Generate memory and argument files from an ELF file.') + parser.add_argument('--elf_file', required=True, help='Path to input ELF.') + parser.add_argument('--out_dir', required=True, help='Output directory for .mem files.') + args = parser.parse_args() + + if not os.path.exists(args.out_dir): + logging.info("Output directory '%s' not found. Creating it.", args.out_dir) + os.makedirs(args.out_dir) + + logging.info("Processing ELF file: %s", args.elf_file) + + itcm_segments = [] + dtcm_segments = [] + tohost_addr = None + run_opts_file = os.path.join(args.out_dir, 'elf_run_opts.f') + + try: + with open(args.elf_file, 'rb') as f: + elf = ELFFile(f) + tohost_addr = find_tohost_addr(elf) + + for segment in elf.iter_segments(): + if segment['p_type'] != 'PT_LOAD': + continue + + p_addr = segment['p_paddr'] + data = segment.data() + logging.info("Found segment at 0x%08x, size %d bytes", p_addr, len(data)) + + if contains(MEM_MAP['itcm'], p_addr): + itcm_segments.append({'p_addr': p_addr, 'data': data}) + elif contains(MEM_MAP['dtcm'], p_addr): + dtcm_segments.append({'p_addr': p_addr, 'data': data}) + else: + logging.warning("Segment at 0x%08x is outside known memory map. Skipping.", p_addr) + + except Exception as e: + if isinstance(e, FileNotFoundError): + logging.error(f"ERROR: ELF file not found at {args.elf_file}") + else: + logging.error("An error occurred: %s", e, exc_info=True) + open(run_opts_file, 'w').close() + open(os.path.join(args.out_dir, MEM_MAP['itcm']['file']), 'w').close() + open(os.path.join(args.out_dir, MEM_MAP['dtcm']['file']), 'w').close() + exit(1) + + # Process segments and dump memory files + itcm_written = process_and_dump_segments( + itcm_segments, MEM_MAP['itcm'], args.out_dir) + dtcm_written = process_and_dump_segments( + dtcm_segments, MEM_MAP['dtcm'], args.out_dir) + + # Generate the arguments file + with open(run_opts_file, 'w') as f_args: + logging.info("Generating arguments file: %s", run_opts_file) + if itcm_written: + f_args.write(f"+ITCM_MEM_FILE=" + + f"{os.path.join(args.out_dir, MEM_MAP['itcm']['file'])}\n") + if dtcm_written: + f_args.write(f"+DTCM_MEM_FILE=" + + f"{os.path.join(args.out_dir, MEM_MAP['dtcm']['file'])}\n") + if tohost_addr is not None: + f_args.write(f"+TOHOST_ADDR='h{tohost_addr:08x}\n") + + logging.info("Successfully generated memory and argument files.") + +if __name__ == '__main__': + main()