본문으로 건너뛰기
ESP32-C3 Mastering · 2/12

ESP32-C3 RISC-V 코어 분석 — RV32IMC·PMP·인터럽트 컨트롤러

· Hawk · 9분 읽기

#한 줄 요약

“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 한 줄로 끝납니다. 무엇이 들어 있는지 풀어 봅니다.

확장의미명령어 수비고
IBase integer (32-bit)약 40개LW/SW, ADD/SUB, BEQ/BNE 등
MMultiplication & division8개MUL, MULH, DIV, REM 등
CCompressed (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 ← rs1
csrrs rd, csr, rs1 # rd ← csr, csr ← csr | rs1 (Set bits)
csrrc rd, csr, rs1 # rd ← csr, csr ← csr & ~rs1 (Clear bits)

C3에 있는 주요 CSR입니다.

CSR주소의미
mstatus0x300global 인터럽트 enable, privilege
misa0x301구현된 ISA 비트맵 (RV32IMC)
mie0x304인터럽트별 enable mask
mtvec0x305trap vector 베이스 주소
mscratch0x340trap handler temp 저장소
mepc0x341trap 발생 시 PC
mcause0x342trap 원인
mtval0x343trap 부가 정보 (잘못된 주소 등)
mip0x344인터럽트 pending 비트
pmpcfg0~30x3A0~0x3A3PMP 설정 (4 entries per CSR)
pmpaddr0~150x3B0~0x3BFPMP base address
mcycle0xB00cycle counter (low 32)
mcycleh0xB80cycle counter (high 32)
minstret0xB02실행된 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
}

csrrcsrrs rd, csr, x0의 축약입니다. csrci/csrsi5-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:

PMP Entry — pmpcfg + pmpaddr

A field (address matching mode):

모드의미
00OFF엔트리 사용 안 함
01TORTop-Of-Range — 이전 entry의 addr ~ 현재 addr
10NA4Naturally Aligned 4-byte
11NAPOTNaturally Aligned Power-Of-Two

pmpcfg0 한 CSR이 entry 0~3의 cfg 4개를 담고, pmpcfg1이 47, … pmpcfg3이 1215를 담습니다.

#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)와 비슷하지만 호환되지 않는 자체 구현입니다.

#구조

ESP32-C3 interrupt flow — 31개 외부 source가 Interrupt Matrix를 거쳐 CPU INT 0~30으로 매핑되고 RV32 trap vector로 진입

특이한 점은 어떤 페리퍼럴 인터럽트가 어떤 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_IRAMISR 본문이 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를 먼저 유효한 값으로 쓰고 그 다음 pmpcfgA 필드를 NAPOT/TOR로 활성화해야 합니다. 순서가 바뀌면 현재 PC가 보호 영역 밖으로 떨어져 즉시 trap합니다.

#”FreeRTOS 안에서 mcycle이 자꾸 0으로 리셋된다”

mcycleCPU local counter이지만, ESP-IDF의 light-sleep 진입/탈출 시점에 재설정될 수 있습니다. 절대 시간이 필요하면 esp_timer_get_time()을 쓰고, 정밀 cycle countmcycle을 쓰되 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 동작을 다룹니다.

#관련 항목