- 将网页输入和外设操作的仿真调度移到状态锁外,覆盖复位、退订和断线释放路径 - 通过真实 GPIO 边沿缓存送丝使能电平,移除快照及激光和波形复位中的反向 GPIO 读取 - 为每个 Renode 会话复制用户配置并设置独立的 Monitor 历史文件,保留原配置及其他设置 - 添加时钟锁和 GPIO 锁回归探针、会话配置与双实例隔离用例,并补充运行说明
157 lines
9.6 KiB
Python
157 lines
9.6 KiB
Python
"""生成隔离的启动会话,协调 Renode、持久化镜像与可选虚拟串口的生命周期"""
|
|
from contextlib import ExitStack, contextmanager
|
|
import ctypes
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
|
|
from scripts.environment import ROOT, RUNTIME, configured_path, free_port, listening, require_runtime, selected_elf, setting
|
|
from scripts.flash_images import flash_image
|
|
from scripts.renode_process import create_session_config, exit_previous, stop_process, wait_for_monitor
|
|
from scripts.settings import as_port
|
|
from scripts.external_devices import ExternalDevices
|
|
|
|
|
|
@contextmanager
|
|
def simulation(*, debug=False, flash=None, internal_flash=None, uart_port=None, gdb_port=None, ws_port=None, fast_serial=False, devices=None):
|
|
"""创建并监督一个 Renode 子进程,在退出上下文前等待其正常退出
|
|
|
|
每次启动隔离临时缓存,避免 Renode 在 Windows 上误清理其他会话的缓存
|
|
本层不取得镜像锁,直接调用方需自行保证传入镜像的独占使用
|
|
"""
|
|
devices = devices or ExternalDevices()
|
|
renode = require_runtime()
|
|
# 共享终端使用独立组;无终端时创建隐藏 console,其首进程同样可按 PID 定向通知
|
|
process_options = {'creationflags': subprocess.CREATE_NEW_PROCESS_GROUP}
|
|
if not ctypes.WinDLL('kernel32', use_last_error=True).GetConsoleProcessList((ctypes.c_ulong * 1)(), 1):
|
|
startup = subprocess.STARTUPINFO()
|
|
startup.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
|
startup.wShowWindow = subprocess.SW_HIDE
|
|
process_options.update(creationflags=subprocess.CREATE_NEW_CONSOLE | subprocess.CREATE_NEW_PROCESS_GROUP,
|
|
startupinfo=startup, stdin=subprocess.DEVNULL, stdout=sys.stdout, stderr=sys.stderr)
|
|
uart, gdb = (
|
|
uart_port or as_port(setting('H723_UART_PORT', '12321')),
|
|
gdb_port or as_port(setting('H723_GDB_PORT', '3333')))
|
|
ws = ws_port or as_port(setting('H723_WS_PORT', '12322'))
|
|
if len({uart, gdb, ws}) != 3:
|
|
raise ValueError('UART, GDB and WebSocket ports must be different')
|
|
monitor_port = free_port()
|
|
while monitor_port in (uart, gdb, ws):
|
|
monitor_port = free_port()
|
|
occupied = [port for port in (uart, gdb, ws) if listening(port)]
|
|
if occupied:
|
|
raise RuntimeError(f'Ports {occupied} already in use. Quit the existing H723 instance first')
|
|
temp_root = RUNTIME / 'tmp'
|
|
temp_root.mkdir(parents=True, exist_ok=True)
|
|
with tempfile.TemporaryDirectory(prefix='run-', dir=temp_root) as temporary:
|
|
session = Path(temporary)
|
|
script = session / 'launch.resc'
|
|
root_script_token = (ROOT / 'platforms/motion_controller.resc').as_posix().replace(' ', '\\ ')
|
|
elf_token = selected_elf().as_posix().replace(' ', '\\ ')
|
|
# 先由平台加载 ELF,再覆盖持久化的内部 Flash,以恢复升级程序与启动标记
|
|
script.write_text(f'$uartPort={uart}\n$gdbPort={gdb}\n$wsPort={ws}\n$elfPath=@{elf_token}\n'
|
|
f'include @{root_script_token}\n'
|
|
+ (f'sysbus.flashController OpenPersistentImage "{internal_flash.as_posix()}"\n' if internal_flash else '')
|
|
+ 'sysbus.cpu VectorTableOffset 0x08000000\n'
|
|
+ (f'sysbus LoadBinary @{flash.as_posix().replace(" ", chr(92) + " ")} 0x90000000\n' if flash else '')
|
|
+ ('sysbus.boardSerial FastMode true\n' if fast_serial else '')
|
|
+ devices.monitor_commands()
|
|
+ ('' if debug else 'start\n')
|
|
+ f'$h723LaunchCompleted="{session.name}"\n', encoding='utf-8')
|
|
environment = os.environ.copy()
|
|
environment.pop('H723_FLASH_IMAGE', None)
|
|
if flash:
|
|
environment['H723_FLASH_IMAGE'] = str(flash)
|
|
environment['TEMP'] = environment['TMP'] = str(session)
|
|
# 位置参数不会正确转义脚本路径中的空格,改用 Monitor 的 PathToken 语法
|
|
script_token = script.as_posix().replace(' ', '\\ ')
|
|
argv = [str(renode), '--plain', '--disable-gui', '--hide-analyzers', '--config',
|
|
str(create_session_config(session)), '--port',
|
|
str(monitor_port), '-e', f'include @{script_token}']
|
|
proc = subprocess.Popen(argv, cwd=ROOT, env=environment, **process_options)
|
|
monitor = None
|
|
try:
|
|
monitor = wait_for_monitor(proc, monitor_port)
|
|
# ELF 加载或后续命令失败时仍能读取 PC,必须确认整个启动脚本已执行完成
|
|
# 成功标记只在脚本末尾赋值,并与本次会话绑定
|
|
try:
|
|
completed = monitor.command('echo $h723LaunchCompleted')
|
|
except RuntimeError as error:
|
|
raise RuntimeError(f'Renode startup did not complete:\n{monitor.initial_output}') from error
|
|
if not re.search(r'(?:^|[\r\n])' + re.escape(session.name) + r'[\r\n]', completed):
|
|
raise RuntimeError('Renode startup completion marker does not match this session')
|
|
websocket = monitor.command('sysbus.boardWebSocket Port')
|
|
if not re.search(r'(?im)^\s*0x0*' + format(ws, 'x') + r'\s*$', websocket):
|
|
raise RuntimeError('Board WebSocket did not start on the configured port')
|
|
yield proc, monitor, uart, gdb
|
|
finally:
|
|
stop_process(proc, monitor, control_break=True)
|
|
|
|
|
|
def run(debug=False, usb=False, flash=None, force=False, com='COM12', uart_port=None, gdb_port=None, ws_port=None, fast_serial=False, devices=None):
|
|
"""取得两类 Flash 的独占锁后运行实例,直到子进程与桥接清理完成才释放锁"""
|
|
if force:
|
|
exit_previous()
|
|
external = (ROOT / flash).resolve() if flash else configured_path('H723_FLASH_IMAGE', '.runtime/w25q16.bin')
|
|
internal = configured_path('H723_INTERNAL_FLASH_IMAGE', '.runtime/stm32-internal.bin')
|
|
if internal == external:
|
|
raise ValueError('Internal and external Flash images must use different paths')
|
|
with flash_image(internal, initialize=False), flash_image(external) as image:
|
|
print(f'Persistent external Flash: {image}', flush=True)
|
|
print(f'Persistent internal Flash: {internal}', flush=True)
|
|
return run_instance(debug, usb, image, com, uart_port, gdb_port, ws_port, fast_serial, internal, devices)
|
|
|
|
|
|
def run_instance(debug, usb, flash, com, uart_port=None, gdb_port=None, ws_port=None, fast_serial=False, internal_flash=None, devices=None):
|
|
"""启动传输桥接并发布实例记录,监督任一子进程异常退出并按顺序清理"""
|
|
from scripts.virtual_com import virtual_com
|
|
# 状态清理栈放在外层,确保退出记录比串口桥接和 Renode 上下文更晚销毁
|
|
with ExitStack() as state_cleanup, ExitStack() as stack:
|
|
state_path = RUNTIME / 'run.json'
|
|
proc, monitor, uart, gdb = stack.enter_context(simulation(
|
|
debug=debug, flash=flash, internal_flash=internal_flash, uart_port=free_port() if usb else uart_port,
|
|
gdb_port=gdb_port, ws_port=ws_port, fast_serial=fast_serial, devices=devices))
|
|
bridge = stack.enter_context(virtual_com(uart, com, RUNTIME, fast=fast_serial)) if usb else None
|
|
com = com.upper() if usb else None
|
|
state_path.write_text(json.dumps({'pid': proc.pid, 'debug': debug, 'usb': usb, 'com_port': com, 'flash': str(flash),
|
|
'monitor_port': monitor.sock.getpeername()[1], 'uart_port': uart, 'gdb_port': gdb,
|
|
'ws_port': ws_port or as_port(setting('H723_WS_PORT', '12322')), 'fast_serial': fast_serial,
|
|
'internal_flash': str(internal_flash), 'control_break': True}), encoding='utf-8')
|
|
# 停止标记最后移除,让 just stop 等待串口桥接和仿真实例全部完成清理
|
|
state_cleanup.callback(state_path.unlink, missing_ok=True)
|
|
print(f'UART4 RTU: 127.0.0.1:{uart}; GDB: 127.0.0.1:{gdb}', flush=True)
|
|
print(f'USB/virtual COM mode: select {com} in your application (115200, 8N1, slave 8).'
|
|
if usb else f'Network mode: set comStr to tcp://127.0.0.1:{uart}. Disconnect any virtual COM bridge from this endpoint.', flush=True)
|
|
print(f'Renode monitor: 127.0.0.1:{monitor.sock.getpeername()[1]}', flush=True)
|
|
print(f'Board WebSocket: ws://127.0.0.1:{ws_port or setting("H723_WS_PORT", "12322")}/board', flush=True)
|
|
if fast_serial:
|
|
print('Fast serial simulation enabled; physical serial timing is accelerated.', flush=True)
|
|
print('CPU paused; connect GDB and continue. Press Ctrl+C to stop.' if debug
|
|
else 'Firmware running. Press Ctrl+C to stop.', flush=True)
|
|
# Monitor 按连接顺序服务,启动检查后释放连接,允许操作者接入调试
|
|
monitor.sock.close()
|
|
interrupted = False
|
|
try:
|
|
try:
|
|
while proc.poll() is None:
|
|
if bridge is not None and bridge.poll() is not None:
|
|
try:
|
|
proc.wait(timeout=1)
|
|
except subprocess.TimeoutExpired as error:
|
|
raise RuntimeError(f'Virtual COM bridge exited; see {RUNTIME / "logs/usb.log"}') from error
|
|
time.sleep(.2)
|
|
except KeyboardInterrupt:
|
|
interrupted = True
|
|
print('\nStopping this Renode instance...', flush=True)
|
|
finally:
|
|
if proc.poll() is None:
|
|
stop_process(proc, control_break=True)
|
|
if not interrupted and proc.returncode not in (0, None):
|
|
raise RuntimeError(f'Renode exited with code {proc.returncode}')
|
|
return 0
|