16 분 소요

1.1: 라즈베리 파이 운영체제 또는 bare-metal “hello world” 도입

작은 bare-metal “hello world” 어플리케이션을 만들어 운영체제 개발을 시작해보려 합니다. 이번 챕터에서는 여러분들이 이미 Prerequisites과 모든 것들이 준비가 되 있는 상태라고 가정하겠습니다. 만약 준비가 되지 않았다면 지금이 그 준비를 할 때입니다.

이번 챕터를 진행하기에 앞서 명칭을 구성할 때 사용할 간단한 규칙을 세우겠습니다. README 파일을 보시면 전 과정이 레슨으로 나눠지는 것을 보실 수 있습니다. 각 레슨은 챕터라 불리는 각각의 파일들로 이뤄져 있습니다. 한 챕터는 제목과 함께 섹션들로 나눠집니다. 이러한 명명 규칙들로 다른 자료의 참조를 쉽게 할 수 있습니다.

여러분들이 집중해야 할 다른 요소는 이 과정이 많은 소스 코드 샘플들로 이뤄져 있다는 것입니다. 주로 완벽한 코드들이 제공되고 이를 줄 단위로 나열하면서 설명하기 시작합니다.

프로젝트 구조

각 레슨의 소스코드는 같은 구조를 가집니다. 이번 레슨의 소스 코드는 여기서 찾을 수 있습니다. 이 폴더의 핵심 요소들을 간단한게 살펴봅시다.

  1. Makefile 커널을 빌드하기 위해 make 유틸리티를 사용할 것입니다. 소스 코드를 어떻게 컴파일하고 링킹하는지에 대한 명령어들로 구성된 Makefile로 make의 동작을 설정합니다.
  2. build.sh or build.bat 만약 Docker를 이용하여 커널을 빌드하고 싶다면 이 파일들을 사용할 수 있습니다. 여러분들의 개인 컴퓨터에 컴파일러 툴체인이나 make 유틸리티를 설치할 필요가 없습니다.
  3. src 이 폴더는 모든 소스코드를 갖고 있습니다.
  4. include 모든 헤더 파일이 이 폴더에 있습니다.

Makefile

프로젝트의 Makefile을 더 상세하게 살펴봅시다. make 유틸리티의 주요한 목적은 프로그램의 어떤 부분들이 재 컴파일 되어야하는지를 자동으로 결정하고 그 부분들을 재컴파일할 명령어들을 생성하는 것입니다. make와 Makefile에 여러분이 익숙하시지 않다면 이 기사를 읽어보기를 권합니다. 첫 번째 레슨에서 사용되는 Makefile은 여기서 찾아보실 수 있습니다. 전체 Makefile은 아래에 있습니다.

ARMGNU ?= aarch64-linux-gnu

COPS = -Wall -nostdlib -nostartfiles -ffreestanding -Iinclude -mgeneral-regs-only
ASMOPS = -Iinclude 

BUILD_DIR = build
SRC_DIR = src

all : kernel8.img

clean :
    rm -rf $(BUILD_DIR) *.img 

$(BUILD_DIR)/%_c.o: $(SRC_DIR)/%.c
    mkdir -p $(@D)
    $(ARMGNU)-gcc $(COPS) -MMD -c $< -o $@

$(BUILD_DIR)/%_s.o: $(SRC_DIR)/%.S
    $(ARMGNU)-gcc $(ASMOPS) -MMD -c $< -o $@

