2023년 12월 31일 일요일

AARCH64 bare metal 로 동작 시키기 - 2

디버깅:

BSP작성시 Trace32같은 장비를 사용하기도 하지만, qemu상에서 개발하므로 gdb를 사용하여 개발이 가능하다.  gdb사용을 위해 아래와 같이 qemu를 시작한다.

$ qemu-system-aarch64 -M virt,virtualization=on,gic-version=3 -m 4G -smp 2 -kernel my_bl3.elf -S -gdb tcp::1234,ipv4 -nographic

-S옵션을 주었으므로 virt machine은 시작되자마자 suspend상태로 gdb를 기다린다.  이제 다른 터미널에서 "gdb-multiarch"를 실행하여 gdb를 연결한다. 


$ gdb-multiarch -q ./my_bl33.elf

Reading symbols from ./my_bl33.elf...

(gdb) target remote:1234

Remote debugging using :1234

_start () at ./src/_start.s:10

10              mrs     x0, mpidr_el1       // get the CPU ID


참고 자료:

아래 책들을 참조하였다. 

1. ARM 64-Bit Assembly Language by Pyeatt Ph.D., Larry D (amazon.com)

이 책에서 부팅 후 초기 ASM code 와 PL011 UART 기기를 사용한  입출력 방법에 관한 정보를 얻을 수 있었다. 

하지만, 이 책에서는 ARM RaspberryPI를 기준으로 설명하므로 GIC를 사용한 SPI/PPI 처리 방법에 대한 정보가 없었고, generic timer 처리 관련 정보 또한 얻을 수 없었다. 

2. 임베디드 OS 개발 프로젝트 | 이만우 - 교보문고 (kyobobook.co.kr)

내가 구현하고자 하는 것과 거의 동일한 작업을 armv7 에서 구현하고 설명한 책으로 개발 진행에 큰 도움이 되었다.  특히 printf 구현 관련 부분은 나의  주된 관심사가 아니었지만 개발 진행에 꼭 필요한 부분이었기 때문에 재구현하지 않고 이만우님의 코드를 사용하여 개발을 진행 하였다. 

3. 시스템 소프트웨어 개발을 위한 Arm 아키텍처의 구조와 원리 | 김동현 - 교보문고 (kyobobook.co.kr)

Armv8 및 GICv3 관련 정보를 얻을 수 있었다. 

4. ARM 공식 문서

GICv2 : ARM Generic Interrupt Controller Architecture Specification - Version 2.0 (B)
GICv3 : Arm A-profile Architecture Registers


Exception Vector table:

1. aarch64에서는 exception vector table주소가 고정되어 있지 않고, VBAR_ELx 레지스터에 설정하면 된다.  다만 이 레지스터들은 Bit 63:11 만 사용되므로, 이 주소에 맞도록 시작 위치가 align만 되면 된다. 

         .align      11 

2. exception vector는 EL별로 존재하는데, 우리는 EL1만 관심이 있으므로 EL1용 만 작성한다. 

3. exception vector는 아래와 같이 4가지 부분으로 나뉜다. 

- exception from sp0 : 사용 안함.
- exception from current el : 현재 el에서 exception 발생 시 사용.
- exception from lower el(aarch64) : 현재 el보다 낮은 el에서 64bit모드로 실행 중 exception발생 시 사용
- exception from lower el(aarch32) : 현재 el보다 낮은 el에서 32bit모드로 실행 중 excepton발생 시 사용. 

boot loader를 항상 el1에서 동작하도록 작성한다면, "exception from current el" 부분에만 실제로 의미 있는 코드를 작성하면 된다. 

4. exception은 아래와 같이 4가지 종류로 나뉜다

- sync
- irq
- fiq
- serror

일반적으로 fiq는 secure world에서 사용한다. 우리의 목표를 구현하기 위해서는 irq만 처리하면 된다. 


프로그램 시작 주소

qemu실행 시 -M virt, virtualization=on,dumpdtb=my_bl.dtb ... 와 같이 dumpdtb를 지정하여 virt machine의 dtb파일을 얻을 수 있음. 

- dtb파일을 text형식인 dts로 변환할 수 있다. 

$ dtb -I dtb -O dts -o my_bl.dts ./my_bl.dtb

아래와 같이 메모리 주소가 0x40000000으로 표시됨. 

 18 »   memory@40000000 {
 19 »   »   reg = <0x00 0x40000000 0x01 0x00>;
 20 »   »   device_type = "memory";
 21 »   };


