v3.0.5: RP2040 브라우저 에뮬레이터 — rp2040js 기반 전체 구현
- RP2040(Pico) 보드 감지·컴파일·에뮬레이션 지원 추가 - Serial → Serial1(UART0) 자동 리다이렉트 (#define 삽입) - .uf2 파일 우선 추출, bootrom 자동 다운로드·캐싱 - ELF .text 추출: arm-none-eabi-objcopy 대신 Node.js 내장 파서 - HTTP 302 리다이렉트 자동 팔로우 (_httpsGetFollow) - rp2040js 에뮬레이터: fast-path 초기화 루프, 피코 SVG 보드, GPIO25 LED - 예외 처리: HardFault(#3)만 중지, 나머지 IRQ/SysTick 정상 통과 - UART onByte: TextDecoder stream 모드로 한글 UTF-8 정상 출력 - heartbeat 메시지 제거 - WiFi scan mock: WSCN 프로토콜로 C++ ↔ 브라우저 동기화, UI 편집 지원 - 에뮬레이터 팝업 → 코드 에디터 인라인 iframe 패널로 전환 - CDN 의존성 → /vendor/ 로컬 서빙 (xterm, Monaco) - SSE 세션 idle timer (창 닫힘 후 5분 자동 종료) - 업로드: 속도 자동 재시도(921600→460800→230400) + FlashMode fallback Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -325,6 +325,7 @@
|
||||
<button onclick="compileAndRun()" id="btn-compile" title="컴파일 & 실행 (Ctrl+Enter)">▶ 컴파일</button>
|
||||
<button onclick="togglePower()" id="btn-stop" title="정지 / 시작" disabled>▶ 시작</button>
|
||||
<button onclick="resetEmulator()" id="btn-reset" title="에뮬레이션 재시작 (리셋)" disabled>↺ 재시작</button>
|
||||
<span id="rp2040-led" title="GPIO25 (내장 LED)" style="display:none;width:12px;height:12px;border-radius:50%;background:#1a2e1a;border:1px solid #2d4a2d;margin:0 2px;flex-shrink:0"></span>
|
||||
<button onclick="toggleBB()" id="btn-bb" title="브레드보드 & SPICE 시뮬레이터">🔌 회로</button>
|
||||
<div class="theme-picker" title="테마">
|
||||
<button class="tdot active" id="td-light" onclick="setTheme('light')" style="--c:#e5e7eb;border:1px solid #94a3b8" title="라이트"></button>
|
||||
@@ -571,6 +572,15 @@
|
||||
<button class="wifi-btn" id="wifi-connect-btn" onclick="_wifiConnectClicked()">연결</button>
|
||||
<button class="wifi-btn" onclick="_wifiDisconnectClicked()">끊기</button>
|
||||
</div>
|
||||
<!-- Scan network mock config -->
|
||||
<div style="margin-top:8px;border-top:1px solid var(--bdr-in);padding-top:8px">
|
||||
<div style="font-size:10px;color:var(--muted);font-weight:600;letter-spacing:.06em;margin-bottom:5px">SCAN 목록 (scanNetworks 모의)</div>
|
||||
<div id="wifi-scan-list" style="display:flex;flex-direction:column;gap:3px;margin-bottom:5px"></div>
|
||||
<div style="display:flex;gap:4px;margin-bottom:4px">
|
||||
<button class="wifi-btn" onclick="_wifiScanAddNet()" style="flex:none;padding:2px 8px;font-size:10px">+ 추가</button>
|
||||
<button class="wifi-btn" onclick="_wifiScanApply()" style="flex:none;padding:2px 8px;font-size:10px;background:var(--accent);border-color:var(--accent);color:#fff">적용</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -617,6 +627,7 @@
|
||||
</div>
|
||||
<span style="margin-left:auto;display:flex;gap:4px;align-items:center">
|
||||
<button class="sch-file-btn" id="sch-gen-btn" onclick="schGenFromCode()" title="스케치 코드를 분석해서 회로도 자동 생성">⚡ 코드→회로</button>
|
||||
<span id="sch-log" style="font-size:11px;color:#e8c860;white-space:nowrap"></span>
|
||||
<button class="sch-file-btn" id="sch-gen-code-btn" onclick="schGenCodeFromCircuit()" title="회로도를 분석해서 Arduino 스케치 자동 생성">🤖 회로→코드</button>
|
||||
<button class="sch-file-btn" onclick="schSaveProject()" title="circuit.json을 프로젝트 폴더에 저장">💾 저장</button>
|
||||
<button class="sch-file-btn" onclick="schLoadLocal()" title="저장된 회로도 불러오기">📂 불러오기</button>
|
||||
@@ -1592,6 +1603,7 @@ function _wifiShow() {
|
||||
const sec = document.getElementById('wifi-section');
|
||||
if (sec) sec.style.display = '';
|
||||
_wifiUpdateUI();
|
||||
_wifiScanRender();
|
||||
}
|
||||
|
||||
function _wifiHandleEvent(msg) {
|
||||
@@ -1648,6 +1660,47 @@ function _wifiDisconnectClicked() {
|
||||
}
|
||||
}
|
||||
|
||||
// WiFi scan mock management
|
||||
let _wifiScanNets = [
|
||||
{ ssid: 'HomeNetwork', rssi: -45, channel: 1 },
|
||||
{ ssid: 'Office_5G', rssi: -55, channel: 6 },
|
||||
{ ssid: 'Neighbor_WiFi', rssi: -65, channel: 11 }
|
||||
];
|
||||
|
||||
function _wifiScanRender() {
|
||||
const list = document.getElementById('wifi-scan-list');
|
||||
if (!list) return;
|
||||
list.innerHTML = '';
|
||||
_wifiScanNets.forEach((net, idx) => {
|
||||
const row = document.createElement('div');
|
||||
row.style.cssText = 'display:flex;align-items:center;gap:3px;font-size:10px';
|
||||
row.innerHTML =
|
||||
`<input value="${net.ssid}" placeholder="SSID" style="flex:2;min-width:0;background:var(--panel);border:1px solid var(--bdr2);color:var(--text);border-radius:3px;padding:2px 4px;font-size:10px;font-family:inherit" oninput="_wifiScanNets[${idx}].ssid=this.value">` +
|
||||
`<input type="number" value="${net.rssi}" min="-100" max="-1" style="width:42px;background:var(--panel);border:1px solid var(--bdr2);color:var(--text);border-radius:3px;padding:2px 4px;font-size:10px;font-family:inherit" oninput="_wifiScanNets[${idx}].rssi=+this.value" title="RSSI (dBm)">` +
|
||||
`<input type="number" value="${net.channel}" min="1" max="14" style="width:30px;background:var(--panel);border:1px solid var(--bdr2);color:var(--text);border-radius:3px;padding:2px 4px;font-size:10px;font-family:inherit" oninput="_wifiScanNets[${idx}].channel=+this.value" title="Ch">` +
|
||||
`<button onclick="_wifiScanRemove(${idx})" style="flex:none;padding:1px 5px;border:1px solid var(--bdr2);background:var(--panel);color:var(--muted);border-radius:3px;cursor:pointer;font-size:10px">✕</button>`;
|
||||
list.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function _wifiScanAddNet() {
|
||||
_wifiScanNets.push({ ssid: 'NewNetwork', rssi: -70, channel: 1 });
|
||||
_wifiScanRender();
|
||||
}
|
||||
|
||||
function _wifiScanRemove(idx) {
|
||||
_wifiScanNets.splice(idx, 1);
|
||||
_wifiScanRender();
|
||||
}
|
||||
|
||||
function _wifiScanApply() {
|
||||
if (!_IS_ESP32 || !_esp32SessionId) return;
|
||||
fetch('/api/arduino/qemu/' + _esp32SessionId + '/scan', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include',
|
||||
body: JSON.stringify({ networks: _wifiScanNets })
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function _showSection(id) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.style.display = '';
|
||||
@@ -2059,6 +2112,12 @@ function _esp32PinState(gpio) {
|
||||
|
||||
// ── ESP8266 state ─────────────────────────────────────────────────────────────
|
||||
let _IS_ESP8266 = false;
|
||||
let _IS_RP2040 = false;
|
||||
let _rp2040Sim = null;
|
||||
let _rp2040Running = false;
|
||||
let _rp2040RafId = null;
|
||||
let _rp2040UartBuf = '';
|
||||
let _avrBoardType = 'board_uno';
|
||||
let _esp8266SessionId = null;
|
||||
let _esp8266EvtSource = null;
|
||||
let _esp8266PinStates = {};
|
||||
@@ -3064,7 +3123,12 @@ window.clearSerial = () => { document.getElementById('serial-output').innerHTML
|
||||
|
||||
window.sendSerial = () => {
|
||||
const inp = document.getElementById('serial-input');
|
||||
if (!inp.value || !uart) return;
|
||||
if (!inp.value) return;
|
||||
if (_IS_RP2040 && _rp2040Sim) {
|
||||
for (const ch of inp.value + '\n') _rp2040Sim.rp2040.uart[0].feedByte(ch.charCodeAt(0));
|
||||
inp.value = ''; return;
|
||||
}
|
||||
if (!uart) return;
|
||||
for (const ch of inp.value + '\n') uart.writeByte(ch.charCodeAt(0));
|
||||
inp.value = '';
|
||||
};
|
||||
@@ -3283,7 +3347,333 @@ window.stopEmulator = () => {
|
||||
_updatePowerSwitch(); // 메시지 자동 출력
|
||||
};
|
||||
|
||||
// ── RP2040 emulation ─────────────────────────────────────────────────────────
|
||||
const _PICO_BOARD_SVG = `<svg id="board-svg" viewBox="0 0 90 200" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- PCB -->
|
||||
<rect x="1" y="1" width="88" height="198" rx="5" fill="#1a5c2a" stroke="#3a9a4a" stroke-width="1.2"/>
|
||||
<!-- USB Micro-B -->
|
||||
<rect x="31" y="0" width="28" height="10" rx="2" fill="#aaa" stroke="#ccc" stroke-width=".5"/>
|
||||
<rect x="34" y="1.5" width="22" height="6" rx="1" fill="#666"/>
|
||||
<text x="45" y="7.5" text-anchor="middle" fill="#eee" font-size="3.5" font-family="monospace">USB</text>
|
||||
<!-- RP2040 chip -->
|
||||
<rect x="22" y="78" width="46" height="44" rx="3" fill="#111" stroke="#555" stroke-width=".8"/>
|
||||
<circle cx="24" cy="80" r="1.5" fill="#333"/>
|
||||
<text x="45" y="97" text-anchor="middle" fill="#ccc" font-size="7" font-family="monospace" font-weight="bold">RP2040</text>
|
||||
<text x="45" y="107" text-anchor="middle" fill="#888" font-size="3.5" font-family="monospace">Raspberry Pi</text>
|
||||
<!-- Flash chip -->
|
||||
<rect x="28" y="118" width="22" height="12" rx="1.5" fill="#222" stroke="#555" stroke-width=".6"/>
|
||||
<text x="39" y="126" text-anchor="middle" fill="#777" font-size="3" font-family="monospace">FLASH</text>
|
||||
<!-- Built-in LED GPIO25 -->
|
||||
<circle id="pico-led" cx="76" cy="25" r="3.5" fill="#0a1f0a" stroke="#2a6a2a" stroke-width=".8"/>
|
||||
<text x="76" y="20" text-anchor="middle" fill="#6aaa6a" font-size="3" font-family="monospace">LED</text>
|
||||
<!-- BOOTSEL button -->
|
||||
<rect x="58" y="148" width="14" height="7" rx="1.5" fill="#2a2a2a" stroke="#666" stroke-width=".5"/>
|
||||
<text x="65" y="153.5" text-anchor="middle" fill="#aaa" font-size="3" font-family="monospace">BOOT</text>
|
||||
<!-- Left pins (20) -->
|
||||
${Array.from({length:20},(_,i)=>{
|
||||
const y=18+i*9;
|
||||
const labels=['VBUS','VSYS','GND','3V3','3V3_EN','ADC_REF','GP28','AGND','GP27','GP26','RUN','GP22','GND','GP21','GP20','GP19','GP18','GND','GP17','GP16'];
|
||||
const isGnd=labels[i].includes('GND')||labels[i]==='AGND';
|
||||
const is3v=labels[i].includes('3V3')||labels[i]==='VBUS'||labels[i]==='VSYS';
|
||||
const col=isGnd?'#888':is3v?'#f80':'#5cf';
|
||||
return `<rect x="1" y="${y}" width="6" height="5" rx="1" fill="#888" stroke="#aaa" stroke-width=".4"/>
|
||||
<text x="9" y="${y+4}" fill="${col}" font-size="3" font-family="monospace">${labels[i]}</text>`;
|
||||
}).join('')}
|
||||
<!-- Right pins (20) -->
|
||||
${Array.from({length:20},(_,i)=>{
|
||||
const y=18+i*9;
|
||||
const labels=['GP0','GP1','GND','GP2','GP3','GP4','GP5','GND','GP6','GP7','GP8','GP9','GND','GP10','GP11','GP12','GP13','GND','GP14','GP15'];
|
||||
const isGnd=labels[i].includes('GND');
|
||||
const col=isGnd?'#888':'#5cf';
|
||||
return `<rect x="83" y="${y}" width="6" height="5" rx="1" fill="#888" stroke="#aaa" stroke-width=".4"/>
|
||||
<text x="81" y="${y+4}" text-anchor="end" fill="${col}" font-size="3" font-family="monospace">${labels[i]}</text>`;
|
||||
}).join('')}
|
||||
</svg>`;
|
||||
|
||||
function _rp2040Start() {
|
||||
if (_rp2040Running || !_rp2040Sim) return;
|
||||
_rp2040Running = true;
|
||||
const NS = 8; // 125 MHz → 8 ns/cycle
|
||||
const rp2040 = _rp2040Sim.rp2040;
|
||||
const clock = _rp2040Sim.clock;
|
||||
const FLASH_BASE = 0x10000000, SRAM_BASE = 0x20000000;
|
||||
// Uint16 뷰: Thumb 명령어(2바이트 정렬) 패턴 감지용
|
||||
const flashU16 = new Uint16Array(rp2040.flash.buffer);
|
||||
// Uint32 뷰: 4바이트 단위 bulk 복사/제로필 용
|
||||
const flashU32 = new Uint32Array(rp2040.flash.buffer);
|
||||
const sramU32 = new Uint32Array(rp2040.sram.buffer);
|
||||
|
||||
// SRAM 코드 fast-path 감지용 Uint16 뷰
|
||||
const sramU16 = new Uint16Array(rp2040.sram.buffer);
|
||||
|
||||
// 범용 초기화 루프 fast-path 함수
|
||||
// 패턴: STMIA Rn!,{r0} / CMP Rn,Rm / BNE or BCC back
|
||||
function _tryFastLoop(pc, memU16, base, isFlash) {
|
||||
const hOff = (pc - base) >>> 1;
|
||||
const op0 = memU16[hOff], op1 = memU16[hOff+1], op2 = memU16[hOff+2];
|
||||
// Fast-path A: LDMIA r1!,{r0}(c901) / STMIA r2!,{r0}(c201) / CMP r2,r3(429a) / BCC(d3fb)
|
||||
if (isFlash && op0 === 0xc901 && op1 === 0xc201 && memU16[hOff+2] === 0x429a && memU16[hOff+3] === 0xd3fb) {
|
||||
const r1 = rp2040.core.registers[1] >>> 0;
|
||||
const r2 = rp2040.core.registers[2] >>> 0;
|
||||
const r3 = rp2040.core.registers[3] >>> 0;
|
||||
if (r2 < r3 && (r1 & 3) === 0 && (r2 & 3) === 0 &&
|
||||
r1 >= FLASH_BASE && r1 < FLASH_BASE + rp2040.flash.length &&
|
||||
r2 >= SRAM_BASE && r2 < SRAM_BASE + rp2040.sram.length) {
|
||||
const cnt = (r3 - r2) >>> 2;
|
||||
sramU32.set(flashU32.subarray((r1-FLASH_BASE)>>>2, ((r1-FLASH_BASE)>>>2)+cnt), (r2-SRAM_BASE)>>>2);
|
||||
rp2040.core.registers[1] = r1 + cnt * 4;
|
||||
rp2040.core.registers[2] = r3;
|
||||
rp2040.core.PC = pc + 8;
|
||||
clock.tick(NS * 2 * cnt * 4);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Fast-path B: STMIA r1!,{r0}(c101) / CMP r1,r2(4291) / BNE(d1fc) — .bss 제로 루프
|
||||
if (op0 === 0xc101 && op1 === 0x4291 && op2 === 0xd1fc) {
|
||||
const r0 = rp2040.core.registers[0] >>> 0;
|
||||
const r1 = rp2040.core.registers[1] >>> 0;
|
||||
const r2 = rp2040.core.registers[2] >>> 0;
|
||||
if (r1 < r2 && (r1 & 3) === 0 && (r2 & 3) === 0 &&
|
||||
r1 >= SRAM_BASE && r2 <= SRAM_BASE + rp2040.sram.length) {
|
||||
const cnt = (r2 - r1) >>> 2;
|
||||
sramU32.fill(r0, (r1-SRAM_BASE)>>>2, ((r1-SRAM_BASE)>>>2)+cnt);
|
||||
rp2040.core.registers[1] = r2;
|
||||
rp2040.core.PC = pc + 6;
|
||||
clock.tick(NS * 2 * cnt * 3);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Stuck detection: same PC for too long usually means a fault/loop
|
||||
let _lastPc = rp2040.core.PC, _stuckCycles = 0, _reportedStuck = false;
|
||||
|
||||
function tick() {
|
||||
if (!_rp2040Running) return;
|
||||
const _booted = !!(_rp2040Sim && _rp2040Sim._booted);
|
||||
// 부팅 중: 10ms 버스트 후 즉시 재스케줄 (RAF 16ms 상한 없이 최대 처리량)
|
||||
// 부팅 후: RAF로 전환 (UI 동기화)
|
||||
const BURST = _booted ? 20 : 10; // ms
|
||||
const deadline = performance.now() + BURST;
|
||||
let checkN = 0;
|
||||
try {
|
||||
while (true) {
|
||||
if (rp2040.core.waiting) {
|
||||
const dt = clock.nanosToNextAlarm;
|
||||
clock.tick(dt > 0 ? dt : NS);
|
||||
} else {
|
||||
const pc = rp2040.core.PC & ~1;
|
||||
if (pc === _lastPc) {
|
||||
if (++_stuckCycles > 50000 && !_reportedStuck) {
|
||||
_reportedStuck = true;
|
||||
appendSerial(`\n[경고: PC ${pc.toString(16)}에서 멈춤 — HardFault/idle loop 의심]\n`, 'error');
|
||||
}
|
||||
} else {
|
||||
_lastPc = pc; _stuckCycles = 0; _reportedStuck = false;
|
||||
}
|
||||
// Flash 범위: fast-path 검사
|
||||
if (pc >= FLASH_BASE && pc < FLASH_BASE + rp2040.flash.length - 8) {
|
||||
if (_tryFastLoop(pc, flashU16, FLASH_BASE, true)) {
|
||||
if (++checkN >= 500 && performance.now() >= deadline) break;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// SRAM 범위: fast-path 검사 (.bss 패턴만)
|
||||
else if (pc >= SRAM_BASE && pc < SRAM_BASE + rp2040.sram.length - 6) {
|
||||
if (_tryFastLoop(pc, sramU16, SRAM_BASE, false)) {
|
||||
if (++checkN >= 500 && performance.now() >= deadline) break;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
rp2040.step();
|
||||
clock.tick(NS * 2);
|
||||
}
|
||||
if (++checkN >= 500 && performance.now() >= deadline) break;
|
||||
}
|
||||
} catch(e) { _rp2040Running = false; appendSerial('\n[실행 오류: ' + e.message + ']\n', 'error'); return; }
|
||||
// Heartbeat: 부팅 중에는 1초마다, 부팅 후에는 3초마다 PC/SP/LR/waiting 출력
|
||||
if (_rp2040Sim && _rp2040Sim._booted) {
|
||||
_rp2040RafId = requestAnimationFrame(tick);
|
||||
} else {
|
||||
// 부팅 중: setTimeout(0)으로 최대한 빠르게 재스케줄
|
||||
_rp2040RafId = setTimeout(tick, 0);
|
||||
}
|
||||
}
|
||||
_rp2040RafId = setTimeout(tick, 0);
|
||||
}
|
||||
|
||||
function _rp2040Stop() {
|
||||
_rp2040Running = false;
|
||||
if (_rp2040RafId) {
|
||||
cancelAnimationFrame(_rp2040RafId);
|
||||
clearTimeout(_rp2040RafId);
|
||||
_rp2040RafId = null;
|
||||
}
|
||||
}
|
||||
|
||||
function _loadUF2(data, rp2040) {
|
||||
const FLASH_START = 0x10000000;
|
||||
for (let i = 0; i + 512 <= data.length; i += 512) {
|
||||
const v = new DataView(data.buffer, data.byteOffset + i, 512);
|
||||
if (v.getUint32(0, true) !== 0x0A324655 || v.getUint32(4, true) !== 0x9E5D5157) continue;
|
||||
const addr = v.getUint32(12, true);
|
||||
const size = v.getUint32(16, true);
|
||||
const off = addr - FLASH_START;
|
||||
if (off < 0 || off + size > rp2040.flash.length) continue;
|
||||
rp2040.flash.set(new Uint8Array(data.buffer, data.byteOffset + i + 32, size), off);
|
||||
}
|
||||
}
|
||||
|
||||
async function initRP2040Mode(uf2B64, fname) {
|
||||
_IS_RP2040 = true;
|
||||
document.getElementById('filename').textContent = fname || 'sketch.ino';
|
||||
document.querySelector('.board-title').textContent = 'RASPBERRY PI PICO · RP2040';
|
||||
document.querySelector('.board-svg').innerHTML = _PICO_BOARD_SVG;
|
||||
const footerEl = document.getElementById('footer-info');
|
||||
if (footerEl) footerEl.textContent = 'rp2040js · RP2040 @ 125 MHz';
|
||||
document.getElementById('btn-compile').style.display = 'none';
|
||||
|
||||
let Simulator, USBCDC;
|
||||
try {
|
||||
({ Simulator, USBCDC } = await import('https://esm.sh/rp2040js@1.3.3'));
|
||||
} catch(e) { showOverlayError('rp2040js 로드 실패\n' + e.message); return; }
|
||||
|
||||
const uf2Bytes = Uint8Array.from(atob(uf2B64), c => c.charCodeAt(0));
|
||||
const sim = new Simulator();
|
||||
_rp2040Sim = sim;
|
||||
|
||||
// 1) RP2040 공식 bootrom 로드 (helper 함수 table을 위해 필수)
|
||||
try {
|
||||
const br = await fetch('/api/arduino/rp2040-bootrom', { credentials: 'include' });
|
||||
if (!br.ok) throw new Error('HTTP ' + br.status);
|
||||
const brData = await br.json();
|
||||
const bootrom = Uint8Array.from(atob(brData.bin), c => c.charCodeAt(0));
|
||||
if (bootrom.length !== 16 * 1024) throw new Error('bootrom size mismatch: ' + bootrom.length);
|
||||
const bootromU32 = new Uint32Array(bootrom.buffer, bootrom.byteOffset, 4096);
|
||||
sim.rp2040.loadBootrom(bootromU32);
|
||||
appendSerial(`[RP2040 bootrom 로드 완료: ${bootrom.length} bytes]\n`, 'sys');
|
||||
} catch(e) {
|
||||
appendSerial('[RP2040 bootrom 로드 실패: ' + e.message + ']\n', 'error');
|
||||
// bootrom 없이도 계속 시도 (helper 호출 시 멈출 수 있음)
|
||||
}
|
||||
|
||||
// 2) UF2 → flash 로드
|
||||
_loadUF2(uf2Bytes, sim.rp2040);
|
||||
|
||||
// 3) 앱 벡터 테이블 찾기
|
||||
// Arduino-Pico UF2: boot2(0x10000000) 뒤에 임시 vector table(0x10000100)이 있고,
|
||||
// 실제 .text 섹션 시작인 0x10003000에 진짜 vector table이 위치함.
|
||||
// 보드/링커 설정에 따라 0x10003000 또는 0x10000100 중 하나를 사용.
|
||||
const fv = new DataView(sim.rp2040.flash.buffer);
|
||||
const readVTOR = (base) => ({ sp: fv.getUint32(base, true), pc: fv.getUint32(base + 4, true), base });
|
||||
const candidates = [readVTOR(0x3000), readVTOR(0x100)].filter(v => v.sp && v.pc && (v.pc & ~1) >= 0x10000000);
|
||||
const appVTOR = candidates[0];
|
||||
if (!appVTOR) {
|
||||
showOverlayError('UF2 로드 실패: 앱 벡터 테이블을 찾을 수 없습니다.\nrp2040:rp2040 코어(arduino-pico)를 사용하세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 3) 부팅을 위해 VTOR를 찾은 앱 vector table로 설정.
|
||||
// bootrom을 덮어쓰지 않으므로 bootrom helper 함수들은 그대로 사용 가능.
|
||||
// reset()이 실행되면 core.PC = flash[VTOR+4], core.SP = flash[VTOR].
|
||||
sim.rp2040.core.VTOR = 0x10000000 + appVTOR.base;
|
||||
|
||||
// 4) CPU 리셋 (VTOR → flash 앱 vector 테이블에서 SP/PC 로드)
|
||||
sim.rp2040.core.reset();
|
||||
|
||||
// rp2040js 로거 경고 억제: 미구현 명령어·주변장치 warn이 콘솔에 범람하는 것을 방지
|
||||
{
|
||||
const logger = sim.rp2040.logger;
|
||||
if (logger && typeof logger.warn === 'function') {
|
||||
logger.warn = () => {}; // 모든 warn 억제 (error/info는 유지)
|
||||
}
|
||||
// core가 별도 logger를 가질 경우도 처리
|
||||
const clogger = sim.rp2040.core?.logger;
|
||||
if (clogger && clogger !== logger && typeof clogger.warn === 'function') {
|
||||
clogger.warn = () => {};
|
||||
}
|
||||
}
|
||||
|
||||
// rp2040js 패치: exception(HardFault 등) 발생 시 실행을 멈추고 메시지를 출력.
|
||||
// (주의: 이전 wide-Thumb 명령어 PC 패치는 BL/BLX 등 정상 32비트 명령어를 망가뜨려 제거함)
|
||||
{
|
||||
const core = sim.rp2040.core;
|
||||
const _origException = core.exceptionEntry.bind(core);
|
||||
core.exceptionEntry = function(exceptionNumber) {
|
||||
if (exceptionNumber === 3) { // HardFault만 중지 (IRQ/SysTick/PendSV는 정상 실행)
|
||||
_rp2040Running = false;
|
||||
_origException(exceptionNumber);
|
||||
appendSerial(`\n[HardFault (LR=${this.LR.toString(16)}, PC=${this.PC.toString(16)}) — 실행 중지]\n`, 'error');
|
||||
} else {
|
||||
_origException(exceptionNumber);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Serial 출력 발생 시 부팅 완료로 전환 (setTimeout→RAF 스케줄러 전환)
|
||||
function _markBooted() {
|
||||
if (_rp2040Sim && !_rp2040Sim._booted) {
|
||||
_rp2040Sim._booted = true;
|
||||
appendSerial('[부팅 완료]\n', 'sys');
|
||||
}
|
||||
}
|
||||
|
||||
// UART0 → Serial1 (UTF-8 스트리밍 디코딩)
|
||||
const _rp2040UartDec = new TextDecoder('utf-8', { fatal: false });
|
||||
sim.rp2040.uart[0].onByte = (value) => {
|
||||
_markBooted();
|
||||
const decoded = _rp2040UartDec.decode(new Uint8Array([value]), { stream: true });
|
||||
if (decoded) appendSerial(decoded);
|
||||
};
|
||||
|
||||
// USB CDC → Serial (default Arduino-Pico Serial)
|
||||
try {
|
||||
const cdc = new USBCDC(sim.rp2040.usbCtrl);
|
||||
const dec = new TextDecoder();
|
||||
cdc.onSerialData = (buf) => { _markBooted(); appendSerial(dec.decode(buf)); };
|
||||
cdc.onDeviceConnected = () => appendSerial('[USB CDC 연결됨]\n', 'sys');
|
||||
} catch(e) {
|
||||
appendSerial('[USB CDC 초기화 실패: ' + e.message + ']\n', 'error');
|
||||
}
|
||||
|
||||
// GPIO25 = 내장 LED
|
||||
sim.rp2040.gpio[25].addListener((state) => {
|
||||
const on = state === 1;
|
||||
const toolbar = document.getElementById('rp2040-led');
|
||||
if (toolbar) toolbar.style.background = on ? '#4ade80' : '#1a2e1a';
|
||||
const svg = document.getElementById('pico-led');
|
||||
if (svg) svg.setAttribute('fill', on ? '#22c55e' : '#0a1f0a');
|
||||
});
|
||||
|
||||
document.getElementById('overlay').style.display = 'none';
|
||||
const ledEl2 = document.getElementById('rp2040-led');
|
||||
if (ledEl2) ledEl2.style.display = 'inline-block';
|
||||
document.getElementById('btn-stop').disabled = false;
|
||||
setStatus('running', '실행 중');
|
||||
const resetBtn = document.getElementById('btn-reset');
|
||||
if (resetBtn) { resetBtn.disabled = false; }
|
||||
appendSerial(`[RP2040 에뮬레이터 시작]\n[Serial → USB CDC, Serial1 → UART0]\n[VTOR=${sim.rp2040.core.VTOR.toString(16)} SP=${sim.rp2040.core.SP.toString(16)} PC=${sim.rp2040.core.PC.toString(16)}]\n`, 'sys');
|
||||
_rp2040Start();
|
||||
}
|
||||
|
||||
window.resetEmulator = () => {
|
||||
if (_IS_RP2040) {
|
||||
if (!_rp2040Sim) return;
|
||||
_rp2040Stop();
|
||||
_rp2040UartBuf = '';
|
||||
appendSerial('\n--- 리셋 ---\n', 'sys');
|
||||
// 주의: rp2040.reset()은 flash.fill(0xff)를 실행하므로 앱 코드가 날아감.
|
||||
// VTOR를 다시 앱 vector table로 재설정한 뒤 core.reset()만 수행.
|
||||
// (flash offset 0x3000 우선, 없으면 0x100)
|
||||
const fv2 = new DataView(_rp2040Sim.rp2040.flash.buffer);
|
||||
const readVTOR2 = (base) => ({ sp: fv2.getUint32(base, true), pc: fv2.getUint32(base + 4, true), base });
|
||||
const cand2 = [readVTOR2(0x3000), readVTOR2(0x100)].filter(v => v.sp && v.pc && (v.pc & ~1) >= 0x10000000);
|
||||
_rp2040Sim.rp2040.core.VTOR = cand2.length ? (0x10000000 + cand2[0].base) : 0x10003000;
|
||||
_rp2040Sim.rp2040.core.reset();
|
||||
_rp2040Start();
|
||||
return;
|
||||
}
|
||||
if (!hexData) return;
|
||||
// 먼저 실행 정지
|
||||
running = false;
|
||||
@@ -3357,6 +3747,11 @@ window.setTheme = (name) => {
|
||||
};
|
||||
|
||||
window.togglePower = () => {
|
||||
if (_IS_RP2040 && _rp2040Sim) {
|
||||
if (_rp2040Running) { _rp2040Stop(); setStatus('stopped', '정지'); }
|
||||
else { _rp2040Start(); setStatus('running', '실행 중'); }
|
||||
return;
|
||||
}
|
||||
if (running) { stopEmulator(); return; }
|
||||
if (!emulatorReady) return;
|
||||
startEmulator();
|
||||
@@ -3375,6 +3770,8 @@ async function boot() {
|
||||
leonardo: 'ARDUINO LEONARDO · ATmega32U4', micro: 'ARDUINO MICRO · ATmega32U4',
|
||||
esp32: 'ESP32 · Xtensa LX6', esp32s3: 'ESP32-S3 · Xtensa LX7',
|
||||
esp8266: 'ESP8266 · L106',
|
||||
rpipico: 'Raspberry Pi Pico · RP2040', rpipicow: 'Raspberry Pi Pico W · RP2040',
|
||||
pico: 'Arduino Nano RP2040 Connect',
|
||||
};
|
||||
if (_fqbnBoard && _BOARD_NAMES[_fqbnBoard]) {
|
||||
const boardTitleEl = document.querySelector('.board-title');
|
||||
@@ -3408,6 +3805,19 @@ async function boot() {
|
||||
return;
|
||||
}
|
||||
|
||||
// ── RP2040 browser mode ──────────────────────────────────────────────────────
|
||||
const uf2B64 = sessionStorage.getItem('arduino_uf2');
|
||||
if (uf2B64) {
|
||||
sessionStorage.removeItem('arduino_uf2');
|
||||
const rp2040Sketch = sessionStorage.getItem('arduino_sketch');
|
||||
sessionStorage.removeItem('arduino_sketch');
|
||||
if (rp2040Sketch) _esp32SketchCode = rp2040Sketch;
|
||||
const rp2040Fname = sessionStorage.getItem('arduino_filename') || 'sketch.ino';
|
||||
sessionStorage.removeItem('arduino_filename');
|
||||
initRP2040Mode(uf2B64, rp2040Fname);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Arduino AVR mode (기존) ──────────────────────────────────────────────────
|
||||
const sketch = sessionStorage.getItem('arduino_sketch');
|
||||
const board = sessionStorage.getItem('arduino_board') || 'uno';
|
||||
@@ -3419,16 +3829,17 @@ async function boot() {
|
||||
sessionStorage.removeItem('arduino_hex');
|
||||
|
||||
// AVR 모드 보드 타이틀 (URL fqbn이 없는 경우 sessionStorage board로 대체)
|
||||
const _avrBoardKey = (_fqbnBoard || (board || 'uno').split(':').pop()).toLowerCase();
|
||||
_avrBoardType = _avrBoardKey.includes('mega') ? 'board_mega' : _avrBoardKey.includes('nano') ? 'board_nano' : 'board_uno';
|
||||
if (!_fqbnBoard) {
|
||||
const _avrBoard = (board || 'uno').toLowerCase();
|
||||
const _avrName = _BOARD_NAMES[_avrBoard] || _BOARD_NAMES.uno;
|
||||
const _avrName = _BOARD_NAMES[_avrBoardKey] || _BOARD_NAMES.uno;
|
||||
const boardTitleEl = document.querySelector('.board-title');
|
||||
if (boardTitleEl && _avrBoard !== 'uno') boardTitleEl.textContent = _avrName;
|
||||
if (boardTitleEl && _avrBoardKey !== 'uno') boardTitleEl.textContent = _avrName;
|
||||
}
|
||||
|
||||
document.getElementById('filename').textContent = fname;
|
||||
|
||||
if (hexDirect) { initEmulator(hexDirect); return; }
|
||||
if (hexDirect) { if (sketch) _esp32SketchCode = sketch; initEmulator(hexDirect); return; }
|
||||
|
||||
if (!sketch) {
|
||||
showOverlayError('스케치가 없습니다.\n코드 에디터에서 .ino 파일을 열고 실행하세요.');
|
||||
@@ -5659,8 +6070,7 @@ ${code.slice(0, 3000)}
|
||||
|
||||
// Auto-layout and apply generated component list to schematic
|
||||
function _schApplyGenerated(boardType, components) {
|
||||
if (!components.length) return;
|
||||
if (SCH.components.length && !confirm(`회로도에 ${components.length}개 부품을 배치합니다. 기존 회로도를 지울까요?`)) return;
|
||||
if (SCH.components.length && !confirm(components.length ? `회로도에 ${components.length}개 부품을 배치합니다. 기존 회로도를 지울까요?` : '회로도를 초기화하고 보드만 표시합니다. 계속할까요?')) return;
|
||||
SCH.components = [];
|
||||
SCH.wires = [];
|
||||
|
||||
@@ -5894,7 +6304,7 @@ function _schApplyGenerated(boardType, components) {
|
||||
window.schGenFromCode = async function schGenFromCode() {
|
||||
const code = _getEditorCode() || (_IS_ESP32 ? _esp32SketchCode : _esp8266SketchCode);
|
||||
if (!code || !code.trim()) { alert('스케치 코드가 없습니다. 먼저 코드를 에디터에 붙여넣거나 컴파일하세요.'); return; }
|
||||
const boardType = _IS_ESP32 ? 'board_esp32' : 'board_esp8266';
|
||||
const boardType = _IS_ESP32 ? 'board_esp32' : _IS_ESP8266 ? 'board_esp8266' : _avrBoardType;
|
||||
const btn = document.getElementById('sch-gen-btn');
|
||||
if (btn) { btn.textContent = '⏳...'; btn.disabled = true; }
|
||||
try {
|
||||
@@ -5908,8 +6318,11 @@ window.schGenFromCode = async function schGenFromCode() {
|
||||
else merged.push(aiComp);
|
||||
}
|
||||
const valid = merged.filter(c => SCH_TERMS[c.type]);
|
||||
if (!valid.length) { alert('인식된 부품이 없습니다. 코드에 라이브러리 include나 pinMode 호출이 있는지 확인하세요.'); return; }
|
||||
_schApplyGenerated(boardType, valid);
|
||||
if (!valid.length) {
|
||||
const log = document.getElementById('sch-log');
|
||||
if (log) { log.textContent = '인식된 부품 없음 — 보드만 표시됩니다.'; log.style.color='#e8c860'; }
|
||||
}
|
||||
} finally {
|
||||
if (btn) { btn.textContent = '⚡ 코드→회로'; btn.disabled = false; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user