335 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			335 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
import {
 | 
						|
    HSYNC,
 | 
						|
    VSYNC,
 | 
						|
} from './vga.js';
 | 
						|
 | 
						|
import {
 | 
						|
    BUTTON_DOWN,
 | 
						|
    BUTTON_A,
 | 
						|
} from './gamepad.js';
 | 
						|
 | 
						|
const {
 | 
						|
    Observable,
 | 
						|
    Subject,
 | 
						|
    concat,
 | 
						|
    defer,
 | 
						|
    range,
 | 
						|
} = rxjs;
 | 
						|
 | 
						|
const {
 | 
						|
    concatMap,
 | 
						|
    concatAll,
 | 
						|
    finalize,
 | 
						|
} = rxjs.operators;
 | 
						|
 | 
						|
const MAX_PAYLOAD_SIZE = 60;
 | 
						|
const START_OF_FRAME = 'L'.charCodeAt(0);
 | 
						|
const INIT_CHECKSUM = 'g'.charCodeAt(0);
 | 
						|
 | 
						|
/** Repeat an Observable count times
 | 
						|
 * @param {number} count
 | 
						|
 * @param {Observable} observable
 | 
						|
 * @return {Observable}
 | 
						|
 */
 | 
						|
function replicate(count, observable) {
 | 
						|
    // return concat(...new Array(count).fill(observable));
 | 
						|
    let go = (observer) => {
 | 
						|
        if (count-- > 0) {
 | 
						|
            observable.subscribe({
 | 
						|
                next: (value) => observer.next(value),
 | 
						|
                error: (err) => observer.error(err),
 | 
						|
                complete: () => go(observer),
 | 
						|
            });
 | 
						|
        } else {
 | 
						|
            observer.complete();
 | 
						|
        }
 | 
						|
    };
 | 
						|
 | 
						|
    return Observable.create(go);
 | 
						|
}
 | 
						|
 | 
						|
/** Loader */
 | 
						|
