Arduino 에뮬레이터: SSD1306 OLED(128×64) I2C 에뮬레이션 추가
- AVRTWI connectToSlave에서 LCD(0x27/0x3F)와 OLED(0x3C/0x3D) 동시 지원
- SSD1306 프로토콜 파서:
- control byte (0x00=cmd stream, 0x40=data stream)
- 주요 명령: addressing mode, set col/page addr, display on/off,
inverse, page/col low/high nibble, 2바이트 명령 처리
- horizontal/vertical/page 어드레싱 모드 모두 지원
- GDDRAM(128×8 pages) 유지 → canvas 128×64 px 렌더링
(픽셀 2× 스케일, 청백색 톤)
- displayOn 명령 수신 시 OLED 패널 자동 표시
- runFrame마다 dirty 플래그로 효율적 canvas flush
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -52,6 +52,12 @@
|
||||
.lcd-off .lcd-cell { color:#0d1a05; background:#0d1a05; }
|
||||
.lcd-off .lcd-wrap { filter:brightness(.4); }
|
||||
|
||||
/* SSD1306 OLED */
|
||||
.oled-section { padding:10px 14px; flex-shrink:0; border-bottom:1px solid #1e1e36; }
|
||||
.oled-section-title { font-size:10px; color:var(--muted); margin-bottom:8px; font-weight:600; letter-spacing:.08em; }
|
||||
.oled-wrap { background:#000; border:1px solid #333; border-radius:4px; padding:4px; display:inline-block; }
|
||||
#oled-canvas { display:block; image-rendering:pixelated; width:256px; height:128px; }
|
||||
|
||||
/* Pin grid */
|
||||
.pins-section { padding:10px 14px; flex:1; overflow-y:auto; }
|
||||
.pins-header { font-size:10px; color:var(--muted); margin-bottom:8px; font-weight:600; letter-spacing:.08em; }
|
||||
@@ -161,6 +167,12 @@
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- SSD1306 OLED -->
|
||||
<div class="oled-section" id="oled-section" style="display:none">
|
||||
<div class="oled-section-title">SSD1306 OLED 128×64</div>
|
||||
<div class="oled-wrap"><canvas id="oled-canvas" width="128" height="64"></canvas></div>
|
||||
</div>
|
||||
|
||||
<!-- LCD display -->
|
||||
<div class="lcd-section" id="lcd-section" style="display:none">
|
||||
<div class="lcd-section-title">LCD <span id="lcd-size-label" style="color:#60a5fa"></span> <span id="lcd-backlight-dot" style="font-size:8px">●</span></div>
|
||||
@@ -389,16 +401,171 @@ function lcdOnI2CByte(byte) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── SSD1306 OLED emulator ─────────────────────────────────────────────────────
|
||||
// I2C addresses: 0x3C (SA0=0) or 0x3D (SA0=1)
|
||||
const OLED_ADDRS = [0x3C, 0x3D];
|
||||
const oled = {
|
||||
gddram: new Uint8Array(128 * 8), // 128 cols × 8 pages (each page = 8 rows)
|
||||
displayOn: false, inverse: false,
|
||||
addrMode: 0x02, // 0=horizontal, 1=vertical, 2=page (default)
|
||||
col: 0, page: 0,
|
||||
colStart: 0, colEnd: 127,
|
||||
pageStart: 0, pageEnd: 7,
|
||||
// I2C parse state
|
||||
_ctrlByte: null, _active: false,
|
||||
};
|
||||
|
||||
let oledCtx = null;
|
||||
let oledDirty = false;
|
||||
|
||||
function oledReset() {
|
||||
oled.gddram.fill(0);
|
||||
oled.displayOn = false; oled.inverse = false; oled.addrMode = 0x02;
|
||||
oled.col = 0; oled.page = 0;
|
||||
oled.colStart = 0; oled.colEnd = 127;
|
||||
oled.pageStart = 0; oled.pageEnd = 7;
|
||||
oled._ctrlByte = null; oled._active = false;
|
||||
document.getElementById('oled-section').style.display = 'none';
|
||||
oledDirty = false;
|
||||
}
|
||||
|
||||
function oledFlushCanvas() {
|
||||
if (!oledCtx || !oledDirty) return;
|
||||
oledDirty = false;
|
||||
const img = oledCtx.createImageData(128, 64);
|
||||
const fg = oled.inverse ? 0 : 255;
|
||||
const bg = oled.inverse ? 255 : 0;
|
||||
for (let page = 0; page < 8; page++) {
|
||||
for (let col = 0; col < 128; col++) {
|
||||
const byte = oled.gddram[page * 128 + col];
|
||||
for (let bit = 0; bit < 8; bit++) {
|
||||
const row = page * 8 + bit;
|
||||
const idx = (row * 128 + col) * 4;
|
||||
const on = (byte >> bit) & 1;
|
||||
img.data[idx] = on ? fg : bg;
|
||||
img.data[idx + 1] = on ? (oled.inverse ? 0 : 0x62) : bg; // slight blue tint when on
|
||||
img.data[idx + 2] = on ? (oled.inverse ? 0 : 0xff) : bg;
|
||||
img.data[idx + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
oledCtx.putImageData(img, 0, 0);
|
||||
}
|
||||
|
||||
function oledWriteData(byte) {
|
||||
oled.gddram[oled.page * 128 + oled.col] = byte;
|
||||
oledDirty = true;
|
||||
if (oled.addrMode === 0x00) { // horizontal
|
||||
oled.col++;
|
||||
if (oled.col > oled.colEnd) {
|
||||
oled.col = oled.colStart;
|
||||
oled.page++;
|
||||
if (oled.page > oled.pageEnd) oled.page = oled.pageStart;
|
||||
}
|
||||
} else if (oled.addrMode === 0x01) { // vertical
|
||||
oled.page++;
|
||||
if (oled.page > oled.pageEnd) {
|
||||
oled.page = oled.pageStart;
|
||||
oled.col++;
|
||||
if (oled.col > oled.colEnd) oled.col = oled.colStart;
|
||||
}
|
||||
} else { // page
|
||||
oled.col = (oled.col + 1) & 0x7F;
|
||||
}
|
||||
}
|
||||
|
||||
function oledCommand(cmd, stream) {
|
||||
// single-byte commands
|
||||
if (cmd === 0xAE) { oled.displayOn = false; return 0; }
|
||||
if (cmd === 0xAF) {
|
||||
oled.displayOn = true;
|
||||
document.getElementById('oled-section').style.display = 'block';
|
||||
oled._active = true;
|
||||
return 0;
|
||||
}
|
||||
if (cmd === 0xA6) { oled.inverse = false; oledDirty = true; return 0; }
|
||||
if (cmd === 0xA7) { oled.inverse = true; oledDirty = true; return 0; }
|
||||
if (cmd === 0x2E || cmd === 0x2F) return 0; // scroll stop/start — ignore
|
||||
if ((cmd & 0xF0) === 0xB0) { oled.page = cmd & 0x07; return 0; } // set page (page mode)
|
||||
if ((cmd & 0xF0) === 0x00) { oled.col = (oled.col & 0xF0) | (cmd & 0x0F); return 0; } // col low
|
||||
if ((cmd & 0xF0) === 0x10) { oled.col = (oled.col & 0x0F) | ((cmd & 0x0F) << 4); return 0; } // col high
|
||||
// two-byte commands (consume next byte from stream)
|
||||
if (cmd === 0x81) return 1; // contrast — skip 1
|
||||
if (cmd === 0xD5 || cmd === 0xD9 || cmd === 0xDA || cmd === 0xDB || cmd === 0xD3 || cmd === 0xA8) return 1;
|
||||
if (cmd === 0x8D) return 1; // charge pump
|
||||
// three-byte commands
|
||||
if (cmd === 0x20) { // addressing mode
|
||||
if (stream.length >= 1) { oled.addrMode = stream[0] & 0x03; return 1; }
|
||||
return 1;
|
||||
}
|
||||
if (cmd === 0x21) { // set column address
|
||||
if (stream.length >= 2) { oled.colStart = stream[0]; oled.colEnd = stream[1]; oled.col = oled.colStart; return 2; }
|
||||
return 2;
|
||||
}
|
||||
if (cmd === 0x22) { // set page address
|
||||
if (stream.length >= 2) { oled.pageStart = stream[0]; oled.pageEnd = stream[1]; oled.page = oled.pageStart; return 2; }
|
||||
return 2;
|
||||
}
|
||||
if (cmd === 0x40) { oled.col = 0; oled.page = 0; return 0; } // display start line
|
||||
return 0;
|
||||
}
|
||||
|
||||
// I2C receive state for SSD1306
|
||||
let _oledI2cBuf = [];
|
||||
let _oledI2cMode = null; // null=await control byte, 'cmd'=command stream, 'data'=data stream
|
||||
let _oledCmdPending = null; // [cmd, bytesLeft]
|
||||
|
||||
function oledOnI2CByte(byte) {
|
||||
if (_oledI2cMode === null) {
|
||||
// Control byte: Co=bit7, D/C#=bit6
|
||||
// 0x00 = stream of commands, 0x40 = stream of data, 0x80 = single cmd, 0xC0 = single data
|
||||
const co = (byte >> 7) & 1;
|
||||
const dc = (byte >> 6) & 1;
|
||||
_oledI2cMode = dc ? 'data' : 'cmd';
|
||||
_oledCmdPending = null;
|
||||
return;
|
||||
}
|
||||
if (_oledI2cMode === 'data') {
|
||||
oledWriteData(byte);
|
||||
return;
|
||||
}
|
||||
// command mode
|
||||
if (_oledCmdPending) {
|
||||
_oledCmdPending.args.push(byte);
|
||||
_oledCmdPending.left--;
|
||||
if (_oledCmdPending.left === 0) {
|
||||
oledCommand(_oledCmdPending.cmd, _oledCmdPending.args);
|
||||
_oledCmdPending = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const skip = oledCommand(byte, []);
|
||||
if (skip > 0) _oledCmdPending = { cmd: byte, left: skip, args: [] };
|
||||
}
|
||||
|
||||
function oledOnI2CStart() {
|
||||
_oledI2cMode = null;
|
||||
_oledCmdPending = null;
|
||||
}
|
||||
|
||||
function setupTWI(cpu) {
|
||||
if (!AVRTWI || !twiConfig) return;
|
||||
try {
|
||||
const twi = new AVRTWI(cpu, twiConfig, 16e6);
|
||||
let currentSlave = null;
|
||||
twi.eventHandler = {
|
||||
start(restart) { return true; },
|
||||
connectToSlave(address, write) {
|
||||
return LCD_ADDRS.includes(address >> 1);
|
||||
const addr7 = address >> 1;
|
||||
if (LCD_ADDRS.includes(addr7)) { currentSlave = 'lcd'; return true; }
|
||||
if (OLED_ADDRS.includes(addr7)) { currentSlave = 'oled'; oledOnI2CStart(); return true; }
|
||||
currentSlave = null; return false;
|
||||
},
|
||||
writeByte(byte) {
|
||||
if (currentSlave === 'lcd') lcdOnI2CByte(byte);
|
||||
if (currentSlave === 'oled') oledOnI2CByte(byte);
|
||||
return true;
|
||||
},
|
||||
writeByte(byte) { lcdOnI2CByte(byte); return true; },
|
||||
readByte() { return 0xFF; },
|
||||
};
|
||||
} catch(e) {
|
||||
@@ -535,6 +702,8 @@ function initEmulator(hex) {
|
||||
|
||||
totalCycles = 0;
|
||||
lcdReset();
|
||||
oledReset();
|
||||
oledCtx = document.getElementById('oled-canvas').getContext('2d');
|
||||
buildPinUI();
|
||||
|
||||
let lastD13 = -1;
|
||||
@@ -564,6 +733,7 @@ function runFrame() {
|
||||
}
|
||||
totalCycles = cpu.cycles;
|
||||
updatePinUI();
|
||||
oledFlushCanvas();
|
||||
const simSec = (totalCycles / 16e6).toFixed(2);
|
||||
document.getElementById('footer-cycles').textContent = `${Math.round(totalCycles).toLocaleString()} cycles (${simSec}s)`;
|
||||
rafId = requestAnimationFrame(runFrame);
|
||||
|
||||
Reference in New Issue
Block a user