feat: STM32 Renode 에뮬레이터 작동 — HAL 폴링 루프 + 비트밴드 + 타이밍 수정

- stm32f103_fixed.repl: Miscellaneous.BitBanding 추가로 HAL bit-band
  RCC 접근(0x42xxxxxx) 정상 처리, SRAM 크기 20KB로 수정,
  systickFrequency 8MHz로 낮춰 실시간 타이밍 보정
- stm32f1_rcc.py(~/.local): UTF-8 인코딩 선언, PLLRDY가 PLLON을
  동적으로 추적해 SystemClock_Config() HAL 폴링 루프 해소
- routes-arduino.ts: SystemClock_Config() 스케치에서 제거(bit-band
  우회), 보드 선택 6종 whitelist, Companion WS 재연결 스팸 수정
- arduino-emulator.html + code.js: Renode 세션 라우팅 + 보라 배지

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kim
2026-06-29 21:10:50 +09:00
co-authored by Claude Sonnet 4.6
parent 75a36cdc7d
commit c13fa4fae5
4 changed files with 654 additions and 23 deletions
+75
View File
@@ -3019,6 +3019,77 @@ function initEsp32Mode(sessionId, fname, fqbn) {
_adcShowSection();
}
// ── STM32 Renode mode ─────────────────────────────────────────────────────────
let _renodeSessionId = null;
let _renodeEvtSource = null;
function initSTM32RenodeMode(sessionId, fname, fqbn) {
_renodeSessionId = sessionId;
document.querySelector('.header h1').textContent = '🖥️ STM32 Emulator (Renode)';
document.getElementById('filename').textContent = fname;
document.getElementById('chip-badge')?.remove();
const badge = document.createElement('span');
badge.id = 'chip-badge';
badge.style.cssText = 'display:inline-block;padding:2px 7px;border-radius:4px;font-size:10px;font-weight:600;border:1px solid;flex-shrink:0;';
const family = fqbn.includes('GenF4') || fqbn.includes('f4') ? 'STM32F4' : 'STM32F1';
badge.textContent = family + ' · Renode';
badge.style.background = '#1a0a3a'; badge.style.color = '#c084fc'; badge.style.borderColor = '#7c3aed';
document.getElementById('filename').after(badge);
document.querySelector('.board-svg')?.style && (document.querySelector('.board-svg').style.display = 'none');
document.querySelector('.board-title')?.style && (document.querySelector('.board-title').style.display = 'none');
document.getElementById('btn-stop')?.style && (document.getElementById('btn-stop').style.display = 'none');
document.getElementById('btn-reset')?.style && (document.getElementById('btn-reset').style.display = 'none');
document.getElementById('renode-stop-btn')?.remove();
const toolbar = document.querySelector('.toolbar-right') || document.querySelector('.toolbar');
if (toolbar) {
const stopBtn = document.createElement('button');
stopBtn.id = 'renode-stop-btn'; stopBtn.textContent = '■ 정지';
stopBtn.style.cssText = 'font-size:12px;padding:3px 8px;background:#3a1010;border:1px solid #cc4444;color:#ff8888;border-radius:4px;cursor:pointer';
stopBtn.onclick = () => {
fetch('/api/arduino/renode/' + _renodeSessionId, { method: 'DELETE', credentials: 'include', keepalive: true }).catch(() => {});
if (_renodeEvtSource) { _renodeEvtSource.close(); _renodeEvtSource = null; }
setStatus('off', '정지됨');
};
toolbar.append(stopBtn);
}
const serialInput = document.getElementById('serial-input');
const serialSend = document.querySelector('.serial-send');
if (serialInput) serialInput.disabled = false;
if (serialSend) serialSend.removeAttribute('disabled');
const doSend = () => {
const text = serialInput?.value || '';
if (!text) return;
fetch('/api/arduino/renode/' + _renodeSessionId + '/input', {
method: 'POST', credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: text + '\n' }),
}).catch(() => {});
if (serialInput) serialInput.value = '';
};
serialInput?.addEventListener('keydown', e => { if (e.key === 'Enter') doSend(); });
serialSend?.addEventListener('click', doSend);
setStatus('loading', 'Renode 기동 중...');
_renodeEvtSource = new EventSource('/api/arduino/renode/' + sessionId + '/output', { withCredentials: true });
_renodeEvtSource.onopen = () => setStatus('running', '실행 중');
_renodeEvtSource.onerror = () => setStatus('error', '연결 오류');
_renodeEvtSource.onmessage = e => {
try {
const d = JSON.parse(e.data);
if (d.serial) appendSerial(d.serial);
if (d.exit !== undefined) setStatus('off', '종료');
} catch {}
};
window.addEventListener('beforeunload', () => {
fetch('/api/arduino/renode/' + sessionId, { method: 'DELETE', credentials: 'include', keepalive: true }).catch(() => {});
});
}
function _buildEsp32PinPopup() {
const body = document.getElementById('pin-popup-body');
if (!body) return;
@@ -4231,6 +4302,10 @@ async function boot() {
const savedSketch = localStorage.getItem('esp8266_sketch');
if (savedSketch) { _esp8266SketchCode = savedSketch; localStorage.removeItem('esp8266_sketch'); }
initEsp8266Mode(qemuSession, fname, fqbn);
} else if (qemuSession.startsWith('renode_')) {
const savedSketch = localStorage.getItem('stm32_sketch');
if (savedSketch) { localStorage.removeItem('stm32_sketch'); }
initSTM32RenodeMode(qemuSession, fname, fqbn);
} else {
// Restore sketch code from code editor (localStorage: shared across windows)
const savedSketch = localStorage.getItem('esp32_sketch');
+37 -6
View File
@@ -610,6 +610,7 @@ function _termOnRunSuccess(shellId) {
// ── Companion WebSocket (local Python execution) ───────────────────────────
let companionWs = null;
let companionConnected = false;
let companionEverConnected = false; // 한 번이라도 성공한 적 있는지
const COMPANION_URL = 'ws://127.0.0.1:9000';
let companionPlatform = 'unix'; // 'windows' | 'unix', set on shell_ready
let companionHome = ''; // home directory from companion
@@ -618,11 +619,12 @@ const localShellIds = new Set(); // shell IDs routed to companion PTY (tab id =
function companionConnect() {
if (companionWs && (companionWs.readyState === WebSocket.OPEN || companionWs.readyState === WebSocket.CONNECTING)) return;
try { companionWs = new WebSocket(COMPANION_URL); } catch { return; }
companionWs.onopen = () => { companionConnected = true; updateCompanionStatus(true); };
companionWs.onopen = () => { companionConnected = true; companionEverConnected = true; updateCompanionStatus(true); };
companionWs.onclose = () => {
companionConnected = false; companionWs = null;
updateCompanionStatus(false);
setTimeout(companionConnect, 5000);
// 한 번이라도 연결됐던 경우에만 자동 재연결 (미연결 상태서 콘솔 에러 spam 방지)
if (companionEverConnected) setTimeout(companionConnect, 5000);
};
companionWs.onerror = () => { companionConnected = false; };
companionWs.onmessage = (ev) => {
@@ -1009,6 +1011,29 @@ function openArduinoEmulator() {
toggleEmuPanel();
});
} else if (fqbn.startsWith('STMicroelectronics:stm32')) {
if (btn) { btn.textContent = '⏳ 컴파일 중…'; btn.disabled = true; }
_emuPanelLoad('/arduino-emulator.html?loading=1', `STM32 — ${file.name}`);
codeAiAppend('system', `🖥️ STM32 Renode 에뮬레이터 컴파일 중... [${_csbSelBoard.name}]`);
try { localStorage.setItem('stm32_sketch', content); } catch {}
api('/api/arduino/renode/start', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ code: content, fqbn }) })
.then(d => {
if (btn) { btn.textContent = '🖥️ 에뮬레이터'; btn.disabled = false; }
if (d.error) {
sendCompileErrorToAI(d.error, file.name, content);
toggleEmuPanel();
return;
}
const _proj = activeProjectName ? '&project=' + encodeURIComponent(activeProjectName) : '';
const url = `/arduino-emulator.html?session=${d.id}&file=${encodeURIComponent(file.name)}&fqbn=${encodeURIComponent(fqbn)}${_proj}`;
_emuPanelLoad(url, `STM32 — ${file.name}`);
codeAiAppend('system', `✅ STM32 Renode 에뮬레이터 실행 중.`);
}).catch(e => {
if (btn) { btn.textContent = '🖥️ 에뮬레이터'; btn.disabled = false; }
codeAiAppend('system', '⚠️ ' + e.message);
toggleEmuPanel();
});
} else {
// AVR 및 기타
if (btn) { btn.textContent = '⏳ 컴파일 중…'; btn.disabled = true; }
@@ -1264,7 +1289,7 @@ const CODE_RUN_MODES = {
{ id: 'terminal', icon: '⌨', label: 'bash', desc: '터미널에서 실행' },
],
ino: [
{ id: 'arduino', icon: '🔌', label: 'Arduino 에뮬레이터', desc: 'AVR 브라우저 에뮬레이터 (avr8js)' },
{ id: 'arduino', icon: '🔌', label: '에뮬레이터', desc: 'AVR/STM32/ESP 에뮬레이터 (보드에 따라 자동 선택)' },
{ id: 'esp32', icon: '⚡', label: 'ESP32 플래시', desc: 'Web Serial로 ESP32에 업로드' },
{ id: 'terminal', icon: '⌨', label: 'arduino-cli', desc: '터미널에서 빌드/업로드' },
],
@@ -1485,8 +1510,9 @@ function codeRunWith(mode) {
if (mode === 'arduino') {
const _isEsp32Board = (_csbSelBoard.fqbn.split(':')[1] || '') === 'esp32';
const _isEsp8266Board = (_csbSelBoard.fqbn.split(':')[0] || '') === 'esp8266';
if (_isEsp32Board || _isEsp8266Board) {
// ESP32/ESP8266 계열 — 팝업 차단 방지를 위해 openArduinoEmulator()와 같은 경로 사용
const _isStm32Board = _csbSelBoard.fqbn.startsWith('STMicroelectronics:stm32');
if (_isEsp32Board || _isEsp8266Board || _isStm32Board) {
// ESP32/ESP8266/STM32 계열 — openArduinoEmulator()로 에뮬레이터 실행
openArduinoEmulator(); return;
}
codeAiAppend('system', `🔌 컴파일 중... (${file.name}) [${_csbSelBoard.name}]`);
@@ -4753,6 +4779,9 @@ function _csbBoardSelSave(board) {
if (board.qemu === 'esp8266-native') {
badge.textContent = '🖥 Native'; badge.style.display = 'inline';
badge.style.background = '#0c3a4a'; badge.style.color = '#67e8f9'; badge.style.borderColor = '#0e7490';
} else if (board.qemu === 'renode') {
badge.textContent = '🖥 Renode'; badge.style.display = 'inline';
badge.style.background = '#1a0a3a'; badge.style.color = '#c084fc'; badge.style.borderColor = '#7c3aed';
} else if (board.qemu) {
badge.textContent = '🖥 QEMU'; badge.style.display = 'inline';
badge.style.background = board.qemu === 'esp32s3' ? '#1a3a5a' : '#1a3a1a';
@@ -4794,7 +4823,9 @@ async function csbBoardSelSearch() {
const sel = b.fqbn === _csbSelBoard.fqbn;
const qemuBadge = b.qemu === 'esp8266-native'
? `<span style="font-size:9px;padding:1px 5px;border-radius:3px;margin-left:4px;background:#0c3a4a;color:#67e8f9;border:1px solid #0e7490">🖥 Native</span>`
: b.qemu
: b.qemu === 'renode'
? `<span style="font-size:9px;padding:1px 5px;border-radius:3px;margin-left:4px;background:#1a0a3a;color:#c084fc;border:1px solid #7c3aed">🖥 Renode</span>`
: b.qemu
? `<span style="font-size:9px;padding:1px 5px;border-radius:3px;margin-left:4px;
background:${b.qemu==='esp32s3'?'#1a3a5a':'#1a3a1a'};
color:${b.qemu==='esp32s3'?'#60a5fa':'#22c55e'};