Skip to content

Commit 15293c7

Browse files
committed
[nrf fromlist] samples: boards: nordic: coresight_stm: Test STM dictionary mode
Extend STM logger test. Use nrfutil trace to decode STM logs in dictionary mode. Add check if timing for a log message is below a threshold. Set thresholds experimentally. Upstream PR: zephyrproject-rtos/zephyr#78947 Signed-off-by: Sebastian Głąb <[email protected]>
1 parent 5bb7bb0 commit 15293c7

File tree

3 files changed

+345
-21
lines changed

3 files changed

+345
-21
lines changed

.github/test-spec.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,8 @@
363363
- "samples/drivers/adc/**/*"
364364
- "samples/basic/fade_led/**/*"
365365
- "samples/basic/blinky_pwm/**/*"
366+
- "samples/boards/nrf/coresight_stm/**/*"
367+
- "samples/boards/nordic/coresight_stm/**/*"
366368
- "samples/subsys/usb/mass/**/*"
367369
- "samples/subsys/usb/cdc_acm/**/*"
368370
- "modules/hal_nordic/**/*"
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
#
2+
# Copyright (c) 2024 Nordic Semiconductor ASA
3+
#
4+
# SPDX-License-Identifier: Apache-2.0
5+
#
6+
7+
import logging
8+
import re
9+
import subprocess
10+
from dataclasses import dataclass
11+
from pathlib import Path
12+
from time import sleep
13+
from typing import Optional
14+
15+
import psutil
16+
from twister_harness import DeviceAdapter
17+
18+
logger = logging.getLogger(__name__)
19+
20+
21+
@dataclass
22+
class STMLimits:
23+
log_0_arg: Optional[float]
24+
log_1_arg: Optional[float]
25+
log_2_arg: Optional[float]
26+
log_3_arg: Optional[float]
27+
log_str: Optional[float]
28+
tracepoint: Optional[float]
29+
tracepoint_d32: Optional[float]
30+
tolerance: Optional[float]
31+
32+
33+
# https://github.com/zephyrproject-rtos/zephyr/blob/main/drivers/misc/coresight/nrf_etr.c#L102
34+
STM_M_ID = {
35+
"sec": 33,
36+
"app": 34,
37+
"rad": 35,
38+
"mod": 36,
39+
"sys": 44,
40+
"flpr": 45,
41+
"ppr": 46,
42+
"hw": 128,
43+
}
44+
45+
46+
def _check_benchmark_results(output: str, core: str, constraints: STMLimits) -> None:
47+
"""
48+
Use regular expressions to parse 'output' string.
49+
Search for benchmark results related to 'core' coprocessor.
50+
Check that benchamrk results are lower than limits provided in 'constraints'.
51+
"""
52+
53+
latency_msg_0_str = re.search(
54+
rf"{core}: Timing for log message with 0 arguments: (.+)us", output
55+
).group(1)
56+
assert latency_msg_0_str is not None, "Timing for log message with 0 arguments NOT found"
57+
# check value
58+
latency_msg_0 = float(latency_msg_0_str)
59+
threshold = (constraints.log_0_arg) * (1 + constraints.tolerance)
60+
assert (
61+
latency_msg_0 < threshold
62+
), f"Timing for log message with 0 arguments {latency_msg_0} us exceeds threshold {threshold} us"
63+
64+
65+
latency_msg_1_str = re.search(
66+
rf"{core}: Timing for log message with 1 argument: (.+)us", output
67+
).group(1)
68+
assert latency_msg_1_str is not None, "Timing for log message with 1 argument NOT found"
69+
# check value
70+
latency_msg_1 = float(latency_msg_1_str)
71+
threshold = (constraints.log_1_arg) * (1 + constraints.tolerance)
72+
assert (
73+
latency_msg_1 < threshold
74+
), f"Timing for log message with 1 argument {latency_msg_1} us exceeds threshold {threshold} us"
75+
76+
77+
latency_msg_2_str = re.search(
78+
rf"{core}: Timing for log message with 2 arguments: (.+)us", output
79+
).group(1)
80+
assert latency_msg_2_str is not None, "Timing for log message with 2 arguments NOT found"
81+
# check value
82+
latency_msg_2 = float(latency_msg_2_str)
83+
threshold = (constraints.log_2_arg) * (1 + constraints.tolerance)
84+
assert (
85+
latency_msg_2 < threshold
86+
), f"Timing for log message with 2 arguments {latency_msg_2} us exceeds threshold {threshold} us"
87+
88+
89+
latency_msg_3_str = re.search(
90+
rf"{core}: Timing for log message with 3 arguments: (.+)us", output
91+
).group(1)
92+
assert latency_msg_3_str is not None, "Timing for log message with 3 arguments NOT found"
93+
# check value
94+
latency_msg_3 = float(latency_msg_3_str)
95+
threshold = (constraints.log_3_arg) * (1 + constraints.tolerance)
96+
assert (
97+
latency_msg_3 < threshold
98+
), f"Timing for log message with 3 arguments {latency_msg_3} us exceeds threshold {threshold} us"
99+
100+
101+
latency_msg_string_str = re.search(
102+
rf"{core}: Timing for log_message with string: (.+)us", output
103+
).group(1)
104+
assert latency_msg_string_str is not None, "Timing for log_message with string NOT found"
105+
# check value
106+
latency_msg_string = float(latency_msg_string_str)
107+
threshold = (constraints.log_str) * (1 + constraints.tolerance)
108+
assert (
109+
latency_msg_string < threshold
110+
), f"Timing for log message with string {latency_msg_string} us exceeds threshold {threshold} us"
111+
112+
113+
latency_tracepoint_str = re.search(
114+
rf"{core}: Timing for tracepoint: (.+)us", output
115+
).group(1)
116+
assert latency_tracepoint_str is not None, "Timing for tracepoint NOT found"
117+
# check value
118+
latency_tracepoint = float(latency_tracepoint_str)
119+
threshold = (constraints.tracepoint) * (1 + constraints.tolerance)
120+
assert (
121+
latency_tracepoint < threshold
122+
), f"Timing for tracepoint {latency_tracepoint} us exceeds threshold {threshold} us"
123+
124+
125+
latency_tracepoint_d32_str = re.search(
126+
rf"{core}: Timing for tracepoint_d32: (.+)us", output
127+
).group(1)
128+
assert latency_tracepoint_d32_str is not None, "Timing for tracepoint_d32 NOT found"
129+
# check value
130+
latency_tracepoint_d32 = float(latency_tracepoint_d32_str)
131+
threshold = (constraints.tracepoint_d32) * (1 + constraints.tolerance)
132+
assert (
133+
latency_tracepoint_d32 < threshold
134+
), f"Timing for tracepoint_d32 {latency_tracepoint_d32} us exceeds threshold {threshold} us"
135+
136+
137+
# nrfutil starts children processes
138+
# when subprocess.terminate(nrfutil_process) is executed, only the parent terminates
139+
# this blocks serial port for other uses
140+
def _kill(proc):
141+
try:
142+
for child in psutil.Process(proc.pid).children(recursive=True):
143+
child.kill()
144+
proc.kill()
145+
except Exception as e:
146+
logger.exception(f'Could not kill nrfutil - {e}')
147+
148+
149+
def _nrfutil_dictionary_from_serial(
150+
dut: DeviceAdapter,
151+
decoded_file_name: str = "output.txt",
152+
collect_time: float = 60.0,
153+
) -> None:
154+
UART_PATH = dut.device_config.serial
155+
UART_BAUDRATE = dut.device_config.baud
156+
dut.close()
157+
158+
logger.debug(f"Using serial: {UART_PATH}")
159+
160+
if Path(f"{decoded_file_name}").exists():
161+
logger.warning("Output file already exists!")
162+
163+
# prepare database config string
164+
BUILD_DIR = str(dut.device_config.build_dir)
165+
logger.debug(f"{BUILD_DIR=}")
166+
config_str = f"{STM_M_ID['app']}:{BUILD_DIR}/coresight_stm/zephyr/log_dictionary.json"
167+
config_str = config_str + f",{STM_M_ID['rad']}:{BUILD_DIR}/remote/zephyr/log_dictionary.json"
168+
logger.debug(f"{config_str=}")
169+
170+
cmd = (
171+
"nrfutil trace stm --database-config "
172+
f"{config_str} "
173+
f"--input-serialport {UART_PATH} --baudrate {UART_BAUDRATE} "
174+
f"--output-ascii {decoded_file_name}"
175+
)
176+
try:
177+
# run nrfutil trace in background non-blocking
178+
logger.info(f"Executing:\n{cmd}")
179+
proc = subprocess.Popen(cmd.split(), stdout=subprocess.DEVNULL)
180+
except OSError as exc:
181+
logger.error(f"Unable to start nrfutil trace:\n{cmd}\n{exc}")
182+
try:
183+
proc.wait(collect_time)
184+
except subprocess.TimeoutExpired:
185+
pass
186+
finally:
187+
_kill(proc)
188+
189+
190+
def test_STM_decoded(dut: DeviceAdapter):
191+
"""
192+
Run sample.boards.nrf.coresight_stm from samples/boards/nrf/coresight_stm sample.
193+
Both Application and Radio cores use STM for logging.
194+
STM proxy (Application core) decodes logs from all domains.
195+
After reset, coprocessors execute code in expected way and Application core
196+
outputs STM traces on UART port.
197+
"""
198+
app_constraints = STMLimits(
199+
# all values in us
200+
log_0_arg=3.3,
201+
log_1_arg=2.4,
202+
log_2_arg=2.1,
203+
log_3_arg=2.1,
204+
log_str=4.6,
205+
tracepoint=0.5,
206+
tracepoint_d32=0.5,
207+
tolerance=0.5,
208+
)
209+
rad_constraints = STMLimits(
210+
# all values in us
211+
log_0_arg=6.6,
212+
log_1_arg=5.8,
213+
log_2_arg=6.0,
214+
log_3_arg=6.4,
215+
log_str=7.1,
216+
tracepoint=0.5,
217+
tracepoint_d32=0.5,
218+
tolerance=0.5,
219+
)
220+
# nrf54h20 prints immediately after it is flashed.
221+
# Wait a bit to skipp logs from previous test.
222+
sleep(3)
223+
224+
# Get output from serial port
225+
output = "\n".join(dut.readlines())
226+
227+
# check results on Application core
228+
_check_benchmark_results(
229+
output=output,
230+
core='app',
231+
constraints=app_constraints
232+
)
233+
234+
# check results on Radio core
235+
_check_benchmark_results(
236+
output=output,
237+
core='rad',
238+
constraints=rad_constraints
239+
)
240+
241+
def test_STM_dictionary_mode(dut: DeviceAdapter):
242+
"""
243+
Run sample.boards.nrf.coresight_stm.dict from samples/boards/nrf/coresight_stm sample.
244+
Both Application and Radio cores use STM for logging.
245+
STM proxy (Application core) prints on serial port raw logs from all domains.
246+
Nrfutil trace is used to decode STM logs.
247+
After reset, coprocessors execute code in expected way and Application core
248+
outputs STM traces on UART port.
249+
"""
250+
BUILD_DIR = str(dut.device_config.build_dir)
251+
test_filename = f"{BUILD_DIR}/stm_benchmark_dictionary_from_serial.txt"
252+
COLLECT_TIMEOUT = 10.0
253+
app_constraints = STMLimits(
254+
# all values in us
255+
log_0_arg=1.8,
256+
log_1_arg=0.7,
257+
log_2_arg=0.7,
258+
log_3_arg=1.5,
259+
log_str=3.4,
260+
tracepoint=0.5,
261+
tracepoint_d32=0.5,
262+
tolerance=0.5,
263+
)
264+
rad_constraints = STMLimits(
265+
# all values in us
266+
log_0_arg=2.0,
267+
log_1_arg=0.8,
268+
log_2_arg=0.9,
269+
log_3_arg=1.7,
270+
log_str=4.0,
271+
tracepoint=0.5,
272+
tracepoint_d32=0.5,
273+
tolerance=0.5,
274+
)
275+
276+
# use nrfutil trace to decode logs
277+
_nrfutil_dictionary_from_serial(
278+
dut=dut,
279+
decoded_file_name=f"{test_filename}",
280+
collect_time=COLLECT_TIMEOUT,
281+
)
282+
283+
# read decoded logs
284+
with open(f"{test_filename}", errors="ignore") as decoded_file:
285+
decoded_file_content = decoded_file.read()
286+
287+
# if nothing in decoded_file, stop test
288+
assert(
289+
len(decoded_file_content) > 0
290+
), f"File {test_filename} is empty"
291+
292+
# check results on Application core
293+
_check_benchmark_results(
294+
output=decoded_file_content,
295+
core='app',
296+
constraints=app_constraints
297+
)
298+
299+
# check results on Radio core
300+
_check_benchmark_results(
301+
output=decoded_file_content,
302+
core='rad',
303+
constraints=rad_constraints
304+
)
Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,36 @@
11
sample:
22
name: Logging using Coresight STM on nrf54h20
3+
34
common:
5+
tags: stm
46
sysbuild: true
7+
platform_allow:
8+
- nrf54h20dk/nrf54h20/cpuapp
9+
integration_platforms:
10+
- nrf54h20dk/nrf54h20/cpuapp
11+
512
tests:
613
sample.boards.nrf.coresight_stm.dict:
7-
platform_allow:
8-
- nrf54h20dk/nrf54h20/cpuapp
9-
integration_platforms:
10-
- nrf54h20dk/nrf54h20/cpuapp
11-
build_only: true
14+
filter: not CONFIG_COVERAGE
15+
harness: pytest
16+
harness_config:
17+
pytest_dut_scope: session
18+
pytest_root:
19+
- "pytest/test_stm.py::test_STM_dictionary_mode"
1220
required_snippets:
1321
- nordic-log-stm-dict
22+
1423
sample.boards.nrf.coresight_stm:
15-
platform_allow:
16-
- nrf54h20dk/nrf54h20/cpuapp
17-
integration_platforms:
18-
- nrf54h20dk/nrf54h20/cpuapp
24+
filter: not CONFIG_COVERAGE
25+
harness: pytest
26+
harness_config:
27+
pytest_dut_scope: session
28+
pytest_root:
29+
- "pytest/test_stm.py::test_STM_decoded"
30+
required_snippets:
31+
- nordic-log-stm
32+
33+
sample.boards.nrf.coresight_stm.local_uart:
1934
harness: console
2035
harness_config:
2136
type: multi_line
@@ -26,20 +41,23 @@ tests:
2641
- "Timing for log message with 2 arguments:"
2742
- "Timing for log message with 3 arguments:"
2843
- "Timing for log_message with string:"
44+
45+
sample.boards.nrf.coresight_stm.coverage:
46+
filter: CONFIG_COVERAGE
47+
harness: console
48+
harness_config:
49+
type: one_line
50+
regex:
51+
- "GCOV_COVERAGE_DUMP_END"
2952
required_snippets:
3053
- nordic-log-stm
31-
sample.boards.nrf.coresight_stm.local_uart:
32-
platform_allow:
33-
- nrf54h20dk/nrf54h20/cpuapp
34-
integration_platforms:
35-
- nrf54h20dk/nrf54h20/cpuapp
54+
55+
sample.boards.nrf.coresight_stm.dict.coverage:
56+
filter: CONFIG_COVERAGE
3657
harness: console
3758
harness_config:
38-
type: multi_line
39-
ordered: true
59+
type: one_line
4060
regex:
41-
- "Timing for log message with 0 arguments:"
42-
- "Timing for log message with 1 argument:"
43-
- "Timing for log message with 2 arguments:"
44-
- "Timing for log message with 3 arguments:"
45-
- "Timing for log_message with string:"
61+
- "GCOV_COVERAGE_DUMP_END"
62+
required_snippets:
63+
- nordic-log-stm-dict

0 commit comments

Comments
 (0)