Skip to content

Commit 069ad6a

Browse files
committed
Add basic native sim tests
1 parent 44f3816 commit 069ad6a

12 files changed

Lines changed: 1891 additions & 2 deletions

File tree

.github/workflows/run-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,5 +88,5 @@ jobs:
8888
uses: ./.github/actions/deps/ports/zephyr-cp
8989
- name: Set up external
9090
uses: ./.github/actions/deps/external
91-
- name: Run Zephyr build tests
91+
- name: Run Zephyr tests
9292
run: make -C ports/zephyr-cp test

ports/zephyr-cp/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ project(circuitpython)
55

66
target_sources(app PRIVATE zephyr_main.c)
77

8+
# Add I2C emulator control for native_sim testing
9+
if(CONFIG_BOARD_NATIVE_SIM)
10+
target_sources(app PRIVATE native_sim_i2c_emul_control.c)
11+
endif()
12+
813
# From: https://github.com/zephyrproject-rtos/zephyr/blob/main/samples/application_development/external_lib/CMakeLists.txt
914
# The external static library that we are linking with does not know
1015
# how to build for this platform so we export all the flags used in

ports/zephyr-cp/Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,6 @@ all:
4646
clean-all:
4747
rm -rf build build-*
4848

49-
test:
49+
test: build-native_native_sim/zephyr-cp/zephyr/zephyr.exe
5050
pytest cptools/tests
51+
pytest tests/ -v

ports/zephyr-cp/boards/native_sim.conf

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,17 @@ CONFIG_NATIVE_SIM_SLOWDOWN_TO_REAL_TIME=n
44

55
# So we can test safe mode
66
CONFIG_NATIVE_SIM_REBOOT=y
7+
8+
CONFIG_TRACING=y
9+
CONFIG_TRACING_PERFETTO=y
10+
CONFIG_TRACING_SYNC=y
11+
CONFIG_TRACING_BACKEND_POSIX=y
12+
CONFIG_TRACING_GPIO=y
13+
14+
# I2C emulation for testing
15+
CONFIG_I2C_EMUL=y
16+
17+
# EEPROM emulation for testing
18+
CONFIG_EEPROM=y
19+
CONFIG_EEPROM_AT24=y
20+
CONFIG_EEPROM_AT2X_EMUL=y

ports/zephyr-cp/boards/native_sim.overlay

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,16 @@
3636
};
3737
};
3838

