serial test harness, readFile/writeFile, tty save/load state

This commit is contained in:
Steven Hugg 2020-10-17 14:34:08 -05:00
parent d482145319
commit 244377e2a6
11 changed files with 374 additions and 29 deletions

View File

@ -4,9 +4,6 @@
.gutter-offset {
width: 3em;
}
.CodeMirror-gutter-elt:hover {
text-decoration: underline;
}
.gutter-bytes {
width: 6em;
}
@ -17,6 +14,10 @@
width: 1em;
cursor: cell;
}
.CodeMirror-gutter-elt:hover {
text-decoration: underline;
cursor: cell;
}
.currentpc-span {
background-color: #7e2a70;
}
@ -659,6 +660,13 @@ div.asset_toolbar {
padding-left: 0.5em;
padding-right: 0.5em;
}
.transcript-style-16 { /* alternate */
color: #6666ff;
background-color: #eeeeff;
}
.transcript-style-32 { /* alternate 2 */
background-color: #eee;
}
.transcript-reverse {
background-color: #666;
color: #ddd;

View File

@ -188,6 +188,7 @@ TODO:
- no import in workers
- copy to gen/ directory (allowJs is weird)
- ca65 line numbers are not aligned with source code
- neither are DASM when you change comments
- segments not read properly
- Debug Browser
- more stuff like 7800 display lists
@ -197,6 +198,7 @@ TODO:
Probing
- probe log doesn't start @ reset
Debug, play then halt cpu doesn't highlight final line
WEB WORKER FORMAT

View File

@ -135,6 +135,9 @@ export interface Platform {
stopProbing?() : void;
isBlocked?() : boolean; // is blocked, halted, or waiting for input?
readFile?(path: string) : FileData;
writeFile?(path: string, data: FileData) : boolean;
}
export interface Preset {
@ -186,6 +189,7 @@ export interface EmuRecorder {
export abstract class BasePlatform {
recorder : EmuRecorder = null;
debugSymbols : DebugSymbols;
internalFiles : {[path:string] : FileData} = {};
abstract loadState(state : EmuState) : void;
abstract saveState() : EmuState;
@ -208,6 +212,13 @@ export abstract class BasePlatform {
getDebugTree() : {} {
return this.saveState();
}
readFile(path: string) : FileData {
return this.internalFiles[path];
}
writeFile(path: string, data: FileData) : boolean {
this.internalFiles[path] = data;
return true;
}
}
export abstract class BaseDebugPlatform extends BasePlatform {
@ -283,7 +294,7 @@ export abstract class BaseDebugPlatform extends BasePlatform {
}
pollControls() {
}
nextFrame(novideo : boolean) {
nextFrame(novideo : boolean) : number {
this.pollControls();
this.updateRecorder();
this.preFrame();
@ -1021,7 +1032,7 @@ export function lookupSymbol(platform:Platform, addr:number, extra:boolean) {
/// new Machine platform adapters
import { Bus, Resettable, FrameBased, VideoSource, SampledAudioSource, AcceptsROM, AcceptsBIOS, AcceptsKeyInput, SavesState, SavesInputState, HasCPU, TrapCondition, CPU } from "./devices";
import { Bus, Resettable, FrameBased, VideoSource, SampledAudioSource, AcceptsROM, AcceptsBIOS, AcceptsKeyInput, SavesState, SavesInputState, HasCPU, TrapCondition, CPU, HasSerialIO, SerialIOInterface } from "./devices";
import { Probeable, RasterFrameBased, AcceptsPaddleInput, SampledAudioSink, ProbeAll, NullProbe } from "./devices";
import { SampledAudio } from "./audio";
import { ProbeRecorder } from "./recorder";
@ -1050,6 +1061,9 @@ export function hasProbe(arg:any): arg is Probeable {
export function hasBIOS(arg:any): arg is AcceptsBIOS {
return typeof arg.loadBIOS == 'function';
}
export function hasSerialIO(arg:any): arg is HasSerialIO {
return typeof arg.connectSerialIO === 'function';
}
export abstract class BaseMachinePlatform<T extends Machine> extends BaseDebugPlatform implements Platform {
machine : T;
@ -1123,6 +1137,9 @@ export abstract class BaseMachinePlatform<T extends Machine> extends BaseDebugPl
m.loadBIOS(data, title);
};
}
if (hasSerialIO(m) && this.serialIOInterface) {
m.connectSerialIO(this.serialIOInterface);
}
}
loadROM(title, data) {
@ -1130,7 +1147,9 @@ export abstract class BaseMachinePlatform<T extends Machine> extends BaseDebugPl
this.reset();
}
loadBIOS; // only set if hasBIOS() is true
loadBIOS : (title, data) => void; // only set if hasBIOS() is true
serialIOInterface : SerialIOInterface; // set if hasSerialIO() is true
pollControls() {
this.poller && this.poller.poll();

View File

@ -107,6 +107,26 @@ export interface AcceptsPaddleInput {
setPaddleInput(controller:number, value:number) : void;
}
// SERIAL I/O
export interface SerialIOInterface {
// from machine to platform
clearToSend() : boolean;
sendByte(b : number);
// from platform to machine
byteAvailable() : boolean;
recvByte() : number;
// implement these too
reset() : void;
advance(clocks: number) : void;
}
export interface HasSerialIO {
connectSerialIO(serial: SerialIOInterface);
}
/// PROFILER
export interface Probeable {
connectProbe(probe: ProbeAll) : void;
}
@ -118,8 +138,6 @@ export function xorshift32(x : number) : number {
return x;
}
/// PROFILER
export interface ProbeTime {
logClocks(clocks:number);
logNewScanline();

View File

@ -159,6 +159,30 @@ export class TeleType {
$(this.page).css('font-size', charwidth + 'px');
this.scrollToBottom();
}
saveState() {
return {
curstyle: this.curstyle,
reverse: this.reverse,
col: this.col,
row: this.row,
ncharsout : this.ncharsout,
lines: this.lines.map((line) => line.cloneNode(true)),
}
}
loadState(state) {
console.log(state);
this.curstyle = state.curstyle;
this.reverse = state.reverse;
this.col = state.col;
this.row = state.row;
this.ncharsout = state.ncharsout;
$(this.page).empty();
for (var i=0; i<state.lines.length; i++) {
this.page.appendChild(state.lines[i]);
}
this.lines = state.lines;
this.curline = state.lines[this.row];
}
}
export class TeleTypeWithKeyboard extends TeleType {

View File

@ -487,3 +487,10 @@ export function getRootBasePlatform(platform : string) : string {
return getRootPlatform(getBasePlatform(platform));
}
export function convertDataToUint8Array(data: string|Uint8Array) : Uint8Array {
return (typeof data === 'string') ? stringToByteArray(data) : data;
}
export function convertDataToString(data: string|Uint8Array) : string {
return (data instanceof Uint8Array) ? byteArrayToUTF8(data) : data;
}

View File

@ -1389,10 +1389,14 @@ function clearBreakpoint() {
showDebugInfo();
}
function resetPlatform() {
platform.reset();
}
function resetAndRun() {
if (!checkRunReady()) return;
clearBreakpoint();
platform.reset();
resetPlatform();
_resume();
}
@ -1403,11 +1407,11 @@ function resetAndDebug() {
if (platform.setupDebug && platform.runEval) { // TODO??
clearBreakpoint();
_resume();
platform.reset();
resetPlatform();
setupBreakpoint("restart");
platform.runEval((c) => { return true; }); // break immediately
} else {
platform.reset();
resetPlatform();
_resume();
}
if (wasRecording) _enableRecording();
@ -1930,8 +1934,9 @@ function globalErrorHandler(msgevent) {
} else {
var err = msgevent.error || msgevent.reason;
if (err != null && err instanceof EmuHalt) {
msg = (err && err.message) || msg;
showExceptionAsError(err, msg);
setDebugButtonState("pause", "stopped");
emulationHalted(err);
// TODO: reset platform?
}
}
}
@ -2294,6 +2299,23 @@ redirectToHTTPS();
//// ELECTRON STUFF
export function setTestInput(path: string, data: FileData) {
platform.writeFile(path, data);
}
export function getTestOutput(path: string) : FileData {
return platform.readFile(path);
}
export function getSaveState() {
return platform.saveState();
}
export function emulationHalted(err: EmuHalt) {
var msg = (err && err.message) || msg;
showExceptionAsError(err, msg);
}
// get remote file from local fs
declare var getWorkspaceFile, putWorkspaceFile;
export function getElectronFile(url:string, success:(text:string|Uint8Array)=>void, datatype:'text'|'arraybuffer') {

View File

@ -1,30 +1,41 @@
import { MOS6502 } from "../common/cpu/MOS6502";
import { BasicHeadlessMachine } from "../common/devices";
import { BasicHeadlessMachine, HasSerialIO, SerialIOInterface } from "../common/devices";
import { padBytes, newAddressDecoder } from "../common/emu"; // TODO
export class Devel6502 extends BasicHeadlessMachine {
const INPUT_HALTED = 31;
export class Devel6502 extends BasicHeadlessMachine implements HasSerialIO {
cpuFrequency = 1000000;
defaultROMSize = 0x8000;
cpu = new MOS6502();
ram = new Uint8Array(0x4000);
rom : Uint8Array;
digits = [];
serial : SerialIOInterface;
constructor() {
super();
this.connectCPUMemoryBus(this);
}
connectSerialIO(serial: SerialIOInterface) {
this.serial = serial;
}
read = newAddressDecoder([
[0x0000, 0x3fff, 0x3fff, (a) => { return this.ram[a]; }],
[0x4000, 0x4000, 0xffff, (a) => { return this.serial.byteAvailable() ? 0x80 : 0 }],
[0x4001, 0x4001, 0xffff, (a) => { return this.serial.recvByte() }],
[0x4002, 0x4002, 0xffff, (a) => { return this.serial.clearToSend() ? 0x80 : 0 }],
[0x8000, 0xffff, 0x7fff, (a) => { return this.rom && this.rom[a]; }],
]);
write = newAddressDecoder([
[0x0000, 0x3fff, 0x3fff, (a,v) => { this.ram[a] = v; }],
[0x4003, 0x4003, 0xffff, (a,v) => { return this.serial.sendByte(v) }],
[0x400f, 0x400f, 0xffff, (a,v) => { this.inputs[INPUT_HALTED] = 1; }],
]);
readConst(a:number) : number {
@ -39,5 +50,20 @@ export class Devel6502 extends BasicHeadlessMachine {
}
return clock;
}
advanceCPU() {
if (this.isHalted()) return 1;
var n = super.advanceCPU();
if (this.serial) this.serial.advance(n);
return n;
}
reset() {
this.inputs[INPUT_HALTED] = 0;
super.reset();
if (this.serial) this.serial.reset();
}
isHalted() { return this.inputs[INPUT_HALTED] != 0; }
}

View File

@ -1,15 +1,169 @@
import { Platform, getOpcodeMetadata_6502, getToolForFilename_6502 } from "../common/baseplatform";
import { PLATFORMS } from "../common/emu";
import { Platform } from "../common/baseplatform";
import { EmuHalt, PLATFORMS } from "../common/emu";
import { Devel6502 } from "../machine/devel";
import { Base6502MachinePlatform } from "../common/baseplatform";
import { SerialIOInterface } from "../common/devices";
import { convertDataToUint8Array, hex } from "../common/util";
import { TeleType } from "../common/teletype";
import { emulationHalted, loadScript } from "../ide/ui";
var DEVEL_6502_PRESETS = [
{id:'hello.dasm', name:'Hello World (ASM)'},
];
class SerialInOutViewer {
div : HTMLElement;
tty : TeleType;
constructor(div: HTMLElement) {
div.style.overflowY = 'auto';
var gameport = $('<div id="gameport"/>').appendTo(div);
var windowport = $('<div id="windowport" class="transcript"/>').appendTo(gameport);
this.div = windowport[0];
}
start() {
this.tty = new TeleType(this.div, false);
//this.tty.ncols = 40;
}
reset() {
this.tty.clear();
}
saveState() {
return this.tty.saveState();
}
loadState(state) {
this.tty.loadState(state);
}
}
function byteToASCII(b: number) : string {
if (b == 10) return '';
if (b < 32)
return String.fromCharCode(b + 0x2400);
else
return String.fromCharCode(b);
}
export class SerialTestHarness implements SerialIOInterface {
viewer : SerialInOutViewer;
bufferedRead : boolean = true;
cyclesPerByte = 1000000/(57600/8); // 138.88888 cycles
maxOutputBytes = 4096;
inputBytes : Uint8Array;
outputBytes : number[];
inputIndex : number;
clk : number;
bufin : string;
clearToSend(): boolean {
return this.outputBytes.length < this.maxOutputBytes;
}
sendByte(b: number) {
if (this.clearToSend()) {
this.outputBytes.push(b);
this.viewer.tty.addtext(byteToASCII(b), 2|32);
if (b == 10) this.viewer.tty.newline();
if (!this.clearToSend()) {
this.viewer.tty.newline();
this.viewer.tty.addtext("⚠️ OUTPUT BUFFER FULL ⚠️", 4);
}
}
}
byteAvailable(): boolean {
return this.readIndex() > this.inputIndex;
}
recvByte(): number {
var index = this.readIndex();
this.inputIndex = index;
var b = this.inputBytes[index] | 0;
//this.bufin += byteToASCII(b);
this.viewer.tty.addtext(byteToASCII(b), 2|16);
if (b == 10) this.viewer.tty.newline();
return b;
}
readIndex(): number {
return this.bufferedRead ? (this.inputIndex+1) : Math.floor(this.clk / this.cyclesPerByte);
}
reset() {
this.inputIndex = -1;
this.clk = 0;
this.outputBytes = [];
this.bufin = '';
}
advance(clocks: number) {
this.clk += clocks;
}
saveState() {
return {
clk: this.clk,
idx: this.inputIndex,
out: this.outputBytes.slice()
}
}
loadState(state) {
this.clk = state.clk;
this.inputIndex = state.idx;
this.outputBytes = state.out.slice();
}
}
class Devel6502Platform extends Base6502MachinePlatform<Devel6502> implements Platform {
serial : SerialTestHarness;
serview : SerialInOutViewer;
constructor(mainElement: HTMLElement) {
super(mainElement);
this.serview = new SerialInOutViewer(mainElement);
}
async start() {
super.start();
await loadScript('./gen/common/teletype.js');
this.serial = new SerialTestHarness();
this.serial.viewer = this.serview;
this.serview.start();
this.machine.connectSerialIO(this.serial);
}
reset() {
this.serial.inputBytes = convertDataToUint8Array(this.internalFiles['serialin.dat']);
super.reset();
this.serview.reset();
}
isBlocked() {
return this.machine.isHalted();
}
advance(novideo: boolean) {
if (this.isBlocked()) {
this.internalFiles['serialout.dat'] = new Uint8Array(this.serial.outputBytes);
throw new EmuHalt(null); // TODO: throws nasty exception
}
return super.advance(novideo);
}
saveState() {
var state : any = super.saveState();
state.serial = this.serial.saveState();
state.serview = this.serview.saveState();
return state;
}
loadState(state) {
super.loadState(state);
this.serial.loadState(state.serial);
this.serview.loadState(state.serview);
// TODO: reload tty UI
}
newMachine() { return new Devel6502(); }
getPresets() { return DEVEL_6502_PRESETS; }
getDefaultExtension() { return ".dasm"; };

View File

@ -1,15 +1,67 @@
import { hasAudio, hasVideo, Machine } from "../common/baseplatform";
import { SampledAudioSink } from "../common/devices";
import { hasAudio, hasSerialIO, hasVideo, Machine } from "../common/baseplatform";
import { SampledAudioSink, SerialIOInterface } from "../common/devices";
global.atob = require('atob');
global.btoa = require('btoa');
class NullAudio implements SampledAudioSink {
feedSample(value: number, count: number): void {
}
}
}
class MachineRunner {
// TODO: merge with platform
class SerialTestHarness implements SerialIOInterface {
bufferedRead: boolean = true;
cyclesPerByte = 1000000 / (57600 / 8); // 138.88888 cycles
maxOutputBytes = 4096;
inputBytes: Uint8Array;
outputBytes: number[];
inputIndex: number;
clk: number;
bufin: string;
clearToSend(): boolean {
return this.outputBytes.length < this.maxOutputBytes;
}
sendByte(b: number) {
if (this.clearToSend()) {
this.outputBytes.push(b);
}
}
byteAvailable(): boolean {
return this.readIndex() > this.inputIndex;
}
recvByte(): number {
var index = this.readIndex();
this.inputIndex = index;
var b = this.inputBytes[index] | 0;
return b;
}
readIndex(): number {
return this.bufferedRead ? (this.inputIndex + 1) : Math.floor(this.clk / this.cyclesPerByte);
}
reset() {
this.inputIndex = -1;
this.clk = 0;
this.outputBytes = [];
this.bufin = '';
}
advance(clocks: number) {
this.clk += clocks;
}
}
///
export class MachineRunner {
machine: Machine;
pixels: Uint32Array;
serial: SerialTestHarness;
constructor(machine: Machine) {
this.machine = machine;
@ -23,6 +75,10 @@ class MachineRunner {
if (hasAudio(this.machine)) {
this.machine.connectAudio(new NullAudio());
}
if (hasSerialIO(this.machine)) {
this.serial = new SerialTestHarness();
this.machine.connectSerialIO(this.serial);
}
this.machine.reset();
}
run() {
@ -30,8 +86,8 @@ class MachineRunner {
}
}
async function loadMachine(modname: string, clsname: string) : Promise<Machine> {
var mod = await import('../machine/'+modname);
async function loadMachine(modname: string, clsname: string): Promise<Machine> {
var mod = await import('../machine/' + modname);
var cls = mod[clsname];
var machine = new cls();
return machine;
@ -45,7 +101,6 @@ async function runMachine() {
console.log(runner.machine.saveState());
}
global.atob = require('atob');
global.btoa = require('btoa');
runMachine();
if (require.main === module) {
runMachine();
}

View File

@ -267,6 +267,11 @@ var PLATFORM_PARAMS = {
extra_link_args: ['crt0-zx.rel'],
extra_link_files: ['crt0-zx.rel', 'crt0-zx.lst'],
},
'devel-6502': {
cfgfile: 'devel-6502.cfg',
libargs: ['crt0.o', 'sim6502.lib'],
extra_link_files: ['crt0.o', 'devel-6502.cfg'],
},
};
PLATFORM_PARAMS['sms-sms-libcv'] = PLATFORM_PARAMS['sms-sg1000-libcv'];
@ -532,6 +537,7 @@ function setupFS(FS, name:string) {
var WORKERFS = FS.filesystems['WORKERFS'];
if (name === '65-vector') name = '65-sim6502'; // TODO
if (name === '65-atari7800') name = '65-sim6502'; // TODO
if (name === '65-devel') name = '65-sim6502'; // TODO
if (!fsMeta[name]) throw Error("No filesystem for '" + name + "'");
FS.mkdir('/share');
FS.mount(WORKERFS, {
@ -2712,6 +2718,8 @@ var TOOL_PRELOADFS = {
'ca65-vector': '65-sim6502',
'cc65-atari7800': '65-sim6502',
'ca65-atari7800': '65-sim6502',
'cc65-devel': '65-sim6502',
'ca65-devel': '65-sim6502',
'sdasz80': 'sdcc',
'sdcc': 'sdcc',
'sccz80': 'sccz80',
@ -2788,6 +2796,8 @@ function handleMessage(data : WorkerMessage) : WorkerResult {
// preload file system
if (data.preload) {
var fs = TOOL_PRELOADFS[data.preload];
if (!fs && data.platform)
fs = TOOL_PRELOADFS[data.preload+'-'+getBasePlatform(data.platform)];
if (!fs && data.platform)
fs = TOOL_PRELOADFS[data.preload+'-'+getRootBasePlatform(data.platform)];
if (fs && !fsMeta[fs])