Skip to content

Commit f77883a

Browse files
authored
Merge pull request #260 from iory/analyze-core-dump
Analyze core dump
2 parents 49b0317 + 564312f commit f77883a

15 files changed

+312
-0
lines changed

README.md

+38
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,44 @@ The `/atom_s3_button_state` topic publishes messages of type `std_msgs/Int32`. T
9292

9393
AtomS3 has multiple modes, which can be switched with a long click. You can get which mode AtomS3 is in by topic. The `/atom_s3_mode` topic publishes messages of type `std_msgs/String`. You can use `/atom_s3_mode` as well as `/atom_s3_button_state` to define robot operations.
9494

95+
##### Analyze Core Dump
96+
97+
Riberry provides a tool to read and analyze core dumps from supported devices (e.g., M5Stack-Basic, Atom S3). `riberry-analyze-core-dump` allows you to retrieve
98+
core dump information and generate a clickable GitHub issue link for reporting.
99+
100+
Run the script to analyze a core dump:
101+
102+
```bash
103+
riberry-analyze-core-dump
104+
```
105+
106+
You can optionally specify an ELF file for additional debugging information:
107+
108+
```bash
109+
riberry-analyze-core-dump --elf-path /path/to/firmware.elf
110+
```
111+
112+
Here’s an example of what the tool outputs when run on a Radxa Zero:
113+
114+
```
115+
Core dumped Firmware version: 20df8eb
116+
LCD rotation: 1
117+
Use Grove: 0
118+
Device: Radxa Zero
119+
Communication: I2CBase
120+
121+
PC : 0x4209c205 PS : 0x00060030 A0 : 0x820092bc A1 : 0x3fcab140
122+
A2 : 0x3fc9b2f0 A3 : 0x3fc9b410 A4 : 0x3fc9b380 A5 : 0x00000004
123+
A6 : 0x3fcedf28 A7 : 0x80000001 A8 : 0x00000000 A9 : 0x3fcf5720
124+
A10 : 0x00060023 A11 : 0x00000003 A12 : 0x00060023 A13 : 0x80000000
125+
A14 : 0x00000000 A15 : 0x00ffffff SAR : 0x00000000 EXCCAUSE: 0x0000001d
126+
EXCVADDR: 0x00000000 LBEG : 0x00000000
127+
128+
Click here to create a GitHub issue
129+
```
130+
131+
Feel free to create a GitHub issue.
132+
95133
## Distribute radxa image as SD card
96134

97135
When distributing images, it's necessary to adjust the disk size among other parameters initially due to writing to an SD card.

firmware/atom_s3_i2c_display/lib/com/communication_base.cpp

