Source code for avl._core.memory

# Copyright 2024 Apheleia
#
# Description:
# Apheleia Verification Library Memory Model

import bincopy
import pandas as pd


[docs] class Memory:
[docs] def __init__(self, width : int = 32) -> None: """ Initialize the memory model. :param width: Width of the memory in bits (default is 32). :type width: int :raises ValueError: If width is not a positive integer. """ if width <= 0 or width % 8 != 0: raise ValueError("Width must be a positive integer and a multiple of 8.") self.width = width self.ranges = [] self.memory = {} self.endianness = 'little' self.init_fn = lambda address : 0
def _align_address_(self, address: int) -> int: """ Align the address to the memory width. :param address: Address to align. :type address: int :return: Aligned address. :rtype: int """ return address & ~(self.width // 8 - 1) def _check_address_(self, address: int) -> None: """ Check if the address is valid. :param address: Address to check. :type address: int :raises ValueError: If address is not a positive integer. """ for r in self.ranges: if address >= r[0] and address < r[1]: return True self.miss(address) return False def _get_byte_(self, address: int) -> int: """ Get the byte at the specified address. :param address: Address to get the byte from. :type address: int :return: Byte value at the specified address. :rtype: int :raises KeyError: If address is not in memory. """ if address not in self.memory: self.memory[address] = self.init_fn(address) return self.memory[address]
[docs] def set_init_fn(self, fn : callable) -> None: """ Set the initialization policy for the memory Defined as a lambda function that takes an address and returns a value. :param fn: Function to initialize memory at a given address. :type fn: callable :raises ValueError: If fn is not callable. :example: memory.set_init_fn(lambda address: 0) """ self.init_fn = fn
[docs] def set_endianness(self, endianness: str) -> None: """ Set the endianness of the memory (little / big). :param endianness: Endianness, can be 'little' or 'big'. :type endianness: str :raises ValueError: If endianness is not 'little' or 'big'. """ if endianness not in ['little', 'big']: raise ValueError("Endianness must be either 'little' or 'big'.") self.endianness = endianness
[docs] def add_range(self, start: int, end: int) -> None: """ Add a memory range. :param start: Start address of the memory range. :type start: int :param end: End address of the memory range. :type end: int :raises ValueError: If start address is not less than end address. """ if start >= end: raise ValueError("Start address must be less than end address.") self.ranges.append((start, end))
[docs] def miss(self, address : int) -> None: """ Action to be taken when a memory miss occurs. This method raises a ValueError indicating that the address was not found in memory. Expect user to override this method to implement custom behavior. :param address: Address that caused the miss. :type address: int :raises ValueError: Always raised to indicate a memory miss. """ raise KeyError(f"Miss at {address}")
[docs] def read(self, address: int, num_bytes : int = None) -> int: """ Read a value from the memory at the specified address. Calls miss() if the address is not found in memory. :param address: Address to read from. :type address: int :return: Value at the specified address. :rtype: int """ if num_bytes is None: num_bytes = self.width // 8 self._check_address_(address) self._check_address_(address + num_bytes - 1) data = bytearray() for i in range(num_bytes): data.append(self._get_byte_(address + i)) return int.from_bytes(data, self.endianness)
[docs] def write(self, address: int, value: int, num_bytes : int = None, strobe : int = None) -> None: """ Write a value to the memory at the specified address. Calls miss() if the address is not found in memory. :param address: Address to write to. :type address: int :param value: Value to write. :type value: int :param num_bytes: Number of bytes to write (default is width // 8). :type num_bytes: int, optional :param strobe: Strobe signal :type strobe: int, optional """ if num_bytes is None: num_bytes = self.width // 8 if strobe is None: strobe = (1 << num_bytes) - 1 self._check_address_(address) self._check_address_(address + num_bytes - 1) data = value.to_bytes(num_bytes, self.endianness) for i in range(num_bytes): offset = i if self.endianness == 'little' else num_bytes - 1 - i if strobe & (1 << offset): self.memory[address + i] = data[i]
[docs] def export_to_file(self, filename: str, fmt : str = None) -> None: """ Export memory contents to a file. If fmt is not specified, it will be inferred from the file extension. :param filename: Path to the file where memory contents will be saved. :type filename: str :param fmt: Format of the file, can be 'vhex' or 'vbin'. :type fmt: str, optional :raises ValueError: If format is not supported. """ def exists(address: int): """ Pad the whole memory width """ exists = False for i in range(address, address + self.width // 8): if i in self.memory: exists = True break self.read(self._align_address_(address)) return exists def verilog(filename: str, fmt : str) -> None: """ Export memory contents to a verilog hex file. """ with open(filename, 'w') as f: for r in self.ranges: new_address = True for a in range(r[0], r[1], self.width // 8): if exists(a): if new_address: f.write(f"@{a:04x}\n") new_address = False if fmt == "vhex": f.write(f"{self.read(a, self.width // 8):0{self.width // 4}x}\n") elif fmt == "vbin": f.write(f"{self.read(a, self.width // 8):0{self.width}b}\n") else: new_address = True def pandas(filename: str, fmt : str) -> None: """ Export memory contents to a pandas DataFrame and save to file. """ df = pd.DataFrame(list(self.memory.items()), columns=["addr", "data"]) if fmt == "csv": df.to_csv(filename, index=False) elif fmt == "json": df.to_json(filename, orient='records', lines=True) else: raise ValueError(f"Unsupported file format: {fmt}") def bcopy(filename: str, fmt : str) -> None: """ Export memory contents using bincopy. """ bf = bincopy.BinFile() for a,d in self.memory.items(): bf[a] = d if fmt in ["ihex", "hex", "ihx"]: with open(filename, 'w') as f: f.write(bf.as_ihex()) elif fmt == "srec": with open(filename, 'w') as f: f.write(bf.as_srec()) elif fmt == "ti-txt": with open(filename, 'w') as f: f.write(bf.as_ti_txt()) elif fmt == "vmem": with open(filename, 'w') as f: f.write(bf.as_verilog_vmem()) else: raise ValueError(f"Unsupported file format: {fmt}") # Default format from file extension if fmt is None: fmt = filename.split('.')[-1] if fmt in ["vhex", "vbin"]: verilog(filename, fmt=fmt) elif fmt in ["csv", "json"]: pandas(filename, fmt=fmt) else: try: bcopy(filename, fmt=fmt) except Exception as e: raise ValueError(f"Unsupported file format: {fmt}") from e
[docs] def import_from_file(self, filename: str, fmt : str = None) -> None: """ Load memory contents from a file. If fmt is not specified, it will be inferred from the file extension. :param filename: Path to the file containing memory contents. :type filename: str :raises FileNotFoundError: If the file does not exist. """ def verilog(filename: str, fmt : str) -> None: """ Load memory contents from a verilog hex file. """ addr = self.ranges[0][0] if self.ranges else 0 with open(filename) as f: for raw in f: line = raw.strip() if not line: continue # Comments (//, ;, #) for sep in ('//', ';', '#'): if sep in line: line = line.split(sep, 1)[0] line = line.strip() if not line: continue # New base address if line.startswith('@'): try: addr = int(line[1:], 16) except Exception as e: raise ValueError(f"Invalid address marker: {line!r}") from e continue # Tokens separated by whitespace t = line.replace(" ", "") if t.startswith('0x') or t.startswith('0b'): t = t[2:] if fmt == "vhex": bytes = int(t, 16).to_bytes(len(t) // 2, self.endianness) for i in range(len(bytes)): self.write(addr+i, bytes[i], 1) elif fmt == "vbin": bytes = int(t, 2).to_bytes(len(t) // 8, self.endianness) for i in range(len(bytes)): self.write(addr+i, bytes[i], 1) else: raise ValueError(f"Unsupported file format: {fmt}") addr += self.width // 8 def pandas(filename: str, fmt : str) -> None: """ Export memory contents to a pandas DataFrame and save to file. """ if fmt == "csv": df = pd.read_csv(filename) elif fmt == "json": df = pd.read_json(filename, orient='records', lines=True) else: raise ValueError(f"Unsupported file format: {fmt}") self.memory = dict(zip(df["addr"], df["data"], strict=False)) def bcopy(filename: str, fmt : str) -> None: """ Import memory contents using bincopy. """ bf = bincopy.BinFile(filename) for start_address, data in bf.segments: for offset, byte_val in enumerate(data): addr = start_address + offset self.memory[addr] = byte_val # Default format from file extension if fmt is None: fmt = filename.split('.')[-1] if fmt in ["vhex", "vbin"]: verilog(filename, fmt=fmt) elif fmt in ["csv", "json"]: pandas(filename, fmt=fmt) else: try: bcopy(filename, fmt=fmt) except Exception as e: raise ValueError(f"Unsupported file format: {fmt}") from e
__all__ = ["Memory"]