Skip to content

Commit 5cd20c5

Browse files
committed
[util] Improve the verilator runner to capture traces
By default the runner will run verilator with the provided ELFs files and display the UART log. If the flag --trace-trigger=STRING, the script will start the traces when the string is detected on the UART. Signed-off-by: Douglas Reis <doreis@lowrisc.org>
1 parent 3f188cd commit 5cd20c5

2 files changed

Lines changed: 364 additions & 0 deletions

File tree

util/verilator_runner.py

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Run a verilator simulation, streaming verilator log and UART output.
4+
Optionally toggle tracing (SIGUSR1) when a trigger string appears on the UART.
5+
6+
The UART device path is extracted from the verilator log (a line in the
7+
format "$ screen /dev/pts/N").
8+
9+
Usage:
10+
verilator_trace_trigger.py [--trace-trigger=STRING] [--verilator-bin=PATH] <elf> [<elf> ...]
11+
12+
Arguments:
13+
elfs One or more ELF files to pass to verilator with -E
14+
15+
Options:
16+
--verilator-bin PATH Path to the verilator simulation binary
17+
(default: build/lowrisc_mocha_top_chip_verilator_0/
18+
sim-verilator/Vtop_chip_verilator)
19+
--trace-trigger STR Watch the UART for STR and toggle tracing (SIGUSR1) when found
20+
"""
21+
22+
import argparse
23+
import asyncio
24+
import re
25+
import sys
26+
from pathlib import Path
27+
from typing import Optional
28+
29+
import serial
30+
31+
_CYAN = "\033[36m"
32+
_GREEN = "\033[32m"
33+
_DIM = "\033[2m"
34+
_RESET = "\033[0m"
35+
36+
_DEFAULT_VERILATOR_BIN = str(
37+
Path(__file__).resolve().parent.parent
38+
/ "build/lowrisc_mocha_top_chip_verilator_0/sim-verilator/Vtop_chip_verilator"
39+
)
40+
41+
42+
def parse_args() -> argparse.Namespace:
43+
parser = argparse.ArgumentParser(
44+
description="Run verilator, streaming its log and UART output. "
45+
"Optionally toggle tracing when a trigger string appears on the UART."
46+
)
47+
parser.add_argument(
48+
"--trace-trigger",
49+
metavar="STRING",
50+
default=None,
51+
help="Watch the UART for STRING and send SIGUSR1 to toggle tracing when found",
52+
)
53+
parser.add_argument(
54+
"--verilator-bin",
55+
default=_DEFAULT_VERILATOR_BIN,
56+
help=f"Path to the verilator simulation binary (default: {_DEFAULT_VERILATOR_BIN})",
57+
)
58+
parser.add_argument("elfs", nargs="+", help="ELF files passed to verilator with -E")
59+
return parser.parse_args()
60+
61+
62+
async def drain_verilator(
63+
proc: asyncio.subprocess.Process,
64+
uart_path_holder: list[Optional[str]],
65+
uart_ready: asyncio.Event,
66+
kill_cmd_holder: list[Optional[str]],
67+
kill_ready: asyncio.Event,
68+
) -> None:
69+
"""Read and print verilator output, capturing UART path and kill command."""
70+
uart_pattern = re.compile(r"\$\s*screen\s+(\S+)")
71+
kill_pattern = re.compile(r"\$\s*(kill\s+-USR1\s+\d+)")
72+
73+
async for line_bytes in proc.stdout:
74+
line = line_bytes.decode(errors="replace")
75+
sys.stderr.write(_DIM + line + _RESET)
76+
77+
if not uart_ready.is_set():
78+
m = uart_pattern.search(line)
79+
if m:
80+
uart_path_holder[0] = m.group(1)
81+
print(
82+
f"{_CYAN}[verilator] UART device: {uart_path_holder[0]}{_RESET}",
83+
file=sys.stderr,
84+
)
85+
uart_ready.set()
86+
87+
if not kill_ready.is_set():
88+
m = kill_pattern.search(line)
89+
if m:
90+
kill_cmd_holder[0] = m.group(1)
91+
print(
92+
f"{_CYAN}[verilator] kill cmd: {kill_cmd_holder[0]}{_RESET}",
93+
file=sys.stderr,
94+
)
95+
kill_ready.set()
96+
97+
# Unblock any waiters if verilator exits before printing expected lines
98+
uart_ready.set()
99+
kill_ready.set()
100+
101+
102+
async def stream_uart(
103+
device_path: str,
104+
trigger_string: Optional[str] = None,
105+
trigger_event: Optional[asyncio.Event] = None,
106+
) -> None:
107+
"""
108+
Read from the UART device and print its output indefinitely until cancelled.
109+
If trigger_string is given, set trigger_event when it is found on the UART,
110+
then continue streaming.
111+
"""
112+
if trigger_string:
113+
print(
114+
f"{_CYAN}[uart] Watching '{device_path}' for '{trigger_string}' ...{_RESET}",
115+
file=sys.stderr,
116+
)
117+
else:
118+
print(f"{_CYAN}[uart] Streaming '{device_path}' ...{_RESET}", file=sys.stderr)
119+
120+
loop = asyncio.get_running_loop()
121+
with serial.Serial(device_path, timeout=1) as ser:
122+
while True:
123+
line_bytes = await loop.run_in_executor(None, ser.readline)
124+
line = line_bytes.decode(errors="replace")
125+
if line:
126+
sys.stdout.write(_GREEN + line + _RESET)
127+
sys.stdout.flush()
128+
if (
129+
trigger_string
130+
and trigger_event
131+
and not trigger_event.is_set()
132+
and trigger_string in line
133+
):
134+
print(f"{_CYAN}[uart] trigger found{_RESET}", file=sys.stderr)
135+
trigger_event.set()
136+
137+
138+
async def main() -> None:
139+
args = parse_args()
140+
141+
verilator_cmd = [args.verilator_bin] + [item for elf in args.elfs for item in ["-E", elf]]
142+
print(f"{_CYAN}[verilator] Starting: {' '.join(verilator_cmd)}{_RESET}", file=sys.stderr)
143+
144+
proc = await asyncio.create_subprocess_exec(
145+
*verilator_cmd,
146+
stdout=asyncio.subprocess.PIPE,
147+
stderr=asyncio.subprocess.STDOUT,
148+
)
149+
150+
uart_path_holder = [None]
151+
kill_cmd_holder = [None]
152+
uart_ready = asyncio.Event()
153+
kill_ready = asyncio.Event()
154+
155+
drain_task = asyncio.create_task(
156+
drain_verilator(proc, uart_path_holder, uart_ready, kill_cmd_holder, kill_ready)
157+
)
158+
159+
await uart_ready.wait()
160+
161+
if uart_path_holder[0] is None:
162+
print(
163+
f"{_CYAN}[verilator] warning: no UART device found in log, "
164+
f"skipping UART streaming{_RESET}",
165+
file=sys.stderr,
166+
)
167+
await proc.wait()
168+
await drain_task
169+
return
170+
171+
trigger_event = None
172+
if args.trace_trigger:
173+
await kill_ready.wait()
174+
if kill_cmd_holder[0] is None:
175+
print(
176+
f"{_CYAN}[verilator] warning: no kill command found in log, "
177+
f"cannot trigger tracing{_RESET}",
178+
file=sys.stderr,
179+
)
180+
else:
181+
trigger_event = asyncio.Event()
182+
183+
uart_task = asyncio.create_task(
184+
stream_uart(
185+
uart_path_holder[0],
186+
trigger_string=args.trace_trigger,
187+
trigger_event=trigger_event,
188+
)
189+
)
190+
191+
if trigger_event is not None:
192+
await trigger_event.wait()
193+
kill_parts = kill_cmd_holder[0].split()
194+
print(f"{_CYAN}[trace_trigger] Executing: {' '.join(kill_parts)}{_RESET}", file=sys.stderr)
195+
kill_proc = await asyncio.create_subprocess_exec(*kill_parts)
196+
await kill_proc.wait()
197+
198+
await proc.wait()
199+
uart_task.cancel()
200+
201+
await drain_task
202+
203+
204+
if __name__ == "__main__":
205+
asyncio.run(main())

util/verilator_trace_trigger.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
#!/usr/bin/env python
2+
3+
# Copyright lowRISC contributors (COSMIC project).
4+
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
"""
8+
Run a verilator simulation, streaming verilator log and UART output.
9+
Optionally toggle tracing (SIGUSR1) when a trigger string appears on the UART.
10+
11+
The UART device path is extracted from the verilator log (a line in the
12+
format "$ screen /dev/pts/N").
13+
14+
Usage:
15+
verilator_trace_trigger.py [--trace-trigger=STRING] <verilator_bin> <elf> [<elf> ...]
16+
17+
Arguments:
18+
verilator_bin Path to the verilator simulation binary
19+
elfs One or more ELF files to pass to verilator with -E
20+
21+
Options:
22+
--trace-trigger STR Watch the UART for STR and toggle tracing (SIGUSR1) when found
23+
"""
24+
25+
import argparse
26+
import asyncio
27+
import re
28+
import sys
29+
30+
import serial
31+
32+
33+
def parse_args():
34+
parser = argparse.ArgumentParser(
35+
description="Run verilator, streaming its log and UART output. "
36+
"Optionally toggle tracing when a trigger string appears on the UART."
37+
)
38+
parser.add_argument(
39+
"--trace-trigger",
40+
metavar="STRING",
41+
default=None,
42+
help="Watch the UART for STRING and send SIGUSR1 to toggle tracing when found",
43+
)
44+
parser.add_argument("verilator_bin", help="Path to the verilator simulation binary")
45+
parser.add_argument("elfs", nargs="+", help="ELF files passed to verilator with -E")
46+
return parser.parse_args()
47+
48+
49+
async def drain_verilator(proc, uart_path_holder, uart_ready, kill_cmd_holder, kill_ready):
50+
"""Read and print verilator output, capturing UART path and kill command."""
51+
uart_pattern = re.compile(r"\$\s*screen\s+(\S+)")
52+
kill_pattern = re.compile(r"\$\s*(kill\s+-USR1\s+\d+)")
53+
54+
async for line_bytes in proc.stdout:
55+
line = line_bytes.decode(errors="replace")
56+
sys.stderr.write(line)
57+
58+
if not uart_ready.is_set():
59+
m = uart_pattern.search(line)
60+
if m:
61+
uart_path_holder[0] = m.group(1)
62+
print(f"[verilator] UART device: {uart_path_holder[0]}", file=sys.stderr)
63+
uart_ready.set()
64+
65+
if not kill_ready.is_set():
66+
m = kill_pattern.search(line)
67+
if m:
68+
kill_cmd_holder[0] = m.group(1)
69+
print(f"[verilator] kill cmd: {kill_cmd_holder[0]}", file=sys.stderr)
70+
kill_ready.set()
71+
72+
# Unblock any waiters if verilator exits before printing expected lines
73+
uart_ready.set()
74+
kill_ready.set()
75+
76+
77+
async def stream_uart(device_path, trigger_string=None):
78+
"""
79+
Read from the UART device and print its output.
80+
If trigger_string is given, return when it is found on the UART.
81+
Otherwise, stream indefinitely until cancelled.
82+
"""
83+
if trigger_string:
84+
print(
85+
f"[uart] Watching '{device_path}' for '{trigger_string}' ...",
86+
file=sys.stderr,
87+
)
88+
else:
89+
print(f"[uart] Streaming '{device_path}' ...", file=sys.stderr)
90+
91+
loop = asyncio.get_running_loop()
92+
with serial.Serial(device_path, timeout=1) as ser:
93+
while True:
94+
line_bytes = await loop.run_in_executor(None, ser.readline)
95+
line = line_bytes.decode(errors="replace")
96+
if line:
97+
print(line, end="")
98+
if trigger_string and trigger_string in line:
99+
print("[uart] trigger found", file=sys.stderr)
100+
return
101+
102+
103+
async def execute_kill_command(kill_cmd):
104+
"""Execute the captured kill command as an async subprocess."""
105+
parts = kill_cmd.split()
106+
print(f"[trace_trigger] Executing: {' '.join(parts)}", file=sys.stderr)
107+
proc = await asyncio.create_subprocess_exec(*parts)
108+
await proc.wait()
109+
110+
111+
async def main():
112+
args = parse_args()
113+
114+
verilator_cmd = [args.verilator_bin] + [item for elf in args.elfs for item in ["-E", elf]]
115+
print(f"[verilator] Starting: {' '.join(verilator_cmd)}", file=sys.stderr)
116+
117+
proc = await asyncio.create_subprocess_exec(
118+
*verilator_cmd,
119+
stdout=asyncio.subprocess.PIPE,
120+
stderr=asyncio.subprocess.STDOUT,
121+
)
122+
123+
uart_path_holder = [None]
124+
kill_cmd_holder = [None]
125+
uart_ready = asyncio.Event()
126+
kill_ready = asyncio.Event()
127+
128+
drain_task = asyncio.create_task(
129+
drain_verilator(proc, uart_path_holder, uart_ready, kill_cmd_holder, kill_ready)
130+
)
131+
132+
await uart_ready.wait()
133+
134+
if uart_path_holder[0] is None:
135+
print("[verilator] warning: no UART device found in log, skipping UART streaming", file=sys.stderr)
136+
await proc.wait()
137+
await drain_task
138+
return
139+
140+
if args.trace_trigger:
141+
await kill_ready.wait()
142+
if kill_cmd_holder[0] is None:
143+
print("[verilator] warning: no kill command found in log, cannot trigger tracing", file=sys.stderr)
144+
uart_task = asyncio.create_task(stream_uart(uart_path_holder[0]))
145+
await proc.wait()
146+
uart_task.cancel()
147+
else:
148+
await stream_uart(uart_path_holder[0], trigger_string=args.trace_trigger)
149+
await execute_kill_command(kill_cmd_holder[0])
150+
else:
151+
uart_task = asyncio.create_task(stream_uart(uart_path_holder[0]))
152+
await proc.wait()
153+
uart_task.cancel()
154+
155+
await drain_task
156+
157+
158+
if __name__ == "__main__":
159+
asyncio.run(main())

0 commit comments

Comments
 (0)