+8
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ uint8_t CommunicationBase::buffer[256];
1111
uint8_t CommunicationBase::requestBytes[100];
1212
uint8_t CommunicationBase::forcedMode;
1313
uint8_t CommunicationBase::selectedModesBytes[100];
14+
core_dump_regs_t CommunicationBase::regs;
1415
Role CommunicationBase::role;
1516
bool CommunicationBase::pairingEnabled = false;
1617
bool CommunicationBase::_stopStream = false;
@@ -230,6 +231,13 @@ void CommunicationBase::processPacket(const String& str, int offset) {
230231
instance->pairing.setDataToSend(dataToSend);
231232
break;
232233
}
234+
case CORE_DUMP_DATA_REQUEST: {
235+
if (esp_reset_reason() == ESP_RST_PANIC && regs.core_dumped == 0) {
236+
parse_core_dump_simple(&regs);
237+
}
238+
write(regs, sizeof(regs));
239+
break;
240+
}
233241
case FIRMWARE_VERSION_REQUEST: {
234242
_stream->flush();
235243
String version = VERSION + String("_") + String(LCD_ROTATION) + String("_") +

firmware/atom_s3_i2c_display/lib/com/communication_base.h

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include <button_manager.h>
77
#include <primitive_lcd.h>
88

9+
#include "core_dump_utils.h"
910
#include "execution_timer.h"
1011
#include "packet.h"
1112
#include "pairing.h"
@@ -54,6 +55,8 @@ class CommunicationBase : public ExecutionTimer {
5455
static uint8_t buffer[256];
5556
static bool _stopStream;
5657

58+
static core_dump_regs_t regs;
59+
5760
bool receiveEventEnabled;
5861
static CommunicationBase* instance;
5962
PrimitiveLCD& lcd;

firmware/atom_s3_i2c_display/lib/com/packet.h

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ enum PacketType : uint8_t {
1515
SET_IP_REQUEST = 0x13,
1616
SPEECH_TO_TEXT_MODE = 0x14,
1717

18+
CORE_DUMP_DATA_REQUEST = 0xFC,
1819
FIRMWARE_VERSION_REQUEST = 0xFD,
1920
FIRMWARE_UPDATE_MODE = 0xFF,
2021
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#ifndef CORE_DUMP_UTILS_H
2+
#define CORE_DUMP_UTILS_H
3+
4+
#include <Arduino.h>
5+
#include <esp_core_dump.h>
6+
#include <esp_log.h>
7+
#include <esp_partition.h>
8+
9+
typedef struct {
10+
uint32_t core_dumped;
11+
uint32_t pc; // Program Counter
12+
uint32_t ps; // Processor State
13+
uint32_t a[16]; // A0-A15 registers
14+
uint32_t sar; // Shift Amount Register
15+
uint32_t exccause; // Exception Cause
16+
uint32_t excvaddr; // Exception Virtual Address
17+
uint32_t lbeg; // Loop Begin
18+
uint32_t lend; // Loop End
19+
uint32_t lcount; // Loop Count
20+
} core_dump_regs_t;
21+
22+
inline esp_err_t parse_core_dump_simple(core_dump_regs_t* regs) {
23+
size_t dump_addr;
24+
size_t dump_size;
25+
regs->core_dumped = 0;
26+
27+
// Check if a core dump exists
28+
if (esp_core_dump_image_get(&dump_addr, &dump_size) != ESP_OK) {
29+
return ESP_FAIL;
30+
}
31+
32+
uint8_t buf[512];
33+
size_t offset = 0;
34+
const uint32_t deadbeef_marker = 0xdeadbeef;
35+
36+
const esp_partition_t* coredump_part = esp_partition_find_first(
37+
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_COREDUMP, NULL);
38+
39+
bool found = false;
40+
while (found == false && offset < dump_size) {
41+
size_t read_size = min(sizeof(buf), dump_size - offset);
42+
if (esp_partition_read(coredump_part, offset, buf, read_size) != ESP_OK) {
43+
break;
44+
}
45+
// Find deadbeef marker
46+
for (size_t i = 0; i < read_size - 4; i++) {
47+
if (*(uint32_t*)(buf + i) == deadbeef_marker) {
48+
if (i + 4 + 72 <= read_size) {
49+
const uint8_t* reg_data = buf + i + 4;
50+
regs->pc = *(uint32_t*)(reg_data);
51+
regs->ps = *(uint32_t*)(reg_data + 4);
52+
for (int j = 0; j < 16; j++) { // a0-a15 (64バイト)
53+
regs->a[j] = *(uint32_t*)(reg_data + 8 + j * 4);
54+
}
55+
regs->sar = *(uint32_t*)(reg_data + 72);
56+
regs->exccause = *(uint32_t*)(reg_data + 76);
57+
regs->excvaddr = *(uint32_t*)(reg_data + 80);
58+
regs->lbeg = *(uint32_t*)(reg_data + 84);
59+
regs->lend = *(uint32_t*)(reg_data + 88);
60+
regs->lcount = *(uint32_t*)(reg_data + 92);
61+
regs->core_dumped = 1;
62+
found = true;
63+
} else {
64+
offset = offset + i; // adjust to deadbeef
65+
break;
66+
}
67+
}
68+
}
69+
offset += read_size;
70+
}
71+
// Erase the core dump after processing
72+
esp_core_dump_image_erase();
73+
return ESP_OK;
74+
}
75+
76+
#endif // CORE_DUMP_UTILS_H

firmware/atom_s3_i2c_display/partition_table/partitions_m5stack_atoms3.csv

+1
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ otadata, data, ota, 0xd000, 0x2000,
44
phy_init, data, phy, 0xf000, 0x1000,
55
ota_0, app, ota_0, 0x10000, 1M,
66
ota_1, app, ota_1, 0x110000, 1M,
7+
coredump, data, coredump, 0x210000, 64K,

pyproject.toml

+3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ classifiers = [
2727
[tool.setuptools]
2828
packages = { find = {} }
2929

30+
[project.scripts]
31+
riberry-analyze-core-dump = "riberry.apps.riberry_analyze_core_dump:main"
32+
3033
[tool.ruff]
3134
target-version = "py38"
3235
line-length = 90

requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ colorama
55
GitPython
66
filelock
77
i2c-for-esp32
8+
platformio
89
pybsc
910
pyserialtransfer==2.1.6
1011
smbus2

riberry/apps/__init__.py

Whitespace-only changes.
+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/usr/bin/env python3
2+
3+
import argparse
4+
5+
from riberry.com.base import ComBase
6+
from riberry.com.i2c_base import I2CBase
7+
from riberry.com.uart_base import UARTBase
8+
from riberry.platformio.core_dump import read_core_dump
9+
10+
11+
def main():
12+
parser = argparse.ArgumentParser(description='Read core dump from device.')
13+
parser.add_argument('--elf-path', '-e', type=str, default=None,
14+
help='Path to the ELF file')
15+
args = parser.parse_args()
16+
17+
device = ComBase.identify_device()
18+
if device in ['m5stack-LLM', 'Linux', 'Darwin']:
19+
com = UARTBase()
20+
else:
21+
com = I2CBase(0x42)
22+
23+
read_core_dump(com, elf_path=args.elf_path)
24+
25+
if __name__ == "__main__":
26+
main()

riberry/com/base.py

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class PacketType(IntEnum):
2424
SET_IP_REQUEST = 0x13
2525
SPEECH_TO_TEXT_MODE = 0x14
2626

27+
CORE_DUMP_DATA_REQUEST = 0xFC
2728
FIRMWARE_VERSION_REQUEST = 0xFD
2829
FIRMWARE_UPDATE_MODE = 0xFF
2930

riberry/git_utils.py

+12
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import urllib
23

34
import git
45

@@ -57,3 +58,14 @@ def update_repository_with_safe_stash_apply(repo_path):
5758
return False
5859

5960
return True
61+
62+
63+
def generate_github_issue_url(repo_owner, repo_name, title, body):
64+
"""Generate a GitHub issue URL with pre-filled title and body."""
65+
base_url = f"https://github.com/{repo_owner}/{repo_name}/issues/new"
66+
params = {
67+
"title": title,
68+
"body": body
69+
}
70+
query_string = urllib.parse.urlencode(params)
71+
return f"{base_url}?{query_string}"

riberry/platformio/__init__.py

Whitespace-only changes.

riberry/platformio/core_dump.py

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import struct
2+
import subprocess
3+
import tempfile
4+
import time
5+
6+
7+
def format_core_dump(data, elf_path=None):
8+
core_dumped = struct.unpack('<I', data[0:4])[0]
9+
pc = struct.unpack('<I', data[4:8])[0]
10+
ps = struct.unpack('<I', data[8:12])[0]
11+
a_regs = struct.unpack('<16I', data[12:76])
12+
sar = struct.unpack('<I', data[76:80])[0]
13+
exccause = struct.unpack('<I', data[80:84])[0]
14+
excvaddr = struct.unpack('<I', data[84:88])[0]
15+
lbeg = struct.unpack('<I', data[88:92])[0]
16+
17+
if core_dumped == 0:
18+
return "No core dump available"
19+
output = (
20+
f"PC : 0x{pc:08x} PS : 0x{ps:08x} "
21+
+ f"A0 : 0x{a_regs[0]:08x} A1 : 0x{a_regs[1]:08x}\n"
22+
+ f"A2 : 0x{a_regs[2]:08x} A3 : 0x{a_regs[3]:08x} "
23+
+ f"A4 : 0x{a_regs[4]:08x} A5 : 0x{a_regs[5]:08x}\n"
24+
+ f"A6 : 0x{a_regs[6]:08x} A7 : 0x{a_regs[7]:08x} "
25+
+ f"A8 : 0x{a_regs[8]:08x} A9 : 0x{a_regs[9]:08x}\n"
26+
+ f"A10 : 0x{a_regs[10]:08x} A11 : 0x{a_regs[11]:08x} "
27+
+ f"A12 : 0x{a_regs[12]:08x} A13 : 0x{a_regs[13]:08x}\n"
28+
+ f"A14 : 0x{a_regs[14]:08x} A15 : 0x{a_regs[15]:08x} "
29+
+ f"SAR : 0x{sar:08x} EXCCAUSE: 0x{exccause:08x}\n"
30+
+ f"EXCVADDR: 0x{excvaddr:08x} LBEG : 0x{lbeg:08x} "
31+
+ '\n'
32+
)
33+
34+
if elf_path is not None:
35+
from riberry.platformio.toolchain import ensure_toolchain
36+
from riberry.platformio.toolchain import get_addr2line_path
37+
ensure_toolchain()
38+
addr2line_path = get_addr2line_path()
39+
addr2line_cmd = [str(addr2line_path), "-e", str(elf_path), '-a', '-pfiaC', str(hex(pc))]
40+
output += '\n'
41+
output += 'Executing addr2line command:\n'
42+
output += ' '.join(addr2line_cmd) + '\n'
43+
output += subprocess.check_output(
44+
addr2line_cmd, stderr=subprocess.DEVNULL
45+
).decode()
46+
return output
47+
48+
49+
def read_core_dump(com, elf_path=None, retry_count=5,
50+
repo_owner="iory", repo_name="riberry"):
51+
formatted_output = ""
52+
for _ in range(retry_count):
53+
with com.lock_context():
54+
com.write([0xFD])
55+
time.sleep(0.01)
56+
version = com.read().decode().split('_')
57+
if len(version) >= 3:
58+
import riberry
59+
from riberry.firmware_update import download_firmware_from_github
60+
riberry_git_version, lcd_rotation, use_grove = version[:3]
61+
formatted_output += f"Core dumped Firmware version: {riberry_git_version}\n"
62+
formatted_output += f"LCD rotation: {lcd_rotation}\n"
63+
formatted_output += f"Use Grove: {use_grove}\n"
64+
65+
if elf_path is None:
66+
model = com.device_type
67+
if model == 'm5stack-LLM':
68+
device_name = 'm5stack-basic'
69+
elif "Radxa" in model or "ROCK Pi" in model \
70+
or model == "Khadas VIM4" \
71+
or model == "NVIDIA Jetson Xavier NX Developer Kit":
72+
device_name = 'm5stack-atoms3'
73+
else:
74+
raise NotImplementedError(f"Not supported device {model}. Please feel free to add the device name to the list or ask the developer to add it.")
75+
76+
url = f'https://github.com/iory/riberry/releases/download/v{riberry.__version__}-{riberry_git_version}/{device_name}-lcd{lcd_rotation}-grove{use_grove}.elf'
77+
temp_file = tempfile.NamedTemporaryFile(suffix=".elf", delete=True)
78+
print(f"Downloading firmware elf from {url} to temporary file {temp_file.name}...")
79+
try:
80+
elf_path = download_firmware_from_github(url, temp_file)
81+
except Exception as e:
82+
print(f"Failed to download firmware: {e}")
83+
break
84+
time.sleep(0.1)
85+
if formatted_output == "":
86+
formatted_output += "Failed to read core dump version\n"
87+
formatted_output += f"Device: {com.device_type}\n"
88+
formatted_output += f"Communication: {com.__class__.__name__}\n\n"
89+
90+
for _ in range(retry_count):
91+
success = False
92+
with com.lock_context():
93+
com.write([0xFC])
94+
time.sleep(0.01)
95+
response = com.read()
96+
if len(response) > 0:
97+
try:
98+
formatted_output += format_core_dump(response,
99+
elf_path=elf_path)
100+
success = True
101+
except Exception as e:
102+
print(f"Failed to format core dump: {e}")
103+
if success:
104+
break
105+
time.sleep(0.1)
106+
if success:
107+
from riberry.git_utils import generate_github_issue_url
108+
print(formatted_output)
109+
110+
# Generate GitHub issue URL
111+
issue_title = f"Core Dump Report - Firmware {version}"
112+
message = "I have encountered a core dump while running the firmware."
113+
issue_body = f"{message}\n```\n{formatted_output}\n```"
114+
issue_url = generate_github_issue_url(repo_owner, repo_name, issue_title, issue_body)
115+
116+
clickable_url = f"\033]8;;{issue_url}\033\\Click here to create a GitHub issue\033]8;;\033\\"
117+
print(clickable_url)
118+
print(f"If not clickable, use this URL:\n{issue_url}")

riberry/platformio/toolchain.py

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import os
2+
import subprocess
3+
4+
5+
def get_addr2line_path():
6+
toolchain_dir = os.path.expanduser("~/.platformio/packages/toolchain-xtensa-esp32s3")
7+
addr2line_path = os.path.join(toolchain_dir, "bin", "xtensa-esp32s3-elf-addr2line")
8+
return addr2line_path
9+
10+
11+
def ensure_toolchain():
12+
addr2line_path = get_addr2line_path()
13+
if not os.path.exists(addr2line_path):
14+
print(f"Toolchain not found at {addr2line_path}. Installing espressif32 platform...")
15+
try:
16+
subprocess.run(["pio", "pkg", "install", "-g", "-t", "toolchain-xtensa-esp32s3"], check=True)
17+
print("Espressif32 platform (including toolchain) installed successfully.")
18+
except subprocess.CalledProcessError as e:
19+
print(f"Failed to install platform: {e}")
20+
return False
21+
if not os.path.exists(addr2line_path):
22+
print("Toolchain still not found after installation. Check PlatformIO setup.")
23+
return False
24+
return True

0 commit comments

Comments
 (0)