[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()