MAME Pull Requests
MAME Pull Requests for KN5000 Emulation
This page describes the planned series of pull requests to upstream MAME that implement functional emulation of the Technics KN5000 music keyboard. The changes span the TLCS-900/H CPU core, the TMP94C241 microcontroller, and the KN5000 machine driver.
Context: The Technics KN5000 is a 1996 professional arranger keyboard built around dual Toshiba TMP94C241F processors (TLCS-900/H family). All firmware ROMs have been dumped and fully disassembled. A MAME skeleton driver has existed since 2024 but could not proceed beyond early boot due to missing CPU core features. These PRs fix that, bringing the driver from non-functional to fully operational with working sound program selection, control panel interaction, and MIDI I/O.
See Also: SubCPU Payload Loading for the investigation that identified these issues, Boot Sequence for overall firmware flow, and Control Panel Protocol for the serial protocol implemented by the HLE.
Dependency Graph
The PRs form a dependency chain. PR 1 and PR 2 are independent and can be reviewed simultaneously. Each subsequent PR builds on the previous.
PR 1 (LDC CR mapping) ──┐
├── PR 3 (DMA + port fix) ── PR 4 (serial) ── PR 5 (KN5000 driver)
PR 2 (EI/RETI shadow) ──┘
Overview of Changes
| PR | Scope | Files Changed | New Files | Risk Level |
|---|---|---|---|---|
| 1 | TLCS-900/H core | 900tbl.hxx, dasm900.cpp, dasm900.h, tmp94c241.cpp, tmp95c061.cpp, tmp95c063.cpp, tmp96c141.cpp | — | None (additive + structural refactor) |
| 2 | TLCS-900/H core | 900tbl.hxx, tlcs900.cpp, tlcs900.h | — | Low (all variants) |
| 3 | TMP94C241 only | tmp94c241.cpp, tmp94c241.h | — | None (TMP94C241 only) |
| 4 | TMP94C241 only | tmp94c241.cpp, tmp94c241.h | tmp94c241_serial.cpp/h | None (TMP94C241 only) |
| 5 | KN5000 driver | kn5000.cpp, kn5000.lay | kn5000_cpanel.cpp/h | None (KN5000 only) |
PR 1: TLCS-900/H — LDC Control Register Mapping for TMP94C241 DMA Registers
Upstream PR: mamedev/mame#14970 (merged)
Branch: kn5000_pr1_ldc_cr_mapping
Summary
Add TMP94C241 DMA control register encodings to the TLCS-900/H CPU core’s LDC instruction handler, and refactor the disassembler’s CR register name resolution from hardcoded switch blocks into model-specific symbol tables.
Problem
The LDC (Load Control Register) instruction uses an immediate byte to select which internal register to access. The TLCS-900/H family shares the same instruction encoding, but different chip variants place their DMA registers at different offsets within the control register space.
MAME’s existing implementation only handles the TMP96C141/TMP95C061/TMP95C063 encodings:
| Register | TMP96C141/TMP95C061/TMP95C063 | TMP94C241 |
|---|---|---|
| DMAM0-3 (CR8) | 0x22, 0x26, 0x2A, 0x2E | 0x42, 0x46, 0x4A, 0x4E |
| DMAC0-3 (CR16) | 0x20, 0x24, 0x28, 0x2C | 0x40, 0x44, 0x48, 0x4C |
| DMAD0-3 (CR32) | 0x10, 0x14, 0x18, 0x1C | 0x20, 0x24, 0x28, 0x2C |
Without the TMP94C241 cases, LDC cr,DMAMn instructions write to a dummy register and DMA never configures correctly. This was the first bug identified in the KN5000 payload loading investigation — the Main CPU’s firmware uses LDC extensively to set up HDMA transfers for the 524KB SubCPU firmware payload.
Additionally, the disassembler had all variants’ CR register names hardcoded in the shared dasm900.cpp, which doesn’t scale well as new variants are added. The existing SFR symbolic names already use a model-specific table pattern — CR register names should follow the same approach.
Changes
-
900tbl.hxx: Add TMP94C241 cases to the six existingswitchblocks inprepare_operands()(three for operand 1, three for operand 2). Each block gains four newcaseentries mapping the TMP94C241 offsets to the samem_dmam[],m_dmac[], andm_dmad[]arrays. Existing TMP96C141/TMP95C061/TMP95C063 cases are unchanged. -
dasm900.h: Add acr_symstruct ({size, encoding, name}) for model-specific control register names, a new constructor overload accepting a CR symbol table alongside the existing SFR symbol table, and a privatecr_name()lookup helper. -
dasm900.cpp: Replace six hardcodedswitchblocks forO_CR8/O_CR16/O_CR32operands (two copies — one for operand 1, one for operand 2) with table-driven lookups viacr_name(). This removes all variant-specific register names from the shared disassembler code. -
tmp94c241.cpp: Define atmp94c241_cr_syms[]table with TMP94C241 DMA register encodings and pass it to the disassembler constructor. -
tmp96c141.cpp,tmp95c061.cpp,tmp95c063.cpp: Move their existing DMA CR register names (previously hardcoded indasm900.cpp) into model-specific*_cr_syms[]tables, following the same pattern.
Risk Assessment
None. The CPU core change only adds new case entries to existing switch statements — all existing cases are untouched and the new encodings do not overlap with any existing variant’s register map. The disassembler refactoring is purely structural: the same register names are resolved for the same encodings, just driven by per-variant tables instead of a shared switch. No existing driver is affected.
Design Note: Why 900tbl.hxx Is Not Model-Specific
The disassembler’s CR register names were moved to model-specific files, but the emulation-side CR register mapping in 900tbl.hxx (prepare_operands()) was intentionally left as a shared switch with all variants’ cases together. We considered the same model-specific split but decided against it for three reasons:
-
Performance.
prepare_operands()runs on every instruction inexecute_run()— it is the hottest path in the CPU emulation. The compiler optimizes theswitchinto a jump table. Replacing it with virtual dispatch or a table lookup would add overhead to every LDC instruction for no functional benefit. -
No overlap. The encoding values are disjoint between variants (TMP94C241 uses 0x42–0x4e for DMAM, TMP96C141 uses 0x22–0x2e). A TMP94C241 binary will never contain TMP96C141 encodings, and vice versa. Both sets of
caseentries coexist harmlessly in the same switch — extra cases that are never reached have zero runtime cost. -
Alternatives are worse. Separating the behavior would require one of: (a) virtual method dispatch per LDC instruction, (b) a runtime table search, (c) overriding the entire 2000+ line
prepare_operands()in each variant, duplicating 99% shared logic, or (d) anif (type() == TMP94C241)guard before variant-specific switch blocks, which adds a runtime branch on every LDC and duplicates the shared DMAS0–3 cases into both branches. None of these improve correctness, and all add complexity or overhead.
The encoding byte itself acts as an implicit variant discriminator — a TMP94C241 binary will never emit encoding 0x22 for DMAM0, so the TMP96C141 case for 0x22 is simply never reached when running TMP94C241 firmware. The compiler merges all cases into a single jump table where unreachable entries cost nothing. Each case in the source is annotated with a comment identifying which variant(s) use that encoding, making the shared switch self-documenting.
The disassembler refactoring was worthwhile because disassembly is not performance-critical and the model-specific symbol table pattern was already established. For the instruction decoder, extra case entries in a shared switch are the idiomatic MAME approach for handling variant differences in the TLCS-900 core.
Datasheet Reference
The TMP94C241 control register map is documented in the Toshiba TMP94C241F Data Sheet, Section 5 “DMA Controller”, Table 5-1 “DMA Register Map”. The offsets differ from TMP96C141 because the TMP94C241 has a larger internal register file with additional peripheral blocks occupying the 0x20-0x3F range.
PR 2: TLCS-900/H — EI/RETI Interrupt Acceptance Shadow
Upstream PR: mamedev/mame#14995 (merged)
Branch: kn5000_pr2_irq_inhibit
Summary
Implement the documented 1-instruction interrupt deferral after EI and RETI instructions, matching real TLCS-900/H hardware behavior.
Problem
Per the Toshiba TLCS-900/H Programming Manual (Section 8.2 “Interrupt Processing”), interrupt acceptance is inhibited for one instruction following EI or RETI. This is an architectural feature of the TLCS-900/H family, not specific to any single variant.
Without this deferral, the common firmware pattern:
EI 0 ; Enable all interrupt levels
NOP ; This MUST execute before any IRQ is accepted
EI 6 ; Restrict to level 6+ only
fails because EI 0 immediately triggers a pending interrupt before NOP executes. The interrupt handler runs, returns via RETI, and the CPU re-enters the handler infinitely — EI 6 never executes, and the CPU is stuck.
This pattern appears extensively in the KN5000 firmware (hundreds of occurrences) and likely in other TLCS-900/H firmware as well. The real hardware’s 1-instruction shadow prevents this problem.
Changes
-
tlcs900.h: Addbool m_irq_inhibitmember totlcs900_device(the base class for all TLCS-900/H variants). tlcs900.cpp:- Register
m_irq_inhibitindevice_start()for save states. - Initialize to
falsein bothtlcs900_device::device_reset()andtlcs900h_device::device_reset(). - In
execute_run(): whenm_check_irqsis set andm_irq_inhibitis true, clear the inhibit flag but skip the IRQ check for this cycle. On the next iteration (after one instruction has executed), the IRQ check proceeds normally. Ifm_check_irqsis not set, clearm_irq_inhibitunconditionally.
- Register
900tbl.hxx: Setm_irq_inhibit = trueinop_EI()andop_RETI().
Risk Assessment
Low. This change affects all TLCS-900/H variants (TMP96C141, TMP95C061, TMP95C063, TMP94C241). It is documented hardware behavior, and the implementation is minimal (a single boolean flag). If any existing driver were relying on the incorrect immediate-acceptance behavior (e.g., using EI without a guard NOP), it could see a timing change. However, real hardware has always had this shadow, so any such driver was already accidentally working.
Drivers using TLCS-900/H variants that should be regression-tested:
ngp(Neo Geo Pocket — TMP95C061)zorba(Zorba portable — TMP95C063)
Datasheet Reference
Toshiba TLCS-900/H Programming Manual, Section 8.2: “After the EI instruction or RETI instruction is executed, the interrupt acceptance is inhibited for one instruction period.”
PR 3: TMP94C241 — DMA Subsystem (HDMA + DMAR) and Port Read Fix
Upstream PR: mamedev/mame#15003 (merged)
Branch: kn5000_pr3_dma_and_port
Depends on: PR 1 + PR 2
Summary
Implement the TMP94C241’s complete DMA subsystem and fix the port read behavior to match real hardware.
Problem
The existing TMP94C241 device in MAME has empty stubs for tlcs900_check_hdma() and no DMA transfer logic. The KN5000’s Main CPU relies heavily on DMA for two critical operations:
-
HDMA (Hardware DMA): Triggered by interrupt events. The Main CPU uses HDMA to transfer the 524KB SubCPU firmware payload. Each time the SubCPU acknowledges a byte via INT5, the HDMA engine automatically transfers the next byte without CPU intervention. Nine bulk transfers (5x 64KB config blocks + 4 payload blocks) use this mechanism.
-
DMAR (Software DMA): Triggered by writing to SFR register 0x109. The SubCPU uses DMAR to receive inter-CPU command bytes — each INT0 event triggers one DMAR write, transferring a single byte from the latch at
0x120000to RAM. This is how the SubCPU receives sound program change commands, mixer settings, and other real-time data from the Main CPU after boot.
Additionally, port_r() always returned the external pin level, but real TMP94C241 hardware returns the output latch value for bits configured as output. Firmware that reads back its own output port state (a common pattern for read-modify-write operations) got wrong values.
Changes
tmp94c241.h:- Add declarations for
tlcs900_process_hdma(),tlcs900_process_software_dma(), anddmar_w().
- Add declarations for
tmp94c241.cpp:tlcs900_process_hdma(channel): Implements one HDMA transfer for a channel. Looks up the channel’s DMA start vector, checks if the corresponding interrupt flag is pending (the DMA trigger condition), performs one transfer according to the DMAM mode register, decrements the transfer count, and fires INTTC on completion. Clears the triggering interrupt flag after the transfer (consuming the interrupt instead of dispatching to the handler).tlcs900_process_software_dma(channel): Same transfer logic as HDMA but triggered by DMAR register writes instead of interrupt events. One write = one transfer unit.tlcs900_check_hdma(): Checks all four channels in priority order (0 highest). Only processes one transfer per call to maintain correct timing. Skips processing when all interrupts are masked.tlcs900_check_irqs(): Added HDMA priority — interrupts targeted by active HDMA channels are skipped (consumed by DMA instead of dispatching to the interrupt handler). Added INT0 level-detect re-assertion for level-triggered interrupt mode. Addeddebug()->interrupt_hook()for MAME debugger integration.dmar_w(): Handler for SFR 0x109 writes. Each set bit triggers one software DMA transfer on the corresponding channel.port_r(): Now returns(latch & direction) | (external & ~direction), correctly mixing output latch bits with external pin levels based on the port control register.- DMAM decoding: Uses switch-based transfer mode decoding (byte/word/long, increment/decrement/fixed) matching the proven TMP95C061 implementation already in MAME.
Risk Assessment
None. All changes are within the TMP94C241 device. No other TLCS-900/H variant or driver is affected.
Datasheet Reference
TMP94C241F Data Sheet:
- Section 5.1 “HDMA Operation” — interrupt-triggered DMA with priority over normal interrupt dispatch
- Section 5.2 “DMAR Register” — software DMA trigger at SFR 0x109
- Section 5.3 “DMAM Register” — transfer mode encoding (bits 4:0)
- Section 9.2 “Port Read Operation” — output latch vs. external pin multiplexing based on PxCR
PR 4: TMP94C241 — Serial Port Sub-Device
Upstream PR: mamedev/mame#15015 (merged)
Branch: kn5000_pr4_serial
Depends on: PR 3
Summary
Replace the inline serial port stubs with a proper sub-device implementation supporting synchronous I/O interface mode, baud rate generation, TX double buffering, and data callbacks for connecting external devices.
Problem
The existing TMP94C241 serial implementation consists of inline stubs that immediately set the TX-complete interrupt flag on every write to SCxBUF. This was sufficient for early bringup but cannot support any real serial communication. The KN5000 uses both serial channels:
- Serial Channel 0: MIDI In/Out at 31.25 kbaud (standard MIDI rate). Uses UART mode with the baud rate generator.
- Serial Channel 1: Control panel communication at ~500 kHz using synchronous I/O interface mode. The Main CPU exchanges button/LED/encoder data with the control panel MCU (whose ROM is not dumped) via a clocked serial protocol.
Changes
tmp94c241_serial.h(new file): Device class fortmp94c241_serial_device. Exposes:txd(),rxd(),sclk_out(),sclk_in()callbacks for connecting to external devicestx_start()callback signaling the start of each byte transmission (with PFFC pin function state, allowing connected devices to distinguish real transmissions from phantom ones during pin reconfiguration)- SFR register accessors:
scNbuf_r/w,scNcr_r/w,scNmod_r/w,brNcr_r/w
tmp94c241_serial.cpp(new file): Full implementation:- I/O interface mode (SCxMOD bits 1:0 = 0): Synchronous clocked serial. Supports internal clock (baud rate generator) and external clock (IOC=1) sources. Data is shifted MSB-first on clock edges.
- Baud rate generator: Configurable via BRxCR register with 4-bit divisor and clock source selection. Drives a timer that toggles SCLK at the configured rate.
- TX double buffering: CPU writes to SCxBUF go to the TX buffer. If the shift register is idle, the buffer auto-transfers immediately. If the shift register is busy, the buffer holds data until the current byte finishes, then auto-loads on the trailing rising edge. This matches real TMP94C241 hardware behavior and prevents byte loss during back-to-back transmissions.
- TX/RX shift registers: 8-bit shift with proper edge timing. TX pre-outputs bit 0 before the first clock edge so the receiver can sample it on the rising edge. RX samples on rising edges and fires INTRX when 8 bits are received.
- SCLK output: Forwarded to connected devices via callbacks, enabling clocked protocols.
- TO2 trigger: Timer 1 match events are forwarded to serial channels for mode 0 transfer gating.
-
tmp94c241.cpp: Replace inline serial stubs with sub-device delegation. Serial register mappings ininternal_mem()now route tom_serial[0]andm_serial[1]. Adddevice_add_mconfig()to instantiate the two serial sub-devices. Set TX-complete flags (INTES0/INTES1bit 7) atdevice_reset()to indicate empty TX buffers at power-on. tmp94c241.h: Add#include "tmp94c241_serial.h", friend declaration,device_add_mconfig()override,required_device_array<tmp94c241_serial_device, 2> m_serial. Remove old inline serial member variables (m_serial_control,m_serial_mode,m_baud_rate).
Risk Assessment
None. All changes are within the TMP94C241 device and its new sub-device. No other driver is affected. The sub-device’s callbacks are optional — if not connected by a driver, they are no-ops, preserving backward compatibility.
Datasheet Reference
TMP94C241F Data Sheet:
- Section 7 “Serial Interface” — I/O interface mode, UART mode, baud rate generator
- Section 7.3 “Baud Rate Generator” — divisor and clock source configuration
- Section 7.4 “Transmit/Receive Operation” — shift register timing, double buffering
- Table 7-1 “Serial Control Register (SCxCR)” — IOC, SCLKS, error flags
PR 5: KN5000 Driver — Control Panel HLE, SubCPU Payload Transfer, Keybed HLE
Upstream PR: mamedev/mame#15143 (merged)
Branch: kn5000_pr5_driver
Depends on: PR 4
Summary
Rework the KN5000 machine driver from a non-functional skeleton to a working system that boots to normal operation with sound program selection, control panel interaction, and MIDI I/O.
Problem
The existing KN5000 driver defines the basic hardware layout (two TMP94C241 CPUs, ROM regions, LCD) but cannot proceed past early boot because:
- The firmware validates backup SRAM contents and enters diagnostic mode if validation fails (NVRAM is empty on first run).
- The SubCPU memory map lacks mappings for the tone generator, DSP, inter-CPU latch, and waveform RAM, causing unmapped memory accesses.
- No scheduling synchronization between CPUs, making the latched inter-CPU communication unreliable.
- The control panel MCU ROM is not dumped, so the serial protocol to the panel has no responder.
- No keybed input mechanism exists.
Changes
kn5000.cpp(major rework):- NVRAM factory defaults: Seeds the 32KB backup SRAM from the program ROM’s factory default region (
0xEF3AC8). On real hardware, this data is written during factory programming. In MAME, the NVRAM starts empty, so the firmware’s validation checksum fails and it enters a diagnostic loop. Seeding from the ROM image allows normal boot to proceed. On subsequent runs, the NVRAM file persists with any user changes. - SubCPU memory map: Maps the tone generator registers at
0x100000/0x100002(IC303, TC183C230002), tone generator keyboard scanning at0x110000/0x110002, the inter-CPU latch at0x120000, DSP registers at0x130000/0x130002, and 4MB waveform RAM at0x200000-0x5FFFFF. These are stub handlers that log accesses and return appropriate idle values. - Inter-CPU latch: Read/write handlers with logging and
machine().scheduler().perfect_quantum(attotime::from_usec(100))on writes. The perfect quantum ensures both CPUs see latch updates atomically, which is critical for the HDMA-based payload transfer where the Main CPU writes a byte to the latch and the SubCPU’s HDMA must read it before the next byte arrives. - MIDI ports: Serial channel 0 of both CPUs is wired to
midi_portdevices for standard MAME MIDI I/O integration.
- NVRAM factory defaults: Seeds the 32KB backup SRAM from the program ROM’s factory default region (
kn5000_cpanel.cpp(new file): Control panel HLE implementing the serial protocol between the Main CPU and the control panel MCU. The control panel MCU’s ROM is not dumped, so HLE is the only option. Key features:- INTA-driven sessions: The panel asserts INTA to request attention, then exchanges button/LED data via synchronous serial on channel 1. Each session consists of 8 segments of 8 bytes each (64 bytes total), carrying button states inbound and LED states outbound.
- Button mapping: 85+ buttons mapped to MAME input ports, organized by panel region (transport, sound selection, accompaniment, registration, numeric pad, etc.).
- LED output routing: LED states received from the firmware are routed to MAME layout output elements, enabling the layout to display button backlighting.
- Rotary encoder input: Tempo, volume, and data entry encoders generate delta values in the protocol format expected by the firmware.
-
kn5000_cpanel.h(new file): Header for the control panel HLE device class. kn5000.lay: LED output element names updated to match the names emitted by the control panel HLE, enabling proper LED visualization.
Result
With all five PRs applied, the KN5000 driver:
- Boots from power-on through the complete boot sequence (ROM validation, SubCPU payload transfer, hardware initialization)
- Displays the main screen with voice names (Piano, Bigband Brass, Modern E.P.1), rhythm patterns, and mixer levels
- Responds to control panel buttons for sound selection, menu navigation, and parameter editing
- Accepts MIDI input for external control
- SubCPU processes inter-CPU commands in real-time (no “Sound Name Error” messages)
Risk Assessment
None. All changes are within the KN5000 driver and its new HLE device. No other driver or shared code is affected.
Testing Notes
The complete change set has been tested with the following ROM set:
kn5000_v10_program.ic9(Main CPU, 2MB)kn5000_subcpu_boot.ic30(SubCPU boot ROM, 128KB)kn5000_table_data.ic8(Table data, 2MB)kn5000_waverom1.ic5throughkn5000_waverom4.ic2(Wave ROM, 4x 8MB)
Test scenarios verified:
- Cold boot with empty NVRAM (factory defaults are seeded)
- Warm boot with existing NVRAM (user settings preserved)
- SubCPU payload transfer (524KB via HDMA, all 9 transfer blocks complete)
- Sound program selection via control panel buttons
- Menu navigation (display updates correctly)
- MIDI note input
- Keybed note input (when assigned via MAME input configuration)