Skip to content

Commit b9ed83d

Browse files
committed
add missing gpio_sdmmc_init and tests
1 parent 14b1906 commit b9ed83d

File tree

11 files changed

+1842
-0
lines changed

11 files changed

+1842
-0
lines changed

CLAUDE.md

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# CLAUDE.md
2+
3+
## Project overview
4+
5+
Panda is an open-source car interface device by comma.ai. This repo contains:
6+
- STM32H7 firmware (C, in `board/`)
7+
- Python userspace library (`python/` and `board/jungle/__init__.py`)
8+
- Tests (unit, protocol, MISRA static analysis, hardware-in-loop)
9+
10+
Adjacent repo **opendbc** lives at `../opendbc` and provides the safety model (`opendbc/safety/safety.h`), `CarParams`, and CAN packet definitions. The firmware includes it as `opendbc/safety/safety.h` and the SConscript uses `opendbc.INCLUDE_PATH`.
11+
12+
## Build
13+
14+
```bash
15+
./setup.sh # install deps into .venv (first time)
16+
PATH="$(pwd)/.venv/bin:$PATH" .venv/bin/scons # debug build (all targets)
17+
RELEASE=1 CERT=board/certs/debug PATH="$(pwd)/.venv/bin:$PATH" .venv/bin/scons # release build
18+
```
19+
20+
The `PATH` prefix is required on macOS so that `board/crypto/sign.py` (shebang: `#!/usr/bin/env python3`) resolves to the venv Python 3.12 (which has `pycryptodome`) rather than the system Python.
21+
22+
Firmware binaries are output to `board/obj/`. Each target builds a bootstub (RSA-2048 signed) and main app binary. `board/obj/gitversion.h` is auto-generated from git HEAD.
23+
24+
Compiler: `arm-none-eabi-gcc`, C standard: GNU11, flags include `-Wall -Wextra -Wstrict-prototypes -Werror -fmax-errors=1`.
25+
26+
## Tests
27+
28+
```bash
29+
./test.sh # scons + ruff check + pytest (non-HITL, non-MISRA)
30+
pytest tests/ # same subset
31+
32+
pytest tests/misra/ # MISRA C:2012 static analysis via cppcheck
33+
pytest tests/hitl/ # hardware-in-loop (requires physical device)
34+
```
35+
36+
pytest config (pyproject.toml): `-nauto --maxprocesses=8`, excludes `tests/som/` and `tests/hitl/` by default.
37+
38+
## Code style
39+
40+
**C (board/):**
41+
- MISRA C:2012 compliance enforced by cppcheck; see `tests/misra/coverage_table` for tracked suppressions
42+
- 2-space indentation in most headers
43+
- Suppress MISRA violations with inline comments: `// misra-c2012-17.7: ...`
44+
45+
**Python:**
46+
- Ruff linter: `ruff check .` (line length 160, target Python 3.11)
47+
- Enabled rule sets: E, F, W, PIE, C4, ISC, RUF100, A
48+
- Ignored: W292, E741, E402, C408, ISC003
49+
50+
## Key directories
51+
52+
| Path | Contents |
53+
|------|----------|
54+
| `board/` | STM32 firmware |
55+
| `board/drivers/` | HAL drivers (CAN, USB, SPI, UART, timers, …) |
56+
| `board/stm32h7/` | STM32H7 linker scripts, startup, peripheral includes |
57+
| `board/jungle/` | Jungle board firmware and Python library |
58+
| `board/jungle/stm32h7/` | Jungle-specific STM32H7 drivers (SDMMC, SD replay) |
59+
| `board/jungle/scripts/` | Jungle utility scripts (CAN printer, spam, provision SD) |
60+
| `board/boards/` | Board variant hardware abstraction layers |
61+
| `board/crypto/` | RSA/SHA implementation and firmware signing |
62+
| `python/` | Core `Panda` class (USB + SPI transports) |
63+
| `tests/libpanda/` | C unit tests via libpanda.so (compiled shared library) |
64+
| `tests/hitl/` | Hardware-in-loop tests |
65+
| `tests/misra/` | cppcheck MISRA analysis |
66+
| `tests/usbprotocol/` | USB/CAN protocol tests |
67+
68+
## Jungle V2 SD card replay
69+
70+
The jungle V2 has an SD card slot that can replay openpilot CAN routes autonomously. See `board/jungle/README.md` for usage. Key files:
71+
72+
- `board/jungle/stm32h7/llsdmmc.h` — SDMMC driver (raw sector read/write via IDMA)
73+
- `board/jungle/stm32h7/sd_replay.h` — replay state machine; call `sd_replay_tick()` from the main loop
74+
- `board/jungle/scripts/provision_sd.py` — writes openpilot rlog → SD binary image
75+
- USB commands: `0xa5` start/stop, `0xa6` status
76+
- Python API: `PandaJungle.sd_replay_start/stop/status()`
77+
78+
## CANPacket_t
79+
80+
Defined in `../opendbc/opendbc/safety/can.h`:
81+
```c
82+
typedef struct {
83+
unsigned char fd : 1;
84+
unsigned char bus : 3;
85+
unsigned char data_len_code : 4; // look up byte length with dlc_to_len[]
86+
unsigned char rejected : 1;
87+
unsigned char returned : 1;
88+
unsigned char extended : 1;
89+
unsigned int addr : 29;
90+
unsigned char checksum;
91+
unsigned char data[64];
92+
} __attribute__((packed, aligned(4))) CANPacket_t;
93+
```
94+
95+
Use `can_set_checksum()` before calling `can_send()`. Use `dlc_to_len[data_len_code]` to get byte length.
96+
97+
## USB control protocol (jungle)
98+
99+
Jungle-specific USB control requests are handled in `board/jungle/main_comms.h`. Assigned codes:
100+
101+
| Code | Direction | Description |
102+
|------|-----------|-------------|
103+
| 0xa0 | OUT | Set panda power (all channels) |
104+
| 0xa1 | OUT | Set harness orientation |
105+
| 0xa2 | OUT | Set ignition |
106+
| 0xa3 | OUT | Set panda power (individual channel) |
107+
| 0xa4 | OUT | Enable generated CAN traffic |
108+
| 0xa5 | OUT | SD replay start (param1=1) / stop (param1=0) |
109+
| 0xa6 | IN | SD replay status (9 bytes: uint8 state, uint32 total, uint32 current) |
110+
| 0xa8 | IN | Microsecond timer (4 bytes) |
111+
| 0xd2 | IN | Jungle health packet |
112+
| 0xde | OUT | Set CAN bitrate |
113+
| 0xf5 | OUT | CAN silent mode |
114+
| 0xf7 | OUT | Header pin enable/disable |
115+
| 0xf9 | OUT | CAN-FD data bitrate |