export class Loader {
 | 
						|
    /** Create a new Loader
 | 
						|
     * @param {Gigatron} cpu
 | 
						|
     */
 | 
						|
    constructor(cpu) {
 | 
						|
        this.cpu = cpu;
 | 
						|
        this.strobes = new Subject();
 | 
						|
    }
 | 
						|
 | 
						|
    /** load a gt1 file
 | 
						|
     * @param {File} file
 | 
						|
     * @return {Observable}
 | 
						|
     */
 | 
						|
    load(file) {
 | 
						|
        return this.readFile(file).pipe(
 | 
						|
            concatMap((buffer) => {
 | 
						|
                let data = new DataView(buffer);
 | 
						|
                return concat(
 | 
						|
                    this.startLoader(),
 | 
						|
                    defer(() => {
 | 
						|
                        // Send one frame with false checksum to force
 | 
						|
                        // a checksum resync at the receiver
 | 
						|
                        this.checksum = 0;
 | 
						|
                        return this.sendFrame(0xff, 0);
 | 
						|
                    }),
 | 
						|
                    defer(() => {
 | 
						|
                        // Setup checksum properly
 | 
						|
                        this.checksum = INIT_CHECKSUM;
 | 
						|
                        return this.sendSegments(data);
 | 
						|
                    }));
 | 
						|
            }),
 | 
						|
            finalize(() => {
 | 
						|
                // Set the input register back to quiesced state
 | 
						|
                this.cpu.inReg = 0xff;
 | 
						|
            }));
 | 
						|
    }
 | 
						|
 | 
						|
    /** read a file returning a Promise
 | 
						|
     * @param {File} file
 | 
						|
     * @return {Observable}
 | 
						|
     */
 | 
						|
    readFile(file) {
 | 
						|
        return Observable.create((observer) => {
 | 
						|
            let reader = new FileReader();
 | 
						|
            reader.onload = (event) => {
 | 
						|
                observer.next(reader.result);
 | 
						|
                observer.complete();
 | 
						|
            };
 | 
						|
            reader.onerror = (event) => {
 | 
						|
                observer.error(new Error('FileReader error'));
 | 
						|
            };
 | 
						|
            reader.readAsArrayBuffer(file);
 | 
						|
        });
 | 
						|
    }
 | 
						|
 | 
						|
    /** start the loader on the gigatron
 | 
						|
     * @return {Observable}
 | 
						|
     */
 | 
						|
    startLoader() {
 | 
						|
        return concat(
 | 
						|
            defer(() => {
 | 
						|
                this.cpu.reset();
 | 
						|
                return replicate(100, this.atPosedge(VSYNC));
 | 
						|
            }),
 | 
						|
            replicate(10, this.pressButton(BUTTON_DOWN, 1, 1)),
 | 
						|
            this.pressButton(BUTTON_A, 1, 60)
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    /** simulate a button press
 | 
						|
     * @param {number} bit
 | 
						|
     * @param {number} downTime
 | 
						|
     * @param {number} upTime
 | 
						|
     * @return {Observable}
 | 
						|
     */
 | 
						|
    pressButton(bit, downTime, upTime) {
 | 
						|
        return concat(
 | 
						|
            defer(() => {
 | 
						|
                this.cpu.inReg = bit ^ 0xff;
 | 
						|
                return replicate(downTime, this.atPosedge(VSYNC));
 | 
						|
            }),
 | 
						|
            defer(() => {
 | 
						|
                this.cpu.inReg = 0xff;
 | 
						|
                return replicate(upTime, this.atPosedge(VSYNC));
 | 
						|
            })
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    /** load sections from data until busy
 | 
						|
     * @param {DataView} data
 | 
						|
     * @return {Observable<Observable<T>>}
 | 
						|
     */
 | 
						|
    sendSegments(data) {
 | 
						|
        return Observable.create((observer) => {
 | 
						|
            let offset = 0;
 | 
						|
            while (offset < data.byteLength) {
 | 
						|
                if (data.getUint8(offset) == 0 && offset != 0) {
 | 
						|
                    // start address segment
 | 
						|
                    offset += 1;
 | 
						|
                    let startAddr = data.getUint16(offset);
 | 
						|
                    offset += 2;
 | 
						|
                    if (startAddr != 0) {
 | 
						|
                        observer.next(this.sendStartCommand(startAddr));
 | 
						|
                    }
 | 
						|
                    break;
 | 
						|
                } else {
 | 
						|
                    // data segment
 | 
						|
                    let addr = data.getUint16(offset);
 | 
						|
                    offset += 2;
 | 
						|
                    let size = data.getUint8(offset);
 | 
						|
                    offset += 1;
 | 
						|
                    if (size == 0) {
 | 
						|
                        size = 256;
 | 
						|
                    }
 | 
						|
                    let payload = new DataView(
 | 
						|
                        data.buffer,
 | 
						|
                        data.byteOffset + offset,
 | 
						|
                        size);
 | 
						|
                    observer.next(this.sendDataSegment(addr, payload));
 | 
						|
                    offset += size;
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            if (offset > data.byteLength) {
 | 
						|
                observer.error(new Error('Last segment exceeds file size'));
 | 
						|
            }
 | 
						|
 | 
						|
            observer.complete();
 | 
						|
        }).pipe(concatAll());
 | 
						|
    }
 | 
						|
 | 
						|
    /** send a start command
 | 
						|
     * @param {number} addr
 | 
						|
     * @return {Observable}
 | 
						|
     */
 | 
						|
    sendStartCommand(addr) {
 | 
						|
        return this.sendFrame(START_OF_FRAME, addr);
 | 
						|
    }
 | 
						|
 | 
						|
    /** send a data block
 | 
						|
     * @param {number} addr
 | 
						|
     * @param {DataView} data
 | 
						|
     * @return {Observable}
 | 
						|
     */
 | 
						|
    sendDataSegment(addr, data) {
 | 
						|
        return Observable.create((observer) => {
 | 
						|
            let buffer = data.buffer;
 | 
						|
            let size = data.byteLength;
 | 
						|
            let offset = data.byteOffset;
 | 
						|
            let bytesInPage = 256 - (addr & 255);
 | 
						|
 | 
						|
            if (size > bytesInPage) {
 | 
						|
                observer.error(new Error('Segment crosses page boundary'));
 | 
						|
            } else {
 | 
						|
                while (size != 0) {
 | 
						|
                    let n = Math.min(size, MAX_PAYLOAD_SIZE);
 | 
						|
                    let payload = new DataView(buffer, offset, n);
 | 
						|
                    observer.next(this.sendFrame(
 | 
						|
                        START_OF_FRAME, addr, payload));
 | 
						|
                    addr += n;
 | 
						|
                    offset += n;
 | 
						|
                    size -= n;
 | 
						|
                }
 | 
						|
                observer.complete();
 | 
						|
            }
 | 
						|
        }).pipe(concatAll());
 | 
						|
    }
 | 
						|
 | 
						|
    /** send the payload frame
 | 
						|
     * @param {number} firstByte
 | 
						|
     * @param {number} addr
 | 
						|
     * @param {DataView} payload
 | 
						|
     * @return {Observable}
 | 
						|
     */
 | 
						|
    sendFrame(firstByte, addr, payload) {
 | 
						|
        return concat(
 | 
						|
            this.atNegedge(VSYNC),
 | 
						|
            // account for 2 cycles delay in 74HCT595 and ?
 | 
						|
            this.atPosedge(HSYNC),
 | 
						|
            this.atPosedge(HSYNC),
 | 
						|
            this.sendDataBits(firstByte, 8),
 | 
						|
            defer(() => {
 | 
						|
                this.checksum = (this.checksum + (firstByte << 6)) & 0xff;
 | 
						|
                return this.sendDataBits(payload ? payload.byteLength : 0, 6);
 | 
						|
            }),
 | 
						|
            this.sendDataBits(addr & 0xff, 8),
 | 
						|
            this.sendDataBits(addr >> 8, 8),
 | 
						|
            this.sendDataBytes(payload),
 | 
						|
            defer(() => {
 | 
						|
                this.checksum = (-this.checksum) & 0xff;
 | 
						|
                return this.sendBits(this.checksum, 8);
 | 
						|
            }));
 | 
						|
    }
 | 
						|
 | 
						|
    /** send bytes from payload
 | 
						|
     * @param {Uint8Array} payload
 | 
						|
     * @return {Observable}
 | 
						|
     */
 | 
						|
    sendDataBytes(payload) {
 | 
						|
        return range(0, MAX_PAYLOAD_SIZE).pipe(
 | 
						|
            concatMap((offset) => {
 | 
						|
                let byte = (payload && offset < payload.byteLength) ?
 | 
						|
                    payload.getUint8(offset) : 0;
 | 
						|
                return this.sendDataBits(byte, 8);
 | 
						|
            }));
 | 
						|
    }
 | 
						|
 | 
						|
    /** send bits and add to checksum
 | 
						|
     * @param {number} value - byte containing bits to send (msb first)
 | 
						|
     * @param {number} n - number of bits to send
 | 
						|
     * @return {Observable}
 | 
						|
     */
 | 
						|
    sendDataBits(value, n) {
 | 
						|
        return defer(() => {
 | 
						|
            this.checksum = (this.checksum + value) & 0xff;
 | 
						|
            return this.sendBits(value, n);
 | 
						|
        });
 | 
						|
    }
 | 
						|
 | 
						|
    /** shift one bit into inReg
 | 
						|
     * @param {number} bit
 | 
						|
     */
 | 
						|
    shiftBit(bit) {
 | 
						|
        this.cpu.inReg = ((this.cpu.inReg << 1) & 0xff) | (bit ? 1 : 0);
 | 
						|
    }
 | 
						|
 | 
						|
    /** send bits
 | 
						|
     * @param {number} value - byte containing bits to send (msb first)
 | 
						|
     * @param {number} n - number of bits to send
 | 
						|
     * @return {Observable}
 | 
						|
     */
 | 
						|
    sendBits(value, n) {
 | 
						|
        return range(0, n).pipe(
 | 
						|
            concatMap((i) => {
 | 
						|
                this.shiftBit(value & (1 << (n - i - 1)));
 | 
						|
                return this.atPosedge(HSYNC);
 | 
						|
            }));
 | 
						|
    }
 | 
						|
 | 
						|
    /** wait for negedge of signal
 | 
						|
     * @param {number} mask
 | 
						|
     * @return {Observable}
 | 
						|
     */
 | 
						|
    atNegedge(mask) {
 | 
						|
        return Observable.create((observer) => {
 | 
						|
            let prev = this.cpu.out;
 | 
						|
            let subscription = this.strobes.subscribe((curr) => {
 | 
						|
                if (prev & ~curr & mask) {
 | 
						|
                    observer.complete();
 | 
						|
                    subscription.unsubscribe();
 | 
						|
                } else {
 | 
						|
                    prev = curr;
 | 
						|
                }
 | 
						|
            });
 | 
						|
        });
 | 
						|
    }
 | 
						|
 | 
						|
    /** wait for posedge of signal
 | 
						|
     * @param {number} mask
 | 
						|
     * @return {Observable}
 | 
						|
     */
 | 
						|
    atPosedge(mask) {
 | 
						|
        return Observable.create((observer) => {
 | 
						|
            let prev = this.cpu.out;
 | 
						|
            let subscription = this.strobes.subscribe((curr) => {
 | 
						|
                if (~prev & curr & mask) {
 | 
						|
                    observer.complete();
 | 
						|
                    subscription.unsubscribe();
 | 
						|
                } else {
 | 
						|
                    prev = curr;
 | 
						|
                }
 | 
						|
            });
 | 
						|
        });
 | 
						|
    }
 | 
						|
 | 
						|
    /** advance one tick */
 | 
						|
    tick() {
 | 
						|
        if ((this.out ^ this.cpu.out) & (HSYNC | VSYNC)) {
 | 
						|
            this.out = this.cpu.out;
 | 
						|
            this.strobes.next(this.out);
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |