ESP32-C3 RISC-V 코어 분석 — RV32IMC·PMP·인터럽트 컨트롤러
#한 줄 요약
“C3의 코어는 M-mode만 도는 RV32IMC + PMP 16 entries + Espressif 자체 인터럽트 컨트롤러입니다.” 표준 RISC-V CLINT/PLIC 대신, Xtensa 시절의 인터럽트 매트릭스를 그대로 가져와 RISC-V에 맞춰 재설계한 변형 CLIC가 들어 있습니다.
ESP-IDF가 가려 두는 부분이 정확히 이 장의 주제입니다. C에서 보이는 코어는 그냥 32-bit MCU지만, 부트로더·인터럽트 진입·PMP 설정·secure boot에서는 RISC-V 특권 모델을 직접 만져야 합니다. 이 장에서는 꼭 알아야 하는 만큼의 RISC-V를 정리합니다.
깊이는 ESP-IDF 사용자가 부트 시퀀스를 읽고 인라인 어셈블리를 짤 수 있는 수준까지입니다. RISC-V 사양 전체를 다루지는 않습니다. 그건 별도 시리즈로 떼어 둡니다.
#RV32IMC — 무엇이 들어 있나
C3 코어가 지원하는 명령어 집합은 RV32IMC 한 줄로 끝납니다. 무엇이 들어 있는지 풀어 봅니다.
| 확장 | 의미 | 명령어 수 | 비고 |
|---|---|---|---|
| I | Base integer (32-bit) | 약 40개 | LW/SW, ADD/SUB, BEQ/BNE 등 |
| M | Multiplication & division | 8개 | MUL, MULH, DIV, REM 등 |
| C | Compressed (16-bit) | 약 30개 | 코드 크기 ~25% 감소 |
C3에 없는 것도 분명히 해 둡니다.
| 없는 것 | 설명 |
|---|---|
| A (atomic) | LR/SC, AMO 없음 |
| F/D (float) | 하드웨어 FPU 없음, soft-float |
| Zicsr 외 CSR 확장 | 표준 Zicsr는 있음 |
| U-mode (user mode) | M-mode only |
| S-mode (supervisor) | MMU 없음, OS-class 칩이 아님 |
| V (vector) | SIMD 없음 |
atomic 없음은 주의할 점입니다. FreeRTOS의 atomic primitive는 인터럽트 mask로 흉내냅니다. 진짜 LR/SC가 없으니 멀티코어 SMP는 처음부터 불가능합니다(어차피 single-core이지만).
FPU 없음은 컴파일 옵션에 영향을 줍니다. float/double 연산이 모두 라이브러리 호출로 펼쳐집니다. 성능이 필요하면 fixed-point 또는 정수 산술로 가야 합니다.
#Compressed 명령어가 만드는 것
C 확장은 자주 쓰는 명령어의 16-bit 버전입니다. 코드가 작아지고 I-cache 미스가 줄어듭니다.
# 일반 32-bit 명령addi a0, a0, 1 # 0x00150513 (4 bytes)sw a0, 0(sp) # 0x00a12023 (4 bytes)
# Compressed 16-bit 명령c.addi a0, 1 # 0x0505 (2 bytes)c.sw a0, 0(sp) # 0xc02a (2 bytes)GCC는 -march=rv32imc로 기본 사용합니다. 디스어셈블리에서 c. prefix가 보이면 compressed입니다.
#특권 모델 — M-mode only
RISC-V는 M (machine), S (supervisor), U (user) 세 단계를 정의하지만, C3는 M-mode만 구현합니다. 모든 코드가 최고 권한에서 돕니다.
RISC-V privilege levels M-mode ← C3는 이것만 S-mode ← Linux 같은 OS가 사용 (C3에 없음) U-mode ← user app (C3에 없음)장단점이 분명합니다.
장점
- 특권 전환 오버헤드 없음 (ECALL 트랩 없음)
- 메모리 보호는 PMP로 충분
- 칩 면적·전력 절감
단점
- “user mode”에서 코드를 격리할 수 없음
- 일부 보안 시나리오에서 sandboxing 불가
ARMv7-M의 thread/handler mode 구분과 비슷한 단순함입니다. 실시간 MCU에서는 흔한 선택입니다.
#CSR — Control and Status Registers
CSR은 코어의 제어판입니다. 인터럽트 enable, trap 핸들러 주소, PMP, cycle counter 등이 모두 CSR로 노출됩니다. CSR 접근은 전용 명령어로 합니다.
# CSR 읽기/쓰기 원자 명령csrrw rd, csr, rs1 # rd ← csr, csr ← rs1csrrs rd, csr, rs1 # rd ← csr, csr ← csr | rs1 (Set bits)csrrc rd, csr, rs1 # rd ← csr, csr ← csr & ~rs1 (Clear bits)C3에 있는 주요 CSR입니다.
| CSR | 주소 | 의미 |
|---|---|---|
mstatus | 0x300 | global 인터럽트 enable, privilege |
misa | 0x301 | 구현된 ISA 비트맵 (RV32IMC) |
mie | 0x304 | 인터럽트별 enable mask |
mtvec | 0x305 | trap vector 베이스 주소 |
mscratch | 0x340 | trap handler temp 저장소 |
mepc | 0x341 | trap 발생 시 PC |
mcause | 0x342 | trap 원인 |
mtval | 0x343 | trap 부가 정보 (잘못된 주소 등) |
mip | 0x344 | 인터럽트 pending 비트 |
pmpcfg0~3 | 0x3A0~0x3A3 | PMP 설정 (4 entries per CSR) |
pmpaddr0~15 | 0x3B0~0x3BF | PMP base address |
mcycle | 0xB00 | cycle counter (low 32) |
mcycleh | 0xB80 | cycle counter (high 32) |
minstret | 0xB02 | 실행된 instruction 수 |
#인라인 어셈블리로 CSR 읽기
ESP-IDF에서 cycle counter를 읽는 가장 짧은 방법입니다.
static inline uint32_t read_mcycle(void) { uint32_t cycles; asm volatile ("csrr %0, mcycle" : "=r"(cycles)); return cycles;}
static inline void disable_interrupts(void) { asm volatile ("csrci mstatus, 0x8"); // MIE bit clear}
static inline void enable_interrupts(void) { asm volatile ("csrsi mstatus, 0x8"); // MIE bit set}csrr은 csrrs rd, csr, x0의 축약입니다. csrci/csrsi는 5-bit immediate로 설정/해제를 한 번에 합니다.
#mcause 디코딩
trap이 발생하면 mcause에 원인이 담깁니다.
mcause[31] Interrupt (1) or Exception (0)mcause[30:0] 원인 코드
예시: 0x00000002 Illegal instruction 0x00000005 Load access fault 0x80000007 Machine timer interrupt 0x8000001F External interrupt (Espressif custom)ESP-IDF의 panic_handler.c가 이 값을 읽어 crash 메시지를 출력합니다.
#PMP — Physical Memory Protection
PMP는 코드/데이터 영역마다 R/W/X 권한을 정합니다. ARM의 MPU와 기능적으로 동일하되 설정 방식이 다릅니다.
#Entry 구조
PMP entry (총 16개) — pmpcfgN 8 bit + pmpaddrN 32 bit:
A field (address matching mode):
| 값 | 모드 | 의미 |
|---|---|---|
00 | OFF | 엔트리 사용 안 함 |
01 | TOR | Top-Of-Range — 이전 entry의 addr ~ 현재 addr |
10 | NA4 | Naturally Aligned 4-byte |
11 | NAPOT | Naturally Aligned Power-Of-Two |
pmpcfg0 한 CSR이 entry 0~3의 cfg 4개를 담고, pmpcfg1이 47, … 15를 담습니다.pmpcfg3이 12
#NAPOT으로 영역 지정
NAPOT은 주소 + 크기를 한 32-bit 워드에 인코딩합니다.
영역 0x40000000~0x4000FFFF (64 KB) 를 read-only로 보호:
pmpaddr0 = (0x40000000 >> 2) | ((0x10000 - 1) >> 3) = 0x10000000 | 0x1FFF = 0x10001FFF
pmpcfg0 byte0 = A=NAPOT(0b11) | X=0 | W=0 | R=1 = 0b00011001 = 0x19#인라인 어셈블리로 PMP 설정
ESP-IDF의 secure boot가 비슷한 작업을 부트로더 단계에서 합니다.
static void set_pmp_region0_readonly(void) { // 0x40000000~0x4000FFFF read-only uint32_t addr = (0x40000000 >> 2) | (0xFFFF >> 3); uint32_t cfg = 0x19; // NAPOT | R
asm volatile ("csrw pmpaddr0, %0" : : "r"(addr)); asm volatile ("csrrs x0, pmpcfg0, %0" : : "r"(cfg));}쓰기 시도 시 load/store access fault가 발생하며 panic handler로 진입합니다.
메모: PMP의
L비트(Lock)를 set하면 다음 reset까지 그 엔트리를 수정할 수 없습니다. secure boot에서 bootloader 영역 보호에 사용합니다.
#인터럽트 컨트롤러 — Espressif CLIC 변형
표준 RISC-V는 PLIC(Platform-Level Interrupt Controller)을 정의하지만, Espressif는 Xtensa 시절의 인터럽트 매트릭스를 RISC-V에 그대로 가져왔습니다. CLIC(Core Local Interrupt Controller)와 비슷하지만 호환되지 않는 자체 구현입니다.
#구조
특이한 점은 어떤 페리퍼럴 인터럽트가 어떤 CPU INT 번호가 될지를 런타임에 설정한다는 것입니다. ESP-IDF의 esp_intr_alloc()이 이를 자동 처리합니다.
#esp_intr_alloc 예시
#include "esp_intr_alloc.h"#include "driver/timer.h"
static void IRAM_ATTR my_isr(void *arg) { // ISR 본문}
void setup_timer_isr(void) { esp_intr_alloc( ETS_TG0_T0_LEVEL_INTR_SOURCE, // 페리퍼럴 인터럽트 소스 ESP_INTR_FLAG_LEVEL3 | ESP_INTR_FLAG_IRAM, my_isr, NULL, NULL );}ESP_INTR_FLAG_IRAM은 ISR 본문이 IRAM에 위치하도록 강제합니다. flash 캐시 미스가 발생해도 ISR이 지연 없이 실행됩니다.
#인터럽트 우선순위
CPU INT priority levels (1~7, 높을수록 우선) Level 1~3 일반 ISR Level 4~5 고우선 ISR (FreeRTOS critical section 위) Level 6 예약 (NMI 유사) Level 7 non-maskable (디버거)C로 작성하는 ISR은 Level 1~3까지만 가능합니다. Level 4 이상은 어셈블리로 작성해야 합니다(FreeRTOS API 호출 불가).
#부트 시퀀스 — 어디서 어디로 가는가
C3의 부트는 3단계입니다.
1. ROM Bootloader (0x40000000)
- 칩에 내장된 immutable 코드
- SPI flash에서 2nd-stage bootloader 로드
- secure boot 검증 (활성화 시)
2. 2nd-stage Bootloader (flash 0x0000)
- ESP-IDF가 빌드한 코드
- 파티션 테이블 읽기
- factory 또는 ota_N 파티션 선택
- application 로드 + 검증
- PMP 초기 설정
3. Application
- app_main() 호출
- FreeRTOS 시작
PMP는 2단계에서 설정되어 application 시작 시점에는 이미 활성입니다. application 코드가 부트로더 영역에 쓰기 시도하면 즉시 trap이 발생합니다.
#자주 하는 실수
#”인라인 어셈블리에서 csrr이 컴파일 에러”
-march=rv32imc만으로는 Zicsr가 활성화되지 않는 GCC 버전이 있습니다. -march=rv32imc_zicsr로 명시하거나 ESP-IDF 기본 옵션을 그대로 둡니다.
#”PMP 설정 후 즉시 fault”
pmpcfg를 먼저 쓰면 안 됩니다. pmpaddr를 먼저 유효한 값으로 쓰고 그 다음 pmpcfg의 A 필드를 NAPOT/TOR로 활성화해야 합니다. 순서가 바뀌면 현재 PC가 보호 영역 밖으로 떨어져 즉시 trap합니다.
#”FreeRTOS 안에서 mcycle이 자꾸 0으로 리셋된다”
mcycle은 CPU local counter이지만, ESP-IDF의 light-sleep 진입/탈출 시점에 재설정될 수 있습니다. 절대 시간이 필요하면 esp_timer_get_time()을 쓰고, 정밀 cycle count는 mcycle을 쓰되 sleep을 피합니다.
#”Level 4+ ISR에서 printf가 안 나온다”
Level 4 이상은 FreeRTOS API 호출 금지입니다. printf는 내부적으로 mutex를 잡으므로 deadlock입니다. 어셈블리 minimal handler에서 flag만 set하고 나머지를 deferred task로 넘기는 패턴을 씁니다.
#”C++ exception이 안 잡힌다”
ESP-IDF는 기본적으로 -fno-exceptions입니다. menuconfig에서 활성화 가능하지만 코드 크기가 크게 증가합니다. 보통 비활성 유지하고 expected<T, E> 또는 esp_err_t 기반 에러 전달을 씁니다.
#정리
- C3 코어는 RV32IMC + Zicsr + PMP 16 entries 구성으로, M-mode only이며 U/S-mode와 atomic·FPU·vector 확장은 없습니다.
- CSR 접근은
csrr/csrw/csrrs/csrrc전용 명령어로 수행하며,mstatus·mcause·mtvec·pmpcfgN·pmpaddrN이 자주 만지는 레지스터입니다. - PMP는 NAPOT·TOR·NA4 매칭 모드로 영역을 정의하고 R/W/X + L 비트로 권한을 지정하며, secure boot에서 부트로더 영역을 Lock합니다.
- 인터럽트는 표준 PLIC가 아닌 Espressif 자체 매트릭스로, 31개 외부 소스를 런타임에 CPU INT에 매핑하며
esp_intr_alloc()이 자동 처리합니다. - Level 4 이상 고우선 ISR은 어셈블리로 작성해야 하고 FreeRTOS API 호출 금지입니다.
- 부트 시퀀스는 ROM → 2nd-stage bootloader → application 3단계이며 PMP는 2단계에서 활성됩니다.
- ESP-IDF 사용자는 대부분 ISA를 몰라도 무방하지만 인라인 어셈블리·secure boot·panic 해석에서 RISC-V 지식이 필요합니다.
#다음 장 예고
다음 편은 Ch 3: 메모리 맵·플래시·SPIFFS/LittleFS입니다. 400 KB SRAM과 4 MB flash가 어떻게 영역으로 나뉘는지, 파티션 테이블과 OTA 동작을 다룹니다.
#관련 항목
- Ch 1: ESP32-C3 — 왜 RISC-V로 갈아탔나
- Ch 3: 메모리 맵·플래시·SPIFFS/LittleFS
- Ch 11: 보안·Secure Boot — PMP Lock 비트 활용
- Practical RTOS Internals Part 2.3: 인터럽트 모델
- Modern Embedded Recipes Part 3.5: 인라인 어셈블리
- 원문 — RISC-V Privileged Spec
- 원문 — ESP32-C3 Technical Reference Manual
ESP32-C3 Mastering · 2 of 12
- 1ESP32-C3 분석 — Espressif가 Xtensa에서 RISC-V로 갈아탄 이유
- 2ESP32-C3 RISC-V 코어 분석 — RV32IMC·PMP·인터럽트 컨트롤러
- 3ESP32-C3 메모리 맵과 플래시 — SPIFFS·LittleFS 파일시스템 선택
- 4ESP32-C3 디지털 출력 — GPIO·LEDC·MCPWM 세 모드 비교
- 5ESP32-C3 시리얼 통신 4종 — UART·SPI·I2C·I2S 분석
- 6ESP32-C3 ADC와 터치 센서 — 아날로그 입력 처리
- 7ESP32-C3 WiFi 4 스택 — Station·SoftAP·Mesh 구성
- 8ESP32-C3 BLE 5.0 분석 — GAP·GATT·Coded PHY
- 9ESP-IDF 빌드 시스템 분석 — 컴포넌트 구조와 CMake 통합
- 10ESP32-C3 위 FreeRTOS — 단일 코어 RTOS 활용 전략
- 11ESP32-C3 보안 분석 — Secure Boot·Flash Encryption·eFuse
- 12ESP32-C3 전력 관리 — Modem·Light·Deep Sleep와 Wake 소스
관련 글
ESP32-C3 분석 — Espressif가 Xtensa에서 RISC-V로 갈아탄 이유
Espressif가 Tensilica Xtensa에서 RISC-V로 전환한 첫 SoC. WiFi 4 + BLE 5.0, 32-bit RV32IMC.
ESP32-C3 전력 관리 — Modem·Light·Deep Sleep와 Wake 소스
5단계 power mode, RTC 도메인 활용, ULP 코프로세서 미지원 — C3는 RTC GPIO만.
ESP32-C3 보안 분석 — Secure Boot·Flash Encryption·eFuse
ECDSA 기반 Secure Boot V2, AES-256 Flash Encryption, eFuse 키 보관.