따라서 작성하는 프로그램을 0x40000000 이후에 로드하면 실행 가능함.  이때 해당 프로그램의 link script에 로드할 주소를 지정하여야 함. 

  6 SECTION
  7 {
  8  . = 0x40400000;
  9  . = ALIGN(8);

위와 같이 링커 스크립트 파일을 설정하여 빌드한 후 qemu에서 elf포맷으로 로드하면 (-kernel my_bl3.elf ) qemu가 알아서 해당 위치에 로드시켜 준다. 

- u-boot의 경우 0x40000000에 dtb를 로드하고 그 뒷부분에 u-boot코드가 로드되며 u-boot가 실행하는 프로그램은 0x40400000 이후에 로드해야만 실행 가능하다.(go addr)  추후 u-boot 를 사용하는 경우를 고려하여 0x40000000대신 0x40400000으로 시작 주소를 설정하였다. 

ARM GIC interrupts:

SGI : S/W generated interrrupt (INTID #0 ~ #15) : IPI

PPI : Private Peripheral interrupt (INTID #16 ~ #31) : 특정 core용

SPI : Shared Peripheral interrupt (INTID #32 ~ #1019) : 일반 interrupts

LPI : Local Peripheral interrupt (INTID #8192 ~) - MSI : for PCI device

 

GIC 설정:

매뉴얼을 보면 너무 많은 register들이 있어 어떤 순서로 어떤 레지스터를 설정하면 좋을지 알기 어렵다.  하지만 virt machine 동작에 꼭 필요한 레지스터들은 많지 않았다. 다만 실제 HW에서는 추가 설정이 필요할 수도 있을 것 같다. 

설정 순서는 gicd->gicr->gicc로 하였다. 

1. Distributor / gicd설정

a. Disable GICD_CTRL

b. (GICv3 only) SPI에 대해 Secure/Non-secure group을 Non-secure group 1로 설정

    group status=1 GICD_IGROUPR

    group modifier=0, GICD_IGRPMODR

c. Route SPI interrupts to CPU0 ( GICD_ITARGETSR )

d. Interrpt trigger type설정 ( GICD_ICFGR ) 

    : SPI는 모두 level trigger로 설정, PPI는 모두 edge trigger로 설정

e. Enable GICD_CTRL


2. Redistributor / gicr설정(GICv3 or later only) 

a. SGI/PPI에 대해 Secure/Non-secure group을 Non-secure group1로 설정

b. Enable SGI/PPI : GICR_ISENABLER0

c. PPI Interrup trigger type설정 (GICR_ICFGR1)

Redistributor는 GICv3에서 도입되었기 때문에 GICv2에서는 설정이 필요하지 않다. 어떤 CPU로도 라우팅이 가능한 SPI는 gicd에서 설정하고, 실행 core가 결정된 SGI/PPI는 gicr에서 설정하는 것으로 보인다.  

3. CPU interface / gicc설정

a. GICC_PMR for GICv2, ICC_PMR_EL1 for GICv3  : Priority Mask설정

b. GICC_CTRL = 1 for GICv2,  ICC_IGRPEN1_EL1 = 1 for GICv3

- Priority Mask로 허용할 최저 interrupt 우선순위를 설정한다. 우선순위는 0~255인데 0이 가장 높다. 0xFF로 설정하면 대부분의 우선순위를 가지는 인터럽트가 모두 허용된다. 

- CPU interface register라고 불리는 gicc 레지스터는 GICv2에서는 memory mapped레지스터였지만, GICv3에서는 system register가 되어 access 하는 방법이 달라졌다. (mrs/msr사용)

- gcc compiler에서는 aarch64의 cpu interface register이름을 해석하지 못하므로 이를 적절히 해석해주는 매크로가 필요하다. 

ex)
#define ICC_IGRPEN1_EL1     S3_0_C12_C12_7


Timer Interrupt

- ARM generic timer의 INTID = 30이다. (PPI)
- Trigger type을 edge로 설정해주어야 동작함 - Redistributor설정 시 일괄 설정함

아래는 gic-version=3 옵션을 사용한 virt machine의 dts파일의 일부이다. 

385 »   timer {
386 »   »   interrupts = <0x01 0x0d 0x04 0x01 0x0e 0x04 0x01 0x0b 0x04 0x01 0x0a 0x04>;
387 »   »   always-on;
388 »   »   compatible = "arm,armv8-timer\0arm,armv7-timer";
389 »   };

- 매뉴얼 및 dtb파일에는 level trigger(0x4)로 표시되어 있음.  확인 필요. 
- Interrupt가 0x0d, 0x0e, 0x0b, 0x0a 4개가 표시되어 있는데, 이 번호가 PPI시작 주소인 16을 더해주면 29,30,27,26 이 된다. generic timer의 INTID는 30인데, 왜 DTS에는 4개 의 interrupt가 표시되는지 확인 필요. 

구현은 아래 내용을 참조하였다.
https://lowenware.com/blog/aarch64-gic-and-timer-interrupt

UART interrupt:

dtb파일에서  UART장치가 pl011이라는 것과 시작 주소가 0x9000000이라는 것,  INTID가 33이라는 것을 알 수 있음.   

308 »   pl011@9000000 {
309 »   »   clock-names = "uartclk\0apb_pclk";
310 »   »   clocks = <0x8000 0x8000>;
311 »   »   interrupts = <0x00 0x01 0x04>;
312 »   »   reg = <0x00 0x9000000 0x00 0x1000>;
313 »   »   compatible = "arm,pl011\0arm,primecell";
314 »   };

- dtb파일의 interrupt번호는 SPI의 번호이므로 INTID로 변환을 위해 32를 더해주어야 함.( 1 + 32 = 33(INTID)
- interrupt는 level trigger방식임. (0x04)


그 외 필요해 보이는 추가 설정:

gicd:

GICD_ICENABLER

GICD_ICPENDR

GICD_PRIORITY

GICD_CTLR( GICD_CTLR_ARE_NS | GICD_CTLR_EN_GR1_NS )

GICD_I ROUTER


gicr:

GICR_TYPER

GICR_IPRIORITY0

GICR_WAKER

GICR_ICPENDR0

GICR_ICENABLER0




댓글 없음: