Files
ardupilot/Tools/renode/run.py
T
Andrew Tridgell cac7c345ab Tools: fix Renode launcher validation and ELF bootloader conversion
Reject unsafe cache identifiers and invalid TCP ports, and handle hosts without CPU-affinity APIs. Allow ELF bootloader conversion without application layout arguments, which are unused for ELF inputs.
2026-09-05 20:44:29 +10:00

1385 lines
59 KiB
Python
Executable File

#!/usr/bin/env python3
# AP_FLAKE8_CLEAN
'''
Launch an ArduPilot board under Renode, in the style of the AM32
harness's gen_target.py --run:
Tools/renode/run.py <board>
drops into an interactive Renode monitor with the firmware running and
the first hardware UART on tcp:localhost:5762. Select a different
SERIAL_ORDER entry with --serial, or use --uds to expose it as a Unix
domain socket below the board state directory.
Persistent media defaults to renode/<board>/ at the repository root. Use
--state-dir to select an exact alternative directory.
Renode is found from --renode, $RENODE, or PATH, in that order. The
performance patches in patches/ matter (roughly 4x): point --renode at
a build of renode v1.16.1 with them applied (see the README).
'''
import argparse
import base64
import binascii
import errno
import json
import os
import secrets
import shutil
import signal
import socket
import stat
import struct
import subprocess
import sys
import time
import zlib
from pathlib import Path
try:
import fcntl
except ImportError:
fcntl = None
import dfu
import fat_image
import gen_board
import renode_data
IOMCU_FLASH_BASE = 0x08000000
IOMCU_FLASH_SIZE = 64 * 1024
IOMCU_APP_OFFSET = 4 * 1024
IOMCU_FIRMWARE_SIZE = IOMCU_FLASH_SIZE - IOMCU_APP_OFFSET
# 1.5 ms is the longest tested reliable quantum for the 1.5 Mbaud FMU/IOMCU
# link; 2 ms loses bytes, while shorter quanta add synchronization overhead.
IOMCU_GLOBAL_QUANTUM = '0.0015'
GDB_LAUNCH = r'''#!/bin/bash
# GENERATED by Tools/renode/run.py.
# Waits for Renode's gdb stub to listen, then attaches.
echo "waiting for the Renode gdb server on port $PORT ..."
for i in $(seq 1 300); do
if [ -e "$READY" ]; then
break
fi
sleep 0.1
done
if [ ! -e "$READY" ]; then
echo "timed out waiting for the ChibiOS GDB proxy" >&2
exit 1
fi
exec "$GDB" \
-ex "set confirm off" \
-ex "set pagination off" \
-ex "set remotetimeout 300" \
-ex "target remote 127.0.0.1:$PORT" \
"$ELF"
'''
def hwdef_value(hwdef_h, name):
'''integer value of a #define in a generated hwdef.h, or None'''
try:
for line in open(hwdef_h):
parts = line.split()
if len(parts) >= 3 and parts[0] == '#define' and parts[1] == name:
try:
return int(parts[2], 0)
except ValueError:
return None
except OSError:
pass
return None
def storage_region(hwdef_h):
'''(base, size) of the AP_FlashStorage sectors, which must read 0xFF.
AP_FlashStorage always uses two sectors starting at
STORAGE_FLASH_PAGE; the sector map is per MCU family.'''
page = hwdef_value(hwdef_h, 'STORAGE_FLASH_PAGE')
if page is None:
return None
text = open(hwdef_h).read() if os.path.exists(hwdef_h) else ''
if 'STM32H7' in text:
sectors = [128 * 1024] * 16 # uniform on the H743
elif 'STM32G4' in text:
sectors = [2 * 1024] * 256 # uniform on the G474
elif 'STM32L4' in text:
sectors = [2 * 1024] * 512 # uniform on L431/L476/L496
elif 'STM32F103' in text:
sectors = [1 * 1024] * 128 # uniform on medium-density F103
elif 'STM32F105' in text:
sectors = [2 * 1024] * 128 # uniform on connectivity-line F105
elif 'STM32F3' in text:
sectors = [2 * 1024] * 128 # uniform on the F303xC
elif 'STM32F7' in text:
# F765/F767 flight-controller hwdefs use the 2 MiB dual-bank layout.
bank = [16 * 1024] * 4 + [64 * 1024] + [128 * 1024] * 7
sectors = bank + bank
elif 'STM32F4' in text:
sectors = [16 * 1024] * 4 + [64 * 1024] + [128 * 1024] * 7
else:
return None
if page + 1 >= len(sectors):
return None
base = 0x08000000 + sum(sectors[:page])
return base, sectors[page] + sectors[page + 1]
def make_erased(path, size):
'''Create a persistent erased image without replacing existing state.'''
if path.exists():
if path.stat().st_size != size:
sys.exit('%s is %u bytes; expected %u (move it aside to reinitialize)' %
(path, path.stat().st_size, size))
return path
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, 'wb') as f:
f.write(b'\xff' * size)
return path
def prepare_unix_socket(path):
"""Remove an inactive socket at path, rejecting live or non-socket paths."""
path = Path(path)
if not hasattr(socket, 'AF_UNIX'):
raise ValueError('Unix domain sockets are unavailable on this host')
path.parent.mkdir(parents=True, exist_ok=True)
try:
original = path.lstat()
except FileNotFoundError:
original = None
if original is not None and not stat.S_ISSOCK(original.st_mode):
raise ValueError('refusing to replace non-socket path %s' % path)
probe = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
probe.settimeout(0.2)
try:
probe.connect(str(path))
except socket.timeout as error:
raise ValueError('timed out probing Unix socket %s' % path) from error
except OSError as error:
if error.errno == errno.ENOENT:
return
if error.errno != errno.ECONNREFUSED:
raise ValueError('cannot probe Unix socket %s: %s' %
(path, error)) from error
else:
raise ValueError('Unix socket %s is already in use' % path)
finally:
probe.close()
if original is None:
return
try:
current = path.lstat()
except FileNotFoundError:
return
if ((current.st_dev, current.st_ino) !=
(original.st_dev, original.st_ino) or
not stat.S_ISSOCK(current.st_mode)):
raise ValueError('Unix socket %s changed while probing it' % path)
path.unlink()
def reserve_unix_socket(path):
"""Reserve a socket name for this process and remove any stale endpoint."""
if fcntl is None:
raise ValueError('Unix socket locking is unavailable on this host')
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
lock_path = path.with_name(path.name + '.lock')
lock_fd = os.open(lock_path, os.O_RDWR | os.O_CREAT, 0o600)
try:
fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError as error:
os.close(lock_fd)
if error.errno in (errno.EACCES, errno.EAGAIN):
raise ValueError('Unix socket %s is already reserved' % path) from error
raise
try:
prepare_unix_socket(path)
except BaseException:
os.close(lock_fd)
raise
return lock_fd
def cleanup_unix_socket(path):
"""Remove an inactive socket path left behind when Renode exits."""
if path is None:
return
try:
prepare_unix_socket(path)
except (OSError, ValueError):
# Preserve a path which was replaced or acquired by another process.
pass
def make_mcu_id(path):
'''Return a persistent, randomly generated 96-bit STM32 unique ID.'''
try:
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
except FileExistsError:
pass
except OSError as error:
sys.exit('failed to create MCU ID %s: %s' % (path, error))
else:
try:
with os.fdopen(fd, 'w') as mcu_id_file:
mcu_id_file.write(secrets.token_hex(12) + '\n')
except OSError as error:
sys.exit('failed to write MCU ID %s: %s' % (path, error))
try:
text = path.read_text().strip()
mcu_id = bytes.fromhex(text)
except (OSError, ValueError) as error:
sys.exit('invalid MCU ID in %s: %s' % (path, error))
if len(mcu_id) != 12 or len(text) != 24:
sys.exit('%s must contain exactly 24 hexadecimal characters' % path)
return mcu_id
def make_flash_image(path, size, legacy_regions):
'''Create full internal flash, importing the old region images once.'''
if path.exists():
if path.stat().st_size != size:
sys.exit('%s is %u bytes; expected %u for this board '
'(move it aside to reinitialize)' %
(path, path.stat().st_size, size))
return path
contents = bytearray(b'\xff' * size)
migrated = []
for legacy_path, address, expected_size in legacy_regions:
if not legacy_path.exists():
continue
legacy_size = legacy_path.stat().st_size
if expected_size is not None and legacy_size != expected_size:
sys.exit('%s is %u bytes; expected %u '
'(move it aside to skip migration)' %
(legacy_path, legacy_size, expected_size))
offset = address - 0x08000000
if legacy_size == 0 or offset < 0 or offset + legacy_size > size:
sys.exit('%s does not fit in the %u-byte internal flash '
'(move it aside to skip migration)' % (legacy_path, size))
with open(legacy_path, 'rb') as legacy:
contents[offset:offset + legacy_size] = legacy.read()
migrated.append((legacy_path, address))
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, 'wb') as flash:
flash.write(contents)
for legacy_path, address in migrated:
print('migrated %s to %s at 0x%08X; retained the old image' %
(legacy_path, path, address))
return path
def overlay_flash_image(flash_path, firmware_path, offset):
'''Update the executable portion of a complete flash image.'''
with open(flash_path, 'r+b') as flash, open(firmware_path, 'rb') as firmware:
flash.seek(offset)
shutil.copyfileobj(firmware, flash)
def prepare_iomcu_flash(path, bootloader, firmware, firmware_size,
force_update):
'''Create or update the persistent 64 KiB IOMCU flash image.'''
new_image = not path.exists()
make_erased(path, IOMCU_FLASH_SIZE)
if bootloader.stat().st_size > IOMCU_APP_OFFSET:
sys.exit('%s does not fit in the IOMCU bootloader region' % bootloader)
if firmware.stat().st_size > firmware_size:
sys.exit('%s is larger than the 0x%X-byte IOMCU application region' %
(firmware, firmware_size))
overlay_flash_image(path, bootloader, 0)
if force_update:
with path.open('r+b') as image:
image.seek(IOMCU_APP_OFFSET)
image.write(b'\xff' * firmware_size)
elif new_image:
overlay_flash_image(path, firmware, IOMCU_APP_OFFSET)
return path
def make_fat_image(path, size, fat_bits=32, sectors_per_cluster=None):
'''Create a persistent FAT image without replacing existing state.'''
try:
return fat_image.create_image(
path, size, fat_bits=fat_bits,
sectors_per_cluster=sectors_per_cluster)
except (OSError, RuntimeError, ValueError) as error:
sys.exit(str(error))
def make_persistence_repl(path, regions):
'''Generate helpers which save mapped-memory regions on pause/exit.'''
lines = [
'// GENERATED by Tools/renode/run.py.',
'// Persistent regions derived from the selected board hwdef.',
'',
]
for name, image, address, size in regions:
lines += [
'%s: Miscellaneous.AP_PersistentMemory @ none' % name,
' fileName: %s' % json.dumps(str(image)),
' address: 0x%08X' % address,
' size: 0x%X' % size,
'',
]
path.write_text('\n'.join(lines))
return path
def has_debug_info(elf):
'''True if the ELF carries DWARF. Without it gdb can only show
addresses, so it is worth failing early rather than after two
windows have opened.'''
readelf = shutil.which('arm-none-eabi-readelf') or shutil.which('readelf')
if readelf is None:
return None
try:
out = subprocess.check_output([readelf, '-S', str(elf)],
stderr=subprocess.DEVNULL).decode()
except (OSError, subprocess.CalledProcessError):
return None
return '.debug_info' in out
def make_runtime_elf(source, destination):
'''Copy an ELF without DWARF for Renode while GDB keeps the original.
Reverse execution serializes Renode's ELF metadata into snapshots. A
debug ArduPilot ELF carries tens of MiB of DWARF, so loading that copy
into Renode makes the first snapshot unnecessarily large and slow.
--strip-debug retains the normal symbol table used by Renode hooks.
'''
objcopy = shutil.which('arm-none-eabi-objcopy') or shutil.which('objcopy')
if objcopy is None:
sys.exit('objcopy is required for a GDB launch')
try:
subprocess.check_call(
[objcopy, '--strip-debug', str(source), str(destination)],
stderr=subprocess.STDOUT,
)
except (OSError, subprocess.CalledProcessError) as error:
sys.exit('failed to make Renode runtime ELF: %s' % error)
return destination
def wrap_binary_in_runtime_elf(source, destination, app_base):
'''Wrap raw application bytes in a minimal ARM ELF for Renode startup.'''
try:
firmware = Path(source).read_bytes()
if len(firmware) < 8:
raise ValueError('firmware is too small to contain reset vectors')
_stack_pointer, reset_vector = struct.unpack_from('<II', firmware)
reset_address = reset_vector & ~1
if not reset_vector & 1:
raise ValueError('reset vector is not a Thumb address')
if not app_base <= reset_address < app_base + len(firmware):
raise ValueError(
'reset vector 0x%08X is outside the application image' %
reset_vector)
data_offset = 0x100
identifier = b'\x7fELF\x01\x01\x01' + b'\x00' * 9
header = struct.pack(
'<16sHHIIIIIHHHHHH',
identifier, 2, 40, 1, reset_vector, 52, 0, 0x05000400,
52, 32, 1, 0, 0, 0)
program_header = struct.pack(
'<IIIIIIII',
1, data_offset, app_base, app_base,
len(firmware), len(firmware), 7, 0x1000)
with Path(destination).open('wb') as output:
output.write(header)
output.write(program_header)
output.write(b'\x00' * (data_offset - output.tell()))
output.write(firmware)
except (OSError, ValueError) as error:
sys.exit('failed to create runtime ELF for %s: %s' %
(source, error))
return Path(destination)
def firmware_format(source):
'''Return the supported firmware format, checking ELF contents.'''
source = Path(source)
try:
with source.open('rb') as source_file:
magic = source_file.read(4)
except OSError as error:
sys.exit('failed to read firmware %s: %s' % (source, error))
if magic == b'\x7fELF':
return 'elf'
suffix = source.suffix.lower()
if suffix in ('.apj', '.bin', '.hex'):
return suffix[1:]
if suffix == '.elf':
sys.exit('%s has an .elf suffix but is not an ELF file' % source)
sys.exit('unsupported firmware format for %s '
'(expected APJ, BIN, HEX, or ELF)' % source)
def _decode_apj(source, maximum_size, expected_board_id):
try:
descriptor = json.loads(source.read_text())
if not isinstance(descriptor, dict):
raise ValueError('APJ descriptor is not a JSON object')
if descriptor.get('magic') != 'APJFWv1':
raise ValueError('invalid APJ magic')
board_id = int(descriptor['board_id'])
if expected_board_id is not None and board_id != expected_board_id:
raise ValueError(
'APJ board ID %u does not match target board ID %u' %
(board_id, expected_board_id))
compressed = base64.b64decode(descriptor['image'], validate=True)
decompressor = zlib.decompressobj()
firmware = decompressor.decompress(compressed, maximum_size + 1)
if len(firmware) > maximum_size or not decompressor.eof:
raise ValueError('expanded APJ image exceeds target flash')
if decompressor.unused_data:
raise ValueError('APJ image has trailing compressed data')
image_size = int(descriptor['image_size'])
if image_size != len(firmware):
raise ValueError(
'APJ image_size is %u but image contains %u bytes' %
(image_size, len(firmware)))
return firmware
except (KeyError, OSError, TypeError, ValueError,
binascii.Error, zlib.error) as error:
sys.exit('failed to decode firmware %s: %s' % (source, error))
def make_firmware_binary(source, destination, app_base=None, flash_size=None,
expected_board_id=None):
'''Convert an APJ, BIN, HEX, or ELF firmware to a flash overlay.'''
source = Path(source)
destination = Path(destination)
image_format = firmware_format(source)
if image_format == 'bin':
try:
shutil.copyfile(source, destination)
except OSError as error:
sys.exit('failed to copy firmware %s: %s' % (source, error))
return destination
if image_format == 'apj':
application_size = 0x08000000 + flash_size - app_base
destination.write_bytes(
_decode_apj(source, application_size, expected_board_id))
return destination
if image_format == 'hex':
try:
from intelhex import IntelHex
from intelhex import IntelHexError
except ImportError:
sys.exit('the intelhex Python module is required for %s' % source)
try:
image = IntelHex(str(source))
if len(image) == 0:
sys.exit('firmware contains no data: %s' % source)
end = image.maxaddr()
if not any(segment_end > app_base
for _segment_start, segment_end in image.segments()):
sys.exit('%s contains no data at application address '
'0x%08X or above' % (source, app_base))
if end >= 0x08000000 + flash_size:
sys.exit('%s ends at 0x%08X, beyond target flash' %
(source, end))
image.padding = 0xff
image.tobinfile(
str(destination), start=app_base, end=end)
except (OSError, IntelHexError) as error:
sys.exit('failed to convert firmware %s: %s' % (source, error))
return destination
# Extract file-backed flash contents, preserving erased ELF gaps.
objcopy = shutil.which('arm-none-eabi-objcopy') or shutil.which('objcopy')
if objcopy is None:
sys.exit('objcopy is required to make the Renode flash overlay')
try:
subprocess.check_call(
[objcopy, '-O', 'binary', '--gap-fill=0xff',
str(source), str(destination)],
stderr=subprocess.STDOUT,
)
except (OSError, subprocess.CalledProcessError) as error:
sys.exit('failed to make Renode flash overlay: %s' % error)
return destination
def make_bootloader_binary(source, destination):
'''Convert an ELF, raw BIN, or Intel HEX bootloader to a flash overlay.'''
try:
with source.open('rb') as source_file:
magic = source_file.read(4)
except OSError as error:
sys.exit('failed to read bootloader %s: %s' % (source, error))
suffix = source.suffix.lower()
if magic == b'\x7fELF' or suffix == '.elf':
return make_firmware_binary(source, destination), 0x08000000
if suffix == '.bin':
try:
shutil.copyfile(source, destination)
except OSError as error:
sys.exit('failed to copy bootloader %s: %s' % (source, error))
return destination, 0x08000000
if suffix in ('.hex', '.ihex'):
try:
from intelhex import IntelHex
from intelhex import IntelHexError
except ImportError:
sys.exit('the intelhex Python module is required for %s' % source)
try:
image = IntelHex(str(source))
if len(image) == 0:
sys.exit('bootloader contains no data: %s' % source)
start = image.minaddr()
image.padding = 0xff
image.tobinfile(str(destination), start=start, end=image.maxaddr())
except (OSError, IntelHexError) as error:
sys.exit('failed to convert bootloader %s: %s' % (source, error))
return destination, start
sys.exit('unsupported bootloader format for %s (expected ELF, BIN, or HEX)' %
source)
def find_terminal():
for term in ('xterm', 'x-terminal-emulator', 'konsole', 'gnome-terminal'):
path = shutil.which(term)
if path:
return path
return None
def find_renode(explicit, here):
if explicit:
return explicit
env = os.environ.get('RENODE')
if env:
return env
# gitignored symlink next to this script: the way to pin a local
# (ideally patched) build without any environment setup
local = here / 'renode'
if local.exists():
return str(local)
path = shutil.which('renode')
if path:
return path
for candidate in ('/opt/renode/renode', '/usr/local/bin/renode'):
if Path(candidate).exists():
return candidate
sys.exit("no renode found: pass --renode, set $RENODE, put renode on\n"
"PATH, or symlink your build here:\n"
" ln -s /path/to/renode %s" % local)
def process_tree(root_pid):
'''Return live process IDs rooted at root_pid from Linux procfs.'''
pending = [root_pid]
processes = set()
while pending:
pid = pending.pop()
if pid in processes:
continue
task_dir = Path('/proc') / str(pid) / 'task'
try:
tasks = list(task_dir.iterdir())
except FileNotFoundError:
continue
processes.add(pid)
for task in tasks:
try:
children = (task / 'children').read_text().split()
except FileNotFoundError:
continue
pending.extend(int(child) for child in children)
return processes
def renode_cpu_threads(root_pid):
'''Find Renode's emulated ArduPilot CPU threads below its launcher.'''
threads = set()
for pid in process_tree(root_pid):
task_dir = Path('/proc') / str(pid) / 'task'
try:
tasks = list(task_dir.iterdir())
except FileNotFoundError:
continue
for task in tasks:
try:
name = (task / 'comm').read_text().strip()
except FileNotFoundError:
continue
# Linux truncates comm to 15 characters. The complete Renode name
# is ardupilot.cpu[0], so match the stable part of that name.
if name.startswith('ardupilot.cpu['):
threads.add(int(task.name))
return threads
def stop_renode_process(process):
if process.poll() is not None:
return
if os.name == 'posix':
os.killpg(process.pid, signal.SIGTERM)
else:
process.terminate()
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
if os.name == 'posix':
os.killpg(process.pid, signal.SIGKILL)
else:
process.kill()
process.wait()
def run_renode(cmd, env, cpusel):
'''Run Renode, optionally pinning only its emulated CPU thread.'''
process = subprocess.Popen(
cmd, env=env, start_new_session=(os.name == 'posix'))
pinned = set()
try:
if cpusel is None:
return process.wait()
while process.poll() is None:
live_threads = renode_cpu_threads(process.pid)
pinned.intersection_update(live_threads)
for thread_id in sorted(live_threads - pinned):
try:
os.sched_setaffinity(thread_id, {cpusel})
except ProcessLookupError:
continue
pinned.add(thread_id)
print('pinned Renode emulation thread %u to host CPU %u' %
(thread_id, cpusel))
time.sleep(0.05 if not pinned else 0.5)
return process.returncode
except BaseException:
stop_renode_process(process)
raise
def main():
root = Path(__file__).resolve().parents[2]
boards = gen_board.supported_boards(root)
parser = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('board', nargs='?',
help='supported ChibiOS board name')
parser.add_argument('--list', action='store_true', help='list available boards')
parser.add_argument('--vehicle',
help='firmware to run (default: AP_Periph for peripheral '
'targets, arducopter otherwise)')
parser.add_argument(
'--elf', '--firmware', dest='firmware',
help='firmware APJ, BIN, HEX, or ELF '
'(default: build/<board>/bin/<vehicle>)')
parser.add_argument('--bootloader',
help='bootloader ELF, BIN, or HEX to execute before the firmware')
parser.add_argument('--hold-bootloader', action='store_true',
help='keep an H7 bootloader running until an upload or '
'reboot command')
parser.add_argument('--renode', help='renode executable to use')
parser.add_argument('--data-cache',
help='downloaded model data cache '
'(default: ~/.cache/ardupilot/renode/data)')
parser.add_argument('--cpusel', type=int, metavar='N',
help='pin only Renode\'s emulated MCU CPU thread to '
'host CPU N')
parser.add_argument('--num-imus', type=int, metavar='N',
help='emulate at most the first N IMUs from the hwdef')
parser.add_argument('--imu', dest='imu_names', action='append', metavar='NAME',
help='emulate the named IMU SPI device from the hwdef '
'(may be repeated)')
parser.add_argument('--state-dir',
help='persistent board state directory '
'(default: renode/<board>)')
parser.add_argument('--serial', type=int,
help='SERIAL_ORDER index exposed on the host socket '
'(default: first hardware UART)')
parser.add_argument('--uart-port', type=int, default=5762,
help='TCP port for the selected UART (default: 5762)')
parser.add_argument('--unix-domain-socket', '--uds', dest='uds',
action='store_true',
help='expose the selected UART as APM-UDS-serialN '
'below the board state directory')
parser.add_argument('--sigrok', action='store_true',
help='stream the MAVLink UART and first SPI bus to sigrok')
parser.add_argument('--sigrok-port', type=int, default=4242,
help='TCP port for sigrok capture (default: 4242)')
parser.add_argument('--sigrok-sample-rate', type=int, default=10000000,
help='sigrok sample rate in Hz (default: 10000000)')
parser.add_argument('--sigrok-channels',
help='comma-separated wildcard patterns selecting '
'sigrok peripherals and pins')
parser.add_argument('--unthrottled', action='store_true',
help='run as fast as possible instead of pacing at real time')
parser.add_argument('--watchdog', action='store_true',
help='enforce the independent watchdog timeout '
'and model the monitor thread fault '
'(disabled by default for GDB usability)')
parser.add_argument('--can', action='store_true',
help='bridge CAN1 and CAN2 to mcast:0 and mcast:1')
parser.add_argument('--can-base', type=int, default=0,
help='first CAN multicast bus number (default: 0)')
parser.add_argument('--device', action='append', default=[], metavar='JSON',
help='attach an emulated device from launch.py')
parser.add_argument('--no-device-sidecars', action='store_true',
help=argparse.SUPPRESS)
parser.add_argument('--ethernet-tap', metavar='INTERFACE',
help='connect the emulated Ethernet MAC to this host TAP')
parser.add_argument('--usb', action='store_true',
help='export firmware USB through a Renode USB/IP server')
parser.add_argument('--dfu', action='store_true',
help='start as an STM32 factory USB DFU device and run '
'Renode after manifestation')
parser.add_argument('--usbip-port', type=int, default=3240,
help='USB/IP server port (default: 3240)')
parser.add_argument('--real-iomcu', action='store_true',
help='run the real IOMCU bootloader and firmware in a second machine')
parser.add_argument('--iomcu-bootloader',
help='IOMCU bootloader (default: Tools/bootloaders/iomcu_bl.bin)')
parser.add_argument('--iomcu-force-update', action='store_true',
help='erase the IOMCU application so the FMU uploads it at startup')
parser.add_argument('--port', type=int,
help='telnet monitor on this port instead of the interactive console')
parser.add_argument('--exec', dest='extra', action='append', default=[],
help='extra monitor command to run after start (repeatable)')
parser.add_argument('--gdb', action='store_true',
help='start a gdb server, halted at reset, and attach '
'gdb in a terminal window')
parser.add_argument('--peripheral-warnings', action='store_true',
help='keep Renode peripheral warnings (slower emulation)')
parser.add_argument('--reverse-debug', action='store_true',
help='with --gdb, enable Renode reverse execution and snapshots')
parser.add_argument('--reverse-gdb-limit', type=int, default=1000,
help='maximum reverse-debug history in guest instructions '
'(default: 1000, 0: unlimited)')
parser.add_argument('--gdb-port', type=int, default=3333)
parser.add_argument('--renode-gdb-port', type=int,
help='internal Renode GDB port (default: --gdb-port + 1)')
parser.add_argument('--gdb-bin', help='gdb to use (default: arm-none-eabi-gdb)')
parser.add_argument('--no-xterm', action='store_true',
help='with --gdb, print the attach command instead of '
'opening a terminal')
args = parser.parse_args()
attachments = []
for value in args.device:
try:
attachment = json.loads(value)
except json.JSONDecodeError as error:
parser.error('invalid --device JSON: %s' % error)
if not isinstance(attachment, dict):
parser.error('--device must contain a JSON object')
attachments.append(attachment)
if args.hold_bootloader and not args.bootloader:
parser.error('--hold-bootloader requires --bootloader')
if args.dfu and not args.usb:
parser.error('--dfu requires --usb')
if args.dfu and args.bootloader:
parser.error('--dfu cannot be combined with --bootloader')
if args.dfu and args.gdb:
parser.error('--dfu cannot be combined with --gdb')
if args.iomcu_bootloader and not args.real_iomcu:
parser.error('--iomcu-bootloader requires --real-iomcu')
if args.iomcu_force_update and not args.real_iomcu:
parser.error('--iomcu-force-update requires --real-iomcu')
if args.cpusel is not None:
if args.cpusel < 0:
parser.error('--cpusel must be a non-negative host CPU number')
if not hasattr(os, 'sched_getaffinity') or not hasattr(os, 'sched_setaffinity'):
parser.error('--cpusel needs host CPU-affinity support')
if not (Path('/proc') / 'self' / 'task').is_dir():
parser.error('--cpusel needs Linux procfs thread information')
allowed = os.sched_getaffinity(0)
if args.cpusel not in allowed:
parser.error('--cpusel %u is not in this process affinity mask (%s)' %
(args.cpusel, ','.join(str(cpu) for cpu in sorted(allowed))))
if args.num_imus is not None and args.num_imus < 0:
parser.error('--num-imus cannot be negative')
if args.reverse_debug and not args.gdb:
parser.error('--reverse-debug requires --gdb')
if args.reverse_gdb_limit < 0:
parser.error('--reverse-gdb-limit cannot be negative')
if args.can_base < 0:
parser.error('--can-base cannot be negative')
if not 1 <= args.uart_port <= 65535:
parser.error('--uart-port must be between 1 and 65535')
if args.port is not None and not 1 <= args.port <= 65535:
parser.error('--port must be between 1 and 65535')
if not 1 <= args.usbip_port <= 65535:
parser.error('--usbip-port must be between 1 and 65535')
if args.usb:
conflicting_ports = [] if args.uds else [args.uart_port]
if args.port is not None:
conflicting_ports.append(args.port)
if args.sigrok:
conflicting_ports.append(args.sigrok_port)
if args.gdb:
conflicting_ports += [
args.gdb_port,
(args.renode_gdb_port if args.renode_gdb_port is not None else
args.gdb_port + 1),
]
if args.usbip_port in conflicting_ports:
parser.error('--usbip-port conflicts with another Renode TCP port')
if args.sigrok_channels is not None and not args.sigrok:
parser.error('--sigrok-channels requires --sigrok')
sigrok_channels = None
if args.sigrok_channels is not None:
sigrok_channels = [pattern.strip()
for pattern in args.sigrok_channels.split(',')]
if any(not pattern for pattern in sigrok_channels):
parser.error('--sigrok-channels contains an empty pattern')
if args.sigrok:
if not 1 <= args.sigrok_port <= 65535:
parser.error('--sigrok-port must be between 1 and 65535')
if not 1 <= args.sigrok_sample_rate <= 1000000000:
parser.error('--sigrok-sample-rate must be between 1 and 1000000000')
conflicting_ports = [] if args.uds else [args.uart_port]
if args.port is not None:
conflicting_ports.append(args.port)
if args.gdb:
conflicting_ports += [
args.gdb_port,
(args.renode_gdb_port if args.renode_gdb_port is not None else
args.gdb_port + 1),
]
if args.usb:
conflicting_ports.append(args.usbip_port)
if args.sigrok_port in conflicting_ports:
parser.error('--sigrok-port conflicts with another Renode TCP port')
if args.list or not args.board:
print('\n'.join(boards))
return 0 if args.list else 1
if args.board not in boards:
sys.exit("unknown board '%s' (have: %s)" % (args.board, ', '.join(boards)))
if args.vehicle is None:
args.vehicle = ('AP_Periph' if gen_board.is_periph_board(root, args.board)
else 'arducopter')
build_target = {
'arducopter': 'copter',
'arduplane': 'plane',
'ardurover': 'rover',
'ardusub': 'sub',
'antennatracker': 'antennatracker',
}.get(args.vehicle, args.vehicle)
firmware = (Path(args.firmware).expanduser() if args.firmware else
root / 'build' / args.board / 'bin' / args.vehicle)
if not firmware.exists():
sys.exit("no firmware at %s - build it first:\n"
" ./waf configure --board %s && ./waf %s" %
(firmware, args.board, build_target))
image_format = firmware_format(firmware)
if args.gdb and image_format != 'elf':
parser.error('--gdb requires ELF firmware')
bootloader = Path(args.bootloader).expanduser() if args.bootloader else None
if bootloader is not None and not bootloader.is_file():
sys.exit('no bootloader at %s' % bootloader)
state_dir = (Path(args.state_dir).expanduser() if args.state_dir else
root / 'renode' / args.board).resolve()
state_dir.mkdir(parents=True, exist_ok=True)
# Generate the board overlay from the production hwdef compiler outputs on
# every invocation. This is under one second on the supported families and
# ensures hwdef.dat/hwdef-bl.dat, not checked-in Renode board copies, are
# authoritative for pins, buses, DMA, flash layout, and devices.
outdir = state_dir
serial_index = args.serial
try:
generated = gen_board.generate(
root, args.board, outdir / 'generated', serial_index, args.uart_port,
state_dir, quiet_peripherals=not (args.reverse_debug or args.peripheral_warnings),
sigrok=args.sigrok, sigrok_channels=sigrok_channels,
num_imus=args.num_imus, imu_names=args.imu_names,
real_iomcu=args.real_iomcu,
attachments=attachments, uds=args.uds)
except (OSError, ValueError) as error:
sys.exit('failed to generate Renode board: %s' % error)
if args.hold_bootloader and generated['family'] not in ('h743', 'h757'):
parser.error('--hold-bootloader is only supported on STM32H7 boards')
unix_socket_lock = None
if args.uds:
if generated['uart_socket'] is None:
parser.error('--uds requires a selected hardware UART')
try:
unix_socket_lock = reserve_unix_socket(generated['uart_socket'])
except (OSError, ValueError) as error:
parser.error(str(error))
for warning in generated['warnings']:
print('warning: %s' % warning, file=sys.stderr)
data_cache = renode_data.cache_path(args.data_cache)
try:
data_files = renode_data.ensure_mcu_data(
generated['mcu_type'], data_cache)
except (OSError, RuntimeError, ValueError) as error:
sys.exit('failed to obtain Renode model data: %s' % error)
mcu_id = make_mcu_id(state_dir / 'mcu_id.txt')
mcu_id_bin = outdir / 'current-mcu-id.bin'
mcu_id_bin.write_bytes(mcu_id)
enable_can = (args.can or generated['is_periph'] or any(
attachment['port']['bus'] == 'can'
for attachment in generated['attachments']))
if enable_can and not generated['can_buses']:
sys.exit('%s has no generated CAN peripherals' % args.board)
if enable_can and args.can_base + len(generated['can_buses']) > 10:
sys.exit('%s needs CAN multicast buses %u..%u; maximum bus is 9' %
(args.board, args.can_base,
args.can_base + len(generated['can_buses']) - 1))
if args.ethernet_tap and not generated['has_ethernet']:
sys.exit('%s has no generated Ethernet peripheral' % args.board)
if args.usb and generated['family'] not in (
'f405', 'f407', 'f427', 'h743', 'h757'):
sys.exit('%s does not have a supported Renode USB controller' %
args.board)
if args.real_iomcu:
if generated['family'] not in ('h743', 'h757'):
sys.exit('--real-iomcu currently requires an STM32H7 flight controller')
if generated['iomcu_uart'] is None or generated['iomcu_firmware'] is None:
sys.exit('%s does not provide an IOMCU UART and embedded firmware' %
args.board)
# Persist the complete physical internal flash. Its size does not move as
# linker regions change between builds, unlike the old crashlog.img. The
# current firmware's file-backed flash bytes are normally overlaid
# afterwards so code represents the selected build without zeroing
# persistent ELF NOBITS regions such as .crash_log. In DFU mode the image
# is instead changed exclusively by the host-side DFU client.
flash_size_kb = hwdef_value(generated['hwdef_h'], 'BOARD_FLASH_SIZE')
if flash_size_kb is None or flash_size_kb <= 0:
sys.exit('%s has no valid BOARD_FLASH_SIZE in %s' %
(args.board, generated['hwdef_h']))
flash_size = flash_size_kb * 1024
legacy_regions = []
storage = storage_region(generated['hwdef_h'])
if storage and not generated['has_fram']:
base, size = storage
legacy_regions.append((state_dir / 'storage.img', base, size))
legacy_crashlog = state_dir / 'crashlog.img'
if legacy_crashlog.exists():
legacy_regions.append(
(legacy_crashlog,
0x08000000 + flash_size - legacy_crashlog.stat().st_size,
None))
firmware_offset = generated['app_base'] - 0x08000000
firmware_bin = make_firmware_binary(
firmware, outdir / 'current-firmware.bin', generated['app_base'],
flash_size, hwdef_value(generated['hwdef_h'], 'APJ_BOARD_ID'))
if firmware_offset < 0 or firmware_offset + firmware_bin.stat().st_size > flash_size:
sys.exit('%s does not fit in the %u-byte internal flash' %
(firmware, flash_size))
flash_img = make_flash_image(
state_dir / 'flash.img', flash_size, legacy_regions)
if not args.dfu:
overlay_flash_image(flash_img, firmware_bin, firmware_offset)
bootloader_address = None
if bootloader is not None:
bootloader_bin, bootloader_address = make_bootloader_binary(
bootloader, outdir / 'current-bootloader.bin')
bootloader_size = bootloader_bin.stat().st_size
if bootloader_size == 0:
sys.exit('bootloader contains no data: %s' % bootloader)
bootloader_offset = bootloader_address - 0x08000000
bootloader_end = bootloader_address + bootloader_size
if (bootloader_offset < 0 or bootloader_end > generated['app_base'] or
bootloader_end > 0x08000000 + flash_size):
sys.exit('%s occupies 0x%08X..0x%08X and does not fit below the '
'application at 0x%08X' %
(bootloader, bootloader_address, bootloader_end - 1,
generated['app_base']))
overlay_flash_image(flash_img, bootloader_bin, bootloader_offset)
initial_loads = [
'sysbus LoadBinary @%s 0x08000000' % flash_img,
'sysbus LoadBinary @%s 0x%08X' % (mcu_id_bin, generated['uid_address']),
]
persistent_regions = [
('persistentFlash', flash_img, 0x08000000, flash_size),
]
if generated['has_fram']:
make_erased(state_dir / 'fram.img', generated['fram_size'])
iomcu_flash = None
iomcu_persistence_repl = None
if args.real_iomcu:
iomcu_bootloader_source = (
Path(args.iomcu_bootloader).expanduser()
if args.iomcu_bootloader else
root / 'Tools' / 'bootloaders' / 'iomcu_bl.bin')
if not iomcu_bootloader_source.is_file():
sys.exit('no IOMCU bootloader at %s' % iomcu_bootloader_source)
iomcu_bootloader, iomcu_bootloader_address = make_bootloader_binary(
iomcu_bootloader_source, outdir / 'current-iomcu-bootloader.bin')
if iomcu_bootloader_address != IOMCU_FLASH_BASE:
sys.exit('%s must load at 0x%08X' %
(iomcu_bootloader_source, IOMCU_FLASH_BASE))
iomcu_firmware_size = hwdef_value(
generated['hwdef_h'], 'AP_IOMCU_FW_FLASH_SIZE') or IOMCU_FIRMWARE_SIZE
if iomcu_firmware_size > IOMCU_FIRMWARE_SIZE:
sys.exit('IOMCU firmware region 0x%X exceeds the emulated F100 flash' %
iomcu_firmware_size)
iomcu_flash = prepare_iomcu_flash(
state_dir / 'iomcu-flash.img', iomcu_bootloader,
generated['iomcu_firmware'], iomcu_firmware_size,
args.iomcu_force_update)
iomcu_persistence_repl = make_persistence_repl(
outdir / 'generated' / 'iomcu-persistent.repl', [
('persistentIomcuFlash', iomcu_flash,
IOMCU_FLASH_BASE, IOMCU_FLASH_SIZE),
])
pre_vars = []
if generated['has_sdcard']:
if args.reverse_debug:
# Renode serializes file-backed storage into every reverse snapshot.
# Keep the normal 512 MiB persistent card out of that path: a small
# FAT16 scratch filesystem is ample for a debugging session and
# reduces each CubeBlack snapshot by roughly 496 MiB.
img = make_fat_image(
outdir / 'reverse-sdcard-16m.img', 16 * 1024 * 1024,
fat_bits=16)
else:
img = make_fat_image(
state_dir / 'sdcard.img', 512 * 1024 * 1024,
sectors_per_cluster=8)
pre_vars.append('$sdcard=@%s' % img)
persistence_repl = None
if persistent_regions:
persistence_repl = make_persistence_repl(
outdir / 'generated' / 'persistent.repl', persistent_regions)
renode = find_renode(args.renode, root / 'Tools' / 'renode')
runtime_elf = (
firmware if image_format == 'elf' else
wrap_binary_in_runtime_elf(
firmware_bin, outdir / 'firmware-runtime.elf',
generated['app_base']))
if args.reverse_debug:
runtime_elf = make_runtime_elf(
firmware, outdir / 'gdb-runtime.elf')
monitor = ['-P', str(args.port)] if args.port else ['--console']
vector_base = (
0x08000000 if args.dfu else
(bootloader_address if bootloader_address is not None else
generated['app_base']))
commands = ['$repo=@%s' % root,
'$vector_base=0x%08X' % vector_base]
commands.insert(1, '$elf=@%s' % runtime_elf)
if data_files:
commands.append('$renode_data=@%s' % data_cache.resolve())
if 'svd' in data_files:
commands.append('$mcu_svd=@%s' % data_files['svd'].resolve())
commands += pre_vars + [
'include @%s' % generated['resc']] + initial_loads
if args.hold_bootloader:
# Match an application-requested reboot into the bootloader.
commands.append('sysbus WriteDoubleWord 0x58004050 0xB0070001')
if args.sigrok:
commands += [
'sysbus.sigrok SampleRate %u' % args.sigrok_sample_rate,
'sysbus.sigrok Port %u' % args.sigrok_port,
]
if enable_can:
for index, bus in enumerate(generated['can_buses']):
commands.append('sysbus.%sMcast Bus %u' %
(bus.lower(), args.can_base + index))
if args.ethernet_tap:
commands += [
'emulation CreateSwitch "ethernetSwitch"',
'connector Connect sysbus.ethernet ethernetSwitch',
'emulation CreateTap %s "ethernetTap" false' %
json.dumps(args.ethernet_tap),
'connector Connect host.ethernetTap ethernetSwitch',
]
if args.usb:
commands += [
'emulation CreateUSBIPServer %u "usb"' % args.usbip_port,
'host.usb Register sysbus.usbOtg',
]
if persistence_repl is not None:
commands.append('machine LoadPlatformDescription @%s' % persistence_repl)
if args.real_iomcu:
commands += [
'include @%s' % (root / 'Tools' / 'renode' / 'peripherals' /
'stm32' / 'AP_STM32F1F3_RCC.cs'),
'include @%s' % (root / 'Tools' / 'renode' / 'peripherals' /
'stm32' / 'AP_STM32F1_UART.cs'),
'include @%s' % (root / 'Tools' / 'renode' / 'peripherals' /
'stm32' / 'AP_STM32F1_FlashController.cs'),
'include @%s' % (root / 'Tools' / 'renode' / 'peripherals' /
'stm32' / 'AP_STM32G4_DMA.cs'),
'include @%s' % (root / 'Tools' / 'renode' / 'peripherals' /
'stm32' / 'AP_STM32F4_I2C.cs'),
'mach create "iomcu"',
'machine LoadPlatformDescription @%s' %
(root / 'Tools' / 'renode' / 'platforms' / 'stm32f103_base.repl'),
'flash ResetByte 0xFF',
'rcc HseFrequency 24000000',
'machine LoadPlatformDescriptionFromString '
'"usart2: { DMARequest -> dma1@6 }"',
'machine LoadPlatformDescriptionFromString '
'"iomcuUart: Miscellaneous.AP_UARTPacer @ sysbus 0x60000004 '
'{ uart: usart2 }"',
'sysbus LoadBinary @%s 0x%08X' %
(iomcu_flash, IOMCU_FLASH_BASE),
'gpioPortB OnGPIO 5 false',
'gpioPortC OnGPIO 14 true',
'gpioPortC OnGPIO 15 false',
'cpu VectorTableOffset 0x%08X' % IOMCU_FLASH_BASE,
'cpu PerformanceInMips 60',
'connector Connect sysbus.iomcuUart iomcuHub',
'machine LoadPlatformDescription @%s' % iomcu_persistence_repl,
'emulation SetGlobalSerialExecution true',
'emulation SetGlobalQuantum "%s"' % IOMCU_GLOBAL_QUANTUM,
'mach set "ardupilot"',
]
if args.unthrottled:
commands.append('emulation SetGlobalAdvanceImmediately true')
post_start_commands = []
if args.watchdog:
# Scheduler::_monitor_thread deliberately branches to 0xE000FFFF to
# obtain a crashdump before the IWDG resets the board. Real Cortex-M
# parts fault when fetching there, but Renode does not execute hooks
# from the unmapped PPB hole. Map the page with an undefined Thumb
# instruction, then pend HardFault when the aligned PC reaches it,
# retaining the real monitor-thread PC and stack.
commands += [
'machine LoadPlatformDescriptionFromString '
'"watchdogFault: Memory.MappedMemory @ sysbus 0xE000F000 '
'{ size: 0x1000 }"',
'sysbus WriteWord 0xE000FFFE 0xDE00',
'watchdog EnforceReset true',
]
# Install this after start: early firmware resets clear CPU hooks.
post_start_commands.append(
'cpu AddHook 0xE000FFFE '
'"monitor.Parse(\'sysbus.nvic SetPendingIRQ 3\')"'
)
gdb_proc = None
gdb_proxy_proc = None
if args.gdb:
dbg = has_debug_info(firmware)
if dbg is False:
sys.exit('%s has no .debug_info; gdb would only show addresses.\n'
'Rebuild with:\n'
' ./waf configure --board %s -g && ./waf %s'
% (firmware, args.board, build_target))
if dbg is None:
print('no readelf found, so not checking the ELF for debug info')
gdb = args.gdb_bin or 'arm-none-eabi-gdb'
outdir = root / 'build' / args.board / 'renode'
outdir.mkdir(parents=True, exist_ok=True)
launcher = outdir / 'gdb.sh'
launcher.write_text(GDB_LAUNCH.replace('$PORT', str(args.gdb_port))
.replace('$GDB', gdb)
.replace('$ELF', str(firmware))
.replace('$READY', str(outdir / 'gdb-proxy.ready')))
launcher.chmod(0o755)
renode_gdb_port = (args.renode_gdb_port if args.renode_gdb_port is not None
else args.gdb_port + 1)
if not 1 <= args.gdb_port <= 65535:
sys.exit('--gdb-port must be between 1 and 65535')
if not 1 <= renode_gdb_port <= 65535:
sys.exit('--renode-gdb-port must be between 1 and 65535')
if renode_gdb_port == args.gdb_port:
sys.exit('--gdb-port and --renode-gdb-port must differ')
# The proxy turns ChibiOS registry entries into GDB threads and passes
# all other packets through to Renode. Renode starts the machine when
# the proxy establishes the upstream debugger connection.
if args.reverse_debug:
# Per-peripheral log levels trigger a Renode 1.16.1 logger bug
# when restoring snapshots. The generated script omits those
# overrides for GDB, so use one global level to keep output useful.
commands.append('logLevel 3')
reverse_command = 'reverseExecMode true'
if args.reverse_gdb_limit:
reverse_command += ' %u' % args.reverse_gdb_limit
commands.append(reverse_command)
commands.append('machine StartGdbServer %d' % renode_gdb_port)
proxy_ready = outdir / 'gdb-proxy.ready'
try:
proxy_ready.unlink()
except FileNotFoundError:
pass
gdb_proxy_proc = subprocess.Popen([
sys.executable, str(root / 'Tools' / 'renode' / 'chibios_gdb.py'),
'--listen', str(args.gdb_port),
'--upstream', str(renode_gdb_port),
'--elf', str(firmware),
'--ready-file', str(proxy_ready),
])
proxy_deadline = time.monotonic() + 10
while not proxy_ready.exists():
returncode = gdb_proxy_proc.poll()
if returncode is not None:
if returncode != 0:
sys.exit(returncode)
sys.exit('ChibiOS GDB proxy stopped before becoming ready')
if time.monotonic() >= proxy_deadline:
gdb_proxy_proc.terminate()
sys.exit('timed out starting the ChibiOS GDB proxy')
time.sleep(0.01)
term = None if args.no_xterm else find_terminal()
if term is None:
print('run this in another window to attach:\n %s' % launcher)
else:
gdb_proc = subprocess.Popen(
[term, '-title', 'gdb %s' % args.board, '-e', str(launcher)])
print('gdb attaching in %s; optimized builds inline heavily, so '
'build with -g for symbols' % os.path.basename(term))
features = 'ChibiOS thread debugging'
if args.reverse_debug:
features += ' and reverse debugging (%s history)' % (
'unlimited' if args.reverse_gdb_limit == 0 else
'%u instructions' % args.reverse_gdb_limit
)
print('%s enabled; execution starts under GDB control' % features)
else:
commands.append('start')
commands += post_start_commands
commands += args.extra
# a source-built renode launcher needs dotnet; the standard
# user-local install location works without any global setup
env = dict(os.environ)
dotnet = Path.home() / '.dotnet'
if not shutil.which('dotnet') and (dotnet / 'dotnet').exists():
env['PATH'] = '%s:%s' % (dotnet, env.get('PATH', ''))
print('firmware: %s (%s)' % (firmware, image_format.upper()))
if bootloader is not None:
print('bootloader: %s at 0x%08X' % (bootloader, bootloader_address))
print('renode: %s' % renode)
print('state: %s' % state_dir)
if generated['serial'] is None:
print('serial: no hardware UART in SERIAL_ORDER')
elif generated['uart_socket'] is not None:
print('serial: SERIAL%s/%s on uds:%s' %
(generated['serial_index'], generated['serial'],
generated['uart_socket']))
else:
print('serial: SERIAL%s/%s on tcp:localhost:%u' %
(generated['serial_index'], generated['serial'], args.uart_port))
print('time: %s' % ('unthrottled' if args.unthrottled else 'paced at 1x'))
if args.watchdog:
print('watchdog: reset enforced')
if args.cpusel is not None:
print('CPU: emulation thread on host CPU %u' % args.cpusel)
if args.num_imus is not None:
print('IMUs: %u emulated (limit %u)' %
(generated['num_imus'], args.num_imus))
elif args.imu_names is not None:
print('IMUs: %u emulated (%s)' %
(generated['num_imus'], ', '.join(args.imu_names)))
if enable_can:
print('CAN: %s' % ', '.join(
'%s=mcast:%u' % (bus, args.can_base + index)
for index, bus in enumerate(generated['can_buses'])))
if generated['gps_uart'] is not None:
print('GPS: simulated u-blox on %s' % generated['gps_uart'])
if args.ethernet_tap:
print('ethernet: host TAP %s' % args.ethernet_tap)
if args.sigrok:
print('sigrok: %u Hz on tcp:localhost:%u' %
(args.sigrok_sample_rate, args.sigrok_port))
print('signals: %s' % ', '.join(generated['sigrok_signals']))
print('PulseView device: renode-la:conn=tcp/127.0.0.1/%u' %
args.sigrok_port)
if args.usb:
print('USB/IP: tcp:localhost:%u' % args.usbip_port)
print('attach: Tools/renode/usbip_attach.py --port %u' %
args.usbip_port)
if args.real_iomcu:
print('IOMCU: real F100 bootloader/firmware (%s)' % iomcu_flash)
if args.iomcu_force_update:
print('IOMCU: application erased; FMU update requested')
for attachment in generated['attachments']:
print('device: %s on %s' % (
attachment['device']['name'], attachment['port']['name']))
cmd = [renode, '--disable-xwt'] + monitor + ['-e', '; '.join(commands)]
if args.dfu:
try:
dfu_device = dfu.DfuDevice(
flash_img, generated['family'], flash_size,
port=args.usbip_port,
log=lambda message: print('[DFU] %s' % message, flush=True))
except (OSError, ValueError) as error:
sys.exit('failed to start USB DFU: %s' % error)
print('USB DFU: 0483:df11 waiting for download on tcp:localhost:%u' %
args.usbip_port, flush=True)
try:
dfu_device.wait_for_manifest()
except KeyboardInterrupt:
return 130
finally:
dfu_device.close()
print('USB DFU: download complete; starting programmed firmware',
flush=True)
sidecars = []
try:
for attachment in generated['attachments']:
sidecar = attachment['device'].get('sidecar')
if sidecar is None or args.no_device_sidecars:
continue
can_bus = args.can_base + attachment['port']['index']
node_id = 120 - len(sidecars)
if node_id < 1:
sys.exit('too many host-side emulated CAN devices')
sidecars.append(subprocess.Popen([
sys.executable, '-u',
str(root / 'Tools' / 'renode' / 'device_emulator.py'),
sidecar, '--can-bus', str(can_bus),
'--node-id', str(node_id),
]))
try:
ret = run_renode(cmd, env, args.cpusel)
except KeyboardInterrupt:
ret = 130
except OSError as error:
sys.exit('failed to set Renode emulation thread affinity: %s' % error)
finally:
for sidecar in sidecars:
sidecar.terminate()
for sidecar in sidecars:
try:
sidecar.wait(timeout=3)
except subprocess.TimeoutExpired:
sidecar.kill()
if gdb_proc is not None:
gdb_proc.terminate()
if gdb_proxy_proc is not None:
gdb_proxy_proc.terminate()
try:
(outdir / 'gdb-proxy.ready').unlink()
except FileNotFoundError:
pass
cleanup_unix_socket(generated['uart_socket'])
if unix_socket_lock is not None:
os.close(unix_socket_lock)
return ret
if __name__ == '__main__':
sys.exit(main())