디버깅:
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
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