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
+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'};