board/jungle/README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,38 @@ Updating the firmware is easy! In the `board/jungle/` folder, run:
2424

2525
If you somehow bricked your jungle, you'll need a [comma key](https://comma.ai/shop/products/comma-key) to put the microcontroller in DFU mode for the V1.
2626
For V2, the onboard button serves this purpose. When powered on while holding the button to put it in DFU mode, running `./recover.sh` in `board/` should unbrick it.
27+
28+
## SD card CAN replay (V2 only)
29+
30+
The jungle V2 SD card slot can replay CAN messages from an openpilot route autonomously, without needing a host PC.
31+
32+
### Provision the SD card
33+
34+
From an openpilot checkout, run:
35+
``` bash
36+
python board/jungle/scripts/provision_sd.py /path/to/rlog /dev/sdX
37+
```
38+
39+
Or write to a binary file and `dd` it to the card:
40+
``` bash
41+
python board/jungle/scripts/provision_sd.py /path/to/rlog replay.bin
42+
sudo dd if=replay.bin of=/dev/sdX bs=512
43+
```
44+
45+
### Start/stop replay
46+
47+
``` python
48+
from panda import PandaJungle
49+
50+
p = PandaJungle()
51+
p.sd_replay_start()
52+
53+
# Poll progress
54+
status = p.sd_replay_status()
55+
# {"state": 1, "total_records": 12345, "current_record": 500}
56+
# state: 0=idle, 1=active, 2=done, 3=error
57+
58+
p.sd_replay_stop()
59+
```
60+
61+
Messages are sent in real time, preserving the original inter-message timing from the route. The jungle replays from the SD card automatically on the next `sd_replay_start()` call without re-provisioning.

board/jungle/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,16 @@ def debug_read(self):
149149

150150
def set_header_pin(self, pin_num, enabled):
151151
self._handle.controlWrite(Panda.REQUEST_OUT, 0xf7, int(pin_num), int(enabled), b'')
152+
153+
# ******************* SD card CAN replay *******************
154+
155+
def sd_replay_start(self):
156+
self._handle.controlWrite(PandaJungle.REQUEST_OUT, 0xa5, 1, 0, b'')
157+
158+
def sd_replay_stop(self):
159+
self._handle.controlWrite(PandaJungle.REQUEST_OUT, 0xa5, 0, 0, b'')
160+
161+
def sd_replay_status(self):
162+
dat = self._handle.controlRead(PandaJungle.REQUEST_IN, 0xa6, 0, 0, 9)
163+
state, total, current = struct.unpack("<BII", dat)
164+
return {"state": state, "total_records": total, "current_record": current}

board/jungle/main.c

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919

2020
#include "board/obj/gitversion.h"
2121

22+
#ifdef STM32H7
23+
#include "board/jungle/stm32h7/llsdmmc.h"
24+
#include "board/jungle/stm32h7/sd_replay.h"
25+
#endif
26+
2227
#include "board/can_comms.h"
2328
#include "board/jungle/main_comms.h"
2429

@@ -155,9 +160,24 @@ int main(void) {
155160
can_init_all();
156161
current_board->set_harness_orientation(HARNESS_ORIENTATION_1);
157162

163+
#ifdef STM32H7
164+
gpio_sdmmc_init();
165+
if (sdmmc_reset() == sd_err_ok) {
166+
sd_replay_init();
167+
} else {
168+
print("SD card not detected\n");
169+
}
170+
#endif
171+
158172
// LED should keep on blinking all the time
159173
uint32_t cnt = 0;
160174
for (cnt=0;;cnt++) {
175+
#ifdef STM32H7
176+
if (sd_replay_state == SD_REPLAY_ACTIVE) {
177+
sd_replay_tick();
178+
continue;
179+
}
180+
#endif
161181
if (generated_can_traffic) {
162182
// fill up all the queues
163183
can_ring *qs[] = {&can_tx1_q, &can_tx2_q, &can_tx3_q};

board/jungle/main_comms.h

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,33 @@ int comms_control_handler(ControlPacket_t *req, uint8_t *resp) {
6868
case 0xa4:
6969
generated_can_traffic = (req->param1 > 0U);
7070
break;
71+
// **** 0xa5: SD card CAN replay control (param1: 0=stop, 1=start)
72+
case 0xa5:
73+
#ifdef STM32H7
74+
if (req->param1 == 1U) {
75+
sd_replay_start();
76+
} else {
77+
sd_replay_stop();
78+
}
79+
#endif
80+
break;
81+
// **** 0xa6: Get SD card replay status
82+
case 0xa6:
83+
#ifdef STM32H7
84+
{
85+
struct __attribute__((packed)) {
86+
uint8_t state;
87+
uint32_t total_records;
88+
uint32_t current_record;
89+
} status;
90+
status.state = (uint8_t)sd_replay_state;
91+
status.total_records = sd_replay_total_records;
92+
status.current_record = sd_replay_current_record;
93+
(void)memcpy(resp, &status, sizeof(status));
94+
resp_len = sizeof(status);
95+
}
96+
#endif
97+
break;
7198
// **** 0xa8: get microsecond timer
7299
case 0xa8:
73100
time = microsecond_timer_get();
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Provision a panda jungle v2 SD card for CAN replay.
4+
5+
Reads CAN messages from an openpilot rlog/qlog file and writes them to an
6+
SD card (or binary file) in the panda jungle raw replay format.
7+
8+
Usage:
9+
provision_sd.py <rlog_or_qlog> <sd_device_or_output_file>
10+
11+
Examples:
12+
# Write directly to an SD card block device (requires root/sudo on Linux):
13+
provision_sd.py /data/media/0/realdata/abc123--0/0/rlog /dev/sdb
14+
15+
# Write to a binary file, then dd to the SD card manually:
16+
provision_sd.py route.rlog replay.bin
17+
dd if=replay.bin of=/dev/sdb bs=512
18+
19+
SD card binary format written by this script:
20+
Sector 0 (512 bytes): header
21+
bytes 0-7: magic b"PNDREPLY"
22+
bytes 8-11: uint32 num_records
23+
bytes 12-15: uint32 record_size (= 20)
24+
bytes 16-19: uint32 format_version (= 1)
25+
bytes 20-511: zero-padded
26+
Sectors 1+: records (20 bytes each)
27+
uint32 mono_time_us - microseconds since replay start (relative)
28+
uint32 addr - CAN address
29+
uint8 bus - CAN bus number (0-2)
30+
uint8 len - data byte length (0-8)
31+
uint8 data[8] - CAN payload (zero-padded)
32+
uint16 pad - reserved (0)
33+
"""
34+
35+
import argparse
36+
import struct
37+
import sys
38+
39+
MAGIC = b"PNDREPLY"
40+
FORMAT_VERSION = 1
41+
RECORD_SIZE = 20
42+
SECTOR_SIZE = 512
43+
HEADER_SECTOR = 0
44+
DATA_START_SECTOR = 1
45+
46+
HEADER_FMT = "<8sIII" # magic, num_records, record_size, format_version
47+
RECORD_FMT = "<II BB 8s H" # mono_time_us, addr, bus, len, data[8], pad
48+
49+
50+
def parse_args():
51+
parser = argparse.ArgumentParser(description="Provision panda jungle v2 SD card for CAN replay")
52+
parser.add_argument("rlog", help="Path to openpilot rlog or qlog file")
53+
parser.add_argument("output", help="SD card block device (e.g. /dev/sdb) or output binary file")
54+
return parser.parse_args()
55+
56+
57+
def read_can_messages(rlog_path):
58+
"""Read CAN messages from an openpilot rlog/qlog file.
59+
60+
Returns list of (mono_time_ns, addr, bus, data) tuples sorted by time.
61+
"""
62+
try:
63+
from openpilot.tools.lib.logreader import LogReader
64+
except ImportError:
65+
try:
66+
from tools.lib.logreader import LogReader
67+
except ImportError:
68+
print("Error: could not import LogReader. Run from an openpilot checkout or install openpilot tools.", file=sys.stderr)
69+
sys.exit(1)
70+
71+
messages = []
72+
lr = LogReader(rlog_path)
73+
for msg in lr:
74+
if msg.which() == "sendcan":
75+
for can_msg in msg.sendcan:
76+
messages.append((msg.logMonoTime, can_msg.address, can_msg.src, bytes(can_msg.dat)))
77+
78+
if not messages:
79+
print("Error: no sendcan messages found in log file.", file=sys.stderr)
80+
sys.exit(1)
81+
82+
messages.sort(key=lambda m: m[0])
83+
return messages
84+
85+
86+
def build_records(messages):
87+
"""Convert (mono_time_ns, addr, bus, data) list to binary record bytes."""
88+
t0_ns = messages[0][0]
89+
records = bytearray()
90+
for mono_time_ns, addr, bus, data in messages:
91+
elapsed_us = (mono_time_ns - t0_ns) // 1000
92+
if elapsed_us > 0xFFFFFFFF:
93+
print(f"Warning: timestamp overflow at {elapsed_us} us, clamping to 32-bit max.", file=sys.stderr)
94+
elapsed_us = 0xFFFFFFFF
95+
96+
data_len = min(len(data), 8)
97+
padded_data = data[:data_len].ljust(8, b'\x00')
98+
99+
record = struct.pack(RECORD_FMT, elapsed_us, addr, bus & 0xFF, data_len, padded_data, 0)
100+
assert len(record) == RECORD_SIZE, f"record size mismatch: {len(record)}"
101+
records.extend(record)
102+
103+
return records
104+
105+
106+
def build_header(num_records):
107+
header_data = struct.pack(HEADER_FMT, MAGIC, num_records, RECORD_SIZE, FORMAT_VERSION)
108+
# Pad to one full sector
109+
return header_data + b'\x00' * (SECTOR_SIZE - len(header_data))
110+
111+
112+
def write_image(output_path, header, records):
113+
with open(output_path, 'wb') as f:
114+
f.write(header)
115+
f.write(records)
116+
# Pad final partial sector with zeros
117+
remainder = len(records) % SECTOR_SIZE
118+
if remainder != 0:
119+
f.write(b'\x00' * (SECTOR_SIZE - remainder))
120+
121+
122+
def main():
123+
args = parse_args()
124+
125+
print(f"Reading CAN messages from {args.rlog}...")
126+
messages = read_can_messages(args.rlog)
127+
print(f" Found {len(messages)} sendcan messages")
128+
129+
records = build_records(messages)
130+
num_records = len(messages)
131+
132+
duration_s = (messages[-1][0] - messages[-1][0]) // 1_000_000_000 if len(messages) > 1 else 0
133+
elapsed_us = (messages[-1][0] - messages[0][0]) // 1000
134+
duration_s = elapsed_us / 1_000_000
135+
136+
header = build_header(num_records)
137+
138+
total_bytes = len(header) + len(records)
139+
total_sectors = (total_bytes + SECTOR_SIZE - 1) // SECTOR_SIZE
140+
141+
print(f" Replay duration: {duration_s:.1f} seconds")
142+
print(f" Total records: {num_records}")
143+
print(f" SD image size: {total_sectors} sectors ({total_bytes / 1024:.1f} KB)")
144+
145+
print(f"Writing to {args.output}...")
146+
write_image(args.output, header, records)
147+
print("Done. Insert SD card into jungle v2 and call sd_replay_start() to begin replay.")
148+
149+
150+
if __name__ == "__main__":
151+
main()

0 commit comments

Comments
 (0)