From 9300483e5455a701a24dc2db5cd5fb1764669830 Mon Sep 17 00:00:00 2001 From: John Hardy Date: Wed, 4 Feb 2026 13:55:56 +1100 Subject: [PATCH 1/2] fix(tec1g): emulate GLCD reverse and dummy read --- src/platforms/tec1g/runtime.ts | 25 ++++++++++++++++++++++++- tests/platforms/tec1g/glcd.test.ts | 12 ++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/platforms/tec1g/runtime.ts b/src/platforms/tec1g/runtime.ts index 90b39dd..d8359f2 100644 --- a/src/platforms/tec1g/runtime.ts +++ b/src/platforms/tec1g/runtime.ts @@ -168,6 +168,8 @@ export interface Tec1gState { glcdExpectColumn: boolean; glcdRe: boolean; glcdGraphics: boolean; + glcdReadPrimed: boolean; + glcdReadLatch: number; glcdDisplayOn: boolean; glcdCursorOn: boolean; glcdCursorBlink: boolean; @@ -370,6 +372,8 @@ export function createTec1gRuntime( glcdExpectColumn: false, glcdRe: false, glcdGraphics: false, + glcdReadPrimed: false, + glcdReadLatch: 0, glcdDisplayOn: true, glcdCursorOn: false, glcdCursorBlink: false, @@ -486,6 +490,7 @@ export function createTec1gRuntime( state.glcdRowAddr = value & TEC1G_MASK_LOW5; state.glcdExpectColumn = true; state.glcdGdramPhase = 0; + state.glcdReadPrimed = false; }; const glcdSetColumn = (value: number): void => { @@ -494,6 +499,7 @@ export function createTec1gRuntime( state.glcdCol = value & TEC1G_GLCD_COL_MASK; state.glcdExpectColumn = false; state.glcdGdramPhase = 0; + state.glcdReadPrimed = false; }; // ST7920 DDRAM row address to linear index mapping. @@ -633,13 +639,26 @@ export function createTec1gRuntime( const col = state.glcdCol & TEC1G_GLCD_COL_MASK; const index = row * TEC1G_GLCD_ROW_STRIDE + col * TEC1G_GLCD_COL_STRIDE + state.glcdGdramPhase; const value = index >= 0 && index < state.glcd.length ? (state.glcd[index] ?? 0) : 0; + if (!state.glcdReadPrimed) { + state.glcdReadPrimed = true; + state.glcdReadLatch = value & TEC1G_MASK_BYTE; + return 0; + } + const out = state.glcdReadLatch & TEC1G_MASK_BYTE; if (state.glcdGdramPhase === 0) { state.glcdGdramPhase = 1; } else { state.glcdGdramPhase = 0; state.glcdCol = (state.glcdCol + 1) & TEC1G_GLCD_COL_MASK; } - return value & TEC1G_MASK_BYTE; + const nextIndex = + ((state.glcdRowBase + state.glcdRowAddr) & TEC1G_GLCD_ROW_MASK) * + TEC1G_GLCD_ROW_STRIDE + + (state.glcdCol & TEC1G_GLCD_COL_MASK) * TEC1G_GLCD_COL_STRIDE + + state.glcdGdramPhase; + state.glcdReadLatch = + nextIndex >= 0 && nextIndex < state.glcd.length ? (state.glcd[nextIndex] ?? 0) : 0; + return out; }; const glcdReadStatus = (): number => { @@ -1053,6 +1072,8 @@ export function createTec1gRuntime( state.glcdGraphics = g; state.glcdExpectColumn = false; state.glcdGdramPhase = 0; + state.glcdReadPrimed = false; + state.glcdReadLatch = 0; glcdSetBusy(TEC1G_GLCD_BUSY_US); queueUpdate(); return; @@ -1356,6 +1377,8 @@ export function createTec1gRuntime( state.glcdExpectColumn = false; state.glcdRe = false; state.glcdGraphics = false; + state.glcdReadPrimed = false; + state.glcdReadLatch = 0; state.glcdDisplayOn = true; state.glcdCursorOn = false; state.glcdCursorBlink = false; diff --git a/tests/platforms/tec1g/glcd.test.ts b/tests/platforms/tec1g/glcd.test.ts index deca3f1..fbf58e6 100644 --- a/tests/platforms/tec1g/glcd.test.ts +++ b/tests/platforms/tec1g/glcd.test.ts @@ -44,10 +44,22 @@ describe('TEC-1G GLCD instruction handling', () => { rt.state.glcdRowBase = 0; rt.state.glcdCol = 0; rt.state.glcdGdramPhase = 0; + const dummy = rt.ioHandlers.read(0x87); const value = rt.ioHandlers.read(0x87); + expect(dummy).toBe(0x00); expect(value).toBe(0xaa); }); + it('toggles reverse line mask in extended mode', () => { + const rt = makeRuntime(); + rt.ioHandlers.write(0x07, 0x24); // function set: RE=1, G=0 + rt.ioHandlers.write(0x07, 0x04); // reverse line 0 + expect(rt.state.glcdReverseMask & 0x01).toBe(0x01); + rt.ioHandlers.write(0x07, 0x04); // toggle off + expect(rt.state.glcdReverseMask & 0x01).toBe(0x00); + rt.ioHandlers.write(0x07, 0x20); // back to basic + }); + it('busy flag clears after cycles', () => { const rt = makeRuntime(); rt.ioHandlers.write(0x07, 0x80); From fc72d39495f5c03a6fe1727233c9d464c674577f Mon Sep 17 00:00:00 2001 From: John Hardy Date: Sun, 8 Feb 2026 02:21:00 +1100 Subject: [PATCH 2/2] feat(tec1g): add config DIP switches and GLCD text mask --- docs/tec1g-emulation-review.md | 14 +++++++------- src/platforms/tec1g/README.md | 9 +++++++-- src/platforms/tec1g/constants.ts | 2 ++ src/platforms/tec1g/runtime.ts | 7 ++++++- src/platforms/types.ts | 2 ++ tests/platforms/tec1g/glcd.test.ts | 1 + tests/platforms/tec1g/lcd.test.ts | 1 + tests/platforms/tec1g/matrix.test.ts | 1 + tests/platforms/tec1g/port03.test.ts | 7 +++++++ tests/platforms/tec1g/portfc.test.ts | 1 + tests/platforms/tec1g/portfd.test.ts | 1 + tests/platforms/tec1g/serial.test.ts | 1 + webview/tec1g/index.ts | 14 +++++++++++--- 13 files changed, 48 insertions(+), 13 deletions(-) diff --git a/docs/tec1g-emulation-review.md b/docs/tec1g-emulation-review.md index 4ac2e74..65d7a0c 100644 --- a/docs/tec1g-emulation-review.md +++ b/docs/tec1g-emulation-review.md @@ -372,11 +372,11 @@ Disco LEDs (Fullisik under mechanical keys) — **N/A** ### 17. CONFIG DIP switch -| Feature | Rating | Notes | -| ----------------------------------------- | ----------- | ------------------------- | -| Switch 1: Keyboard mode (74C923 / Matrix) | **Missing** | Always in 74C923 mode | -| Switch 2: Protect on reset (OFF / ON) | **Missing** | Protect always starts OFF | -| Switch 3: Expansion bank (LO / HI) | **Missing** | No bank select | +| Feature | Rating | Notes | +| ----------------------------------------- | ------------ | ------------------------------------------------- | +| Switch 1: Keyboard mode (74C923 / Matrix) | **Complete** | Configurable via `matrixMode` | +| Switch 2: Protect on reset (OFF / ON) | **Complete** | Configurable via `protectOnReset` | +| Switch 3: Expansion bank (LO / HI) | **Complete** | Configurable via `expansionBankHi` | ### 18. Joystick (J9) @@ -402,8 +402,8 @@ Disco LEDs (Fullisik under mechanical keys) — **N/A** | Expansion window | Med | **Partial** | 60% | | SYS_CTRL latch (0xFF) | Med | **Partial** | 37% | | SYS_INPUT register (0x03) | Med | **Partial** | 50% | -| Matrix keyboard | Med | **Missing** | 0% | -| CONFIG DIP switch | Low | **Missing** | 0% | +| Matrix keyboard | Med | **Complete** | 100% | +| CONFIG DIP switch | Low | **Complete** | 100% | | RTC (DS1302) | Low | **Complete** | 100% | | SD card SPI | Low | **Missing** | 0% | | Cartridge | Low | **Partial** | 40% | diff --git a/src/platforms/tec1g/README.md b/src/platforms/tec1g/README.md index 7cbbf1a..138be4c 100644 --- a/src/platforms/tec1g/README.md +++ b/src/platforms/tec1g/README.md @@ -13,7 +13,6 @@ workflow and hardware contract. For full MON-3 behavior notes, see - RTC (DS1302) and SD SPI (0xFC/0xFD) when enabled in config. ## Not yet emulated -- Matrix keyboard input on `IN 0xFE` (and keypad-disable behavior when matrix mode is active). - Cartridge boot entry uses CART flag (MON-3 style) and maps payload into expansion banks. - SYS_CTRL bits 3-7: latched and decoded but bank switching not yet wired to memory. - SYS_INPUT bits 0 (SKEY), 4 (RKEY), 5 (GIMP): state exposed but no hardware trigger wired. @@ -107,10 +106,16 @@ config (and optionally ROM listings via `extraListings`). "tec1g": { "romHex": "../roms/tec1g/mon-3/mon-3.hex", "appStart": 16384, - "entry": 0 + "entry": 0, + "matrixMode": false, + "protectOnReset": false, + "expansionBankHi": false } } ``` +`matrixMode`, `protectOnReset`, and `expansionBankHi` correspond to the CONFIG DIP +switches (keyboard mode, protect on reset, expansion bank select). + ## Examples - `examples/Tec1g` includes a 4800-baud serial echo program for MON-3. diff --git a/src/platforms/tec1g/constants.ts b/src/platforms/tec1g/constants.ts index 1e9170c..3b96249 100644 --- a/src/platforms/tec1g/constants.ts +++ b/src/platforms/tec1g/constants.ts @@ -59,6 +59,8 @@ export const TEC1G_ENTRY_DEFAULT = 0x0000; export const TEC1G_ADDR_MAX = 0xffff; // ===== System Control Bits ===== +/** Write protection enabled (sysctrl bit 1). */ +export const TEC1G_SYSCTRL_PROTECT = 0x02; /** Expansion bank A14 select (sysctrl bit 3). */ export const TEC1G_SYSCTRL_BANK_A14 = 0x08; diff --git a/src/platforms/tec1g/runtime.ts b/src/platforms/tec1g/runtime.ts index d8359f2..69eed60 100644 --- a/src/platforms/tec1g/runtime.ts +++ b/src/platforms/tec1g/runtime.ts @@ -84,6 +84,7 @@ import { TEC1G_PORT_STATUS, TEC1G_PORT_SYSCTRL, TEC1G_ADDR_MAX, + TEC1G_SYSCTRL_PROTECT, TEC1G_SYSCTRL_BANK_A14, TEC1G_KEY_SHIFT_MASK, TEC1G_LCD_ARROW_LEFT, @@ -301,6 +302,7 @@ export function normalizeTec1gConfig(cfg?: Tec1gPlatformConfig): Tec1gPlatformCo const gimpSignal = config.gimpSignal === true; const expansionBankHi = config.expansionBankHi === true; const matrixMode = config.matrixMode === true; + const protectOnReset = config.protectOnReset === true; const rtcEnabled = config.rtcEnabled === true; const sdEnabled = config.sdEnabled === true; const sdImagePath = @@ -319,6 +321,7 @@ export function normalizeTec1gConfig(cfg?: Tec1gPlatformConfig): Tec1gPlatformCo gimpSignal, expansionBankHi, matrixMode, + protectOnReset, rtcEnabled, sdEnabled, ...(sdImagePath !== undefined ? { sdImagePath } : {}), @@ -341,7 +344,9 @@ export function createTec1gRuntime( onSerialByte?: (byte: number) => void, onPortWrite?: (payload: { port: number; value: number }) => void ): Tec1gRuntime { - const initialSysCtrl = config.expansionBankHi ? TEC1G_SYSCTRL_BANK_A14 : 0; + const initialSysCtrl = + (config.expansionBankHi ? TEC1G_SYSCTRL_BANK_A14 : 0) | + (config.protectOnReset ? TEC1G_SYSCTRL_PROTECT : 0); const matrixMode = config.matrixMode; const rtcEnabled = config.rtcEnabled; const rtc = rtcEnabled ? new Ds1302() : null; diff --git a/src/platforms/types.ts b/src/platforms/types.ts index a212b10..81fa507 100644 --- a/src/platforms/types.ts +++ b/src/platforms/types.ts @@ -63,6 +63,7 @@ export interface Tec1gPlatformConfig { gimpSignal?: boolean; expansionBankHi?: boolean; matrixMode?: boolean; + protectOnReset?: boolean; rtcEnabled?: boolean; sdEnabled?: boolean; sdImagePath?: string; @@ -91,6 +92,7 @@ export interface Tec1gPlatformConfigNormalized { gimpSignal: boolean; expansionBankHi: boolean; matrixMode: boolean; + protectOnReset: boolean; rtcEnabled: boolean; sdEnabled: boolean; sdImagePath?: string; diff --git a/tests/platforms/tec1g/glcd.test.ts b/tests/platforms/tec1g/glcd.test.ts index fbf58e6..58ac770 100644 --- a/tests/platforms/tec1g/glcd.test.ts +++ b/tests/platforms/tec1g/glcd.test.ts @@ -16,6 +16,7 @@ function makeRuntime() { gimpSignal: false, expansionBankHi: false, matrixMode: false, + protectOnReset: false, rtcEnabled: false, sdEnabled: false, }; diff --git a/tests/platforms/tec1g/lcd.test.ts b/tests/platforms/tec1g/lcd.test.ts index de6768b..7709427 100644 --- a/tests/platforms/tec1g/lcd.test.ts +++ b/tests/platforms/tec1g/lcd.test.ts @@ -16,6 +16,7 @@ function makeRuntime() { gimpSignal: false, expansionBankHi: false, matrixMode: false, + protectOnReset: false, rtcEnabled: false, sdEnabled: false, }; diff --git a/tests/platforms/tec1g/matrix.test.ts b/tests/platforms/tec1g/matrix.test.ts index b036638..81fa78a 100644 --- a/tests/platforms/tec1g/matrix.test.ts +++ b/tests/platforms/tec1g/matrix.test.ts @@ -16,6 +16,7 @@ function makeRuntime(matrixMode = true) { gimpSignal: false, expansionBankHi: false, matrixMode, + protectOnReset: false, rtcEnabled: false, sdEnabled: false, }; diff --git a/tests/platforms/tec1g/port03.test.ts b/tests/platforms/tec1g/port03.test.ts index 71d1103..0984b9c 100644 --- a/tests/platforms/tec1g/port03.test.ts +++ b/tests/platforms/tec1g/port03.test.ts @@ -17,6 +17,7 @@ function makeRuntime(overrides: Partial = {}) { gimpSignal: false, expansionBankHi: false, matrixMode: false, + protectOnReset: false, rtcEnabled: false, sdEnabled: false, ...overrides, @@ -122,4 +123,10 @@ describe('port 0x03 (SYS_INPUT)', () => { } expect(highSeen).toBe(true); }); + + it('sets protect on reset when configured', () => { + const rt = makeRuntime({ protectOnReset: true }); + expect(rt.state.sysCtrl & 0x02).toBe(0x02); + expect(rt.ioHandlers.read(0x03) & 0x02).toBe(0x02); + }); }); diff --git a/tests/platforms/tec1g/portfc.test.ts b/tests/platforms/tec1g/portfc.test.ts index a7a9315..21d0567 100644 --- a/tests/platforms/tec1g/portfc.test.ts +++ b/tests/platforms/tec1g/portfc.test.ts @@ -20,6 +20,7 @@ function makeRuntime(rtcEnabled: boolean) { gimpSignal: false, expansionBankHi: false, matrixMode: false, + protectOnReset: false, rtcEnabled, sdEnabled: false, }; diff --git a/tests/platforms/tec1g/portfd.test.ts b/tests/platforms/tec1g/portfd.test.ts index 778eba7..06136fd 100644 --- a/tests/platforms/tec1g/portfd.test.ts +++ b/tests/platforms/tec1g/portfd.test.ts @@ -19,6 +19,7 @@ function makeRuntime(sdEnabled: boolean) { gimpSignal: false, expansionBankHi: false, matrixMode: false, + protectOnReset: false, rtcEnabled: false, sdEnabled, }; diff --git a/tests/platforms/tec1g/serial.test.ts b/tests/platforms/tec1g/serial.test.ts index 809dbea..59dcd9f 100644 --- a/tests/platforms/tec1g/serial.test.ts +++ b/tests/platforms/tec1g/serial.test.ts @@ -16,6 +16,7 @@ function makeRuntime(onByte: (byte: number) => void) { gimpSignal: false, expansionBankHi: false, matrixMode: false, + protectOnReset: false, rtcEnabled: false, sdEnabled: false, }; diff --git a/webview/tec1g/index.ts b/webview/tec1g/index.ts index 27670d7..4f9e2e3 100644 --- a/webview/tec1g/index.ts +++ b/webview/tec1g/index.ts @@ -465,9 +465,17 @@ function drawGlcd() { const py = (py0 + dy - scroll + GLCD_HEIGHT) & 0x3f; if (px < GLCD_WIDTH && py < GLCD_HEIGHT) { const idx = (py * GLCD_WIDTH + px) * 4; - data[idx] = onR; - data[idx + 1] = onG; - data[idx + 2] = onB; + if (glcdGraphicsOn) { + const isOn = + data[idx] === onR && data[idx + 1] === onG && data[idx + 2] === onB; + data[idx] = isOn ? offR : onR; + data[idx + 1] = isOn ? offG : onG; + data[idx + 2] = isOn ? offB : onB; + } else { + data[idx] = onR; + data[idx + 1] = onG; + data[idx + 2] = onB; + } } } }