Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/2026-11-08-crash-names-module.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Octatrack base: a crash record now names the installed modules behind it. Before the restart, the base looks the crash address and the eight stack words up in its own table of installed modules. At the next start, the crash in the unit's log carries the module at the crash address and the ones it was called through, so shared diagnostics and `npm run diagnostics:pull` say "crash pc … in Mini Verb, stack through …" instead of a bare address. The lookup reads only the base's own table, never module memory, and runs on its own stack. If the lookup itself fails, the unit restarts without the names.
2 changes: 1 addition & 1 deletion docs/OCTATRACK_DRIVING.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ and stock modifier release, and stop on a mismatch.
| effect ids and chooser rows | FX1_IDS `0x400d5f58`, FX2_IDS `0x400d5fdc` (id -> descriptor, name at +9), FX1_ID2POS `0x400d60d0`, ID2POS `0x400d6150` (id -> row) |
| chooser cursor | FX1 `0x460d5c94`, FX2 `0x460d5ca8` |
| input layers (menus, windows) | list at `0x460d165c`: two entries with no menu open |
| crash record, stall record | `0x100ff800`, `0x100ff900` (battery RAM) |
| crash record, the modules behind it, stall record | `0x100ff800`, `0x100ff8b4`, `0x100ff900` (battery RAM) |

To compare a Part's working and saved copies: `mem:` both addresses with length 6322 and compare the output.

