arch/riscv 구현을 기준으로 쓴 독립 해설입니다. RISC-V 포트는 이미 존재하는 읽기 사례이며, 아래 점검 순서는 이 작업 환경에서 새 아키텍처를 빌드했다는 기록이 아닙니다. CPU/SoC 구성과 Kconfig에 따라 실제 부트·인터럽트 경로는 달라집니다.1. 보드가 아니라 실행 규약을 추가한다
Zephyr의 아키텍처 포트는 아직 지원하지 않는 ISA 또는 ABI에서 커널이 실행되도록 만드는 층이다. 이미 지원되는 RISC-V 코어를 단 새 보드라면 보통 기존 아키텍처 포트를 재사용하고 SoC·보드 정의를 추가한다. 반대로 새 실행 규약을 추가한다면 C 컴파일러가 어떤 레지스터를 호출 전후에 보존하는지부터, 인터럽트가 레지스터를 어디에 쌓는지까지 커널과 합의해야 한다.
각 열은 책임 경계입니다. 좁은 화면에서는 좌우로 이동하세요.
| 증상 또는 목표 | 우선 살필 층 | 이유 |
|---|---|---|
| 지원 ISA의 새 개발 보드 | 보드·SoC | 기존 ABI·문맥 전환 코드를 재사용한다. |
| 같은 ISA지만 다른 IRQ 컨트롤러 | SoC와 드라이버 경계 | RISC-V isr.S도 SoC IRQ 처리 hook을 기대한다. |
| 지원하지 않는 ISA 또는 ABI | 아키텍처 | 초기 진입, 문맥 전환, 예외 프레임을 새 규약에 맞춰야 한다. |
근거: Architecture Porting Guide, SoC Porting Guide, Board Porting Guide.
2. 리셋 상태를 C가 실행 가능한 상태로 바꾼다
부트 경로의 목적은 아직 일반 C 함수의 전제가 성립하지 않는 CPU 상태에서 스택과 메모리를 준비해 공통 커널 초기화 z_cstart()로 넘기는 것이다. 공식 가이드는 초기 스택 준비, 필요할 때 ROM의 초기화 데이터 복사, BSS 0 초기화를 공통 작업으로 든다. 이 작업의 순서와 일부 담당자는 아키텍처 구현마다 다를 수 있다. RISC-V v3.7.0의 z_prep_c()는 z_bss_zero(), z_data_copy(), 설정에 따른 soc_interrupt_init() 다음 z_cstart()를 호출한다.
실제 호출 순서는 v3.7.0 prep_c.c를 기준으로 표기했습니다.
reset.S 등 초기 어셈블리CPU 상태·스택을 준비z_prep_c()아직 전역 데이터 사용 조건을 점검z_bss_zero()z_data_copy()RISC-V v3.7.0 구현 순서soc_interrupt_init관련 Kconfig 설정이 있을 때 호출z_cstart()가짜 초기 문맥에서 메인 스레드로 이동| 경계 | 보장할 상태 | 어긋날 때의 관찰 |
|---|---|---|
| 첫 C 함수 진입 | ABI에 맞는 초기 스택 | 함수 호출 또는 복귀에서 즉시 예외 |
| 전역 데이터 접근 | BSS 0 초기화, 초기화 데이터 복사 | 전역 상태·포인터 값의 비결정적 오염 |
z_cstart() 호출 | 공통 커널이 실행할 수 있는 CPU 상태 | 메인 스레드 전환 전에 정지 |
3. 스케줄러의 선택을 CPU 레지스터로 바꾼다
스케줄러가 다음 스레드를 골라도 CPU의 스택 포인터와 보존 레지스터가 이전 스레드를 가리키면 전환은 끝나지 않는다. RISC-V의 z_riscv_switch()는 이전 스레드의 복귀 주소 ra, 보존 레지스터 s0·s1(일반 RV32/64I에서는 s2~s11도), 그리고 sp를 저장한다. 다음 스레드의 sp와 해당 레지스터를 불러온 뒤 복귀한다. 코드가 RV_E·RV_I 매크로로 저장 대상을 나누는 것은 RV32E처럼 레지스터 집합이 작은 구성을 위한 것이다. 선택한 기능에 따라 TLS용 tp, FPU 공유, PMP 설정도 같은 전환 지점에서 다룬다.
ra가 결정한다A는 arch_switch()에서 멈췄습니다. B의 과거 정지 이유가 오른쪽 복귀 경로를 정합니다. 첫 진입은 표 3 아래를 보세요.
ra, s0/s1 → thread 구조RV32/64I라면
s2…s11도 저장sp → thread 구조호출자 보존 레지스터는 ABI상 호출 경계에서 처리ra로 복귀thread 구조 → sp, ra, s 복원이전 자발적 전환: arch_switch() 호출점이전 IRQ 선점: isr.S 예외 복귀 → mret| 전환 사례 | 저장된 돌아갈 곳 | 전환 뒤 실행 |
|---|---|---|
| A가 자발적으로 대기·양보 | A의 ra는 arch_switch() 호출 경계 | A가 나중에 선택되면 자신의 호출점으로 재개한다. 지금 선택된 B의 복귀 지점은 별개다. |
| 기존 B가 앞서 자발적으로 멈춤 | B의 저장된 ra가 arch_switch() 복귀점 | z_riscv_switch()의 ret 뒤 B의 호출점에서 이어 실행한다. |
| 기존 B가 앞서 IRQ에서 선점됨 | B의 저장된 ra가 isr.S의 z_riscv_thread_start 쪽 | 예외 프레임을 복원하고 mret로 중단된 명령어 흐름에 복귀한다. |
| 새 B의 첫 실행 | arch_new_thread()가 초기 ra와 예외 프레임 생성 | 예외 복귀를 거쳐 첫 entry로 진입한다. 이전 호출점은 없다. |
switch.S의 마지막 주석도 복귀 대상을 arch_switch() 또는 _irq_wrapper()라고 구분한다. 이는 지금 A가 어떻게 양보했는지와 B의 저장된 복귀 지점이 독립적이라는 뜻이다. 첫 실행은 이 둘과도 다르다. RISC-V v3.7.0의 arch_new_thread()는 초기 예외 프레임의 a0~a3에 앱 entry와 인자를 넣고, supervisor 스레드의 mepc에는 z_thread_entry, K_USER 스레드에는 k_thread_user_mode_enter를 둔다. 스레드의 callee_saved.ra는 z_riscv_thread_start를 가리킨다. 첫 z_riscv_switch()가 반환하면 isr.S의 예외 복귀 경로가 이 프레임을 복원해 mret로 첫 진입을 마친다.
근거: RISC-V thread.c, switch.S, isr.S, 문맥 전환 가이드.
4. 예외 복귀는 중첩과 재스케줄을 구분한다
인터럽트는 C 함수의 호출 규약대로 찾아오지 않는다. 포트의 진입 stub은 중단된 실행 상태를 프레임에 담고, 장치 ISR이나 예외 처리로 제어를 넘기며, 끝날 때 원래 문맥 또는 선택된 다음 문맥으로 복귀한다. RISC-V v3.7.0의 isr.S는 아키텍처 공통 레지스터 저장·복원을 맡으면서 SoC에 IRQ 판별·처리 hook을 요구한다. RISC-V에서 모든 구현의 인터럽트 컨트롤러가 하나로 고정된 것처럼 다루지 않는 이유다.
_isr_wrapper의 복귀 판단그림은 여러 조건부 분기를 접은 읽기 지도이며 모든 Kconfig 조합의 단일 실행 추적은 아닙니다.
mret| 계약 | v3.7.0 RISC-V 근거 | 실패할 때 점검할 것 |
|---|---|---|
| 레지스터 프레임 | isr.S 저장·복원 | 중단 위치가 바뀔 때만 깨지는 값 |
| SoC IRQ 경로 | __soc_handle_irq 등 hook | ISR 미호출 또는 잘못된 IRQ 번호 |
| 중첩 해제 후 전환 | nested 검사·z_get_next_switch_handle() | 중첩 복귀 중 스택 오염 |
| 예외 PC·상태 복원 | mepc·mstatus 복원 뒤 mret | ISR 후 다른 주소·권한으로 복귀 |
근거: RISC-V isr.S, 인터럽트·예외 포팅 가이드.
5. 컴파일되는 코드와 실행되는 이미지 사이
어셈블리의 레지스터 이름만 맞추어도 포팅은 끝나지 않는다. 호출 규약에 맞는 스택 정렬, 컴파일러의 CPU 옵션과 정의, 링크할 섹션·초기 벡터, 원자 연산 및 idle 진입 같은 저수준 함수가 함께 들어맞아야 한다. 공식 가이드는 아키텍처별 링커 스크립트와 도구체인 연결을 필수 범위로 둔다. 또한 ARCH_STACK_PTR_ALIGN을 정의하고, 메모리 보호·사용자 모드·TLS·FPU 등은 선택한 기능에 맞춰 별도 계약을 채우도록 설명한다.
| 묶음 | 필수·조건 | 확인할 질문 |
|---|---|---|
| 컴파일러·링커 | 기본 포트에 필수 | ISA/ABI 옵션과 초기 진입·섹션 배치가 같은 바이너리를 가리키는가? |
| 스택 정렬·원자 연산·idle | 기본 커널 계약 | ABI 정렬과 저수준 함수가 스레드·IRQ 양쪽에서 유지되는가? |
| TLS·FPU | 해당 기능을 켠 경우 | 전환 때 thread별 상태를 올바르게 복원하는가? |
| MMU/MPU·사용자 모드 | 지원 기능을 켠 경우 | 스택 영역과 권한 경계가 정확히 매핑되는가? |
예를 들어 switch.S는 CONFIG_THREAD_LOCAL_STORAGE가 활성화되면 다음 스레드의 TLS 포인터를 tp로 불러온다. 이 한 줄은 어셈블리, k_thread의 필드, 툴체인이 기대하는 TLS 레지스터 사용이 같은 약속을 공유해야 함을 보여준다. 기능을 켠 뒤에도 기존 switch 경로만 시험했다면 포트의 검증은 끝나지 않은 셈이다.
근거: 도구체인·링커, 스택 객체, TLS, RISC-V switch 구현.
6. 포트를 읽고 검증하는 순서
먼저 리셋에서 z_cstart()까지 단일 CPU 경로를 좁혀 본다. 다음으로 새 스레드의 첫 예외 복귀와 기존 스레드의 자발적 대기·양보 후 호출점 복귀를 각각 확인한다. 그 뒤 타이머 IRQ로 선점이 발생하는 경우와 IRQ 중첩 후 원래 스레드로 돌아오는 경우를 분리해 본다. 마지막에 TLS, 사용자 모드, FPU처럼 선택 기능을 하나씩 더해야 실패 원인을 추적하기 쉽다.
| 단계 | 관찰 지점 | 주요 의심 경계 |
|---|---|---|
| ① 첫 C 진입 | z_prep_c()에서 z_cstart()까지 도달 | 스택·BSS·데이터 초기화 |
| ② 첫 스레드 | 메인 스레드 진입 후 대기·양보·재개 | 초기 프레임과 피호출자 보존 레지스터 |
| ③ 타이머·IRQ | 중첩 없는 선점과 중첩 후 복귀 비교 | 예외 프레임·IRQ 분배·복귀 PC |
| ④ 선택 기능 | TLS 변수, FPU 상태, 권한 경계 별도 확인 | 전환 때 추가되는 상태와 설정 조건 |
이 표는 실행했다고 주장하는 테스트 로그가 아니라 소스 리뷰와 보드 bring-up에서 확인할 관찰 순서다. Zephyr 가이드는 TLS 구현 후 tests/kernel/threads/tls를 실행하라고 명시한다. 실제 포트 검증에서는 이와 함께 대상 SoC·보드의 테스트 가능 범위와 예외 로그 수단을 먼저 고정해야 한다.
7. 고정 버전의 원문과 다음 읽을 글
구조 설명은 v3.7.0 문서, 구체적인 레지스터·호출 순서는 같은 태그의 RISC-V 소스에서 확인했다. 다른 릴리스의 이름이나 구현을 v3.7.0 동작으로 섞지 않았다.
- Zephyr v3.7.0 Architecture Porting Guide
- v3.7.0 RISC-V
prep_c.c— 메모리 준비와 공통 커널 진입 - v3.7.0 RISC-V
thread.c— 새 스레드의 첫 예외 프레임과 진입 PC - v3.7.0 RISC-V
switch.S— 기존 스레드의 문맥 전환 - v3.7.0 RISC-V
isr.S— 예외 프레임과 IRQ 복귀