esp32emu / js /ps2.js
HelloSun's picture
ESP32 emulator update
8d577f5 verified
Raw History Blame Contribute Delete
7.9 kB
// PS/2 keyboard (and mouse) device models attached to a pair of GPIO pads (open-drain bus with pull-ups)
const SET2 = {
// key code (KeyboardEvent.code) -> [scancode bytes (make)], extended flag handled via 0xE0 prefix
Escape: [0x76], F1: [0x05], F2: [0x06], F3: [0x04], F4: [0x0C], F5: [0x03], F6: [0x0B], F7: [0x83], F8: [0x0A], F9: [0x01], F10: [0x09], F11: [0x78], F12: [0x07],
Backquote: [0x0E], Digit1: [0x16], Digit2: [0x1E], Digit3: [0x26], Digit4: [0x25], Digit5: [0x2E], Digit6: [0x36], Digit7: [0x3D], Digit8: [0x3E], Digit9: [0x46], Digit0: [0x45],
Minus: [0x4E], Equal: [0x55], Backspace: [0x66], Tab: [0x0D],
KeyQ: [0x15], KeyW: [0x1D], KeyE: [0x24], KeyR: [0x2D], KeyT: [0x2C], KeyY: [0x35], KeyU: [0x3C], KeyI: [0x43], KeyO: [0x44], KeyP: [0x4D], BracketLeft: [0x54], BracketRight: [0x5B], Backslash: [0x5D],
CapsLock: [0x58], KeyA: [0x1C], KeyS: [0x1B], KeyD: [0x23], KeyF: [0x2B], KeyG: [0x34], KeyH: [0x33], KeyJ: [0x3B], KeyK: [0x42], KeyL: [0x4B], Semicolon: [0x4C], Quote: [0x52], Enter: [0x5A],
ShiftLeft: [0x12], KeyZ: [0x1A], KeyX: [0x22], KeyC: [0x21], KeyV: [0x2A], KeyB: [0x32], KeyN: [0x31], KeyM: [0x3A], Comma: [0x41], Period: [0x49], Slash: [0x4A], ShiftRight: [0x59],
ControlLeft: [0x14], AltLeft: [0x11], Space: [0x29], ControlRight: [0xE0, 0x14], AltRight: [0xE0, 0x11], MetaLeft: [0xE0, 0x1F], MetaRight: [0xE0, 0x27], ContextMenu: [0xE0, 0x2F],
Insert: [0xE0, 0x70], Delete: [0xE0, 0x71], Home: [0xE0, 0x6C], End: [0xE0, 0x69], PageUp: [0xE0, 0x7D], PageDown: [0xE0, 0x7A],
ArrowUp: [0xE0, 0x75], ArrowDown: [0xE0, 0x72], ArrowLeft: [0xE0, 0x6B], ArrowRight: [0xE0, 0x74],
NumLock: [0x77], ScrollLock: [0x7E], NumpadDivide: [0xE0, 0x4A], NumpadMultiply: [0x7C], NumpadSubtract: [0x7B], NumpadAdd: [0x79], NumpadEnter: [0xE0, 0x5A], NumpadDecimal: [0x71],
Numpad0: [0x70], Numpad1: [0x69], Numpad2: [0x72], Numpad3: [0x7A], Numpad4: [0x6B], Numpad5: [0x73], Numpad6: [0x74], Numpad7: [0x6C], Numpad8: [0x75], Numpad9: [0x7D],
IntlBackslash: [0x61], PrintScreen: [0xE0, 0x12, 0xE0, 0x7C],
};
export { SET2 };
export class Ps2Device {
constructor(soc, clkPad, datPad, kind = 'keyboard') {
this.soc = soc; this.gpio = soc.gpio; this.clk = clkPad; this.dat = datPad; this.kind = kind;
this.txq = []; this.state = 'idle'; this.half = 40000; this.pending = null;
this.gpio.setPullup(clkPad, true); this.gpio.setPullup(datPad, true);
this.lastClk = 1; this.clkFallNs = 0; this.enabled = false; this.scanning = false; this.pendingCmd = 0; this.sendTimer = null;
this.gpio.listeners.push((n, lvl) => { if (n === this.clk) this.onClk(lvl); else if (n === this.dat) this.onDat(lvl); });
this.mouseBuf = [];
}
reset() { this.txq.length = 0; this.state = 'idle'; this.scanning = false; this.pendingCmd = 0; this.release(); }
release() { this.gpio.setExt(this.clk, false); this.gpio.setExt(this.dat, false); }
now() { return this.soc.nowNs; }
// ---- host events ----
onClk(lvl) {
const t = this.now();
if (this.driving) { return; }
if (lvl === 0) this.clkFallNs = t;
else {
// host released CLK: if DATA is low and CLK was held >= 60us -> host wants to send
if (this.gpio.level(this.dat) === 0 && this.state === 'idle') { // host released CLK while holding DATA low: request-to-send
this.startReceive();
} else if (this.state === 'idle') this.scheduleTx(200000);
}
this.lastClk = lvl;
}
onDat(lvl) {}
// ---- device -> host ----
send(bytes, delayNs = 1000000) {
for (const b of bytes) this.txq.push(b);
this.scheduleTx(delayNs);
}
scheduleTx(delayNs) {
if (this.sendTimer) return;
this.sendTimer = this.soc.scheduleIn(delayNs, () => { this.sendTimer = null; this.tryTx(); });
}
tryTx() {
if (this.state !== 'idle' || !this.txq.length) return;
if (this.gpio.level(this.clk) === 0 || this.gpio.level(this.dat) === 0) { this.scheduleTx(300000); return; } // bus busy / inhibited
const b = this.txq[0];
let par = 1; for (let i = 0; i < 8; i++) par ^= (b >> i) & 1;
const bits = [0]; for (let i = 0; i < 8; i++) bits.push((b >> i) & 1); bits.push(par); bits.push(1);
this.state = 'tx'; this.driving = true;
let i = 0;
const H = this.half;
const step = () => {
if (i >= bits.length) { this.gpio.setExt(this.dat, false); this.gpio.setExt(this.clk, false); this.driving = false; this.state = 'idle'; this.txq.shift(); this.lastClk = 1; if (this.txq.length) this.scheduleTx(500000); return; }
// host inhibit check (CLK held low by host while we're not driving)
this.gpio.setExt(this.dat, bits[i] === 0);
this.soc.scheduleIn(8000, () => {
if (this.gpio.level(this.clk) === 0 && !this.gpio.extLow[this.clk >> 5]) { /* unexpected */ }
this.gpio.setExt(this.clk, true);
this.soc.scheduleIn(H, () => { this.gpio.setExt(this.clk, false); this.soc.scheduleIn(H - 8000, () => { i++; step(); }); });
});
};
step();
}
// ---- host -> device ----
startReceive() {
this.state = 'rx'; this.driving = true;
const H = this.half; let bit = 0; const got = [];
const next = () => {
if (bit === 10) { // ACK
this.gpio.setExt(this.dat, true);
this.soc.scheduleIn(8000, () => { this.gpio.setExt(this.clk, true); this.soc.scheduleIn(H, () => { this.gpio.setExt(this.clk, false); this.soc.scheduleIn(H, () => { this.gpio.setExt(this.dat, false); this.finishReceive(got); }); }); });
return;
}
this.gpio.setExt(this.clk, true); // CLK low (device)
this.soc.scheduleIn(H, () => {
this.gpio.setExt(this.clk, false); // CLK high; sample data
this.soc.scheduleIn(H / 2, () => { got.push(this.gpio.level(this.dat)); this.soc.scheduleIn(H / 2, () => { bit++; next(); }); });
});
};
this.soc.scheduleIn(30000, next);
}
finishReceive(bits) {
this.state = 'idle'; this.driving = false;
let b = 0; for (let i = 0; i < 8; i++) b |= (bits[i] << i);
this.lastClk = 1; this.clkFallNs = 0;
this.command(b);
if (this.txq.length) this.scheduleTx(300000);
}
command(b) {
if (this.pendingCmd) {
const c = this.pendingCmd; this.pendingCmd = 0;
if (c === 0xF0 && b === 0) this.txq.push(0xFA, 0x02); else this.txq.push(0xFA);
return;
}
switch (b) {
case 0xFF: this.txq.push(0xFA, 0xAA); if (this.kind === 'mouse') this.txq.push(0x00); this.scanning = false; break;
case 0xF2: this.txq.push(0xFA); if (this.kind === 'keyboard') this.txq.push(0xAB, 0x83); else this.txq.push(0x00); break;
case 0xEE: this.txq.push(0xEE); break;
case 0xED: case 0xF3: case 0xF0: case 0xE8: this.pendingCmd = b; this.txq.push(0xFA); break;
case 0xF4: this.scanning = true; this.txq.push(0xFA); break;
case 0xF5: this.scanning = false; this.txq.push(0xFA); break;
case 0xE9: this.txq.push(0xFA, 0x00, 0x02, 0x64); break;
default: this.txq.push(0xFA);
}
}
// ---- user input ----
key(code, down) {
const sc = SET2[code]; if (!sc) return false;
if (!this.scanning && this.kind === 'keyboard') { /* still queue; real keyboards buffer */ }
let out;
if (code === 'PrintScreen') out = down ? sc : [0xE0, 0xF0, 0x7C, 0xE0, 0xF0, 0x12];
else if (down) out = sc.slice();
else { out = []; if (sc[0] === 0xE0) { out.push(0xE0, 0xF0, sc[1]); } else out.push(0xF0, sc[0]); }
if (code === 'Pause') out = down ? [0xE1, 0x14, 0x77, 0xE1, 0xF0, 0x14, 0xF0, 0x77] : [];
this.send(out, 200000);
return true;
}
mouse(dx, dy, buttons) {
if (this.kind !== 'mouse' || !this.scanning) return;
dx = Math.max(-255, Math.min(255, dx | 0)); dy = Math.max(-255, Math.min(255, dy | 0));
const b0 = 0x08 | (buttons & 7) | (dx < 0 ? 0x10 : 0) | (dy < 0 ? 0x20 : 0);
this.send([b0, dx & 255, dy & 255], 100000);
}
}