mz700-js
Version:
MZ-700 Full JavaScript Emulator
625 lines (590 loc) • 21.4 kB
text/typescript
"use strict";
/* tslint:disable: no-console no-bitwise */
import FractionalTimer from "fractional-timer";
import MZ_TapeHeader from '../lib/mz-tape-header';
import MZ_Tape from '../lib/mz-tape';
import MZ_DataRecorder from '../lib/mz-data-recorder';
import Intel8253 from '../lib/intel-8253';
import FlipFlopCounter from '../lib/flip-flop-counter';
import IC556 from '../lib/ic556';
import MZMMIO from "../lib/mz-mmio.js";
import MZ700KeyMatrix from './mz700-key-matrix';
import MZ700_Memory from "./mz700-memory.js";
import Z80 from '../Z80/Z80.js';
import Z80LineAssembler from "../Z80/Z80-line-assembler";
import PCG700 from "../lib/PCG-700";
type MZ700Opts = {
started?,
stopped?,
onBreak?,
onVramUpdate?,
onUpdateScrn?,
onMmioRead?,
onMmioWrite?,
startSound?,
stopSound?,
onStartDataRecorder?,
onStopDataRecorder?,
};
export default class MZ700 {
static Z80_CLOCK = 3.579545 * 1000000;// 3.58 MHz
static DEFAULT_TIMER_INTERVAL = 1.0 / MZ700.Z80_CLOCK;
opt:MZ700Opts;
tid:NodeJS.Timeout;
clockFactor:number;
tidMeasClock:NodeJS.Timeout;
tCycle0:number;
actualClockFreq:number;
_cycleToWait:number;
mztArray:{header:MZ_TapeHeader, body:{buffer:number[]}}[];
keymatrix:MZ700KeyMatrix;
dataRecorder: MZ_DataRecorder;
memory:MZ700_Memory;
mmio:MZMMIO;
z80:Z80;
intel8253:Intel8253;
ic556:IC556
hblank:FlipFlopCounter;
vblank:FlipFlopCounter;
INTMSK:boolean;
VBLK:boolean;
MLDST:boolean;
ic556Out:boolean
constructor() { /* empty */ }
create(opt:MZ700Opts):void {
// MZ700 Key Matrix
this.keymatrix = new MZ700KeyMatrix();
// Create 8253
this.intel8253 = new Intel8253();
this.intel8253.counter(1).initCount(15700, () => {
this.intel8253.counter(2).count(1);
});
this.intel8253.counter(2).initCount(43200, () => {
if (this.INTMSK) {
this.z80.interrupt();
}
});
// HBLNK F/F in 15.7 kHz
this.hblank = new FlipFlopCounter(MZ700.Z80_CLOCK / 15700);
this.hblank.addEventListener("change", () => {
this.intel8253.counter(1).count(1);
});
// VBLNK F/F in 50 Hz
this.vblank = new FlipFlopCounter(MZ700.Z80_CLOCK / 50);
this.VBLK = false;
this.vblank.addEventListener("change", () => {
this.VBLK = !this.VBLK;
});
// create IC 556 to create HBLNK(cursor blink) by 3 Hz?
this.ic556 = new IC556(MZ700.Z80_CLOCK / 3);
this.ic556Out = false;
this.ic556.addEventListener("change", () => {
this.ic556Out = !this.ic556Out;
});
this.INTMSK = false;
this.MLDST = false;
let motorOffDelayTid = null;
this.dataRecorder = new MZ_DataRecorder(motorState => {
if (motorState) {
if (motorOffDelayTid != null) {
clearTimeout(motorOffDelayTid);
motorOffDelayTid = null;
}
this.opt.onStartDataRecorder();
} else {
motorOffDelayTid = setTimeout(() => {
motorOffDelayTid = null;
this.opt.onStopDataRecorder();
}, 100);
}
});
//
// Default option settings to notify from WebWorker
// to UI thread by transworker
//
this.opt = {
started: () => { /* empty */ },
stopped: () => { /* empty */ },
onBreak: () => { /* empty */ },
onVramUpdate: ( /*index, dispcode, attr*/) => { /* empty */ },
onUpdateScrn: ( /*buffer*/) => { /* empty */ },
onMmioRead: ( /*address, value*/) => { /* empty */ },
onMmioWrite: ( /*address, value*/) => { /* empty */ },
startSound: ( /*freq*/) => { /* empty */ },
stopSound: () => { /* empty */ },
onStartDataRecorder: () => { /* empty */ },
onStopDataRecorder: () => { /* empty */ }
};
//
// Override option to receive notifications with callbacks.
//
opt = opt || { /* empty */ };
Object.keys(opt).forEach(key => {
if (!(key in this.opt)) {
console.warn(`Unknown option key ${key} is specified.`);
}
});
Object.keys(this.opt).forEach(key => {
if (key in opt) {
this.opt[key] = opt[key];
}
});
this.tid = null;
this.clockFactor = 1.0;
this.tidMeasClock = null;
this.tCycle0 = 0;
this.actualClockFreq = 0.0;
this._cycleToWait = 0;
this.mmio = new MZMMIO();
for (let address = 0xE000; address < 0xE800; address++) {
this.mmio.onRead(address,
value => this.opt.onMmioRead(address, value));
this.mmio.onWrite(address,
value => this.opt.onMmioWrite(address, value));
}
// MMIO $E000
this.mmio.onWrite(0xE000, value => {
this.memory.poke(0xE001, this.keymatrix.getKeyData(value));
this.ic556.loadReset((value & 0x80) !== 0);
});
// MMIO $E001
// No Device
// MMIO $E002
this.mmio.onRead(0xE002, value => {
// [VBLK~] [556OUT] [RDATA] [MOTOR] [M-ON] [INTMSK] [WDATA] [*****]
// | | | | | | | |
// | | | | | | | +---- b0. --- (undefined)
// | | | | | | +------------ b1. OUT CMT WRITE DATA
// | | | | | +-------------------- b2. OUT CLOCK INT MASK
// | | | | +---------------------------- b3. OUT DRIVE CMT MOTOR
// | | | +------------------------------------ b4. IN CMT MOTOR FEEDBACK
// | | +-------------------------------------------- b5. IN CMT READ DATA
// | +---------------------------------------------------- b6. IN BLINK CURSOR
// +------------------------------------------------------------- b7. IN VERTICAL BLANK
value = value & 0x0f; // 入力上位4ビットをオフ
// PC4 - MOTOR : The motor driving state (high active)
if (this.dataRecorder.motor()) {
value = value | 0x10;
} else {
value = value & 0xef;
}
// PC5 - RDATA : A bit data to read
if (this.dataRecorder_readBit()) {
value = value | 0x20;
} else {
value = value & 0xdf;
}
// PC6 - 556_OUT : A signal to blink cursor on the screen
if (this.ic556Out) {
value = value | 0x40;
} else {
value = value & 0xbf;
}
// PC7 - VBLK : A virtical blanking signal
// set V-BLANK bit
if (this.VBLK) {
value = value | 0x80;
} else {
value = value & 0x7f;
}
return value;
});
// MMIO $E003
this.mmio.onWrite(0xE003, value => {
// MSB==0の場合、PortCへのビット単位の書き込みを指示する。
//
// [ 7 6 5 4 3 2 1 0 ]
// --- ----------- ---
// 0 - - - ビット番号 値
//
// MSB==1の場合は、モードセット
//
// [ 7 6 5 4 3 2 1 0 ]
// --- ------- --- --- --- --- ---
// 1 ModeA | | | | |
// PortA --+ | | | |
// PortCH------+ | | +----- PortCL
// ModeB --+ +--------- PortB
//
// ModeA: 1x - モード2、01 - モード1、00 - モード0
// ModeB: 1 - モード1、0 - モード0
// PortA: Port A 入出力設定 0 - 出力、1 - 入力
// PortB: Port B 入出力設定 0 - 出力、1 - 入力
// PortCH: Port C 上位ニブル入出力設定 0 - 出力、1 - 入力
// PortCL: Port C 下位ニブル入出力設定 0 - 出力、1 - 入力
//
if ((value & 0x80) === 0) {
const bit = ((value & 0x01) !== 0);
const bitno = (value & 0x0e) >> 1;
// const name = [
// "SOUNDMSK(MZ-1500)",
// "WDATA","INTMSK","M-ON",
// "MOTOR","RDATA", "556 OUT", "VBLK"][bitno];
// console.log("$E003 8255 CTRL BITSET", name, bit);
switch (bitno) {
case 0: // SOUNDMSK
break;
case 1: // WDATA
this.dataRecorder_writeBit(bit);
break;
case 2: // INTMSK
this.INTMSK = bit; // trueで割り込み許可
break;
case 3: // M-ON
this.dataRecorder_motorOn(bit);
break;
}
}
});
// MMIO $E004
this.mmio.onRead(0xE004, () => this.intel8253.counter(0).read());
this.mmio.onWrite(0xE004, value => {
if (this.intel8253.counter(0).load(value) && this.MLDST) {
this.opt.startSound(895000 / this.intel8253.counter(0).value);
}
});
// MMIO $E005
this.mmio.onRead(0xE005, () => this.intel8253.counter(1).read());
this.mmio.onWrite(0xE005, value => this.intel8253.counter(1).load(value));
// MMIO $E006
this.mmio.onRead(0xE006, () => this.intel8253.counter(2).read());
this.mmio.onWrite(0xE006, value => this.intel8253.counter(2).load(value));
// MMIO $E007
this.mmio.onWrite(0xE007, value => this.intel8253.setCtrlWord(value));
// MMIO $E008
this.mmio.onRead(0xE008, value => {
value = value & 0xfe; // MSBをオフ
// set H-BLANK bit
if (this.hblank.readOutput()) {
value = value | 0x01;
} else {
value = value & 0xfe;
}
return value;
});
this.mmio.onWrite(0xE008, value => {
this.MLDST = ((value & 0x01) !== 0);
if (this.MLDST) {
this.opt.startSound(895000 / this.intel8253.counter(0).value);
} else {
this.opt.stopSound();
}
});
this.memory = new MZ700_Memory();
this.memory.create({
onVramUpdate: (index, dispcode, attr) => {
this.opt.onVramUpdate(index, dispcode, attr);
},
onMappedIoRead: (address, value) => {
// MMIO: Input from memory mapped peripherals
const readValue = this.mmio.read(address, value);
if (readValue == null || readValue === undefined) {
return value;
}
return readValue;
},
onMappedIoUpdate: (address, value) => {
// MMIO: Output to memory mapped peripherals
this.mmio.write(address, value);
return value;
}
});
this.z80 = new Z80({ memory: this.memory });
this.z80.onWriteIoPort(0xe0, () => this.memory.changeBlock0_DRAM());
this.z80.onWriteIoPort(0xe1, () => this.memory.changeBlock1_DRAM());
this.z80.onWriteIoPort(0xe2, () => this.memory.changeBlock0_MONITOR());
this.z80.onWriteIoPort(0xe3, () => this.memory.changeBlock1_VRAM());
this.z80.onWriteIoPort(0xe4, () => {
this.memory.changeBlock0_MONITOR();
this.memory.changeBlock1_VRAM();
});
this.z80.onWriteIoPort(0xe5, () => this.memory.disableBlock1());
this.z80.onWriteIoPort(0xe6, () => this.memory.enableBlock1());
}
setMonitorRom(bin:number[]):void {
this.memory.setMonitorRom(bin);
}
writeAsmCode(assembled:{buffer:number[],minAddr:number}):number {
for (let i = 0; i < assembled.buffer.length; i++) {
this.memory.poke(
assembled.minAddr + i,
assembled.buffer[i]);
}
return assembled.minAddr;
}
exec(execCount:number):number {
execCount = execCount || 1;
try {
for (let i = 0; i < execCount; i++) {
this.z80.exec();
this.clock();
}
} catch (ex) {
return -1;
}
return 0;
}
clock():void {
// HBLNK - 15.7 kHz clock
this.hblank.count();
// VBLNK - 50 Hz
this.vblank.count();
// CURSOR BLNK - 1 Hz
this.ic556.count();
}
setCassetteTape(tapeData:number[]):{header:MZ_TapeHeader, body:{buffer:number[]}}[] {
if (tapeData.length > 0) {
if (tapeData.length <= 128) {
this.dataRecorder_setCmt([]);
console.error("error buf.length <= 128");
return null;
}
this.mztArray = MZ_Tape.parseMZT(tapeData);
if (this.mztArray == null || this.mztArray.length < 1) {
console.error("setCassetteTape fail to parse");
return null;
}
}
this.dataRecorder_setCmt(tapeData);
return this.mztArray;
}
/**
* Get CMT content without ejecting.
* @returns {Buffer|null} CMT data buffer
*/
getCassetteTape():number[] {
const cmt = this.dataRecorder.getCmt();
if (cmt == null) {
return null;
}
return MZ_Tape.toBytes(cmt);
}
loadCassetteTape():void {
for (const mzt of this.mztArray) {
for (let i = 0; i < mzt.header.fileSize; i++) {
this.memory.poke(
mzt.header.addrLoad + i,
mzt.body.buffer[i]);
}
}
}
reset():void {
this.memory.enableBlock1();
this.memory.enableBlock1();
this.memory.changeBlock0_MONITOR();
this.memory.changeBlock1_VRAM();
// Clear VRAM
for (let i = 0; i < 40 * 25; i++) {
this.memory.poke(0xd000 + i, 0x00);
this.memory.poke(0xd800 + i, 0x71);
}
this.z80.reset();
}
getRegister():Record<string, number>[] {
const reg = this.z80.reg.cloneRaw();
const _reg = this.z80.regB.cloneRaw();
reg.IFF1 = this.z80.IFF1;
reg.IFF2 = this.z80.IFF2;
reg.IM = this.z80.IM;
reg.HALT = this.z80.HALT;
return [reg, _reg];
}
setPC(addr:number):void {
this.z80.reg.PC = addr;
}
/**
* Read memory.
* @param {number} addrStart start address
* @param {number} addrEnd (optional) end address
* @returns {number|Array<number>} A value in the start addr or memory block
*/
readMemory(addrStart:number, addrEnd:number):number|number[] {
if (addrEnd) {
return Array(addrEnd - addrStart).fill(null)
.map(() => this.memory.peek(addrStart++));
}
return this.memory.peek(addrStart);
}
setKeyState(strobe:number, bit:number, state:boolean):void {
this.keymatrix.setKeyMatrixState(strobe, bit, state);
}
clearBreakPoints():void {
this.z80.clearBreakPoints();
}
getBreakPoints():boolean[] {
return this.z80.getBreakPoints();
}
removeBreak(addr:number, size:number):void {
this.z80.removeBreak(addr, size);
}
addBreak(addr:number, size:number):void {
this.z80.setBreak(addr, size);
}
//
// For TransWorker
//
start():boolean {
if ("tid" in this && this.tid != null) {
console.warn("MZ700.start(): already started");
return false;
}
this.startEmulation();
this.opt.started();
return true;
}
stop():void {
const running = (this.tid != null);
this.stopEmulation();
if (running) {
this.opt.stopped();
}
}
step():void {
if ("tid" in this && this.tid != null) {
this.stop();
return;
}
this.exec(1);
this.opt.started();
this.opt.stopped();
}
run():void {
try {
if (this._cycleToWait > 0) {
this._cycleToWait--;
} else {
const cycle0 = this.z80.consumedTCycle;
this.z80.exec();
this._cycleToWait = this.z80.consumedTCycle - cycle0;
}
this.clock();
} catch (ex) {
console.log("Error:", ex);
console.log(ex.stack);
this.stop();
this.opt.onBreak();
}
}
dataRecorder_setCmt(bytes:number[]):boolean[] {
if (bytes.length === 0) {
this.dataRecorder.setCmt([]);
return [];
}
const cmt = MZ_Tape.fromBytes(bytes);
this.dataRecorder.setCmt(cmt);
return cmt;
}
dataRecorder_ejectCmt():number[] {
if (this.dataRecorder.isCmtSet()) {
const cmt = this.dataRecorder.ejectCmt();
if (cmt != null) {
return MZ_Tape.toBytes(cmt);
}
}
return [];
}
dataRecorder_pushPlay():void {
this.dataRecorder.play();
}
dataRecorder_pushRec():void {
if (this.dataRecorder.isCmtSet()) {
this.dataRecorder.ejectCmt();
}
this.dataRecorder.setCmt([]);
this.dataRecorder.rec();
}
dataRecorder_pushStop():void {
this.dataRecorder.stop();
}
dataRecorder_motorOn(state:boolean):void {
this.dataRecorder.m_on(state);
}
dataRecorder_readBit():boolean {
return this.dataRecorder.rdata(this.z80.consumedTCycle);
}
dataRecorder_writeBit(state:boolean):void {
this.dataRecorder.wdata(state, this.z80.consumedTCycle);
}
getClockFactor():number {
return this.clockFactor;
}
setClockFactor(clockFactor:number):void {
const running = (this.tid != null);
if (running) {
this.stopEmulation();
}
this.clockFactor = clockFactor;
if (running) {
this.startEmulation();
}
}
getActualClockFreq():number {
return this.actualClockFreq;
}
startEmulation():void {
const execCount = Math.round(200 * this.clockFactor);
this.tid = FractionalTimer.setInterval(
this.run.bind(this), MZ700.DEFAULT_TIMER_INTERVAL, 80, execCount);
const mint = 1000;
this.tidMeasClock = setInterval(() => {
this.actualClockFreq = (this.z80.consumedTCycle - this.tCycle0) / (mint / 1000);
this.tCycle0 = this.z80.consumedTCycle;
}, mint);
}
stopEmulation():void {
if (this.tid != null) {
FractionalTimer.clearInterval(this.tid);
this.tid = null;
}
if (this.tidMeasClock != null) {
clearInterval(this.tidMeasClock);
this.tidMeasClock = null;
this.actualClockFreq = 0.0;
}
}
//
// Disassemble
//
static disassemble(mztArray:{header:MZ_TapeHeader, body:{buffer:number[]}}[]):{
outbuf:string,
dasmlines:string[],
asmlist:Z80LineAssembler[],
} {
const dasmlist = [];
mztArray.forEach(mzt => {
console.assert(
mzt.header.constructor === MZ_TapeHeader,
"No MZT-header");
const mzthead = mzt.header.getHeadline().split("\n");
Array.prototype.push.apply(dasmlist, mzthead.map(line => {
const asmline = new Z80LineAssembler();
asmline.setComment(line);
return asmline;
}));
Array.prototype.push.apply(dasmlist, Z80.dasm(
mzt.body.buffer, 0,
mzt.header.fileSize,
mzt.header.addrLoad));
});
const dasmlines = Z80.dasmlines(dasmlist);
return {
outbuf: dasmlines.join("\n") + "\n",
dasmlines,
asmlist: dasmlist
};
}
attachPCG700(pcg700:PCG700):void {
this.mmio.onWrite(0xE010, value => pcg700.setPattern(value & 0xff));
this.mmio.onWrite(0xE011, value => pcg700.setAddrLo(value & 0xff));
this.mmio.onWrite(0xE012, value => {
pcg700.setAddrHi(value & PCG700.ADDR);
pcg700.setCopy(value & PCG700.COPY);
pcg700.setWE(value & PCG700.WE);
pcg700.setSSW(value & PCG700.SSW);
});
this.memory.poke(0xE010, 0x00);
this.memory.poke(0xE011, 0x00);
this.memory.poke(0xE012, 0x18);
}
}
module.exports = MZ700;