Skip to content
Closed
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
274 changes: 274 additions & 0 deletions docs/SYNTAKT_OS_RESEARCH.md

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions docs/VERIFICATION.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Verification record

## Syntakt boot probe on OS 1.40 and 1.42 — 16 October 2026

Source: `sdk/syntakt/core`, `src/engine/elektron/syntakt.ts`, `src/engine/elektron/stock-writes.ts`, from the static research in [SYNTAKT_OS_RESEARCH.md](SYNTAKT_OS_RESEARCH.md). Nothing below ran on a unit or in an emulator.

**Firmware-free (CI).** `src/engine/elektron/stock-writes.test.ts` proves the checker on synthetic NOP images: declared code change accepted, change outside a site refused, bad or missing guard refused, overlapping sites refused, protected range refused, tail over budget or without its hook refused, partial-instruction and PC-relative code sites refused, every problem reported at once. `src/engine/elektron/syntakt.test.ts` proves the profile: both releases bound by hash, no SDRAM area, protected map consistent with the probe site, the boot stub decodes as eight whole ColdFire instructions ending in the continuation jump (Ghidra 12.1.4's Coldfire decoder agreed locally), the probe links against a stand-in image with the `jmp`/`nop` site and relocated continuation, and any mod with `.run`, `.bss` or `.fast` is refused.

**Local (owner's files, `node scripts/verify-syntakt-core-probe.mjs`).** For each stock file the probe linked with one site (0x400004b2, 8 bytes) and a 48-byte tail, the stock-write checker reported exactly that change, and the rebuilt container passed `verifyEle3Build` (sections other than 3 byte-identical, 16-aligned layout, checksums, version `MW01`):

| Release | Stock `.syx` | Build `.syx` | Linked image | Stored main | Container | In-place margin (stock → build) |
| --- | --- | --- | --- | --- | --- | --- |
| 1.42 | `ded3c59f…37c1` | `f8493da70c09706337b20be1911eb8c77ca584f00d314e8d9052ec84d49f059f` | `041903b65fedbf885195c94f71e58dbb4371e84966b80f552507028e6bb095f6` | 1,363,181 B (stock 1,442,812) | 1,949,760 B (stock 2,029,392) | 96,357 → 16,679 B |
| 1.40 | `77812459…88ebf` | `5c7a5ac2714462f434b717188d223f924164634cae06613fbdbd07b326218c08` | `e46d4a52870eb44fdec3db7f281dc90f6f61f33e022ecf5fdc0c662aea2d44b6` | 1,332,261 B (stock 1,409,140) | 1,815,408 B (stock 1,892,288) | 160,989 → 84,063 B |

The repository's packer stores section 3 about 80 KB more densely than Elektron's, which narrows the in-place depack margin; the tail budget is 16 KB and every build is checked exactly.

**Not verified.** That the unit's bootstrap accepts a recompressed section 3 rests on public reports (Synthack-OS builds on 1.41 and 1.42); that the hook runs and the OS reaches the live UI; the SRAM signature; persistence; the FUNC + TRIG 4 recovery path on the owner's unit. The hardware steps are in the report's §10.

## Six Digitakt mods by sumedho on Core 2.1 — 14 October 2026

Source: `sdk/digitakt/dependencies/{machine-pages,dt-support}` and
Expand Down
24 changes: 24 additions & 0 deletions scripts/verify-syntakt-core-probe.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Explicit local evidence command for the Syntakt boot probe. Never run in CI or a visitor build: it reads the owner's
// stock OS files, links the probe, checks every stock write, rebuilds the container and writes the result plus a JSON
// record of hashes, layout and margins (no firmware bytes) to a local folder.
import { readFile, writeFile, mkdir } from 'node:fs/promises'
import { basename, resolve } from 'node:path'
import { createHash } from 'node:crypto'
import { buildSyntaktBootProbe } from '../src/engine/elektron/syntakt.ts'

const args = process.argv.slice(2), values = name => args.flatMap((arg, i) => arg === name ? [args[i + 1]] : []), one = name => values(name)[0]
if (!one('--out') || !values('--firmware').length) throw new Error('Usage: verify-syntakt-core-probe.mjs --out LOCAL_DIR --firmware Syntakt_OS.syx [--firmware ...]')
const output = resolve(one('--out')), sha = bytes => createHash('sha256').update(bytes).digest('hex'), cases = []
await mkdir(output, { recursive: true })
for (const path of values('--firmware')) {
const raw = new Uint8Array(await readFile(path))
const { release, linked, writes, syx, facts, stockMargin } = await buildSyntaktBootProbe(raw)
const name = 'syntakt-' + release + '-boot-probe.syx'
await writeFile(resolve(output, name), syx)
const result = { machine: 'syntakt', release, stock: basename(path), stockSha256: sha(raw), stage: 'boot-probe', providesInterface: false, containerVerified: true,
buildSha256: sha(syx), imageSha256: sha(linked.image), writes, layout: linked.layout, facts, stockInplaceMargin: stockMargin, hardware: 'not tested' }
cases.push(result)
console.log('syntakt ' + release + ': boot probe linked (' + writes.sites + ' site, ' + writes.tail + ' tail bytes), container verified, in-place margin ' + facts.inplaceGap + ' (stock ' + stockMargin + '); hardware evidence still required')
}
await writeFile(resolve(output, 'boot-probes.json'), JSON.stringify({ cases }, null, 2) + '\n')
39 changes: 39 additions & 0 deletions sdk/machines/syntakt/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Syntakt

Status: **research, core started**. The OS files are understood and rebuilt, modified builds boot (public reports), and the first Modwerk core stage, a boot probe, links and verifies locally. No module can be built for this machine yet, and nothing in this guide has run on the owner's unit. Read [the research report](../../../docs/SYNTAKT_OS_RESEARCH.md) before changing anything here.

## Platform

The Syntakt uses the `elemod` platform ([platform decision](../../../docs/SYNTAKT_OS_RESEARCH.md#8-platform-decision-brief)): the same `ELE3` container and SysEx transport as the Digitakt, the main OS as section 3 at 0x40000400 staged at 0x40200000, the same guarded-site linker. What differs: the appended tail lies inside the memory the OS zeroes at reset, so the core's hook is the BSS-clear entry rather than a main-initialisation call; the machine has a second ColdFire, two FPGAs and a UI MCU whose images stay byte-identical in every build.

Supported releases (file and image hashes in `sdk/syntakt/core/stock-map.json`): 1.40 (the analysed file) and 1.42. 1.40A and 1.41 are relinked builds (research report §5.1) that match no release and are refused until analysed.

## Core interface

Interface 0.1 ([sdk/syntakt/core/interface.json](../../syntakt/core/interface.json)): no events, tables, exports or claims yet. The hook sites a later interface would use are inventoried in the report, with the UI/audio/project sites still unknown.

## Memory areas and budgets

| Area | Range | Budget | Notes |
| --- | --- | --- | --- |
| Fast SRAM tail | 0x8000733c–0x80008000 | 3,268 bytes | Proven free in both releases. Zeroed at every reset; the core writes its 16-byte signature at its start. |
| Boot stub space | appended to the main OS, up to 16 KB | 2,256 bytes of image fill exist, but the stub lives in the appended tail | Inside the BSS-clear range: valid only until the clear, which the hook runs before. |
| SDRAM for mods | none | 0 | Not proven. Candidates: 128 KB above a lowered initial stack (0x47fe0000–0x48000000, data patch at 0x400004f4) or the first 64 KB block of the OS heap. Either needs the owner's decision and a hardware stress run with every sample slot loaded. |
| In-place depack margin | — | 16,679 bytes (1.42) and 84,063 bytes (1.40) on the probe builds; 96,357 and 160,989 on the stock files | The repository packer compresses section 3 better than Elektron's, which narrows the margin. Every build is checked exactly; a tail that compresses badly consumes it. |
| Container | — | 2 MB | The in-OS upgrade receive buffer; 67,760 bytes of stored growth remain on 1.42, 204,864 on 1.40. |

## Build

```sh
node scripts/verify-syntakt-core-probe.mjs --out /tmp/syntakt-probe --firmware ~/path/Syntakt_OS1.42.syx
```

Links the boot probe into the owner's stock file, checks every stock write, verifies the container and writes the `.syx` and a JSON record locally. There is no browser build and no module build for this machine.

## Flash and recover

Flash with Elektron Transfer over USB (MIDI IN and OUT set to the Syntakt, drop the `.syx`, confirm on the unit). To recover a unit that does not boot: hold FUNC while powering on, press TRIG 4 (OS UPGRADE) and send the stock `.syx` from Transfer's legacy OS-upgrade page over MIDI DIN; USB does not work there. Public reports (Synthack-OS) describe this path; it is **not tested on the owner's unit**. Keep the stock file and a DIN interface ready before flashing any build.

## Credits

The container and transport were first documented by [elektron-firmware-tool](https://github.com/mischa85/elektron-firmware-tool) (Marcel Bierling, MIT). [Synthack-OS](https://github.com/Angelo-politek/Synthack-OS) (MIT) published the 1.41 memory map, the decompression limit, the recovery path and the hardware observation that runtime page writes reach the memory above the BSS. The [digiemu Syntakt port](https://github.com/irpina/digiemu/pull/33) identified the MCF5441x, the second ColdFire and the section roles. Modwerk's research re-derived every address and guard from the owner's own files; no code from these projects is included.
20 changes: 17 additions & 3 deletions sdk/machines/syntakt/machine.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,18 @@
"order": 60,
"name": "Syntakt",
"status": "research",
"summary": "OS 1.40A and 1.42 files documented and rebuilt byte-identical; modified builds boot on a unit and the stock 1.42 file returns it to stock. No shared core yet.",
"summary": "OS 1.40, 1.40A and 1.42 files documented and rebuilt byte-identical; modified builds boot on a unit. Core started: a boot probe links and verifies locally; no SDRAM for mods is proven yet.",
"firmware": {
"releases": [
{
"version": "1.40",
"files": [
{
"name": "Syntakt_OS1.40.syx",
"sha256": "7781245917cd3b0334e85da252846c0dc81a676ec0873b735cf51d9029e88ebf"
}
]
},
{
"version": "1.40A",
"files": [
Expand All @@ -33,7 +42,7 @@
"format": "done",
"rebuild": "done",
"boot": "done",
"core": "open",
"core": "started",
"mods": "open"
},
"research": [
Expand All @@ -45,7 +54,7 @@
{
"label": "Synthack-OS",
"url": "https://github.com/Angelo-politek/Synthack-OS",
"note": "MIT mods for OS 1.41 as patch.json with expected-byte hashes, plus modarea/vparams infrastructure. The project states seven mods run on hardware, and it documents recovery over MIDI DIN. No firmware hosted."
"note": "MIT mods for OS 1.41 as patch.json with expected-byte hashes, plus modarea/vparams infrastructure. The project states seven mods run on hardware, documents recovery over MIDI DIN, and its issue #4 reports runtime page writes above the BSS on a unit."
},
{
"label": "Synthack-OS 1.42 port",
Expand All @@ -56,6 +65,11 @@
"label": "digiemu Syntakt port",
"url": "https://github.com/irpina/digiemu/pull/33",
"note": "Emulator boots OS 1.40A to the live UI with the second ColdFire rendering its digital machines; analog tracks and the FPGA are not modelled. Open PR."
},
{
"label": "Modwerk Syntakt OS research",
"url": "https://github.com/repeat98/modwerk/blob/main/docs/SYNTAKT_OS_RESEARCH.md",
"note": "Static analysis of the owner's 1.40 and 1.42 files: boot chain, memory ownership with verdicts, hook sites with guards, cross-release diff, the boot-probe core. Nothing run on a unit."
}
],
"art": {
Expand Down
49 changes: 49 additions & 0 deletions sdk/syntakt/core/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Syntakt core

The Modwerk core for the Syntakt, at its first stage: a **boot probe** that proves a build can enter Modwerk code on this machine and reserve memory without touching anything else. It is the outcome of [the OS research](../../../docs/SYNTAKT_OS_RESEARCH.md), which also lists every open decision. No module can be built for the Syntakt yet.

**Status:** boot-probe stage, interface 0.1. Not qualified on hardware. The owner's unit has not run it; the recovery path (FUNC + TRIG 4, DIN) is untested there.

## Files

| File | Content |
| --- | --- |
| [interface.json](interface.json) | The contract modules will link against: events, tables, exports, claims and memory. Empty except the reserved SRAM area. |
| [probe.json](probe.json) | The boot hook per supported release: address, displaced length, SHA-256 of the stock bytes, continuation. The stack-top word is recorded as a candidate data site and is not used. |
| [stock-map.json](stock-map.json) | What the linker and the stock-write checker refuse to touch: per-release image extents, the SRAM initialisation data, the heap descriptor, the SysEx tables, the reset chain and the vector filler; the proven-free area and the two unproven SDRAM candidates; the tail budget. |
| [boot.s](boot.s) | The probe's own code. Its assembled bytes are `SYNTAKT_BOOT_STUB` in `src/engine/elektron/syntakt.ts`. |

Everything in these files is an address, a length, a hash or a note. No firmware byte, code or table of the OS is kept here.

## What the probe does

At the BSS-clear entry the stock OS has already copied its SRAM image and has not yet zeroed its BSS. The planted `jmp` enters the stub, which replays the two displaced instructions, writes a 16-byte signature (`MWSK`, version, length, 0) into the reserved SRAM tail and jumps back to stock. The signature is the only observable effect: an emulator or a debugger can read 0x8000733c after boot to see that the hook ran. The stub itself sits in the appended tail of the main OS, inside the memory the OS zeroes next, so it runs exactly once and nothing may return to it.

## Memory

| Area | Range | Size | Verdict | Use |
| --- | --- | --- | --- | --- |
| `sram-tail` | 0x8000733c–0x80008000 | 3,268 bytes | proven free in 1.40 and 1.42: no reference in either image, zero after the reset copy, no allocator or DMA ring reaches it | the core's fast data; re-zeroed at every reset |
| SDRAM for mods | none | — | not proven; see the two candidates in `stock-map.json` and §3 of the report | the linker refuses every mod with `.run`, `.bss` or `.fast` sections |

## Rules the build enforces

- The linker profile (`SYNTAKT_DEVICE`) binds the two researched releases by file and image hash, offers only `sram-tail`, and protects the image header, the reset chain (except the hook), the vector filler and, per release, the SRAM initialisation data, the heap descriptor and the SysEx tables.
- The stock-write checker (`src/engine/elektron/stock-writes.ts`) compares the linked image with stock and refuses any changed byte outside a declared, guard-verified site, overlapping or unguarded sites, sites inside protected ranges, code sites that are not whole instructions or that become PC-relative, a tail over 16 KB, and a tail without the hook that moves it before the clear.
- The container writer keeps every section but the main OS byte-identical and refuses a build whose in-place depack would overrun or whose container exceeds the 2 MB receive-buffer bound (`ELE3_DEVICES.syntakt`).
- The profile is not yet in the shared `LINK_DEVICES` table (owner review), so no builder or CI job picks it up.

## Build and check locally

```sh
node scripts/verify-syntakt-core-probe.mjs --out /tmp/syntakt-probe --firmware ~/path/Syntakt_OS1.42.syx
```

It links the probe into the owner's stock file, runs the checker, rebuilds the container and writes the `.syx` with a JSON record of hashes, layout, margins and the checker's report. The record carries no firmware bytes; the `.syx` stays on the developer's computer. Results of the owner's runs are in [docs/VERIFICATION.md](../../../docs/VERIFICATION.md).

## Open decisions (owner)

1. Register the Syntakt in the shared linker and container tables.
2. Choose and qualify an SDRAM region for mods: the SP-relocated top window (a 4-byte data patch at 0x400004f4) or a heap block taken at initialisation.
3. Accept the 2 MB container bound as the flash limit until the flash geometry is read from a unit.
4. Keep the analysed 1.40 file as a supported release beside 1.40A (whose image was not available) or drop it.
20 changes: 20 additions & 0 deletions sdk/syntakt/core/boot.s
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
| SPDX-License-Identifier: GPL-3.0-or-later
| Modwerk Syntakt boot probe, ColdFire ISA_A. Original code; the assembled bytes live in src/engine/elektron/syntakt.ts
| (SYNTAKT_BOOT_STUB) and src/engine/elektron/syntakt.test.ts decodes them instruction by instruction.
|
| The core is entered through a `jmp` planted at the BSS-clear entry (0x400004b2 in OS 1.40 and 1.42), the one place a
| core must run on this machine: the OS copies its on-chip SRAM image first, then zeroes everything from the SRAM-init
| data to the end of its BSS, and anything appended to the main OS lies inside that range. At this point interrupts are
| masked, SRAM is initialised, A0 and D1 are free (the BSS clear sets them next) and D4-D7 must be preserved.

.text
.globl mw_boot
mw_boot:
lea -16(%sp),%sp | replay the two displaced stock instructions
movem.l %d4-%d7,(%sp)
lea 0x8000733c,%a0 | the proven-free SRAM tail (sdk/syntakt/core/stock-map.json)
move.l #0x4d57534b,(%a0)+ | signature "MWSK": a trial can read it back to see that the hook ran
move.l #0x00010000,(%a0)+ | probe version 0.1.0
move.l #0x00000cc4,(%a0)+ | length of the reserved area (3268 bytes)
move.l #0,(%a0) | reserved
jmp mw_stock_resume | back to stock at 0x400004ba (relocated by the linker)
14 changes: 14 additions & 0 deletions sdk/syntakt/core/interface.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"schemaVersion": 1,
"machine": "syntakt",
"interface": "0.1",
"status": "Boot-probe stage. The core reserves on-chip SRAM only, exposes no events and offers no SDRAM to mods; the research report names the two SDRAM candidates the owner must decide on after hardware qualification.",
"events": [],
"tables": [],
"exports": [],
"claims": [],
"memory": {
"area": null,
"fast": "0x8000733c-0x80008000"
}
}
18 changes: 18 additions & 0 deletions sdk/syntakt/core/probe.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"schemaVersion": 1,
"machine": "syntakt",
"version": "0.1.0-dev",
"stage": "boot-probe",
"providesInterface": false,
"hook": "The BSS-clear entry: the one place a core must run on this machine, because everything appended to the main OS lies inside the memory the OS zeroes right after. The displaced instructions are replayed by the stub and stock resumes at the continuation.",
"releases": {
"1.40": {
"boot": { "addr": "0x400004b2", "len": 8, "stockSha256": "02baba67a7ec7d7bdf683fa37f268620e3a694853b398205df521c64128da818", "resume": "0x400004ba" },
"stackTop": { "addr": "0x400004f4", "len": 4, "stockSha256": "97f3f29734d22b764958054cf0fcce9cb81a679be081fb982af63bf246237d49", "note": "Initial supervisor stack pointer word; a candidate data patch for an SDRAM region above a lowered stack. Not used by the probe." }
},
"1.42": {
"boot": { "addr": "0x400004b2", "len": 8, "stockSha256": "02baba67a7ec7d7bdf683fa37f268620e3a694853b398205df521c64128da818", "resume": "0x400004ba" },
"stackTop": { "addr": "0x400004f4", "len": 4, "stockSha256": "97f3f29734d22b764958054cf0fcce9cb81a679be081fb982af63bf246237d49", "note": "Initial supervisor stack pointer word; a candidate data patch for an SDRAM region above a lowered stack. Not used by the probe." }
}
}
}
Loading
Loading