한선씨 임베디드 MCU 백엔드 — 1단계 (toau2c→Cortex-M + 베어메탈 소형 런타임)
개요
세션2(임베디드) 브리프의 첫 단계 완료: 한선씨 → TOAU → toau2c → C → ARM Cortex-M3 크로스컴파일 → QEMU 베어메탈 실행, 인터프리터와 왕복(비트동일) 검증.핵심 난제는 toau2c 의 기존 런타임(runtime_stub.c)이 crownyc.c(11K줄+Security 프레임워크+48M malloc 아레나)를 통째로 #include 한다는 점 — MCU(수십KB RAM)에 부적합. 이를 독립 정적할당 소형 런타임(runtime_mcu.c) 으로 대체했다.
무엇을 했는지
- 소형 런타임
mcu/runtime_mcu.c—runtime_stub.h와 동일rt_*인터페이스라 toau2c 생성prog.c무수정 링크.
int_to_cube/cube_to_int/cube_add/cube_sub/cube_mul/cube_divmod/cube_cmp/cube_to_trit)를 crownyc.c v10.3 에서 1:1 이식 → 인터프리터와 비트동일.
- int64 고정: ARM long=32bit 라 cube_to_int 의 base=3^23 오버플로 위험 → 내부 long long 으로 호스트(64bit) 결과와 일치 보장.
- 정적 할당만: 값스택(256)·리턴스택(128)·변수 메모리 윈도우(1024큐브)·프레임스택(64) 전부 고정 배열. malloc 0, GC/프레임회수 없음(FRAME_LEAVE 는 FP만 복원, 메모리 유지 = 정적할당. feedback_crownyc_frame_reclaim_unsafe 정합).
- 주소 윈도우 리매핑: crownyc 는 전역=[1000,10000), 프레임=frame_ptr=10000부터(48M 전제). 프레임 주소는 런타임 frame_ptr 상대라 초기 frame_ptr/mem_count 를 전역 바로 위(MEM_BASE+512)로 당겨 단일 수KB 윈도우에 접음. 결과 동일.
- program[]은 FLASH const 포인터만 보관(RAM 복사 안 함).
- 구현 opcode: 스택(0,1,3~25), 산술(81~89: ADD/SUB/MUL/DIV/MOD/NEG/POW/MIN/MAX), 비교(162~174), Kleene논리(166/167/172), 제어(243~248 JMP/JMPIF/JMPIFN/CALL/RET), 프레임(78/79/80), 출력(324 PRINT 정수), ABS(605).
- 베어메탈 부트스트랩 (부득이 C — 한선씨 불가 영역):
mcu/startup_lm3s.c — Cortex-M3 벡터테이블 + Reset_Handler(.data 복사/.bss 0초기화/main 호출) + rt_putchar = Stellaris UART0(0x4000C000) MMIO 폴링 송신.
- mcu/lm3s6965.ld — FLASH 256K@0x0 / SRAM 64K@0x20000000, 스택=SRAM 최상단.
- 빌드/실행
mcu/toau2c-mcu-build.sh—.한선 → ELF원클릭(+run시 QEMU). prog.c 의 미사용<stdio.h>만 sed 제거(newlib 불요). - 한선씨 동반
mcu/trit_alu.한선— 트릿 ALU 로직을 순수 한선씨로 구현 + 자가테스트 6/6 통과. (한선씨 자연반올림%가 균형3진 자릿수를 네이티브로 생성 → C 의 r==2 보정 불요, 더 간결.)
검증 결과 (왕복 = 인터프리터 기준값 일치)
| 프로그램 | 기대 | 호스트 MCU런타임 | ARM QEMU(lm3s6965evb) |
|---|---|---|---|
변수+산술 7+5 | 12 | ✅12 | ✅12 |
| 반복문+함수+조건 (제곱합/비교) | 55,1 | ✅ | ✅ |
| 음수+절댓값 | -25,9 | ✅ | ✅ |
| 산술(우선순위/나눗셈자연반올림/모듈로) | 28,14,2,13 | ✅ | — |
| 재귀 피보(10) | 55 | ✅ | — |
| 논리(그리고/또는) | 1,7 | ✅ | — |
| 중첩루프 | 36 | ✅ | — |
- 풋프린트(t1): FLASH text 12,980B / SRAM bss 35,488B (64KB 내).
관련 파일
/Users/ef/CrownyOS/crownyc/mcu/runtime_mcu.cruntime_mcu.hruntime_stub.h(심)/Users/ef/CrownyOS/crownyc/mcu/startup_lm3s.clm3s6965.ld/Users/ef/CrownyOS/crownyc/mcu/platform_host.c(호스트 검증 백엔드)/Users/ef/CrownyOS/crownyc/mcu/toau2c-mcu-build.sh/Users/ef/CrownyOS/crownyc/mcu/trit_alu.한선(한선씨 동반)- 기반:
/Users/ef/CrownyOS/crownyc/toau2c.c(무수정),crownyc.c(무수정)
도구 환경
arm-none-eabi-gcc 16.1.0(brew),qemu-system-arm(lm3s6965evb Cortex-M3). newlib 불필요(freestanding+nostdlib).
잔여 이슈 / 다음 단계
- 문자열/배열 미지원: PRINT 는 정수만. 문자열 풀(str pool)·배열 힙을 윈도우 안에 정적 구현 필요 →
출력값("text"), UART echo 의 텍스트 처리. - 부동소수점/libm 빌트인 미지원: SIN/COS/로그/루트(606~) 등 — MCU 정수펌웨어 범위 밖, 필요 시 고정소수점.
- 드라이버: GPIO/UART 입력(GETC) 빌트인 → LED blink + UART echo 데모(브리프 성공기준). UART0 RX(
UART_DR/UART_FRRXFE) + GPIO 레지스터 빌트인 추가. - 타깃 확장: 현재 lm3s6965evb(Cortex-M3). STM32/RP2040(M0+)/ESP32 링커·startup 추가.
- 재귀 깊이: 프레임스택 64 + 윈도우 1024큐브 한계 → 깊은 재귀는 윈도우 확대 필요(또는 깊이 가드).
2단계 완료 (2026-06-20) — 문자열 + GPIO/UART 드라이버 → blink + echo 데모
브리프 성공기준 전부 충족: 한선씨 펌웨어가 실 Cortex-M3(QEMU lm3s6965evb)에서 LED blink + UART echo 네이티브 동작(VM 인터프리터 아님).
추가 구현
- 정적 문자열 풀 (runtime_mcu.c): handle=STR_HANDLE_BASE(1e9)+id 규약(int64 큐브). STR_NEW(338)/STR_LEN(339)/글자(343)/PRINT문자열분기(324)/INPUT(325)/ADD문자열연결(81). 리터럴 스크래치 밴드 주소 50000 리매핑(STR_SCRATCH 256큐브). UTF-8 한글 바이트 그대로 통과.
- MMIO 드라이버 (컴파일러·VM 무수정): MCU에서 무의미한 파일IO 키워드를 하드웨어 레지스터로 재해석 —
쓰기(주소,값)=POKE(620),읽기(주소)=PEEK(249). 플랫폼 훅rt_mmio_write/read분리(호스트=시뮬배열 segfault방지, 베어메탈=volatile 32bit 접근).rt_getchar=UART0 RX 폴링. - 데모 펌웨어 (순수 한선씨):
mcu/blink.한선 — GPIO Port F PF1 토글(클럭enable RCGC2→DIR/DEN→DATA), read-back 검증. PF0은 Stellaris 잠금핀(NMI)이라 PF1 사용.
- mcu/echo.한선(test.sh 내) — 입력()/출력값() 라인 echo.
- 회귀 하니스
mcu/test.sh— 호스트 왕복 5/5 + ARM 데모.
검증 (test.sh, PASS=5 FAIL=0)
- 호스트 왕복: arith/fib(피보12=144)/loop/str/echo 5/5 = 인터프리터 일치.
- ARM blink:
set=1 reg=2↔set=0 reg=06회 교대 — 실 GPIO 쓰기+read-back 일치. - ARM echo:
ping→echo: ping,pong→echo: pong— UART RX→TX. - MMIO 확정:
쓰기(0x4000C000, 90)→ UART에 'Z' 출력(실 레지스터 도달 증명).
함정 기록
- PF0 = Stellaris 잠금핀(NMI/GPIOLOCK) — DIR/DEN 무시됨. blink는 PF1 사용.
- 16진→10진 수동변환 오류 — 주소 틀리면 엉뚱한 레지스터 poke(무증상 실패). python 검산 필수.
- bg qemu stdin=/dev/null (POSIX sh) — UART RX echo 검증은 qemu 포그라운드+백그라운드 킬러로.
- 인라인
;주석 — 구버전 hanseonc_high가 코드로 파싱(특히0x/{/—포함). 주석은 전용 줄로.
남은 확장 (선택)
- 배열 힙(현재 윈도우 공유, 프레임회수와 충돌 가능 — 함수 밖 escape 배열 주의), 부동소수점/libm, 인터럽트/타이머, STM32/RP2040/ESP32 타깃, 실하드웨어 플래싱(openocd).
3단계 (2026-06-20) — 배열 + 두 번째 보드(이식성)
배열 지원
- opcode 405(ARRAY)/406(LEN)/407(INDEX)/408(APPEND)/415(ARRAY_SET). crownyc ARRAY_CAP=4095(블록 110KB)는 MCU 불가 → MCU CAP=64(슬롯 오프셋은 런타임 상수라 축소해도 동작 동일).
- 별도 배열 힙: 윈도우 상단에서 하향 성장(arr_ptr), 프레임힙(mem_count↑)과 중앙에서 만남. 회수 안 함 → 함수 밖 escape 배열도 안전(프레임회수와 물리적 분리. 사용자 "프레임회수 금지" 취지와 배열안전 양립).
- 배열 리터럴 임시영역 리매핑: hanseonc_high
array_temp_base=11999700(중첩 시 -256)에 STORE되는 임시값 → 밴드 [11999700-256,+64) 320큐브로 리매핑(미리매핑 시 단일 sink 붕괴→전원소 동일값 버그). str 리터럴(50000)과 동일 패턴. - str_scratch 256→128 축소(SRAM 여유). blink bss 52.7KB(64KB 내, 스택 ~12KB).
두 번째 보드 — mps2-an385 (이식성 증명)
- 동일 runtime_mcu.c, startup/linker만 교체:
startup_mps2.c(CMSDK UART0 0x40004000: DATA/STATE/CTRL) +mps2.ld(코드 0x0, SRAM 0x20000000). 빌드MCU_TARGET=mps2. - 그리팅·배열(3/60/99/179)·echo(RX→TX) 전부 동작 → 백엔드가 lm3s 전용 아님 입증.
- 함정: mps2-an385는
-serial stdio필요(lm3s의-nographic과 UART RX 라우팅 다름).
검증 (test.sh, PASS=6 FAIL=0)
- 호스트 왕복 6/6: arith/fib/loop/str/echo/array.
- lm3s6965evb: blink(실GPIO)/echo.
- mps2-an385: array/echo (동일 런타임).
누적 구현 opcode
스택0~25 · 산술81~89 · 비교162~174 · Kleene166/167/172 · 제어243~248 · 프레임78/79/80 · 배열405~408/415 · 문자열338/339/343 · MMIO620(POKE)/249(PEEK) · IO324/325 · ABS605.4단계 (2026-06-20/21) — 멀티-ISA + 타이머 + 고정소수 + 플래싱 ("다 같이")
사용자 "다 같이 하자" → 잔여 확장 5종 전부. 4개 ISA/코어가 동일 runtime_mcu.c.
① Cortex-M0 (microbit, nRF51822)
-mcpu=cortex-m0(Thumb-1, HW나눗셈 없음→libgcc). startup_microbit.c(nRF51 이벤트형 UART)+microbit.ld.- RAM 16KB 제약 → m0 타깃이 캡 대폭 축소(-DMEM_WIN=256 등). 재귀 피보(12)=144, 문자열·정수 동작(bss ~12KB).
④ RISC-V (rv64, QEMU virt)
riscv64-elf-gcc -march=rv64imac -mabi=lp64 -mcmodel=medany. startup_riscv.c(ns16550 UART)+riscv_virt.ld. 그리팅·배열·echo 동작.- 함정: medlow→0x80000000 relocation truncated→
-mcmodel=medany.-nostdlib서 memcpy/memset 미정의→mcu_libc.c(freestanding mem) 추가.
③ 타이머 (SysTick, ARM 코어)
- timer.한선: MMIO(쓰기/읽기)로 SysTick(CSR/RVR/CVR) 직접 제어, COUNTFLAG 폴링 5틱. 실 하드웨어 페이싱. (진짜 ISR 콜백은 toau2c 모델 한계로 향후과제)
② 부동소수점 → 고정소수점 (정직한 대체)
- 발견: 부동소수점은 hanseonc_high→TOAU 상위경로 미구현 —
출력값(3.5)조차 인터프리터가 garbage(전용 opcode 없음). MCU에서 이식할 올바른 기준 없음. - FPU 없는 MCU 표준 = 고정소수점.
고정소수.한선(밀리 스케일, floor 나눗셈으로 자연반올림 함정 회피). 3.5+1.2=4.700, ×=4.200, ÷=2.916. 전 타깃·호스트 일치.
⑤ 실하드웨어 플래싱
flash.sh <보드> <입력>: openocdprogram ELF verify reset exit. lm3s(stellaris)/microbit(nrf51) — 실 칩이라 QEMU용 startup/ld 그대로. 미검증(하드웨어 미보유), 표준 절차 제공.
크로스세션 함정 (중요)
- 다른 세션이 toau2c.c 업그레이드(PRINT 인라인화 →
rt_putint/rt_puts/rt_str_valid/rt_str_ptr/rt_str_len직접 호출). runtime_stub엔 추가됐으나 runtime_mcu엔 없어 빌드 실패 → 동일 함수 5개 추가로 해소. 공유 toau2c 변경 시 runtime_mcu도 동반 갱신 필요.
최종 검증 (test.sh, PASS=7 FAIL=0)
- 호스트 왕복 7/7: arith/fib/loop/str/echo/array/fixed.
- lm3s6965evb: blink/echo/timer(5틱). mps2-an385: array/echo.
- RISC-V rv64: array. Cortex-M0: 재귀 피보(12)=144(16KB).
산출물 추가
startup_microbit.c+microbit.ld(M0) · startup_riscv.c+riscv_virt.ld(RV) · mcu_libc.c(freestanding mem) · timer.한선 · 고정소수.한선 · flash.sh. 빌드MCU_TARGET=lm3s|mps2|m0|riscv.