[otbn,dv] Add loop warping support to ISS
This is the first commit for "loop warping". The idea is that you
might want to see the loop counter in the design incrementing at some
really big values (it goes up to 2^32), but don't want to simulate 4
billion cycles to get there.
Instead, you want to simulate the first few iterations around the loop
and then "jump" to the last few. This patch teaches the ISS to do this
when special symbols are added to the ELF file. Specifically, such a
symbol should have a name of the form:
_loop_warp_123_456
and value that match an IMEM address. This means "if I execute the
given address and the innermost loop count is 123, alter the loop
count to 345".
We put the counts in the symbol name and address in the value so that
you can easily specify these in assembly code, without needing to know
the exact layout of the generated binary.
Of course, there's a duplication problem: what if you had two
different loops where you wanted to do the same warping? To get around
this, we allow any string after the second count:
_loop_warp_123_456ANYOLDRUBBISH
and ignore it.
Signed-off-by: Rupert Swarbrick <rswarbrick@lowrisc.org>
diff --git a/hw/ip/otbn/dv/otbnsim/sim/elf.py b/hw/ip/otbn/dv/otbnsim/sim/elf.py
index 69ff7c8..3e9aee7 100644
--- a/hw/ip/otbn/dv/otbnsim/sim/elf.py
+++ b/hw/ip/otbn/dv/otbnsim/sim/elf.py
@@ -4,6 +4,7 @@
'''OTBN ELF file handling'''
+import re
from typing import List, Optional, Tuple
from elftools.elf.elffile import ELFFile, SymbolTableSection # type: ignore
@@ -11,7 +12,7 @@
from shared.mem_layout import get_memory_layout
from .decode import decode_bytes
-from .sim import OTBNSim
+from .sim import LoopWarps, OTBNSim
_SegList = List[Tuple[int, bytes]]
@@ -117,13 +118,8 @@
return (imem_bytes, dmem_bytes)
-def _get_exp_end_addr(elf_file: ELFFile) -> Optional[int]:
- '''Get the expected end address for a run of this binary
-
- This is the value of the ELF symbol _expected_end_addr. If the symbol
- doesn't exist, returns None.
-
- '''
+def _get_symtab(elf_file: ELFFile) -> Optional[SymbolTableSection]:
+ '''Get the symbol table from elf_file if there is one'''
section = elf_file.get_section_by_name('.symtab')
if section is None:
# No symbol table found
@@ -133,7 +129,17 @@
# Huh? The .symtab section isn't a symbol table?? Oh well, nevermind.
return None
- for sym in section.iter_symbols():
+ return section
+
+
+def _get_exp_end_addr(symtab: SymbolTableSection) -> Optional[int]:
+ '''Get the expected end address for a run of this binary
+
+ This is the value of the ELF symbol _expected_end_addr. If the symbol
+ doesn't exist, returns None.
+
+ '''
+ for sym in symtab.iter_symbols():
if sym.name == '_expected_end_addr':
assert isinstance(sym['st_value'], int)
return sym['st_value']
@@ -141,21 +147,74 @@
return None
+def _get_loop_warps(symtab: SymbolTableSection) -> LoopWarps:
+ '''Return a list of the requested loop warps
+
+ These are read in the format described in sim.py. A warp is specified as a
+ symbol of the form
+
+ _loop_warp_FROM_TO
+
+ pointing at the address where it should take effect. If a symbol specifies
+ TO < FROM, we raise a RuntimeError. If there are multiple symbols that
+ specify warps at a particular address/count pair, we raise a RuntimeError.
+
+ '''
+ pat = re.compile(r'_loop_warp_([0-9]+)_([0-9]+)')
+
+ ret = {} # type: LoopWarps
+
+ for sym in symtab.iter_symbols():
+ match = pat.match(sym.name)
+ if match is None:
+ continue
+
+ count_from = int(match.group(1))
+ count_to = int(match.group(2))
+ addr = sym['st_value']
+ assert isinstance(addr, int)
+
+ if count_to < count_from:
+ raise RuntimeError('Loop warp instruction from symbol {!r}'
+ 'implies an infinite loop (because {} < {}).'
+ .format(sym.name, count_to, count_from))
+
+ at_addr = ret.setdefault(addr, {})
+ if count_from in at_addr:
+ raise RuntimeError('Multiple symbols specify a loop warp at {:#x} '
+ 'with a starting count of {}.'
+ .format(addr, count_from))
+
+ at_addr[count_from] = count_to
+
+ return ret
+
+
def _read_elf(path: str,
imem_desc: _MemDesc,
- dmem_desc: _MemDesc) -> Tuple[bytes, bytes, Optional[int]]:
+ dmem_desc: _MemDesc) -> Tuple[bytes,
+ bytes,
+ Optional[int],
+ LoopWarps]:
'''Load the ELF file at path.
- Returns a tuple (imem_bytes, dmem_bytes, exp_end_addr). The first two
- coordinates are as returned by _get_elf_mem_data. exp_end_addr is as
- returned by _get_exp_end_addr.
+ Returns a tuple (imem_bytes, dmem_bytes, exp_end_addr, loop_warps). The
+ first two coordinates are as returned by _get_elf_mem_data. exp_end_addr is
+ as returned by _get_exp_end_addr. loop_warps is as returned by
+ _get_loop_warps.
'''
with open(path, 'rb') as handle:
elf_file = ELFFile(handle)
imem_bytes, dmem_bytes = _get_elf_mem_data(elf_file,
imem_desc, dmem_desc)
- exp_end_addr = _get_exp_end_addr(elf_file)
+ symtab = _get_symtab(elf_file)
+ if symtab is None:
+ exp_end_addr = None
+ loop_warps = {} # type: LoopWarps
+ else:
+ exp_end_addr = _get_exp_end_addr(symtab)
+ loop_warps = _get_loop_warps(symtab)
assert len(imem_bytes) <= imem_desc[1]
if len(imem_bytes) & 3:
@@ -163,7 +222,7 @@
'not a multiple of 4.'
.format(path, len(imem_bytes)))
- return (imem_bytes, dmem_bytes, exp_end_addr)
+ return (imem_bytes, dmem_bytes, exp_end_addr, loop_warps)
def load_elf(sim: OTBNSim, path: str) -> Optional[int]:
@@ -176,9 +235,13 @@
imem_desc = mems['IMEM']
dmem_desc = mems['DMEM']
- imem_bytes, dmem_bytes, exp_end = _read_elf(path, imem_desc, dmem_desc)
+ (imem_bytes, dmem_bytes,
+ exp_end, loop_warps) = _read_elf(path, imem_desc, dmem_desc)
+
imem_insns = decode_bytes(0, imem_bytes)
sim.load_program(imem_insns)
+ sim.loop_warps = loop_warps
sim.load_data(dmem_bytes)
+
return exp_end
diff --git a/hw/ip/otbn/dv/otbnsim/sim/loop.py b/hw/ip/otbn/dv/otbnsim/sim/loop.py
index b584a7f..0b38580 100644
--- a/hw/ip/otbn/dv/otbnsim/sim/loop.py
+++ b/hw/ip/otbn/dv/otbnsim/sim/loop.py
@@ -2,7 +2,7 @@
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0
-from typing import List, Optional
+from typing import Dict, List, Optional
from .constants import ErrBits
from .trace import Trace
@@ -112,11 +112,13 @@
# instruction.
self.err_flag = True
- def step(self, pc: int) -> Optional[int]:
+ def step(self, pc: int, warps: Dict[int, int]) -> Optional[int]:
'''Update loop stack. If we should loop, return new PC'''
self._pop_stack_on_commit = False
+ self.apply_warps(warps)
+
if self.is_last_insn_in_loop_body(pc):
assert self.stack
top = self.stack[-1]
@@ -156,3 +158,22 @@
def abort(self) -> None:
self.trace = []
self.err_flag = False
+
+ def apply_warps(self, warps: Dict[int, int]) -> None:
+ '''Apply any loop warping specified by warps.
+
+ Here, warps maps values for the innermost loop iteration count from
+ what they are currently to what they should be warped to.
+
+ '''
+ if not self.stack:
+ return
+
+ top = self.stack[-1]
+ cur_iter_count = top.loop_count - (1 + top.restarts_left)
+ new_iter_count = warps.get(cur_iter_count)
+ if new_iter_count is None:
+ return
+
+ assert cur_iter_count <= new_iter_count
+ top.restarts_left = top.loop_count - new_iter_count - 1
diff --git a/hw/ip/otbn/dv/otbnsim/sim/sim.py b/hw/ip/otbn/dv/otbnsim/sim/sim.py
index cfc4d7e..1b698c9 100644
--- a/hw/ip/otbn/dv/otbnsim/sim/sim.py
+++ b/hw/ip/otbn/dv/otbnsim/sim/sim.py
@@ -2,7 +2,7 @@
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0
-from typing import Iterator, List, Optional, Tuple
+from typing import Dict, Iterator, List, Optional, Tuple
from .isa import OTBNInsn
from .state import OTBNState
@@ -12,17 +12,28 @@
_TEST_RND_DATA = \
0x99999999_99999999_99999999_99999999_99999999_99999999_99999999_99999999
+# A dictionary that defines a function of the form "address -> from -> to". If
+# PC is the current PC and cnt is the count for the innermost loop then
+# warps[PC][cnt] = new_cnt means that we should warp the current count to
+# new_cnt.
+LoopWarps = Dict[int, Dict[int, int]]
+
class OTBNSim:
def __init__(self) -> None:
self.state = OTBNState()
self.program = [] # type: List[OTBNInsn]
+ self.loop_warps = {} # type: LoopWarps
self.stats = None # type: Optional[ExecutionStats]
self._execute_generator = None # type: Optional[Iterator[None]]
def load_program(self, program: List[OTBNInsn]) -> None:
self.program = program.copy()
+ def add_loop_warp(self, addr: int, from_cnt: int, to_cnt: int) -> None:
+ '''Add a new loop warp to the simulation'''
+ self.loop_warps.setdefault(addr, {})[from_cnt] = to_cnt
+
def load_data(self, data: bytes) -> None:
self.state.dmem.load_le_words(data)
@@ -120,7 +131,7 @@
self.stats.record_stall()
else:
assert self._execute_generator is None
- self.state.post_insn()
+ self.state.post_insn(self.loop_warps.get(self.state.pc, {}))
if collect_stats:
self.stats.record_insn(insn, self.state)
diff --git a/hw/ip/otbn/dv/otbnsim/sim/state.py b/hw/ip/otbn/dv/otbnsim/sim/state.py
index 33d7788..0a613a4 100644
--- a/hw/ip/otbn/dv/otbnsim/sim/state.py
+++ b/hw/ip/otbn/dv/otbnsim/sim/state.py
@@ -2,7 +2,7 @@
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0
-from typing import List, Optional
+from typing import Dict, List, Optional
from shared.mem_layout import get_memory_layout
@@ -74,8 +74,8 @@
def loop_start(self, iterations: int, bodysize: int) -> None:
self.loop_stack.start_loop(self.pc + 4, iterations, bodysize)
- def loop_step(self) -> None:
- back_pc = self.loop_stack.step(self.pc)
+ def loop_step(self, loop_warps: Dict[int, int]) -> None:
+ back_pc = self.loop_stack.step(self.pc, loop_warps)
if back_pc is not None:
self.set_next_pc(back_pc)
@@ -247,10 +247,10 @@
return True
- def post_insn(self) -> None:
+ def post_insn(self, loop_warps: Dict[int, int]) -> None:
'''Update state after running an instruction but before commit'''
self.ext_regs.increment_insn_cnt()
- self.loop_step()
+ self.loop_step(loop_warps)
self.gprs.post_insn()
self._err_bits |= self.gprs.err_bits() | self.loop_stack.err_bits()
diff --git a/hw/ip/otbn/dv/otbnsim/stepped.py b/hw/ip/otbn/dv/otbnsim/stepped.py
index 825361a..51af33d 100755
--- a/hw/ip/otbn/dv/otbnsim/stepped.py
+++ b/hw/ip/otbn/dv/otbnsim/stepped.py
@@ -23,6 +23,12 @@
load_elf <path> Load the ELF file at <path>, replacing current
contents of DMEM and IMEM.
+ add_loop_warp <addr> <from> <to>
+
+ Add a loop warp to the simulation. This will trigger
+ at address <addr> and will jump from iteration <from>
+ to iteration <to>.
+
load_d <path> Replace the current contents of DMEM with <path>
(read as an array of 32-bit little-endian words)
@@ -125,6 +131,30 @@
load_elf(sim, path)
+def on_add_loop_warp(sim: OTBNSim, args: List[str]) -> None:
+ '''Add a loop warp to the simulation'''
+ if len(args) != 3:
+ raise ValueError('add_loop_warp expects exactly 3 arguments. Got {}.'
+ .format(args))
+
+ try:
+ addr = int(args[0], 0)
+ if addr < 0:
+ raise ValueError('addr is negative')
+ from_cnt = int(args[1], 0)
+ if from_cnt < 0:
+ raise ValueError('from_cnt is negative')
+ to_cnt = int(args[2], 0)
+ if to_cnt < 0:
+ raise ValueError('to_cnt is negative')
+ except ValueError as err:
+ raise ValueError('Bad argument to add_loop_warp: {}'
+ .format(err)) from None
+
+ print('ADD_LOOP_WARP {:#x} {} {}'.format(addr, from_cnt, to_cnt))
+ sim.add_loop_warp(addr, from_cnt, to_cnt)
+
+
def on_load_d(sim: OTBNSim, args: List[str]) -> None:
'''Load contents of data memory from file at path given by only argument'''
if len(args) != 1:
@@ -207,6 +237,7 @@
'step': on_step,
'run': on_run,
'load_elf': on_load_elf,
+ 'add_loop_warp': on_add_loop_warp,
'load_d': on_load_d,
'load_i': on_load_i,
'dump_d': on_dump_d,