Expand Down
Binary file not shown.
4 changes: 3 additions & 1 deletion scripts/device.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -444,13 +444,15 @@ async function run(sequence, until = Infinity) { // serve: past its time limit a
}
}
else if (step === 'crash') { // crash.c's record in battery RAM: what the last error exception was, and where
const b = await mem(0x100ff800, 180), v = new DataView(b.buffer, b.byteOffset, 180), w = i => v.getUint32(4 * i)
const b = await mem(0x100ff800, 224), v = new DataView(b.buffer, b.byteOffset, 224), w = i => v.getUint32(4 * i) // then its owners ("MWCO")
if (w(0) !== 0x4d574352) console.log('crash: none recorded')
else console.log(`crash ${w(1)} (shown ${w(28)}): vector ${w(2) >>> 18 & 0xff} PC 0x${w(3).toString(16)} SR 0x${(w(2) & 0xffff).toString(16)} ` +
`FS ${(w(2) >>> 24 & 0xc) | (w(2) >>> 16 & 3)} SP 0x${w(19).toString(16)}\n d ${[...Array(8)].map((_, i) => w(4 + i).toString(16)).join(' ')}\n` +
` a ${[...Array(7)].map((_, i) => w(12 + i).toString(16)).join(' ')}\n stack ${[...Array(8)].map((_, i) => w(20 + i).toString(16)).join(' ')}\n` +
` code at PC ${[...Array(8)].map((_, i) => w(29 + i).toString(16).padStart(8, '0')).join(' ')}\n` +
` uncached ${[...Array(8)].map((_, i) => w(37 + i).toString(16).padStart(8, '0')).join(' ')}`)
if (w(0) === 0x4d574352 && w(45) === 0x4d57434f && w(46) === w(1)) // crash.c named the modules behind it (0: stock or none)
console.log(` modules: PC ${w(47).toString(16).padStart(8, '0')}, stack ${[...Array(8)].map((_, i) => w(48 + i).toString(16).padStart(8, '0')).join(' ')}`)
}
else if (step === 'loader') { // packets each core took, the jobs in flight, the manager's transactions (report words)
const w = await statusWords()
Expand Down
10 changes: 8 additions & 2 deletions scripts/diagnostics-pull.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,18 @@ function percent(permille) { return (permille / 10).toFixed(1) + '%' }
const REFUSALS = Object.assign(['no refusal', 'malformed', 'module memory', 'conflict', 'no free handle', 'busy', 'DSP time', 'in use', 'stock bytes differ', 'a base event', 'a base feature down'],
{ 0x100: 'the last start crashed or froze loading it' }) // modset.c's boot guard, past the loader's codes
/** An incident from the unit's own log (diag.c): when in its session, what, and what the unit was doing. */
const ids = r => [...r.machines, ...r.fx, ...r.keys].slice(0, 4 * r.a).map((b, i, all) => i % 4 ? '' : all.slice(i, i + 4).map(x => x.toString(16).padStart(2, '0')).join('')).filter(Boolean)
const ids = (r, n = r.a) => [...r.machines, ...r.fx, ...r.keys].slice(0, 4 * n).map((b, i, all) => i % 4 ? '' : all.slice(i, i + 4).map(x => x.toString(16).padStart(2, '0')).join('')).filter(Boolean)
const STATELESS = ['crash', 'stall', 'set', 'base'] // records that carry no cpu, keys or screen
const moduleName = id => MODULE_NAMES.get(id) ?? id
/** crash.c's names (diag.c): the module behind the PC in screen, the stack's in the id run; an older base's names none. */
const crashModules = r => {
const stack = ids(r, 14).filter(id => id !== '00000000' && id !== 'ffffffff').map(moduleName)
return (r.screen ? ' in ' + moduleName(r.screen.toString(16).padStart(8, '0')) : '') + (stack.length ? ', stack through ' + stack.join(', ') : '')
}
const incident = r => `#${r.seq} start ${r.boot} +${Math.floor(r.second / 60)}m${r.second % 60}s ${r.kind}` +
(r.kind === 'base' ? ` ${r.a.toString(16).padStart(8, '0')}` : r.kind === 'set' ? ` ${r.a} installed: ${ids(r).map(id => MODULE_NAMES.get(id) ?? id).join(', ') || 'none'}` +
(r.tempo24 === 0x101 ? '; all held: the last start crashed or froze' : r.screen ? `; refused ${MODULE_NAMES.get(r.screen.toString(16).padStart(8, '0')) ?? r.screen.toString(16).padStart(8, '0')} (${REFUSALS[r.tempo24] ?? 'refusal ' + r.tempo24})` : '') : '') +
(r.kind === 'freeze' ? (r.a ? ` ${(r.a * 16 / 44100).toFixed(1)} s` : ' until a restart') : r.kind === 'hot' ? ` (${r.a} half-seconds at 90 %+)` : r.kind === 'crash' ? ` pc 0x${r.a.toString(16)}` : r.kind === 'stall' ? ` reason ${r.a}` : r.kind === 'shed' ? ` ${r.a & 0xffffff} renders skipped, tracks ${[...Array(8).keys()].filter(t => r.a >>> 24 >> t & 1).map(t => t + 1).join(',')}` : '') +
(r.kind === 'freeze' ? (r.a ? ` ${(r.a * 16 / 44100).toFixed(1)} s` : ' until a restart') : r.kind === 'hot' ? ` (${r.a} half-seconds at 90 %+)` : r.kind === 'crash' ? ` pc 0x${r.a.toString(16)}${crashModules(r)}` : r.kind === 'stall' ? ` reason ${r.a}` : r.kind === 'shed' ? ` ${r.a & 0xffffff} renders skipped, tracks ${[...Array(8).keys()].filter(t => r.a >>> 24 >> t & 1).map(t => t + 1).join(',')}` : '') +
(STATELESS.includes(r.kind) ? '' : `, cpu ${percent(r.cpu)} T1-4 ${percent(r.dsp1to4)} T5-8 ${percent(r.dsp5to8)}, ${r.playing ? 'playing' : 'stopped'} at ${(r.tempo24 / 24).toFixed(1)} BPM, screen 0x${r.screen.toString(16)}` +
`, keys ${r.keys.filter(k => k !== 255).map(k => k.toString(16)).join(' ') || '-'}, fx ${r.fx.map(f => f.toString(16)).join(' ')}, machines ${r.machines.join(' ')}`)

Expand Down
6 changes: 6 additions & 0 deletions scripts/diagnostics-pull.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ describe('diagnostics pull', () => {
fx: Array(16).fill(0), keys: Array(16).fill(0), screen: 0x773c84aa, tempo24: 5, playing: false }
expect(unitLines([set])[0]).toContain('1 installed: Mini Verb; refused SC Ringmod (busy)')
})
it('names the modules behind a crash, and none for an older base', () => {
const crash = (screen, machines, keys) => ({ seq: 5, boot: 4, second: 0, kind: 'crash', a: 0x40d30010, cpu: 0, dsp1to4: 0, dsp5to8: 0, machines, fx: Array(16).fill(0), keys, screen, tempo24: 0, playing: false })
expect(unitLines([crash(0x3cbedc9c, [0x77, 0x3c, 0x84, 0xaa, ...Array(20).fill(0)], Array(16).fill(0))])[0])
.toBe(' unit: #5 start 4 +0m0s crash pc 0x40d30010 in Mini Verb, stack through SC Ringmod')
expect(unitLines([crash(0, Array(24).fill(0), Array(16).fill(255))])[0]).toBe(' unit: #5 start 4 +0m0s crash pc 0x40d30010')
})
it('calls a stall found at a start on another base an OS upgrade, and one on the same base nothing', () => {
const at = (seq, boot, kind, a = 0) => ({ seq, boot, second: 0, kind, a, cpu: 0, dsp1to4: 0, dsp5to8: 0, machines: Array(24).fill(0),
fx: Array(16).fill(0), keys: Array(16).fill(255), screen: 0, tempo24: 0, playing: false })
Expand Down
24 changes: 12 additions & 12 deletions sdk/machines/octatrack/base-binding.json
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
{
"schema": 1,
"identity": "9b3ba41715140d5fae07868a8b2adc6d89c3ec0272d8ab34c332461cf990a14c",
"arena": 1089508832,
"pool": 1087548400,
"identity": "4189017df1c1ae910a3736da7a38cd2e1cb7ffe505dd2220efd1da14a2acbf96",
"arena": 1089514976,
"pool": 1087549424,
"symbols": {
"modwerk_controller_write_begin": 1084872232,
"modwerk_controller_write_end": 1084872260,
"modwerk_cue_cfg_clamp": 1085077496,
"modwerk_raise_ev_mute_keep": 1085079850
"modwerk_cue_cfg_clamp": 1085078264,
"modwerk_raise_ev_mute_keep": 1085080618
},
"controllers": {
"sidechain-compressor": "178d200073896dd2416f07bb952f835087f9250778e773fc9426a5165db259ce",
"output-matrix": "17e9afe714d2707147804360bbf4e614240d9d5afc037df5fec0d2a1fc56c3e1",
"analog-bassdrum": "579b9b5e76b000a8b9c95f6131a547fbb5117f497d4bc3ee0f98385edc96e5a3"
},
"variants": {
"tracks-main-cue": "9b3ba41715140d5fae07868a8b2adc6d89c3ec0272d8ab34c332461cf990a14c",
"tracks": "810c7051a4049e8df8a9c9b9e86258ab2eae083a71afd3a29b75ec9295d6bef8",
"tracks-post": "9edadde680c9212d37e7db6ed9944a9b48e867beded71f94c8e1a94d8500edd3",
"main-cue": "19b38199cb83e72c518cab74f8ee021ec0c2d82cd8675cb0a66e8273fc5d16cc",
"main": "fe9de90eb189e553c7a4ef6be13e8c40a60bbe805a7078adf947a62e73e6a4b8",
"master": "23c62390d969045fcc248cb75c98c335abc51fec488d6b53d39d71cb64e4491c",
"off": "cdbbde238cdd77e1ba92bc8728a8450fa63364c0305654c4cdf1448f25496f2d"
"tracks-main-cue": "4189017df1c1ae910a3736da7a38cd2e1cb7ffe505dd2220efd1da14a2acbf96",
"tracks": "9cb11bdfd42786b86533a0cd912951846176c22208f6ebb7779f114a79b59944",
"tracks-post": "7b042ceaa7fb49b836db6a7b2d980f09b413c15a849d517cdc35b6fe974a40f3",
"main-cue": "f3f86292c03ed8c457478c3ba2b92e664c827ef946d0ae612f267ba4fe0c1d82",
"main": "4bed870f70d337be081b3ed9b1a92ac876668f97c8aba8ea6cd464f201311956",
"master": "ad3d5b78693397578f14d600f0705bebbad9f8a09a51d9fcc90c3bb9bddf74b6",
"off": "751ed57dfd4fb3489eec81822911a6f51f4a9483c6137ac270261d9d2b0eb793"
}
}
39 changes: 37 additions & 2 deletions sdk/machines/octatrack/elekloader/crash.c
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* SPDX-License-Identifier: GPL-3.0-or-later
* Crash recovery (owner, 11 October 2026: an exception should not need a power cycle). crash.s takes the error
* vectors, records the crash in battery RAM and restarts the unit; after the restart the base says so once, and
* the record stays for a host to read. */
* vectors, records the crash in battery RAM, names the modules behind it and restarts the unit; after the restart
* the base says so once, and the record stays for a host to read. */
#include <stdint.h>
#ifdef MODWERK_DEV
void modwerk_cost_stamp(unsigned k); /* diag.c: the development profile of the base's hooks */
Expand All @@ -11,13 +11,46 @@ void modwerk_cost_stamp(unsigned k); /* diag.c: the development profile of the b
#endif

struct crash { uint32_t magic, count, frame, pc, d[8], a[7], sp, stack[8], seen, code[8], memory[8]; }; /* code: 32 bytes at the PC, read cached; memory: the same through the uncached alias (crash.s) */
/* Right after the record, below dsp.c's at 0x100ff900: the installed modules whose memory holds the PC and each of
* the eight stack longs (0: stock code or none), for the record of that count. diag.c puts them in the CRASH incident. */
struct crash_owners { uint32_t magic, count, pc, stack[8]; };
#ifdef MODWERK_HOST /* test_crash.c models battery RAM */
extern volatile uint32_t modwerk_test_crash[(sizeof(struct crash) + sizeof(struct crash_owners)) / 4u];
#define REC ((volatile struct crash *)modwerk_test_crash)
#else
#define REC ((volatile struct crash *)0x100ff800u)
#endif
#define OWNERS ((volatile struct crash_owners *)(REC + 1))
#define MAGIC 0x4d574352u /* "MWCR" */
#define OWNERS_MAGIC 0x4d57434fu /* "MWCO" */
#define OWNERS_BUSY 0x4d574342u /* "MWCB": set while the lookup runs */
#define VECTORS ((volatile uint32_t *)0x40000000u) /* the vector base (0x400b9668 holds it) */
#define POPUP ((void (*)(const char *, unsigned))0x4005a2b8u)

void mw_crash(void);
uint32_t modwerk_runtime_owner_module(uintptr_t address); /* loader.c */
volatile uint32_t modwerk_crash_test; /* dev.c: crash on the next tick, to test this */
uint32_t mw_crash_stack[64]; /* crash.s runs modwerk_crash_owners on it: the stack that faulted may be what broke */

/* A module's code may run from either SDRAM alias (runtime.c), so the other one is asked too. */
static uint32_t owner_of(uint32_t address)
{
uint32_t id = modwerk_runtime_owner_module(address);
if (!id && address - 0x40000000u < 0x01000000u) id = modwerk_runtime_owner_module(address + 0x08000000u);
if (!id && address - 0x48000000u < 0x01000000u) id = modwerk_runtime_owner_module(address - 0x08000000u);
return id;
}
/* crash.s, interrupts masked, after the record: reads only the base's owner records. Should the lookup itself fault,
* crash.s comes back here for the next count and restarts without it, instead of faulting again. */
void modwerk_crash_owners(void);
void modwerk_crash_owners(void)
{
if (OWNERS->magic == OWNERS_BUSY && OWNERS->count + 1u == REC->count) { OWNERS->magic = 0; return; }
OWNERS->magic = OWNERS_BUSY, OWNERS->count = REC->count;
OWNERS->pc = owner_of(REC->pc);
for (unsigned i = 0; i < 8u; ++i) OWNERS->stack[i] = owner_of(REC->stack[i]);
OWNERS->magic = OWNERS_MAGIC;
}

/* Sys task, ev_tick (60 Hz). Access and address errors, illegal, unimplemented (line F) and unsupported
* instructions, privilege and format errors: the exceptions stock code never takes on purpose. */
Expand All @@ -33,8 +66,10 @@ void modwerk_crash_tick(void)
REC->seen = REC->count; /* once a crash, 4 s after the start, once the UI runs */
POPUP("CRASH RECOVERED", 0xa0);
}
#ifndef MODWERK_HOST
if (modwerk_crash_test) {
modwerk_crash_test = 0;
__asm__ volatile("illegal");
}
#endif
}
2 changes: 2 additions & 0 deletions sdk/machines/octatrack/elekloader/crash.s
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,6 @@ mw_crash:
clr.l (112,%a0)
2: addq.l #1,(4,%a0)
move.l %d0,(%a0)
lea (mw_crash_stack+256).l,%sp | crash.c names the modules behind the PC and the stack longs, on its
jsr (modwerk_crash_owners).l | own stack (the faulting one may be what broke)
jmp (modwerk_boot_reset).l
29 changes: 27 additions & 2 deletions sdk/machines/octatrack/elekloader/diag.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ extern const uint8_t modwerk_base_digest[32]; /* the base's identity (buil

#ifdef MODWERK_HOST /* test_diag.c models the unit's memory */
extern volatile struct diag_log modwerk_test_log;
extern volatile uint32_t modwerk_test_crash[DIAG_CRASH_BYTES / 4u], modwerk_test_stall[DIAG_STALL_BYTES / 4u], modwerk_test_playing, modwerk_test_tempo24,
extern volatile uint32_t modwerk_test_crash[DIAG_CRASH_BYTES / 4u + 11u], modwerk_test_stall[DIAG_STALL_BYTES / 4u], modwerk_test_playing, modwerk_test_tempo24,
modwerk_test_screen, modwerk_test_timer;
extern volatile uint8_t modwerk_test_running[16];
#define LOG (&modwerk_test_log)
Expand Down Expand Up @@ -91,6 +91,8 @@ static void bare(volatile struct diag_incident *r, unsigned kind)
r->kind = (uint16_t)kind;
for (unsigned i = 0; i < 16u; ++i) r->keys[i] = 0xffu;
}
/* crash.c's owners, right after its record: "MWCO", the record's count, the module behind the PC, the eight stack longs'. */
#define CRASH_OWNERS (CRASH + DIAG_CRASH_BYTES / 4u)
/* The next incident: its seq is 0 while it is written and set last, so a reader never takes half of one. */
static volatile struct diag_incident *slot(volatile struct diag_log *l)
{
Expand All @@ -111,6 +113,29 @@ static void add(volatile struct diag_log *l, unsigned kind, uint32_t a, int with
r->boot = l->boot, r->second = ticks / 60u, r->a = a;
push(l, r);
}
/* A crash: its PC, and when crash.c named them for this record, the module behind the PC (screen) and the distinct ones
* behind the stack longs (the 56-byte run, as a set record's ids). A crash an older base recorded keeps screen 0 and
* the keys at 0xff. */
static void add_crash(volatile struct diag_log *l)
{
volatile struct diag_incident *r = slot(l);
bare(r, DIAG_CRASH);
r->boot = l->boot, r->second = ticks / 60u, r->a = CRASH[3];
if (CRASH_OWNERS[0] == 0x4d57434fu && CRASH_OWNERS[1] == CRASH[1]) {
uint32_t ids[8];
unsigned n = 0;
for (unsigned i = 0; i < 8u; ++i) {
uint32_t id = CRASH_OWNERS[3u + i];
unsigned j = 0;
while (j < n && ids[j] != id) ++j;
if (id && j == n) ids[n++] = id;
}
r->screen = CRASH_OWNERS[2];
volatile uint8_t *b = r->machine;
for (unsigned i = 0; i < 56u; ++i) b[i] = i < 4u * n ? (uint8_t)(ids[i / 4u] >> (24u - 8u * (i % 4u))) : 0;
}
push(l, r);
}
/* The frame interrupt's freeze, into the incidents (it keeps its own boot and second). */
static void add_freeze(volatile struct diag_log *l)
{
Expand All @@ -132,7 +157,7 @@ static void start(volatile struct diag_log *l)
l->boot = l->boot + 1u;
add(l, DIAG_START, crashes << 16 | (stalls & 0xffffu), 1);
add(l, DIAG_BASE, (uint32_t)modwerk_base_digest[0] << 24 | (uint32_t)modwerk_base_digest[1] << 16 | (uint32_t)modwerk_base_digest[2] << 8 | modwerk_base_digest[3], 0);
if (crashes != l->crash_seen) add(l, DIAG_CRASH, CRASH[3], 0);
if (crashes != l->crash_seen) add_crash(l);
if (stalls != l->stall_seen) add(l, DIAG_STALL, STALL[2], 0);
l->crash_seen = crashes, l->stall_seen = stalls;
}
Expand Down
34 changes: 34 additions & 0 deletions sdk/machines/octatrack/elekloader/test_crash.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/* SPDX-License-Identifier: GPL-3.0-or-later
* Host check of crash.c's owners: the module behind the PC and each stack long, asked in either SDRAM alias, for the
* record's count; a lookup that faulted is skipped on the way back in. */
#include "crash.c"
#include <stdio.h>

static unsigned failures;
#define CHECK(x) do { if (!(x)) { failures++; fprintf(stderr, "%s:%d: %s\n", __FILE__, __LINE__, #x); } } while (0)
volatile uint32_t modwerk_test_crash[(sizeof(struct crash) + sizeof(struct crash_owners)) / 4u];
void mw_crash(void) {}
/* One module placed in the uncached alias at 0x48d30000, 0x1000 bytes. */
uint32_t modwerk_runtime_owner_module(uintptr_t address) { return address - 0x48d30000u < 0x1000u ? 0x95cff696u : 0; }

int main(void)
{
CHECK(sizeof(struct crash) == 180u && 0x100ff800u + sizeof(struct crash) + sizeof(struct crash_owners) <= 0x100ff900u); /* below dsp.c's */
REC->magic = MAGIC, REC->count = 3, REC->pc = 0x40d30010u; /* the cached alias of that module */
REC->stack[0] = 0x48d30ffcu, REC->stack[1] = 0x40012345u, REC->stack[2] = 0x48d31000u, REC->stack[7] = 0x40d30000u;
modwerk_crash_owners();
CHECK(OWNERS->magic == OWNERS_MAGIC && OWNERS->count == 3u && OWNERS->pc == 0x95cff696u);
CHECK(OWNERS->stack[0] == 0x95cff696u && !OWNERS->stack[1] && !OWNERS->stack[2] && OWNERS->stack[7] == 0x95cff696u);
/* The lookup faulted: crash.s came back for the next count; the owners are dropped, no second lookup. */
OWNERS->magic = OWNERS_BUSY, OWNERS->count = 3, REC->count = 4;
modwerk_crash_owners();
CHECK(OWNERS->magic == 0u);
/* The next crash is named again. */
REC->count = 5;
modwerk_crash_owners();
CHECK(OWNERS->magic == OWNERS_MAGIC && OWNERS->count == 5u && OWNERS->pc == 0x95cff696u);

if (failures) fprintf(stderr, "%u failure(s)\n", failures);
else puts("crash: owners in both aliases, for the record's count, re-entry skipped");
return failures != 0;
}
Loading
Loading