channelf: initial verison, not working, no BIOS

This commit is contained in:
Steven Hugg committed 2026-03-12 14:57:51 +01:00
1 parent 2d65e63943
commit ebcae078c7
7 files changed
+1269

No files matched your search

+691
View File
@@ -0,0 +1,691 @@
// based on https://github.com/mamedev/mame/blob/master/src/devices/cpu/f8/f8.cpp
// license:BSD-3-Clause
// copyright-holders:Juergen Buchmueller
// and https://raw.githubusercontent.com/mamedev/mame/master/src/mame/fairchild/channelf.cpp
// license:GPL-2.0+
// copyright-holders:Juergen Buchmueller, Frank Palazzolo, Sean Riddle
import { Bus, CPU, InstructionBased, SavesState, Interruptable, MemoryBusConnected } from "../devices";
export interface F8State {
PC: number; // alias for PC0 (required by CpuState)
PC0: number; // program counter
PC1: number; // saved program counter (stack)
DC0: number; // data counter
DC1: number; // alternate data counter
A: number; // accumulator
W: number; // status register (flags)
IS: number; // indirect scratchpad address register (6-bit)
R: number[]; // 64-byte scratchpad RAM
o: number; // last opcode
irq: boolean;
}
// Status register flag bits
const FLAG_S = 0x01; // Sign (complementary: set when positive)
const FLAG_C = 0x02; // Carry
const FLAG_Z = 0x04; // Zero
const FLAG_O = 0x08; // Overflow
const FLAG_I = 0x10; // Interrupt Control Bit
export class F8CPU implements CPU, InstructionBased, SavesState<F8State>, Interruptable<number>, MemoryBusConnected {
// Registers
PC0: number = 0; // active program counter
PC1: number = 0; // saved program counter
DC0: number = 0; // active data counter
DC1: number = 0; // alternate data counter
A: number = 0; // accumulator
W: number = 0; // status/flags
IS: number = 0; // indirect scratchpad address (6-bit)
R: Uint8Array = new Uint8Array(64); // scratchpad RAM
bus: Bus;
iobus: Bus;
o: number = 0; // last opcode
stable: boolean = true;
irqRequest: boolean = false;
connectMemoryBus(bus: Bus): void {
this.bus = bus;
}
connectIOBus(bus: Bus): void {
this.iobus = bus;
}
reset(): void {
this.PC0 = 0;
this.PC1 = 0;
this.DC0 = 0;
this.DC1 = 0;
this.A = 0;
this.W = 0;
this.IS = 0;
this.R.fill(0);
this.irqRequest = false;
}
interrupt(type: number): void {
this.irqRequest = true;
}
getPC(): number { return this.PC0; }
getSP(): number { return this.PC1; }
isStable(): boolean { return this.stable; }
saveState(): F8State {
return {
PC: this.PC0,
PC0: this.PC0,
PC1: this.PC1,
DC0: this.DC0,
DC1: this.DC1,
A: this.A,
W: this.W,
IS: this.IS,
R: Array.from(this.R),
o: this.o,
irq: this.irqRequest,
};
}
loadState(state: F8State): void {
this.PC0 = state.PC0 ?? state.PC;
this.PC1 = state.PC1;
this.DC0 = state.DC0;
this.DC1 = state.DC1;
this.A = state.A;
this.W = state.W;
this.IS = state.IS;
for (let i = 0; i < 64; i++) this.R[i] = state.R[i];
this.o = state.o;
this.irqRequest = state.irq;
}
// Read byte at PC0, increment PC0
private fetchPC0(): number {
const val = this.bus.read(this.PC0);
this.PC0 = (this.PC0 + 1) & 0xFFFF;
return val;
}
// Read byte at DC0, increment DC0
private fetchDC0(): number {
const val = this.bus.read(this.DC0);
this.DC0 = (this.DC0 + 1) & 0xFFFF;
return val;
}
// Write byte at DC0, increment DC0
private storeDC0(val: number): void {
this.bus.write(this.DC0, val);
this.DC0 = (this.DC0 + 1) & 0xFFFF;
}
// Clear O, Z, C, S flags
private clrOZCS(): void {
this.W &= ~(FLAG_O | FLAG_Z | FLAG_C | FLAG_S);
}
// Set Sign and Zero flags from value
// Note: F8 sign flag is COMPLEMENTARY - set when bit 7 is CLEAR (positive)
private setSZ(val: number): void {
if ((val & 0xFF) === 0) this.W |= FLAG_Z;
if ((val & 0x80) === 0) this.W |= FLAG_S; // complementary sign
}
// Addition with carry/overflow detection
private doAdd(a: number, b: number, carry: number = 0): number {
const result = (a & 0xFF) + (b & 0xFF) + (carry & 1);
// Set carry if overflow past 8 bits
if (result & 0x100) this.W |= FLAG_C;
// Set overflow using XOR method
if (((a ^ result) & (b ^ result) & 0x80) !== 0) this.W |= FLAG_O;
return result & 0xFF;
}
// BCD addition
private doAddDecimal(augend: number, addend: number): number {
let tmp = (augend & 0x0F) + (addend & 0x0F);
let c1 = (tmp > 9) ? 1 : 0;
tmp = (augend >> 4) + (addend >> 4) + c1;
let c2 = (tmp > 9) ? 1 : 0;
// Do binary add for flags
this.clrOZCS();
let result = this.doAdd(augend, addend);
this.setSZ(result);
// Apply BCD correction
let correction = 0;
if (c2) correction |= 0xA0;
if (c1) correction |= 0x0A;
if (correction) {
this.clrOZCS();
result = this.doAdd(result, correction);
this.setSZ(result);
}
return result;
}
// Get scratchpad register for operand (0-11 direct, 12=ISAR, 13=ISAR++, 14=ISAR--)
private getScratchpadIndex(r: number): number {
if (r < 12) return r;
if (r === 12) return this.IS & 0x3F;
if (r === 13) {
const idx = this.IS & 0x3F;
// Increment lower 3 bits of IS
this.IS = (this.IS & 0x38) | ((this.IS + 1) & 0x07);
return idx;
}
if (r === 14) {
const idx = this.IS & 0x3F;
// Decrement lower 3 bits of IS
this.IS = (this.IS & 0x38) | ((this.IS - 1) & 0x07);
return idx;
}
return 0; // illegal
}
advanceInsn(): number {
// Check for interrupts
if (this.irqRequest && (this.W & FLAG_I)) {
this.irqRequest = false;
// Save PC0 to PC1
this.PC1 = this.PC0;
// Load interrupt vector (at address 0x0020 typically)
this.PC0 = (this.bus.read(0x0FFE) << 8) | this.bus.read(0x0FFF);
// Clear interrupt enable
this.W &= ~FLAG_I;
return 6;
}
const opcode = this.fetchPC0();
this.o = opcode;
this.stable = true;
let cycles = 4; // short cycle default
let tmp: number;
let r: number;
switch (opcode) {
// 0x00: LR A,KU
case 0x00: this.A = this.R[12]; break;
// 0x01: LR A,KL
case 0x01: this.A = this.R[13]; break;
// 0x02: LR A,QU
case 0x02: this.A = this.R[14]; break;
// 0x03: LR A,QL
case 0x03: this.A = this.R[15]; break;
// 0x04: LR KU,A
case 0x04: this.R[12] = this.A; break;
// 0x05: LR KL,A
case 0x05: this.R[13] = this.A; break;
// 0x06: LR QU,A
case 0x06: this.R[14] = this.A; break;
// 0x07: LR QL,A
case 0x07: this.R[15] = this.A; break;
// 0x08: LR K,P (K <- PC1)
case 0x08:
this.R[12] = (this.PC1 >> 8) & 0xFF;
this.R[13] = this.PC1 & 0xFF;
cycles = 8;
break;
// 0x09: LR P,K (PC1 <- K)
case 0x09:
this.PC1 = (this.R[12] << 8) | this.R[13];
cycles = 8;
break;
// 0x0A: LR A,IS
case 0x0A: this.A = this.IS & 0x3F; break;
// 0x0B: LR IS,A
case 0x0B: this.IS = this.A & 0x3F; break;
// 0x0C: PK (Push K to PC: PC1<-PC0+1, PC0<-K)
case 0x0C:
this.PC1 = this.PC0;
this.PC0 = (this.R[12] << 8) | this.R[13];
cycles = 8;
break;
// 0x0D: LR P0,Q (PC0 <- Q)
case 0x0D:
this.PC0 = (this.R[14] << 8) | this.R[15];
cycles = 8;
break;
// 0x0E: LR Q,DC (Q <- DC0)
case 0x0E:
this.R[14] = (this.DC0 >> 8) & 0xFF;
this.R[15] = this.DC0 & 0xFF;
cycles = 8;
break;
// 0x0F: LR DC,Q (DC0 <- Q)
case 0x0F:
this.DC0 = (this.R[14] << 8) | this.R[15];
cycles = 8;
break;
// 0x10: LR DC,H (DC0 <- H)
case 0x10:
this.DC0 = (this.R[10] << 8) | this.R[11];
cycles = 8;
break;
// 0x11: LR H,DC (H <- DC0)
case 0x11:
this.R[10] = (this.DC0 >> 8) & 0xFF;
this.R[11] = this.DC0 & 0xFF;
cycles = 8;
break;
// 0x12: SR 1 (shift right 1)
case 0x12:
this.A = (this.A >> 1) & 0xFF;
this.clrOZCS();
this.setSZ(this.A);
break;
// 0x13: SL 1 (shift left 1)
case 0x13:
this.A = (this.A << 1) & 0xFF;
this.clrOZCS();
this.setSZ(this.A);
break;
// 0x14: SR 4 (shift right 4)
case 0x14:
this.A = (this.A >> 4) & 0xFF;
this.clrOZCS();
this.setSZ(this.A);
break;
// 0x15: SL 4 (shift left 4)
case 0x15:
this.A = (this.A << 4) & 0xFF;
this.clrOZCS();
this.setSZ(this.A);
break;
// 0x16: LM (load A from memory at DC0, increment DC0)
case 0x16:
this.A = this.fetchDC0();
cycles = 6;
break;
// 0x17: ST (store A to memory at DC0, increment DC0)
case 0x17:
this.storeDC0(this.A);
cycles = 6;
break;
// 0x18: COM (complement A)
case 0x18:
this.A = (~this.A) & 0xFF;
this.clrOZCS();
this.setSZ(this.A);
break;
// 0x19: LNK (add carry to A)
case 0x19:
tmp = (this.W & FLAG_C) ? 1 : 0;
this.clrOZCS();
this.A = this.doAdd(this.A, tmp);
this.setSZ(this.A);
break;
// 0x1A: DI (disable interrupts)
case 0x1A:
this.W &= ~FLAG_I;
break;
// 0x1B: EI (enable interrupts)
case 0x1B:
this.W |= FLAG_I;
break;
// 0x1C: POP (PC0 <- PC1)
case 0x1C:
this.PC0 = this.PC1;
break;
// 0x1D: LR W,J (W <- scratchpad[9])
case 0x1D:
this.W = this.R[9] & 0x1F;
break;
// 0x1E: LR J,W (scratchpad[9] <- W)
case 0x1E:
this.R[9] = this.W & 0x1F;
break;
// 0x1F: INC (increment A)
case 0x1F:
this.clrOZCS();
this.A = this.doAdd(this.A, 1);
this.setSZ(this.A);
break;
// 0x20: LI immediate
case 0x20:
this.A = this.fetchPC0();
cycles = 6;
break;
// 0x21: NI immediate (AND)
case 0x21:
tmp = this.fetchPC0();
this.A = this.A & tmp;
this.clrOZCS();
this.setSZ(this.A);
cycles = 6;
break;
// 0x22: OI immediate (OR)
case 0x22:
tmp = this.fetchPC0();
this.A = this.A | tmp;
this.clrOZCS();
this.setSZ(this.A);
cycles = 6;
break;
// 0x23: XI immediate (XOR)
case 0x23:
tmp = this.fetchPC0();
this.A = this.A ^ tmp;
this.clrOZCS();
this.setSZ(this.A);
cycles = 6;
break;
// 0x24: AI immediate (ADD)
case 0x24:
tmp = this.fetchPC0();
this.clrOZCS();
this.A = this.doAdd(this.A, tmp);
this.setSZ(this.A);
cycles = 6;
break;
// 0x25: CI immediate (compare: sets flags from ~A + immediate + 1)
case 0x25:
tmp = this.fetchPC0();
this.clrOZCS();
this.doAdd((~this.A) & 0xFF, tmp, 1);
this.setSZ(this.doAdd((~this.A) & 0xFF, tmp, 1));
cycles = 6;
break;
// 0x26: IN immediate (input from port)
case 0x26:
tmp = this.fetchPC0();
this.A = this.iobus.read(tmp) & 0xFF;
this.clrOZCS();
this.setSZ(this.A);
cycles = 8;
break;
// 0x27: OUT immediate (output to port)
case 0x27:
tmp = this.fetchPC0();
this.iobus.write(tmp, this.A);
cycles = 8;
break;
// 0x28: PI (push and jump immediate: PC1<-PC0, PC0<-address)
case 0x28: {
const hi = this.fetchPC0();
const lo = this.fetchPC0();
this.PC1 = this.PC0;
this.PC0 = (hi << 8) | lo;
cycles = 12;
break;
}
// 0x29: JMP (jump immediate)
case 0x29: {
const hi = this.fetchPC0();
const lo = this.fetchPC0();
this.PC0 = (hi << 8) | lo;
cycles = 10;
break;
}
// 0x2A: DCI (load DC0 immediate)
case 0x2A: {
const hi = this.fetchPC0();
const lo = this.fetchPC0();
this.DC0 = (hi << 8) | lo;
cycles = 10;
break;
}
// 0x2B: NOP
case 0x2B: break;
// 0x2C: XDC (exchange DC0 and DC1)
case 0x2C:
tmp = this.DC0;
this.DC0 = this.DC1;
this.DC1 = tmp;
break;
// 0x2D-0x2F: illegal
case 0x2D: case 0x2E: case 0x2F: break;
// 0x30-0x3E: DS (decrement scratchpad register, skip if zero)
case 0x30: case 0x31: case 0x32: case 0x33:
case 0x34: case 0x35: case 0x36: case 0x37:
case 0x38: case 0x39: case 0x3A: case 0x3B:
case 0x3C: case 0x3D: case 0x3E:
r = this.getScratchpadIndex(opcode & 0x0F);
this.clrOZCS();
this.R[r] = this.doAdd(this.R[r], 0xFF);
this.setSZ(this.R[r]);
cycles = 6;
break;
case 0x3F: break; // illegal
// 0x40-0x4E: LR A,r (load A from scratchpad)
case 0x40: case 0x41: case 0x42: case 0x43:
case 0x44: case 0x45: case 0x46: case 0x47:
case 0x48: case 0x49: case 0x4A: case 0x4B:
case 0x4C: case 0x4D: case 0x4E:
r = this.getScratchpadIndex(opcode & 0x0F);
this.A = this.R[r];
break;
case 0x4F: break; // illegal
// 0x50-0x5E: LR r,A (store A to scratchpad)
case 0x50: case 0x51: case 0x52: case 0x53:
case 0x54: case 0x55: case 0x56: case 0x57:
case 0x58: case 0x59: case 0x5A: case 0x5B:
case 0x5C: case 0x5D: case 0x5E:
r = this.getScratchpadIndex(opcode & 0x0F);
this.R[r] = this.A;
break;
case 0x5F: break; // illegal
// 0x60-0x67: LISU (load IS upper 3 bits)
case 0x60: case 0x61: case 0x62: case 0x63:
case 0x64: case 0x65: case 0x66: case 0x67:
this.IS = (this.IS & 0x07) | ((opcode & 0x07) << 3);
break;
// 0x68-0x6F: LISL (load IS lower 3 bits)
case 0x68: case 0x69: case 0x6A: case 0x6B:
case 0x6C: case 0x6D: case 0x6E: case 0x6F:
this.IS = (this.IS & 0x38) | (opcode & 0x07);
break;
// 0x70-0x7F: LIS (load A with 4-bit immediate)
case 0x70: case 0x71: case 0x72: case 0x73:
case 0x74: case 0x75: case 0x76: case 0x77:
case 0x78: case 0x79: case 0x7A: case 0x7B:
case 0x7C: case 0x7D: case 0x7E: case 0x7F:
this.A = opcode & 0x0F;
break;
// 0x80-0x87: BT (branch if flag test true)
case 0x80: case 0x81: case 0x82: case 0x83:
case 0x84: case 0x85: case 0x86: case 0x87: {
const mask = opcode & 0x07;
const offset = this.fetchPC0();
if (this.W & mask) {
// Sign-extend offset and add to PC0
this.PC0 = (this.PC0 + ((offset & 0x80) ? (offset - 256) : offset)) & 0xFFFF;
cycles = 6;
} else {
cycles = 6;
}
break;
}
// 0x88: AM (add memory at DC0 to A)
case 0x88:
tmp = this.fetchDC0();
this.clrOZCS();
this.A = this.doAdd(this.A, tmp);
this.setSZ(this.A);
cycles = 6;
break;
// 0x89: AMD (decimal add memory at DC0 to A)
case 0x89:
tmp = this.fetchDC0();
this.A = this.doAddDecimal(this.A, tmp);
cycles = 6;
break;
// 0x8A: NM (AND memory at DC0)
case 0x8A:
tmp = this.fetchDC0();
this.A = this.A & tmp;
this.clrOZCS();
this.setSZ(this.A);
cycles = 6;
break;
// 0x8B: OM (OR memory at DC0)
case 0x8B:
tmp = this.fetchDC0();
this.A = this.A | tmp;
this.clrOZCS();
this.setSZ(this.A);
cycles = 6;
break;
// 0x8C: XM (XOR memory at DC0)
case 0x8C:
tmp = this.fetchDC0();
this.A = this.A ^ tmp;
this.clrOZCS();
this.setSZ(this.A);
cycles = 6;
break;
// 0x8D: CM (compare memory at DC0)
case 0x8D:
tmp = this.fetchDC0();
this.clrOZCS();
{ const cmp = this.doAdd((~this.A) & 0xFF, tmp, 1); this.setSZ(cmp); }
cycles = 6;
break;
// 0x8E: ADC (add accumulator to DC0)
case 0x8E:
this.DC0 = (this.DC0 + ((this.A & 0x80) ? (this.A - 256) : this.A)) & 0xFFFF;
cycles = 6;
break;
// 0x8F: BR7 (branch if IS lower 3 bits != 7)
case 0x8F: {
const offset = this.fetchPC0();
if ((this.IS & 0x07) !== 7) {
this.PC0 = (this.PC0 + ((offset & 0x80) ? (offset - 256) : offset)) & 0xFFFF;
}
cycles = 6;
break;
}
// 0x90-0x9F: BF (branch if flag false)
case 0x90: case 0x91: case 0x92: case 0x93:
case 0x94: case 0x95: case 0x96: case 0x97:
case 0x98: case 0x99: case 0x9A: case 0x9B:
case 0x9C: case 0x9D: case 0x9E: case 0x9F: {
const mask = opcode & 0x0F;
const offset = this.fetchPC0();
if (mask === 0 || !(this.W & mask)) {
// BF 0 is unconditional branch (BR)
this.PC0 = (this.PC0 + ((offset & 0x80) ? (offset - 256) : offset)) & 0xFFFF;
}
cycles = 6;
break;
}
// 0xA0-0xA1: INS 0-1 (input from port 0/1, direct)
case 0xA0: case 0xA1:
this.A = this.iobus.read(opcode & 0x01) & 0xFF;
this.clrOZCS();
this.setSZ(this.A);
break;
// 0xA2-0xA3: illegal
case 0xA2: case 0xA3: break;
// 0xA4-0xAF: INS 4-F (input from addressed port)
case 0xA4: case 0xA5: case 0xA6: case 0xA7:
case 0xA8: case 0xA9: case 0xAA: case 0xAB:
case 0xAC: case 0xAD: case 0xAE: case 0xAF:
this.A = this.iobus.read(opcode & 0x0F) & 0xFF;
this.clrOZCS();
this.setSZ(this.A);
cycles = 8;
break;
// 0xB0-0xB1: OUTS 0-1 (output to port 0/1, direct)
case 0xB0: case 0xB1:
this.iobus.write(opcode & 0x01, this.A);
break;
// 0xB2-0xB3: illegal
case 0xB2: case 0xB3: break;
// 0xB4-0xBF: OUTS 4-F (output to addressed port)
case 0xB4: case 0xB5: case 0xB6: case 0xB7:
case 0xB8: case 0xB9: case 0xBA: case 0xBB:
case 0xBC: case 0xBD: case 0xBE: case 0xBF:
this.iobus.write(opcode & 0x0F, this.A);
cycles = 8;
break;
// 0xC0-0xCE: AS (add scratchpad to A)
case 0xC0: case 0xC1: case 0xC2: case 0xC3:
case 0xC4: case 0xC5: case 0xC6: case 0xC7:
case 0xC8: case 0xC9: case 0xCA: case 0xCB:
case 0xCC: case 0xCD: case 0xCE:
r = this.getScratchpadIndex(opcode & 0x0F);
this.clrOZCS();
this.A = this.doAdd(this.A, this.R[r]);
this.setSZ(this.A);
break;
case 0xCF: break; // illegal
// 0xD0-0xDE: ASD (decimal add scratchpad to A)
case 0xD0: case 0xD1: case 0xD2: case 0xD3:
case 0xD4: case 0xD5: case 0xD6: case 0xD7:
case 0xD8: case 0xD9: case 0xDA: case 0xDB:
case 0xDC: case 0xDD: case 0xDE:
r = this.getScratchpadIndex(opcode & 0x0F);
this.A = this.doAddDecimal(this.A, this.R[r]);
cycles = 8;
break;
case 0xDF: break; // illegal
// 0xE0-0xEE: XS (XOR scratchpad with A)
case 0xE0: case 0xE1: case 0xE2: case 0xE3:
case 0xE4: case 0xE5: case 0xE6: case 0xE7:
case 0xE8: case 0xE9: case 0xEA: case 0xEB:
case 0xEC: case 0xED: case 0xEE:
r = this.getScratchpadIndex(opcode & 0x0F);
this.A = (this.A ^ this.R[r]) & 0xFF;
this.clrOZCS();
this.setSZ(this.A);
break;
case 0xEF: break; // illegal
// 0xF0-0xFE: NS (AND scratchpad with A)
case 0xF0: case 0xF1: case 0xF2: case 0xF3:
case 0xF4: case 0xF5: case 0xF6: case 0xF7:
case 0xF8: case 0xF9: case 0xFA: case 0xFB:
case 0xFC: case 0xFD: case 0xFE:
r = this.getScratchpadIndex(opcode & 0x0F);
this.A = (this.A & this.R[r]) & 0xFF;
this.clrOZCS();
this.setSZ(this.A);
break;
case 0xFF: break; // illegal
}
return cycles;
}
}
+139
View File
@@ -0,0 +1,139 @@
// based on https://github.com/mamedev/mame/blob/master/src/devices/cpu/f8/f8.cpp
// license:BSD-3-Clause
// copyright-holders:Juergen Buchmueller
// and https://raw.githubusercontent.com/mamedev/mame/master/src/mame/fairchild/channelf.cpp
// license:GPL-2.0+
// copyright-holders:Juergen Buchmueller, Frank Palazzolo, Sean Riddle
import { hex } from "../util";
// Scratchpad register names for operands 0-14
function regName(r: number): string {
if (r < 9) return 'r' + r;
if (r === 9) return 'J';
if (r === 10) return 'HU';
if (r === 11) return 'HL';
if (r === 12) return 'S'; // ISAR
if (r === 13) return 'I'; // ISAR++
if (r === 14) return 'D'; // ISAR--
return '?';
}
export function disassembleF8(pc: number, b0: number, b1: number, b2: number): { line: string, nbytes: number, isaddr: boolean } {
let mn = '';
let args = '';
let nb = 1;
let isaddr = false;
if (b0 <= 0x07) {
const regs = ['KU', 'KL', 'QU', 'QL'];
if (b0 < 4) { mn = 'LR'; args = 'A,' + regs[b0]; }
else { mn = 'LR'; args = regs[b0 - 4] + ',A'; }
} else if (b0 === 0x08) { mn = 'LR'; args = 'K,P'; }
else if (b0 === 0x09) { mn = 'LR'; args = 'P,K'; }
else if (b0 === 0x0A) { mn = 'LR'; args = 'A,IS'; }
else if (b0 === 0x0B) { mn = 'LR'; args = 'IS,A'; }
else if (b0 === 0x0C) { mn = 'PK'; }
else if (b0 === 0x0D) { mn = 'LR'; args = 'P0,Q'; }
else if (b0 === 0x0E) { mn = 'LR'; args = 'Q,DC'; }
else if (b0 === 0x0F) { mn = 'LR'; args = 'DC,Q'; }
else if (b0 === 0x10) { mn = 'LR'; args = 'DC,H'; }
else if (b0 === 0x11) { mn = 'LR'; args = 'H,DC'; }
else if (b0 === 0x12) { mn = 'SR'; args = '1'; }
else if (b0 === 0x13) { mn = 'SL'; args = '1'; }
else if (b0 === 0x14) { mn = 'SR'; args = '4'; }
else if (b0 === 0x15) { mn = 'SL'; args = '4'; }
else if (b0 === 0x16) { mn = 'LM'; }
else if (b0 === 0x17) { mn = 'ST'; }
else if (b0 === 0x18) { mn = 'COM'; }
else if (b0 === 0x19) { mn = 'LNK'; }
else if (b0 === 0x1A) { mn = 'DI'; }
else if (b0 === 0x1B) { mn = 'EI'; }
else if (b0 === 0x1C) { mn = 'POP'; }
else if (b0 === 0x1D) { mn = 'LR'; args = 'W,J'; }
else if (b0 === 0x1E) { mn = 'LR'; args = 'J,W'; }
else if (b0 === 0x1F) { mn = 'INC'; }
else if (b0 === 0x20) { mn = 'LI'; args = '$' + hex(b1, 2); nb = 2; }
else if (b0 === 0x21) { mn = 'NI'; args = '$' + hex(b1, 2); nb = 2; }
else if (b0 === 0x22) { mn = 'OI'; args = '$' + hex(b1, 2); nb = 2; }
else if (b0 === 0x23) { mn = 'XI'; args = '$' + hex(b1, 2); nb = 2; }
else if (b0 === 0x24) { mn = 'AI'; args = '$' + hex(b1, 2); nb = 2; }
else if (b0 === 0x25) { mn = 'CI'; args = '$' + hex(b1, 2); nb = 2; }
else if (b0 === 0x26) { mn = 'IN'; args = '$' + hex(b1, 2); nb = 2; }
else if (b0 === 0x27) { mn = 'OUT'; args = '$' + hex(b1, 2); nb = 2; }
else if (b0 === 0x28) {
mn = 'PI'; args = '$' + hex((b1 << 8) | b2, 4); nb = 3; isaddr = true;
}
else if (b0 === 0x29) {
mn = 'JMP'; args = '$' + hex((b1 << 8) | b2, 4); nb = 3; isaddr = true;
}
else if (b0 === 0x2A) {
mn = 'DCI'; args = '$' + hex((b1 << 8) | b2, 4); nb = 3; isaddr = true;
}
else if (b0 === 0x2B) { mn = 'NOP'; }
else if (b0 === 0x2C) { mn = 'XDC'; }
else if (b0 <= 0x2F) { mn = '???'; }
else if (b0 >= 0x30 && b0 <= 0x3E) {
mn = 'DS'; args = regName(b0 & 0x0F); nb = 1;
}
else if (b0 === 0x3F) { mn = '???'; }
else if (b0 >= 0x40 && b0 <= 0x4E) {
mn = 'LR'; args = 'A,' + regName(b0 & 0x0F);
}
else if (b0 === 0x4F) { mn = '???'; }
else if (b0 >= 0x50 && b0 <= 0x5E) {
mn = 'LR'; args = regName(b0 & 0x0F) + ',A';
}
else if (b0 === 0x5F) { mn = '???'; }
else if (b0 >= 0x60 && b0 <= 0x67) {
mn = 'LISU'; args = '' + (b0 & 0x07);
}
else if (b0 >= 0x68 && b0 <= 0x6F) {
mn = 'LISL'; args = '' + (b0 & 0x07);
}
else if (b0 >= 0x70 && b0 <= 0x7F) {
mn = 'LIS'; args = '' + (b0 & 0x0F);
}
else if (b0 >= 0x80 && b0 <= 0x87) {
const offset = (b1 & 0x80) ? (pc + 2 + b1 - 256) : (pc + 2 + b1);
mn = 'BT'; args = '' + (b0 & 0x07) + ',$' + hex(offset & 0xFFFF, 4);
nb = 2; isaddr = true;
}
else if (b0 === 0x88) { mn = 'AM'; }
else if (b0 === 0x89) { mn = 'AMD'; }
else if (b0 === 0x8A) { mn = 'NM'; }
else if (b0 === 0x8B) { mn = 'OM'; }
else if (b0 === 0x8C) { mn = 'XM'; }
else if (b0 === 0x8D) { mn = 'CM'; }
else if (b0 === 0x8E) { mn = 'ADC'; }
else if (b0 === 0x8F) {
const offset = (b1 & 0x80) ? (pc + 2 + b1 - 256) : (pc + 2 + b1);
mn = 'BR7'; args = '$' + hex(offset & 0xFFFF, 4);
nb = 2; isaddr = true;
}
else if (b0 >= 0x90 && b0 <= 0x9F) {
const offset = (b1 & 0x80) ? (pc + 2 + b1 - 256) : (pc + 2 + b1);
const mask = b0 & 0x0F;
if (mask === 0) mn = 'BR';
else mn = 'BF';
if (mask !== 0) args = '' + mask + ',';
args += '$' + hex(offset & 0xFFFF, 4);
nb = 2; isaddr = true;
}
else if (b0 >= 0xA0 && b0 <= 0xA1) { mn = 'INS'; args = '' + (b0 & 0x0F); }
else if (b0 >= 0xA2 && b0 <= 0xA3) { mn = '???'; }
else if (b0 >= 0xA4 && b0 <= 0xAF) { mn = 'INS'; args = '' + (b0 & 0x0F); }
else if (b0 >= 0xB0 && b0 <= 0xB1) { mn = 'OUTS'; args = '' + (b0 & 0x0F); }
else if (b0 >= 0xB2 && b0 <= 0xB3) { mn = '???'; }
else if (b0 >= 0xB4 && b0 <= 0xBF) { mn = 'OUTS'; args = '' + (b0 & 0x0F); }
else if (b0 >= 0xC0 && b0 <= 0xCE) { mn = 'AS'; args = regName(b0 & 0x0F); }
else if (b0 === 0xCF) { mn = '???'; }
else if (b0 >= 0xD0 && b0 <= 0xDE) { mn = 'ASD'; args = regName(b0 & 0x0F); }
else if (b0 === 0xDF) { mn = '???'; }
else if (b0 >= 0xE0 && b0 <= 0xEE) { mn = 'XS'; args = regName(b0 & 0x0F); }
else if (b0 === 0xEF) { mn = '???'; }
else if (b0 >= 0xF0 && b0 <= 0xFE) { mn = 'NS'; args = regName(b0 & 0x0F); }
else { mn = '???'; }
return { line: mn + (args ? ' ' + args : ''), nbytes: nb, isaddr: isaddr };
}
+357
View File
@@ -0,0 +1,357 @@
// based on https://github.com/mamedev/mame/blob/master/src/devices/cpu/f8/f8.cpp
// license:BSD-3-Clause
// copyright-holders:Juergen Buchmueller
// and https://raw.githubusercontent.com/mamedev/mame/master/src/mame/fairchild/channelf.cpp
// license:GPL-2.0+
// copyright-holders:Juergen Buchmueller, Frank Palazzolo, Sean Riddle
import { F8CPU, F8State } from "../common/cpu/F8";
import { BasicMachine, Bus, SampledAudioSink } from "../common/devices";
import { KeyFlags, Keys, newAddressDecoder, padBytes } from "../common/emu";
import { hex } from "../common/util";
// Channel F color palette (RGBA)
const CHANNELF_COLORS = [
0xFF484848, // 0: dark gray/background (approximation for "field" color)
0xFF5050FF, // 1: blue
0xFF50FF50, // 2: green -> actually the red
0xFF808080, // 3: gray
0xFFFFFFFF, // 4: white (BG palette)
0xFF50FF50, // 5: green
0xFFFF5050, // 6: red
0xFFC8C8C8, // 7: light gray
];
// Full 16-entry color map (4 palette banks x 4 colors)
const CHANNELF_COLORMAP = [
// Palette 0 (default)
0xFF484848, // 0: background
0xFF50FF50, // 1: green
0xFF5050FF, // 2: blue
0xFF808080, // 3: gray
// Palette 1
0xFFFF5050, // 0: red (background)
0xFF50FF50, // 1: green
0xFF5050FF, // 2: blue
0xFF808080, // 3: gray
// Palette 2
0xFF484848, // 0: background
0xFFFF5050, // 1: red
0xFF5050FF, // 2: blue
0xFF808080, // 3: gray
// Palette 3
0xFF484848, // 0: background
0xFF50FF50, // 1: green
0xFFFF5050, // 2: red
0xFF808080, // 3: gray
];
// Input port bit definitions
const INPUT_RIGHT = 0x01;
const INPUT_LEFT = 0x02;
const INPUT_BACK = 0x04;
const INPUT_FORWARD = 0x08;
const INPUT_CCW = 0x10; // counter-clockwise
const INPUT_CW = 0x20; // clockwise
const INPUT_PULL = 0x40;
const INPUT_PUSH = 0x80;
export class ChannelF extends BasicMachine {
cpuFrequency = 1789773; // NTSC: 3.579545 MHz / 2
defaultROMSize = 0x10000;
// Video params
canvasWidth = 128;
numVisibleScanlines = 64;
numTotalScanlines = 64;
cpuCyclesPerLine = this.cpuFrequency / 60 / 64; // ~466 cycles per line
sampleRate = this.cpuFrequency / 2; // audio sample rate
cpu = new F8CPU();
ram = new Uint8Array(0x10000); // address space (ROM + VRAM)
vram = new Uint8Array(0x2000); // 8KB video RAM (128x64, 2 bits per pixel)
bios: Uint8Array;
// I/O port latches
port0: number = 0; // console switches
port1: number = 0; // right controller
port4: number = 0; // left controller
port5: number = 0; // row/column/sound
// Video registers
row_reg: number = 0;
col_reg: number = 0;
color: number = 0;
// Audio state
soundMode: number = 0;
soundCounter: number = 0;
soundEnvelope: number = 0;
soundOnTime: number = 0;
constructor() {
super();
this.bios = new Uint8Array(0x800); // 2KB BIOS
this.connectCPUMemoryBus(this);
this.cpu.connectIOBus(this.newIOBus());
}
newIOBus(): Bus {
return this.probeIOBus({
read: (port: number): number => {
return this.readPort(port);
},
write: (port: number, val: number): void => {
this.writePort(port, val);
}
});
}
readPort(port: number): number {
switch (port & 0xFF) {
case 0:
// Console switches: TIME(0), MODE(1), HOLD(2), START(3)
return (~this.inputs[0]) & 0xFF;
case 1:
// Right controller
return (~this.inputs[1]) & 0xFF;
case 4:
// Left controller
return (~this.inputs[2]) & 0xFF;
default:
return 0xFF;
}
}
writePort(port: number, val: number): void {
switch (port & 0xFF) {
case 0:
this.port0 = val;
break;
case 1:
this.port1 = val;
// The right controller port is also used for video column
this.col_reg = val;
break;
case 4:
this.port4 = val;
break;
case 5:
this.port5 = val;
// Upper 2 bits control sound
this.soundMode = (val >> 6) & 3;
if (this.soundMode > 0) {
this.soundEnvelope = 0x3FF; // reset envelope on new sound
this.soundOnTime = 60; // minimum on time (~2ms)
}
// Lower 6 bits control video row
this.row_reg = val & 0x3F;
// Write pixel to VRAM when both row and column are set
this.writePixel();
break;
}
}
writePixel(): void {
const row = this.row_reg;
const col = this.col_reg & 0x7F;
if (row < 64 && col < 128) {
// Color comes from port 1 upper bits
const color = (this.port1 >> 6) & 0x03;
const addr = row * 128 + col;
this.vram[addr] = color;
}
}
read = newAddressDecoder([
[0x0000, 0x07FF, 0x07FF, (a) => { return this.bios[a]; }],
[0x0800, 0xFFFF, 0xFFFF, (a) => { return this.rom ? (this.rom[a - 0x0800] || 0) : 0; }],
], { gmask: 0xFFFF });
write = newAddressDecoder([
// Channel F has no writable main memory in the base config
// Some cartridges have RAM at 0x2800-0x2FFF
[0x2800, 0x2FFF, 0x07FF, (a, v) => { if (this.rom) this.rom[a - 0x0800] = v; }],
], { gmask: 0xFFFF });
readConst(a: number): number {
if (a < 0x0800) return this.bios[a];
return this.rom ? (this.rom[a - 0x0800] || 0) : 0;
}
loadROM(data: Uint8Array): void {
if (!this.rom) this.rom = new Uint8Array(this.defaultROMSize);
this.rom.set(data);
this.reset();
}
loadBIOS(data: Uint8Array): void {
this.bios = padBytes(data, 0x800);
this.reset();
}
reset(): void {
super.reset();
this.vram.fill(0);
this.port0 = 0;
this.port1 = 0;
this.port4 = 0;
this.port5 = 0;
this.row_reg = 0;
this.col_reg = 0;
this.soundMode = 0;
this.soundCounter = 0;
this.soundEnvelope = 0;
}
advanceFrame(trap): number {
var clock = 0;
var targetClocks = this.cpuFrequency / 60;
this.probe.logNewFrame();
while (clock < targetClocks) {
if (trap && trap()) break;
clock += this.advanceCPU();
}
this.renderFrame();
this.generateAudio(targetClocks);
return clock;
}
renderFrame(): void {
if (!this.pixels) return;
// Determine palette offset from VRAM locations 125 and 126 of row 0
// (simplified - uses palette 0 by default)
for (let y = 0; y < 64; y++) {
// Palette selection per row from vram[y*128 + 125] and [y*128 + 126]
let palOffset = 0;
const p125 = this.vram[y * 128 + 125];
const p126 = this.vram[y * 128 + 126];
palOffset = ((p125 & 2) | ((p126 & 2) >> 1)) * 4;
for (let x = 0; x < 128; x++) {
const color = this.vram[y * 128 + x] & 0x03;
const rgba = CHANNELF_COLORMAP[palOffset + color];
// Double vertical: each Channel F row maps to 4 display rows (NTSC)
this.pixels[y * 128 + x] = rgba;
}
}
}
generateAudio(cycles: number): void {
if (!this.audio) return;
// Simple tone generation based on sound mode
const samplesPerFrame = Math.floor(this.sampleRate / 60);
for (let i = 0; i < samplesPerFrame; i++) {
let sample = 0;
if (this.soundMode > 0 && this.soundEnvelope > 0) {
// Generate square wave at appropriate frequency
let freq: number;
switch (this.soundMode) {
case 1: freq = 1000; break; // ~1000 Hz
case 2: freq = 500; break; // ~500 Hz
case 3: freq = 120; break; // ~120 Hz
default: freq = 0;
}
if (freq > 0) {
const period = Math.floor(this.sampleRate / freq);
this.soundCounter = (this.soundCounter + 1) % period;
sample = this.soundCounter < period / 2 ? 0.3 : -0.3;
sample *= this.soundEnvelope / 0x3FF;
}
// Decay envelope
if (this.soundOnTime > 0) {
this.soundOnTime--;
} else {
this.soundEnvelope -= 2;
if (this.soundEnvelope < 0) this.soundEnvelope = 0;
}
}
this.audio.feedSample(sample, 1);
}
}
setKeyInput(key: number, code: number, flags: number): void {
// inputs[0] = console switches
// inputs[1] = right controller
// inputs[2] = left controller
const pressed = !!(flags & KeyFlags.KeyDown);
const released = !!(flags & KeyFlags.KeyUp);
// Console buttons (mapped to number keys)
if (key === Keys.VK_1.c) { // TIME
if (pressed) this.inputs[0] |= 0x01; else if (released) this.inputs[0] &= ~0x01;
}
if (key === Keys.VK_2.c) { // MODE
if (pressed) this.inputs[0] |= 0x02; else if (released) this.inputs[0] &= ~0x02;
}
if (key === Keys.VK_3.c) { // HOLD
if (pressed) this.inputs[0] |= 0x04; else if (released) this.inputs[0] &= ~0x04;
}
if (key === Keys.VK_4.c) { // START
if (pressed) this.inputs[0] |= 0x08; else if (released) this.inputs[0] &= ~0x08;
}
// Right controller (WASD + QE for twist + RF for push/pull)
if (key === Keys.VK_UP.c || key === Keys.VK_W.c) {
if (pressed) this.inputs[1] |= INPUT_FORWARD; else if (released) this.inputs[1] &= ~INPUT_FORWARD;
}
if (key === Keys.VK_DOWN.c || key === Keys.VK_S.c) {
if (pressed) this.inputs[1] |= INPUT_BACK; else if (released) this.inputs[1] &= ~INPUT_BACK;
}
if (key === Keys.VK_LEFT.c || key === Keys.VK_A.c) {
if (pressed) this.inputs[1] |= INPUT_LEFT; else if (released) this.inputs[1] &= ~INPUT_LEFT;
}
if (key === Keys.VK_RIGHT.c || key === Keys.VK_D.c) {
if (pressed) this.inputs[1] |= INPUT_RIGHT; else if (released) this.inputs[1] &= ~INPUT_RIGHT;
}
if (key === Keys.VK_Q.c) { // CCW twist
if (pressed) this.inputs[1] |= INPUT_CCW; else if (released) this.inputs[1] &= ~INPUT_CCW;
}
if (key === Keys.VK_E.c) { // CW twist
if (pressed) this.inputs[1] |= INPUT_CW; else if (released) this.inputs[1] &= ~INPUT_CW;
}
if (key === Keys.VK_R.c) { // Push
if (pressed) this.inputs[1] |= INPUT_PUSH; else if (released) this.inputs[1] &= ~INPUT_PUSH;
}
if (key === Keys.VK_F.c) { // Pull
if (pressed) this.inputs[1] |= INPUT_PULL; else if (released) this.inputs[1] &= ~INPUT_PULL;
}
}
saveState() {
return {
c: this.cpu.saveState(),
ram: this.ram.slice(0),
inputs: this.inputs.slice(0),
vram: this.vram.slice(0),
port0: this.port0,
port1: this.port1,
port4: this.port4,
port5: this.port5,
row_reg: this.row_reg,
col_reg: this.col_reg,
soundMode: this.soundMode,
soundCounter: this.soundCounter,
soundEnvelope: this.soundEnvelope,
};
}
loadState(state) {
this.cpu.loadState(state.c);
this.ram.set(state.ram);
this.inputs.set(state.inputs);
this.vram.set(state.vram);
this.port0 = state.port0;
this.port1 = state.port1;
this.port4 = state.port4;
this.port5 = state.port5;
this.row_reg = state.row_reg;
this.col_reg = state.col_reg;
this.soundMode = state.soundMode;
this.soundCounter = state.soundCounter;
this.soundEnvelope = state.soundEnvelope;
}
}
+3
View File
@@ -1,3 +1,6 @@
// based on https://raw.githubusercontent.com/mamedev/mame/refs/heads/master/src/mame/midway/mcr.cpp
// license:BSD-3-Clause
// copyright-holders:Aaron Giles
import { Z80, Z80State } from "../common/cpu/ZilogZ80";
import { BasicScanlineMachine } from "../common/devices";
+1
View File
@@ -9,6 +9,7 @@ export function importPlatform(name: string) : Promise<any> {
case "atari8": return import("../platform/atari8");
case "basic": return import("../platform/basic");
case "c64": return import("../platform/c64");
case "channelf": return import("../platform/channelf");
case "coleco": return import("../platform/coleco");
case "cpc": return import("../platform/cpc");
case "devel": return import("../platform/devel");
+75
View File
@@ -0,0 +1,75 @@
// based on https://github.com/mamedev/mame/blob/master/src/devices/cpu/f8/f8.cpp
// license:BSD-3-Clause
// copyright-holders:Juergen Buchmueller
// and https://raw.githubusercontent.com/mamedev/mame/master/src/mame/fairchild/channelf.cpp
// license:GPL-2.0+
// copyright-holders:Juergen Buchmueller, Frank Palazzolo, Sean Riddle
import { Platform, BaseMachinePlatform, DisasmLine } from "../common/baseplatform";
import { PLATFORMS } from "../common/emu";
import { ChannelF } from "../machine/channelf";
import { disassembleF8 } from "../common/cpu/disasmF8";
import { hex, printFlags } from "../common/util";
var CHANNELF_PRESETS = [
{ id: 'hello.dasm', name: 'Hello World (ASM)' },
];
function cpuStateToLongString_F8(c): string {
return "PC0 = " + hex(c.PC0, 4) + " PC1 = " + hex(c.PC1, 4) + "\n"
+ "DC0 = " + hex(c.DC0, 4) + " DC1 = " + hex(c.DC1, 4) + "\n"
+ " A = " + hex(c.A, 2) + " W = " + hex(c.W, 2) + "\n"
+ " IS = " + hex(c.IS, 2) + "\n"
+ printFlags(c.W, ['S', 'C', 'Z', 'O', 'I'], true) + "\n"
+ " R0-R3: " + c.R.slice(0, 4).map(v => hex(v, 2)).join(' ') + "\n"
+ " R4-R7: " + c.R.slice(4, 8).map(v => hex(v, 2)).join(' ') + "\n"
+ " R8/J/H: " + c.R.slice(8, 12).map(v => hex(v, 2)).join(' ') + "\n"
+ " K/Q: " + c.R.slice(12, 16).map(v => hex(v, 2)).join(' ') + "\n";
}
class ChannelFPlatform extends BaseMachinePlatform<ChannelF> implements Platform {
newMachine() { return new ChannelF(); }
getPresets() { return CHANNELF_PRESETS; }
getDefaultExtensions() { return [".dasm"]; }
getToolForFilename(fn: string): string {
if (fn.endsWith(".c")) return "cc65";
if (fn.endsWith(".s") || fn.endsWith(".ca65")) return "ca65";
return "dasm";
}
readAddress(a) { return this.machine.readConst(a); }
disassemble(pc: number, read: (addr: number) => number): DisasmLine {
return disassembleF8(pc, read(pc), read(pc + 1), read(pc + 2));
}
getDebugCategories() {
return ['CPU', 'I/O', 'VRAM'];
}
getDebugInfo(category: string, state): string {
switch (category) {
case 'CPU': return cpuStateToLongString_F8(state.c);
case 'I/O':
return "Port 0: " + hex(state.port0 || 0, 2) + "\n"
+ "Port 1: " + hex(state.port1 || 0, 2) + "\n"
+ "Port 4: " + hex(state.port4 || 0, 2) + "\n"
+ "Port 5: " + hex(state.port5 || 0, 2) + "\n"
+ "Row: " + hex(state.row_reg || 0, 2) + "\n"
+ "Col: " + hex(state.col_reg || 0, 2) + "\n";
case 'VRAM': return '';
default: return '';
}
}
getMemoryMap() {
return {
main: [
{ name: 'BIOS', start: 0x0000, size: 0x0800, type: 'rom' },
{ name: 'Cart ROM', start: 0x0800, size: 0x7800, type: 'rom' },
]
};
}
}
PLATFORMS['channelf'] = ChannelFPlatform;
+3
View File
@@ -1,3 +1,6 @@
// based on https://raw.githubusercontent.com/mamedev/mame/refs/heads/master/src/mame/midway/mcr.cpp
// license:BSD-3-Clause
// copyright-holders:Aaron Giles
import { MCR2Machine } from "../machine/mcr";
import { BaseZ80MachinePlatform } from "../common/baseplatform";