mirror of
https://github.com/ArduPilot/ardupilot.git
synced 2026-10-06 19:00:27 +08:00
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.
1385 lines
59 KiB
Python
Executable File
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())
|