Inter-CPU Protocol
Inter-CPU Protocol
The KN5000 uses two TMP94C241F CPUs that communicate via a memory-mapped latch at 0x120000. The Main CPU handles UI, MIDI, and control while the Sub CPU handles all audio generation.
Architecture
┌──────────────────────────────────────────────────────────────────────┐
│ MAIN CPU (TMP94C241F) │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Audio_Lock_ │ │ Audio_DMA_ │ │ Audio_Lock_ │ │
│ │ Acquire │───>│ Transfer │───>│ Release │ │
│ │ (0xEF1FEE) │ │ (0xEF341B) │ │ (0xEF1F0F) │ │
│ └─────────────────┘ └────────┬────────┘ └─────────────────┘ │
│ │ │
│ Lock counter at (0x0532 + lock_index) │
│ Linked list of waiting requests at 0x0487 │
└──────────────────────────────────┬───────────────────────────────────┘
│
v
┌──────────────────────────┐
│ LATCH │
│ @ 0x120000 │
│ │
│ Bidirectional Data │
│ DMA-capable transfer │
└──────────────────────────┘
│
v
┌──────────────────────────────────────────────────────────────────────┐
│ SUB CPU (TMP94C241F) │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌────────────────┐ │
│ │ InterCPU_Latch_ │ │ MicroDMA CH0/2 │ │ CMD_DISPATCH_ │ │
│ │ Setup (0x020C15) │───>│ Handlers │───>│ TABLE │ │
│ └──────────────────┘ └──────────────────┘ └────────────────┘ │
│ │ │
│ ┌───────────────────────────────┘ │
│ v │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ COMMAND HANDLERS │ │
│ │ │ │
│ │ 0x00-0x1F: Audio_CmdHandler_00_1F (MIDI/notes → 0x2B0D) │ │
│ │ 0x20-0x3F: Audio_CmdHandler_20_3F (Stub) │ │
│ │ 0x40-0x5F: Audio_CmdHandler_40_5F (...) │ │
│ │ 0x60-0x7F: Audio_CmdHandler_60_7F (Audio/DSP → 0x3B60) │ │
│ │ 0x80-0x9F: Serial port setup (38400 baud) │ │
│ │ 0xA0-0xBF: Audio_CmdHandler_A0_BF (System audio) │ │
│ │ 0xC0-0xFF: Audio_CmdHandler_C0_FF (Extended system) │ │
│ └────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
Main CPU: Sending Commands
Lock Mechanism
The Main CPU uses a semaphore system to serialize access to the inter-CPU latch:
; Acquire lock before sending
LD A, lock_index ; Lock index 0-7
CALL Audio_Lock_Acquire ; Decrements counter, waits if busy
; ... send audio commands via DMA ...
; Release lock when done
LD A, lock_index
CALL Audio_Lock_Release ; Increments counter, signals waiters
Lock Variables:
- Counter:
(0x0532 + lock_index)- Decremented on acquire, incremented on release - Wait List:
0x0487- Linked list of pending requests
DMA Transfer
The Audio_DMA_Transfer routine handles bulk data transfer:
Audio_DMA_Transfer:
; Data pointer at 0x05DA
; Byte count at 0x05DE
; Transfers in chunks to latch at 0x120000
Transfer Variables:
- Data Pointer: 0x05DA (XWA)
- Byte Count: 0x05DE (16-bit)
- Completion Flag: 0x05E0
Sub CPU: Receiving Commands
Interrupt Configuration
The InterCPU_Latch_Setup routine (0x020C15) configures the Sub CPU’s interrupt controller for inter-CPU DMA reception:
InterCPU_Latch_Setup:
LD (INTETC01), 005h ; INTTC0 level = 5 (DMA ch0 completion)
LD (INTETC23), 005h ; INTTC2 level = 5 (DMA ch2 completion)
LD (INTE0AD), 001h ; INT0 level = 1 (latch data pending)
Interrupt Priority Hierarchy:
| Interrupt | Level | Source | Purpose |
|---|---|---|---|
| INT0 | 1 | Latch data_pending callback | New command byte available |
| INTTC0 | 5 | HDMA ch0 transfer complete | Command payload received |
| INTTC2 | 5 | HDMA ch2 transfer complete | Response data sent |
The firmware normally runs with EI 6 (IFF=6), which means only level 6+ interrupts are accepted. Since INT0 is at level 1, it only fires during brief EI 0 windows. INTTC0/INTTC2 at level 5 are also normally masked — they fire during EI 0 windows or when the ISR handler lowers IFF.
INT0 Detection Mode:
The Sub CPU configures INT0 as level-triggered (IIMC bit 1 = 0). This means the interrupt flag is asserted whenever the latch data_pending signal is high, and deasserted when the latch is read (clearing data_pending).
HDMA-Based Command Reception Flow
The complete flow for receiving a command from the Main CPU involves three interrupt sources working in sequence:
Main CPU writes command byte to latch (0x120000)
│
v
generic_latch data_pending → set_input_line(INT0, ASSERT)
│
v
┌──────────────────────────────────────────────────┐
│ INT0 ISR (INT0_HANDLER at 0x020C47) │
│ │
│ 1. Read command byte from latch (0x120000) │
│ → This clears data_pending → deasserts INT0 │
│ │
│ 2. Decode command type: │
│ - E1: DMAD0=0x10E0, DMAC0=6 │
│ - E2: DMAD0=0x111C, DMAC0=10 │
│ - E3: Set PAYLOAD_LOADED_FLAG, return │
│ - Standard (0x00-0xDF): │
│ DMAD0=0x10F0, DMAC0=(byte & 0x1F)+1 │
│ │
│ 3. Write DMA0V = 0x0A (INT0 vector) │
│ → Configures HDMA ch0 to steal future INT0 │
│ triggers for automatic byte-by-byte xfer │
│ │
│ 4. Return from interrupt │
└──────────────────────────────────────────────────┘
│
v
Main CPU writes data bytes to latch (one at a time)
│
v
Each data_pending → INT0 flag set → HDMA ch0 steals it
(HDMA has priority over ISR dispatch)
│
v
HDMA ch0 transfers: latch (DMAS0) → DMAD0++
DMAC0 decremented each transfer
│
v
When DMAC0 reaches 0:
│
v
┌──────────────────────────────────────────────────┐
│ INTTC0 ISR (MICRODMA_CH0_HANDLER at 0x020F1F) │
│ │
│ 1. DMA0V cleared automatically (HDMA disabled) │
│ │
│ 2. Dispatch based on CMD_PROCESSING_STATE: │
│ - State 0: Standard command → dispatch via │
│ CMD_DISPATCH_TABLE (upper 3 bits) │
│ - State 2: E1 phase 1 → set up phase 2 │
│ - State 3: E2 processing → handle ext. cmd │
│ │
│ 3. Process command data from receive buffer │
└──────────────────────────────────────────────────┘
Key design insight: The INT0 ISR only fires for the first byte (the command header). After that, HDMA ch0 takes over and silently transfers remaining bytes without CPU intervention. The ISR configures DMA0V = 0x0A (INT0’s DMA start vector) so that subsequent INT0 flag assertions trigger HDMA instead of the ISR.
MicroDMA Handlers
| Handler | Channel | Purpose |
|---|---|---|
MICRODMA_CH0_HANDLER |
CH0 | Command payload reception (triggered by INTTC0) |
MICRODMA_CH2_HANDLER |
CH2 | Response data transmission (triggered by INTTC2) |
Command Dispatch
Commands are dispatched based on upper 3 bits of the command byte:
CMD_DISPATCH_TABLE: ; Address: 0x00F428
dd Audio_CmdHandler_00_1F ; 0x00-0x1F: MIDI/note commands → ring buffer at 0x2B0D
dd Audio_CmdHandler_20_3F ; 0x20-0x3F: Stub (returns 0)
dd Audio_CmdHandler_40_5F ; 0x40-0x5F: (line 9619)
dd Audio_CmdHandler_60_7F ; 0x60-0x7F: Audio/DSP commands → ring buffer at 0x3B60
dd Serial1_DataTransmit_Loop ; 0x80-0x9F: Serial port 1 data
dd Audio_CmdHandler_A0_BF ; 0xA0-0xBF: (line 51502)
dd Audio_CmdHandler_C0_FF ; 0xC0-0xDF: (line 10951)
dd Audio_CmdHandler_C0_FF ; 0xE0-0xFF: Same as 0xC0-0xDF
Audio Command Format
Ring Buffer at 0x2B0D (MIDI/Note Commands)
Commands 0x00-0x1F write MIDI-like data to a 4KB ring buffer:
| Variable | Address | Description |
|---|---|---|
| Write Pointer | 0x2B0D | 12-bit circular index |
| Read Pointer | 0x2B0F | Current read position |
| Byte Count | 0x2B11 | Bytes available |
| Base Address | 0x2B13 | Buffer start |
| Buffer Data | +0x06 | 4KB circular buffer |
Ring Buffer at 0x3B60 (DSP/Audio Config)
Commands 0x60-0x7F write audio config data to a 2KB ring buffer:
| Variable | Address | Description |
|---|---|---|
| Write Pointer | 0x3B60 | 11-bit circular index (wraps at 0x800) |
| (reserved) | 0x3B62 | Padding |
| Available Count | 0x3B64 | Bytes queued for processing |
| Buffer Base | 0x3B66 | 3-byte pointer to actual data area |
Audio_Process_DSP (called from main loop) reads commands from this buffer. The most important command is 0x2D (DSP configuration change). See Audio Subsystem — Command 0x2D Protocol for the full 4-layer protocol.
Command Byte Format
The command byte sent to the latch encodes both the handler index and payload length:
Bits 7-5: Handler index (0-7) → selects CMD_DISPATCH_TABLE entry
Bits 4-0: Payload length - 1 (0-31 = 1 to 32 bytes)
Examples:
0x02 = Handler 0 (MIDI), 3 bytes → Note On/Off or Control Change
0x01 = Handler 0 (MIDI), 2 bytes → Program Change
0x7F = Handler 3 (DSP config), 32 bytes
Special command bytes bypass this encoding:
0xE1: Bulk transfer setup (6 bytes: dest_addr[4] + count[2])0xE2: Extended parameter block (10 bytes)0xE3: Payload ready signal (no data, sets boot flag)
MIDI Message Format (Ring Buffer at 0x2B0D)
The ring buffer contains standard MIDI messages parsed by MIDI_Dispatch (0x020FA4):
| Status Byte | Length | Format | Handler |
|---|---|---|---|
| 0x80-0x8F | 3 | [0x8n] [note] [velocity] |
Note Off (→ Voice_NoteOn with vel=0) |
| 0x90-0x9F | 3 | [0x9n] [note] [velocity] |
Note On (Voice_NoteOn at 0x2CF97) |
| 0xB0-0xBF | 3 | [0xBn] [CC#] [value] |
Control Change (Voice_CtrlChange at 0x2A282) |
| 0xC0-0xCF | 2 | [0xCn] [program] |
Program Change (Voice_ProgChange at 0x34A4A) |
| 0xD0-0xDF | 2 | [0xDn] [pressure] |
Channel Pressure (Voice_ChanPressure at 0x2A4EA) |
| 0xE0-0xEF | 3 | [0xEn] [LSB] [MSB] |
Pitch Bend (Voice_PitchBend at 0x2A5E6) |
| 0xF0-0xFF | var | [0xF0] ... [0xF7] |
System Message (Voice_SystemMsg at 0x2A7AF) |
The channel number (n) is in the low nibble of the status byte (bits 3-0). The Sub CPU supports up to 26 channels (0x00-0x19).
Supported MIDI Control Changes
| CC# | Name | Description |
|---|---|---|
| 0x01 | Mod Wheel | Modulation depth |
| 0x07 | Volume | Channel volume |
| 0x0A | Pan | Stereo position |
| 0x0B | Expression | Expression controller |
| 0x40 | Sustain | Sustain pedal (on/off) |
| 0x5B | Sostenuto | Sostenuto pedal |
| 0x5D | Soft | Soft pedal |
| 0x5E | Portamento | Portamento control |
| 0x78-0x81 | Extended | Proprietary extended functions (lookup table dispatch) |
| 0x91-0x9D | Effects | Proprietary effects depth control |
Voice Processing
Note On processing (Voice_NoteOn at 0x2CF97):
- Validates channel (must be < 0x1A = 26)
- If velocity = 0: triggers note off (voice release)
- If velocity > 0: allocates voice slot, sets pitch and velocity
Voice parameters are stored in per-channel structures of 287 bytes each at base address 0x041381:
channel_params[channel] = 0x041381 + channel * 0x11F
Pitch bend uses a 14-bit value (MSB and LSB combined): value = (MSB << 7) | LSB, with bit 15 set as a sign marker.
Sub CPU Response
The Sub CPU can send data back to the Main CPU via InterCPU_DMA_Send:
InterCPU_DMA_Send:
; Entry: XDE = source data pointer
; BC = byte count
; A = command/channel identifier
; Splits large transfers into 32-byte chunks
; Used by tone generator for status responses
Boot Sequence Transfer
During boot, the Main CPU loads the 192KB Sub CPU payload:
- Main CPU initializes DMA channels via
SubCPU_Init_DMA_Channels - Main CPU writes payload in chunks to latch
- Sub CPU’s MicroDMA handler receives data
- Sub CPU executes payload from internal RAM
- Sub CPU enters
Audio_System_Initand starts processing loop
Code References
Main CPU (maincpu/kn5000_v10_program.asm)
| Routine | Address | Description |
|---|---|---|
SubCPU_Init_DMA_Channels |
0xEF329E | Initialize DMA channels for inter-CPU communication |
SubCPU_Send_Payload |
0xEF068A | Transfer 192KB payload to Sub CPU |
InterCPU_E1_Bulk_Transfer |
0xEF3457 | E1 two-phase bulk transfer protocol |
InterCPU_E2_Send |
0xEF33AA | E2 extended transfer command |
InterCPU_Send_Data_Block |
0xEF3345 | Send variable-length data packet |
Audio_DMA_Transfer |
0xEF341B | Core DMA transfer routine |
Audio_Lock_Acquire |
0xEF1FEE | Acquire communication lock |
Audio_Lock_Release |
0xEF1F0F | Release communication lock |
Sub CPU (subcpu/kn5000_subprogram_v142.asm)
| Routine | Address | Description |
|---|---|---|
InterCPU_Latch_Setup |
0x020C15 | Configure DMA and interrupts |
InterCPU_DMA_Send |
0x020C6B | Send data to Main CPU |
Audio_CmdHandler_00_1F |
0x034D5F | Audio command handler |
MICRODMA_CH0_HANDLER |
0x020F1F | DMA channel 0 interrupt |
MICRODMA_CH2_HANDLER |
0x020F01 | DMA channel 2 interrupt |
Related Pages
- Audio Subsystem - Sound generation details
- CPU Subsystem - Both CPU specifications
- Boot Sequence - Startup process
- System Overview - Overall architecture
Boot ROM Protocol (Sub CPU Side)
During boot, the Sub CPU boot ROM implements a DMA-based protocol for receiving the 192KB payload from the Main CPU.
Boot ROM INT0 Handler (0xFF881F)
The Sub CPU boot ROM’s INT0 handler processes commands from the latch:
InterCPU_RX_Handler: ; 0xFF881F
push XWA
bit 2, (0x34) ; Check busy flag
jr NZ, .exit
ld A, (0x120000) ; Read command from latch
ld (0x051A), A ; Store for processing
cp A, 0xE1 ; E1 command?
jr NZ, .check_e2
; ... set up DMA for 6-byte header ...
jr .done
.check_e2:
cp A, 0xE2 ; E2 command?
jr NZ, .check_e3
; ... set up DMA for 10-byte header ...
jr .done
.check_e3:
cp A, 0xE3 ; E3 command? (CRITICAL!)
jr NZ, .data_packet
set 6, (0x04FE) ; SET PAYLOAD_LOADED_FLAG bit 6!
jr .cleanup
.data_packet:
; Handle variable-length data packet
...
.cleanup:
res 1, (0x34) ; Clear handshake flag
.exit:
pop XWA
reti
E1/E2/E3 Command Protocol
| Command | Byte | Action | Data Size |
|---|---|---|---|
| E1 | 0xE1 | Bulk data transfer header | 6 bytes (dest addr + count) |
| E2 | 0xE2 | Extended transfer header | 10 bytes |
| E3 | 0xE3 | Payload complete signal | 0 bytes |
| Data | 0x00-0xDF | Data packet | Low 5 bits + 1 |
PAYLOAD_LOADED_FLAG (0x04FE)
| Bit | Meaning |
|---|---|
| 6 | Payload ready - triggers call 0x000400 in boot ROM main loop |
| 7 | Transfer active flag |
Boot ROM Main Loop (0xFF8410)
Boot_Main_Loop: ; 0xFF840C
res 6, (0x04FE) ; Clear payload-ready flag
.poll:
bit 6, (0x04FE) ; Check if payload ready
jr Z, .check_status
ei 6 ; Enable interrupts level 6
call 0x000400 ; CALL PAYLOAD ENTRY POINT!
.check_status:
; Update SSTAT signals based on MSTAT
ldcf 1, (0x30) ; Read Port D bit 1
; ... process and update Port D ...
jr T, .poll ; Loop forever
Main CPU Payload Transfer
SubCPU_Init_DMA_Channels (0xEF329E)
Configures MicroDMA channels for inter-CPU communication:
SubCPU_Init_DMA_Channels: ; 0xEF329E
; Configure interrupt priorities
AND (INTET23), 0xF8
RES 2, (T8RUN)
; ... more interrupt config ...
; Set DMA destination 2 to latch address
LDA XWA, INTER_CPU_COMM_LATCHES ; 0x140000
LDC_DMAD2_XWA
; Set DMA source 0 to latch address
LDA XWA, INTER_CPU_COMM_LATCHES
LDC_DMAS0_XWA
; Clear state variables
LD (0x05E0), 0x00
LD (0x05E2), 0x00
RET
SubCPU_Send_Payload (0xEF068A)
Sends the 192KB payload to Sub CPU in chunks. This is the core routine for loading the Sub CPU firmware during boot.
See Also:
- Boot Sequence: SubCPU_Send_Payload Details - Transfer sequence table, timing, error handling
- LZSS Compression - Optional preset data decompression
Source: maincpu/kn5000_v10_program.asm:134212
SubCPU_Send_Payload: ; 0xEF068A
PUSH XIZ
CP (0xFFFEEF), 0xFF ; Check transfer enable flag
JRL NZ, .skip_transfer
; Initial timing delay (0x2000 iterations)
LD XIZ, 0
.delay_loop:
INC 1, XIZ
CP XIZ, 0x2000
JR C, .delay_loop
; Send 64KB chunks from Table Data ROM (0x830000-0x870000)
LD XWA, 0x830000 ; Source: table_data + 0x30000
LD XBC, 0x10000 ; Count: 64KB
LD XDE, 0x050000 ; Dest: Sub CPU RAM
CALL InterCPU_E1_Bulk_Transfer
LD XWA, 0x840000 ; Next 64KB
LD XBC, 0x10000
LD XDE, 0x060000
CALL InterCPU_E1_Bulk_Transfer
; ... 3 more 64KB chunks (0x850000, 0x860000, 0x870000) ...
; Optional LZSS decompression from 0x3E0000
CP (0xFFFEED), 0xFF ; Check decompression flag
JR Z, .skip_decompress
CALL SLIDE_Parse_Header ; Decompress SLIDE4K data
; ... copy decompressed data to Sub CPU ...
.skip_transfer:
POP XIZ
RET
Transfer Summary:
| Chunk | Source | Sub CPU Dest | Size |
|---|---|---|---|
| 1-5 | 0x830000-0x870000 | 0x050000-0x090000 | 5 × 64KB |
| 6-8 | Decompressed/Fallback | 0x00F000-0x02F000 | 3 × 64KB |
| 9 | Buffer | 0x000400 | 256 bytes |
InterCPU_E1_Bulk_Transfer (0xEF3457)
Implements the E1 two-phase transfer protocol:
InterCPU_E1_Bulk_Transfer: ; 0xEF3457
PUSH IZ
; Wait for previous transfer to complete
...
.wait_ready:
BIT 3, (PZ) ; Check SSTAT1 (Sub CPU ready)
JRL Z, .timeout
; Send E1 command
RES 0, (PZ) ; Clear MSTAT0
LD (0x05E0), 0x02 ; Set state = 2 (two-phase)
LD (INTER_CPU_COMM_LATCHES), 0xE1 ; Send E1 command!
.wait_ack:
BIT 3, (PZ) ; Wait for Sub CPU response
JRL NZ, .ack_timeout
SET 0, (PZ) ; Set MSTAT0
; Send 6-byte header (dest addr + byte count)
LDA XHL, 0x0608
LD (XHL), XWA ; Store dest address
LDA XWA, 0x05D0
LD (XWA), XDE ; Store source address
LD (XHL+4), BC ; Store count
LD (XWA+4), BC
LD (0x05DA), XWA ; Setup for Audio_DMA_Transfer
LDW (0x05DE), 0x0006 ; 6 bytes for header
CALR Audio_DMA_Transfer ; Send header
; Wait for phase 1 complete, then send actual data
...
CALR Audio_DMA_Transfer ; Send data
POP IZ
RET
Command 0x2B: Sound Name Query
The Main CPU uses command 0x2B to query the Sub CPU for the display name of the currently selected voice/sound in a given keyboard part. This is used to populate the on-screen voice name display.
Request Format (10 bytes, Main CPU → Sub CPU)
| Offset | Value | Description |
|---|---|---|
| 0 | 0x2B | Command: sound/voice query |
| 1 | 0x30–0x3F | Voice slot (0x30 = slot 0, 0x3F = slot 15) |
| 2 | 0x7F | Parameter selector |
| 3 | 0x20 | Address/routing byte |
| 4–5 | 0x00 0x00 | Reserved |
| 6 | 0x02 | Sub-command group |
| 7 | 0x12–0x16 | Part identifier (see table below) |
| 8 | XX | Voice/sound index within bank |
| 9 | 0x00 | Padding/terminator |
Response Format (25 bytes, Sub CPU → Main CPU)
| Offset | Value | Description |
|---|---|---|
| 0–7 | (echo) | Echo of request bytes 0–7 |
| 8–23 | ASCII | 16-character sound name (space-padded) |
| 24 | 0x10/0x20 | Status/category byte |
Part Identifiers (byte 7)
| Value | Part | Default Voice (from trace) |
|---|---|---|
| 0x12 | Right 1 | Modern E.P.1 (idx 0x06) |
| 0x13 | Right 2 | Bigband Brass (idx 0x38) |
| 0x14 | Left | Piano (idx 0x00) |
| 0x15 | Right 1 (conductor) | Modern E.P.1 (idx 0x06) |
| 0x16 | Right 2 (conductor) | Bigband Brass (idx 0x38) |
Sub CPU Handling
- Processed in
Audio_Process_DSP(0x035B36), not the standard command dispatch table - Sub-command 0x00 at offset 0x436B triggers sound name lookup from RAM tables at 0x041xxx
- Names are pre-loaded during boot from Table Data ROM, not queried from tone generator
- Response sent via
InterCPU_DMA_Sendwith HDMA ch2
Example Exchange (from MAME trace)
Request: 2B 30 7F 20 00 00 02 14 00 00
Response: 2B 30 7F 20 00 00 02 14 "Piano " 10
MAME Emulation Notes
Latch Pending State and INT0
The MAME generic_latch_8_device tracks a data_pending flag that drives the Sub CPU’s INT0 input. Two emulation issues were discovered and fixed:
Issue 1: Stale pending state blocking INT0 (latch write without read)
On real hardware, each latch write generates a new INT0 edge/level regardless of previous state. In MAME, generic_latch’s data_pending_callback only fires on state changes — if the latch is already marked “written” (because the previous value wasn’t read due to a handshake flag check), subsequent writes silently update the value without triggering INT0.
Fix: Force-clear the pending state (acknowledge_w(0)) before each latch write, then write the new data. This ensures data_pending transitions 0→1 and the callback fires.
Issue 2: INT0 level-detect re-assertion causing runaway ISR loop
The TMP94C241 uses level-triggered INT0 (IIMC bit 1 = 0). On real hardware, the ISR reads the latch, which deasserts INT0 within the same clock cycle. In MAME, set_input_line(CLEAR_LINE) goes through synchronize() (see diexec.cpp:663-691), which defers the actual execute_set_input() call until after the current timeslice ends. This creates a window where m_level[INT0] remains ASSERT_LINE even though the latch has been read.
The TMP94C241’s check_irqs() contains a level-detect re-assertion path: after dispatching an INT0 ISR, if m_level[INT0] == ASSERT_LINE, it re-sets the INT0 interrupt flag. With the stale m_level, this causes INT0 to fire again immediately — creating a runaway loop where the ISR fires ~20 times, each reading stale/empty latch data, corrupting the command protocol.
Symptoms observed during boot:
- Main CPU sends E2 command byte to latch
- INT0 fires correctly, ISR reads E2
- INT0 re-fires spuriously (stale
m_level), ISR reads 0xFF - ISR interprets 0xFF as standard command: count=(0xFF & 0x1F)+1=32, dest=0x10F0
- HDMA ch0 now expects 32 bytes, but only 10 data bytes follow E2
- DMAC0 reaches 21 (not 0), so INTTC0 never fires
- Sub CPU never processes the E2 command → never responds to Main CPU
- Main CPU times out after 10 seconds waiting at
0xFB770F
Failed first approach: Removing the re-assertion code entirely fixed boot but broke button input — both CPUs’ INT0 lines are driven by latches, and the Main CPU needs re-assertion for processing Sub CPU responses during normal operation.
Fix: Added clear_int0_level() public method to tmp94c241_device that synchronously updates m_level[INT0] and clears the INT0 interrupt flag. The driver’s latch-read wrappers (subcpu_latch_r(), maincpu_latch_r()) call this immediately after generic_latch::read(), bypassing the deferred synchronize() mechanism.
This is safe because:
- Same-CPU context — the latch read occurs during the CPU’s own memory read (HDMA or instruction), so we’re modifying the CPU’s own state within its execution context
- Idempotent — the deferred
set_input_line(CLEAR)callback runs later butm_levelis alreadyCLEAR_LINE, soexecute_set_input()becomes a no-op - Re-assertion preserved — the level-detect re-assertion code in
check_irqs()stays intact, but now checks the correctm_levelvalue - New ASSERT works normally — when new data is written to the latch,
set_input_line(INT0, ASSERT)fires through the normal deferred path
CPU Scheduling for Latch Transfers
Each latch write must be followed by a context switch so the receiving CPU can process it via HDMA. Without this, the writing CPU continues its timeslice and writes multiple bytes before the receiver gets a chance to read, causing data loss.
Fix: After each latch write:
machine().scheduler().perfect_quantum(100µs)— ensures tight interleavingwriting_cpu->abort_timeslice()— forces immediate context switch
Main CPU → Sub CPU Cross-Reference
This table maps Main CPU sending routines to the Sub CPU handlers that process their commands, showing the complete data flow for each command range.
Command Flow Overview
Main CPU Sub CPU
──────── ───────
Audio_SendCommand
→ Audio_CommandEncoder
→ AssswbWr (ring buffer)
→ sendCOMM
→ InterCPU_Send_Data_Block → INT0 / MICRODMA_CH0_HANDLER
(latch write @ 0x120000) → CMD_DISPATCH_TABLE[bits 7-5]
→ Audio_CmdHandler_XX_XX
→ MIDI_Dispatch (for 0x00-0x1F)
Command Range Cross-Reference
| Cmd Range | Main CPU Sender | Main CPU Address | Sub CPU Handler | Purpose |
|---|---|---|---|---|
| 0x00-0x1F | sendCOMM (A=0) |
0xEF32F4 | Audio_CmdHandler_00_1F |
MIDI/audio data → ring buffer → MIDI_Dispatch |
| 0x20-0x3F | sendCOMM (A=1) |
0xEF32F4 | Audio_CmdHandler_20_3F |
Tone generator (stub/reserved) |
| 0x40-0x5F | sendCOMM (A=2) |
0xEF32F4 | Audio_CmdHandler_40_5F |
Buffer drain/flush (skips data) |
| 0x60-0x7F | sendCOMM (A=3) |
0xEF32F4 | Audio_CmdHandler_60_7F |
DSP streaming state machine |
| 0x80-0x9F | sendCOMM (A=4) |
0xEF32F4 | Serial1_DataTransmit_Loop |
Serial/MIDI TX to external port |
| 0xA0-0xBF | sendCOMM (A=5) |
0xEF32F4 | Audio_CmdHandler_A0_BF |
Tone generator mode/config |
| 0xC0-0xDF | sendCOMM (A=6) |
0xEF32F4 | Audio_CmdHandler_C0_FF |
Reserved (stub) |
| 0xE0-0xFF | sendCOMM (A=7) |
0xEF32F4 | Audio_CmdHandler_C0_FF |
Reserved (stub); E1/E2/E3 handled separately |
Special Command Cross-Reference
| Command | Main CPU Routine | Main CPU Address | Sub CPU Handler | Purpose |
|---|---|---|---|---|
| 0xE1 | InterCPU_E1_Bulk_Transfer |
0xEF3457 | InterCPU_RX_Handler (boot) / CH0_State2_E1 |
Two-phase bulk DMA (6-byte header + payload) |
| 0xE2 | InterCPU_E2_Send |
0xEF3555 | InterCPU_RX_Handler (boot) / CH0_State3_E2 |
Extended parameter transfer (10-byte header) |
| 0xE3 | SubCPU_Send_Payload |
0xEF0222 | InterCPU_RX_Handler (boot) |
Payload ready signal (sets bit 6 of 0x04FE) |
Boot-Time Transfer Cross-Reference
| Step | Main CPU Routine | Main CPU Address | Sub CPU Handler | Description |
|---|---|---|---|---|
| 1 | SubCPU_Send_Payload |
0xEF0222 | InterCPU_RX_Handler |
Transfer 192KB firmware payload via E1 bulk transfers |
| 2 | SubCPU_Send_Payload (E3) |
0xEF0222 | InterCPU_RX_Handler |
Signal payload ready → Sub CPU jumps to payload entry |
| 3 | SubCPU_Init_DMA_Channels |
0xEF329E | InterCPU_Latch_Setup |
Both CPUs configure DMA channels for runtime communication |
| 4 | SubCPU_Payload_Verify |
0xEF092B | — | Main CPU verifies payload checksums (no Sub CPU involvement) |
Audio Command Pipeline (Runtime)
The Main CPU’s Audio_SendCommand (called from 197+ locations) is the primary interface for sending audio parameters to the Sub CPU:
| Stage | Routine | Address | CPU | Description |
|---|---|---|---|---|
| 1. Lock | Audio_Lock_Acquire |
0xEF1FEE | Main | Acquire lock #7 (serializes audio commands) |
| 2. Format | Audio_CommandEncoder |
0xFF0ABC | Main | Printf-like formatter builds command packet |
| 3. Queue | AssswbWr |
0xFDBAEA | Main | Write to ring buffer at 0xBD3C (max 0x1FC bytes) |
| 4. Send | sendCOMM |
0xEF32F4 | Main | Chunk into ≤32-byte blocks, call InterCPU_Send_Data_Block |
| 5. Transfer | InterCPU_Send_Data_Block |
0xEF3492 | Main | Handshake + DMA write to latch at 0x120000 |
| 6. Receive | MICRODMA_CH0_HANDLER |
0x020F1F | Sub | DMA interrupt reads from latch, dispatches to handler |
| 7. Parse | MIDI_Dispatch |
0x034D93 | Sub | Parse MIDI status bytes, route to voice handlers |
| 8. Unlock | Audio_Lock_Release |
0xEF1F0F | Main | Release lock #7 |
Sub CPU MIDI Voice Handlers
After MIDI_Dispatch parses the ring buffer, it routes MIDI messages to these voice processing routines:
| MIDI Status | Handler | Description |
|---|---|---|
| 0x80/0x90 | Voice_NoteOn |
Note On/Off (velocity 0 = Note Off) |
| 0xB0 | Voice_CtrlChange |
Control Change (CC0-CC127 + proprietary 0x78-0x9D) |
| 0xC0 | Voice_ProgChange |
Program Change (voice selection) |
| 0xD0 | Voice_ChanPressure |
Channel Pressure (aftertouch) |
| 0xE0 | Voice_PitchBend |
Pitch Bend (14-bit value) |
| 0xF0 | Voice_SystemMsg |
System messages (reset, timing, etc.) |
Naming Conventions
The following naming patterns are used consistently across both CPUs:
| Main CPU Pattern | Sub CPU Pattern | Relationship |
|---|---|---|
Audio_Lock_* |
— | Serialization (Main CPU only, 8 lock slots) |
Audio_DMA_Transfer |
InterCPU_DMA_Send |
Core DMA routines (one per CPU direction) |
InterCPU_E1_* |
InterCPU_E1_DMA_Transfer |
E1 bulk transfer protocol (both sides) |
InterCPU_E2_Send |
Cmd_Check_E2_Pending |
E2 parameter transfer (send vs. deferred receive) |
InterCPU_Send_Data_Block |
MICRODMA_CH0_HANDLER |
Standard command send vs. receive dispatch |
SubCPU_Send_Payload |
InterCPU_RX_Handler (boot) |
Boot-time payload transfer |
SubCPU_Init_DMA_Channels |
InterCPU_Latch_Setup |
DMA channel initialization |
sendCOMM |
Audio_CmdHandler_* |
High-level send wrapper vs. per-range handlers |
Research Needed
- Document exact latch register bit layout - See Boot ROM Protocol above
- Analyze handshaking timing requirements - Uses 60,000 iteration timeout (~300ms)
- Document command 0x2D (DSP configuration) format — Complete: 4-layer protocol fully traced
- Document two ring buffers (0x2B0D for MIDI, 0x3B60 for DSP) — Complete
- Document command 0x2B (sound name query) format — Complete
- Document Sub CPU interrupt configuration (INTETC01, INTE0AD) — Complete
- Document HDMA-based command reception flow — Complete
- Document INT0 level-detect emulation issues — Complete
- Document MIDI message format in ring buffer (Note On/Off, CC, ProgramChange, PitchBend, etc.)
- Document command byte encoding (handler index in bits 7-5, length in bits 4-0)
- Document supported MIDI Control Changes (standard + proprietary 0x78-0x9D)
- Document voice processing architecture (26 channels, 287 bytes/channel at 0x041381)
- Cross-reference Main CPU and Sub CPU symbol names — Complete: see cross-reference tables above
- Document handlers 2 (0x40-0x5F), 5 (0xA0-0xBF), 6-7 (0xC0-0xFF) — undocumented command ranges
- Map status response message formats
- Determine actual subprogram storage location (table_data vs custom_data flash)