39+
/* Add emulated I2C devices for testing */
40+
&i2c0 {
41+
at24_eeprom: eeprom@50 {
42+
compatible = "atmel,at24";
43+
reg = <0x50>;
44+
size = <256>;
45+
pagesize = <8>;
46+
address-width = <8>;
47+
timeout = <5>;
48+
};
49+
};
50+
3951
#include "../app.overlay"
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# Perfetto Tracing
2+
3+
The Zephyr port supports Perfetto tracing for performance analysis. This document
4+
describes how to capture, validate, and view traces.
5+
6+
## Capturing Traces
7+
8+
Traces are written to `circuitpython.perfetto-trace` in the port directory when
9+
running with tracing enabled (e.g., on native_sim).
10+
11+
## Validating Traces
12+
13+
### Using trace_processor
14+
15+
The Perfetto trace_processor tool can validate and query trace files:
16+
17+
```bash
18+
~/repos/perfetto/tools/trace_processor circuitpython.perfetto-trace
19+
```
20+
21+
This will download the trace_processor binary if needed and open an interactive
22+
SQL shell. If the trace loads successfully, you can query it:
23+
24+
```sql
25+
SELECT COUNT(*) FROM slice;
26+
```
27+
28+
### Using the Perfetto UI
29+
30+
Open https://ui.perfetto.dev and drag your trace file onto the page.
31+
32+
## Debugging Invalid Traces
33+
34+
### Common Error: Packets Skipped Due to Invalid Incremental State
35+
36+
If trace_processor reports packets being skipped with messages like:
37+
38+
```
39+
packet_skipped_seq_needs_incremental_state_invalid
40+
```
41+
42+
This means packets have `SEQ_NEEDS_INCREMENTAL_STATE` (value 2) set but no
43+
prior packet set `SEQ_INCREMENTAL_STATE_CLEARED` (value 1) to initialize the
44+
incremental state.
45+
46+
**Root Cause**: The process descriptor packet (which sets `SEQ_INCREMENTAL_STATE_CLEARED`)
47+
must be emitted before any other trace packets.
48+
49+
**Diagnosis**: Use protoc to inspect the raw trace:
50+
51+
```bash
52+
protoc --decode_raw < circuitpython.perfetto-trace | head -100
53+
```
54+
55+
Look for field 13 (sequence_flags) in the first few packets:
56+
57+
- `13: 1` = SEQ_INCREMENTAL_STATE_CLEARED (good - should be first)
58+
- `13: 2` = SEQ_NEEDS_INCREMENTAL_STATE (requires prior cleared packet)
59+
60+
A valid trace should have the process descriptor with `13: 1` as one of the
61+
first packets.
62+
63+
**Fix**: Ensure `perfetto_start()` is called before any trace events are emitted.
64+
The descriptor emit functions in `perfetto_encoder.c` should check:
65+
66+
```c
67+
if (!started) {
68+
perfetto_start();
69+
}
70+
```
71+
72+
### Analyzing Raw Trace Structure
73+
74+
To understand the trace structure:
75+
76+
```bash
77+
# Count total packets
78+
protoc --decode_raw < circuitpython.perfetto-trace | grep -c "^1 {"
79+
80+
# Find all sequence_flags values
81+
protoc --decode_raw < circuitpython.perfetto-trace | grep "13:" | sort | uniq -c
82+
83+
# Look for track descriptors (field 60)
84+
protoc --decode_raw < circuitpython.perfetto-trace | grep -A20 "60 {"
85+
86+
# Look for process descriptors (field 3 inside track_descriptor)
87+
protoc --decode_raw < circuitpython.perfetto-trace | grep -B5 "3 {"
88+
```
89+
90+
### Key Protobuf Field Numbers
91+
92+
TracePacket fields:
93+
94+
| Field | Description |
95+
|-------|-------------|
96+
| 8 | timestamp |
97+
| 10 | trusted_packet_sequence_id |
98+
| 11 | track_event |
99+
| 12 | interned_data |
100+
| 13 | sequence_flags |
101+
| 60 | track_descriptor |
102+
103+
TrackDescriptor fields (inside field 60):
104+
105+
| Field | Description |
106+
|-------|-------------|
107+
| 1 | uuid |
108+
| 2 | name |
109+
| 3 | process (ProcessDescriptor) |
110+
| 4 | thread (ThreadDescriptor) |
111+
| 5 | parent_uuid |
112+
113+
## Build Verification
114+
115+
After modifying tracing code, verify the build is updated:
116+
117+
```bash
118+
# Check source vs object file timestamps
119+
ls -la zephyr/subsys/tracing/perfetto/perfetto_encoder.c
120+
ls -la zephyr/build/zephyr/subsys/tracing/perfetto/CMakeFiles/subsys__tracing__perfetto.dir/perfetto_encoder.c.obj
121+
```
122+
123+
The object file timestamp must be newer than the source file timestamp. If not,
124+
rebuild the project before capturing a new trace.
125+
126+
## Architecture
127+
128+
The tracing implementation consists of:
129+
130+
- `perfetto_encoder.c`: Encodes trace packets using nanopb
131+
- `perfetto_top.c`: Implements Zephyr tracing hooks (sys_trace_*)
132+
- `perfetto_encoder.h`: Public API and UUID definitions
133+
134+
Key UUIDs:
135+
136+
| Constant | Value | Description |
137+
|----------|-------|-------------|
138+
| PROCESS_UUID | 1 | Root process track |
139+
| ISR_TRACK_UUID | 2 | Interrupt service routine track |
140+
| TRACE_TRACK_UUID | 3 | Top-level trace track |
141+
142+
### Initialization Flow
143+
144+
1. `SYS_INIT` calls `perfetto_init()` at POST_KERNEL priority 0
145+
2. `perfetto_init()` calls `perfetto_encoder_init()`
146+
3. `perfetto_initialized` is set to true
147+
4. Thread hooks start firing
148+
5. First emit function calls `perfetto_start()`
149+
6. `perfetto_start()` emits process descriptor with `SEQ_INCREMENTAL_STATE_CLEARED`
150+
7. Subsequent packets use `SEQ_NEEDS_INCREMENTAL_STATE`
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* Command-line control for enabling/disabling emulated I2C devices
5+
* on native_sim. This allows testing device hot-plug and error scenarios.
6+
*/
7+
8+
#include <zephyr/kernel.h>
9+
#include <zephyr/device.h>
10+
#include <zephyr/drivers/emul.h>
11+
#include <zephyr/drivers/i2c_emul.h>
12+
#include <zephyr/init.h>
13+
#include <zephyr/logging/log.h>
14+
15+
#include "nsi_cmdline.h"
16+
#include "posix_native_task.h"
17+
18+
LOG_MODULE_REGISTER(i2c_emul_control, LOG_LEVEL_INF);
19+
20+
#define MAX_DISABLED_DEVICES 16
21+
22+
struct disabled_device {
23+
const char *name;
24+
const struct emul *emul;
25+
struct i2c_emul_api mock_api;
26+
bool disabled;
27+
};
28+
29+
static struct disabled_device disabled_devices[MAX_DISABLED_DEVICES];
30+
static int num_disabled_devices = 0;
31+
32+
static char *disabled_device_args[MAX_DISABLED_DEVICES];
33+
static int num_disabled_device_args = 0;
34+
35+
/*
36+
* Mock transfer function that returns -EIO (NACK) when device is disabled,
37+
* or -ENOSYS to fall back to the real emulator.
38+
*/
39+
static int disabled_device_transfer(const struct emul *target,
40+
struct i2c_msg *msgs,
41+
int num_msgs,
42+
int addr) {
43+
ARG_UNUSED(msgs);
44+
ARG_UNUSED(num_msgs);
45+
ARG_UNUSED(addr);
46+
47+
for (int i = 0; i < num_disabled_devices; i++) {
48+
if (disabled_devices[i].emul == target) {
49+
if (disabled_devices[i].disabled) {
50+
LOG_DBG("Device %s is disabled, returning -EIO",
51+
disabled_devices[i].name);
52+
return -EIO;
53+
}
54+
break;
55+
}
56+
}
57+
/* Fall back to normal emulator behavior */
58+
return -ENOSYS;
59+
}
60+
61+
int i2c_emul_control_disable_device(const char *name) {
62+
const struct emul *emul = emul_get_binding(name);
63+
if (!emul) {
64+
LOG_ERR("Emulator '%s' not found", name);
65+
return -ENODEV;
66+
}
67+
68+
if (emul->bus_type != EMUL_BUS_TYPE_I2C) {
69+
LOG_ERR("Emulator '%s' is not an I2C device", name);
70+
return -EINVAL;
71+
}
72+
73+
/* Find existing entry or create new one */
74+
int idx = -1;
75+
for (int i = 0; i < num_disabled_devices; i++) {
76+
if (disabled_devices[i].emul == emul) {
77+
idx = i;
78+
break;
79+
}
80+
}
81+
82+
if (idx < 0) {
83+
if (num_disabled_devices >= MAX_DISABLED_DEVICES) {
84+
LOG_ERR("Too many disabled devices");
85+
return -ENOMEM;
86+
}
87+
idx = num_disabled_devices++;
88+
disabled_devices[idx].name = name;
89+
disabled_devices[idx].emul = emul;
90+
disabled_devices[idx].mock_api.transfer = disabled_device_transfer;
91+
92+
/* Install our mock_api to intercept transfers */
93+
emul->bus.i2c->mock_api = &disabled_devices[idx].mock_api;
94+
}
95+
96+
disabled_devices[idx].disabled = true;
97+
LOG_INF("Disabled I2C emulator: %s", name);
98+
return 0;
99+
}
100+
101+
int i2c_emul_control_enable_device(const char *name) {
102+
for (int i = 0; i < num_disabled_devices; i++) {
103+
if (strcmp(disabled_devices[i].name, name) == 0) {
104+
disabled_devices[i].disabled = false;
105+
LOG_INF("Enabled I2C emulator: %s", name);
106+
return 0;
107+
}
108+
}
109+
LOG_ERR("Device '%s' not in disabled list", name);
110+
return -ENODEV;
111+
}
112+
113+
bool i2c_emul_control_is_disabled(const char *name) {
114+
for (int i = 0; i < num_disabled_devices; i++) {
115+
if (strcmp(disabled_devices[i].name, name) == 0) {
116+
return disabled_devices[i].disabled;
117+
}
118+
}
119+
return false;
120+
}
121+
122+
/* Command-line option handler */
123+
static void cmd_disable_i2c_device(char *argv, int offset) {
124+
/* The value is at argv + offset (after the '=' in --disable-i2c=value) */
125+
char *value = argv + offset;
126+
if (num_disabled_device_args < MAX_DISABLED_DEVICES) {
127+
disabled_device_args[num_disabled_device_args++] = value;
128+
} else {
129+
printk("i2c_emul_control: Too many --disable-i2c arguments, ignoring: %s\n", value);
130+
}
131+
}
132+
133+
static struct args_struct_t i2c_emul_args[] = {
134+
{
135+
.option = "disable-i2c",
136+
.name = "device",
137+
.type = 's',
138+
.call_when_found = cmd_disable_i2c_device,
139+
.descript = "Disable an emulated I2C device by name (can be repeated). "
140+
"Example: --disable-i2c=bmi160"
141+
},
142+
ARG_TABLE_ENDMARKER
143+
};
144+
145+
static void register_cmdline_opts(void) {
146+
nsi_add_command_line_opts(i2c_emul_args);
147+
}
148+
149+
/* Register command-line options early in boot */
150+
NATIVE_TASK(register_cmdline_opts, PRE_BOOT_1, 0);
151+
152+
static int apply_disabled_devices(void) {
153+
LOG_DBG("Applying %d disabled device(s)", num_disabled_device_args);
154+
for (int i = 0; i < num_disabled_device_args; i++) {
155+
int rc = i2c_emul_control_disable_device(disabled_device_args[i]);
156+
if (rc != 0) {
157+
LOG_WRN("Failed to disable I2C device '%s': %d",
158+
disabled_device_args[i], rc);
159+
}
160+
}
161+
return 0;
162+
}
163+
164+
/*
165+
* Apply after emulators are initialized.
166+
* I2C emulators are registered at POST_KERNEL level, so we need to run
167+
* at APPLICATION level to ensure they exist.
168+
*/
169+
SYS_INIT(apply_disabled_devices, APPLICATION, 99);

0 commit comments

Comments
 (0)