C_FILES = $(wildcard $(SRC_DIR)/*.c)
ASM_FILES = $(wildcard $(SRC_DIR)/*.S)
OBJ_FILES = $(C_FILES:$(SRC_DIR)/%.c=$(BUILD_DIR)/%_c.o)
OBJ_FILES += $(ASM_FILES:$(SRC_DIR)/%.S=$(BUILD_DIR)/%_s.o)

DEP_FILES = $(OBJ_FILES:%.o=%.d)
-include $(DEP_FILES)

kernel8.img: $(SRC_DIR)/linker.ld $(OBJ_FILES)
    $(ARMGNU)-ld -T $(SRC_DIR)/linker.ld -o $(BUILD_DIR)/kernel8.elf  $(OBJ_FILES)
    $(ARMGNU)-objcopy $(BUILD_DIR)/kernel8.elf -O binary kernel8.img

이제 이 파일을 상세하게 살펴봅시다:

ARMGNU ?= aarch64-linux-gnu

이 Makefile은 한 변수를 선언하는 것으로 시작합니다. ARMGNU는 크로스 컴파일러 접두사(prefix)입니다. x86 환경에서 arm64 아키텍쳐를 위한 소스코드를 컴파일하기 때문에 크로스 컴파일러를 사용해야 합니다. 따라서 gcc 대신에 aarch64-linux-gnu-gcc를 사용합니다.

COPS = -Wall -nostdlib -nostartfiles -ffreestanding -Iinclude -mgeneral-regs-only
ASMOPS = -Iinclude 

COPSASMOPS는 각각 C와 어셈블러 코드를 컴파일 할 때 컴파일러에게 전해주는 옵션값들입니다. 이러한 옵션들은 짧은 설명이 필요합니다:

  • -Wall 모든 warnings을 보여줍니다.

  • -nostdlib C 표준 라이브러리를 사용하지 않습니다. C 표준 라이브러리안에 대부분의 함수 호출들은 최종적으로 운영체제와 상호작용합니다. bare-metal 프로그램을 작성하고 있고 기반하는 운영체제가 없기 때문에 C 표준 라이브러리는 우리에게 작동하지 않을 것입니다.

  • -nostartfiles 표준 startup 파일들은 사용하지 않습니다. startup 파일들은 초기 스택 포인터(setting an initial stack pointer)를 설정하고 정적 데이터를 초기화(initializing static data)하고 메인 엔트리 포인트로 점프시켜(jumping to the main entry point)주는 역할을 합니다. 이러한 모든 작업을 이 파일을 사용하지 않고 자체적으로 수행합니다.

  • -ffreestanding freestandig 환경은 표준 라이브러리가 존재하지 않고 프로그램이 메인에서 필수적으로 시작하지 않는 환경입니다. -ffreestanding 옵션은 컴파일러가 표준 함수들이 일반적인 정의를 가진다고 가정하지 않게 해줍니다.(The option -ffrestanding directs the compiler to not assume that standard functions have their usual definition)

  • -linclude include 폴더에서 헤더파일을 찾습니다.

  • -mgeneral-regs-only 오로지 general-purpose 레지스터만 사용합니다. ARM 프로세서들은 NEON 레지스터 또한 가집니다. 이 레지스터들을 사용하게 되면 더 복잡해지기 때문에 컴파일러가 사용하지 않게 합니다.(예를 들어 context switch시 해당 레지스터들을 저장해야 합니다)

BUILD_DIR = build
SRC_DIR = src

SRC_DIRBUILD_DIR은 각각 소스코드와 컴파일된 오브젝트 파일을 가지는 디렉토리들입니다.

all : kernel8.img

clean :
    rm -rf $(BUILD_DIR) *.img 

다음으로 make 타겟을 정의합니다. 첫 두개의 타겟은 아주 간단합니다: all 타겟은 기본적인 타겟이고 어떤 인수들 없이 make를 실행할 때 실해될 타겟입니다.(make는 항상 기본값으로 첫번째 타겟을 사용합니다). 이러한 타겟은 단순히 모든 작업을 다른 타겟인 kernel8.img로 리다이렉트 합니다. clean 타겟은 모든 컴파일 부산물과 컴파일된 커널 이미지를 삭제하는 역할을 담당합니다.

$(BUILD_DIR)/%_c.o: $(SRC_DIR)/%.c
    mkdir -p $(@D)
    $(ARMGNU)-gcc $(COPS) -MMD -c $< -o $@

$(BUILD_DIR)/%_s.o: $(SRC_DIR)/%.S
    $(ARMGNU)-gcc $(ASMOPS) -MMD -c $< -o $@

다음 두 타겟들은 C와 어셈블러 파일들을 컴파일을 담당합니다. 예를 들어서 만약 src 디렉토리에 foo.cfoo.S 파일들이 있다면 각각 build/foo_c.obuild/foo_s.o로 컴파일됩니다. $<$@는 실행 시에 입력과 출력 파일 이름으로 대체됩니다. C 파일들을 컴파일하기 전에 build 디렉토리가 존재하지 않다면 생성하기도 합니다.

C_FILES = $(wildcard $(SRC_DIR)/*.c)
ASM_FILES = $(wildcard $(SRC_DIR)/*.S)
OBJ_FILES = $(C_FILES:$(SRC_DIR)/%.c=$(BUILD_DIR)/%_c.o)
OBJ_FILES += $(ASM_FILES:$(SRC_DIR)/%.S=$(BUILD_DIR)/%_s.o)

여기서 모든 오브젝트 파일의 배열인 OBJ_FILES가 C와 소스파일의 연속으로 생성됩니다.

DEP_FILES = $(OBJ_FILES:%.o=%.d)
-include $(DEP_FILES)

다음 2 줄은 약간 교묘합니다. 만약 C와 어셈블러 소스 파일을 위한 타겟의 컴파일을 어떻게 정의했는지 살펴본다면 -MMD 인수가 사용되는 것을 확인할 수 있습니다. 이 인수는 gcc 컴파일러가 각각의 생성된 오브젝트 파일을 위한 의존 파일(dependency file)을 생성하게 합니다. 한 의존 파일은 하나의 특정 소스 파일을 위한 모든 의존성을 정의합니다. 이러한 의존성은 주로 모든 포함된(included) 헤더들의 목록을 포함합니다. 헤더가 바뀔 경우 어떤 것을 재컴파일해야 하는지 알기 위해서 생성된 모든 의존 파일(dependency file)을 포함해야 합니다.

$(ARMGNU)-ld -T $(SRC_DIR)/linker.ld -o kernel8.elf  $(OBJ_FILES)

OBJ_FILES 배열을 kernel8.elf 파일을 빌드하기 위해 사용합니다. 생성된 실행 파일의 기초 레이아웃을 정의하기 위해 src/linker.ld링커 스크립트를 사용합니다.(링커 스크립는 다음 섹션에서 다루겠습니다)

$(ARMGNU)-objcopy kernel8.elf -O binary kernel8.img

kernel8.elfELF 파일 포맷입니다. 문제는 ELF 파일들은 운영체제에 의해 동작되게 설계되 있다는 것입니다. bare-metal 프로그램을 작성하기 위해서 실행파일(all executable)과 데이터 섹션(data section)을 ELF 파일에서 추출하고 kernel8.img에 넣어야 합니다. 뒤따라오는 8은 64-bit 아키텍쳐인 ARMv8을 나타냅니다. config.txt 파일안에 arm_control=0x200 플래그를 사용하여도 CPU를 64-bit 모드로 부팅시킬 수 있습니다. RPI 운영체제는 이전에 이러한 방식을 사용했고 몇개의 예시에서 이런 방법을 사용하는 것을 확인할 수 있다. 그러나 arm_control 플래그는 문서화 되있지 않고 kernel8.img 명명 규칙을 대신 사용하는 것이 선호된다.

The linker script

링커 스크립트의 주요 목적은 입력 오브젝트 파일들(_c.o,_s.o)의 섹션들이 출력 파일(.elf)에 어떻게 맵핑 되어야 하는지 서술하는 것이다. 링커 스크립트에 대해 더 자세한 정보는 여기서 찾을 수 있다. 그럼 RPi 운영체제의 링커 스크립트를 살펴봅시다:

SECTIONS
{
    .text.boot : { *(.text.boot) }
    .text :  { *(.text) }
    .rodata : { *(.rodata) }
    .data : { *(.data) }
    . = ALIGN(0x8);
    bss_begin = .;
    .bss : { *(.bss*) } 
    bss_end = .;
}

시작된 후(after startup) 라즈베리 파이는 kernel8.img를 메모리에 로드하고 파일의 시작부터 실행시키기 시작합니다. 이것이 .text.boot 섹션이 처음으로 와야하는 이유입니다; 운영체제 startup code를 이 섹션안에 넣을 것입니다. .text, .rodata, .data 섹션들은 커널 컴파일 명령어(kernel-compiled instructions), read-only 데이터, 그리고 일반적인 데이터(normal data)를 포함합니다.-그들과 관련된 것을 추가하는 것은 특별할게 없습니다.(there is nothing special to add about them). .bss 섹션은 0으로 반드시 초기화 되어야 하는 데이터들을 가집니다. 이러한 데이터들을 분리된 섹션에 넣음으로서 컴파일러는 ELF 바이너리안에 어떤 공간을 절약할 수 있습니다.-ELF 헤더안에서는 섹션 사이즈만 저장되고 섹션자체는 생략한다. 메모리안에 이미지를 로드한 후에 .bss 섹션을 0으로 초기화해줘야 한다.; 따라서 섹션의 시작과 끝(bss_begin, bss_end symbols)을 기록해야하고 섹션을 정렬(align)해서 8의 배수로 주소가 시작되게 한다. str 명령어가 오로지 8-byte-aligned 주소로만 사용될 수 있기 때문에 만약 섹션이 정렬되지 않는다면 bss 섹션의 시작부터 str 명령어를 이요하여 0으로 초기화하는 것이 어려울 것입니다.

Booting the kernel

이제 boot.S 파일을 살펴봅시다. 이 파일은 커널 시작 코드(kernel startup code)를 포함합니다.:

#include "mm.h"

.section ".text.boot"

.globl _start
_start:
    mrs    x0, mpidr_el1        
    and    x0, x0,#0xFF        // Check processor id
    cbz    x0, master        // Hang for all non-primary CPU
    b    proc_hang

proc_hang: 
    b proc_hang

master:
    adr    x0, bss_begin
    adr    x1, bss_end
    sub    x1, x1, x0
    bl     memzero

    mov    sp, #LOW_MEMORY
    bl    kernel_main

이 파일을 자세히 리뷰해보겠습니다.:

.section ".text.boot"

우선 boot.S안에 정의된 모든 것들이 .text.boot 섹션에 들어가야 한다는 것을 명시합니다. 이전에 이 섹션이 링커 스크립트에 의해서 커널 이미지의 시작에 위치된다는 것을 볼 수 있었습니다. 따라서 커널이 시작될 때 동작은 start 함수에서 시작됩니다.:

.globl _start
_start:
    mrs    x0, mpidr_el1        
    and    x0, x0,#0xFF        // Check processor id
    cbz    x0, master        // Hang for all non-primary CPU
    b    proc_hang

이 함수가 하는 첫 번째 일은 프로세서 아이디(processor ID)를 확인하는 것입니다. 라즈베리 파이 3은 4개의 코어 프로세서를 가지고 있고 기기(device)가 켜지고 각각의 코어는 같은 코드를 실행시키기 시작합니다. 그러나, 4개의 코어를 다루고 싶지 않습니다.; 첫 번째 코어만 다루고 나머지 코어는 무한 루프 상태로 넣습니다. 이것이 정확하게 _start 함수가 맡은 역할입니다. mpidr_el1 시스템 레지스터로 부터 프로세서 ID를 얻습니다. 만약 현재 프로세스 ID가 0이면 master 함수로 실행이 넘어가게 됩니다.:

master:
    adr    x0, bss_begin
    adr    x1, bss_end
    sub    x1, x1, x0
    bl     memzero

여기서 memzero 함수를 호출하여 .bss 섹션을 청소합니다. 이 함수는 나중에 정의 내릴 것입니다. ARMv8 구조에서는 문법적(by convention)으로 첫 일곱개의 인수들을 레지스터 x0 ~ x6를 통해 호출된 함수에게 전달합니다. memzero 함수는 오로지 2개의 인수만을 받습니다; 시작 주소(bss_begin)와 청소되야 할 섹션의 사이즈(bss_end - bss_begin).

mov    sp, #LOW_MEMORY
    bl    kernel_main

.bss 섹션을 청소한 후 스택 포인터를 초기화하고 kernel_main 함수로 실행을 넘겨줍니다. 라즈베리파이는 커널을 주소 0에 업로드합니다; 따라서 초기 스택 포인터가 어떤 위치로 설정될 수 있고 스택이 많이 커질 때 커널 이미지를 덮어쓰지 않습니다. LOW_MEMORYmm.h에 정의되 있고 4MB와 동일합니다. 이 커널의 스택은 그리 크게 커지지 않고 이미지 자체가 작아 4MB로도 충분합니다.

ARM 어셈블러 문법이 익숙하지 않은 여러분들을 위해 사용된 명령어들을 빠르게 요약해보겠습니다.:

  • mrs 시스템 레지스터에서 일반 레지스터(general purpose registers, x0 ~ x30)으로 값을 로드합니다.

  • and 논리 AND 연산을 수행합니다. mpidr_el1 레지스터에서 얻은 값에서 마지막 바이트를 벗기기 위해 이 명령어를 사용합니다.

  • cbz 이전에 실행된 수행결과와 0을 비교한 후 비교가 참을 반환하면 제공된 라벨로 점프(branch in ARM terminology)합니다.

  • b 특정 라벨로 무조건적인 건너뜀을 수행합니다.

  • adr 타겟 레지스터로 라벨의 상대 주소를 로드합니다. 이 경우에는 .bss 지역의 시작과 끝을 지정했습니다.

  • sub 두 개의 레지스터에서 값을 뺍니다.

  • bl “Branch and link”: 무조건적인 건너뜀을 수행하고 반환 주소(return address)를 x30(link register)에 저장합니다. 서브루틴이 끝나면 ret 명령어를 이용하여 반환 주소로 돌아갑니다.

  • mov 레지스터간 또는 레지스에 상수로 부터 값을 옮깁니다.

여기에 ARMv8-A 개발자 가이드가 있습니다. ARM ISA가 익숙하지 않다면 이것은 좋은 자료입니다. 이 페이지는 특히 ABI안에 레지스터 사용 관습의 개요를 설명합니다.

The kernel_main function

부트 코드가 결국 kernel_main 함수로 제어를 넘겨주는 것을 볼 수 있었습니다. 이 함수를 살펴봅시다:

#include "mini_uart.h"

void kernel_main(void)
{
    uart_init();
    uart_send_string("Hello, world!\r\n");

    while (1) {
        uart_send(uart_recv());
    }
}

이 함수는 커널에서 가장 간단한 것 중 하나입니다. 사용자 입력값을 일고 화면에 출력하기 위해 Mini UART 기기를 이용합니다. 커널은 단순히 Hello, world!를 출력하고 사용자 입력값을 화면으로 출력하는 무한 루프로 들어가게 됩니다.

Raspberry Pi devices

이제 라즈베리 파이에 특정된 것으로 들어가봅시다. 시작하기 전에 여러분들이 BCM2837 ARM Peripherals 메뉴얼. BCM2837는 라즈베리 파이 3 모델 B, B+에서 사용되는 보드입니다. 이 과정내에서 가끔 BCM2835과 BCM2836이 언급되기도 할 것입니다.-라즈베리 파이의 이전 버전들에서 사용된 보드들입니다.

구현 상세에 들어가기전에 메모리 매핑된 기기(memory-mapped devices)를 어떻게 다루는지 기본적인 개념을 다루겠습니다. BCM2837은 간단한 SOC(System on a chip) 보드입니다. 이러한 보드에서 모든 기기에 접근이 메모리 매핑된 레지스터(memory-mapped registers)에 의해서 수행됩니다. 라즈베리 파이 3은 기기들을 위해 0x3F000000 주소 위에 메모리들을 선점해놓았습니다. 특정 기기를 활성화하거나 설정하기 위해서는 디바이스의 레지스터에 데이터를 적어야합니다. 한 디바이스 레지스터(device register)는 단순한 32-bit 메모리 영역입니다. 각각의 디바이스 레지스터안에 각 비트의 의미는 BCM2837 ARM Peripherals 메뉴얼에 설명되있습니다. 메뉴얼에 섹션 1.2.3 ARM physical address을 살펴보고 0x3F000000을 기준 주소(base address)로 사용하는지에 대한 상세한 이유를 위해서는 기타 문서들(surrounding documentation)을 살펴보세요(비록 0x7#00000이 메뉴얼내에서 사용되더라도).

kernel_main 함수로 부터, 이제 Mini UART 기기를 다룰 것이라는 것을 추측할 수 있습니다. UART는 Universal asynchronous receiver-transmitter 를 나타냅니다. 이러한 기기는 메모리 매핑된 레지스터에 저장된 변수를 high와 low 전압의 연속(sequence)으로 변환할 수 있습니다. 이러한 연속(sequence)은 TTL-to-serial cable을 통해 컴퓨터로 전송되고 터미널 에뮬레이터(terminal emulater)에 의해 해석됩니다. 앞으로 라즈베리 파이와 소통하기 위해서 Mini UART를 사용할 것입니다. 만약 Mini UART 레지스터들의 상세사항을 보고 싶다면 BCM2837 ARM Peripherals 메뉴얼의 8 페이지로 가세요.

라즈베리 파이는 2개의 UART를 가집니다.:Minu UART, PL011 UART. 이 과정에서는 전자가 더 간단하기 때문에 전자를 활용할 것입니다. 하지만 PL011 UART를 어떻게 다루는지 보여주는 추가적인 활동도 있습니다. 만약 라즈베리 파이 UART에 대해 더 알고 싶고 그들의 차이를 배우고 싶다면 공식 문서를 참조할 수 있습니다.

여러분들이 친해져야 할 또 다른 기기는 GPIO General-purpose input/output입니다. GPIO는 GPIO pins을 제어하는 역할을 합니다. 아래의 이미지에서 GPIO 핀들을 쉽게 인지할 수 있어야 합니다.:

gpio-pins

GPIO는 다른 GPIO 핀들의 동작ㅇ을 설정하기 위해서 사용될 수 있습니다. 예를 들어 Mini UART를 사용하기 위해서는 14, 15번 핀을 활성화하고 이 기기를 사용하기 위해 핀으 up으로 설정해야 한다. 아래 이미지는 GPIO 핀들에 번호가 어떻게 할당되는 지 표현한 것입니다.:

gpio-numbers

Mini UART initialization

이제 Mini UART를 어떻게 초기화하는지 살펴봅시다. 이러한 코드는 mini_uart.c안에 정의되 있습니다.

void uart_init ( void )
{
    unsigned int selector;

    selector = get32(GPFSEL1);
    selector &= ~(7<<12);                   // clean gpio14
    selector |= 2<<12;                      // set alt5 for gpio14
    selector &= ~(7<<15);                   // clean gpio15
    selector |= 2<<15;                      // set alt5 for gpio 15
    put32(GPFSEL1,selector);

    put32(GPPUD,0);
    delay(150);
    put32(GPPUDCLK0,(1<<14)|(1<<15));
    delay(150);
    put32(GPPUDCLK0,0);

    put32(AUX_ENABLES,1);                   //Enable mini uart (this also enables access to it registers)
    put32(AUX_MU_CNTL_REG,0);               //Disable auto flow control and disable receiver and transmitter (for now)
    put32(AUX_MU_IER_REG,0);                //Disable receive and transmit interrupts
    put32(AUX_MU_LCR_REG,3);                //Enable 8 bit mode
    put32(AUX_MU_MCR_REG,0);                //Set RTS line to be always high
    put32(AUX_MU_BAUD_REG,270);             //Set baud rate to 115200

    put32(AUX_MU_CNTL_REG,3);               //Finally, enable transmitter and receiver
}

여기서 put32get32 두 개의 함수를 사용합니다. 이 함수들은 매우 간단합니다: 이 함수들은 32-bit 레지스터에서 데이터를 읽어오거나 쓰기를 할 수 있게 해줍니다. 이 함수들이 어떻게 구현되어 있는지는 utils.S을 살펴보실 수 있습니다. uart_init은 이번 레슨에서 가장 복잡하고 중요한 함수 중 하나 이며 다음 3 섹션에서 계속 시험을 할 것입니다.

GPIO alternative function selection

첫 번째로 GPIO 핀들을 활성화시켜야 합니다. 대부분의 핀들은 다른 디바이스들과 사용될 수 있기 때문에 특정 핀을 사용하기 전에 핀의 alternative function을 선택해야합니다. alternative function은 각각의 핀이 설정될 수 있는 단순한 0부터 5까지의 숫자이고 어떤 기기가 핀에 연결되는지를 설정합니다. 아래 이미지에서 모든 가능한 GPIO alternative function드을 보실 수 있습니다.

alt

여기서 14와 15핀이 TXD1와 RXD1 alternative function을 가지는 것을 보실 수 있습니다. 이것은 만약 14와 15번 핀에 alternative function으로 5를 선택한다면 핀들이 Mini UART Transmit Data 핀과 Mini UART Receive Data pin으로 각각 사용될 것임을 의미합니다. GPFSEL1 레지스터는 10 ~ 19 핀들의 alternative functions들을 제어하기 위해 사용됩니다. 이 레지스터안에 모든 비트들의 의미는 아래 테이블에 나와 있습니다.:

gpfsel1

이제 Mini UART 기기를 사용하기 위해 14,15번 GPIO 핀을 설정하기 위해서 사용되는 아래 코드들을 이해하기 위해 필요한 모든 것들을 알게 되었습니다.:

    unsigned int selector;

    selector = get32(GPFSEL1);
    selector &= ~(7<<12);                   // clean gpio14
    selector |= 2<<12;                      // set alt5 for gpio14
    selector &= ~(7<<15);                   // clean gpio15
    selector |= 2<<15;                      // set alt5 for gpio 15
    put32(GPFSEL1,selector);

GPIO pull-up/down

라즈베리 파이 GPIO 핀을 다룰때 pull-up/pull-down과 같은 용어들을 가끔 볼 수 있을 것입니다. 이 개념들은 이 기사에 자세하게 설명되있습니다. 전체 기사를 읽기에는 너무 게으른 이들을 위해서 간단하게 pull-up/pull-down 개념을 설명하겠습니다.

만약 특정 핀을 입력으로 사용하고 이 핀에 아무것도 연결하지 않는다면 핀의 값이 1인지 0인지 식별할 수 없을 것입니다. pull-up/pull-down 방식은 이러한 이슈를 해결하게 해줍니다. 만약 핀을 pull-up 상태로 설정하고 아무것도 연결이 되지 않았다면 항상 1을 보고(report)할 것입니다.(pull-down 상태에서는 항상 값이 0이 됩니다). 우리의 경우에는 14와 15번 핀이 항상 기기에 연결될 것이기 때문에 둘 다 필요가 없습니다. 핀 상태는 재부팅 후에도 보존되기에 어떤 핀을 사용하기 전에 항상 핀의 상태를 초기화해야 합니다. 3개의 가능한 상태들이 있습니다:pull-up, pull-down, neither(현재의 pull-up 또는 pull-down 상태를 지우기 위한) 그리고 우리는 3번째 상태가 필요합니다.

핀 상태를 바꾸는 것은 물리적으로 전자 회로에 스위치를 바꿔야(toggling)하기 때문에 간단한 절차는 아닙니다. 이러한 과정은 GPPUDGPPUDCLK레지스터를 포함하고 BCM2837 ARM Peripherals 메뉴얼의 101 페이지에 설명되 있습니다. 설명을 여기 복사해놓았습니다.:

The GPIO Pull-up/down Clock Registers control the actuation of internal pull-downs on
the respective GPIO pins. These registers must be used in conjunction with the GPPUD
register to effect GPIO Pull-up/down changes. The following sequence of events is
required:
1. Write to GPPUD to set the required control signal (i.e. Pull-up or Pull-Down or neither
to remove the current Pull-up/down)
2. Wait 150 cycles – this provides the required set-up time for the control signal
3. Write to GPPUDCLK0/1 to clock the control signal into the GPIO pads you wish to
modify – NOTE only the pads which receive a clock will be modified, all others will
retain their previous state.
4. Wait 150 cycles – this provides the required hold time for the control signal
5. Write to GPPUD to remove the control signal
6. Write to GPPUDCLK0/1 to remove the clock

이러한 절차는 아래 코드에서 14, 15 핀에 하는 핀으로 부터 pull-up과 pull-down 상태를 삭제하는 방법을 설명합니다.:

put32(GPPUD,0);
    delay(150);
    put32(GPPUDCLK0,(1<<14)|(1<<15));
    delay(150);
    put32(GPPUDCLK0,0);

initializing the Mini UART

이제 Mini UART는 GPIO 핀에 연결되 있으며 핀들이 설정되 있습니다. uart_init 함수의 나머지는 Mini UART 초기화에 할애됩니다.

put32(AUX_ENABLES,1);                   //Enable mini uart (this also enables access to its registers)
    put32(AUX_MU_CNTL_REG,0);               //Disable auto flow control and disable receiver and transmitter (for now)
    put32(AUX_MU_IER_REG,0);                //Disable receive and transmit interrupts
    put32(AUX_MU_LCR_REG,3);                //Enable 8 bit mode
    put32(AUX_MU_MCR_REG,0);                //Set RTS line to be always high
    put32(AUX_MU_BAUD_REG,270);             //Set baud rate to 115200
    put32(AUX_MU_IIR_REG,6);                //Clear FIFO

    put32(AUX_MU_CNTL_REG,3);               //Finally, enable transmitter and receiver

이 코드를 줄 단위로 살펴봅시다.

put32(AUX_ENABLES,1);                   //Enable mini uart (this also enables access to its registers)

이 줄은 Mini UART를 가능하게 합니다. 이 코드가 모든 Mini UART 레지스터에 접근을 가능하게 하기 때문에 이 코드를 처음에 수행해야 합니다.

put32(AUX_MU_CNTL_REG,0);               //Disable auto flow control and disable receiver and transmitter (for now)

여기서 설정이 끝나기 전에 리시버와 송신기를 비활성화합니다. 또한 auto-flow control은 추가적인 GPIO 핀 사용이 요구되고 TTL-to-serial 케이블이 지원을 하지 않기 때문에 영구적으로 비활성화합니다. auto-flow control에 부가적인 정보는 이 기사를 참조하실 수 있습니다.

put32(AUX_MU_IER_REG,0);                //Disable receive and transmit interrupts

Mini UART가 새로운 데이터가 활성화 됬을 때마다 프로세서 인터럽트를 발생시키게 설정할 수 있습니다. 레슨 3에서 인터럽트를 다룰 것이기 때문에 지금은 그냥 이 기능을 비활성화 합니다.

put32(AUX_MU_LCR_REG,3);                //Enable 8 bit mode

Mini UART는 7-또는 8-bit 명령어를 지원합니다. 표준 셋을 위한 ASCII 캐릭터가 7 bit이고 확장 셋을 위한 ASCII 캐릭터는 8 bit이기 때문입니다. 우리는 8-bit 모드를 사용할 것입니다.

put32(AUX_MU_MCR_REG,0);                //Set RTS line to be always high

RTS 줄은 흐름 제어(flow control)에서 사용되고 필요가 없습니다. 항상 high로 설정합니다.

put32(AUX_MU_BAUD_REG,270);             //Set baud rate to 115200

전송 속도 비율(baud rate)는 통신 채널에서 데이터가 전송되는 비율입니다. “115200 baud”는 해당 시리얼 포트가 최대 1초당 115200 비트를 전송할 수 있다는 것을 의미합니다. 라즈베리 파이 Mini UART 기기의 전송 속도 비율은 터미널의 전송 속도 비율과 일치해야합니다. Mini UART는 아래의 식에 따라 전송 속도 비율을 계산합니다.

baudrate = system_clock_freq / (8 * ( baudrate_reg + 1 )) 

system_clock_freq은 250MHz이기에 쉽게 baudrate_reg를 270으로 계산할 수 있습니다.

put32(AUX_MU_CNTL_REG,3);               //Finally, enable transmitter and receiver

이 줄이 실행된 후 Mini UART는 동작할 준비가 되었습니다!

Sending data using the Mini UART

Mini UART가 준비된 후 데이터를 주고받기 위해 활용할 수 있습니다. 이를 위해서 아래 두 함수를 사용할 수 있습니다:

void uart_send ( char c )
{
    while(1) {
        if(get32(AUX_MU_LSR_REG)&0x20) 
            break;
    }
    put32(AUX_MU_IO_REG,c);
}

char uart_recv ( void )
{
    while(1) {
        if(get32(AUX_MU_LSR_REG)&0x01) 
            break;
    }
    return(get32(AUX_MU_IO_REG)&0xFF);
}

기기가 데이터를 전송하고 수신할 준비가 되있는지 확인하기 위한 목적으로 두 함수 모두 무한 루프로 시작합니다. 이를 위해서 AUX_MU_LSR_REG 레지스터를 사용합니다. 만약 0번째 비트가 1로 설정되 있다면 비트 0은 데이터가 준비되 있다는 것을 가르킵니다; UART로 부터 데이터를 읽을 수 있다는 것을 의미합니다. 5번 째 비트가 1로 설정되 있따면 송신기(transmitter)가 비어있어 UART로 전송할 수 있다는 것을 의미합니다. 다음으로 AUX_MU_IO_REG를 전송된 문자를 저장하거나 수신한 문자의 값을 읽어오는 것에 사용합니다.

또한 문자들을 보내는 대신에 문자열을 보낼 수 있는 매우 간단한 함수도 있습니다.:

void uart_send_string(char* str)
{
    for (int i = 0; str[i] != '\0'; i ++) {
        uart_send((char)str[i]);
    }
}

이 함수는 단순히 문자열내에 모든 문자들을 순환하고 하나씩 문자를 전송합니다.

Raspberry Pi config

라즈베리 파이의 스타트 업 순서(startup sequenc)는 아래와 같습니다(간단하게 서술함):

  1. 기기에 전원이 들어옵니다.
  2. GPU가 시작되고 부트 파티션에서 config.txt파일을 읽습니다. 이 파일은 GPU가 스타트업 과정(startup sequence)을 더 조정하기 위해 사용하는 설정 인자(configuration parameters)들을 포함합니다.
  3. kernel8.img는 메모리에 로드되고 실행됩니다.

간단한 운영체제를 실행시키기 위해서 config.txt 파일은 아래와 같아야 합니다.:

kernel_old=1
disable_commandline_tags=1

  • kernel_old=1은 커널 이미지가 0번째 주소에 로드되어야 한다는 것을 식별합니다.
  • disable_commandline_tags는 GPU가 어떤 커맨드라인 인수도 부팅된 이미지에 보내지 않게 합니다.

Testing the kernel

이제 모든 소스 코드를 살펴봤고 작동하는지 살펴봐야할 시간입니다. 커널을 빌드하고 시험하기 위해서 해야할 일은 아래와 같습니다:

  1. 커널을 빌드하기 위해 src/lesson01 안에 ./build.sh또는 ./build.bat 실행하세요
  2. 생성된 kernel8.img 파일을 라즈베리 파이 플래시 카드의 boot 파티션에 복사하고 kernel7.img를 삭제하세요. 부트 파티션내에 다른 파일들은 건들이지 말아야 합니다.(자세한 사항은 여기 이슈를 봐주세요)
  3. config.txt 파일을 이전 섹션에 서술된 것처럼 수정하세요
  4. USB-to_TTL serial 케이블을 Prerequisites에서 서술된 것처럼 연결하세요
  5. 라즈베리 파이를 키세요
  6. 터미널을 열면 Hello, World! 메시지를 볼 수 있어야 합니다.

위에 설명된 단계들은 SD 카드에 라즈비안이 설치되있다고 가정한다는 것을 알아야 합니다. 빈 SD 카드를 이용해 RPi OS를 구동하는 것 또한 가능합니다.

  1. SD 카드를 준비합니다:

    • MBR 파티션 테이블을 사용합니다

    • 부트 파티션을 FAT32로 포맷합니다.

      카드는 라즈비안을 설치할 때 요구되는 것과 같은 방식으로 정확하게 포맷되어야 합니다. 공식 문서HOW TO FORMAT AN SD CARD AS FAT 섹션을 살펴보세요.

  2. 아래 파일들을 카드로 복사합니다:

    • bootcode.bin 이것은 GPU 부트로더이며 GPU를 시작하고 GPU 펌웨어를 로드하는 코드로 이뤄져 있습니다.
    • start.elf 이것은 GPU 펌웨어입니다. config.txt를 읽고 GPU가 ARM 전용 유저 코드 형태를 로드하고 실행시킬 수 있게 합니다.
  3. kernel8.imgconfig.txt 파일을 복사합니다.

  4. USB-to-TTL 시리얼 케이블을 연결합니다

  5. 라즈베리 파이를 킵니다

  6. 터미널을 사용하여 RPi 운영체제에 연결합니다

안타깝게 모든 라즈베리 파이 펌웨어 파일이 공개되지 않고(closed-sourced) 문서화되있지 않습니다. 라즈베리 파이 스타트업 과정에 더 많은 정보를 위해서는 StackExchange 또는 Github과 같은 비공식적인 출처를 참조할 수 있습니다.

태그:

카테고리:

업데이트:

댓글남기기