Zephyr v3.7.0 · CPU와 커널의 접점

아키텍처 포팅: 새 CPU가 Zephyr 스레드를 실행하려면

새 칩의 보드 파일을 만드는 일과 새 ISA·ABI를 커널에 연결하는 일은 깊이가 다릅니다. 리셋 직후 C로 진입하는 길, 스레드 레지스터의 보존 범위, 예외에서 돌아오는 길을 실제 RISC-V 포트와 함께 추적합니다.

본문 기준: Zephyr 3.7.0 LTS3 · 이후 유지보수 패치는 별도입니다.

이 글은 Zephyr v3.7.0의 공식 가이드와 같은 태그의 arch/riscv 구현을 기준으로 쓴 독립 해설입니다. RISC-V 포트는 이미 존재하는 읽기 사례이며, 아래 점검 순서는 이 작업 환경에서 새 아키텍처를 빌드했다는 기록이 아닙니다. CPU/SoC 구성과 Kconfig에 따라 실제 부트·인터럽트 경로는 달라집니다.

1. 보드가 아니라 실행 규약을 추가한다

Zephyr의 아키텍처 포트는 아직 지원하지 않는 ISA 또는 ABI에서 커널이 실행되도록 만드는 층이다. 이미 지원되는 RISC-V 코어를 단 새 보드라면 보통 기존 아키텍처 포트를 재사용하고 SoC·보드 정의를 추가한다. 반대로 새 실행 규약을 추가한다면 C 컴파일러가 어떤 레지스터를 호출 전후에 보존하는지부터, 인터럽트가 레지스터를 어디에 쌓는지까지 커널과 합의해야 한다.

그림 1. 같은 하드웨어 지원 요청이라도 수정 층이 다르다

각 열은 책임 경계입니다. 좁은 화면에서는 좌우로 이동하세요.

① 아키텍처 / ABICPU 진입·예외 프레임레지스터 보존·스택 정렬도구체인·링커 규약같은 실행 규약을 쓰는 여러 SoC가 공유
② SoC / 시리즈칩 초기화·메모리 구성IRQ 컨트롤러·타이머 연결CPU 포트가 요청하는 SoC hook칩 구현마다 달라질 수 있는 부분
③ 보드실장된 클록·핀·주변장치보드 DTS·기본 설정플래시·디버그 실행기같은 SoC라도 기판마다 다름
표 1. 먼저 어느 층을 볼 것인가
증상 또는 목표우선 살필 층이유
지원 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()를 호출한다.

그림 2. RISC-V 사례로 읽는 초기 실행의 경계

실제 호출 순서는 v3.7.0 prep_c.c를 기준으로 표기했습니다.

① 리셋 진입reset.S 등 초기 어셈블리CPU 상태·스택을 준비
② C 진입z_prep_c()아직 전역 데이터 사용 조건을 점검
③ 메모리 준비z_bss_zero()
z_data_copy()RISC-V v3.7.0 구현 순서
④ 선택된 SoC 작업soc_interrupt_init관련 Kconfig 설정이 있을 때 호출
⑤ 공통 커널z_cstart()가짜 초기 문맥에서 메인 스레드로 이동
표 2. 첫 printk 이전에 확인할 전제
경계보장할 상태어긋날 때의 관찰
첫 C 함수 진입ABI에 맞는 초기 스택함수 호출 또는 복귀에서 즉시 예외
전역 데이터 접근BSS 0 초기화, 초기화 데이터 복사전역 상태·포인터 값의 비결정적 오염
z_cstart() 호출공통 커널이 실행할 수 있는 CPU 상태메인 스레드 전환 전에 정지

근거: v3.7.0 부트 계약, RISC-V prep_c.c, RISC-V core 파일 목록.

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 설정도 같은 전환 지점에서 다룬다.

그림 3. A가 자발적으로 넘겨도 기존 B의 복귀 지점은 저장된 ra가 결정한다

A는 arch_switch()에서 멈췄습니다. B의 과거 정지 이유가 오른쪽 복귀 경로를 정합니다. 첫 진입은 표 3 아래를 보세요.

스레드 A · 저장ra, s0/s1 → thread 구조
RV32/64I라면 s2…s11도 저장
sp → thread 구조호출자 보존 레지스터는 ABI상 호출 경계에서 처리
기존 스레드 B · 저장된 ra로 복귀thread 구조 → sp, ra, s 복원이전 자발적 전환: arch_switch() 호출점이전 IRQ 선점: isr.S 예외 복귀 → mret
표 3. 다음 스레드의 복귀 주소는 어떻게 생겼나
전환 사례저장된 돌아갈 곳전환 뒤 실행
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에서 모든 구현의 인터럽트 컨트롤러가 하나로 고정된 것처럼 다루지 않는 이유다.

그림 4. RISC-V _isr_wrapper의 복귀 판단

그림은 여러 조건부 분기를 접은 읽기 지도이며 모든 Kconfig 조합의 단일 실행 추적은 아닙니다.

① 예외 진입호출자 보존 상태·예외 PC 등을 프레임에 기록임의 지점 중단에 대비
② IRQ·예외 처리SoC hook 및 등록된 ISR 경유칩마다 다른 IRQ 분배를 연결
③ 중첩 수 감소바깥쪽 복귀인지 확인중첩 안쪽이면 즉시 스레드 전환하지 않음
④ 복귀 대상필요할 때 다음 switch handle 조회·전환, 아니면 기존 문맥 복원마지막에 mret
협력형 스레드가 중단되었거나 스케줄러가 잠겨 있을 때 선점하면 안 된다는 정책은 커널의 실행 조건이다. 아키텍처 예외 경로는 그 결정을 실제 레지스터·스택 복귀에 안전하게 반영해야 한다.
표 4. 인터럽트 포트에서 따로 점검할 계약
계약v3.7.0 RISC-V 근거실패할 때 점검할 것
레지스터 프레임isr.S 저장·복원중단 위치가 바뀔 때만 깨지는 값
SoC IRQ 경로__soc_handle_irq 등 hookISR 미호출 또는 잘못된 IRQ 번호
중첩 해제 후 전환nested 검사·z_get_next_switch_handle()중첩 복귀 중 스택 오염
예외 PC·상태 복원mepc·mstatus 복원 뒤 mretISR 후 다른 주소·권한으로 복귀

근거: RISC-V isr.S, 인터럽트·예외 포팅 가이드.

5. 컴파일되는 코드와 실행되는 이미지 사이

어셈블리의 레지스터 이름만 맞추어도 포팅은 끝나지 않는다. 호출 규약에 맞는 스택 정렬, 컴파일러의 CPU 옵션과 정의, 링크할 섹션·초기 벡터, 원자 연산 및 idle 진입 같은 저수준 함수가 함께 들어맞아야 한다. 공식 가이드는 아키텍처별 링커 스크립트와 도구체인 연결을 필수 범위로 둔다. 또한 ARCH_STACK_PTR_ALIGN을 정의하고, 메모리 보호·사용자 모드·TLS·FPU 등은 선택한 기능에 맞춰 별도 계약을 채우도록 설명한다.

표 5. 첫 부팅 외에 남는 인터페이스
묶음필수·조건확인할 질문
컴파일러·링커기본 포트에 필수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처럼 선택 기능을 하나씩 더해야 실패 원인을 추적하기 쉽다.

표 6. 최소 동작에서 선택 기능까지
단계관찰 지점주요 의심 경계
① 첫 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 동작으로 섞지 않았다.

1차 자료

Board Porting →Interrupt & Exception →Thread Scheduler →