티스토리 뷰

컴퓨팅 시스템

OS from scratch

jiggyjiggy 2024. 4. 30. 10:08

들어가며

운영체제(Operating System, OS)는 현대의 컴퓨팅 시스템을 이루는 요소 중 가장 중요한 요소라 해도 과언이 아니다.

컴퓨터 공학, 전자공학 등 관련학과에서는 필수과목이다.

운영체제 교과서로 널리 쓰이는 일명 "공룡책"

위 책은 OS를 공부했다면 한번쯤 본적 있을 것이다.

아카데믹한 이론을 학습하기에 좋은 자료이다.

하지만, 글이다. 구현해보고싶지 않은가? 아니라면, 코드가 궁금하지 않은가?

 

코드가 궁금하다면, 구현체를 살펴보면 된다. 운영체제의 구현체들은 수없이 많다.

그 중 가장 유명한 Linux, Windows, MacOS 등, 여러 상용 운영체제가 존재하며, 이 중 Linux가 오픈소스로 공개되어있다.

 

하지만, 내부를 보려고 들어가면 수천줄의 코드와 분리되어있는 수많은 파일들에 압도당할 것이다.

이론은 알지만, 코드분석까지의 괴리감이 느껴질 것이다.

 

해당 포스팅에서는 간단하면서도 핵심 역할을 하는 OS를 직접 만들어볼 것이다.

동작하는 OS를 만들어보면, 깨닫는 깊이가 달라질 것이다.

Course

임베디드 OS 개발 프로젝트 / 이만우 저자(글)

도서 "임베디드 OS 개발 프로젝트 / 이만우 저자(글)"를 기반으로 따라간다.

"임베디드", "ARM 기반 펌웨어", "RTOS" 라는 부가적인 키워드가 있다. 

다른 키워드는 그렇다 해도, 임베디드 개발자용인가? 임베디드가 뭐길래? 라는 의문이 들수 있다. 

HW를 SW로 제어하고, OS라 불릴 수 있는 핵심 기능을 구현한다고 인지하고 들어가자.

 

해당 도서에서는 ARM 기반의 HW에뮬레이터를 이용하여 HW를 대체한다. 그리고 그 위에 OS를 만들어본다.

더욱 자세한 설명은 책을 참조하는게 좋을 것이지만, 본 포스팅만 보더라도 충분한 인사이트를 가질 수 있을 것이다.

Course 요구 조건

  • C언어
    • 포인터로 작성한 코드를 보고 어떠한 동작을 하는지 이해할 수 있으면 된다. 
  • ARM assembly 언어
    • 알고있었다면 편하겠으나, 모르더라도 프로젝트를 진행하면 무슨 의미인지 파악할 수 있는 정도의 난이도이다.
  • HW
    • 데이터 시트를 보고 레지스터별 메모리 주소와 설명을 보고 무슨 의미인지 이해할 수 있으면 된다.
    • SW는 HW위에서 동작한다. SW 중에서도 OS는 HW와 가장 맞닿은 부분이다. HW에 대한 이해는 필수라고 생각한다.
      • HW 내부를 알고싶다면  CPU from scratch를 추천한다. CPU를 논리회로부터 만드는 경험을 담은 포스팅이다.

🚧 시작 Mark

해당 프로젝트는 크게는 2부, 그리고 2부는 소주제로 2개로 나눌 수 있다

  • 1부: HW 제어에 관한 "펌웨어" (3~7장)
    • 빌드 환경 구성, 부트로더, 주변장치제어, 타이머
  • 2부: OS 중에서도 "RTOS"를 만족하는 구현
    • 2-1부: 태스크, 스케줄러, 컨텍스트 스위칭을 만들어서 RTOS다운 기능 만들기 (8~10장)
    • 2-2부: 메시지와 동기화 기능으로 멀티코어 환경에 필수적인 기능 만들기 (11~13장)

우선 중간 Remark 전까지, 밑바닥부터 잘 동작하는 펌웨어를 만드는 과정을 설명한다. 


개발환경 사전 설명

🔥 책에서와는 다른 부분이 있다. 버전이나 패키지를 실습환경에 맞게 바꾸었다.

진행 환경

도커(Docker)를 이용하여, 리눅스를 사용하자.

프로젝트를 진행한 환경은 아래와 같다. 

  • docker 4.29.0
  • ubuntu 20.04

유틸리티 및 패키지

Course를 따라 진행하다보면 설치할 유틸리티 및 패키지들을 미리 정리했다. 

 

필수

apt install gcc-arm-none-eabi
apt install qemu-system-arm
apt install bsdmainutils	
apt install gdb-multiarch
apt install make

 

추천

apt install bash-completion
apt install vim
apt install tree

1장 임베디드 운영체제

임베디드 컴퓨팅 장치를 자기 자신의 고유하고 한정된 기능을 지속적으로 수행하는 독립된 장비로 정의하자.
위와 같은 정의라면, 휴대용 장비뿐 아니라 자동차나 항공기, 대형 선박에 탑재한 제어 장치들도 임베디드 컴퓨팅 장치이다.

임베디드 운영체제와 펌웨어

임베디드 컴퓨팅 장치도 SW가 있어야 동작한다.

규모가 작거나 극단적인 최적화가 필요한 임베디드 장치는 OS없이 펌웨어로만 동작하기도 한다. 

그럼에도 시스템의 자원과 복잡도를 관리하는것이 더 중요하다면 임베디드 OS를 사용하는것이 일반적이다.

HW 성능이 좋아짐에따라 임베디드 OS를 사용해도 문제가 없기에 대부분 사용한다.

 

Linux나 Windows같은 대형 OS도 크게보면 PC에서 동작하는 SW이다. 마찬가지로 임베디드 OS도 임베디드 시스템에서 동작하는 펌웨어이다. 임베디드 OS를 포함하는 임베디드 시스템용 전체 SW를 펌웨어라 부른다.

RTOS

RTOS(RealTime Operating System)은 "실시간 운영체제"라고 번역한다. OS의 응답과 동작이 즉각적이고 실시간이어야 해서 붙여진 이름이다. 

임베디드 OS는 대부분 RTOS이지만, 아닌것도 존재한다. 실시간적 응답과 반응이 필수적이지 않은 시스템이 그러할 것이다.

나빌로스(navilos)

아주 작은 임베디드 OS, navilos를 만들어보자. 

에뮬레이터 개발환경

임베디드 SW개발시에는, 대상 HW가 필요하다. 적당한 실물 개발보드는 비싸므로, QEMU라는 에뮬레이터로 대체한다.


2장 개발 환경 구성하기

우분투 가동

도커(Docker)를 이용하여, 리눅스를 사용하자. (참고로, 우분투는 리눅스의 배포판이다.)

 

도커에 대한 설명은 생략하겠다.

docker run -d --name ubuntu -p 22:22 -it --privileged ubuntu:20.04

참고로 나는 docker desktop으로 진행했다.

docker desktop

exec 탭으로가면 터미널을 쓸수 있다. 

도커에서 리눅스를 기동했다면, apt-get update 를 먼저 해주자

apt-get update

 

bash를 사용할 것이고, 자동완성 기능을 쓸 것이다. 

apt install bash-completion

컴파일러 설치

apt install gcc-arm-none-eabi

cf) 컴파일러의 naming이 특이하다. gcc가 맨 뒤로 가있다. `arm-none-eabi-gcc`

 

패키지 이름을 보면 'gcc-arm-플랫폼-ABI타입' 형태이다. 패키지 이름에서 유추해볼 의미가 있다. 

 

플랫폼은 linux, none 두가지가 있다.

linux는 ARM용으로 동작하는 리눅스의 실행 파일을 만드는 것이 목적이라는 의미.

none은 플랫폼이 없다는 뜻이므로, raw한 ARM 바이너리를 생성한다는 의미.

 

ABI(Application Binary Interface)는 함수 호출을 어떻게 하느냐를 정해놓은 규약을 의미한다.

어떤 레지스터를 몇번째 파라미터에 배정하고 스택과 힙은 어떻게 쓰고 하는 것 등을 정해놓은 규약이다.

⭐️ ABI, EABI

ABI

바이너리 수준에서 애플리케이션이 호환 가능하도록 만든 인터페이스이다.
바이너리 수준이란 쉽게 말해 컴파일이 완료된 오브젝트 파일을 말한다. 
정리하면, 링커가 오브젝트 파일들을 링킹할 수 있도록 함수 호출 방법을 정의한 인터페이스라는 의미이다. 여기서 오브젝트 파일에는 라이브러리도 포함된다.
호출 방법이라는 폭넓은 용어를 사용했는데, 자세하게 정리해보자. 여기서 호출 방법은 실행 파일 형식, 자료형, 레지스터 사용, 스택 프레임 조직, 호출 규칙을 말한다.
실행 파일 형식 : 컴파일러가 생성하는 바이너리 파일의 구조를 정의한다자료형 : 프로그래밍 언어가 사용하는 자료형의 실제 크기를 정의한다레지스터 사용 : 파라미터와 로컬 변수가 레지스터를 몇 개나 사용하는지에 대한 내용을 정의한다스택 프레임 조직 : 스택을 어떻게 만들지 정의한다. 예를 들어 스택에 변수가 정의될 때 파라미터가 우선인지 로컬 변수가 우선인지와 같은 내용이 포함된다. 그리고 선언 순서대로 저장되는지 혹은 선언 순서의 반대로 저장되는지에 대한 내용이 정의되어 있다호출 규칙 : 함수의 인수가 전달되는 방식을 정의한다. 예를 들어 모든 파라미터가 스택으로 전달되는지 또는 일부는 레지스터를 사용하는지에 대한 정의이다.

EABI

Embedded ABI. 임베디드 환경에서 사용하는 ABI를 정해놓은 것이다. ABI는 OS에서 동작하는 실행 파일에 대한 폭넓은 정의까지도 모두 포함하고 있는 개념이므로 EABI라는 규약을 만들어 임베디드 환경에서의 ABI를 구분하고 있다.
EABI와 ABI의 가장 큰 차이점은 동적 링크의 지원 유무이다. Windows OS에서 확장자가 dll인 파일이나 Linux OS에서 확장자가 so인 동적 라이브러리가 EABI에서는 지원되지 않는다. 무조건 정적 링크만 지원된다. 생각해보면 당연하다. OS가 없는데 누가 동적 라이브러리를 관리하고 동적 링킹을 해줄수 있겠는가. 펌웨어는 그 자체로 필요한 모든 기능을 다 포함하고 있는 바이너리여야 한다.

QEMU 설치

QEMU는 x86, ARM 등 여러 환경을 가상 머신으로 지원하는 에뮬레이터이다.

ARM에 관련한 패키지로 받는다.

apt install qemu-system-arm

설치 중간에 사는 지역, 도시를 물어본다. 

 

설치가 완료됐다면, 지원하는 머신 목록을 확인해보자.

root@21d947b2e449:/# qemu-system-arm -M ?
Supported machines are:
akita                Sharp SL-C1000 (Akita) PDA (PXA270)
ast2500-evb          Aspeed AST2500 EVB (ARM1176)
ast2600-evb          Aspeed AST2600 EVB (Cortex A7)
borzoi               Sharp SL-C3100 (Borzoi) PDA (PXA270)
canon-a1100          Canon PowerShot A1100 IS
cheetah              Palm Tungsten|E aka. Cheetah PDA (OMAP310)
collie               Sharp SL-5500 (Collie) PDA (SA-1110)
connex               Gumstix Connex (PXA255)
cubieboard           cubietech cubieboard (Cortex-A8)
emcraft-sf2          SmartFusion2 SOM kit from Emcraft (M2S010)
highbank             Calxeda Highbank (ECX-1000)
imx25-pdk            ARM i.MX25 PDK board (ARM926)
integratorcp         ARM Integrator/CP (ARM926EJ-S)
kzm                  ARM KZM Emulation Baseboard (ARM1136)
lm3s6965evb          Stellaris LM3S6965EVB
lm3s811evb           Stellaris LM3S811EVB
mainstone            Mainstone II (PXA27x)
mcimx6ul-evk         Freescale i.MX6UL Evaluation Kit (Cortex A7)
mcimx7d-sabre        Freescale i.MX7 DUAL SABRE (Cortex A7)
microbit             BBC micro:bit
midway               Calxeda Midway (ECX-2000)
mps2-an385           ARM MPS2 with AN385 FPGA image for Cortex-M3
mps2-an505           ARM MPS2 with AN505 FPGA image for Cortex-M33
mps2-an511           ARM MPS2 with AN511 DesignStart FPGA image for Cortex-M3
mps2-an521           ARM MPS2 with AN521 FPGA image for dual Cortex-M33
musca-a              ARM Musca-A board (dual Cortex-M33)
musca-b1             ARM Musca-B1 board (dual Cortex-M33)
musicpal             Marvell 88w8618 / MusicPal (ARM926EJ-S)
n800                 Nokia N800 tablet aka. RX-34 (OMAP2420)
n810                 Nokia N810 tablet aka. RX-44 (OMAP2420)
netduino2            Netduino 2 Machine
none                 empty machine
nuri                 Samsung NURI board (Exynos4210)
palmetto-bmc         OpenPOWER Palmetto BMC (ARM926EJ-S)
raspi2               Raspberry Pi 2
realview-eb          ARM RealView Emulation Baseboard (ARM926EJ-S)
realview-eb-mpcore   ARM RealView Emulation Baseboard (ARM11MPCore)
realview-pb-a8       ARM RealView Platform Baseboard for Cortex-A8
realview-pbx-a9      ARM RealView Platform Baseboard Explore for Cortex-A9
romulus-bmc          OpenPOWER Romulus BMC (ARM1176)
sabrelite            Freescale i.MX6 Quad SABRE Lite Board (Cortex A9)
smdkc210             Samsung SMDKC210 board (Exynos4210)
spitz                Sharp SL-C3000 (Spitz) PDA (PXA270)
swift-bmc            OpenPOWER Swift BMC (ARM1176)
sx1                  Siemens SX1 (OMAP310) V2
sx1-v1               Siemens SX1 (OMAP310) V1
terrier              Sharp SL-C3200 (Terrier) PDA (PXA270)
tosa                 Sharp SL-6000 (Tosa) PDA (PXA255)
verdex               Gumstix Verdex (PXA270)
versatileab          ARM Versatile/AB (ARM926EJ-S)
versatilepb          ARM Versatile/PB (ARM926EJ-S)
vexpress-a15         ARM Versatile Express for Cortex-A15
vexpress-a9          ARM Versatile Express for Cortex-A9
virt-2.10            QEMU 2.10 ARM Virtual Machine
virt-2.11            QEMU 2.11 ARM Virtual Machine
virt-2.12            QEMU 2.12 ARM Virtual Machine
virt-2.6             QEMU 2.6 ARM Virtual Machine
virt-2.7             QEMU 2.7 ARM Virtual Machine
virt-2.8             QEMU 2.8 ARM Virtual Machine
virt-2.9             QEMU 2.9 ARM Virtual Machine
virt-3.0             QEMU 3.0 ARM Virtual Machine
virt-3.1             QEMU 3.1 ARM Virtual Machine
virt-4.0             QEMU 4.0 ARM Virtual Machine
virt-4.1             QEMU 4.1 ARM Virtual Machine
virt                 QEMU 4.2 ARM Virtual Machine (alias of virt-4.2)
virt-4.2             QEMU 4.2 ARM Virtual Machine
witherspoon-bmc      OpenPOWER Witherspoon BMC (ARM1176)
xilinx-zynq-a9       Xilinx Zynq Platform Baseboard for Cortex-A9
z2                   Zipit Z2 (PXA27x)

해당 프로젝트에서는 realview-pb-a8 머신을 선택한다. 데이터시트를 구하기 쉽기 때문이다.

해당 머신은 ARM 사의 ARM RealView Platform Baseboard를 에뮬레이팅한 머신이다.

32비트(4바이트) 머신임을 기억해두자.


 

3장 일단 시작하기

project 디렉토리 밑에 작업을 진행하겠다

mkdir project
cd project

 

⭐️ 전원 그리고 리셋 벡터

ARM 코어에 전원이 들어가면 리셋 벡터(reset vector)에 있는 명령어를 실행한다. 리셋 벡터는 메모리주소 0x0000 0000(가시성을 위해 4개마다 띄우겠다)을 의미한다.

즉, ARM 코어에 전원이 들어오면 메모리 주소 0x0000 0000에서 32 비트를 읽고, 그 명령을 실행한다.

따라서, 전원이 들어왔을 때 곧장 동작시킬 명령어를 메모리 주소 0x0000 0000에 넣어주자.

mkdir boot

tree 유틸리티로 계층 구조를 확인하자

root@21d947b2e449:/project# tree
.
`-- boot

 

 

우분투를 띄운 컨테이너 환경 내에서 코드 작성 및 수정을 위해 vim 을 다운받겠다.

apt install vim

참고로, Host 머신(Macbook)에서 작성후 복붙하면서 진행했다.

 

.text   # text섹션(시작)
    .code 32    # 명령어의 크기가 32비트이다

    .global vector_start    # .global == c의 extern 지시어 
    .global vector_end      # vector_start, vector_end 주소 정보를 외부 파일에서 심벌로 읽을 수 있게 설정하는 것

    vector_start:   # vector_start라는 레이블 선언
        MOV     R0, R1  # R1의 값을 R0에 넣어라
    vector_end:
        .space 1024, 0	# 해당 위치부터 1024바이트를 0으로 채워라
.end	 # text섹션(끝)

 

이것을 우분투를 띄운 컨테이너에 옮겨넣는다

vim boot/Entry.S

이후 복사, 붙여넣기를 한다.

 

cd boot

arm-none-eabi-as -march=armv7-a -mcpu=cortex-a8 -o Entry.o ./Entry.S 
arm-none-eabi-objcopy -O binary Entry.o Entry.bin

 

hexdump 를 포함한 유틸리티 패키지를 다운받자.

apt install bsdmainutils

 

위 어셈블리 코드를 컴파일 후 바이너리로 덤프하면 아래와 같다.

root@21d947b2e449:/project/boot# hexdump Entry.bin
0000000 0001 e1a0 0000 0000 0000 0000 0000 0000
0000010 0000 0000 0000 0000 0000 0000 0000 0000
*
0000404

 

올바른 결과인지 분석해보자.

 

메모리 주소 0000 0000 에 MOV R0, R1에 해당하는 머신코드가  0001(opcode) e1a0(operand)가 존재하고, 16진수 숫자이므로 숫자 하나는 4비트를 의미한다. 따라서 4바이트(==32비트)가 차지하게 되었다.

그 다음 메모리 주소 0000 0004부터 1024 바이트만큼을 0으로 채워야 한다. 1024바이트는 16진수로 400에 해당한다. 따라서 0000 0004 부터 0000 0404 이전까지 0으로 채워진 것이 맞다.

실행파일 만들기

실행파일의 형식에 대해 알아볼 필요가 있다.

ELF 파일형식은 실행파일 형식 중 대표적인 형식이다. 리눅스의 표준 실행 파일 형식이기도 하다.

 

실행 파일 형식에 대한 설명

더보기

컴파일러로 오브젝트 파일을 만들고 링커로 라이브러리를 링킹하고나면 최종 결과물로 파일이 하나 나온다. 이것을 실행 파일이라고 부르며, 대부분 ELF(Executable and Linkable Format) 파일 형식으로 차용하고있다. OS에서 사용자가 실행시켜 프로그램을 시작한다. 하지만, 펌웨어 바이너리는 OS도 없고 사용자가 직접 실행시키지 않아도 대상 시스템에 전원이 켜지면 실행되는 파일 형식이므로 실행 파일 형식이라고 한다.

ELF 파일 형식은 크게 두 부분으로 구분된다. ELF 헤더와 섹션으로 나뉜다. ELF 헤더는 ELF 포맷이 지정하는 여러 정보를 담고있다. OS나 임베디드 시스템의 로더는 ELF 헤더를 읽은 후 필요한 데이터를 찾아서 메모리에 복사하고 CPU의 레지스터 값을 조정해서 파일을 실행한다.

ELF 헤더에는 ELF를 표시하는 매직 넘버(magic number), ABI의 버전 및 종류, 해당 바이너리 파일의 타깃 시스템 아키텍처, ELF 포맷의 버전, 엔트리 포인트 주소 위치, 심벌 테이블 오프셋, 섹션 헤더 오프셋 등 매우 많은 정보가 들어있다. 다 알필요는 없고, 중요한 부분은 ELF 파일 포맷의 시작은 ELF 헤더라는 것과 섹션으로 나누어져있다는 것이다. 나머지는 필요할때 찾아서 쓰면된다.

ELF 섹션에는 이름이 있다. 각각 .text, .rdata, .data, .bss, .symtab, .rel.text, .rel.data, .debug, .line, .strtab이다. 각 섹션의 설명은 생략한다. 포스팅에서 특정 섹션을 사용하면 간략히 설명하도록 하겠다.

링커는 ELF 파일의 헤더와 섹션 정보를 읽어서 오브젝트 파일들을 하나로 묶은 다음 실행 가능한 최종 바이너리 파일을 만든다. 최종 바이너리 파일 자체도 ELF파일이다. 다만, 각 섹션별로 메모리의 어느 주소에 위치해야 하는지에 대한 정보도 같이 갖고 있다. 이 메모리 위치에 대한 정보를 제공하는 파일이 링커에 입력으로 전달하는 스캐터 파일이다. 모든 정보를 갖고있는 실행 가능한 최종 바이너리 파일은 로더에 의해 분해되어 메모리에 복사된다. Windows나 Linux라면 OS가 로더의 역할을 담당한다. 임베디드 시스템에서는 아예 실행 가능한 최종 바이너리 파일을 로더가 필요없게 만들거나 별도의 로더를 만들어서 먼저 실행시키고 로더가 펌웨어 바이너리 본체를 읽어서 메모리에 올리는 식으로 만든다. 형태가 보이지 않을 뿐 어떤식으로든 로더의 역할을 하는 절차는 임베디드 시스템에서도 필요하다.

 

위에서 arm-none-eabi-as 로 생성한 Entry.o 파일도 ELF 파일 형식이다.

그리고 그 실행파일의 바이너리를 덤프해서 확인해본 것이다.

 

ELF 파일을 만들기 위해서 추가적인 작업이 필요하다. 링커(Linker)의 도움이 필요하다. 링커는 오브젝트 파일들을 연결(Linking)하여 하나의 실행파일로 만드는 프로그램이다. 이때 링커가 동작을 위해 정보를 주어야한다. 링커 스크립트라는 파일로 주면된다.

root@21d947b2e449:/project# vim navilos.ld
ENTRY(vector_start)
SECTIONS
{
	. = 0x0;
	
	
	.text :
	{
		*(vector_start)
		*(.text .rodata)
	}
	.data :
	{
		*(.data)
	}
	.bss :
	{
		*(.bss)
	}
}
  • ENTRY 지시어 : 시작 위치의 심벌 지정
  • SECTIONS 지시어 : 블록이 섹션 배치 설정 정보를 갖고있음을 알림
    • .=0x0; : 첫 번째 섹션이 메모리주소 0x00000000에 위치함을 알림
    • text : text 섹션의 배치 순서를 지정
      • 추가 정보를 입력하면 배치 메모리 주소까지 지정할 수 있으나, 현 시점에서는 지정하지 않았음
      • 추가 정보가 없으면 링커는 시작 주소부터 순서대로 섹션 데이터를 배치함
      • 메모리 주소 0x0000 0000에 리섹 벡터가 위치해야 하므로
      • vector_start 심벌이 먼저 나오고
      • 이어서 .text 섹션을 적음
      • 이어서 data 섹션, bss섹션을 연속된 메모리에 배치하도록 설정
root@21d947b2e449:/project# arm-none-eabi-ld -n -T ./navilos.ld -nostdlib -o navilos.axf boot/Entry.o
root@21d947b2e449:/project# arm-none-eabi-objdump -D navilos.axf

navilos.axf:     file format elf32-littlearm


Disassembly of section .text:

00000000 <vector_start>:
   0:   e1a00001        mov     r0, r1

00000004 <vector_end>:
        ...

Disassembly of section .ARM.attributes:

00000000 <.ARM.attributes>:
   0:   00002441        andeq   r2, r0, r1, asr #8
   4:   61656100        cmnvs   r5, r0, lsl #2
   8:   01006962        tsteq   r0, r2, ror #18
   c:   0000001a        andeq   r0, r0, sl, lsl r0
  10:   726f4305        rsbvc   r4, pc, #335544320      ; 0x14000000
  14:   2d786574        cfldr64cs       mvdx6, [r8, #-464]!     ; 0xfffffe30
  18:   06003841        streq   r3, [r0], -r1, asr #16
  1c:   0841070a        stmdaeq r1, {r1, r3, r8, r9, sl}^
  20:   44020901        strmi   r0, [r2], #-2305        ; 0xfffff6ff
  24:   Address 0x0000000000000024 is out of bounds.
  • arm-none-eabi-ld 옵션 및 동작 설명:
    • -n : 링커에 섹션의 정렬을 자동으로 맞추지 말아라
    • -T : 링커 스크립트의 파일명을 알려줌
    • -nostdlib : 링커가 자동으로 표준 라이브러리를 링킹하지 못하도록함
    • 링커가 동작을 완료하면 navilos.axf 파일이 생성됌

 

  • arm-none-eabi-objdump 옵션 및 동작 설명:
    • -D : 디스어셈블; 내부가 어떻게 되어있는지 풀어헤침
    • boot/Entry.S 의 코드가 디스어셈블한 결과로 나옴을 볼 수 있다

QEMU에서 실행해보기

링커 동작으로 ELF 파일 포맷으로 만든 navilos.axf라는 실행 파일을 생성했다. 하지만 실행되지 않는다.

root@21d947b2e449:/project# ./navilos.axf 
qemu-arm: /project/navilos.axf: Error mapping file: Invalid argument

 

리눅스 커널에서 동작하지 않는 섹션 배치로 만들어져있기 때문이다.

 

ARM 에 특정적인 빌드를 했기 때문이다. 따라서 ARM HW 위에서 동작시킬 수 있도록 마련한 QEMU로 실행하자.

root@21d947b2e449:/project# qemu-system-arm -M realview-pb-a8 -kernel navilos.axf -S -gdb tcp::1234,ipv4 -display none -nographic
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5220:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2642:(snd_pcm_open_noupdate) Unknown PCM default
alsa: Could not initialize DAC
alsa: Failed to open `default':
alsa: Reason: No such file or directory
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5220:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2642:(snd_pcm_open_noupdate) Unknown PCM default
alsa: Could not initialize DAC
alsa: Failed to open `default':
alsa: Reason: No such file or directory
audio: Failed to create voice `lm4549.out'

 

컴퓨터 본체를 껍데기만 본다고 내부의 동작을 볼수는 없다. 따라서 gdb를 연결해서 내부를 보자.

다른 터미널을 열고 gdb를 연결한다. docker desktop 을 쓰면 빨간 표시에서 간단하게 열 수 있다.

gdb를 쓰기위해 설치

apt install gdb-multiarch

 

gdb와 QEMU를 연결하자

root@21d947b2e449:/# cd project/
root@21d947b2e449:/project# ls
boot  navilos.axf  navilos.ld
root@21d947b2e449:/project# gdb-multiarch navilos.axf
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04.1) 9.2
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "aarch64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from navilos.axf...
(No debugging symbols found in navilos.axf)
(gdb)
(gdb) target remote:1234
Remote debugging using :1234
0x00000000 in vector_start ()
(gdb) x/4b 0
0x0 <vector_start>:	1	0	-96	-31
(gdb) set output-radix 16
Output radix now set to decimal 16, hex 10, octal 20.
(gdb) x/4b 0
0x0 <vector_start>:	0x1	0x0	0xa0	0xe1

디스어셈블해서 봤던 결과와 동일하다.

 

cf) QEMU 종료가 안돼요

command + c 로 종료되지 않는다.

다른 터미널에서 pkill 로 종료했다.

pkill -9 qemu-system-arm

 

빌드 자동화하기

Make 빌드시스템을 이용하여 자동화하자. 사용하기 위해 패키지를 다운받자.

apt install make

 

Makefile을 작성하자.

ARCH = armv7-a
MCPU = cortex-a8

CC = arm-none-eabi-gcc
AS = arm-none-eabi-as
LD = arm-none-eabi-ld
OC = arm-none-eabi-objcopy

LINKER_SCRIPT = ./navilos.ld

ASM_SRCS = $(wildcard boot/*.S)
ASM_OBJS = $(patsubst boot/%.S, build/%.o, $(ASM_SRCS))

navilos = build/navilos.axf
navilos_bin = build/navilos.bin

.PHONY: all clean run debug kill_qemu gdb

all: $(navilos)

clean:
	@rm -fr build
	
run: $(navilos)
	qemu-system-arm -M realview-pb-a8 -kernel $(navilos)
	
debug: $(navilos)
	qemu-system-arm -M realview-pb-a8 -kernel $(navilos) -S -gdb tcp::1234,ipv4 -display none -nographic

kill_qemu:
	pkill -9 qemu-system-arm
	
gdb:
	gdb-multiarch
	
$(navilos): $(ASM_OBJS) $(LINKER_SCRIPT)
	$(LD) -n -T $(LINKER_SCRIPT) -o $(navilos) $(ASM_OBJS)
	$(OC) -O binary $(navilos) $(navilos_bin)
	
build/%.o: boot/%.S
	mkdir -p $(shell dirname $@)
	$(AS) -march=$(ARCH) -mcpu=$(MCPU) -g -o $@ $<

 

사용해보자

root@21d947b2e449:/project# make all
mkdir -p build
arm-none-eabi-as -march=armv7-a -mcpu=cortex-a8 -g -o build/Entry.o boot/Entry.S
arm-none-eabi-ld -n -T ./navilos.ld -o build/navilos.axf  build/Entry.o
arm-none-eabi-objcopy -O binary build/navilos.axf build/navilos.bin
root@21d947b2e449:/project# make debug
qemu-system-arm -M realview-pb-a8 -kernel navilos.axf -S -gdb tcp::1234,ipv4 -display none -nographic
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5220:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2642:(snd_pcm_open_noupdate) Unknown PCM default
alsa: Could not initialize DAC
alsa: Failed to open `default':
alsa: Reason: No such file or directory
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5220:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2642:(snd_pcm_open_noupdate) Unknown PCM default
alsa: Could not initialize DAC
alsa: Failed to open `default':
alsa: Reason: No such file or directory
audio: Failed to create voice `lm4549.out'

make all 로 빌드를 하고, make debug로 동작시킨 모습이다.

빌드 시스템으로 매우 간단하게 진행할 수 있게됐다.

⭐️ 하드웨어 정보 읽어오기 - 데이터 시트를 읽는 방법

SW의 최하단부에서는 HW와 상호작용을 한다. OS환경 위에서의 프로그래밍하는 보통의 프로그래밍 때에는 느낄 일 이 없겠지만, HW를 직접 사용하기 위해서는 필연적인 부분이다.

 

어떻게 HW에서 정보를 읽어오고, 어떻게 HW에 정보를 쓸까?

레지스터를 이용해야한다. 레지스터가 HW와 SW의 인터페이스이다.

대상 HW의 레지스터 사용법을 알아야하는데, 데이터시트로 제공된다. 데이터시트는 해당 HW의 레지스터 목록과 설명, 그리고 레지스터에 어떤 값을 쓰냐에 따라 어떻게 동작하는지를 설명하는 문서다.  그외에도 해당 HW에 대한 수많은 정보를 제공한다. 

ARM에서 제공하는 문서를 확인해보자.

 

 

특히 프로그래머가 SW로 해당 HW를 "사용"하기 위한 설명을 친절하게 문서화해두었다.

Chapter 4. Programmer's Reference

 

Documentation – Arm Developer

 

developer.arm.com

 

HW를 식별해서 사용해보자

HW를 식별할 수 있는 정보를 가진 레지스터(SYS_ID)

SYS_ID 레지스터 설명

메모리 주소 0x1000 0000 는 SYS_ID 레지스터의 시작와 매핑된다.

 

Entry.S를 수정하자.

.text
	.code 32

	.global vector_start
	.global vector_end

	vector_start:
		LDR		R0, =0x10000000
		LDR		R1, [R0]
	vector_end:
		.space 1024, 0
.end

무엇을 수정한걸까?

.text
    .code 32

    .global vector_start
    .global vector_end

    vector_start:
-        MOV     R0, R1
+        LDR R0, =0x10000000    # R0에 0x10000000 이라는 숫자를 넣어라
+        LDR R1, [R0]           # R0에 저장된 메모리 주소에서 값을 읽어서 R1에 넣어라.(== 0x10000000에서 값을 읽어서 그 값을 R1에 저장해라.)
    vector_end:
        .space 1024, 0
.end

기존 Entry.S에서는 MOV R0, R1 은 동작확인을 위해 임의의 명령을 작성했다.

이번에는 "SYS_ID 레지스터의 값을 읽어서 R1에 저장해라"라는 의미이다.

 

SYS_ID 레지스터 설명을 보면 기본값이 있는 영역은 아래와 같다.

+---------+---------------------------+---------+---------+------------------+
|   REV   |            HBI            |  BUILD  |  ARCH   |       FPGA       |
+---------+---------------------------+---------+---------+------------------+
| [31:28] |          [27:16]          | [15:12] | [11:8]  |       [7:0]      |
+---------+---------------------------+---------+---------+------------------+

- 기본값이 있는 영역(HBI, ARCH)
    - HBI  : 0x178
    - ARCH : 0x5

제대로 읽었다면, HBI, ARCH 영역에 기본값이 동일하게 나올것이다.

 

QEMU에 올려서 확인해보자.

우선 Entry.S를 수정했으니 다시 빌드한다.

 

터미널 두개를 준비하고, 한쪽은 QEMU를 실행시키고 , 다른 한쪽은 gdb를 실행시킨다.

(gdb) target remote:1234
Remote debugging using :1234
warning: No executable has been specified and target does not support
determining executable automatically.  Try using the "file" command.
0x00000000 in ?? ()
(gdb) file build/navilos.axf
A program is being debugged already.
Are you sure you want to change the file? (y or n) Y
Reading symbols from build/navilos.axf...
(gdb) list
1	.text
2		.code 32
3	
4		.global vector_start
5		.global vector_end
6	
7		vector_start:
8			LDR		R0, =0x10000000
9			LDR		R1, [R0]
10		vector_end:
(gdb) info register
r0             0x0                 0
r1             0x0                 0
r2             0x0                 0
r3             0x0                 0
r4             0x0                 0
r5             0x0                 0
r6             0x0                 0
r7             0x0                 0
r8             0x0                 0
r9             0x0                 0
r10            0x0                 0
r11            0x0                 0
r12            0x0                 0
sp             0x0                 0x0 <vector_start>
lr             0x0                 0
pc             0x0                 0x0 <vector_start>
cpsr           0x400001d3          1073742291
...중략

아직 아무런 실행을 하지 않았으므로, 레지스터에는 아무런 정보가 없다.

첫 명령어를 실행해보자

(gdb) step
vector_start () at boot/Entry.S:9
9	        LDR R1, [R0]

(gdb) info register
r0             0x10000000          268435456
r1             0x0                 0
r2             0x0                 0
r3             0x0                 0
r4             0x0                 0
r5             0x0                 0
r6             0x0                 0
r7             0x0                 0
r8             0x0                 0
r9             0x0                 0
r10            0x0                 0
r11            0x0                 0
r12            0x0                 0
sp             0x0                 0x0 <vector_start>
lr             0x0                 0
pc             0x4                 0x4 <vector_start+4>
cpsr           0x400001d3          1073742291
...중략

R0 에 0x1000 0000 이 저장되어있음을 확인할 수 있다.

 

한줄 더 실행하자.

0x1000 000에서 값을 읽어서 R1에 넣을 것이고, 그 값은 0x178을 포함해야한다

(gdb) step
0x00000408 in ?? ()

(gdb) info register
r0             0x10000000          268435456
r1             0x1780500           24642816
r2             0x0                 0
r3             0x0                 0
r4             0x0                 0
r5             0x0                 0
r6             0x0                 0
r7             0x0                 0
r8             0x0                 0
r9             0x0                 0
r10            0x0                 0
r11            0x0                 0
r12            0x0                 0
sp             0x0                 0x0 <vector_start>
lr             0x0                 0
pc             0x408               0x408
cpsr           0x400001d3          1073742291
...중략

정확하게는 0x1780500를 저장하고있다.

 

무슨의미일지 분석해보자.

- 0x1780500은 무엇을 의미하나?
  - 2진수로 변환 : 00000001 01111000 00000101 00000000
  - SYS_ID 레지스터 영역에 맞게 쪼개면... 

+---------+---------------------------+---------+---------+------------------+
|   REV   |            HBI            |  BUILD  |  ARCH   |       FPGA       |
+---------+---------------------------+---------+---------+------------------+
| [31:28] |          [27:16]          | [15:12] | [11:8]  |       [7:0]      |
+---------+---------------------------+---------+---------+------------------+
   0000            000101111000           0000     0101           00000000
   0x0                0x178               0x0      0x5              0x00

- 기본값이 있는 영역(HBI, ARCH)
    - HBI  : 0x178
    - ARCH : 0x5
    올바르게 읽어왔음!

올바르게 읽어왔음을 확인할 수 있다.

4장 부팅하기

어디까지의 범위가 부팅일까?

정해진 것은 없다.

 

  1. 시스템에 전원이 들어가고
  2. ARM 코어가 리셋 익셉션 핸들러를 모두 처리한 다음에
  3. 본격적으로 C 언어 코드로 넘어가기 직전까지

를 부팅이라고 정의하자.

메모리 설계

실행 파일은 메모리를 크게 세 가지로 나누어 사용한다.

  • text 영역 : 코드가 있는 공간
    • 코드이므로 임의로 변경해서는 안된다
  • data 영역 : 초기화한 전역 변수가 있는 공간
    • 전역 변수를 선언할 때 초기 값을 할당해서 선언하면, 해당 전역 변수가 점유하는 공간은 여기에 할당된다
  • BSS 영역 : 초기화하지 않은 전역 변수가 있는 공간
    • 초기화하지 않은 전역 변수이므로, 빌드 완료되어 생성된 바이너리 파일에는 심벌과 크기만 들어있다

Topic 1. 영역 배치 

속도가 빠르고 용량이 작은 메모리, 속도가 느리고 용량이 큰 메모리 2가지의 메모리가 존재한다하자.

  • text 영역 : 빠른 메모리에 배치
  • data 영역 중에서도 일부 속도에 민감한 데이터들이 있는 영역 : 링커에게 정보를 주어서, 빠른 메모리에 배치해야함.
  • 나머지 data영역, BSS 영역 : 속도는 느려도 용량이 큰 메모리에 배치

QEMU는 그런 구분이 없으므로(소프트웨어 에뮬레이터) 그냥 순서대로 배치해보자.

즉, 순서나 위치는 고민에서 제외하자.

Topic 2. 크기 배정

QEMU에서는 고민하지 않아도 되지만, 실무에서는 중요한 부분이다.

  • text 영역  
    • 크기
      • 수십 KB 정도면 충분
      • 여기서는 메모리가 넉넉하니 1MB 할당
    • 익셉션 벡터 테이블을 text영역에 포함 시킬것이다,
      • 시작 주소는 0x00000000
      • 크기를 1MB로 설정했으니, 끝나는 주소는 0x000FFFFF

어떤 성격의 데이터를, 어떤 순서로, 얼마만큼의 영역을 할당해서 배치할지 진지한 고민이 필요한 시점이다.

  • data 영역
  • BSS 영역

 

  • 데이터 형태
    • 동작 모드별 스택
    • 태스크 스택
    • 전역 변수
    • 동적 메모리 할당 영역
  • 데이터 속성
    • 성능 중시 데이터
    • 큰 공간이 필요한 데이터
    • 공유 데이터

데이터는 형태, 속성 성질을 모두 고려해야 한다. 하지만, QEMU에서는 의미가 없어서 그냥 죽 배치하자.

 

ARM의 동작 모드는 7개가 있다. 동작모드에 대해 설명하기 위해서는 HW 특정적인 지식이 필요하다.

  • 동작모드
  • 레지스터 별 역할
  • ARM 모드와 Thumb모드
  • 프로그램 상태 레지스터
데이터 형태 관점에서 나눈 모습 


개별 동작 모드당 1MB를 할당 (== 모드 별 스택을 1MB 할당)
- USR, SYS (2MB) 
  - 메모리 공간과 레지스터를 모두 공유하므로 하나로 묶어서 보았고, 기본 동작 모드로 사용될 것이므로 2MB 할당
- SVC (1MB)
- IRQ (1MB)
- FIQ (1MB)
- ABT (1MB)
- UND (1MB)

RTOS 위에서 동작할 task 스택 영역은?
- task 마다 1MB씩 스택 영역을 할당할 생각이므로, 총 64MB를 배정 (== 본 OS의 최대 task 수는 64개)

전역 변수용?
- 1MB

그리고 남은 공간을 동적 할당 메모리용

책에서 캡쳐

익셉션 벡터 테이블 만들기

익셉션 벡터 테이블(Exception Vector Table)은 컴퓨터 시스템에서 발생한 예외(예를 들어, 인터럽트, 트랩, 오류 등)를 처리하기 위한 중요한 데이터 구조이다. 이 테이블은 예외가 발생했을 때 프로세서가 수행할 동작을 결정하는 데 사용된다.

보통 이 테이블은 특정 주소 범위에 위치하며, 각 예외 유형에 대해 해당하는 예외 처리 루틴의 주소를 저장한다. 프로세서가 예외를 감지하면 해당 예외 벡터의 인덱스를 찾아 해당하는 주소로 점프하여 예외 처리를 시작한다.

익셉션 벡터 테이블은 운영 체제나 하드웨어의 초기화 과정에서 설정되며, 시스템이 예외에 대해 어떻게 반응해야 하는지 결정하는 데 중요한 역할을 한다.

책에서 캡쳐

ARM은 익셉션 벡터 테이블에 정의된 상황이 발새앟면 프로그램 카운터(PC)를 익셉션 벡터 테이블에 정의된 오프셋 주소로 강제 변환한다. 그리고 익셉션 벡터 테이블에 있는 명령을 바로 실행한다.

익셉션 벡터 테이블의 익셉션은 각각 4바이트씩 할당되어있다. 32비트 머신이므로 한 익셉션에 명령어 한개만 실행할 수 있다. 따라서 보통 브랜치 명령어를 두어 익셉션을 처리하는 코드를 분리한다. 이 익셉션 처리하는 코드 뭉치를 익셉션 핸들러라고 부른다.

 

책에서 캡쳐

 

동작 모드와 레지스터 별 역할

  • User 모드(USR)
    • 일반적으로 사용하는 모드. ARM 상태와 Thumb 상태로 동작. 만약 OS를 쓴다면, 사용자 프로그램은 일반적으로 USR 모드에서 동작한다.
  • Fast Interrupt 모드 (FIQ)
    • FIQ 익셉션 발생시 FIQ 모드로 전환된다. FIQ 모드는 ARM 상태일때만 동작한다. 빠른 처리를 위해서 별도로 레지스터를 몇개 더 갖는다. 이 레지스터를 뱅크드 레지스터(banked ragister)라 한다.
  • Interrupt 모드(IRQ)
    • IRQ 익셉션이 발생하면 IRQ 모드로 전환된다. IRQ 모드는 ARM 상태와 Thumb 상태일 때 모두 동작 가능하다.
  • Supervisor 모드(SVC)
    • OS 등에서 시스템 코드를 수행하기 위한 보호모드이다. 보통 OS에서 시스템 콜(system call)을 호출하면 SVC 익셉션을 발생시켜 SVC 모드로 전환 후에 커널 동작을 수행한다. SVC 익셉션은 메모리나 HW에 상관없이 순수하게 SW에 의해서 발생하는 익셉션이다.
  • Abort 모드(ABT)
    • Data abort나 Prefetch abort가 발생했을때 전환되는 동작 모드이다.
  • System 모드 (SYS)
    • OS 등에서 사용자 프로세스가 임시로 커널 모드를 획득해야 하는 경우가 있는데, 이대 SYS 모드를 사용한다. 혹은 사용자 모드와 커널 모드를 구분하는 OS가 아니라면 SYS모드가 기본 동작 모드인 경우가 많다. 그래서 SYS 모드는 익셉션과 연관되어 있지 않고 SW로 모드 전환을 하여 진입할 수 있다.
  • Undefined 모드(UND)
    • Undefined instruction이 발생했을때 진입하는 동작모드이다.

책에서 캡쳐

  • R0 ~ R12 : 범용 레지스터(general purpose register)
  • R13: 스택 포인터(Stack Pointer, SP)레지스터
    • 대부분의 SW는 스택을 기반으로 동작한다. 따라서, 스택의 현재위치를 알아야한다. 그 위치를 저장하는 레지스터이다.
  • R14: 링크 레지스터(Link Register, LR)
    • 대부분의 SW는 서브 루틴 호출 혹은 함수 호출로 구성되어있다.
    • 예를들어 funcA()에서 funcB()를 호출하는 상황을 가정하자. funcB()가 수행을 마치고 리턴했을때, funcA()의 호출지점으로 돌아가야한다. 그 지점을 리턴 어드레스(return address)라고 부른다. 그 위치를 저장하는 레지스터이다.
    • ARM에서 BL 같은 분기 명령어를 통해서 서브루틴으로 점프하면, HW가 자동으로 LR에 리턴 어드레스를 넣어준다.
  • R15: 프로그램 카운터(Program Counter, PC)
    • 프로그램은 메모리에서 명령어를 읽어서 실행하고 그다음 명령어를읽어서 수행하는 작업의 반복이다. 다음 명령어가 있는 메모리 주소를 저장하고 있어야 한다.

개별 동작 모드는 모두 SP와 LR 을 뱅크드 레지스터로 갖고 있다. 그래야만 각 동작 모드가 독립된 스택 영역을 유지할 수 있다.

 

ARM 모드와 Thumb 모드

ARM은 두 종류의 명령어 집합을 갖는다. 32비트 명령어 집합인 ARM 모드 명령어, 16비트 명령어 집합인 Thumb 모드 명령어.

Thumb모드에서 R8이상의 높은 번호 레지스터의 사용이 제한된다. SP, LR, PC만 사용가능하며 R8에서 R12까지의 범용 레지스터는 몇가지 명령어에서만 사용 가능하다.

⭐️프로그램 상태 레지스터(PSR)

ARM에 관해 많은 상태에 대해 알아봤다. 동작모드가 있고, 동작모드에 연결되어 있는 익셉션들의 상태가 있다. 현재 ARM이 처리하는 명령어 모드도 있다. 이와같은 프로세서의 상태 외에도 프로그램이 동작하면서 생기는 많은 상태가 있다. 예를 을면 계산 결과가 음수이거나 0일 때 이것을 상태로 가지고 있어야 수행할 수 있는 동작들이 있다. 이런 상태들을 관리하는 레지스터를 프로그램 상태 레지스터(Program Status Register, PSR)라고 한다. 현재 상태를 저장하는 레지스터는 CPSR(Current PSR)이라 하고, 상태를 저장하는 레지스터는 SPSR(Saved PSR)이라고 한다. 즉, SPSR은 CPSR의 백업이다. 그러므로 SPSR과 CPSR은 구조가 같다. PSR은 ARM 아키텍쳐를 공유하는 프로세서들이 비슷하면서 조금씩 다르다. 일단, ARM의 아키텍쳐 중 Cortex-A8의 PSR 구성은 아래와 같다

  • N: 계산 결과가 음수일 때 1로 변경된다
  • Z: 계산 결과가 0일 때 1로 변경된다
  • C: 계산 결과에 자리 내림(carry)이 발생하거나 나눗셈을 할 때자리 빌림(borrow)이 발생하면 1로 변경된다
  • V: 계산 결과에 오버플로(overflow)가 발생하면 1로 변경된다
  • Q: 곱셈을 할 때 32비트가 넘어가면 올림수에 이용한다
  • J: Cortex-A 이상 프로세서에서 Jazelle 상태로 전환시 1로 변경된다. Jazelle는 일부 ARM 프로세서에서 ARM 및 Thumb 모드와 함께 쓰는 세번째 실행 상태이다. HW에서 Java 바이트 코드를 실행할수 있도록 하는 확장이다
  • DNM: Do Not Modify의 약자로, 확장을 위해 비워 둔다
  • GE[3:0]: SIMD(Single Instruction Multi Data) 명령을 사용해서 연산을 할 때 하프워드(half word) 단위로 크거나 같은지를 표시하는 비트이다
  • IT[7:2]: ITSTATE로 Thumb-2에 포함된 IT(if-then) 명령을 처리할 때 참조하는 비트이다. 원래 Thumb 모드 명령어는 조건부 실행이 안되는데, 이 IT 비트 영역으로 Thumb 모드에서 조건부 실행이 가능하도록 만든 것이다
  • E: 데이터의 엔디안을 표시하는 비트이다
  • A: 예측 가능한 data abort만 발생하도록 한다. 이 비트를 끄면 예측 불가능한 비동기 데이터 abort를 허용한다
  • I: 이 비트가 1이면 IRQ가 비활성화된다
  • F: 이 비트가 1이면 FIQ가 비활성화된다
  • T: Thumb 모드일 때 1로 변경된다
  • M[4:0]: 모드 비트이다. 각 동작 모드별로 비트 설정 값은 다음과 같다
    • 10000: USR
    • 10001: FIQ
    • 10010: IRQ
    • 10011: SVC
    • 10111: ABT
    • 11011: UND
    • 11111: SYS

모드 비트를 바꾸면 동작 모드가 변경된 것이다. 익셉션이 발생한다면 HW가 알아서 값을 변경한다.

예를 들어 현재 동작 모드가 USR이면 CPSR의 M 영역값은 0x10이다. 이 값을 펌웨어에서 0x1F로 바꾸면 SYS 모드로 변경된다. 이러다가 HW에서 IRQ가 발생하면 HW가 이 값을 0x12로 바꾸고 익셉션 핸들러로 진입한다.

 

이제 익셉션 벡터 테이블을 작성해보자. 우선 뼈대를 만든다.

Entry.S을 수정하자.

.text
	.code 32

	.global vector_start
	.global vector_end

	vector_start:
		LDR		PC, reset_handler_addr
		LDR		PC, undef_handler_addr
		LDR		PC, svc_handler_addr
		LDR		PC, pftch_abt_handler_addr
		LDR		PC, data_abt_handler_addr
		B		.
		LDR		PC, irq_handler_addr
		LDR		PC, fiq_handler_addr

		reset_handler_addr: 	.word reset_handler
		undef_handler_addr: 	.word dummy_handler
		svc_handler_addr: 		.word dummy_handler
		pftch_abt_handler_addr: .word dummy_handler
		data_abt_handler_addr:  .word dummy_handler
		irq_handler_addr:		.word dummy_handler
		fiq_handler_addr:		.word dummy_handler
	vector_end:

	reset_handler:
		LDR		R0, =0x10000000
		LDR		R1, [R0]

	dummy_handler:
		B .
.end

8~15번째 줄이 익셉션 테이블 부분이다.

 

익셉션 벡터 테이블에 각 핸들러로 점프하는 코드를 작성했다.

리셋 핸들러는 기존에 작성했던 코드(SYS_ID 레지스터의 정보를 읽어오는 코드)를 두었다. 

더미 핸들러는 무한루프를 돌게 만들었다.

익셉션 핸들러 만들기

리셋 익셉션 핸들러를 만들자. 

책에서 캡쳐

메모리 맵을 설정해야한다. 메모리를 설계한대로 동작 모드별 스택 주소를 각 동작 모드의  뱅크드 레지스터 SP에 설정하자. 그 외 메모리 영역은 navilos 에서 관리하게 할 것이다. 동작 모드별 스택이 모두 설정되면 C언어 main() 함수로 진입하게 하여, 어셈블리어가 아닌 C언어로 제어할 수 있게 한다.

ARM의 7개의 동작 모드 중 USR모드와 SYS 모드는 레지스터를 공유하므로 SP레지스터는 총 6개가 뱅크들 레지스터로 제공된다. 리셋 익셉션 핸들러에서는 동작 모드를 순서대로 변경해 가면서 SP 레지스터에 정해진 값을 넣는 작업을 수행한다. 이러면 각 동작 모드의 스택이 초기화 된 것이다.

 

  1. 메모리 맵을 설계하고 
  2. C언어의 main()으로 연결하겠다

스택 만들기

메모리 맵을 C언어로 작성하자.

include 디렉터리 하에 작성하겠다.

mkdir include
vim include/MemoryMap.h
#define INST_ADDR_START     0
#define USRSYS_STACK_START  0x00100000
#define SVC_STACK_START     0x00300000
#define IRQ_STACK_START     0x00400000
#define FIQ_STACK_START     0x00500000
#define ABT_STACK_START     0x00600000
#define UND_STACK_START     0x00700000
#define TASK_STACK_START    0x00800000
#define GLOBAL_ADDR_START   0x04800000
#define DALLOC_ADDR_START   0x04900000

#define INST_MEM_SIZE       (USRSYS_STACK_START - INST_ADDR_START)
#define USRSYS_STACK_SIZE   (SVC_STACK_START - USRSYS_STACK_START)
#define SVC_STACK_SIZE      (IRQ_STACK_START - SVC_STACK_START)
#define IRQ_STACK_SIZE      (FIQ_STACK_START - IRQ_STACK_START)
#define FIQ_STACK_SIZE      (ABT_STACK_START - FIQ_STACK_START)
#define ABT_STACK_SIZE      (UND_STACK_START - ABT_STACK_START)
#define UND_STACK_SIZE      (TASK_STACK_START - UND_STACK_START)
#define TASK_STACK_SIZE     (GLOBAL_ADDR_START - TASK_STACK_START)
#define DALLOC_MEM_SIZE     (55 * 1024 * 1024)

#define USRSYS_STACK_TOP    (USRSYS_STACK_START + USRSYS_STACK_SIZE - 4)
#define SVC_STACK_TOP       (SVC_STACK_START + SVC_STACK_SIZE - 4)
#define IRQ_STACK_TOP       (IRQ_STACK_START + IRQ_STACK_SIZE - 4)
#define FIQ_STACK_TOP       (FIQ_STACK_START + FIQ_STACK_SIZE - 4)
#define ABT_STACK_TOP       (ABT_STACK_START + ABT_STACK_SIZE - 4)
#define UND_STACK_TOP       (UND_STACK_START + UND_STACK_SIZE - 4)

"스택 꼭대기 주소 = 스택의 시작 주소 + 스택의 크기" 에 -4 를 해두었다.

이는 저자의 취향으로, 패딩(padding)값을 준것이다. 

 

vim include/ARMv7AR.h
/* PSR Mode Bit Values */
#define ARM_MODE_BIT_USR 0x10
#define ARM_MODE_BIT_FIQ 0x11
#define ARM_MODE_BIT_IRQ 0x12
#define ARM_MODE_BIT_SVC 0x13
#define ARM_MODE_BIT_ABT 0x17
#define ARM_MODE_BIT_UND 0x1B
#define ARM_MODE_BIT_SYS 0x1F
#define ARM_MODE_BIT_MON 0x16

 

헤더 파일도 GCC로 컴파일 하면 어셈블리어 파일에서도 사용할 수 있다.

#include "ARMv7AR.h"
#include "MemoryMap.h"

.text
    .code 32

    .global vector_start
    .global vector_end

    vector_start:
        LDR PC, reset_handler_addr
        LDR PC, undef_handler_addr
        LDR PC, svc_handler_addr
        LDR PC, pftch_abt_handler_addr
        LDR PC, data_abt_handler_addr
        B   .
        LDR PC, irq_handler_addr
        LDR PC, fiq_handler_addr

        reset_handler_addr:     .word reset_handler
        undef_handler_addr:     .word dummy_handler
        svc_handler_addr:       .word dummy_handler
        pftch_abt_handler_addr: .word dummy_handler
        data_abt_handler_addr:  .word dummy_handler
        irq_handler_addr:       .word dummy_handler
        fiq_handler_addr:       .word dummy_handler
    vector_end:

    reset_handler:
        MRS r0, cpsr
        BIC r1, r0, #0x1F
        ORR r1, r1, #ARM_MODE_BIT_SVC
        MSR cpsr, r1
        LDR sp, =SVC_STACK_TOP

        MRS r0, cpsr
        BIC r1, r0, #0x1F
        ORR r1, r1, #ARM_MODE_BIT_IRQ
        MSR cpsr, r1
        LDR sp, =IRQ_STACK_TOP

        MRS r0, cpsr
        BIC r1, r0, #0x1F
        ORR r1, r1, #ARM_MODE_BIT_FIQ
        MSR cpsr, r1
        LDR sp, =FIQ_STACK_TOP

        MRS r0, cpsr
        BIC r1, r0, #0x1F
        ORR r1, r1, #ARM_MODE_BIT_ABT
        MSR cpsr, r1
        LDR sp, =ABT_STACK_TOP

        MRS r0, cpsr
        BIC r1, r0, #0x1F
        ORR r1, r1, #ARM_MODE_BIT_UND
        MSR cpsr, r1
        LDR sp, =UND_STACK_TOP

        MRS r0, cpsr
        BIC r1, r0, #0x1F
        ORR r1, r1, #ARM_MODE_BIT_SYS
        MSR cpsr, r1
        LDR sp, =USRSYS_STACK_TOP

    dummy_handler:
        B .
.end

 

무엇이 수정된걸까?

+#include "ARMv7AR.h"
+#include "MemoryMap.h"
 
 .text
     .code 32

     .global vector_start
     .global vector_end
 
     vector_start:
         LDR     PC, reset_handler_addr
         LDR     PC, undef_handler_addr
         LDR     PC, svc_handler_addr
         LDR     PC, pftch_abt_handler_addr
         LDR     PC, data_abt_handler_addr
         B       .
         LDR     PC, irq_handler_addr
         LDR     PC, fiq_handler_addr 
 
         reset_handler_addr:     .word reset_handler
         undef_handler_addr:     .word dummy_handler
         svc_handler_addr:       .word dummy_handler
         pftch_abt_handler_addr: .word dummy_handler
         data_abt_handler_addr:  .word dummy_handler
         irq_handler_addr:       .word dummy_handler
         fiq_handler_addr:       .word dummy_handler
     vector_end:

     reset_handler:
-        LDR		R0, =0x10000000
-	     LDR		R1, [R0]
+        MRS r0, cpsr
+        BIC r1, r0, #0x1F
+        ORR r1, r1, #ARM_MODE_BIT_SVC
+        MSR cpsr, r1
+        LDR sp, =SVC_STACK_TOP
+    
+        MRS r0, cpsr
+        BIC r1, r0, #0x1F
+        ORR r1, r1, #ARM_MODE_BIT_IRQ
+        MSR cpsr, r1
+        LDR sp, =IRQ_STACK_TOP
+    
+        MRS r0, cpsr
+        BIC r1, r0, #0x1F
+        ORR r1, r1, #ARM_MODE_BIT_FIQ
+        MSR cpsr, r1
+        LDR sp, =FIQ_STACK_TOP
+    
+        MRS r0, cpsr
+        BIC r1, r0, #0x1F
+        ORR r1, r1, #ARM_MODE_BIT_ABT
+        MSR cpsr, r1
+        LDR sp, =ABT_STACK_TOP
+    
+        MRS r0, cpsr
+        BIC r1, r0, #0x1F
+        ORR r1, r1, #ARM_MODE_BIT_UND
+        MSR cpsr, r1
+        LDR sp, =UND_STACK_TOP
+    
+        MRS r0, cpsr
+        BIC r1, r0, #0x1F
+        ORR r1, r1, #ARM_MODE_BIT_SYS
+        MSR cpsr, r1
+        LDR sp, =USRSYS_STACK_TOP

     dummy_handler:
         B   .
 .end

 

무엇을 한 걸까?

MRS r0, cpsr
BIC r1, r0, #0x1F
ORR r1, r1, #동작 모드
MSR cpsr, r1
LDR sp, =스택 꼭대기 메모리 주소

 

각 모드별로 "#동작모드"에는 각자의 동작모드 비트, "#스택 꼭대기 메모리 주소"에는 스택 꼭대기 주소값을 넣는 작업이다.

스택의 시작 주소를 넣지 않고 꼭대기 주소를 넣는 이유가 있다. 스택은 높은 주소에서 낮은주소로 자라므로, 스택의 꼭대기 메모리 주소를 넣는다.

 

빌드에 관련한 파일이 늘었다. Makefile의 수정이 필요하다.

ARCH = armv7-a
MCPU = cortex-a8

CC = arm-none-eabi-gcc
AS = arm-none-eabi-as
LD = arm-none-eabi-ld
OC = arm-none-eabi-objcopy

LINKER_SCRIPT = ./navilos.ld

ASM_SRCS = $(wildcard boot/*.S)
ASM_OBJS = $(patsubst boot/%.S, build/%.o, $(ASM_SRCS))

INC_DIRS = include

navilos = build/navilos.axf
navilos_bin = build/navilos.bin

.PHONY: all clean run debug kill_qemu gdb

all: $(navilos)

clean:
	@rm -fr build
	
run: $(navilos)
	qemu-system-arm -M realview-pb-a8 -kernel $(navilos)
	
debug: $(navilos)
	qemu-system-arm -M realview-pb-a8 -kernel $(navilos) -S -gdb tcp::1234,ipv4 -display none -nographic

kill_qemu:
	pkill -9 qemu-system-arm
	
gdb:
	gdb-multiarch
	
$(navilos): $(ASM_OBJS) $(LINKER_SCRIPT)
	$(LD) -n -T $(LINKER_SCRIPT) -o $(navilos) $(ASM_OBJS)
	$(OC) -O binary $(navilos) $(navilos_bin)
	
build/%.o: boot/%.S
	mkdir -p $(shell dirname $@)
	$(CC) -march=$(ARCH) -mcpu=$(MCPU) -I $(INC_DIRS) -c -g -o $@ $<

무엇을 수정했는가

ARCH = armv7-a
MCPU = cortex-a8

CC = arm-none-eabi-gcc
AS = arm-none-eabi-as
LD = arm-none-eabi-ld
OC = arm-none-eabi-objcopy

LINKER_SCRIPT = ./navilos.ld

ASM_SRCS = $(wildcard boot/*.S)
ASM_OBJS = $(patsubst boot/%.S, build/%.o, $(ASM_SRCS))

+INC_DIRS = include

navilos = build/navilos.axf
navilos_bin = build/navilos.bin

.PHONY: all clean run kill_qemu debug gdb

all: $(navilos)

clean:
	@rm -rf build

run: $(navilos)
	qemu-system-arm -M realview-pb-a8 -kernel $(navilos)

kill_qemu:
	pkill -9 qemu-system-arm
    
debug: $(navilos)
	qemu-system-arm -M realview-pb-a8 -kernel $(navilos) -S -gdb tcp::1234,ipv4 -display none -serial stdio

gdb:
	gdb-multiarch

$(navilos): $(ASM_OBJS) $(LINKER_SCRIPT)
	$(LD) -n -T $(LINKER_SCRIPT) -o $(navilos) $(ASM_OBJS)
	$(OC) -O binary $(navilos) $(navilos_bin)

build/%.o: boot/%.S
	mkdir -p $(shell dirname $@)
-	$(AS) -march=$(ARCH) -mcpu=$(MCPU) -g -o $@ $<
+	$(CC) -march=$(ARCH) -mcpu=$(MCPU) -I $(INC_DIRS) -c -g -o $@ $<
  • 헤더파일 디렉터리 경로 추가
    • -I : 헤더파일 디렉터리 경로 지시 옵션
  • 전처리를 위해서 arm-none-eabi-gcc를 사용
    • arm-none-eabi-as는 어셈블러만 존재
    • `#define`구문은 전처리기에 의해서 처리된다
    • GCC 옵션에 맞게 -c 추가
      • -c : 오브젝트 파일 생성 지시 옵션

 

빌드 후, gdb에 연결하여 확인해보자.

(gdb) file build/navilos.axf
A program is being debugged already.
Are you sure you want to change the file? (y or n) y
Reading symbols from build/navilos.axf...
(gdb) s
vector_end () at boot/Entry.S:30
30	        MRS r0, cpsr
(gdb) s
31	        BIC r1, r0, #0x1F
(gdb) s
32	        ORR r1, r1, #ARM_MODE_BIT_SVC
(gdb) s
33	        MSR cpsr, r1
(gdb) s
34	        LDR sp, =SVC_STACK_TOP
(gdb) s
vector_end () at boot/Entry.S:36
36	        MRS r0, cpsr
(gdb) i r
r0             0x400001d3          1073742291
r1             0x400001d3          1073742291
r2             0x0                 0
r3             0x0                 0
r4             0x0                 0
r5             0x0                 0
r6             0x0                 0
r7             0x0                 0
r8             0x0                 0
r9             0x0                 0
r10            0x0                 0
r11            0x0                 0
r12            0x0                 0
sp             0x3ffffc            0x3ffffc
lr             0x0                 0
pc             0x50                0x50 <vector_end+20>
cpsr           0x400001d3          1073742291
...중략

SVC 모드 스택이 설정된 모습이다.

SVC 모드 스택은 0x0030 0000부터 0x003F FFFF까지 메모리 주소 영역에 해당한다. 패딩값으로 4바이트를 두었으므로, 스택 포인터가 저장되어야 할 값이 0x003F FFFC이다. sp에 저장된 값(0x3ffffc)과 일치한다. 

CPSR 의 마지막 바이트(M 영역)가 0xd3이다. 2진수로 나타내면 1101 0011이고, 하위 5비트만 보면 10011 (SVC 동작모드 비트)이다. 동작 모드 값도 제대로 들어있다.

 

gdb에서 step 명령으로 한줄씩 진행하면서 다른 동작 모드도 확인할 수 있다. 골자는 동일하다.

 

실무에서는 어셈블리어로 해야하는 일이 더 많다. HW 시스템 클럭 설정, 메모리 컨트롤러 초기화 등. 해당 프로젝트에서는 하지 않는다.

메인으로 진입하기

C언어의 entry point는 관습적으로 main()함수이다. 

리셋 핸들러에서 메모리 세팅이 끝나면 main으로 분기하게 하는 것이다.

#include "ARMv7AR.h"
#include "MemoryMap.h"

.text
    .code 32

    .global vector_start
    .global vector_end

    vector_start:
        LDR PC, reset_handler_addr
        LDR PC, undef_handler_addr
        LDR PC, svc_handler_addr
        LDR PC, pftch_abt_handler_addr
        LDR PC, data_abt_handler_addr
        B   .
        LDR PC, irq_handler_addr
        LDR PC, fiq_handler_addr

        reset_handler_addr:     .word reset_handler
        undef_handler_addr:     .word dummy_handler
        svc_handler_addr:       .word dummy_handler
        pftch_abt_handler_addr: .word dummy_handler
        data_abt_handler_addr:  .word dummy_handler
        irq_handler_addr:       .word dummy_handler
        fiq_handler_addr:       .word dummy_handler
    vector_end:

    reset_handler:
        MRS r0, cpsr
        BIC r1, r0, #0x1F
        ORR r1, r1, #ARM_MODE_BIT_SVC
        MSR cpsr, r1
        LDR sp, =SVC_STACK_TOP

        MRS r0, cpsr
        BIC r1, r0, #0x1F
        ORR r1, r1, #ARM_MODE_BIT_IRQ
        MSR cpsr, r1
        LDR sp, =IRQ_STACK_TOP

        MRS r0, cpsr
        BIC r1, r0, #0x1F
        ORR r1, r1, #ARM_MODE_BIT_FIQ
        MSR cpsr, r1
        LDR sp, =FIQ_STACK_TOP

        MRS r0, cpsr
        BIC r1, r0, #0x1F
        ORR r1, r1, #ARM_MODE_BIT_ABT
        MSR cpsr, r1
        LDR sp, =ABT_STACK_TOP

        MRS r0, cpsr
        BIC r1, r0, #0x1F
        ORR r1, r1, #ARM_MODE_BIT_UND
        MSR cpsr, r1
        LDR sp, =UND_STACK_TOP

        MRS r0, cpsr
        BIC r1, r0, #0x1F
        ORR r1, r1, #ARM_MODE_BIT_SYS
        MSR cpsr, r1
        LDR sp, =USRSYS_STACK_TOP

        BL  main

    dummy_handler:
        B .
.end

main 함수는 동작확인용으로 100MB 메모리 주소영역 (0x0640 0000)에 특정 값을 쓰는 것이다. 특정 값으로 long 타입 크기를 쓰는 동작을 해두자.

#include "stdint.h"

void main(void)
{
    uint32_t* dummyAddr = (uint32_t*)(1024*1024*100);
    *dummyAddr = sizeof(long);
}

32비트 ARM 머신은 long의 크기가 4바이트이다. 따라서 숫자 4가 메모리 주소 0x0640 0000 에 저장될 것이다.

 

참고로, 임베디드 환경에서는 표준 라이브러리 중에서도 필요한 파일만 넣어주는 듯 하다.

접는 글에 저자가 넣어둔 stdint.h 파일을 두겠다.

/* Copyright (C) 1997-2016 Free Software Foundation, Inc.
   This file is part of the GNU C Library.

   The GNU C Library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 2.1 of the License, or (at your option) any later version.

   The GNU C Library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public
   License along with the GNU C Library; if not, see
   <http://www.gnu.org/licenses/>.  */

/*
 *	ISO C99: 7.18 Integer types <stdint.h>
 */

#ifndef _STDINT_H
#define _STDINT_H	1

/* Exact integral types.  */

/* Signed.  */

/* There is some amount of overlap with <sys/types.h> as known by inet code */
#ifndef __int8_t_defined
# define __int8_t_defined
typedef signed char		int8_t;
typedef short int		int16_t;
typedef int			int32_t;
# if __WORDSIZE == 64
typedef long int		int64_t;
# else
__extension__
typedef long long int		int64_t;
# endif
#endif

/* Unsigned.  */
typedef unsigned char		uint8_t;
typedef unsigned short int	uint16_t;
#ifndef __uint32_t_defined
typedef unsigned int		uint32_t;
# define __uint32_t_defined
#endif
#if __WORDSIZE == 64
typedef unsigned long int	uint64_t;
#else
__extension__
typedef unsigned long long int	uint64_t;
#endif


/* Small types.  */

/* Signed.  */
typedef signed char		int_least8_t;
typedef short int		int_least16_t;
typedef int			int_least32_t;
#if __WORDSIZE == 64
typedef long int		int_least64_t;
#else
__extension__
typedef long long int		int_least64_t;
#endif

/* Unsigned.  */
typedef unsigned char		uint_least8_t;
typedef unsigned short int	uint_least16_t;
typedef unsigned int		uint_least32_t;
#if __WORDSIZE == 64
typedef unsigned long int	uint_least64_t;
#else
__extension__
typedef unsigned long long int	uint_least64_t;
#endif


/* Fast types.  */

/* Signed.  */
typedef signed char		int_fast8_t;
#if __WORDSIZE == 64
typedef long int		int_fast16_t;
typedef long int		int_fast32_t;
typedef long int		int_fast64_t;
#else
typedef int			int_fast16_t;
typedef int			int_fast32_t;
__extension__
typedef long long int		int_fast64_t;
#endif

/* Unsigned.  */
typedef unsigned char		uint_fast8_t;
#if __WORDSIZE == 64
typedef unsigned long int	uint_fast16_t;
typedef unsigned long int	uint_fast32_t;
typedef unsigned long int	uint_fast64_t;
#else
typedef unsigned int		uint_fast16_t;
typedef unsigned int		uint_fast32_t;
__extension__
typedef unsigned long long int	uint_fast64_t;
#endif


/* Types for `void *' pointers.  */
#if __WORDSIZE == 64
# ifndef __intptr_t_defined
typedef long int		intptr_t;
#  define __intptr_t_defined
# endif
typedef unsigned long int	uintptr_t;
#else
# ifndef __intptr_t_defined
typedef int			intptr_t;
#  define __intptr_t_defined
# endif
typedef unsigned int		uintptr_t;
#endif


/* Largest integral types.  */
#if __WORDSIZE == 64
typedef long int		intmax_t;
typedef unsigned long int	uintmax_t;
#else
__extension__
typedef long long int		intmax_t;
__extension__
typedef unsigned long long int	uintmax_t;
#endif


# if __WORDSIZE == 64
#  define __INT64_C(c)	c ## L
#  define __UINT64_C(c)	c ## UL
# else
#  define __INT64_C(c)	c ## LL
#  define __UINT64_C(c)	c ## ULL
# endif

/* Limits of integral types.  */

/* Minimum of signed integral types.  */
# define INT8_MIN		(-128)
# define INT16_MIN		(-32767-1)
# define INT32_MIN		(-2147483647-1)
# define INT64_MIN		(-__INT64_C(9223372036854775807)-1)
/* Maximum of signed integral types.  */
# define INT8_MAX		(127)
# define INT16_MAX		(32767)
# define INT32_MAX		(2147483647)
# define INT64_MAX		(__INT64_C(9223372036854775807))

/* Maximum of unsigned integral types.  */
# define UINT8_MAX		(255)
# define UINT16_MAX		(65535)
# define UINT32_MAX		(4294967295U)
# define UINT64_MAX		(__UINT64_C(18446744073709551615))


/* Minimum of signed integral types having a minimum size.  */
# define INT_LEAST8_MIN		(-128)
# define INT_LEAST16_MIN	(-32767-1)
# define INT_LEAST32_MIN	(-2147483647-1)
# define INT_LEAST64_MIN	(-__INT64_C(9223372036854775807)-1)
/* Maximum of signed integral types having a minimum size.  */
# define INT_LEAST8_MAX		(127)
# define INT_LEAST16_MAX	(32767)
# define INT_LEAST32_MAX	(2147483647)
# define INT_LEAST64_MAX	(__INT64_C(9223372036854775807))

/* Maximum of unsigned integral types having a minimum size.  */
# define UINT_LEAST8_MAX	(255)
# define UINT_LEAST16_MAX	(65535)
# define UINT_LEAST32_MAX	(4294967295U)
# define UINT_LEAST64_MAX	(__UINT64_C(18446744073709551615))


/* Minimum of fast signed integral types having a minimum size.  */
# define INT_FAST8_MIN		(-128)
# if __WORDSIZE == 64
#  define INT_FAST16_MIN	(-9223372036854775807L-1)
#  define INT_FAST32_MIN	(-9223372036854775807L-1)
# else
#  define INT_FAST16_MIN	(-2147483647-1)
#  define INT_FAST32_MIN	(-2147483647-1)
# endif
# define INT_FAST64_MIN		(-__INT64_C(9223372036854775807)-1)
/* Maximum of fast signed integral types having a minimum size.  */
# define INT_FAST8_MAX		(127)
# if __WORDSIZE == 64
#  define INT_FAST16_MAX	(9223372036854775807L)
#  define INT_FAST32_MAX	(9223372036854775807L)
# else
#  define INT_FAST16_MAX	(2147483647)
#  define INT_FAST32_MAX	(2147483647)
# endif
# define INT_FAST64_MAX		(__INT64_C(9223372036854775807))

/* Maximum of fast unsigned integral types having a minimum size.  */
# define UINT_FAST8_MAX		(255)
# if __WORDSIZE == 64
#  define UINT_FAST16_MAX	(18446744073709551615UL)
#  define UINT_FAST32_MAX	(18446744073709551615UL)
# else
#  define UINT_FAST16_MAX	(4294967295U)
#  define UINT_FAST32_MAX	(4294967295U)
# endif
# define UINT_FAST64_MAX	(__UINT64_C(18446744073709551615))


/* Values to test for integral types holding `void *' pointer.  */
# if __WORDSIZE == 64
#  define INTPTR_MIN		(-9223372036854775807L-1)
#  define INTPTR_MAX		(9223372036854775807L)
#  define UINTPTR_MAX		(18446744073709551615UL)
# else
#  define INTPTR_MIN		(-2147483647-1)
#  define INTPTR_MAX		(2147483647)
#  define UINTPTR_MAX		(4294967295U)
# endif


/* Minimum for largest signed integral type.  */
# define INTMAX_MIN		(-__INT64_C(9223372036854775807)-1)
/* Maximum for largest signed integral type.  */
# define INTMAX_MAX		(__INT64_C(9223372036854775807))

/* Maximum for largest unsigned integral type.  */
# define UINTMAX_MAX		(__UINT64_C(18446744073709551615))


/* Limits of other integer types.  */

/* Limits of `ptrdiff_t' type.  */
# if __WORDSIZE == 64
#  define PTRDIFF_MIN		(-9223372036854775807L-1)
#  define PTRDIFF_MAX		(9223372036854775807L)
# else
#  define PTRDIFF_MIN		(-2147483647-1)
#  define PTRDIFF_MAX		(2147483647)
# endif

/* Limits of `sig_atomic_t'.  */
# define SIG_ATOMIC_MIN		(-2147483647-1)
# define SIG_ATOMIC_MAX		(2147483647)

/* Limit of `size_t' type.  */
# if __WORDSIZE == 64
#  define SIZE_MAX		(18446744073709551615UL)
# else
#  ifdef __WORDSIZE32_SIZE_ULONG
#   define SIZE_MAX		(4294967295UL)
#  else
#   define SIZE_MAX		(4294967295U)
#  endif
# endif

/* Limits of `wchar_t'.  */
# ifndef WCHAR_MIN
/* These constants might also be defined in <wchar.h>.  */
#  define WCHAR_MIN		__WCHAR_MIN
#  define WCHAR_MAX		__WCHAR_MAX
# endif

/* Limits of `wint_t'.  */
# define WINT_MIN		(0u)
# define WINT_MAX		(4294967295u)

/* Signed.  */
# define INT8_C(c)	c
# define INT16_C(c)	c
# define INT32_C(c)	c
# if __WORDSIZE == 64
#  define INT64_C(c)	c ## L
# else
#  define INT64_C(c)	c ## LL
# endif

/* Unsigned.  */
# define UINT8_C(c)	c
# define UINT16_C(c)	c
# define UINT32_C(c)	c ## U
# if __WORDSIZE == 64
#  define UINT64_C(c)	c ## UL
# else
#  define UINT64_C(c)	c ## ULL
# endif

/* Maximal type.  */
# if __WORDSIZE == 64
#  define INTMAX_C(c)	c ## L
#  define UINTMAX_C(c)	c ## UL
# else
#  define INTMAX_C(c)	c ## LL
#  define UINTMAX_C(c)	c ## ULL
# endif

#endif /* stdint.h */

 

빌드 관련한 파일의 수정이 있었으므로, Makefile을 수정하자.

ARCH = armv7-a
MCPU = cortex-a8

CC = arm-none-eabi-gcc
AS = arm-none-eabi-as
LD = arm-none-eabi-ld
OC = arm-none-eabi-objcopy

LINKER_SCRIPT = ./navilos.ld
MAP_FILE = build/navilos.map

ASM_SRCS = $(wildcard boot/*.S)
ASM_OBJS = $(patsubst boot/%.S, build/%.os, $(ASM_SRCS))

C_SRCS = $(wildcard boot/*.c)
C_OBJS = $(patsubst boot/%.c, build/%.o, $(C_SRCS))

INC_DIRS = -I include

navilos = build/navilos.axf
navilos_bin = build/navilos.bin

.PHONY: all clean run debug kill_qemu gdb

all: $(navilos)

clean:
	@rm -fr build
	
run: $(navilos)
	qemu-system-arm -M realview-pb-a8 -kernel $(navilos)
	
debug: $(navilos)
	qemu-system-arm -M realview-pb-a8 -kernel $(navilos) -S -gdb tcp::1234,ipv4 -display none -nographic

kill_qemu:
	pkill -9 qemu-system-arm
	
gdb:
	gdb-multiarch
	
$(navilos): $(ASM_OBJS) $(C_OBJS) $(LINKER_SCRIPT)
	$(LD) -n -T $(LINKER_SCRIPT) -o $(navilos) $(ASM_OBJS) $(C_OBJS) -Map=$(MAP_FILE)
	$(OC) -O binary $(navilos) $(navilos_bin)

build/%.os: $(ASM_SRCS)
	mkdir -p $(shell dirname $@)
	$(CC) -march=$(ARCH) -mcpu=$(MCPU) $(INC_DIRS) -c -g -o $@ $<

build/%.o: $(C_SRCS)
	mkdir -p $(shell dirname $@)
	$(CC) -march=$(ARCH) -mcpu=$(MCPU) $(INC_DIRS) -c -g -o $@ $<

 

무엇이 바뀌었는지 아래서 확인하자.

ARCH = armv7-a
MCPU = cortex-a8

CC = arm-none-eabi-gcc
AS = arm-none-eabi-as
LD = arm-none-eabi-ld
OC = arm-none-eabi-objcopy

LINKER_SCRIPT = ./navilos.ld
+MAP_FILE = build/navilos.map

ASM_SRCS = $(wildcard boot/*.S)
-ASM_OBJS = $(patsubst boot/%.S, build/%.o, $(ASM_SRCS))
+ASM_OBJS = $(patsubst boot/%.S, build/%.os, $(ASM_SRCS))

+C_SRCS = $(wildcard boot/*.c)
+C_OBJS = $(patsubst boot/%.c, build/%.o, $(C_SRCS))

INC_DIRS = include
+INC_DIRS = -I include

navilos = build/navilos.axf
navilos_bin = build/navilos.bin

.PHONY: all clean run debug kill_qemu gdb

all: $(navilos)

clean:
	@rm -fr build
	
run: $(navilos)
	qemu-system-arm -M realview-pb-a8 -kernel $(navilos)
	
debug: $(navilos)
	qemu-system-arm -M realview-pb-a8 -kernel $(navilos) -S -gdb tcp::1234,ipv4 -display none -nographic

kill_qemu:
	pkill -9 qemu-system-arm
	
gdb:
	gdb-multiarch

-$(navilos): $(ASM_OBJS) $(LINKER_SCRIPT)
-	$(LD) -n -T $(LINKER_SCRIPT) -o $(navilos) $(ASM_OBJS)
+$(navilos): $(ASM_OBJS) $(C_OBJS) $(LINKER_SCRIPT)
+	$(LD) -n -T $(LINKER_SCRIPT) -o $(navilos) $(ASM_OBJS) $(C_OBJS) -Map=$(MAP_FILE)
	$(OC) -O binary $(navilos) $(navilos_bin)

-build/%.o: boot/%.S
+build/%.os: $(ASM_SRCS)
	mkdir -p $(shell dirname $@)
-	$(CC) -march=$(ARCH) -mcpu=$(MCPU) -I $(INC_DIRS) -c -g -o $@ $<
+	$(CC) -march=$(ARCH) -mcpu=$(MCPU) $(INC_DIRS) -c -g -o $@ $<
    

+build/%.o: $(C_SRCS)
+	mkdir -p $(shell dirname $@)
+	$(CC) -march=$(ARCH) -mcpu=$(MCPU) $(INC_DIRS) -c -g -o $@ $<

 

여태까지 해온것처럼, 빌드 후 qemu에서 동작시키고 gdb로 연결하여 확인하자.

main에서 작성한 것처럼, 메모리 100MB 위치(메모리 주소 0x0640 0000)값에 4가 저장되어있는지 확인해보자.

(gdb) target remote:1234
Remote debugging using :1234
warning: No executable has been specified and target does not support
determining executable automatically.  Try using the "file" command.
0x00000000 in ?? ()
(gdb) c
Continuing.
^C
Program received signal SIGINT, Interrupt.
0x000000b8 in ?? ()
(gdb) x/8wx 0x06400000
0x6400000:	0x00000004	0x00000000	0x00000000	0x00000000
0x6400010:	0x00000000	0x00000000	0x00000000	0x00000000

제대로 동작한다.

⭐️5장 UART

HW를 제어해보자. 처음 제어해볼 HW는 UART이다. UART(Universal Asynchronous Receiver/Transmitter)는 범용 비동기화 송수신기로 해석할 수 있다. UART를 제어함으로써, gdb가 아닌 터미널 화면으로 HW와 상호작용 할 수 있을 것이다.

문자 전용 입출력 프로토콜은 아니고, 어떤 데이터 값이든 가능하다. 터미널 프로그램을 UART 포트와 연결하면 UART를 통해 받은 아스키 코드를, 그 코드에 해당하는 문자로 화면에 출력한다.

데이터시트로 파악하기

RealViewPB에는 PL011이라는 UART HW 모듈이 붙어있다. PL011의 데이터시트도 ARM에서 제공한다. (참고로 RealViewPB에서 UART의 기본 주소는 0x1000 9000 이다.)

 

프로그래머가 사용하는 관점에서의 챕터도 친절하게 설명되어있다. 

https://developer.arm.com/documentation/ddi0183/g/programmers-model?lang=en

 

Documentation – Arm Developer

 

developer.arm.com

여러개의 레지스터를 확인 할 수 있다

PL011 의 레지스터 요약 1 of 2
PL011 의 레지스터 요약 2 of 2

대표적인 레지스터 UARTDR를 예로들어 어떻게 사용하는지 파악해보자.

https://developer.arm.com/documentation/ddi0183/g/programmers-model/register-descriptions/data-register--uartdr?lang=en

 

Documentation – Arm Developer

 

developer.arm.com

UARTDR의 설명
UARTDR 레지스터의 bits 역할설명

  • UARTDR (UART Data Register)는 데이터 레지스터이다.
  • 0~7번 비트까지 총 8비트는 입출력 데이터가 사용하는 레지스터이다. 한번에 8비트, 즉 1바이트씩 통신 가능한 HW이다.
  • 8~11번 비트 각각은 에러를 의미한다. 프레임에러, 패리티 에러, 브레이크 에러, 오버런 에러가 있고, 설명이 있다. 대상 에러가 발생하면 해당 비트 값이 1로 바뀐다.

코드로 옮기기

파악했으니, SW로써 만들기 위해 코드로 옮겨보자.

C언어 스타일 중 구조체를 이용하겠다.

 

SW로 HW를 제어한다는 것은 레지스터에 접근하여 값을 수정하는 것이라 말할 수 있다.

RealViewPB에서 UART의 메모리 주소는 0x1000 9000 이고, 구성 레지스터들을 구조체화 하자.

 

Uart.h

#ifndef HAL_RVPB_UART_H_
#define HAL_RVPB_UART_H_

typedef union UARTDR_t
{
    uint32_t all;
    struct {
        uint32_t DATA:8;    // 7:0
        uint32_t FE:1;      // 8
        uint32_t PE:1;      // 9
        uint32_t BE:1;      // 10
        uint32_t OE:1;      // 11
        uint32_t reserved:20;
    } bits;
} UARTDR_t;

typedef union UARTRSR_t
{
    uint32_t all;
    struct {
        uint32_t FE:1;      // 0
        uint32_t PE:1;      // 1
        uint32_t BE:1;      // 2
        uint32_t OE:1;      // 3
        uint32_t reserved:28;
    } bits;
} UARTRSR_t;

typedef union UARTFR_t
{
    uint32_t all;
    struct {
        uint32_t CTS:1;     // 0
        uint32_t DSR:1;     // 1
        uint32_t DCD:1;     // 2
        uint32_t BUSY:1;    // 3
        uint32_t RXFE:1;    // 4
        uint32_t TXFF:1;    // 5
        uint32_t RXFF:1;    // 6
        uint32_t TXFE:1;    // 7
        uint32_t RI:1;      // 8
        uint32_t reserved:23;
    } bits;
} UARTFR_t;

typedef union UARTILPR_t
{
    uint32_t all;
    struct {
        uint32_t ILPDVSR:8; // 7:0
        uint32_t reserved:24;
    } bits;
} UARTILPR_t;

typedef union UARTIBRD_t
{
    uint32_t all;
    struct {
        uint32_t BAUDDIVINT:16; // 15:0
        uint32_t reserved:16;
    } bits;
} UARTIBRD_t;

typedef union UARTFBRD_t
{
    uint32_t all;
    struct {
        uint32_t BAUDDIVFRAC:6; // 5:0
        uint32_t reserved:26;
    } bits;
} UARTFBRD_t;

typedef union UARTLCR_H_t
{
    uint32_t all;
    struct {
        uint32_t BRK:1;     // 0
        uint32_t PEN:1;     // 1
        uint32_t EPS:1;     // 2
        uint32_t STP2:1;    // 3
        uint32_t FEN:1;     // 4
        uint32_t WLEN:2;    // 6:5
        uint32_t SPS:1;     // 7
        uint32_t reserved:24;
    } bits;
} UARTLCR_H_t;

typedef union UARTCR_t
{
    uint32_t all;
    struct {
        uint32_t UARTEN:1;      // 0
        uint32_t SIREN:1;       // 1
        uint32_t SIRLP:1;       // 2
        uint32_t Reserved1:4;   // 6:3
        uint32_t LBE:1;         // 7
        uint32_t TXE:1;         // 8
        uint32_t RXE:1;         // 9
        uint32_t DTR:1;         // 10
        uint32_t RTS:1;         // 11
        uint32_t Out1:1;        // 12
        uint32_t Out2:1;        // 13
        uint32_t RTSEn:1;       // 14
        uint32_t CTSEn:1;       // 15
        uint32_t reserved2:16;
    } bits;
} UARTCR_t;

typedef union UARTIFLS_t
{
    uint32_t all;
    struct {
        uint32_t TXIFLSEL:3;    // 2:0
        uint32_t RXIFLSEL:3;    // 5:3
        uint32_t reserved:26;
    } bits;
} UARTIFLS_t;

typedef union UARTIMSC_t
{
    uint32_t all;
    struct {
        uint32_t RIMIM:1;   // 0
        uint32_t CTSMIM:1;  // 1
        uint32_t DCDMIM:1;  // 2
        uint32_t DSRMIM:1;  // 3
        uint32_t RXIM:1;    // 4
        uint32_t TXIM:1;    // 5
        uint32_t RTIM:1;    // 6
        uint32_t FEIM:1;    // 7
        uint32_t PEIM:1;    // 8
        uint32_t BEIM:1;    // 9
        uint32_t OEIM:1;    // 10
        uint32_t reserved:21;
    } bits;
} UARTIMSC_t;

typedef union UARTRIS_t
{
    uint32_t all;
    struct {
        uint32_t RIRMIS:1;  // 0
        uint32_t CTSRMIS:1; // 1
        uint32_t DCDRMIS:1; // 2
        uint32_t DSRRMIS:1; // 3
        uint32_t RXRIS:1;   // 4
        uint32_t TXRIS:1;   // 5
        uint32_t RTRIS:1;   // 6
        uint32_t FERIS:1;   // 7
        uint32_t PERIS:1;   // 8
        uint32_t BERIS:1;   // 9
        uint32_t OERIS:1;   // 10
        uint32_t reserved:21;
    } bits;
} UARTRIS_t;

typedef union UARTMIS_t
{
    uint32_t all;
    struct {
        uint32_t RIMMIS:1;  // 0
        uint32_t CTSMMIS:1; // 1
        uint32_t DCDMMIS:1; // 2
        uint32_t DSRMMIS:1; // 3
        uint32_t RXMIS:1;   // 4
        uint32_t TXMIS:1;   // 5
        uint32_t RTMIS:1;   // 6
        uint32_t FEMIS:1;   // 7
        uint32_t PEMIS:1;   // 8
        uint32_t BEMIS:1;   // 9
        uint32_t OEMIS:1;   // 10
        uint32_t reserved:21;
    } bits;
} UARTMIS_t;

typedef union UARTICR_t
{
    uint32_t all;
    struct {
        uint32_t RIMIC:1;   // 0
        uint32_t CTSMIC:1;  // 1
        uint32_t DCDMIC:1;  // 2
        uint32_t DSRMIC:1;  // 3
        uint32_t RXIC:1;    // 4
        uint32_t TXIC:1;    // 5
        uint32_t RTIC:1;    // 6
        uint32_t FEIC:1;    // 7
        uint32_t PEIC:1;    // 8
        uint32_t BEIC:1;    // 9
        uint32_t OEIC:1;    // 10
        uint32_t reserved:21;
    } bits;
} UARTICR_t;

typedef union UARTDMACR_t
{
    uint32_t all;
    struct {
        uint32_t RXDMAE:1;  // 0
        uint32_t TXDMAE:1;  // 1
        uint32_t DMAONERR:1;// 2
        uint32_t reserved:29;
    } bits;
} UARTDMACR_t;

typedef struct PL011_t
{
    UARTDR_t    uartdr;         //0x000
    UARTRSR_t   uartrsr;        //0x004
    uint32_t    reserved0[4];   //0x008-0x014
    UARTFR_t    uartfr;         //0x018
    uint32_t    reserved1;      //0x01C
    UARTILPR_t  uartilpr;       //0x020
    UARTIBRD_t  uartibrd;       //0x024
    UARTFBRD_t  uartfbrd;       //0x028
    UARTLCR_H_t uartlcr_h;      //0x02C
    UARTCR_t    uartcr;         //0x030
    UARTIFLS_t  uartifls;       //0x034
    UARTIMSC_t  uartimsc;       //0x038
    UARTRIS_t   uartris;        //0x03C
    UARTMIS_t   uartmis;        //0x040
    UARTICR_t   uarticr;        //0x044
    UARTDMACR_t uartdmacr;      //0x048
} PL011_t;

#define UART_BASE_ADDRESS0       0x10009000
#define UART_INTERRUPT0          44

#endif /* HAL_RVPB_UART_H_ */

UART의 내부(레지스터) SW로 옮겼다. 이제, UART 자체로 묶어보자.

 

Regs.c

#include "stdint.h"
#include "Uart.h"

volatile PL011_t* Uart = (PL011_t*)UART_BASE_ADDRESS0;

 

 

프로젝트 구조를 HAL(Hardware Abstraction Layer, 하드웨어 추상화 계층) 을 두고, 그 아래 RealViewPB 라는 특정 HW를 위한 디렉터리를 두자.

HAL 에서 인터페이스를 두는 것이다. UART 에 대해 인터페이스를 두어 동작하는 시나리오는 아래처럼 작성할수 있다.

── hal
   ├── HalUart.h
   └── rvpb
       └── Uart.c

클라이언트 코드에서는 HalUart.h에 정의된 인터페이스를 쓰고, 구현은 특정 HW디렉터리에 구현하는 것이다.

 

코드를 작성해보자.

 

HalUart.h

#ifndef HAL_HALUART_H_
#define HAL_HALUART_H_

void Hal_uart_init(void);
void Hal_uart_put_char(uint8_t ch);

#endif /* HAL_HALUART_H_ */

인터페이스 역할을 한다.

 

Uart.c

#include "stdint.h"
#include "Uart.h"
#include "HalUart.h"

extern volatile PL011_t* Uart;

void Hal_uart_init(void)
{
    // Enable UART
    Uart->uartcr.bits.UARTEN = 0;
    Uart->uartcr.bits.TXE = 1;
    Uart->uartcr.bits.RXE = 1;
    Uart->uartcr.bits.UARTEN = 1;
}

void Hal_uart_put_char(uint8_t ch)
{
    while(Uart->uartfr.bits.TXFF);
    Uart->uartdr.all = (ch & 0xFF);
}

인터페이스를 구현했다.

레지스터의 비트값을 수정하면서 하드웨어를 사용하는 모습을 볼 수 있다.

 

현재까지의 디렉터리 구조를 정리해보면 아래와 같다.

.
├── Makefile
├── boot
│   ├── Entry.S
│   └── Main.c
├── hal
│   ├── HalUart.h
│   └── rvpb
│       ├── Regs.c
│       ├── Uart.c
│       └── Uart.h
├── include
│   ├── ARMv7AR.h
│   ├── MemoryMap.h
│   └── stdint.h
└── navilos.ld

 

테스트해보자.

Main.c의 기존 main() 함수 속은 동작 확인 용이였으므로, 지우고  테스트용 코드를 작성하자.

결과로 문자 'N'이 100번 출력되어야한다. 

#include "stdint.h"

#include "HalUart.h"

static void Hw_init(void);

void main(void)
{
	Hw_init();

	uint32_t i = 100;
	while(i--)
	{
		Hal_uart_put_char('N');
	}
}

static void Hw_init(void)
{
	Hal_uart_init();
}

 

빌드 관련해서 수정사항이 생겼으니, Makefile을 수정하자.

ARCH = armv7-a
MCPU = cortex-a8

TARGET = rvpb

CC = arm-none-eabi-gcc
AS = arm-none-eabi-as
LD = arm-none-eabi-ld
OC = arm-none-eabi-objcopy

LINKER_SCRIPT = ./navilos.ld
MAP_FILE = build/navilos.map

ASM_SRCS = $(wildcard boot/*.S)
ASM_OBJS = $(patsubst boot/%.S, build/%.os, $(ASM_SRCS))

VPATH = boot \
        hal/$(TARGET)

C_SRCS  = $(notdir $(wildcard boot/*.c))
C_SRCS += $(notdir $(wildcard hal/$(TARGET)/*.c))
C_OBJS = $(patsubst %.c, build/%.o, $(C_SRCS))

INC_DIRS  = -I include 			\
            -I hal	   			\
            -I hal/$(TARGET)

CFLAGS = -c -g -std=c11

navilos = build/navilos.axf
navilos_bin = build/navilos.bin

.PHONY: all clean run debug kill_qemu gdb

all: $(navilos)

clean:
	@rm -fr build
	
run: $(navilos)
	qemu-system-arm -M realview-pb-a8 -kernel $(navilos) -nographic
	
debug: $(navilos)
	qemu-system-arm -M realview-pb-a8 -kernel $(navilos) -S -gdb tcp::1234,ipv4 -display none -nographic

kill_qemu:
	pkill -9 qemu-system-arm
	
gdb:
	gdb-multiarch
	
$(navilos): $(ASM_OBJS) $(C_OBJS) $(LINKER_SCRIPT)
	$(LD) -n -T $(LINKER_SCRIPT) -o $(navilos) $(ASM_OBJS) $(C_OBJS) -Map=$(MAP_FILE)
	$(OC) -O binary $(navilos) $(navilos_bin)

build/%.os: %.S
	mkdir -p $(shell dirname $@)
	$(CC) -march=$(ARCH) -mcpu=$(MCPU) $(INC_DIRS) $(CFLAGS) -o $@ $<

build/%.o: %.c
	mkdir -p $(shell dirname $@)
	$(CC) -march=$(ARCH) -mcpu=$(MCPU) $(INC_DIRS) $(CFLAGS) -o $@ $<

무엇이 수정되었을까?

ARCH = armv7-a
MCPU = cortex-a8

+TARGET = rvpb

CC = arm-none-eabi-gcc
AS = arm-none-eabi-as
LD = arm-none-eabi-ld
OC = arm-none-eabi-objcopy

LINKER_SCRIPT = ./navilos.ld
MAP_FILE = build/navilos.map

ASM_SRCS = $(wildcard boot/*.S)
ASM_OBJS = $(patsubst boot/%.S, build/%.os, $(ASM_SRCS))

-C_SRCS = $(wildcard boot/*.c)
-C_OBJS = $(patsubst boot/%.c, build/%.o, $(C_SRCS))
+VPATH = boot \
+        hal/$(TARGET)
+
+C_SRCS  = $(notdir $(wildcard boot/*.c))
+C_SRCS += $(notdir $(wildcard hal/$(TARGET)/*.c))
+C_OBJS = $(patsubst %.c, build/%.o, $(C_SRCS))

-INC_DIRS = -I include
+INC_DIRS  = -I include 			\
+            -I hal	   			\
+            -I hal/$(TARGET)

CFLAGS = -c -g -std=c11

navilos = build/navilos.axf
navilos_bin = build/navilos.bin

.PHONY: all clean run debug kill_qemu gdb

all: $(navilos)

clean:
	@rm -fr build
	
run: $(navilos)
-	qemu-system-arm -M realview-pb-a8 -kernel $(navilos)
+	qemu-system-arm -M realview-pb-a8 -kernel $(navilos) -nographic
	
debug: $(navilos)
	qemu-system-arm -M realview-pb-a8 -kernel $(navilos) -S -gdb tcp::1234,ipv4 -display none -nographic

kill_qemu:
	pkill -9 qemu-system-arm
	
gdb:
	gdb-multiarch
	
$(navilos): $(ASM_OBJS) $(C_OBJS) $(LINKER_SCRIPT)
	$(LD) -n -T $(LINKER_SCRIPT) -o $(navilos) $(ASM_OBJS) $(C_OBJS) -Map=$(MAP_FILE)
	$(OC) -O binary $(navilos) $(navilos_bin)

-build/%.os: $(ASM_SRCS)
+build/%.os: %.S
	mkdir -p $(shell dirname $@)
-	$(CC) -march=$(ARCH) -mcpu=$(MCPU) $(INC_DIRS) -c -g -o $@ $<
+	$(CC) -march=$(ARCH) -mcpu=$(MCPU) $(INC_DIRS) $(CFLAGS) -o $@ $<

-build/%.o: $(C_SRCS)
+build/%.o: %.c
	mkdir -p $(shell dirname $@)
-	$(CC) -march=$(ARCH) -mcpu=$(MCPU) $(INC_DIRS) -c -g -o $@ $<
+	$(CC) -march=$(ARCH) -mcpu=$(MCPU) $(INC_DIRS) $(CFLAGS) -o $@ $<

VPATH = boot \
        hal/$(TARGET)

으로 rvpb로 선택되도록 했다.

 

QEMU를 실행시키는 옵션의 변화에 -nographic을 주었다. (qemu-system-arm -M realview-pb-a8 -kernel $(navilos) -nographic) UART 입출력이 호스트의 콘솔과 연결된다. UART 입출력 컨테이너에서 터미널에 출력된다는 의미이다.

 

빌드 후, make run으로 실행시키자.

root@21d947b2e449:/project# make run
qemu-system-arm -M realview-pb-a8 -kernel build/navilos.axf -nographic
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5220:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2642:(snd_pcm_open_noupdate) Unknown PCM default
alsa: Could not initialize DAC
alsa: Failed to open `default':
alsa: Reason: No such file or directory
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5220:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2642:(snd_pcm_open_noupdate) Unknown PCM default
alsa: Could not initialize DAC
alsa: Failed to open `default':
alsa: Reason: No such file or directory
audio: Failed to create voice `lm4549.out'
NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN

문자 'N'이 100개 출력됨을 볼 수 있다.

Hello World!

printf() 구현 전에, 기초작업으로 문자열을 출력해보자.

  • 전략
    • 문자 한 개를 출력하는 함수를 반복 호출
    • 고려할 점: SW의 계층
      • 문자 한 개를 출력하는 함수는 UART 하드웨어에 직접 접근해야 구현 가능하다
      • 그러나, 문자열을 출력하는 함수는 UART 하드웨어를 직접 건드리는 작업이 아니다
      • UART 하드웨어에 직접 접근하는 함수를, 다시 호출하는 함수다
      • 앞으로 이런 류의 "기능 함수"들이 많이 추가될 것이므로, 계층을 나눈다

디렉터리 구조를 아래와 같이 구성하자.

.
├── Makefile
├── boot
│   ├── Entry.S
│   └── Main.c
├── hal
│   ├── HalUart.h
│   └── rvpb
│       ├── Regs.c
│       ├── Uart.c
│       └── Uart.h
├── include
│   ├── ARMv7AR.h
│   ├── MemoryMap.h
│   └── stdint.h
├── lib
│   ├── stdio.c
│   └── stdio.h
└── navilos.ld

 

stdio.h

#ifndef LIB_STDIO_H_
#define LIB_STDIO_H_

uint32_t putstr(const char* s);

#endif /* LIB_STDIO_H_ */

 

stdio.c

#include "stdint.h"
#include "HalUart.h"
#include "stdio.h"


uint32_t putstr(const char* s)
{
    uint32_t c = 0;
    while(*s)
    {
        Hal_uart_put_char(*s++);
        c++;
    }
    return c;
}

Hal_uart_put_char() 함수는 문자 하나를 Uart의 UARTDR 레지스터에 입력하는 함수다. 쉽게말해, 하나의 문자를 출력한다. 

해당 함수를 반복처리하여, 문자열을 출력하는 기능을 구현했다. 포인터 s가 0인 경우, 즉 NULL문자를 받으면 종료한다. 

 

main() 함수를 수정해서 테스트하자.

#include "stdint.h"
#include "HalUart.h"

#include "stdio.h"

static void Hw_init(void);

void main(void)
{
	Hw_init();

	uint32_t i = 100;
	while(i--)
	{
		Hal_uart_put_char('N');
	}
	Hal_uart_put_char('\n');

	putstr("Hello World!\n");
}

static void Hw_init(void)
{
	Hal_uart_init();
}

 

 

빌드에 관련한 파일이 늘어났다. Makefile을 수정하자.

ARCH = armv7-a
MCPU = cortex-a8

TARGET = rvpb

CC = arm-none-eabi-gcc
AS = arm-none-eabi-as
LD = arm-none-eabi-ld
OC = arm-none-eabi-objcopy

LINKER_SCRIPT = ./navilos.ld
MAP_FILE = build/navilos.map

ASM_SRCS = $(wildcard boot/*.S)
ASM_OBJS = $(patsubst boot/%.S, build/%.os, $(ASM_SRCS))

VPATH = boot \
        hal/$(TARGET)	\
        lib

C_SRCS  = $(notdir $(wildcard boot/*.c))
C_SRCS += $(notdir $(wildcard hal/$(TARGET)/*.c))
C_SRCS += $(notdir $(wildcard lib/*.c))
C_OBJS = $(patsubst %.c, build/%.o, $(C_SRCS))

INC_DIRS  = -I include 			\
            -I hal	   			\
            -I hal/$(TARGET)	\
            -I lib

CFLAGS = -c -g -std=c11

navilos = build/navilos.axf
navilos_bin = build/navilos.bin

.PHONY: all clean run debug kill_qemu gdb

all: $(navilos)

clean:
	@rm -fr build
	
run: $(navilos)
	qemu-system-arm -M realview-pb-a8 -kernel $(navilos) -nographic
	
debug: $(navilos)
	qemu-system-arm -M realview-pb-a8 -kernel $(navilos) -S -gdb tcp::1234,ipv4 -display none -nographic

kill_qemu:
	pkill -9 qemu-system-arm
	
gdb:
	gdb-multiarch
	
$(navilos): $(ASM_OBJS) $(C_OBJS) $(LINKER_SCRIPT)
	$(LD) -n -T $(LINKER_SCRIPT) -o $(navilos) $(ASM_OBJS) $(C_OBJS) -Map=$(MAP_FILE)
	$(OC) -O binary $(navilos) $(navilos_bin)

build/%.os: %.S
	mkdir -p $(shell dirname $@)
	$(CC) -march=$(ARCH) -mcpu=$(MCPU) $(INC_DIRS) $(CFLAGS) -o $@ $<

build/%.o: %.c
	mkdir -p $(shell dirname $@)
	$(CC) -march=$(ARCH) -mcpu=$(MCPU) $(INC_DIRS) $(CFLAGS) -o $@ $<

무엇이 수정되었을까?

ARCH = armv7-a
MCPU = cortex-a8

TARGET = rvpb

CC = arm-none-eabi-gcc
AS = arm-none-eabi-as
LD = arm-none-eabi-ld
OC = arm-none-eabi-objcopy

LINKER_SCRIPT = ./navilos.ld
MAP_FILE = build/navilos.map

ASM_SRCS = $(wildcard boot/*.S)
ASM_OBJS = $(patsubst boot/%.S, build/%.os, $(ASM_SRCS))

VPATH = boot \
-        hal/$(TARGET)
+        hal/$(TARGET)	\
+        lib

C_SRCS  = $(notdir $(wildcard boot/*.c))
C_SRCS += $(notdir $(wildcard hal/$(TARGET)/*.c))
+C_SRCS += $(notdir $(wildcard lib/*.c))
C_OBJS = $(patsubst %.c, build/%.o, $(C_SRCS))

INC_DIRS  = -I include 			\
            -I hal	   			\
-            -I hal/$(TARGET)	
+            -I hal/$(TARGET)	\
+            -I lib

CFLAGS = -c -g -std=c11

navilos = build/navilos.axf
navilos_bin = build/navilos.bin

.PHONY: all clean run debug kill_qemu gdb

all: $(navilos)

clean:
	@rm -fr build
	
run: $(navilos)
	qemu-system-arm -M realview-pb-a8 -kernel $(navilos) -nographic
	
debug: $(navilos)
	qemu-system-arm -M realview-pb-a8 -kernel $(navilos) -S -gdb tcp::1234,ipv4 -display none -nographic

kill_qemu:
	pkill -9 qemu-system-arm
	
gdb:
	gdb-multiarch
	
$(navilos): $(ASM_OBJS) $(C_OBJS) $(LINKER_SCRIPT)
	$(LD) -n -T $(LINKER_SCRIPT) -o $(navilos) $(ASM_OBJS) $(C_OBJS) -Map=$(MAP_FILE)
	$(OC) -O binary $(navilos) $(navilos_bin)

build/%.os: %.S
	mkdir -p $(shell dirname $@)
	$(CC) -march=$(ARCH) -mcpu=$(MCPU) $(INC_DIRS) $(CFLAGS) -o $@ $<

build/%.o: %.c
	mkdir -p $(shell dirname $@)
	$(CC) -march=$(ARCH) -mcpu=$(MCPU) $(INC_DIRS) $(CFLAGS) -o $@ $<

 

빌드하고, 실행해보면 정상동작을 확인 할 수 있다.

root@21d947b2e449:/project# make run
qemu-system-arm -M realview-pb-a8 -kernel build/navilos.axf -nographic
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5220:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2642:(snd_pcm_open_noupdate) Unknown PCM default
alsa: Could not initialize DAC
alsa: Failed to open `default':
alsa: Reason: No such file or directory
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5220:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2642:(snd_pcm_open_noupdate) Unknown PCM default
alsa: Could not initialize DAC
alsa: Failed to open `default':
alsa: Reason: No such file or directory
audio: Failed to create voice `lm4549.out'
NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN
Hello World!

UART로 입력 받기

UART로 입력 받으려면? 어떻게 해야할까? 출력 상황일때를 다시 보자.

  • UART로 출력할때 어떻게 했나
    • 보내기 버퍼가 비어있는지 확인
    • 비어있으면, 데이터 레지스터를 통해서 데이터를 보내기 버퍼로 보낸다
    • 그러면 하드웨어가 알아서 나머지 작업을 처리해주고
    • 하드웨어와 연결된 콘솔에 데이터가 나타난다
    • 현재까지 다룬 데이터가 아스키 코드였기에, 알파벳이 보일것

입력은 출력의 반대이다

  • 받기 버퍼가 채워져 있는지 확인
  • 받기 버퍼에 데이터가 있으면, 데이터 레지스터를 통해서 데이터를 하나 읽어온다
  • 데이터는 콘솔과 하드웨어를 통해 전달되어, 레지스터에서 펌웨어가 읽어가기만을 기다리고 있을 것이다.

비효율적이지만 직관적인 출력함수를 작성하자.

HalUart.h에 인터페이스를 추가하자

#ifndef HAL_HALUART_H_
#define HAL_HALUART_H_

void Hal_uart_init(void);
void Hal_uart_put_char(uint8_t ch);
+uint8_t Hal_uart_get_char(void);

#endif /* HAL_HALUART_H_ */

Hal_uart_get_char()함수의 인터페이스를 추가한 모습이다.

 

rvpb/Uart.c 에서 해당 함수를 구현하자.

uint8_t Hal_uart_get_char(void)
{
	uint8_t data;

	while (Uart->uartfr.bits.RXFE);
	
	// Check for an error flag
	if (Uart->uartdr.bits.BE || Uart->uartdr.bits.FE || Uart->uartdr.bits.OE || Uart->uartdr.bits.PE)
	{
		// Clear the error
		Uart->uartdr.bits.BE = 1; 
		Uart->uartdr.bits.FE = 1;
		Uart->uartdr.bits.OE = 1;
		Uart->uartdr.bits.PE = 1;
		return 0;
	}

	data = Uart->uartdr.bits.DATA;
	return data;
}

직관적이지만, 성능이 썩 좋지 않다. 

  • if 문에서 각 에러 플래그 4개를 개별적으로 확인하기 때문에 성능이 좋지 않다
    • 레지스터에 대한 접근이 4번 일어나고, 각각 비트 플래그를 확인하므로,
    • 즉, 비트 시프트 연산 4번 그리고 0이 아닌지 확인하는 연산이 4번 일어난다.
  • 물론 컴파일러가 최적화해줄 여지가 있으나, 그럼에도 생성되는 코드는 많을 것
  • 또한, 각 에러 플래그를 클리어하는 코드에서도
    •  레지스터 접근, 비트 시프트, 데이터 복사가 각각 4번씩 발생하므로 비효율적이다
uint8_t Hal_uart_get_char(void)
{
	uint8_t data;

	while (Uart->uartfr.bits.RXFE)
	
	// Check for an error flag	
-	if (Uart->uartdr.bits.BE || Uart->uartdr.bits.FE ||
-		Uart->uartdr.bits.OE || Uart->uartdr.bits.PE)
+	if (Uart->uartdr.all & 0xFFFFFF00)
	{
		// Clear the error
-		Uart->uartrsr.bits.BE = 1;
-		Uart->uartrsr.bits.FE = 1;
-		Uart->uartrsr.bits.OE = 1;
-		Uart->uartrsr.bits.PE = 1;
+		Uart->uartrsr.all = 0xFF;
		return 0;
	}
	
	data = Uart->uartdr.bits.DATA;
	return data;
}
  • 32비트 레지스터 자체로 접근하면서, 32비트 값을 직접 비교하고 입력하는 코드로 변경
    • 레지스터 접근: 2회
    • 비교연산: 1회
    • 데이터 입력 연산: 1회

 

여기서 더 최적화가 가능할까? 자고로 펌웨어란 마른수건도 쥐어짜듯 최적화를 해야한다.

uint8_t Hal_uart_get_char(void)
{
	uint8_t data;

	while (Uart->uartfr.bits.RXFE)

+	data = Uart->uartdr.all;
	
	// Check for an error flag
-	if (Uart->uartdr.all & 0xFFFFFF00)
+	if (data & 0xFFFFFF00)
	{
		// Clear the error
		Uart->uartrsr.all = 0xFF;
		return 0;
	}

-	data = Uart->uartdr.bits.DATA;
-	return data;
+	return (uint8_t)(data & 0xFF);
}
  • UARTDR 접근: 2회
    • 에러 플래그 읽기위해 접근
    • 데이터 읽기위해 접근
    • 그 중간에 UARTDR은 변경되지 않는다고 간주한다
    • 그러면, UARTDR의 값은 한 번 읽어와서 재사용하자
      • 하드웨어 레지스터에 접근하는데 걸리는 시간 >>> 변수의 값에 접근하는데 걸리는 시간
        • 로컬 변수는 보통 스택에 생성되거나, ARM의 범용 레지스터에 할당된다
        • 둘 다, 다른 하드웨어 레지스터에 접근하는것에 비해 수십~수백배 빠름
      • 또한 이미 값을 저장해 놓은 로컬 변수를 사용하므로, 바이너리 크기가 줄어들 수도 있음

 

최종적으로 최적화한 Hal_uart_get_char() 함수이다.

uint8_t Hal_uart_get_char(void)
{
    uint32_t data;

    while(Uart->uartfr.bits.RXFE);

    data = Uart->uartdr.all;

    // Check for an error flag
    if (data & 0xFFFFFF00)
    {
        // Clear the error
        Uart->uartrsr.all = 0xFF;
        return 0;
    }


    return (uint8_t)(data & 0xFF);
}

 

main() 함수를 수정해서 테스트해보자.

#include "stdint.h"
#include "HalUart.h"

#include "stdio.h"

static void Hw_init(void);

void main(void)
{
    Hw_init();

    uint32_t i = 100;
    while(i--)
    {
        Hal_uart_put_char('N');
    }
    Hal_uart_put_char('\n');

    putstr("Hello World!\n");

    i = 100;
    while(i--)
    {
        uint8_t ch = Hal_uart_get_char();
        Hal_uart_put_char(ch);
    }
}

static void Hw_init(void)
{
    Hal_uart_init();
}

 

100자까지, 타이핑하는대로 echo한다.

root@21d947b2e449:/project# make run
qemu-system-arm -M realview-pb-a8 -kernel build/navilos.axf -nographic
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5220:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2642:(snd_pcm_open_noupdate) Unknown PCM default
alsa: Could not initialize DAC
alsa: Failed to open `default':
alsa: Reason: No such file or directory
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5220:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2642:(snd_pcm_open_noupdate) Unknown PCM default
alsa: Could not initialize DAC
alsa: Failed to open `default':
alsa: Reason: No such file or directory
audio: Failed to create voice `lm4549.out'
NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN
Hello World!
NNNNNNNNNNNNNNNNNN

N을 타이핑 했더니 그대로 출력된다.

printf 만들기

  • 문자열 출력하는것과 printf()함수와의 결정적 차이
    • 포맷 지정
    • 로그, 디버깅 등 유용. 즉, 필수로 구현해야함
    • 이외의 printf의 기능은 매우 많지만, 필요한 것만 구현하자
  • printf()라는 이름을 직접 사용하면, 
    • GCC를 포함한 많은 컴파일러가 별다른 옵션을 주지 않는 한 printf() 함수를 단순하게 사용하면 최적화 과정에서 printf()함수를 puts()함수로 바꿔버림
    • 그래서 보통 잠재적인 문제를 피하기 위해서 표준 라이브러리 함수를 다시 만들어 사용하는 경우에는 함수 이름을 표준 라이브러리 함수와 똑같게 만들지 않는다
    • debug_printf()라고 명명하도록 하겠다
  • printf()를 아무리 단순하게 만들어도, 매우 복잡한 함수이므로 단계별로 만들어야 한다

 

debug_printf()의 인터페이스부터 작성하자.

 

stdio.h

#ifndef LIB_STDIO_H_
#define LIB_STDIO_H_

uint32_t putstr(const char* s);
+uint32_t debug_printf(const char* format, ...);

#endif /* LIB_STDIO_H_ */

 

stdio.c

#define PRINTF_BUF_LEN  1024

static char printf_buf[PRINTF_BUF_LEN];   // 1KB

uint32_t debug_printf(const char* format, ...)
{
	va_list args;
	va_start(args, format);
	vsprintf(printf_buf, format, args);
	va_end(args);

	return putstr(printf_buf);
}

형식문자는 vsprintf()함수에서 처리하겠다.

 

C언어는 가변인자를 stdarg.h에 있는 va_start, va_end, va_arg매크로와 va_list 라는 자료형을 이용하여 처리한다. 표준라이브러리는 아니고 이는 컴파일러의 빌트힌 함수로 지원된다. 따라서 전통적인 이름으로 재정의해서 사용하자.

 

stdarg.h

#ifndef INCLUDE_STDARG_H_
#define INCLUDE_STDARG_H_

typedef __builtin_va_list va_list;

#define va_start(v,l)   __builtin_va_start(v,l)
#define va_end(v)       __builtin_va_end(v)
#define va_arg(v,l)     __builtin_va_arg(v,l)

#endif /* INCLUDE_STDARG_H_ */

 

vsprintf()함수의 인터페이스를 작성하자

stdio.h

#ifndef LIB_STDIO_H_
#define LIB_STDIO_H_

+#include "stdarg.h"

uint32_t putstr(const char* s);
uint32_t debug_printf(const char* format, ...);
+uint32_t vsprintf(char* buf, const char* format, va_list arg);

#endif /* LIB_STDIO_H_ */

 

vsprintf()함수를 구현하자.

stdio.c

uint32_t vsprintf(char* buf, const char* format, va_list arg)
{
    uint32_t c = 0;

    char     ch;
    char*    str;
    uint32_t uint;
    uint32_t hex;

    for (uint32_t i = 0 ; format[i] ; i++)
    {
        if (format[i] == '%')
        {
            i++;
            switch(format[i])
            {
            case 'c':
                ch = (char)va_arg(arg, int32_t);
                buf[c++] = ch;
                break;
            case 's':
                str = (char*)va_arg(arg, char*);
                if (str == NULL)
                {
                    str = "(null)";
                }
                while(*str)
                {
                    buf[c++] = (*str++);
                }
                break;
            case 'u':
                uint = (uint32_t)va_arg(arg, uint32_t);
                c += utoa(&buf[c], uint, utoa_dec);
                break;
            case 'x':
                hex = (uint32_t)va_arg(arg, uint32_t);
                c += utoa(&buf[c], hex, utoa_hex);
                break;
            }
        }
        else
        {
            buf[c++] = format[i];
        }
    }

    if (c >= PRINTF_BUF_LEN)
    {
        buf[0] = '\0';
        return 0;
    }

    buf[c] = '\0';
    return c;
}

%s, %c, %u, %x의 4개 형식을 지원한다. 

 

숫자를 ascii 코드로 변환하는 작업은 atou()함수를 통해 구현하자.

 

stdio.h

#ifndef LIB_STDIO_H_
#define LIB_STDIO_H_

#include "stdarg.h"

+typedef enum utoa_t
+{
+	utoa_dec = 10,
+	utoa_hex = 16,
+} utoa_t;

uint32_t putstr(const char* s);
uint32_t debug_printf(const char* format, ...);
uint32_t vsprintf(char* buf, const char* format, va_list arg);
+uint32_t utoa(char* buf, uint32_t val, utoa_t base);

#endif /* LIB_STDIO_H_ */

 

stdio.c

uint32_t utoa(char* buf, uint32_t val, utoa_t base)
{
    const char asciibase = 'a';

    uint32_t c = 0;
    int32_t idx = 0;
    char     tmp[11];   // It is enough for 32 bit int

    do {
        uint32_t t = val % (uint32_t)base;
        if (t >= 10)
        {
            t += asciibase - '0' - 10;
        }
        tmp[idx] = (t + '0');
        val /= base;
        idx++;
    } while(val);

    // reverse
    idx--;
    while (idx >= 0)
    {
        buf[c++] = tmp[idx];
        idx--;
    }

    return c;
}

 

debug_printf() 함수를 다 만들었으니, 테스트하자. 

#include "stdint.h"
#include "HalUart.h"

#include "stdio.h"

static void Hw_init(void);
+static void Printf_test(void);

void main(void)
{
	Hw_init();

	uint32_t i = 100;
	while(i--)
	{
		Hal_uart_put_char('N');
	}
	Hal_uart_put_char('\n');

	putstr("Hello World!\n");

+	Printf_test();

	i = 100;
	while(i--)
	{
		uint8_t ch = Hal_uart_get_char();
		Hal_uart_put_char(ch);
	}
}

static void Hw_init(void)
{
	Hal_uart_init();
}


+static void Printf_test(void)
+{
+	char* str = "printf pointer test";
+	char* nullptr = 0;
+	uint32_t i = 5;
+
+	debug_printf("%s\n", "Hello printf");
+	debug_printf("output string pointer: %s\n", str);
+	debug_printf("%s is null pointer, %u number\n", nullptr, 10);
+	debug_printf("%u = 5\n", i);
+	debug_printf("dec=%u hex=%x\n", 0xff, 0xff);
+}

 

NULL 문자에 대해 매크로를 stdint.h 최 하단에 추가해두겠다

stdint.h

...중략
/* Maximal type.  */
# if __WORDSIZE == 64
#  define INTMAX_C(c)	c ## L
#  define UINTMAX_C(c)	c ## UL
# else
#  define INTMAX_C(c)	c ## LL
#  define UINTMAX_C(c)	c ## ULL
# endif

+#define NULL    ((void*)0)

#endif /* stdint.h */

 

빌드를 해보자. 

root@21d947b2e449:/project# make
mkdir -p build
arm-none-eabi-gcc -march=armv7-a -mcpu=cortex-a8 -I include -I hal -I hal/rvpb -I lib -c -g -std=c11 -o build/stdio.o lib/stdio.c
cc1: warning: switch '-mcpu=cortex-a8' conflicts with '-march=armv7-a' switch
arm-none-eabi-ld -n -T ./navilos.ld -o build/navilos.axf  build/Entry.os  build/Main.o  build/Regs.o  build/Uart.o  build/stdio.o -Map=build/navilos.map
arm-none-eabi-ld: build/stdio.o: in function `utoa':
/project/lib/stdio.c:96: undefined reference to `__aeabi_uidivmod'
arm-none-eabi-ld: /project/lib/stdio.c:102: undefined reference to `__aeabi_uidiv'
make: *** [Makefile:56: build/navilos.axf] Error 1

에러가 발생한다.

나머지(%)와 나누기(/)를 사용했는데 ARM은 기본적으로 나누기와 나머지를 지원하는 HW가 없다고 간주한다. 따라서 GCC가 이를 SW로 구현해 놓은 라이브러리 함수로 자동으로 링킹한다. 만약 나누기와 나머지를 지원하는 HW가 있는 플랫폼 기반의 펌웨어라면 위 에러 메시지에 나온 __aeabi_uidivmod, __aeabi_uidiv 함수를 만들고 그 함수에서 HW를 사용하는 코드를 작성하여 링킹해야한다.

 

나누기, 나머지 연산이 가능하도록 Makefile를 수정하자.

ARCH = armv7-a
MCPU = cortex-a8

TARGET = rvpb

CC = arm-none-eabi-gcc
AS = arm-none-eabi-as
LD = arm-none-eabi-gcc
OC = arm-none-eabi-objcopy

LINKER_SCRIPT = ./navilos.ld
MAP_FILE = build/navilos.map

ASM_SRCS = $(wildcard boot/*.S)
ASM_OBJS = $(patsubst boot/%.S, build/%.os, $(ASM_SRCS))

VPATH = boot \
        hal/$(TARGET)	\
        lib

C_SRCS  = $(notdir $(wildcard boot/*.c))
C_SRCS += $(notdir $(wildcard hal/$(TARGET)/*.c))
C_SRCS += $(notdir $(wildcard lib/*.c))
C_OBJS = $(patsubst %.c, build/%.o, $(C_SRCS))

INC_DIRS  = -I include 			\
            -I hal	   			\
            -I hal/$(TARGET)	\
            -I lib

CFLAGS = -c -g -std=c11

LDFLAGS = -nostartfiles -nostdlib -nodefaultlibs -static -lgcc

navilos = build/navilos.axf
navilos_bin = build/navilos.bin

.PHONY: all clean run debug kill_qemu gdb

all: $(navilos)

clean:
	@rm -fr build
	
run: $(navilos)
	qemu-system-arm -M realview-pb-a8 -kernel $(navilos) -nographic
	
debug: $(navilos)
	qemu-system-arm -M realview-pb-a8 -kernel $(navilos) -S -gdb tcp::1234,ipv4 -display none -nographic

kill_qemu:
	pkill -9 qemu-system-arm
	
gdb:
	gdb-multiarch
	
$(navilos): $(ASM_OBJS) $(C_OBJS) $(LINKER_SCRIPT)
	$(LD) -n -T $(LINKER_SCRIPT) -o $(navilos) $(ASM_OBJS) $(C_OBJS) -Wl,-Map=$(MAP_FILE) $(LDFLAGS)
	$(OC) -O binary $(navilos) $(navilos_bin)

build/%.os: %.S
	mkdir -p $(shell dirname $@)
	$(CC) -march=$(ARCH) -mcpu=$(MCPU) -marm $(INC_DIRS) $(CFLAGS) -o $@ $<

build/%.o: %.c
	mkdir -p $(shell dirname $@)
	$(CC) -march=$(ARCH) -mcpu=$(MCPU) -marm $(INC_DIRS) $(CFLAGS) -o $@ $<

무엇이 수정되었나?

ARCH = armv7-a
MCPU = cortex-a8

TARGET = rvpb

CC = arm-none-eabi-gcc
AS = arm-none-eabi-as
-LD = arm-none-eabi-ld
+LD = arm-none-eabi-gcc
OC = arm-none-eabi-objcopy

LINKER_SCRIPT = ./navilos.ld
MAP_FILE = build/navilos.map

ASM_SRCS = $(wildcard boot/*.S)
ASM_OBJS = $(patsubst boot/%.S, build/%.os, $(ASM_SRCS))

VPATH = boot \
        hal/$(TARGET)	\
        lib

C_SRCS  = $(notdir $(wildcard boot/*.c))
C_SRCS += $(notdir $(wildcard hal/$(TARGET)/*.c))
C_SRCS += $(notdir $(wildcard lib/*.c))
C_OBJS = $(patsubst %.c, build/%.o, $(C_SRCS))

INC_DIRS  = -I include 			\
            -I hal	   			\
            -I hal/$(TARGET)	\
            -I lib

CFLAGS = -c -g -std=c11

+LDFLAGS = -nostartfiles -nostdlib -nodefaultlibs -static -lgcc

navilos = build/navilos.axf
navilos_bin = build/navilos.bin

.PHONY: all clean run debug kill_qemu gdb

all: $(navilos)

clean:
	@rm -fr build
	
run: $(navilos)
	qemu-system-arm -M realview-pb-a8 -kernel $(navilos) -nographic
	
debug: $(navilos)
	qemu-system-arm -M realview-pb-a8 -kernel $(navilos) -S -gdb tcp::1234,ipv4 -display none -nographic

kill_qemu:
	pkill -9 qemu-system-arm
	
gdb:
	gdb-multiarch
	
$(navilos): $(ASM_OBJS) $(C_OBJS) $(LINKER_SCRIPT)
-	$(LD) -n -T $(LINKER_SCRIPT) -o $(navilos) $(ASM_OBJS) $(C_OBJS) -Map=$(MAP_FILE)
+	$(LD) -n -T $(LINKER_SCRIPT) -o $(navilos) $(ASM_OBJS) $(C_OBJS) -Wl,-Map=$(MAP_FILE) $(LDFLAGS)
	$(OC) -O binary $(navilos) $(navilos_bin)

build/%.os: %.S
	mkdir -p $(shell dirname $@)
-   	$(CC) -march=$(ARCH) -mcpu=$(MCPU) $(INC_DIRS) $(CFLAGS) -o $@ $<
+	$(CC) -march=$(ARCH) -mcpu=$(MCPU) -marm $(INC_DIRS) $(CFLAGS) -o $@ $<

build/%.o: %.c
	mkdir -p $(shell dirname $@)
-   	$(CC) -march=$(ARCH) -mcpu=$(MCPU) $(INC_DIRS) $(CFLAGS) -o $@ $<
+	$(CC) -march=$(ARCH) -mcpu=$(MCPU) -marm $(INC_DIRS) $(CFLAGS) -o $@ $<

 

빌드 후 실행하면 main() 함수에 테스트한 내용이 아래와 같이 출력된다.

root@21d947b2e449:/project# make run
qemu-system-arm -M realview-pb-a8 -kernel build/navilos.axf -nographic
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5220:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2642:(snd_pcm_open_noupdate) Unknown PCM default
alsa: Could not initialize DAC
alsa: Failed to open `default':
alsa: Reason: No such file or directory
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5220:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2642:(snd_pcm_open_noupdate) Unknown PCM default
alsa: Could not initialize DAC
alsa: Failed to open `default':
alsa: Reason: No such file or directory
audio: Failed to create voice `lm4549.out'
NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN
Hello World!
Hello printf
output string pointer: printf pointer test
(null) is null pointer, 10 number
5 = 5
dec=255 hex=ff

⭐️6장 인터럽트

인터럽트(interrupt)는 컴퓨팅 시스템의 꽃이라 할 수 있다. 컴퓨팅 시스템의 모든 것이 다 인터럽트라고 말해도 과언이 아니다. 커

  • 인터럽트는 임베디드 시스템을 포함한 모든 컴퓨팅 시스템의 꽃
    • 컴퓨팅 시스템의 모든 것이 다 인터럽트라고 말해도 과언이 아니다
    • 컴퓨팅 시스템은 사람이든 다른 시스템이든, 외부의 다른 존재와의 상호작용을 인터럽트로 처리한다
      • 예시 1 (사용자와 상호작용)
        • 글을쓰는동안 키보드를 수만 번 누를때마다 키보드 안에서 동작하는 펌웨어는 인터럽트를 받아서 처리하고
        • 그 결과로 PC에, 정해진 신호를 보낸다
        • 그러면 PC에서도 인터럽트가 발생하고, 이 인터럽트를 운영체제가 받아서 처리한다
        • 그리고 운영체제는 정해진 신호를 모니터로 보낸다
        • 그러면 모니터에 딸려있는 하드웨어는 PC에서 받은 신호로 인터럽트를 발생시킨다
        • 모니터에 있는 펌웨어는 이 인터럽트를 받아서 적절한 처리를하고, 그 결과 화면에 글자가 출력되는 것이다
      • 예시 2 (사용자와 상호작용이 아닌, 하드웨어 자체적으로 인터럽트가 발생)
        • 타이머같은 종류의 인터럽트
        • 타이머는 나중에 설명함. 지금은 쉬운 하드웨어 인터럽트부터 다룬다
  • 인터럽트를 처리하려면,
    • 우선 인터럽트 컨트롤러를 어떻게 사용해야 하는지 알아야한다
    • 그 다음, 인터럽트 컨트롤러를 초기화하고 사용하는 코드를 작성해야한다
    • 인터럽트 컨트롤러 관련 코드를 다 작성했으면, 실제 인터럽트를 발생시키는 하드웨어와 인터럽트 컨트롤러를 연결해야한다.
      • 현재까지 우리는 UART만 사용중이다
      • 그리고 UART 하드웨어는 인터럽트를 발생시킨다
      • 그러므로 UART 하드웨어와 인터럽트 컨트롤러를 연결한다
      • 그러면 UART 하드웨어가 인터럽트 컨트롤러로 인터럽트 신호를 보낸다
    •  인터럽트 컨트롤러는 ARM 코어로 인터럽트를 보낸다
    • 펌웨어에서 cpsr의 IRQ 혹은 FIQ마스크를 끄면 IRQ나 FIQ가 발생했을때 코어가 자동으로 익셉션 핸들러를 호출한다
      • 익셉션 핸들러는 펌웨어다
      • 그러므로 익셉션 핸들러도 작성해야한다
    • 익셉션 핸들러에서 적절한 인터럽트 핸들러를 호출하면 인터럽트 처리를 완료한 것이다

인터럽트를 사용하면 어떤 동작이 가능해질까? 

void main(void)
{
	Hw_init();

	uint32_t i = 100;
	while(i--)
	{
		Hal_uart_put_char('N');
	}
	Hal_uart_put_char('\n');

	putstr("Hello World!\n");

	Printf_test();

	while(true);
}

무한 루프에 들어가서 종료하지 않는 main() 함수가 있다고 해보자. 무한루프를 돌기때문에 멈춘것처럼 느껴진다. 이럴때 인터럽트를 사용하면 무한 루프 상황에서도 어떤 동작을 하게 할 수 있다.

 

인터럽트 컨트롤러

RealViewPB에는 Generic Interrupt Controller라는 이름의 인터럽트 컨트롤러 HW가 달려있다. 

https://developer.arm.com/documentation/dui0417/d/hardware-description/interrupts/generic-interrupt-controller?lang=en

 

Documentation – Arm Developer

 

developer.arm.com

GIC로 부르자. GIC는 나름의 방식으로 인터럽트를 처리한다. 다른 인터럽트 컨트롤러는 또 그것만의 고유한 방식으로 인터럽트를 처리한다. 그래도 기본적인 기능은 비슷하다.

 

 

HW를 SW로 조절하기 위해서 5장에서 한 과정과 동일하다. GIC의 레지스터 구조체를 만들자.

https://developer.arm.com/documentation/dui0417/d/programmer-s-reference/generic-interrupt-controller--gic/generic-interrupt-controller-registers?lang=en

 

Documentation – Arm Developer

 

developer.arm.com

 

hal/rvpb/Interrupt.h

#ifndef HAL_RVPB_INTERRUPT_H_
#define HAL_RVPB_INTERRUPT_H_

typedef union CpuControl_t
{
    uint32_t all;
    struct {
        uint32_t Enable:1;          // 0
        uint32_t reserved:31;
    } bits;
} CpuControl_t;

typedef union PriorityMask_t
{
    uint32_t all;
    struct {
        uint32_t Reserved:4;        // 3:0
        uint32_t Prioritymask:4;    // 7:4
        uint32_t reserved:24;
    } bits;
} PriorityMask_t;

typedef union BinaryPoint_t
{
    uint32_t all;
    struct {
        uint32_t Binarypoint:3;     // 2:0
        uint32_t reserved:29;
    } bits;
} BinaryPoint_t;

typedef union InterruptAck_t
{
    uint32_t all;
    struct {
        uint32_t InterruptID:10;    // 9:0
        uint32_t CPUsourceID:3;     // 12:10
        uint32_t reserved:19;
    } bits;
} InterruptAck_t;

typedef union EndOfInterrupt_t
{
    uint32_t all;
    struct {
        uint32_t InterruptID:10;    // 9:0
        uint32_t CPUsourceID:3;     // 12:10
        uint32_t reserved:19;
    } bits;
} EndOfInterrupt_t;

typedef union RunningInterrupt_t
{
    uint32_t all;
    struct {
        uint32_t Reserved:4;        // 3:0
        uint32_t Priority:4;        // 7:4
        uint32_t reserved:24;
    } bits;
} RunningInterrupt_t;

typedef union HighestPendInter_t
{
    uint32_t all;
    struct {
        uint32_t InterruptID:10;    // 9:0
        uint32_t CPUsourceID:3;     // 12:10
        uint32_t reserved:19;
    } bits;
} HighestPendInter_t;

typedef union DistributorCtrl_t
{
    uint32_t all;
    struct {
        uint32_t Enable:1;          // 0
        uint32_t reserved:31;
    } bits;
} DistributorCtrl_t;

typedef union ControllerType_t
{
    uint32_t all;
    struct {
        uint32_t IDlinesnumber:5;   // 4:0
        uint32_t CPUnumber:3;       // 7:5
        uint32_t reserved:24;
    } bits;
} ControllerType_t;



typedef struct GicCput_t
{
    CpuControl_t       cpucontrol;        //0x000
    PriorityMask_t     prioritymask;      //0x004
    BinaryPoint_t      binarypoint;       //0x008
    InterruptAck_t     interruptack;      //0x00C
    EndOfInterrupt_t   endofinterrupt;    //0x010
    RunningInterrupt_t runninginterrupt;  //0x014
    HighestPendInter_t highestpendinter;  //0x018
} GicCput_t;

typedef struct GicDist_t
{
    DistributorCtrl_t   distributorctrl;    //0x000
    ControllerType_t    controllertype;     //0x004
    uint32_t            reserved0[62];      //0x008-0x0FC
    uint32_t            reserved1;          //0x100
    uint32_t            setenable1;         //0x104
    uint32_t            setenable2;         //0x108
    uint32_t            reserved2[29];      //0x10C-0x17C
    uint32_t            reserved3;          //0x180
    uint32_t            clearenable1;       //0x184
    uint32_t            clearenable2;       //0x188
} GicDist_t;

#define GIC_CPU_BASE  0x1E000000  //CPU interface
#define GIC_DIST_BASE 0x1E001000  //distributor

#define GIC_PRIORITY_MASK_NONE  0xF

#define GIC_IRQ_START           32
#define GIC_IRQ_END             95

#endif /* HAL_RVPB_INTERRUPT_H_ */

GIC 레지스터는 크게 두 그룹으로 나눈다. CPU Interface registers 와 Distributor registers 이다.

더 많은 레지스터가 존재하지만, 본 프로젝트에서 쓰일 수준만 정리했다.

 

레지스터 구조체를 선언했고, 레지스터의 베이스 주소도 알고 있으므로 UART때와 마찬가지로 실제 인스턴스를 선언하자.

hal/rvpb/Regs.c

#include "stdint.h"
#include "Uart.h"
+#include "Interrupt.h"

volatile PL011_t*   Uart    = (PL011_t*)UART_BASE_ADDRESS0;
+volatile GicCput_t* GicCpu  = (GicCput_t*)GIC_CPU_BASE;
+volatile GicDist_t* GicDist = (GicDist_t*)GIC_DIST_BASE;

두 그룹을 따로 인스턴스화했다.

 

인터럽트에 대해 인터페이스를 작성하자.

hal/HalInterrupt.h

#ifndef HAL_HALINTERRUPT_H_
#define HAL_HALINTERRUPT_H_

#define INTERRUPT_HANDLER_NUM   255

typedef void (*InterHdlr_fptr)(void);

void Hal_interrupt_init(void);
void Hal_interrupt_enable(uint32_t interrupt_num);
void Hal_interrupt_disable(uint32_t interrupt_num);
void Hal_interrupt_register_handler(InterHdlr_fptr handler, uint32_t interrupt_num);
void Hal_interrupt_run_handler(void);

#endif /* HAL_HALINTERRUPT_H_ */

초기화 함수, 인터럽트 활성 함수, 인터럽트 비활성화 함수, 인터럽트 핸들러 등록 함수, 인터럽트 핸들러를 호출하는 함수이다.

 

인터페이스를 구현하자.

hal/rvpb/Interrupt.c

#include "stdint.h"
#include "memio.h"
#include "Interrupt.h"
#include "HalInterrupt.h"
#include "armcpu.h"

extern volatile GicCput_t* GicCpu;
extern volatile GicDist_t* GicDist;

static InterHdlr_fptr sHandlers[INTERRUPT_HANDLER_NUM];

void Hal_interrupt_init(void)
{
    GicCpu->cpucontrol.bits.Enable = 1;
    GicCpu->prioritymask.bits.Prioritymask = GIC_PRIORITY_MASK_NONE;
    GicDist->distributorctrl.bits.Enable = 1;

    for (uint32_t i = 0 ; i < INTERRUPT_HANDLER_NUM ; i++)
    {
        sHandlers[i] = NULL;
    }

    enable_irq();
}

void Hal_interrupt_enable(uint32_t interrupt_num)
{
    if ((interrupt_num < GIC_IRQ_START) || (GIC_IRQ_END < interrupt_num))
    {
        return;
    }

    uint32_t bit_num = interrupt_num - GIC_IRQ_START;

    if (bit_num < GIC_IRQ_START)
    {
        SET_BIT(GicDist->setenable1, bit_num);
    }
    else
    {
        bit_num -= GIC_IRQ_START;
        SET_BIT(GicDist->setenable2, bit_num);
    }
}

void Hal_interrupt_disable(uint32_t interrupt_num)
{
    if ((interrupt_num < GIC_IRQ_START) || (GIC_IRQ_END < interrupt_num))
    {
        return;
    }

    uint32_t bit_num = interrupt_num - GIC_IRQ_START;

    if (bit_num < GIC_IRQ_START)
    {
        CLR_BIT(GicDist->setenable1, bit_num);
    }
    else
    {
        bit_num -= GIC_IRQ_START;
        CLR_BIT(GicDist->setenable2, bit_num);
    }
}

void Hal_interrupt_register_handler(InterHdlr_fptr handler, uint32_t interrupt_num)
{
    sHandlers[interrupt_num] = handler;
}

void Hal_interrupt_run_handler(void)
{
    uint32_t interrupt_num = GicCpu->interruptack.bits.InterruptID;

    if (sHandlers[interrupt_num] != NULL)
    {
        sHandlers[interrupt_num]();
    }

    GicCpu->endofinterrupt.bits.InterruptID = interrupt_num;
}

결국 레지스터의 비트들을 어떻게 처리하는가에 대한 것이다.

 

Hal_interrupt_init()함수에서는 각각 CPU interface와 Distributor 레지스터에서 인터럽트 컨트롤러를 켠다.

Prioritymask레지스터는 무엇일까? 데이터시트의 설명을 보자

4~7번 비트만 의미 있는 레지스터인데이다.

4~7번 비트 모두 0으로 설정하면 인터럽트를 다 마스크(쉽게 말해서 막는다) 한다.  

4~7번 비트를 0xF로 설정하면 우선순위가 0x0부터 0xE까지인 인터럽트를 허용한다.

그런데 인터럽트 우선순위의 기본값은 0 이므로 실제로는 모든 인터럽트를 모두 허용한다. 본 프로젝트에서는 우선순위를 조절하면서 인터럽트를 세밀하게 쓰지 않을 것이기에 모두 허용하겠다. 따라서 코드에 0xF를 넣는것이다.

 

enable_irq() 함수는 아직 구현 전이다.

스펙을 설명하면, ARM의 cspr을 제어해서 코어 수준의 IRQ를 켜는 함수이다.

기억해두자.

 

Hal_interrupt_enable() 함수와 Hal_interrupt_disable() 함수는 개별 인터럽트를 켜고 끄는 함수이다. 

GIC는 인터럽트를 64개 관리할수 있는데, 32개씩 레지스터 두개에 할당해 놓고 이름을 Set Enable1과 Set Enable2 레지스터로 붙여뒀다. IRQ의 시작번호는 32이다. 따라서 GIC는 각 레지스터의 개별 비트를 IRQ ID 32번부터 IRQ ID95번에 연결했다. 원하는 비트 오프셋을 찾으려면 할당된 IRQ 번호에서 32를 빼면된다. 예를 들어, UART는 IRQ ID가 44번이다.  그러면 Set Enable1 레지스터으 ㅣ12번 비트에 UART 인터럽트가 연결되는 것이다.

그림으로 나타내면 아래와 같다.

책에서 캡쳐

IRQ ID 64번을 켜거나 끄고 싶다면, Set Enable2 레지스터의 0번 비트에 1을 써서 켜고 0을 써서 끄면된다.

 

동작을 전체적으로 기술해보자. 우선 인터럽트의 번호에서 32를 뺀다. 그러면 Set Enable1 레지스터의 오프셋이다. 그런데 해당 인터럽트가 Set Enable2 레지스터에 설정해야할 값이였다면, 31보다 클 것이다. 그럼 다시 32를 빼면 Set Enable2 레지스터 비트 오프셋이 나온다.

 

SET_BIT, CLR_BIT 매크로는 변수의 특정 오프셋 비트를 1로 설정하거나 0으로 클리어하는 매크로다.

include/memio.h

#ifndef INCLUDE_MEMIO_H_
#define INCLUDE_MEMIO_H_

#define SET_BIT(p,n) ((p) |=  (1 << (n)))
#define CLR_BIT(p,n) ((p) &= ~(1 << (n)))

#endif /* INCLUDE_MEMIO_H_ */

 

Hal_interrupt_register_handler() 함수는 static으로 선언해둔 sHandlers 배열에 함수 포인터를 저장한다.

 

Hal_interrupt_run_handler() 함수는 Interrupt acknowledge 레지스터에서 값을 읽어온다. 이 값이 현재 HW에서 대기중인 인터럽트 IRQ ID 번호다. Hal_interrupt_register_handler() 함수에서 IRQ ID 번호를 기준으로 인터럽트 핸드러를 등록했기 때문에 Interrupt acknowledge 레지스터에서 읽은 값을 그대로 배열 인덱스로 이용해서 인터럽트 핸들러 함수 포인터를 찾을 수 있다. 함수 포인터를 실행하고, 인터럽트 핸들러를 실행하고 나면 인터럽트가 다 처리된다. 처리가 끝났다면,  인터럽트 컨트롤러에 해당 인터럽트에 대한 처리가 끝났음을 End of interrupt 레지스터에 IRQ ID를 써 알린다. 

 

잠시 미뤄뒀던 CSPR의 IRQ 마스크를 끄는 코드를 설명한다.

lib/armcpu.h

#ifndef LIB_ARMCPU_H_
#define LIB_ARMCPU_H_

void enable_irq(void);
void enable_fiq(void);
void disable_irq(void);
void disable_fiq(void);

#endif /* LIB_ARMCPU_H_ */

lib/armcpu.c

#include "armcpu.h"

void enable_irq(void)
{
    __asm__ ("PUSH {r0, r1}");
    __asm__ ("MRS  r0, cpsr");
    __asm__ ("BIC  r1, r0, #0x80");
    __asm__ ("MSR  cpsr, r1");
    __asm__ ("POP {r0, r1}");
}

void enable_fiq(void)
{
    __asm__ ("PUSH {r0, r1}");
    __asm__ ("MRS  r0, cpsr");
    __asm__ ("BIC  r1, r0, #0x40");
    __asm__ ("MSR  cpsr, r1");
    __asm__ ("POP {r0, r1}");
}

void disable_irq(void)
{
    __asm__ ("PUSH {r0, r1}");
    __asm__ ("MRS  r0, cpsr");
    __asm__ ("ORR  r1, r0, #0x80");
    __asm__ ("MSR  cpsr, r1");
    __asm__ ("POP {r0, r1}");
}

void disable_fiq(void)
{
    __asm__ ("PUSH {r0, r1}");
    __asm__ ("MRS  r0, cpsr");
    __asm__ ("ORR  r1, r0, #0x40");
    __asm__ ("MSR  cpsr, r1");
    __asm__ ("POP {r0, r1}");
}

CSPR을 제어하기 위해선 어셈블리어를 사용할수밖에없다. ARM에서 제공하는 컴파일러(ARMCC)는 빌트인 변수로 CSPR에 접근할 수 있지만, GCC는 그렇지 않다.

C언어의 인라인 어셈블리어를 사용했다. 인라인 어셈블리어로 작성시 함수의 프롤로그과 에필로그를 자동생성해주는 장점도 존재한다. 

함수들은 같은 구조를 반복하고 있다. 이 중 중간 코드가 핵심이다. 명령어는 ORR 혹은 BIC 이고 operand는 r1, r0, #CSPR의 대상 인터럽트 마스크 비트 위치이다.

예를들어 enable_irq를 보면 0x80 을 2진수로 변환하면 10000000이다. 이는 IRQ 마스크 비트위치인 7번 비트에 1이 있는 것이다. 그리고 BIC 명령어를 사용하면 해당 비트자리에 0을 쓴다. ORR 명령어는 1을 쓴다.

 

UART입력과 인터럽트 연결

GIC 설정 작업은 마무리했다. 인터럽트를 받고 처리할 준비를 한것이다. 실제 인터럽트를 발생시킬 HW가 연결되어야한다. UART로 인터럽트를 발생시켜보자.

UART의 여러 인터럽트 중 입력 인터럽트를 설정해보자.

 

Uart.c

#include "stdint.h"
#include "Uart.h"
#include "HalUart.h"
#include "HalInterrupt.h"

extern volatile PL011_t* Uart;

static void interrupt_handler(void);

void Hal_uart_init(void)
{
	// Enable UART
	Uart->uartcr.bits.UARTEN = 0;
	Uart->uartcr.bits.TXE = 1;
	Uart->uartcr.bits.RXE = 1;
	Uart->uartcr.bits.UARTEN = 1;

	// Enable input interrupt
	Uart->uartimsc.bits.RXIM = 1;

	// Register UART interrupt handler
	Hal_interrupt_enable(UART_INTERRUPT0);
	Hal_interrupt_register_handler(interrupt_handler, UART_INTERRUPT0);
}

void Hal_uart_put_char(uint8_t ch)
{
	while(Uart->uartfr.bits.TXFF);
	Uart->uartdr.all = (ch & 0xFF);
}

uint8_t Hal_uart_get_char(void)
{
	uint32_t data;

	while(Uart->uartfr.bits.RXFE);

	data = Uart->uartdr.all;

	// Check for an error flag
	if (data & 0xFFFFFF00)
	{
		// Clear the error
		Uart->uartrsr.all = 0xFF;
		return 0;
	}

	return (uint8_t)(data & 0xFF);
}

static void interrupt_handler(void)
{
	uint8_t ch = Hal_uart_get_char();
	Hal_uart_put_char(ch);
}

수정된 것을 확인하자.

#include "stdint.h"
#include "Uart.h"
#include "HalUart.h"
+#include "HalInterrupt.h"

extern volatile PL011_t* Uart;

+static void interrupt_handler(void);

void Hal_uart_init(void)
{
	// Enable UART
	Uart->uartcr.bits.UARTEN = 0;
	Uart->uartcr.bits.TXE = 1;
	Uart->uartcr.bits.RXE = 1;
	Uart->uartcr.bits.UARTEN = 1;

+	// Enable input interrupt
+	Uart->uartimsc.bits.RXIM = 1;
+
+	// Register UART interrupt handler
+	Hal_interrupt_enable(UART_INTERRUPT0);
+	Hal_interrupt_register_handler(interrupt_handler, UART_INTERRUPT0);
}

void Hal_uart_put_char(uint8_t ch)
{
	while(Uart->uartfr.bits.TXFF);
	Uart->uartdr.all = (ch & 0xFF);
}

uint8_t Hal_uart_get_char(void)
{
	uint32_t data;

	while(Uart->uartfr.bits.RXFE);

	data = Uart->uartdr.all;

	// Check for an error flag
	if (data & 0xFFFFFF00)
	{
		// Clear the error
		Uart->uartrsr.all = 0xFF;
		return 0;
	}

	return (uint8_t)(data & 0xFF);
}

+static void interrupt_handler(void)
+{
+	uint8_t ch = Hal_uart_get_char();
+	Hal_uart_put_char(ch);
+}

#include로 인터럽트 설정 관련 함수(Hal_interrupt_enable() 등)를 포함시켰다. 또한 interrupt_handler() 함수를 추가했다. Hal_uart_init() 함수를 수정하여 장비 초기화때 추가되도록 했다.

인터럽트 핸들러에서는 간단한 작업을 작성했다. UART HW에서 읽은 값을 다시 그대로 UART출력으로 내보내는 작업이다. (echo 기능)

 

인터럽트와 UART가 연결되었으므로 초기화 순서를 맞추자. Main.c에서 HW 초기화 코드를 수정하자.

Main.c

+#include "HalInterrupt.h"

static void Hw_init(void)
{
+	Hal_interrupt_init();
	Hal_uart_init();
}

순서가 중요하다. 인터럽트 컨트롤러 초기화 함수를 UART 초기화 함수보다 먼저 호출해야한다. Hal_uart_init() 함수 내부에서 인터럽트 관련 함수 호출하므로 그전에 인터럽트 컨트롤러를 초기화해놔야 정상동작한다.

IRQ 익셉션 벡터 연결

  • 지금까지 작업한 내용 정리
    • main() 함수를 무한 루프로 종료하지 않게 변경
    • 인터럽트 컨트롤러 초기화
    • cspr의 IRQ 마스크를 해제
    • UART 인터럽트 핸들러를 인터럽트 컨트롤러에 등록
    • 인터럽트 컨트롤러와 UART 하드웨어 초기화 순서 조정
  • 이제 마지막 작업이 남았다
    • 인터럽트가 발생하면, 인터럽트 컨트롤러는 이 인터럽트를 접수해서 ARM 코어에 바로 전달한다
      • ARM에는 FIQ와 IRQ라는 두 종류의 인터럽트가 있다
      • 이 책에서는 IRQ만 사용한다
    • 그래서 ARM 코어는 인터럽트를 받으면 IRQ익셉션을 발생시킨다
    • 그리고 동작모드를 IRQ모드로 바꾸면서, 동시에 익셉션 벡터 테이블으 ㅣIRQ익셉션 벡터로 바로 점프한다
    • 그래서 하드웨어 동작을 빼고 소프트웨어 동작만 보면, 인터럽트 종류가 무엇이든 일단 익셉션 벡터 테이블의 IRQ익셉션 핸들러가 무조건 실행된다
    • 그렇다면 남은 작업은 익셉션 벡터 테이블의 IRQ익셉션 벡터와 인터럽트 컨트롤러의 인터럽트 핸들러를 연결하는 작업이다

그 이전에 익셉션 핸들러부터 작성하고 진행하자.

boot/Handler.c

#include "stdbool.h"
#include "stdint.h"
#include "HalInterrupt.h"

 __attribute__ ((interrupt ("IRQ"))) void Irq_Handler(void)
{
    Hal_interrupt_run_handler();
}

 __attribute__ ((interrupt ("FIQ"))) void Fiq_Handler(void)
{
    while(true); //dummy handler
}

IRQ 와 FIQ 익셉션 핸들러다. FIQ 익셉션 핸들러는 더미다. 

__attribute__는 GCC의 컴파일러 확장기능을 사용하겠다는 지시어다. __attribute__의 많은 기능 중 __attribute__ ((interrupt ("IRQ"))), __attribute__ ((interrupt ("FIQ"))) 는 ARM용 GCC 전용 확장기능이다. IRQ와 FIQ의 핸들러에 진입하고 나가는 코드(프롤로그 & 에필로그)를 자동으로 생성한다. 이 과정에서 핸들러 별로 리턴 주소를 컴파일러가 자동으로 만든다.

해당 과정을 보자. 빌드 후 오브젝트파일을 역어셈블하면 다음과 같은 코드가 나온다.

00000000 <Irq_Handler>:
   0:	e24ee004	sub	lr, lr, #4
   4:	e92d581f	push	{r0, r1, r2, r3, r4, fp, ip, lr}
   8:	e28db01c	add	fp, sp, #28
   c:	ebfffffe	bl	0 <Hal_interrupt_run_handler>
  10:	e24bd01c	sub	sp, fp, #28
  14:	e8fd981f	ldm	sp!, {r0, r1, r2, r3, r4, fp, ip, pc}

0x0 오프셋의 코드를 보면 LR에서 4를 빼는 것을 볼 수 있다.

 

익셉션 핸들러를 만들었으니, 이제 익셉션 벡터 테이블에 연결시키자.

#include "ARMv7AR.h"
#include "MemoryMap.h"

.text
    .code 32

    .global vector_start
    .global vector_end

    vector_start:
        LDR PC, reset_handler_addr
        LDR PC, undef_handler_addr
        LDR PC, svc_handler_addr
        LDR PC, pftch_abt_handler_addr
        LDR PC, data_abt_handler_addr
        B   .
        LDR PC, irq_handler_addr
        LDR PC, fiq_handler_addr

        reset_handler_addr:     .word reset_handler
        undef_handler_addr:     .word dummy_handler
        svc_handler_addr:       .word dummy_handler
        pftch_abt_handler_addr: .word dummy_handler
        data_abt_handler_addr:  .word dummy_handler
+       irq_handler_addr:       .word Irq_Handler
+       fiq_handler_addr:       .word Fiq_Handler
    vector_end:

    reset_handler:
        MRS r0, cpsr
        BIC r1, r0, #0x1F
        ORR r1, r1, #ARM_MODE_BIT_SVC
        MSR cpsr, r1
        LDR sp, =SVC_STACK_TOP

        MRS r0, cpsr
        BIC r1, r0, #0x1F
        ORR r1, r1, #ARM_MODE_BIT_IRQ
        MSR cpsr, r1
        LDR sp, =IRQ_STACK_TOP

        MRS r0, cpsr
        BIC r1, r0, #0x1F
        ORR r1, r1, #ARM_MODE_BIT_FIQ
        MSR cpsr, r1
        LDR sp, =FIQ_STACK_TOP

        MRS r0, cpsr
        BIC r1, r0, #0x1F
        ORR r1, r1, #ARM_MODE_BIT_ABT
        MSR cpsr, r1
        LDR sp, =ABT_STACK_TOP

        MRS r0, cpsr
        BIC r1, r0, #0x1F
        ORR r1, r1, #ARM_MODE_BIT_UND
        MSR cpsr, r1
        LDR sp, =UND_STACK_TOP

        MRS r0, cpsr
        BIC r1, r0, #0x1F
        ORR r1, r1, #ARM_MODE_BIT_SYS
        MSR cpsr, r1
        LDR sp, =USRSYS_STACK_TOP

        BL  main

    dummy_handler:
        B .
.end

 

빌드 후 테스트해보자. 빌드에 필요한 디렉터리들에는 변화가 없으니 Makefile의 수정은 필요없다.

인터럽트가 정상 동작한다면 main()의 무한루프가 존재하지만 키보드 입력을 인터럽트로 전달하여 반응하는 모습을 볼 수 있을 것이다.

root@21d947b2e449:/project# make run
qemu-system-arm -M realview-pb-a8 -kernel build/navilos.axf -nographic
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5220:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2642:(snd_pcm_open_noupdate) Unknown PCM default
alsa: Could not initialize DAC
alsa: Failed to open `default':
alsa: Reason: No such file or directory
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5220:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2642:(snd_pcm_open_noupdate) Unknown PCM default
alsa: Could not initialize DAC
alsa: Failed to open `default':
alsa: Reason: No such file or directory
audio: Failed to create voice `lm4549.out'
NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN
Hello World!
Hello printf
output string pointer: printf pointer test
(null) is null pointer, 10 number
5 = 5
dec=255 hex=ff
print zero 0
interrupt works well! echo data

인터럽트가 정상 동작한다!

7장 타이머

타이머는 스톱워치의 기능을 한다.

일반적으로 타이머는 목표 카운트 레지스터와 측정 카운트 레지스터를 조합해서 활용한다. 목표 카운트 레지스터 값을 지정하고 측정 카운트 레지스터를 증가 혹은 감소로 설정한다. 증가라면 0부터 시작해서 목표 카운터 값에 도달하면 인터럽트를 발생시키고, 감소라면 목표 카운트부터 시작해서 0이되면 인터럽트를 발생시킨다. 이는 구현하기 나름이다.

  • RealViewPB는 SP804라는 타이머 하드웨어를 갖고있다
    • 해당 타이머는 측정 카운터가 감소하는 형식이다
  • 목표
    • 일정 시간 간격으로 타이머 인터럽트를 발생시켜서, 얼마만큼 시간이 지났는지 알아내는 것
      • 이것을 알아낼 수 있다면 다양한 응용이 가능하다
      • 대표적인 delay() 함수 구현

 

HW 추가부터 시작하자. 이제는 익숙할 것이다.

 

RealViewPBd 데이터시트의 Timer 관련 부분이다.

https://developer.arm.com/documentation/dui0417/d/programmer-s-reference/timers?lang=en

 

Documentation – Arm Developer

 

developer.arm.com

 

SP804의 데이터 시트이다. https://developer.arm.com/documentation/ddi0271/d

 

Documentation – Arm Developer

 

developer.arm.com

프로그래머 관점에서 자세하게 설명된 챕터는 다음과 같다. https://developer.arm.com/documentation/ddi0271/d/programmer-s-model?lang=en

 

Documentation – Arm Developer

 

developer.arm.com

 

hal/rvpb/Timer.h

#ifndef HAL_RVPB_TIMER_H_
#define HAL_RVPB_TIMER_H_

typedef union TimerXControl_t
{
    uint32_t all;
    struct {
        uint32_t OneShot:1;     //0
        uint32_t TimerSize:1;   //1
        uint32_t TimerPre:2;    //3:2
        uint32_t Reserved0:1;   //4
        uint32_t IntEnable:1;   //5
        uint32_t TimerMode:1;   //6
        uint32_t TimerEn:1;     //7
        uint32_t Reserved1:24;  //31:8
    } bits;
} TimerXControl_t;

typedef union TimerXRIS_t
{
    uint32_t all;
    struct {
        uint32_t TimerXRIS:1;   //0
        uint32_t Reserved:31;   //31:1
    } bits;
} TimerXRIS_t;

typedef union TimerXMIS_t
{
    uint32_t all;
    struct {
        uint32_t TimerXMIS:1;   //0
        uint32_t Reserved:31;   //31:1
    } bits;
} TimerXMIS_t;

typedef struct Timer_t
{
    uint32_t        timerxload;     // 0x00
    uint32_t        timerxvalue;    // 0x04
    TimerXControl_t timerxcontrol;  // 0x08
    uint32_t        timerxintclr;   // 0x0C
    TimerXRIS_t     timerxris;      // 0x10
    TimerXMIS_t     timerxmis;      // 0x14
    uint32_t        timerxbgload;   // 0x18
} Timer_t;

#define TIMER_CPU_BASE  0x10011000
#define TIMER_INTERRUPT 36

#define TIMER_FREERUNNING   0
#define TIMER_PERIOIC       1

#define TIMER_16BIT_COUNTER 0
#define TIMER_32BIT_COUNTER 1

#define TIMER_10HZ_INTERVAL       (32768 * 4)

#endif /* HAL_RVPB_TIMER_H_ */

인스턴스화 하자.

hal/rvpb/Regs.c

#include "stdint.h"
#include "Uart.h"
#include "Interrupt.h"
+#include "Timer.h"

volatile PL011_t*   Uart    = (PL011_t*)UART_BASE_ADDRESS0;
volatile GicCput_t* GicCpu  = (GicCput_t*)GIC_CPU_BASE;
volatile GicDist_t* GicDist = (GicDist_t*)GIC_DIST_BASE;
+volatile Timer_t*   Timer   = (Timer_t*)TIMER_CPU_BASE;

 

인터페이스를 작성하자.

hal/HalTimer.h 

#ifndef HAL_HALTIMER_H_
#define HAL_HALTIMER_H_

void     Hal_timer_init(void);

#endif /* HAL_HALTIMER_H_ */

 

인터페이스를 구현하자

hal/rvpb/Timer.c

#include "stdint.h"
#include "Timer.h"
#include "HalTimer.h"
#include "HalInterrupt.h"

extern volatile Timer_t* Timer;

static void interrupt_handler(void);

static uint32_t internal_1ms_counter;

void Hal_timer_init(void)
{
    // interface reset
    Timer->timerxcontrol.bits.TimerEn = 0;
    Timer->timerxcontrol.bits.TimerMode = 0;
    Timer->timerxcontrol.bits.OneShot = 0;
    Timer->timerxcontrol.bits.TimerSize = 0;
    Timer->timerxcontrol.bits.TimerPre = 0;
    Timer->timerxcontrol.bits.IntEnable = 1;
    Timer->timerxload = 0;
    Timer->timerxvalue = 0xFFFFFFFF;

    // set periodic mode
    Timer->timerxcontrol.bits.TimerMode = TIMER_PERIOIC;
    Timer->timerxcontrol.bits.TimerSize = TIMER_32BIT_COUNTER;
    Timer->timerxcontrol.bits.OneShot = 0;
    Timer->timerxcontrol.bits.TimerPre = 0;
    Timer->timerxcontrol.bits.IntEnable = 1;

    uint32_t interval = TIMER_10HZ_INTERVAL / 100;

    Timer->timerxload = interval;
    Timer->timerxcontrol.bits.TimerEn = 1;

    internal_1ms_counter = 0;

    // Register Timer interrupt handler
    Hal_interrupt_enable(TIMER_INTERRUPT);
    Hal_interrupt_register_handler(interrupt_handler, TIMER_INTERRUPT);
}

static void interrupt_handler(void)
{
    internal_1ms_counter++;

    Timer->timerxintclr = 1;
}

데이터 시트에 나온대로 인터페이스를 초기화했다.

 

periodic을 설정하는 부분에서 1밀리초 간격으로 인터럽트를 발생하게 타이머를 설정하고있다.

interval 변수를 정의하는 부분이 중요하다. interval변수의 값이 로드 레지스터로 들어간다. 따라서 이 값이 타이머 인터럽트의 발생 간격을 지정한다.

1밀리초는 어떻게 계산한것인지 따라가보자.

RealViewPB는 타이머 클럭 소스(clock source)로 1MHz 클럭을 받거나 32.768kHz짜리 크리스탈 오실레이터를 클럭으로 쓸 수 있다. 그렇다면 QEMU의 RealViewPB는 무엇을 써야할까? RealViewPB 데이터시트에 시스템 컨트롤 레지스터0(SYS_CTRL0 register)의 15번 비트가 타이머 클록이 무엇으로 설정되어있는지 나와있다.

 

SYS_CTRL0의 메모리 값은 0x1000 1000 이다. 메모리 값을 찍어보자.

Main.c

static void Printf_test(void)
{
    char* str = "printf pointer test";
    char* nullptr = 0;
    uint32_t i = 5;
+    uint32_t* sysctrl0 = (uint32_t*)0x10001000;

    debug_printf("%s\n", "Hello printf");
    debug_printf("output string pointer: %s\n", str);
    debug_printf("%s is null pointer, %u number\n", nullptr, 10);
    debug_printf("%u = 5\n", i);
    debug_printf("dec=%u hex=%x\n", 0xff, 0xff);
    debug_printf("print zero %u\n", 0);
+    debug_printf("SYSCTRL0 %x\n", *sysctrl0);
}

 

0이 출력된다. 따라서 QEMU의 RealViewPB는 클럭 소스로 1MHz 클럭을 쓴다. 알아두어야 할 것은, QEMU는 SW 에뮬레이터이므로 해당 클럭이 정확하게 동작되지는 않는다. 

기준 클럭이 1MHz

타이머의 로드 레지스터에 값을 어떻게 설정해야할까?

https://developer.arm.com/documentation/ddi0271/d/functional-overview/functional-description/programming-the-timer-interval

 

Documentation – Arm Developer

 

developer.arm.com

마지막 예제처럼 계산한다.

책에서 캡쳐. PL804 는 오타이고, SP804가 맞는 듯 하다.

TIMCLK이 1048576(1024*1024 = 1M)이고 TIMCLKENX가 1이고 PRESCALE도 1이므로 분모는 신경쓰지 않아도 된다. 그러므로 클럭값 자체가 로드 레지스터 값이 된다. 공식대로라면 1M인 1048576을 설정하면 타이머 인터럽트가 1초마다 한번씩 발생하게 된다. 현재 원하는 건 1밀리초이므로 1/1000로 줄이자. 따라서 1048576을 1000으로 나눈 값으로 로드 레지스터에 설정한 것이다.

 

인터럽트 핸들러가 동작한다면 1밀리초마다 실행될 것이므로 internal_1ms_counter 변수의 값이 1밀리초마다 1씩 증가하게된다. 변수 값을 증가시킨 다음에는, 반드시 타이머 HW에서 인터럽트를 클리어해야한다. 그렇지 않으면 HW가 인터럽트 신호를 계속 GIC에 보내는 동작으로 해석될 것이다.

 

타이머 카운터 오버플로

전원이 들어오면 꺼질때까지 internal_1ms_counter변수가 1씩 증가한다. 단, 32비트이므로 오버플로(overflow)문제가 생긴다. 

32비트변수의 최대값은 0xFFFF FFFF이고 10진수로는 4294967295이다. 1000마다 1초이므로, 4294967.295 약 4294967초를 오버플로 없이 잴 수 있다. 일로 계산하면 49.x 이므로 50일 이전에 시스템을 다시 켜주어야한다는 이슈가 있다. 

다음에 만들 delay()함수도 타이머 카운터를 쓴다. 해당 함수에서 오버플로가 생겼을때도 정상작동할 수 있게 해보자.

delay() 함수

가장 간단한 형태의 시간 지연 함수 delay() 함수를 만들어보자. 해당 함수는 특정 시간만큼 아무 일도 하지 않고 시간이 지날 때까지 기다리는 함수이다.

 

lib/stdlib.h

#ifndef LIB_STDLIB_H_
#define LIB_STDLIB_H_

void delay(uint32_t ms);

#endif /* LIB_STDLIB_H_ */

 

hal/HalTimer.h

#ifndef HAL_HALTIMER_H_
#define HAL_HALTIMER_H_

void     Hal_timer_init(void);
+uint32_t Hal_timer_get_1ms_counter(void);

#endif /* HAL_HALTIMER_H_ */

 

hal/rvpb/Timer.c 에 아래의 코드를 추가하자

uint32_t Hal_timer_get_1ms_counter(void)
{
    return internal_1ms_counter;
}

 

이제 delay() 함수를 구현하자.

lib/stdlib.c

#include "stdint.h"
#include "stdbool.h"
#include "HalTimer.h"

void delay(uint32_t ms)
{
    uint32_t goal = Hal_timer_get_1ms_counter() + ms;

    while(goal != Hal_timer_get_1ms_counter());
}

 

while 절의 비교연산자를 != 로 썻다. 만약 > 로 했다면, goal 이 오버플로가 발생한 경우에 while 의 조건절이 항상 false가 되므로 잘못 작성한 것이된다.

 

테스트해보자. 1초마다 한번씩 출력되도록 작성했다.

Main.c

#include "stdint.h"
#include "stdbool.h"

#include "HalUart.h"
#include "HalInterrupt.h"
+#include "HalTimer.h"

#include "stdio.h"
+#include "stdlib.h"

static void Hw_init(void);

static void Printf_test(void);
+static void Timer_test(void);

void main(void)
{
	Hw_init();

	uint32_t i = 100;
	while(i--)
	{
		Hal_uart_put_char('N');
	}
	Hal_uart_put_char('\n');

	putstr("Hello World!\n");

	Printf_test();
	Timer_test();

	while(true);
}

static void Hw_init(void)
{
	Hal_interrupt_init();
	Hal_uart_init();
	Hal_timer_init();
}


static void Printf_test(void)
{
	char* str = "printf pointer test";
	char* nullptr = 0;
	uint32_t i = 5;
	uint32_t* sysctrl0 = (uint32_t*)0x10001000;

	debug_printf("%s\n", "Hello printf");
	debug_printf("output string pointer: %s\n", str);
	debug_printf("%s is null pointer, %u number\n", nullptr, 10);
	debug_printf("%u = 5\n", i);
	debug_printf("dec=%u hex=%x\n", 0xff, 0xff);
	debug_printf("print zero %u\n", 0);
	debug_printf("SYSCTRL0 %x\n", *sysctrl0);
}

+static void Timer_test(void)
+{
+	while(true)
+	{
+		debug_printf("current count : %u\n", Hal_timer_get_1ms_counter());
+		delay(1000);
+	}
+}

 

출력결과를 보니 제대로 동작한다.

root@21d947b2e449:/project# make run
qemu-system-arm -M realview-pb-a8 -kernel build/navilos.axf -nographic
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5220:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2642:(snd_pcm_open_noupdate) Unknown PCM default
alsa: Could not initialize DAC
alsa: Failed to open `default':
alsa: Reason: No such file or directory
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5220:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2642:(snd_pcm_open_noupdate) Unknown PCM default
alsa: Could not initialize DAC
alsa: Failed to open `default':
alsa: Reason: No such file or directory
audio: Failed to create voice `lm4549.out'
NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN
Hello World!
Hello printf
output string pointer: printf pointer test
(null) is null pointer, 10 number
5 = 5
dec=255 hex=ff
print zero 0
SYSCTRL0 0
current count : 0
current count : 1000
current count : 2000
current count : 3000
current count : 4000
current count : 5000
current count : 6000
current count : 7000
current count : 8000
current count : 9000
current count : 10000
current count : 11000
current count : 12000
current count : 13000
current count : 14000
current count : 15000
current count : 16000
current count : 17000
current count : 18000
current count : 19000
current count : 20000
current count : 21000
current count : 22000
current count : 23000
current count : 24000
current count : 25000
current count : 26000
current count : 27000
current count : 28000

🚧 중간 Remark 1

7장까지 펌웨어를 구현했다.

 

이후의 장부터는 RTOS라고 부를 수 있는 기능들을 만든다.

"중간 Remark 2" 이전까지는 8~10장을 다룬다. 태스크, 스케줄러, 컨텍스트 스위칭을 만들어서 RTOS다운 기능 만든다.


8장 태스크

OS란 무엇일까? 저자는 "태스크(Task)를 관리하여 사용자가 하고 싶은 것을 할 수 있도록 도와주는 것"이라 표현한다. 

 

중심에는 태스크라는 것이 있고, 이 태스크가 인터럽트와도 연관이 있다. 그리고 우선순위라는 것이 있어서 어떤 태스크는 다른 태스크에 의해서 동작이 지연될 수도 있다.

 

태스크라는 용어는 Windows나 Linux같은 범용 OS에서는 프로그램 혹은 프로세스라고 봐도 된다. RTOS에서는 일반적으로 태스크라고 부른다. 그래서 RTOS를 개발하는 과정에서 가장 먼저 해야하는 일은 태스크라는 것 자체를 추상화하고 설계하는 일이다. 즉, 태스크를 어떻게 관리해야 할지 결정하는 것을 의미한다.

⭐️태스크 컨트롤 블록(TCB)

  • 태스크 컨트롤 블록(Task Control Block, TCB)
    • 개별 태스크 자체를 추상화하는 자료구조
    • 그렇다면 무엇을 포함하고 있어야 태스크를 추상화한다고 말할수있나?
      • 필수적인 요소
        • 컨텍스트
        • 태스크 이름
        • 태스크 번호
        • 태스크 우선순위
        • 등 태스크를 관리하는 데 필요한 부수적인 정보

TCB 구현은 RTOS 커널(kernel)을 만드는 첫 번째 작업이다. kernel 디렉터리를 새로 만들어서 진행하겠다.

 

kernel/task.h

#ifndef KERNEL_TASK_H_
#define KERNEL_TASK_H_

#include "MemoryMap.h"

#define NOT_ENOUGH_TASK_NUM     0xFFFFFFFF

#define USR_TASK_STACK_SIZE     0x100000
#define MAX_TASK_NUM            (TASK_STACK_SIZE / USR_TASK_STACK_SIZE)

typedef struct KernelTaskContext_t
{
	uint32_t spsr;
	uint32_t r0_r12[13];
	uint32_t pc;
} KernelTaskContext_t;

typedef struct KernelTcb_t
{
	uint32_t sp;
	uint8_t* stack_base;
} KernelTcb_t;

typedef void (*KernelTaskFunc_t)(void);

void     Kernel_task_init(void);
uint32_t Kernel_task_create(KernelTaskFunc_t startFunc);

#endif /* KERNEL_TASK_H_ */

 

USR_TASK_STACK_SIZE 는 개별 태스크의 스택 크기이다. 0x100000는 10진수로 1024*1024이므로, 1MB이다. 각 태스크별로 1MB씩 스택을 할당하겠다는 의미이다. 참고로 QEMU에서 개발중이므로 메모리제한에서 자유로운 것이지, 임베디드 실무 프로젝트라면 몇 KB의 스택도 간신히 사용하는 경우가 대부분이다.

또한 태스크의 스택 크기가 모두 같도록 설계되었는데, 당연히 개별 태스크마다 필요에 따라 스택 크기를 다르게 하는 것이 더 유연한 설계이다. 필요하다면 기능을 확장해보자.

태스크 스택용으로 64MB를 할당해놓았고 각각의 태스크가 동일하게 1MB씩 스택을 쓸 수 있으므로 task.h에 따르면 태스크를 최대 64개까지 사용가능하다. 당연하게도 USR_TASK_STACK_SIZE를 반으로 줄이면 태스크 개수가 2배가 된다.

 

구조체를 보자.

KernelTaskContext_t는 컨텍스트를 추상화한 자료구조다. "컨텍스트"의 실체인 것이다.

KernelTcb_t에는 스택 관련 정보만 저장한다. sp는 스택 포인터, stack_base 멤버 변수는 컨텍스트에 포함되지 않는 부가 데이터라고 볼 수 있다. 개별 태스크의 스택 베이스 주소를 저장하기 위해 만들었다.

  • 태스크 컨텍스트(context) == 레지스터들의 값 (특히 스택 포인터의 값을 생각해보자)
  • 스택 포인터도 레지스터의 일부이므로, 태스크 컨텍스트를 전환한다는 것은 코어의 레지스터 값을 다른 태스크의 것으로 바꾼다는 말과 같다

태스크 컨트롤 블록(TCB) 초기화

kernel/task.c

#include "stdint.h"
#include "stdbool.h"

#include "ARMv7AR.h"
#include "task.h"

static KernelTcb_t  sTask_list[MAX_TASK_NUM];
static uint32_t     sAllocated_tcb_index;

void Kernel_task_init(void)
{
	sAllocated_tcb_index = 0;

	for(uint32_t i = 0 ; i < MAX_TASK_NUM ; i++)
	{
		sTask_list[i].stack_base = (uint8_t*)(TASK_STACK_START + (i * USR_TASK_STACK_SIZE));
		sTask_list[i].sp = (uint32_t)sTask_list[i].stack_base + USR_TASK_STACK_SIZE - 4;

		sTask_list[i].sp -= sizeof(KernelTaskContext_t);
		KernelTaskContext_t* ctx = (KernelTaskContext_t*)sTask_list[i].sp;
		ctx->pc = 0;
		ctx->spsr = ARM_MODE_BIT_SYS;
	}
}

uint32_t Kernel_task_create(KernelTaskFunc_t startFunc)
{
	return NOT_ENOUGH_TASK_NUM;
}

Kernel_task_create() 함수는 지금은 에러를 리턴하도록 두었다.

 

  • sTask_list
    • TCB 을 64개 배열로 선언했다.
    • static으로 동적 메모리 할당을 피했다. 일종의 객체 풀(object pool)로 만든 것이다.
  • sAllocated_tcb_index
    • 생성한 TCB 인덱스를 저장하고 있는 변수이다.
    • 태스크를 생성할 때마다 하나씩 이 변수의 값을 늘린다. 그래서 태스크를 몇개까지 생성했는지, 이 변수 값을 보고 추적한다.
    • Kernel_task_create() 함수가 한 번 호출될 때마다 sTask_list 배열에서 TCB를 한개씩 사용한다. 그러므로 커널은 배열에서 어떤 인덱스가 사용가능한지 알아야한다. 
    • 종합해보면, 64개 객체들 중 sAllocated_tcb_index의 값보다 작은 인덱스의 TCB들은 이미 할당된 것이라는 소리다.

반복문 속에서는 TCB를 순회하면서 초기화한다. 특히, PSR 기본값을 SYS모드로 설정해두고 있다.

 

스택 베이스 주소도 설정해주고 있는데, 1MB씩 늘려가면서 지정해주는 것이다. 

스택 포인터도 할당하고있는데, 스택은 거꾸로 내려가므로 스택 포인터는 stack_base값에서 USR_TASK_STACK_SIZE만큼 증가한 값을 지정한다. 저자의 취향대로 태스크 간의 스택 경계를 표시하는 패딩값 4바이트도 두었다.

위 두 과정이 태스크 관리의 중요한 특징을 만든 것이다.

 

navilos는 태스크의 컨텍스트를 TCB가 아니라 해당 태스크의 스택에 저장한다. 태스크의 컨텍스트를 어디에 저장하느냐는 개발자가 설계하기 나름이다.

 

초기화 한 태스크 스택 구조는 아래와 같다.

책에서 캡쳐

스택 포인터가 태스크 컨텍스트 다음에 위치하지만, 태스크 컨텍스트는 앞으로 설명할 컨텍스트 스위칭 작업에 의해서 모두 레지스터로 복사되고 스택 포인터는 태스크 컨텍스트가 없는 위치로 이동하게 된다. 따라서 동작 중인 태스크의 스택에는 태스크 컨텍스트가 존재하지 않는다.

태스크 생성

Kernel_task_create() 함수를 완성시키자.

태스크로 동작할 함수를 TCB에 등록하자. 그리고 TCB를 커널에 만든다.

kernel/task.c 

중략

uint32_t Kernel_task_create(KernelTaskFunc_t startFunc)
{
-	return NOT_ENOUGH_TASK_NUM;
+	KernelTcb_t* new_tcb = &sTask_list[sAllocated_tcb_index++];
+
+	if (sAllocated_tcb_index > MAX_TASK_NUM)
+	{
+		return NOT_ENOUGH_TASK_NUM;
+	}
+
+	KernelTaskContext_t* ctx = (KernelTaskContext_t*)new_tcb->sp;
+	ctx->pc = (uint32_t)startFunc;
+
+	return (sAllocated_tcb_index - 1);
}

코드 흐름을 분석해보자.

 

미사용 중인 TCB를 갖고와서, 에러검사를 한다.

 

현재 스택에 저장되어있는 컨텍스트 메모리 주소 포인터를 갖고오는 코드가 핵심이다.

 

파라미터로 넘어온 함수포인터(함수의 시작 주소)를 PC에 넣어준다. 이 코드가 태스크 함수를 TCB에 등록하는 역할을 한다.

 

인덱스에서 1을 빼서(첫줄에서 배열 인덱싱 하자마자 올렷으니, 현재 인덱스는 -1 을 해준 값이다) 리턴해준다.

 

 

태스크를 동작시키려면 스케줄러와 컨텍스트 스위칭까지 만들어야한다. 따라서 지금은 태스크 생성과 등록에 대해서만 작업하자.

보통 전체 시스템을 각 기능별로 나누어 개발하고, 해당 기능을 실행하는 태스크 함수를 대표로 하나 만든다. 그리고 펌웨어가 시작될 때 RTOS를 초기화하는 코드에서 개별적으로 태스크를 등록한다. 

 

두가지 예를 들어보자. 

  1. 네트워크를 처리하는 기능을 구현하는 소스파일들은 별도의 디렉터리에 들어있고 그중 한 소스 파일에 태스크 함수가 있는 것이다.
  2. 화면 출력 기능이라면 해당 기능을 구현하는 소스파일들은 또 별도의 디렉터리에 모여있고 그중 하나에 대표 태스크 함수가 있는 식이다.

지금은 그런것이 없으니 일단 Main.c 파일에 더미 태스크 함수를 만들고 커널에 등록하자.

#include "stdint.h"
#include "stdbool.h"

#include "HalUart.h"
#include "HalInterrupt.h"
#include "HalTimer.h"

#include "stdio.h"
#include "stdlib.h"

+#include "Kernel.h"

static void Hw_init(void);
+static void Kernel_init(void);

static void Printf_test(void);
static void Timer_test(void);

+void User_task0(void);
+void User_task1(void);
+void User_task2(void);

void main(void)
{
	Hw_init();

	uint32_t i = 100;
	while(i--)
	{
		Hal_uart_put_char('N');
	}
	Hal_uart_put_char('\n');

	putstr("Hello World!\n");

	Printf_test();
	Timer_test();

	while(true);
}

static void Hw_init(void)
{
	Hal_interrupt_init();
	Hal_uart_init();
	Hal_timer_init();
}

+static void Kernel_init(void)
+{
+	uint32_t taskId;
+
+	Kernel_task_init();
+
+	taskId = Kernel_task_create(User_task0);
+	if (NOT_ENOUGH_TASK_NUM == taskId)
+	{
+		putstr("Task0 creation fail\n");
+	}
+
+	taskId = Kernel_task_create(User_task1);
+	if (NOT_ENOUGH_TASK_NUM == taskId)
+	{
+		putstr("Task1 creation fail\n");
+	}
+
+	taskId = Kernel_task_create(User_task2);
+	if (NOT_ENOUGH_TASK_NUM == taskId)
+	{
+		putstr("Task2 creation fail\n");
+	}
+}


static void Printf_test(void)
{
	char* str = "printf pointer test";
	char* nullptr = 0;
	uint32_t i = 5;
	uint32_t* sysctrl0 = (uint32_t*)0x10001000;

	debug_printf("%s\n", "Hello printf");
	debug_printf("output string pointer: %s\n", str);
	debug_printf("%s is null pointer, %u number\n", nullptr, 10);
	debug_printf("%u = 5\n", i);
	debug_printf("dec=%u hex=%x\n", 0xff, 0xff);
	debug_printf("print zero %u\n", 0);
	debug_printf("SYSCTRL0 %x\n", *sysctrl0);
}

static void Timer_test(void)
{
	while(true)
	{
		debug_printf("current count : %u\n", Hal_timer_get_1ms_counter());
		delay(1000);
	}
}

+void User_task0(void)
+{
+	debug_printf("User Task #0");
+
+	while(true);
+}

+void User_task1(void)
+{
+	debug_printf("User Task #1");
+
+	while(true);
+}

+void User_task2(void)
+{
+	debug_printf("User Task #2");
+
+	while(true);
+}

더미 태스크 함수 3개를 등록했다.

함수포인터를 Kernel_task_create() 함수에 넘긴다. 해당 함수 포인터는 TCB의 PC에 저장된다. 나중에 컨텍스트 스위칯ㅇ 시, ARM 코어의 PC 레지스터에 TCB의 PC값이 저장된다. 그 순간 해당 태스크 함수가 호출(실행)되는 것이다.

 

또한, 태스크들에 무한루프를 주었다. 이게 중요한 포인트이다. 현재 navilos의 태스크 관리 설계에는 태스크의 종료를 보장하는 기능이 없기 때문에 중요한 포인트이다. 태스크는 종료되면 안된다.

 

태스크를 동작시키기 위해, 스케줄러와 컨텍스트 스위칭의 구현이 필요하다고 했다. 스케줄러를 먼저 보자

9장 스케줄러

  • 스케줄러
    • 지금 실행 중인 태스크 다음에 실행할 태스크가 무엇인지 골라주는 녀석
    • 스케줄러를 얼마나 효율적으로 만드느냐에 따라서 RTOS의 성능이 좌우되기도 할 정도로 중요하다
    • 하지만 가장 단순하고 쉬운 스케줄러로도 충분한 성능과 목적을 달성할 수 있기도 하다

간단한 스케줄러

  • 가장 간단한 방법?
    • 현재 실행 중인 태스크 컨트롤 블록의 바로 다음 태스크 컨트롤 블록을 선택
    • 예를 들어
      • 현재 실행중인 TCB의 배열 인덱스가 1이라면 다음 실행할 TCB의 태스크 인덱스를 2로 선택하는 것
    • 이처럼 추가적 계산없이 인덱스를 계속 증가시키면서 대상을 선택하는 알고리즘을 "라운드 로빈(round robin) 알고리즘"이라 함
    • 당연히 최대값에 이르면 0으로 돌아간다 (그래서 이름에 round가 붙음)

스케줄러 역시 태스크 관련 작업이므로 kernel/task.c에 작성하자.

 

라운드 로빈 알고리즘은 아래와 같이 작성할 수 있다.

static uint32_t     sCurrent_tcb_index;

static KernelTcb_t* Scheduler_round_robin_algorithm(void);

static KernelTcb_t* Scheduler_round_robin_algorithm(void)
{
    sCurrent_tcb_index++;
    sCurrent_tcb_index %= sAllocated_tcb_index;

    return &sTask_list[sCurrent_tcb_index];
}

우선순위 스케줄러

에뮬레이터라는 QEMU의 제약으로, 우선순위 스케줄러는 프로젝트로써는 구현하지 않는다. 

본 포스팅에서는 스킵!

⭐️10장 컨텍스트 스위칭

  • 컨텍스트 스위칭
    • 말 그대로, 컨텍스트를 전환(switching)한다는 것
  • 태스크 == 동작하는 프로그램
  • 동작하는 프로그램의 정보 == 컨텍스트
  • 이 컨텍스트를 어딘가에 저장하고, 또 다른 어딘가에서 컨텍스트를 가져다가 프로세서 코어에 복구하면 다른 프로그램이 동작한다.
    • 즉, 태스크가 바뀐 것이다
  • 이전 장에서 navilos의 태스크 컨텍스트를, 태스크의 스택에 저장하겠다고 결정했다
  • 그러므로 navilos의 컨텍스트 스위칭은 아래와 같은 과정으로 진행된다
    1. 현재 동작하고있는 태스크의 컨텍스트를 현재 스택에 백업한다
    2. 다음에 동작할 TCB를 스케줄러에서 받는다
    3. 2에서 받은 TCB에서 스택 포인터를 읽는다
    4. 3에서 읽은 태스크의 스택에서 컨텍스트를 읽어서 ARM코어에 복구한다
    5. 다음에 동작할 태스크의 직전 프로그램 실행위치로 이동한다. 이러면 이제 현재 동작하고 있는 태스크가 된다

코드로 작성해보자.

kernel/task.c

 

우선 태스크 스케줄러의 클라이언트로써의 코드 뼈대를 구성하자.

static KernelTcb_t* sCurrent_tcb;
static KernelTcb_t* sNext_tcb;

void Kernel_task_scheduler(void)
{
    sCurrent_tcb = &sTask_list[sCurrent_tcb_index];
    sNext_tcb = Scheduler_round_robin_algorithm();

    Kernel_task_context_switching();
}

마지막에 컨텍스트 스위칭 함수를 호출하도록 구성했다.

 

⭐️컨텍스트 스위칭 함수를 작성하자.

__attribute__ ((naked)) void Kernel_task_context_switching(void)
{
    __asm__ ("B Save_context");
    __asm__ ("B Restore_context");
}

__attribute__ ((naked)) : GCC 어트리부트 기능으로 naked 키워드를 이용해, 프롤로그와 에필로그에 관한 어셈블리어가 생성되지 않는다. 작성한 어셈블리어만 생긴다.

navilos는 컨텍스트를 스택에 백업하고 스택에서 복구할것이므로, 컨텍스트 스위칭을 할 때 되도록 스택을 그대로 유지하는 것이 좋기때문에 __attribute__ ((naked))를 사용하여 레지스터 값에 변화가 없도록 했다. 또한 B 명령어를 이용한 것도 LR 을 변경하지 않기 위해서이다. 

Save_context 함수를 호출하는 것처럼, Restore_context 함수를 호출하는 것처럼 작성했다. C언어로 작성했지만, C언어 규약과 관계없이 진짜 어셈블리어로 조작하도록 만든 것이다. 

 

태스크가 2개 있을때, 컨텍스트 스위칭 과정을 그림으로 표현하면 아래와 같다.

책에서 캡쳐

Task1은 non-active상태로 갈 태스크고, Task2는 active상태로 갈 태스크이다.

Task1의 스택 포인터(SP) 값을 를 TCB에 저장하고, 복구시 SP에 Task2의 저장되어있던 스택 포인터 값을 쓴다.

 

cf) navilos의 컨텍스트 스위칭은 엄밀하게 말해서 Windows나 Linux의 프로세스를 전환하는 컨텍스트 스위칭보다는, 한 프로세스 안에서 쓰레드(thread) 간에 전환하는 모습에 더 가깝다. 하지만 의미적으로 둘은 거의 차이가 없다.

 

위에서 작성한 코드

Save_context 함수, Restore_context 함수를 구현하자.

컨텍스트 백업하기

 

⭐️컨텍스트 백업코드를 작성하자

 

그 전에 컨텍스트 자료구조를 다시 보자

kernel/task.h

typedef struct KernelTaskContext_t
{
    uint32_t spsr;
    uint32_t r0_r12[13];
    uint32_t pc;
} KernelTaskContext_t;

C언어의 구조체 멤버변수는 메모리주소가 작은 값부터 큰값으로 배정된다. spsr이 0x04에 저장된다면, r0는 0x08, r1은 0x0C에 저장되는 식이다. 하지만 스택은 메모리 주소가 큰값에서 작은값으로 진행된다.

그래서 KernelTaskContext_t에 맞춰 컨텍스트를 스택에 백업할 때는 pc, r0_r12, spsr 순서로 백업해야, 의도한 자료구조 의미에 맞는 메모리 주소에 값이 저장된다.

 

kernel/task.c

static __attribute__ ((naked)) void Save_context(void)
{
    // save current task context into the current task stack
    __asm__ ("PUSH {lr}");
    __asm__ ("PUSH {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12}");
    __asm__ ("MRS   r0, cpsr");
    __asm__ ("PUSH {r0}");
    // save current task stack pointer into the current TCB
    __asm__ ("LDR   r0, =sCurrent_tcb");
    __asm__ ("LDR   r0, [r0]");
    __asm__ ("STMIA r0!, {sp}");
}

LR을 스택에 푸시한다. LR은 KernelTaskContext_t의 pc 멤버변수에 저장된다. 나중에 태스크가 다시 스케줄링 받았을 때 복귀하는 위치는 pc 멤버 변수가 저장하고 있고, 이 위치는 Kernel_task_context_switching() 함수의 리턴 주소이다. 그렇기에 pc 멤버변수에 현재 컨텍스트의 LR값을 그대로 저장한다.

 

범용레지스터 r0~r12까지를 스택에 푸시하고있다. __attribute__ ((naked)) 컴파일러 어트리부트 지시어로인해 다른 값들이 덮어씌지 않기때문에 Kernel_task_context_switching() 함수 호출하기 직전의 값이 유지되고 있다. 따라서 본 함수에도 동일 지시어를 쓴다.

이제 범용 레지스터를 백업했기에 다음코드부터는 덮어쓰고 활용해도된다. 컨텍스트가 넘어갈때 백업값이 복구되므로 마치 스코프영역에서 쓰는 변수에 비유하면 비슷하다.

 

6~7번째 줄이 CPSR을 KernelTaskContext_t의 spsr 멤버 변수 위치에 저장하는 코드이다. 프로그램 상태 레지스터는 직접 메모리에 저장할 수 없으므로 R0를 사용한다. 

 

2번째 단락의 코드뭉치는 C언어로 표현하면 이해하기 쉽다. C언어로 표현하면 아래와 같이 표현된다.

sCurrent_tcb->sp = ARM_코어_SP_레지스터값;

//혹은

(uint32_t)(*sCurrent_tcb) = ARM_코어_SP_레지스터값;

컨텍스트 복구하기

컨텍스트 복구 작업은, 컨텍스트 백업 작업의 역순이다.

 

⭐️컨텍스트 복구코드를 작성하자

kernel/task.c

static __attribute__ ((naked)) void Restore_context(void)
{
    // restore next task stack pointer from the next TCB
    __asm__ ("LDR   r0, =sNext_tcb");
    __asm__ ("LDR   r0, [r0]");
    __asm__ ("LDMIA r0!, {sp}");
    // restore next task context from the next task stack
    __asm__ ("POP  {r0}");
    __asm__ ("MSR   cpsr, r0");
    __asm__ ("POP  {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12}");
    __asm__ ("POP  {pc}");
}

 

yield 만들기

스케줄러와 컨텍스트 스위칭을 만들었으니, 태스크를 전환할 수 있다. 스케줄러와 컨텍스트 스위칭을 합쳐서 스케줄링(scheduling)이라고 한다. 

그렇다면 언제 스케줄링 할 것인지를 결정해야한다.

 

OS의 몇개의 스케줄링 정책에 대해 알아보자.

  • 정기정으로 발생하는 타이머 인터럽트에 연동해서 스케줄링을 하고 각 태스크가 일정한 시간만 동작하고 다음 태스크로 전환되는 시스템이라면, 이 시스템의 OS를 "시분할 시스템"이라고 한다.
  • 태스크가 명시적으로 스케줄링을 요청하지 않았는데 커널이 강제로 스케줄링 하는 시스템을 "선점형 멀티태스킹 시스템"이라고 한다.
  • 반대로 태스크가 명시적으로 스케줄링을 요청하지 않으면 커널이 스케줄링하지 않는 시스템을 "비선점형 멀티태스킹 시스템"이라고 한다.

RTOS를 시분할로할지 말지, 그리고 선점형으로 할지 비선점형으로하지는 임베디드 시스템의 요구사항에 따라 달라진다. 설계하기 나름이지만, 일반적으로 시분할 시스템은 거의 선점형 멀티태스킹 시스템이다.

 

navilos 프로젝트에서는 시분할이 아니고 비선점형 스케줄링을 사용하자.

이는 스케줄링하려면 태스크가 명시적으로 커널에 스케줄링을 요청해야 한다는 말이다. 태스크가 커널에 스케줄링을 요청하는 동작은 태스크가 CPU 자원을 다음 태스크에 "양보"한다는 의미로 해석할 수있다. 그래서  yield라는 naming을 한다.

 

그러면 yield() 함수는 어떤 디렉터리 계층구조에 두는게 적절할까? 저자는 커널 API가 적절하다고 판단했다. 커널 API를 별도로 만들어서 외부에서 사용하도록 하는것이다.

커널 API 용으로 kernel/Kernel.c와 kernel/Kernel.h 파일을 만들자

 

kernel/Kernel.h

#ifndef KERNEL_KERNEL_H_
#define KERNEL_KERNEL_H_

#include "task.h"

void Kernel_yield(void);

#endif /* KERNEL_KERNEL_H_ */

kernel/Kernel.c

#include "stdint.h"
#include "stdbool.h"

#include "Kernel.h"

void Kernel_yield(void)
{
    Kernel_task_scheduler();
}

 

구현은 단순하게 Kernel_task_scheduler() 함수를 직접 호출하는 것이 전부이다.

동작중인 태스크가 Kernel_yield() 함수를 호출하면 즉시 스케줄러를 호출해서 다음 동작할 태스크를 선정하는 것이다. 스케줄링되는 과정은 스케줄링 정책과 컨텍스트 스위칭 과정이 더해진 것임을 상기하자.

커널 시작하기

이전에 만든 태스크 3개를 동작시켜보자. 

그냥 스케줄러를 실행시키면될까? 안된다. 왜냐하면 커널을 시작할 때는 동작중인 태스크가 없기 때문이다. 

커널이 시작하는 시점에 동작중인 태스크가 없으므로 컨텍스트를 스위칭할때 커널 스택에 현재 커널의 컨텍스트가 저장되는 문제가 생긴다. 커널 스택에 쓰레기 데이터가 쌓일 뿐 동작에는 문제가 없긴하지만, 의도된 동작은 아니다.

 

최초로 스케줄링할때는 컨텍스트를 백업하지 않도록 수정하자. 0번 TCB를 복구 대상으로 하자.

 

kernel/task.c

static KernelTcb_t  sTask_list[MAX_TASK_NUM];
static KernelTcb_t* sCurrent_tcb;
static KernelTcb_t* sNext_tcb;
static uint32_t     sAllocated_tcb_index;
static uint32_t     sCurrent_tcb_index;

void Kernel_task_init(void)
{
    sAllocated_tcb_index = 0;
+    sCurrent_tcb_index = 0;

    for(uint32_t i = 0 ; i < MAX_TASK_NUM ; i++)
    {
        sTask_list[i].stack_base = (uint8_t*)(TASK_STACK_START + (i * USR_TASK_STACK_SIZE));
        sTask_list[i].sp = (uint32_t)sTask_list[i].stack_base + USR_TASK_STACK_SIZE - 4;

        sTask_list[i].sp -= sizeof(KernelTaskContext_t);
        KernelTaskContext_t* ctx = (KernelTaskContext_t*)sTask_list[i].sp;
        ctx->pc = 0;
        ctx->spsr = ARM_MODE_BIT_SYS;
    }
}

+void Kernel_task_start(void)
+{
+    sNext_tcb = &sTask_list[sCurrent_tcb_index];
+    Restore_context();
+}

 

헤더파일로 커널 API를 정리해두자.

kernel/Kernel.h

#ifndef KERNEL_KERNEL_H_
#define KERNEL_KERNEL_H_

#include "task.h"

+void Kernel_start(void);
void Kernel_yield(void);

#endif /* KERNEL_KERNEL_H_ */

 

Kernel_start를 구현하자.

Kernel/Kernel.c

void Kernel_start(void)
{
    Kernel_task_start();
}

 

main() 함수에서 커널 시작하는 함수의 최 하단에서 Kernel_start API를 넣으면 된다.

boot/Main.c

static void Kernel_init(void)
{
    uint32_t taskId;

    Kernel_task_init();

    taskId = Kernel_task_create(User_task0);
    if (NOT_ENOUGH_TASK_NUM == taskId)
    {
        putstr("Task0 creation fail\n");
    }

    taskId = Kernel_task_create(User_task1);
    if (NOT_ENOUGH_TASK_NUM == taskId)
    {
        putstr("Task1 creation fail\n");
    }

    taskId = Kernel_task_create(User_task2);
    if (NOT_ENOUGH_TASK_NUM == taskId)
    {
        putstr("Task2 creation fail\n");
    }

+    Kernel_start();
}

 

태스크의 스택 주소를 확인하기 위해 태스크들에 디버깅용 코드를 추가해주자.

boot/Main.c

void User_task0(void)
{
+    uint32_t local = 0;


    while(true)
+    {
+	    debug_printf("User Task #0 SP=0x%x\n", &local);
+		Kernel_yield();
+    }
}

void User_task1(void)
{
+    uint32_t local = 0;


    while(true)
+    {
+	    debug_printf("User Task #1 SP=0x%x\n", &local);
+		Kernel_yield();
+    }
}

void User_task2(void)
{
+    uint32_t local = 0;


    while(true)
+    {
+	    debug_printf("User Task #2 SP=0x%x\n", &local);
+		Kernel_yield();
+    }
}

 

빌드 관련해서 kernel 디렉터리가 추가되었다. Makefile도 수정하자

ARCH = armv7-a
MCPU = cortex-a8

TARGET = rvpb

CC = arm-none-eabi-gcc
AS = arm-none-eabi-as
LD = arm-none-eabi-gcc
OC = arm-none-eabi-objcopy
+OD = arm-none-eabi-objdump

LINKER_SCRIPT = ./navilos.ld
MAP_FILE = build/navilos.map
+SYM_FILE = build/navilos.sym

ASM_SRCS = $(wildcard boot/*.S)
ASM_OBJS = $(patsubst boot/%.S, build/%.os, $(ASM_SRCS))

VPATH = boot \
        hal/$(TARGET)	\
-	lib
+	lib				\
+	kernel


C_SRCS  = $(notdir $(wildcard boot/*.c))
C_SRCS += $(notdir $(wildcard hal/$(TARGET)/*.c))
C_SRCS += $(notdir $(wildcard lib/*.c))
+C_SRCS += $(notdir $(wildcard kernel/*.c))
C_OBJS = $(patsubst %.c, build/%.o, $(C_SRCS))

INC_DIRS  = -I include 			\
            -I hal	   			\
            -I hal/$(TARGET)	\
-	    -I lib
+	    -I lib				\
+	    -I kernel

-CFLAGS = -c -g -std=c11
+CFLAGS = -c -g -std=c11 -mthumb-interwork

LDFLAGS = -nostartfiles -nostdlib -nodefaultlibs -static -lgcc

navilos = build/navilos.axf
navilos_bin = build/navilos.bin

.PHONY: all clean run debug kill_qemu gdb

all: $(navilos)

clean:
	@rm -fr build
	
run: $(navilos)
	qemu-system-arm -M realview-pb-a8 -kernel $(navilos) -nographic
	
debug: $(navilos)
	qemu-system-arm -M realview-pb-a8 -kernel $(navilos) -S -gdb tcp::1234,ipv4 -display none -nographic

kill_qemu:
	pkill -9 qemu-system-arm
	
gdb:
	gdb-multiarch
	
$(navilos): $(ASM_OBJS) $(C_OBJS) $(LINKER_SCRIPT)
	$(LD) -n -T $(LINKER_SCRIPT) -o $(navilos) $(ASM_OBJS) $(C_OBJS) -Wl,-Map=$(MAP_FILE) $(LDFLAGS)
+	$(OD) -t $(navilos) > $(SYM_FILE)
	$(OC) -O binary $(navilos) $(navilos_bin)
	
build/%.os: %.S
	mkdir -p $(shell dirname $@)
	$(CC) -march=$(ARCH) -mcpu=$(MCPU) -marm $(INC_DIRS) $(CFLAGS) -o $@ $<
	
build/%.o: %.c
	mkdir -p $(shell dirname $@)
	$(CC) -march=$(ARCH) -mcpu=$(MCPU) -marm $(INC_DIRS) $(CFLAGS) -o $@ $<

 

빌드 후 실행해보면 아래와 같은 출력이 반복된다.

User Task #0 SP=0x8ffff0
User Task #1 SP=0x9ffff0
User Task #2 SP=0xaffff0
반복...

 

메모리 설계대로 동작함을 확인할 수 있다.


🚧 중간 Remark 2

8~10장을 통해 "2-1부: 태스크, 스케줄러, 컨텍스트 스위칭을 만들어서 RTOS다운 기능 만들기"를 진행했다


11~13장을 통해 "2-2부: 메시지와 동기화 기능으로 멀티코어 환경에 필수적인 기능 만들기"를 진행하자


11장 이벤트

  • "이벤트"가 쓰이는 대표적 분야 : GUI 프로그래밍
  • 어디선가 이벤트가 발생하고, 태스크는 이벤트를 받아서 해당 이벤트에 맞는일을 하는것
    • 예시
      • 어떤 임베디드 시스템에 있는 버튼을 사용자가 누른다
      • 시스템 내부적으로는 버튼에 연결된 스위치에서 전기가 연결된다
      • 컨트롤러는 해당 전기 신호를 인식한다
      • 그러면 이제부터 물리적 전기 신호를 소프트웨어적인 인터럽트로 처리한다
        • ARM이라면 IRQ나 FIQ가 발생하는 것
      • 앞장에서 설명한대로 IRQ나 FIQ의 핸들러에서 인터럽트 컨트롤러의 레지스터를 읽어서 어떤 인터럽트인지 확인한다
      • 그리고 해당 인터럽트의 핸들러를 호출하면 인터럽트 핸들러에서 버튼이 눌렸을때의 처리를 한다
  • 지금은 RTOS 커널이 태스크를 관리하고 있으므로, 
    • 좀 더 멋있고 유연하게 동작하려면 인터럽트 핸들러의 구체적 기능을 태스크로 옮기는것이 낫다
    • 그렇다면 인터럽트와 태스크 간의 연결 매체가 필요한데, 이때 사용하는 것이 이벤트이다
    • GUI 버튼 클릭을 예로 들어보자
      • 윈도우에서 버튼은 마우스로 클릭하는 것을 인터럽트의 발생이라 생각할 수 있다
      • 윈도우의 닷넷프레임워크나 리눅스의 X-윈도우 시스템이 버튼 클릭을 이벤트로 바꿔서, 해당 프로그램의 핸들러 함수로 전달한다
      • 이 역할을 RTOS 커널이 하는것
      • 그리고 개별 프로그램의 버튼 클릭 이벤트 함수가, 태스크에 있는 이벤트 핸들러 함수와 같은 역할을 한다

이벤트 플래그

이벤트는 개발자가 정한 어떤 값으로 전달된다. 

개발자가 처리할 수 있는 어떤 형태로든 이벤트를 만들 수 있다. 

 

저자는 비트맵으로 처리하는 것을 선호한다고 한다. 비트 위치마다 독립된 이벤트를 할당해서, 이벤트가 있다 없다를 표시하는 것이다.

책에서 캡쳐

위 그림을 2진수로 표현하면 10000000 00000000 00000000 00000010 이다. 이를 16진수로 표현하면 0x80000002 이다. 비트맵 표현이므로 0x80000000 이벤트와 0x00000002 이벤트가 발생해서 처리 대기중(pending)인 것을 의미한다.

 

이것을 처리하는 커널의 기능을 만들어보자.

 

kernel/event.h

#ifndef KERNEL_EVENT_H_
#define KERNEL_EVENT_H_

typedef enum KernelEventFlag_t
{
    KernelEventFlag_UartIn      = 0x00000001,
    KernelEventFlag_Reserved01  = 0x00000002,
    KernelEventFlag_Reserved02  = 0x00000004,
    KernelEventFlag_Reserved03  = 0x00000008,
    KernelEventFlag_Reserved04  = 0x00000010,
    KernelEventFlag_Reserved05  = 0x00000020,
    KernelEventFlag_Reserved06  = 0x00000040,
    KernelEventFlag_Reserved07  = 0x00000080,
    KernelEventFlag_Reserved08  = 0x00000100,
    KernelEventFlag_Reserved09  = 0x00000200,
    KernelEventFlag_Reserved10  = 0x00000400,
    KernelEventFlag_Reserved11  = 0x00000800,
    KernelEventFlag_Reserved12  = 0x00001000,
    KernelEventFlag_Reserved13  = 0x00002000,
    KernelEventFlag_Reserved14  = 0x00004000,
    KernelEventFlag_Reserved15  = 0x00008000,
    KernelEventFlag_Reserved16  = 0x00010000,
    KernelEventFlag_Reserved17  = 0x00020000,
    KernelEventFlag_Reserved18  = 0x00040000,
    KernelEventFlag_Reserved19  = 0x00080000,
    KernelEventFlag_Reserved20  = 0x00100000,
    KernelEventFlag_Reserved21  = 0x00200000,
    KernelEventFlag_Reserved22  = 0x00400000,
    KernelEventFlag_Reserved23  = 0x00800000,
    KernelEventFlag_Reserved24  = 0x01000000,
    KernelEventFlag_Reserved25  = 0x02000000,
    KernelEventFlag_Reserved26  = 0x04000000,
    KernelEventFlag_Reserved27  = 0x08000000,
    KernelEventFlag_Reserved28  = 0x10000000,
    KernelEventFlag_Reserved29  = 0x20000000,
    KernelEventFlag_Reserved30  = 0x40000000,
    KernelEventFlag_Reserved31  = 0x80000000,

    KernelEventFlag_Empty       = 0x00000000,
} KernelEventFlag_t;

void Kernel_event_flag_init(void);
void Kernel_event_flag_set(KernelEventFlag_t event);
void Kernel_event_flag_clear(KernelEventFlag_t event);
bool Kernel_event_flag_check(KernelEventFlag_t event);

#endif /* KERNEL_EVENT_H_ */

이벤트가 제대로 동작하는지 확인할 때 UART를 사용할 것이므로, UartIn 이벤트만 우선 선언해두었다.

 

kernel/event.c

#include "stdint.h"
#include "stdbool.h"

#include "stdio.h"
#include "event.h"

static uint32_t sEventFlag;

void Kernel_event_flag_init(void)
{
    sEventFlag = 0;
}

void Kernel_event_flag_set(KernelEventFlag_t event)
{
    sEventFlag |= (uint32_t)event;
}

void Kernel_event_flag_clear(KernelEventFlag_t event)
{
    sEventFlag &= ~((uint32_t)event);
}

bool Kernel_event_flag_check(KernelEventFlag_t event)
{
    if (sEventFlag & (uint32_t)event)
    {
        Kernel_event_flag_clear(event);
        return true;
    }
    return false;
}

 

태스크 관련 함수처럼 태스크에서는 커널 API를 통해서 이벤트를 처리하게 하자. 

 

kernel/Kernel.h

#ifndef KERNEL_KERNEL_H_
#define KERNEL_KERNEL_H_

#include "task.h"
+#include "event.h"

void              Kernel_start(void);
void              Kernel_yield(void);
+void              Kernel_send_events(uint32_t event_list);
+KernelEventFlag_t Kernel_wait_events(uint32_t waiting_list);

#endif /* KERNEL_KERNEL_H_ */

kernel/Kernel.c

#include "stdint.h"
#include "stdbool.h"

+#include "memio.h"
#include "Kernel.h"


void Kernel_start(void)
{
	Kernel_task_start();
}

void Kernel_yield(void)
{
	Kernel_task_scheduler();
}

+void Kernel_send_events(uint32_t event_list)
+{
+	for (uint32_t i = 0 ; i < 32 ; i++)
+	{
+		if ((event_list >> i) & 1)
+		{
+			KernelEventFlag_t sending_event = KernelEventFlag_Empty;
+			sending_event = (KernelEventFlag_t)SET_BIT(sending_event, i);
+			Kernel_event_flag_set(sending_event);
+		}
+	}
+}

+KernelEventFlag_t Kernel_wait_events(uint32_t waiting_list)
+{
+	for (uint32_t i = 0 ; i < 32 ; i++)
+	{
+		if ((waiting_list >> i) & 1)
+		{
+			KernelEventFlag_t waiting_event = KernelEventFlag_Empty;
+			waiting_event = (KernelEventFlag_t)SET_BIT(waiting_event, i);
+
+			if (Kernel_event_flag_check(waiting_event))
+			{
+				return waiting_event;
+			}
+		}
+	}
+
+	return KernelEventFlag_Empty;
+}

Kernel_send_events() 함수와, Kernel_wait_events() 함수의 파라미터가 KernelEventFlag_t가 아닌 uint32이다. 비트맵을 이용하기 위함이다. event가 여러개 있다하면 클라이언트코드로 다음과 같이 작성할 수 있다.

Kernel_send_events(event1|event2|event3)

또한 이벤트를 받아서 처리하는 핸들러 측에서도 자신이 기다리는 이벤트의 전체목록을 Kernel_wait_events()에 한번에 보내서 처리할 수 있다. 이벤트를 보내는 쪽과 받는 쪽이 직접적 관계가 없으므로 여러 태스크에서 나눠서 처리할수도 있다

//Task#1
Kernel_wait_events(event1|event3)

//Task#2
Kernel_wait_events(event2)

인터럽트와 이벤트

  • 이벤트는 인터럽트와 엮어서 사용하는 것이 일반적이다
  • QEMU라는 에뮬레이팅 환경의 제약때문에 이용할 수 있는 인터럽트가 별로 없다
  • 이 책의 범주에서 사용가능한 인터럽트는 UART와 타이머 뿐
    • 타이머는 너무 자주 발생하므로, 사실상 사용가능한 이벤트는 UART뿐이다
  • 현재까지의 UART관련 구현은
    • UART 인터럽트 핸들러에서, UART 입력 인터럽트가 발생하면, UART 하드웨어에서 입력된 글자를 받아서, 다시 그대로 UART로 출력하는 일
  • 이 기능을 태스크의 이벤트 핸들러로 옮길것이다
    • 지금은 중간단계이고, 이벤트부터 발생시켜서 태스크의 이벤트 핸들러가 동작하는 것을 먼저 확인해보자

인터럽트 핸들러를 수정하자.

hal/rvpb/Uart.c

+#include "stdbool.h"

+#include "Kernel.h"

// ...중략

static void interrupt_handler(void)
{
    uint8_t ch = Hal_uart_get_char();
    Hal_uart_put_char(ch);

+    Kernel_send_events(KernelEventFlag_UartIn);
}

인터럽트와 이벤트의 연결을 했다. 

 

테스트를 해보자. 태스크에서 이벤트를 받아서 처리하는 코드를 넣자.

Main.c

// ...중략

static void Kernel_init(void)
{
	uint32_t taskId;

	Kernel_task_init();
+	Kernel_event_flag_init();

	taskId = Kernel_task_create(User_task0);
	if (NOT_ENOUGH_TASK_NUM == taskId)
	{
		putstr("Task0 creation fail\n");
	}

	taskId = Kernel_task_create(User_task1);
	if (NOT_ENOUGH_TASK_NUM == taskId)
	{
		putstr("Task1 creation fail\n");
	}

	taskId = Kernel_task_create(User_task2);
	if (NOT_ENOUGH_TASK_NUM == taskId)
	{
		putstr("Task2 creation fail\n");
	}

	Kernel_start();
}

// ...중략

+void User_task0(void)
+{
+	uint32_t local = 0;
+
+	debug_printf("User Task #0 SP=0x%x\n", &local);
+
+	while(true)
+	{
+		KernelEventFlag_t handle_event = Kernel_wait_events(KernelEventFlag_UartIn);
+		switch(handle_event)
+		{
+		case KernelEventFlag_UartIn:
+			debug_printf("\nEvent handled\n");
+			break;
+		}
+		Kernel_yield();
+	}
+}

 

이전 테스트때 태스크들 속에 스택 포인터의 값을 출력하는 코드를 무한루프 속에 넣었다. 한번만 출력되게 바꾸었다. 다른 태스크들도 수정하자

void User_task1(void)
{
    uint32_t local = 0;

+   debug_printf("User Task #1 SP=0x%x\n", &local);

    while(true)
    {
-        debug_printf("User Task #1 SP=0x%x\n", &local);
        Kernel_yield();
    }
}

void User_task2(void)
{
    uint32_t local = 0;

+   debug_printf("User Task #2 SP=0x%x\n", &local);

    while(true)
    {
-   	debug_printf("User Task #2 SP=0x%x\n", &local);
        Kernel_yield();
    }
}

 

빌드 후 실행하고, a, b, c, d를 순서대로 눌렀다

root@21d947b2e449:/project# make run
qemu-system-arm -M realview-pb-a8 -kernel build/navilos.axf -nographic
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5220:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2642:(snd_pcm_open_noupdate) Unknown PCM default
alsa: Could not initialize DAC
alsa: Failed to open `default':
alsa: Reason: No such file or directory
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5220:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2642:(snd_pcm_open_noupdate) Unknown PCM default
alsa: Could not initialize DAC
alsa: Failed to open `default':
alsa: Reason: No such file or directory
audio: Failed to create voice `lm4549.out'
NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN
Hello World!
Hello printf
output string pointer: printf pointer test
(null) is null pointer, 10 number
5 = 5
dec=255 hex=ff
print zero 0
SYSCTRL0 0
current count : 0
current count : 1000
current count : 2000
current count : 3000
current count : 4000
User Task #0 SP=0x8fffec
User Task #1 SP=0x9ffff0
User Task #2 SP=0xaffff0
a
Event handled
b
Event handled
c
Event handled
d
Event handled

기대하던 값인 "Event handled" 메시지가 출력되었다.

  • 키보드에서 누른 글자가 그대로 출력된 것은 인터럽트 핸들러가 한일이고
  • "Event handled" 메시지는 이벤트 핸들러에서 한 일이다

사용자 정의 이벤트

  • 이벤트는 꼭 인터럽트와 연관지어서 사용해야할까?
    • 당연히 아니다
    • 필요하다 생각되면, 사용하지 않는 이벤트 플래그 하나에 이름을 주어서 태스크에서 태스크로 이벤트를 보낼수도 있다
      • 이러한 특징이 이벤트와 인터럽트의 차이다
      • 인터럽트 핸들러에서 인터럽트의 발생소식을 태스크로 전달하기 위해 이벤트를 이용하는것이지, 이벤트가 반드시 인터럽트와 연결되어야하는것은 아니다

UART입력에 KernelEventFlag_UartIn 이벤트를 연결했었다. 그리고 Task0에서 해당 이벤트를 받았다는 것을 알려주는 출력을 하여 이벤트를 처리했다.

 

이번에는 사용하지 않는 이벤트 플래그 하나에 이름을 붙여주고, 이 이벤트 플래그를 Task0에서 보내겠다. 그리고 Task1에서 이벤트 플래그를 받아보자.

 

KernelEventFlag_t를 수정하자.

kernel/event.h

// ...중략

typedef enum KernelEventFlag_t
{
    KernelEventFlag_UartIn      = 0x00000001,
-   KernelEventFlag_Reserved01  = 0x00000002,
+	KernelEventFlag_CmdIn	    = 0x00000002,
    KernelEventFlag_Reserved02  = 0x00000004,
    KernelEventFlag_Reserved03  = 0x00000008,
    KernelEventFlag_Reserved04  = 0x00000010,
    KernelEventFlag_Reserved05  = 0x00000020,
    KernelEventFlag_Reserved06  = 0x00000040,
    KernelEventFlag_Reserved07  = 0x00000080,
    KernelEventFlag_Reserved08  = 0x00000100,
    KernelEventFlag_Reserved09  = 0x00000200,
    KernelEventFlag_Reserved10  = 0x00000400,
    KernelEventFlag_Reserved11  = 0x00000800,
    KernelEventFlag_Reserved12  = 0x00001000,
    KernelEventFlag_Reserved13  = 0x00002000,
    KernelEventFlag_Reserved14  = 0x00004000,
    KernelEventFlag_Reserved15  = 0x00008000,
    KernelEventFlag_Reserved16  = 0x00010000,
    KernelEventFlag_Reserved17  = 0x00020000,
    KernelEventFlag_Reserved18  = 0x00040000,
    KernelEventFlag_Reserved19  = 0x00080000,
    KernelEventFlag_Reserved20  = 0x00100000,
    KernelEventFlag_Reserved21  = 0x00200000,
    KernelEventFlag_Reserved22  = 0x00400000,
    KernelEventFlag_Reserved23  = 0x00800000,
    KernelEventFlag_Reserved24  = 0x01000000,
    KernelEventFlag_Reserved25  = 0x02000000,
    KernelEventFlag_Reserved26  = 0x04000000,
    KernelEventFlag_Reserved27  = 0x08000000,
    KernelEventFlag_Reserved28  = 0x10000000,
    KernelEventFlag_Reserved29  = 0x20000000,
    KernelEventFlag_Reserved30  = 0x40000000,
    KernelEventFlag_Reserved31  = 0x80000000,

    KernelEventFlag_Empty       = 0x00000000,
} KernelEventFlag_t;

// ...중략

 

태스크도 수정해주자.

Main.c 

void User_task0(void)
{
	uint32_t local = 0;

	debug_printf("User Task #0 SP=0x%x\n", &local);

	while(true)
	{
		KernelEventFlag_t handle_event = Kernel_wait_events(KernelEventFlag_UartIn);
		switch(handle_event)
		{
		case KernelEventFlag_UartIn:
-			debug_printf("\nEvent handled\n");
+			debug_printf("\nEvent handled by Task0\n");
+			Kernel_send_events(KernelEventFlag_CmdIn);
			break;
		}
		Kernel_yield();
	}
}

void User_task1(void)
{
	uint32_t local = 0;

	debug_printf("User Task #1 SP=0x%x\n", &local);

	while(true)
	{
+		KernelEventFlag_t handle_event = Kernel_wait_events(KernelEventFlag_CmdIn);
+		switch(handle_event)
+		{
+		case KernelEventFlag_CmdIn:
+			debug_printf("\nEvent handled by Task1\n");
+			break;
+		}
		Kernel_yield();
	}
}

 

빌드하고 실행해서 테스트 해보자.

root@21d947b2e449:/project# make run
qemu-system-arm -M realview-pb-a8 -kernel build/navilos.axf -nographic
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5220:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2642:(snd_pcm_open_noupdate) Unknown PCM default
alsa: Could not initialize DAC
alsa: Failed to open `default':
alsa: Reason: No such file or directory
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5220:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2642:(snd_pcm_open_noupdate) Unknown PCM default
alsa: Could not initialize DAC
alsa: Failed to open `default':
alsa: Reason: No such file or directory
audio: Failed to create voice `lm4549.out'
NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN
Hello World!
Hello printf
output string pointer: printf pointer test
(null) is null pointer, 10 number
5 = 5
dec=255 hex=ff
print zero 0
SYSCTRL0 0
current count : 0
current count : 1000
current count : 2000
current count : 3000
current count : 4000
User Task #0 SP=0x8fffec
User Task #1 SP=0x9fffec
User Task #2 SP=0xaffff0
a
Event handled by Task0

Event handled by Task1
b
Event handled by Task0

Event handled by Task1
c
Event handled by Task0

Event handled by Task1
d
Event handled by Task0

Event handled by Task1

 

여러 이벤트 플래그를 동시에 보내고 처리하기

  • 이벤트 플래그 설계시, 비트맵을 사용한 가장 큰 이유 == 이벤트 플래그를 동시에 여러개 보내고 받을 수 있게끔 코딩할 수 있게 하기위해서이다

현재 이벤트 플래그를 두개 사용하고 있고, 하나를 더 추가해보자

 

kernel/event.h

// ...중략

typedef enum KernelEventFlag_t
{
    KernelEventFlag_UartIn      = 0x00000001,
    KernelEventFlag_CmdIn       = 0x00000002,
-   KernelEventFlag_Reserved02  = 0x00000004,
+   KernelEventFlag_CmdOut      = 0x00000004,
    KernelEventFlag_Reserved03  = 0x00000008,
    KernelEventFlag_Reserved04  = 0x00000010,
    KernelEventFlag_Reserved05  = 0x00000020,
    KernelEventFlag_Reserved06  = 0x00000040,
    KernelEventFlag_Reserved07  = 0x00000080,
    KernelEventFlag_Reserved08  = 0x00000100,
    KernelEventFlag_Reserved09  = 0x00000200,
    KernelEventFlag_Reserved10  = 0x00000400,
    KernelEventFlag_Reserved11  = 0x00000800,
    KernelEventFlag_Reserved12  = 0x00001000,
    KernelEventFlag_Reserved13  = 0x00002000,
    KernelEventFlag_Reserved14  = 0x00004000,
    KernelEventFlag_Reserved15  = 0x00008000,
    KernelEventFlag_Reserved16  = 0x00010000,
    KernelEventFlag_Reserved17  = 0x00020000,
    KernelEventFlag_Reserved18  = 0x00040000,
    KernelEventFlag_Reserved19  = 0x00080000,
    KernelEventFlag_Reserved20  = 0x00100000,
    KernelEventFlag_Reserved21  = 0x00200000,
    KernelEventFlag_Reserved22  = 0x00400000,
    KernelEventFlag_Reserved23  = 0x00800000,
    KernelEventFlag_Reserved24  = 0x01000000,
    KernelEventFlag_Reserved25  = 0x02000000,
    KernelEventFlag_Reserved26  = 0x04000000,
    KernelEventFlag_Reserved27  = 0x08000000,
    KernelEventFlag_Reserved28  = 0x10000000,
    KernelEventFlag_Reserved29  = 0x20000000,
    KernelEventFlag_Reserved30  = 0x40000000,
    KernelEventFlag_Reserved31  = 0x80000000,

    KernelEventFlag_Empty       = 0x00000000,
} KernelEventFlag_t;

// ...중략

 

인터럽트 핸들러를 수정해서, 동시에 이벤트 여러개를 보내자.

 

hal/rvpb/Uart.c

// ...중략

static void interrupt_handler(void)
{
    uint8_t ch = Hal_uart_get_char();
    Hal_uart_put_char(ch);

-   Kernel_send_events(KernelEventFlag_UartIn);
+   Kernel_send_events(KernelEventFlag_UartIn|KernelEventFlag_CmdIn);
+
+   if (ch == 'X')
+   {
+       Kernel_send_events(KernelEventFlag_CmdOut);
+   }
}

// ...중략

 

테스트를 위해 Task0의 동작을 수정하자.

void User_task0(void)
{
    uint32_t local = 0;

    debug_printf("User Task #0 SP=0x%x\n", &local);

    while(true)
    {
-       KernelEventFlag_t handle_event = Kernel_wait_events(KernelEventFlag_UartIn);
+       KernelEventFlag_t handle_event = Kernel_wait_events(KernelEventFlag_UartIn|KernelEventFlag_CmdOut);

        switch(handle_event)
        {
        case KernelEventFlag_UartIn:
        debug_printf("\nEvent handled by Task0\n");
-       Kernel_send_events(KernelEventFlag_CmdIn);
            break;
+       case KernelEventFlag_CmdOut:
+           debug_printf("\nCmdOut Event by Task0\n");
+           break;            
        }
        Kernel_yield();
    }
}

Task1의 동작은 다시 상기하자.

void User_task1(void)
{
    uint32_t local = 0;

    debug_printf("User Task #1 SP=0x%x\n", &local);

    while(true)
    {
        KernelEventFlag_t handle_event = Kernel_wait_events(KernelEventFlag_CmdIn);
        switch(handle_event)
        {
        case KernelEventFlag_CmdIn:
            debug_printf("\nEvent handled by Task1\n");
            break;
        }
        Kernel_yield();
    }
}

 

두 태스크의 역할을 정리해보면, Task0은 KernelEventFlag_UartIn, KernelEventFlag_CmdOut 이벤트를 기다렸다가 처리하는 것이고, Task1은 KernelEventFlag_CmdIn 이벤트를 기다렸다가 처리하는 것이다.

 

빌드하고 실행해보자.

root@21d947b2e449:/project# make run
qemu-system-arm -M realview-pb-a8 -kernel build/navilos.axf -nographic
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5220:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2642:(snd_pcm_open_noupdate) Unknown PCM default
alsa: Could not initialize DAC
alsa: Failed to open `default':
alsa: Reason: No such file or directory
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5220:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2642:(snd_pcm_open_noupdate) Unknown PCM default
alsa: Could not initialize DAC
alsa: Failed to open `default':
alsa: Reason: No such file or directory
audio: Failed to create voice `lm4549.out'
NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN
Hello World!
Hello printf
output string pointer: printf pointer test
(null) is null pointer, 10 number
5 = 5
dec=255 hex=ff
print zero 0
SYSCTRL0 0
current count : 0
current count : 1000
current count : 2000
current count : 3000
current count : 4000
User Task #0 SP=0x8fffec
User Task #1 SP=0x9fffec
User Task #2 SP=0xaffff0
a
Event handled by Task0

Event handled by Task1
b
Event handled by Task0

Event handled by Task1
x
Event handled by Task0

Event handled by Task1
X
Event handled by Task1

Event handled by Task0

CmdOut Event by Task0

a, b를 입력하면  UART 핸들러는 KernelEventFlag_UartIn 이벤트와 KernelEventFlag_CmdIn 이벤트를 동시에 보낸다. 동시에 보낸 두 이벤트를 Task0과 Task1이 각각 받아서 Task0에서는 "Event handled by Task0" 메시지를, Task1에서는 "Event handled by Task1" 메시지를 출력한다.

'X' 를 입력하면 UART 핸들러는 KernelEventFlag_UartIn 이벤트와 KernelEventFlag_CmdIn 이벤트 그리고 새로 추가한 KernelEventFlag_CmdOut 이벤트를 모두 보낸다. 이 중 KernelEventFlag_UartIn, KernelEventFlag_CmdOut 이벤트는 Task0에서 처리하고, KernelEventFlag_CmdIn 이벤트는 Task1에서 처리한다.

 

여기서 생각해볼 점이 있다.

태스크당 하나의 이벤트를 처리하고 다음 스케줄링을 하는 것이다.

만약 해당 태스크에서 대기중인 이벤트가 모두 처리되고 스케줄링되게 하려면 코드 수정이 필요하다. 이는 요구사항에 따라 결정하면된다.

12장 메시징

  • 이제 (인터럽트 핸들러 -> 태스크) 혹은 (태스크 -> 태스크) 이벤트를 보낼 수 있다
  • 하지만 충분치 않다
    • 이벤트 이상의 더 많은 정보를 보낼수 없기 떄문이다
    • 예를 들어
      • 앞장에서 UART로 문자를 입력했다는 이벤트를 받았으나, 어떤문자인지는 아직 모른다
      • 문자 자체를 전달할 방법을 만들어야 한다
  • 그래서 메시징 기능을 만들자
    • 메시징: 임의의 데이터를 메시지라는 이름으로 전송하는 기능
      • 해당 기능의 구현을 위한 요소들?
      • 이벤트 : 일종의 벨 (구체적 내용은 없음; 약속된 상황만 가능)
      • 메시징 : 사서함 (도착여부는 알수없음)
      • 그렇다면!
        • 사서함에 편지가 도착할때마다, 벨을 울려준다면?!

 

  • 인터럽트 핸들러에서 이벤트 외에 별도 데이터를 더 보내고싶으면
    • 메시징 기능을 이용해서 데이터를 보내고 이벤트를 보낸다
    • 그러고나면 태스크에서 이벤트를 처리할 때 이벤트 핸들러에서 메시지를 읽어와서 처리하는 것이다

메시징 기능또한 kernel 디렉토리에 작성하자. (kernel/msg.h, kernel/msg.c)

메시지 큐

  • 메시지를 어떻게 관리할 것인가?
    • 여기서는 큐(Queue)로 관리로 결정
    • 가장 일반적이고, 효율적임
    • 메시지를 큐로 관리한다고하여, 메시지 큐라 부름
  • 어떤 구현을 할것인가?
    • 큐의 여러 구현방법이 존재하지만, 
    • 여기서는 링 형태

kernel/msg.h

#ifndef KERNEL_MSG_H_
#define KERNEL_MSG_H_

#define MSG_Q_SIZE_BYTE     512

typedef enum KernelMsgQ_t
{
    KernelMsgQ_Task0,
    KernelMsgQ_Task1,
    KernelMsgQ_Task2,

    KernelMsgQ_Num
} KernelMsgQ_t;

typedef struct KernelCirQ_t
{
    uint32_t front;
    uint32_t rear;
    uint8_t  Queue[MSG_Q_SIZE_BYTE];
} KernelCirQ_t;

void Kernel_msgQ_init(void);
bool Kernel_msgQ_is_empty(KernelMsgQ_t Qname);
bool Kernel_msgQ_is_full(KernelMsgQ_t Qname);
bool Kernel_msgQ_enqueue(KernelMsgQ_t Qname, uint8_t data);
bool Kernel_msgQ_dequeue(KernelMsgQ_t Qname, uint8_t* out_data);

#endif /* KERNEL_MSG_H_ */

 

kernel/msg.c

#include "stdint.h"
#include "stdbool.h"
#include "stdlib.h"

#include "msg.h"

KernelCirQ_t sMsgQ[KernelMsgQ_Num];

void Kernel_msgQ_init(void)
{
    for (uint32_t i = 0 ; i < KernelMsgQ_Num ; i++)
    {
        sMsgQ[i].front = 0;
        sMsgQ[i].rear = 0;
        memclr(sMsgQ[i].Queue, MSG_Q_SIZE_BYTE);
    }
}

bool Kernel_msgQ_is_empty(KernelMsgQ_t Qname)
{
    if (Qname >= KernelMsgQ_Num)
    {
        return false;
    }

    if (sMsgQ[Qname].front == sMsgQ[Qname].rear)
    {
        return true;
    }

    return false;
}

bool Kernel_msgQ_is_full(KernelMsgQ_t Qname)
{
    if (Qname >= KernelMsgQ_Num)
    {
        return false;
    }

    if (((sMsgQ[Qname].rear + 1) % MSG_Q_SIZE_BYTE) == sMsgQ[Qname].front)
    {
        return true;
    }

    return false;
}

bool Kernel_msgQ_enqueue(KernelMsgQ_t Qname, uint8_t data)
{
    if (Qname >= KernelMsgQ_Num)
    {
        return false;
    }

    if (Kernel_msgQ_is_full(Qname))
    {
        return false;
    }
    sMsgQ[Qname].rear++;
    sMsgQ[Qname].rear %= MSG_Q_SIZE_BYTE;

    uint32_t idx = sMsgQ[Qname].rear;
    sMsgQ[Qname].Queue[idx] = data;

    return true;
}

bool Kernel_msgQ_dequeue(KernelMsgQ_t Qname, uint8_t* out_data)
{
    if (Qname >= KernelMsgQ_Num)
    {
        return false;
    }

    if (Kernel_msgQ_is_empty(Qname))
    {
        return false;
    }

    sMsgQ[Qname].front++;
    sMsgQ[Qname].front %= MSG_Q_SIZE_BYTE;

    uint32_t idx = sMsgQ[Qname].front;
    *out_data = sMsgQ[Qname].Queue[idx];

    return true;
}

 

lib/stdlib.h

#ifndef LIB_STDLIB_H_
#define LIB_STDLIB_H_

void delay(uint32_t ms);
+void memclr(void* dst, uint32_t count);

#endif /* LIB_STDLIB_H_ */

lib/stdlib.c

#include "stdint.h"
#include "stdbool.h"
#include "HalTimer.h"

void delay(uint32_t ms)
{
    uint32_t goal = Hal_timer_get_1ms_counter() + ms;

    while(goal != Hal_timer_get_1ms_counter());
}

+void memclr(void* dst, uint32_t count)
+{
+	uint8_t* d = (uint8_t*)dst;
+
+	while(count--)
+	{
+		*d++ = 0;
+	}
+}

 

메시징 기능을 커널 API로 제공하자.

kernel/Kernel.h

#ifndef KERNEL_KERNEL_H_
#define KERNEL_KERNEL_H_

#include "task.h"
#include "event.h"
+#include "msg.h"

void              Kernel_start(void);
void              Kernel_yield(void);
void              Kernel_send_events(uint32_t event_list);
KernelEventFlag_t Kernel_wait_events(uint32_t waiting_list);
+bool             Kernel_send_msg(KernelMsgQ_t Qname, void* data, uint32_t count);
+uint32_t         Kernel_recv_msg(KernelMsgQ_t Qname, void* out_data, uint32_t count);

#endif /* KERNEL_KERNEL_H_ */

 

kernel/Kernel.c

// ...중략

+bool Kernel_send_msg(KernelMsgQ_t Qname, void* data, uint32_t count)
+{
+    uint8_t* d = (uint8_t*)data;

+    for (uint32_t i = 0 ; i < count ; i++)
+    {
+        if (false == Kernel_msgQ_enqueue(Qname, *d))
+        {
+            for (uint32_t j = 0 ; j < i ; j++)
+            {
+                uint8_t rollback;
+                Kernel_msgQ_dequeue(Qname, &rollback);
+            }
+            return false;
+        }
+        d++;
+    }

+    return true;
+}

+uint32_t Kernel_recv_msg(KernelMsgQ_t Qname, void* out_data, uint32_t count)
+{
+    uint8_t* d = (uint8_t*)out_data;
+
+    for (uint32_t i = 0 ; i < count ; i++)
+    {
+        if (false == Kernel_msgQ_dequeue(Qname, d))
+        {
+            return i;
+        }
+        d++;
+    }
+
+    return count;
+}

 

메시지 큐의 enqueue 함수와 dequeue 함수는 1바잍트 크기의 데이터만 처리할 수 있도록 만든 것에 비해, 커널 API는 count크기만큼의 데이터를 한번에 처리할 수 있도록 작성했다.

그러다보니 데이터를 넣거나 빼는 도중에 count 크기만큼 처리를 다 마치지 못했음에도 메시지 큐가 꽉 차거나 비어버리는 상황이 생길 수 있다. 

  • 9~16번째 줄: 데이터를 메시지 큐에 넣는 도중에 큐가 꽉 차버리는 상황에 대한 에러를 처리하는 코드
    • Kernel_msgQ_enqueue 함수가 false 리턴하면 큐가 꽉 찼고 일부는 들어가지 못했다는 의미이다.
    • 이때 큐에 들어간 데이터는 불완전한 데이터이다.
    • 따라서 다시 빼내는 작업을 해야 무결성을 보장하는 것이다. 내부에 for 반복문이 그 역할을 하고 있다.
  • 30~33번째 줄: 데이터를 메시지 큐에서 읽는 도중에 메시지 큐에 더 읽을 것이 없는 경우 에러를 처리하는 코드
    • 예를 들어보자
      • 10바이트를 읽으리라 기대하고 count에 10을 넣어 호출했는데 7바이트를 읽고 메시지 큐가 비어버린 것이다. 
      • 이때는 현재까지 읽은 바이트 수만 리턴하고 호출하는 쪽에서 알아서 처리하도록 한다.

boot/Main.c

static void Kernel_init(void)
{
	uint32_t taskId;

	Kernel_task_init();
	Kernel_event_flag_init();
+	Kernel_msgQ_init();

	taskId = Kernel_task_create(User_task0);
	if (NOT_ENOUGH_TASK_NUM == taskId)
	{
		putstr("Task0 creation fail\n");
	}

	taskId = Kernel_task_create(User_task1);
	if (NOT_ENOUGH_TASK_NUM == taskId)
	{
		putstr("Task1 creation fail\n");
	}

	taskId = Kernel_task_create(User_task2);
	if (NOT_ENOUGH_TASK_NUM == taskId)
	{
		putstr("Task2 creation fail\n");
	}

	Kernel_start();
}

태스크 간 데이터 전달

예제를 만들어보자.

인터럽트 핸들러를 수정해서 키보드 입력을 메시지로 보내고, 입력이 들어왔다는 소식을 이벤트로 보낸다.

 

hal/rvpb/Uart.c

// ...중략

static void interrupt_handler(void)
{
	uint8_t ch = Hal_uart_get_char();
	Hal_uart_put_char(ch);

-   Kernel_send_events(KernelEventFlag_UartIn|KernelEventFlag_CmdIn);
-
-   if (ch == 'X')
-   {
-       Kernel_send_events(KernelEventFlag_CmdOut);
-   }
+   Kernel_send_msg(KernelMsgQ_Task0, &ch, 1);
+   Kernel_send_events(KernelEventFlag_UartIn);
}

Task0용으로 만든 메시지 큐에 UART 입력으로 받은 값을 전달한다. 그리고 바로 UartIn 이벤트를 보낸다. 이러면 Task0은 UartIn이벤트를 기다리고 있다가 이벤트가 발생하면 메시지 큐에서 1바이트 값을 읽는다.

 

boot/Main.c

void User_task0(void)
{
	uint32_t local = 0;
	debug_printf("User Task #0 SP=0x%x\n", &local);

+	uint8_t  cmdBuf[16];
+	uint32_t cmdBufIdx = 0;
+	uint8_t  uartch = 0;

	while(true)
	{
		KernelEventFlag_t handle_event = Kernel_wait_events(KernelEventFlag_UartIn|KernelEventFlag_CmdOut);
		switch(handle_event)
		{
		case KernelEventFlag_UartIn:
-			debug_printf("\nEvent handled by Task0\n");
+			Kernel_recv_msg(KernelMsgQ_Task0, &uartch, 1);
+			if (uartch == '\r')
+			{
+				cmdBuf[cmdBufIdx] = '\0';
+
+				Kernel_send_msg(KernelMsgQ_Task1, &cmdBufIdx, 1);
+				Kernel_send_msg(KernelMsgQ_Task1, cmdBuf, cmdBufIdx);
+				Kernel_send_events(KernelEventFlag_CmdIn);
+
+				cmdBufIdx = 0;
+			}
+			else
+			{
+				cmdBuf[cmdBufIdx] = uartch;
+				cmdBufIdx++;
+				cmdBufIdx %= 16;
+			}
			break;
		case KernelEventFlag_CmdOut:
			debug_printf("\nCmdOut Event by Task0\n");
			break;
		}
		Kernel_yield();
	}
}

 

UartIn 이벤트 핸들링 코드를 보자.

엔터값인지 확인하는 탈출조건을 먼저 두었다. 

아니라면, uartch로 받은 UART 입력값을 cmdBuf라는 16바이트짜리 로컬 배열에 순서대로 쌓아놓는 일을 한다. 오버플로를 방지하는 모듈러 연산을 작성했다. 

그러다가 UART를 통해서 엔터값이 들어오면, 문자열을 완성시키고 문자열 길이를 Task1로 메시지를 통해 보낸다. 당연히 Task1에서 처리 과정에 대한 약속 하에 작성되어야한다. 메시지 길이를 먼저 보내고, 그 이후 메시지 데이터를 보내는 것이다. 그리고 CmdIn 이벤트를 보낸다. CmdIn 이벤트는 Task1이 받아서 처리하는 것을 기대하고 있다.

cf) 메시지를 보내는 것과 이벤트를 보내는 코드에 대해서 잠재적 버그가 있다. 에러 처리가 없다는 것이다. 

 

Task0과 Task1 간의 데이터 처리 약속이 필요하다 했으니, Task1의 이벤트 핸들러 코드도 작성하자.

boot/Main.c

void User_task1(void)
{
    uint32_t local = 0;

    debug_printf("User Task #1 SP=0x%x\n", &local);

+   uint8_t cmdlen = 0;
+   uint8_t cmd[16] = {0};

    while(true)
    {
        KernelEventFlag_t handle_event = Kernel_wait_events(KernelEventFlag_CmdIn);
        switch(handle_event)
        {
        case KernelEventFlag_CmdIn:
-           debug_printf("\nEvent handled by Task1\n");
+           memclr(cmd, 16);
+           Kernel_recv_msg(KernelMsgQ_Task1, &cmdlen, 1);
+           Kernel_recv_msg(KernelMsgQ_Task1, cmd, cmdlen);
+           debug_printf("\nRecv Cmd: %s\n", cmd);
            break;
        }
        Kernel_yield();
    }
}

 

빌드 후 실행하여 테스트 해보자.

root@21d947b2e449:/project# make run
qemu-system-arm -M realview-pb-a8 -kernel build/navilos.axf -nographic
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5220:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2642:(snd_pcm_open_noupdate) Unknown PCM default
alsa: Could not initialize DAC
alsa: Failed to open `default':
alsa: Reason: No such file or directory
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5220:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2642:(snd_pcm_open_noupdate) Unknown PCM default
alsa: Could not initialize DAC
alsa: Failed to open `default':
alsa: Reason: No such file or directory
audio: Failed to create voice `lm4549.out'
NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN
Hello World!
Hello printf
output string pointer: printf pointer test
(null) is null pointer, 10 number
5 = 5
dec=255 hex=ff
print zero 0
SYSCTRL0 0
current count : 0
current count : 1000
current count : 2000
current count : 3000
current count : 4000
User Task #0 SP=0x8fffec
User Task #1 SP=0x9fffec
User Task #2 SP=0xaffff0
abc
Recv Cmd: abc
def
Recv Cmd: def

"abc" 입력 후 엔터키를 입력하니 Recv Cmd: abc이 출력되었다.

"def" 입력 후 엔터키를 입력하니 Recv Cmd: def가 출력되었다.

 

그림으로 표현하면 아래와 같다.

책에서 캡쳐

 

13장 동기화

  • 나빌로스는 비선점형 스케줄링인데다가 태스크가 명시적으로 Kernel_yield()를 호출해야만 다른 태스크로 컨텍스트가 넘어가므로, 싱글코어 환경에서는 동기화 문제가 발생하지 않는다
  • 동기화(sychronizatioin) : OS에서 어떤 작업을 아토믹 오퍼레이션(amotic operation)으로 만들어준다는 의미
  • 어떤 작업이 아토믹하게 구현되어야만 한다면 해당작업을 크리티컬 섹션(critical section)이라고 부름

 

  • 용어를 사용해서 다시 정리해보자
    • 동기화란 어떤 작업이 크리티컬 섹션이라고 판단되었을 경우, 해당 크리티컬 섹션을 아토믹 오퍼레이션으로 만들어주는 것을 말한다
  • 동기화를 구현하는 알고리즘에는 여러 종류가 있다
    • 그중 가장 많이 쓰는 세가지를 구현해보고자한다
      • 세마포어(semaphore)
      • 뮤텍스(mutex)
      • 스핀락(spin lock)

세마포어(semaphore)

의사코드로 먼저 확인하자

Test(S)
{
    while S <= 0 ; // 대기
    S--;
}

Release(S)
{
    S++;
}
  • Test() 함수
    • 크리티컬 섹션에 진입 가능한지를 확인해 보는 함수
    • 다른 의미로는 세마포어를 잠글(lock)수 있는지 확인한다는 의미도 갖고있다.
  • Release() 함수
    • 크리티컬 섹션을 나갈 때 호출해서 세마포어를 놓아주는(release) 함수
    • 다른 의미로는 세마포어의 잠금을 푸는(unlock) 역할을 갖고있다.

 

동기화를 구현하는 두 가지 중요한 개념을 잘 기억하자. 잠금과 잠금의 해제이다. 크리티컬 섹션에 들어갈 때 잠그고, 크리티컬 섹션을 나올 때 잠금을 푸는 것이다. 잠겨있는 도중에는 컨텍스트 스위칭도 발생하지 않고, 다른 코어가 끼어들지도 못한다. 

 

세마포어를 구현해보자. (kernel/synch.h, kernel/synch.c)

 

kernel/synch.h

#ifndef KERNEL_SYNCH_H_
#define KERNEL_SYNCH_H_

void Kernel_sem_init(int32_t max);
bool Kernel_sem_test(void);
void Kernel_sem_release(void);

#endif /* KERNEL_SYNCH_H_ */

 

kernel/synch.c

#include "stdint.h"
#include "stdbool.h"

#include "synch.h"

#define DEF_SEM_MAX 8

static int32_t sSemMax;
static int32_t sSem;

void Kernel_sem_init(int32_t max)
{
    sSemMax = (max <= 0) ? DEF_SEM_MAX : max;
    sSemMax = (max >= DEF_SEM_MAX) ? DEF_SEM_MAX : max;
    
    sSem = sSemMax;
}

bool Kernel_sem_test(void)
{
    if (sSem <= 0)
    {
        return false;
    }

    sSem--;

    return true;
}

void Kernel_sem_release(void)
{
    if (sSem >= sSemMax)
    {
        sSem = sSemMax;
    }

    sSem++;
}

 

의사코드와 거의 같고, 에러처리가 포함되었다. 

 

세마포어 최대값을 설정해두었다.

max 파라미터로 세마포어의 최대값을 설정하는데, max값이 1이면 크리티컬 섹션에는 컨텍스트가 딱 한개만 진입할 수 있다. 그리고 이런 세마포어를 바이너리 세마포어(binary semaphore)라고 부른다.

 

세마포어도 커널 API를 통해 사용할 수 있도록 하자.

kernel/Kernel.h

#ifndef KERNEL_KERNEL_H_
#define KERNEL_KERNEL_H_

#include "task.h"
#include "event.h"
#include "msg.h"
+#include "synch.h"

void              Kernel_start(void);
void              Kernel_yield(void);
void              Kernel_send_events(uint32_t event_list);
KernelEventFlag_t Kernel_wait_events(uint32_t waiting_list);
bool              Kernel_send_msg(KernelMsgQ_t Qname, void* data, uint32_t count);
uint32_t          Kernel_recv_msg(KernelMsgQ_t Qname, void* out_data, uint32_t count);
+void              Kernel_lock_sem(void);
+void              Kernel_unlock_sem(void);

#endif /* KERNEL_KERNEL_H_ */

kernel/Kernel.c

// ...중략

+void Kernel_lock_sem(void)
+{
+    while(false == Kernel_sem_test())
+    {
+        Kernel_yield();
+    }
+}
+
+void Kernel_unlock_sem(void)
+{
+    Kernel_sem_release();
+}

크리티컬 섹션에 진입하는 Kernel_lock_sem() 함수를 보자. Kernel_yield()를 하고 있는데, 의사코드에서 무한루프로 대기하는 기능을 대체한 것이다. 이렇게 대기 해야만 해당 크리티컬 섹션의 잠금을 소유하고 있는 다른 태스크로 컨텍스트가 넘어가서 세마포어의 잠금을 풀 수 있다. 간단하지만, 멀티태스킹에서 대기(waiting)을 어떻게 구현하는지 보여주는 코드이다.

 

테스트 해보자. 하지만 제약사항이 있다.

QEMU의 RealViewPB는 싱글코어 에뮬레이터이다. 게다가 navilos는 비선점형 스케줄리인데다가 커널이 강제로 스케줄링을 하는 것이 아니라 태스크가 Kernel_yield()함수를 호출해야만 스케줄링이 동작하므로 동기화 문제가 발생하는 코드를 만드는 것이 더 어렵다. 억지로 이상한 코드를 작성해서 상황을 만들어야한다.

해보자.

UART 인터럽트 핸들러 코드를 수정하자.

 

hal/rvpb/Uart.c

// ...중략

static void interrupt_handler(void)
{
	uint8_t ch = Hal_uart_get_char();
-	Hal_uart_put_char(ch);
-
-	Kernel_send_msg(KernelMsgQ_Task0, &ch, 1);
-	Kernel_send_events(KernelEventFlag_UartIn);
+	if (ch != 'X')
+	{
+		Hal_uart_put_char(ch);
+		Kernel_send_msg(KernelMsgQ_Task0, &ch, 1);
+		Kernel_send_events(KernelEventFlag_UartIn);
+	}
+	else
+	{
+		Kernel_send_events(KernelEventFlag_CmdOut);
+	}
}

대문자 X를 입력하면 CmdOut 이벤트가 발생한다. 해당 이벤트는 Task0에서 처리한다.

 

Task0도 수정하자.

boot/Main.c

+static void Test_critical_section(uint32_t p, uint32_t taskId);

+static uint32_t shared_value;

+static void Test_critical_section(uint32_t p, uint32_t taskId)
+{
+    debug_printf("User Task #%u Send=%u\n", taskId, p);
+    shared_value = p;
+    Kernel_yield();
+    delay(1000); // 테스트 쉽게하기 위한 딜레이
+    debug_printf("User Task #%u Shared Value=%u\n", taskId, shared_value);
+}

void User_task0(void)
{
	uint32_t local = 0;
	debug_printf("User Task #0 SP=0x%x\n", &local);

	uint8_t  cmdBuf[16];
	uint32_t cmdBufIdx = 0;
	uint8_t  uartch = 0;

	while(true)
	{
		KernelEventFlag_t handle_event = Kernel_wait_events(KernelEventFlag_UartIn|KernelEventFlag_CmdOut);
		switch(handle_event)
		{
		case KernelEventFlag_UartIn:
			Kernel_recv_msg(KernelMsgQ_Task0, &uartch, 1);
			if (uartch == '\r')
			{
				cmdBuf[cmdBufIdx] = '\0';

				Kernel_send_msg(KernelMsgQ_Task1, &cmdBufIdx, 1);
				Kernel_send_msg(KernelMsgQ_Task1, cmdBuf, cmdBufIdx);
				Kernel_send_events(KernelEventFlag_CmdIn);

				cmdBufIdx = 0;
			}
			else
			{
				cmdBuf[cmdBufIdx] = uartch;
				cmdBufIdx++;
				cmdBufIdx %= 16;
			}
			break;
		case KernelEventFlag_CmdOut:
-			debug_printf("\nCmdOut Event by Task0\n");        
+			Test_critical_section(5, 0);
			break;
		}
		Kernel_yield();
	}
}

테스트용 함수를 만들었다. 공유자원 문제를 만들기 위한 억지스러운 코드이다. 

동작은 매우 단순하다. shared_value 로컬 전역 변수를 공유 자원으로 두었다. 

전달 받은 변수로 공유 변수의 값을 바꾼다. 그리고 Kernel_yield() 함수를 호출해서 스케줄링해버린다. 마지막으로 태스크 번호와 공유 변수의 값을 출력하는데, 테스트 해보자.

root@21d947b2e449:/project# make run
qemu-system-arm -M realview-pb-a8 -kernel build/navilos.axf -nographic
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5220:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2642:(snd_pcm_open_noupdate) Unknown PCM default
alsa: Could not initialize DAC
alsa: Failed to open `default':
alsa: Reason: No such file or directory
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5220:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2642:(snd_pcm_open_noupdate) Unknown PCM default
alsa: Could not initialize DAC
alsa: Failed to open `default':
alsa: Reason: No such file or directory
audio: Failed to create voice `lm4549.out'
NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN
Hello World!
Hello printf
output string pointer: printf pointer test
(null) is null pointer, 10 number
5 = 5
dec=255 hex=ff
print zero 0
SYSCTRL0 0
current count : 0
current count : 1000
current count : 2000
current count : 3000
current count : 4000
User Task #0 SP=0x8fffec
User Task #1 SP=0x9fffec
User Task #2 SP=0xaffff0
User Task #0 Send=5
User Task #0 Shared Value=5
User Task #0 Send=5
User Task #0 Shared Value=5

X를 누를때마다 메시지가 출력된다.

  • User Task #0 Send=5
    • Task0이 숫자 5를 전달했다
  • User Task #0 Shared Value=5
    • Task0에서 출력한 공유 변수의 값은 5다

이제부터 진짜 테스트다.

이때, 같은 Test_critical_section() 함수를 Task2에서 동시에 호출한다면 어떤일이 생길까?

 

Task2의 동작을 수정하자.

boot/Main.c

// ...중략

void User_task2(void)
{
    uint32_t local = 0;

    debug_printf("User Task #2 SP=0x%x\n", &local);

    while(true)
    {
+       Test_critical_section(3, 2);
        Kernel_yield();
    }
}

// ...중략

 

빌드 후 실행해보자.

root@21d947b2e449:/project# make run
qemu-system-arm -M realview-pb-a8 -kernel build/navilos.axf -nographic
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5220:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2642:(snd_pcm_open_noupdate) Unknown PCM default
alsa: Could not initialize DAC
alsa: Failed to open `default':
alsa: Reason: No such file or directory
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5220:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2642:(snd_pcm_open_noupdate) Unknown PCM default
alsa: Could not initialize DAC
alsa: Failed to open `default':
alsa: Reason: No such file or directory
audio: Failed to create voice `lm4549.out'
NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN
Hello World!
Hello printf
output string pointer: printf pointer test
(null) is null pointer, 10 number
5 = 5
dec=255 hex=ff
print zero 0
SYSCTRL0 0
current count : 0
current count : 1000
current count : 2000
current count : 3000
current count : 4000
User Task #0 SP=0x8fffec
User Task #1 SP=0x9fffec
User Task #2 SP=0xaffff0
User Task #2 Send=3
User Task #2 Shared Value=3
User Task #2 Send=3
User Task #2 Shared Value=3
User Task #2 Send=3
User Task #2 Shared Value=3
User Task #2 Send=3
User Task #2 Shared Value=3
User Task #2 Send=3
User Task #2 Shared Value=3
User Task #2 Send=3
User Task #2 Shared Value=3
User Task #2 Send=3
User Task #2 Shared Value=3
User Task #0 Send=5
User Task #2 Send=3
User Task #0 Shared Value=3
User Task #2 Shared Value=3
User Task #0 Send=5
User Task #2 Send=3

Task2의 출력이 계속 나온다.  이때 X를 눌러준다.

Task2가 Task0이 shared_value에 5를 넣는 동작을 방해하는 형태가 된다. (동시성 문제를 억지로 만든 상황)

 

멀티코어 환경에서 크리티컬 섹션에 이와 비슷한 성격의 공유 자원 문제는 매우 자주 발생한다. 여러 코어가 공유하는 자원에 대한 값을 바꾸고 사용하는 코드라면 개발자가 판단해서 이것을 크리티컬 섹션으로 식별하고 반드시 동기화 처리를 해야만한다.

 

바이너리 세마포어를 만들어서 크리티컬 섹션에 동기화 처리를 해보자.

boot/Main.c

static void Kernel_init(void)
{
	uint32_t taskId;

	Kernel_task_init();
	Kernel_event_flag_init();
	Kernel_msgQ_init();
+	Kernel_sem_init(1);

	taskId = Kernel_task_create(User_task0);
	if (NOT_ENOUGH_TASK_NUM == taskId)
	{
		putstr("Task0 creation fail\n");
	}

	taskId = Kernel_task_create(User_task1);
	if (NOT_ENOUGH_TASK_NUM == taskId)
	{
		putstr("Task1 creation fail\n");
	}

	taskId = Kernel_task_create(User_task2);
	if (NOT_ENOUGH_TASK_NUM == taskId)
	{
		putstr("Task2 creation fail\n");
	}

	Kernel_start();
}

세마포어의 잠금 개수를 1로 한다는 의미이고, 이것이 바이너리 세마포어이다.

 

세마포어를 사용하자.

boot/Main.c

static uint32_t shared_value;
static void Test_critical_section(uint32_t p, uint32_t taskId)
{
+   Kernel_lock_sem();

    debug_printf("User Task #%u Send=%u\n", taskId, p);
    shared_value = p;
    Kernel_yield();
    delay(1000);
    debug_printf("User Task #%u Shared Value=%u\n", taskId, shared_value);

+   Kernel_unlock_sem();
}

크리티컬 섹션에 진입할 때 Kernel_lock_sem() 커널 API를 사용해서 세마포어를 잠그고, 크리티컬 섹션이 끝나면 Kernel_unlock_sem() 커널 API를 사용해서 세마포어의 잠금을 풀어준다.

크리티컬 섹션으로 식별된 Test_critical_section() 함수의 동작을 아토믹 오퍼레이션으로 만든 것이다.

 

빌드후 동작을 확인해보자.

root@21d947b2e449:/project# make run
qemu-system-arm -M realview-pb-a8 -kernel build/navilos.axf -nographic
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5220:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2642:(snd_pcm_open_noupdate) Unknown PCM default
alsa: Could not initialize DAC
alsa: Failed to open `default':
alsa: Reason: No such file or directory
ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory
ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name
ALSA lib conf.c:4732:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5220:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2642:(snd_pcm_open_noupdate) Unknown PCM default
alsa: Could not initialize DAC
alsa: Failed to open `default':
alsa: Reason: No such file or directory
audio: Failed to create voice `lm4549.out'
NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN
Hello World!
Hello printf
output string pointer: printf pointer test
(null) is null pointer, 10 number
5 = 5
dec=255 hex=ff
print zero 0
SYSCTRL0 0
current count : 0
current count : 1000
current count : 2000
current count : 3000
current count : 4000
User Task #0 SP=0x8fffec
User Task #1 SP=0x9fffec
User Task #2 SP=0xaffff0
User Task #2 Send=3
User Task #2 Shared Value=3
User Task #2 Send=3
User Task #2 Shared Value=3
User Task #2 Send=3
User Task #2 Shared Value=3
User Task #0 Send=5
User Task #0 Shared Value=5
User Task #2 Send=3
User Task #2 Shared Value=3
User Task #0 Send=5
User Task #0 Shared Value=5
User Task #2 Send=3
User Task #2 Shared Value=3
User Task #0 Send=5
User Task #0 Shared Value=5
User Task #2 Send=3
User Task #2 Shared Value=3
User Task #0 Send=5

이전과 동일하게, Task2의 결과가 계속 출력되는 동안 X를 타이핑했다. 세마포어를 쓰기 전처럼 Task2의 입력값이 Task0에 출력되지 않는다.

Task0일 때는 shared_value의 값이 항상 5이고, Task2일 때는 shared_value의 값이 항상 3이다. 세마포어가 제대로 동작한다.

뮤텍스(Mutex)

  • 또 다른 동기화 알고리즘
  • 뮤텍스 == 이진 세마포어 + 소유의 개념
    • 세마포어는 잠금에 대한 소유 개념이 없으므로, 누가 잠근 세마포어이든 간에 누구나 잠금을 풀 수 있었다.
    • 소유의 개념이 있다는건, 뮤텍스를 잠근 태스크만이 뮤텍스의 잠금을 풀 수 있다는 말이다.

kernel/synch.h

#ifndef KERNEL_SYNCH_H_
#define KERNEL_SYNCH_H_

+typedef struct KernelMutext_t
+{
+	uint32_t owner;
+	bool     lock;
+} KernelMutext_t;

void Kernel_sem_init(int32_t max);
bool Kernel_sem_test(void);
void Kernel_sem_release(void);

+void Kernel_mutex_init(void);
+bool Kernel_mutex_lock(uint32_t owner);
+bool Kernel_mutex_unlock(uint32_t owner);

#endif /* KERNEL_SYNCH_H_ */

 

kernel/synch.c

#include "stdint.h"
#include "stdbool.h"

#include "synch.h"

#define DEF_SEM_MAX 8

static int32_t sSemMax;
static int32_t sSem;

+KernelMutext_t sMutex;

void Kernel_sem_init(int32_t max)
{
    sSemMax = (max <= 0) ? DEF_SEM_MAX : max;
    sSemMax = (max >= DEF_SEM_MAX) ? DEF_SEM_MAX : max;

    sSem = sSemMax;
}

bool Kernel_sem_test(void)
{
    if (sSem <= 0)
    {
        return false;
    }

    sSem--;

    return true;
}

void Kernel_sem_release(void)
{
    if (sSem >= sSemMax)
    {
        sSem = sSemMax;
    }

	sSem++;
}

+void Kernel_mutex_init(void)
+{
+	sMutex.owner = 0;
+	sMutex.lock = false;
+}
+
+bool Kernel_mutex_lock(uint32_t owner)
+{
+	if (sMutex.lock)
+	{
+		return false;
+	}
+
+	sMutex.owner = owner;
+	sMutex.lock = true;
+	return true;
+}
+
+bool Kernel_mutex_unlock(uint32_t owner)
+{
+	if (owner == sMutex.owner)
+	{
+		sMutex.lock = false;
+		return true;
+	}
+	return false;
+}

뮤텍스 자료구조를 전역변수 두었다. 해당 전역 변수로 커널 뮤텍스를 제어하는 것이다. 필요에 따라서 배열로 만들어 뮤텍스를 여러개 사용할 수도 있다.

 

우선 커널 초기화시 등록하도록 수정하자.

boot/Main.c

// ...중략

static void Kernel_init(void)
{
    uint32_t taskId;

    Kernel_task_init();
    Kernel_event_flag_init();
    Kernel_msgQ_init();
    Kernel_sem_init(1);
+   Kernel_mutex_init();

    taskId = Kernel_task_create(User_task0);
    if (NOT_ENOUGH_TASK_NUM == taskId)
    {
        putstr("Task0 creation fail\n");
    }

    taskId = Kernel_task_create(User_task1);
    if (NOT_ENOUGH_TASK_NUM == taskId)
    {
        putstr("Task1 creation fail\n");
    }

    taskId = Kernel_task_create(User_task2);
    if (NOT_ENOUGH_TASK_NUM == taskId)
    {
        putstr("Task2 creation fail\n");
    }

    Kernel_start();
}

// ...중략

 

Kernel_mutex_unlock() 함수는 뮤텍스의 잠금을 푸는 함수이다. 세마포어와 다른점을 확인할수 있다. 파라미터로 넘어온 owner의 값과 뮤텍스 전역 변수에 저장되어있는 owner를 비교한다.

 

커널 API로 사용하도록 하자.

kernel/Kernel.h

#ifndef KERNEL_KERNEL_H_
#define KERNEL_KERNEL_H_

#include "task.h"
#include "event.h"
#include "msg.h"
#include "synch.h"

void              Kernel_start(void);
void              Kernel_yield(void);
void              Kernel_send_events(uint32_t event_list);
KernelEventFlag_t Kernel_wait_events(uint32_t waiting_list);
bool              Kernel_send_msg(KernelMsgQ_t Qname, void* data, uint32_t count);
uint32_t          Kernel_recv_msg(KernelMsgQ_t Qname, void* out_data, uint32_t count);
void              Kernel_lock_sem(void);
void              Kernel_unlock_sem(void);
+void              Kernel_lock_mutex(void);
+void              Kernel_unlock_mutex(void);

#endif /* KERNEL_KERNEL_H_ */

kernel/Kernel.c

// ...중략

void Kernel_lock_mutex(void)
{
	while(true)
	{
		uint32_t current_task_id = Kernel_task_get_current_task_id();
		if (false == Kernel_mutex_lock(current_task_id))
		{
			Kernel_yield();
		}
		else
		{
			break;
		}
	}
}

void Kernel_unlock_mutex(void)
{
	uint32_t current_task_id = Kernel_task_get_current_task_id();
	if (false == Kernel_mutex_unlock(current_task_id))
	{
		Kernel_yield();
	}
}

Kernel_task_get_current_task_id() 함수는 아직 구현전.

 

뮤텍스의 커널 API는 뮤텍스 함수가 false를 리턴할 때 Kernel_yield() 함수를 호출하는 것 외에 다른 작업을 해준다. 뮤텍스의 소유자를 뮤텍스 함수에 알려주는 것이다. 7번째 줄과 21번째 줄에 있는 Kernel_task_get_current_task_id() 함수를 호출하는 부분이다. 해당 함수는 현재 동작 중인 태스크의 ID를 리턴하는 함수이다.

 

이제 구현해보자.

태스크에 관련한 것이기에 task.c파일을 수정하자.

kernel/task.h

#ifndef KERNEL_TASK_H_
#define KERNEL_TASK_H_

#include "MemoryMap.h"

#define NOT_ENOUGH_TASK_NUM     0xFFFFFFFF

#define USR_TASK_STACK_SIZE     0x100000
#define MAX_TASK_NUM            (TASK_STACK_SIZE / USR_TASK_STACK_SIZE)

typedef struct KernelTaskContext_t
{
	uint32_t spsr;
	uint32_t r0_r12[13];
	uint32_t pc;
} KernelTaskContext_t;

typedef struct KernelTcb_t
{
	uint32_t sp;
	uint8_t* stack_base;
} KernelTcb_t;

typedef void (*KernelTaskFunc_t)(void);

void     Kernel_task_init(void);
void     Kernel_task_start(void);
uint32_t Kernel_task_create(KernelTaskFunc_t startFunc);
void     Kernel_task_scheduler(void);
void     Kernel_task_context_switching(void);
+uint32_t Kernel_task_get_current_task_id(void);

#endif /* KERNEL_TASK_H_ */

kernel/task.c

// ...중략

+uint32_t Kernel_task_get_current_task_id(void)
+{
+    return sCurrent_tcb_index;
+}

// ...중략

 

테스트를 해보자. 세마포어와의 확실한 차이를 확인해보자. 세마포어를 Task0에서 잠그고, Task1에서 잠금을 풀어보자.

새로운 이벤트를 추가하자.

kernel/event.h

// ...중략

typedef enum KernelEventFlag_t
{
    KernelEventFlag_UartIn      = 0x00000001,
    KernelEventFlag_CmdIn	    = 0x00000002,
    KernelEventFlag_CmdOut      = 0x00000004,
-   KernelEventFlag_Reserved03  = 0x00000008,
+   KernelEventFlag_Unlock      = 0x00000008,
    KernelEventFlag_Reserved04  = 0x00000010,
    KernelEventFlag_Reserved05  = 0x00000020,
    KernelEventFlag_Reserved06  = 0x00000040,
    KernelEventFlag_Reserved07  = 0x00000080,
    KernelEventFlag_Reserved08  = 0x00000100,
    KernelEventFlag_Reserved09  = 0x00000200,
    KernelEventFlag_Reserved10  = 0x00000400,
    KernelEventFlag_Reserved11  = 0x00000800,
    KernelEventFlag_Reserved12  = 0x00001000,
    KernelEventFlag_Reserved13  = 0x00002000,
    KernelEventFlag_Reserved14  = 0x00004000,
    KernelEventFlag_Reserved15  = 0x00008000,
    KernelEventFlag_Reserved16  = 0x00010000,
    KernelEventFlag_Reserved17  = 0x00020000,
    KernelEventFlag_Reserved18  = 0x00040000,
    KernelEventFlag_Reserved19  = 0x00080000,
    KernelEventFlag_Reserved20  = 0x00100000,
    KernelEventFlag_Reserved21  = 0x00200000,
    KernelEventFlag_Reserved22  = 0x00400000,
    KernelEventFlag_Reserved23  = 0x00800000,
    KernelEventFlag_Reserved24  = 0x01000000,
    KernelEventFlag_Reserved25  = 0x02000000,
    KernelEventFlag_Reserved26  = 0x04000000,
    KernelEventFlag_Reserved27  = 0x08000000,
    KernelEventFlag_Reserved28  = 0x10000000,
    KernelEventFlag_Reserved29  = 0x20000000,
    KernelEventFlag_Reserved30  = 0x40000000,
    KernelEventFlag_Reserved31  = 0x80000000,

    KernelEventFlag_Empty       = 0x00000000,
} KernelEventFlag_t;

// ...중략

이 이벤트를 UART 인터럽트 핸들러에서 보낼 것이다.

 

UART 인터럽트 핸들러를 수정하자.

hal/rvpb/Uart.c

// ...중략

static void interrupt_handler(void)
{
	uint8_t ch = Hal_uart_get_char();

-	if (ch != 'X')
-	{
-		Hal_uart_put_char(ch);
-		Kernel_send_msg(KernelMsgQ_Task0, &ch, 1);
-		Kernel_send_events(KernelEventFlag_UartIn);
-	}
-	else
-	{
-		Kernel_send_events(KernelEventFlag_CmdOut);
-	}   
+	if (ch == 'U')
+	{
+		Kernel_send_events(KernelEventFlag_Unlock);
+		return;
+	}
+
+	if (ch == 'X')
+	{
+		Kernel_send_events(KernelEventFlag_CmdOut);
+		return;
+	}
+
+	Hal_uart_put_char(ch);
+	Kernel_send_msg(KernelMsgQ_Task0, &ch, 1);
+	Kernel_send_events(KernelEventFlag_UartIn);
}

U 입력시 Unlock 이벤트를 보내는 동작을 추가한 것이다. Unlock 이벤트는 Task1에서 받아서 처리하도록 하자.

 

boot/Main.c

// ...중략

void User_task1(void)
{
	uint32_t local = 0;

	debug_printf("User Task #1 SP=0x%x\n", &local);

	uint8_t cmdlen = 0;
	uint8_t cmd[16] = {0};

	while(true)
	{
-		KernelEventFlag_t handle_event = Kernel_wait_events(KernelEventFlag_CmdIn);    
+		KernelEventFlag_t handle_event = Kernel_wait_events(KernelEventFlag_CmdIn|KernelEventFlag_Unlock);
		switch(handle_event)
		{
		case KernelEventFlag_CmdIn:
			memclr(cmd, 16);
			Kernel_recv_msg(KernelMsgQ_Task1, &cmdlen, 1);
			Kernel_recv_msg(KernelMsgQ_Task1, cmd, cmdlen);
			debug_printf("\nRecv Cmd: %s\n", cmd);
			break;
+		case KernelEventFlag_Unlock:
+			Kernel_unlock_sem();
+			break;
		}
		Kernel_yield();
	}
}

// ...중략

U 입력시 Task1은 세마포어를 해제하게되었다. 크리티컬 섹션 함수도 수정하자.

 

boot/Main.c

static uint32_t shared_value;
static void Test_critical_section(uint32_t p, uint32_t taskId)
{
    Kernel_lock_sem();

    debug_printf("User Task #%u Send=%u\n", taskId, p);
    shared_value = p;
    Kernel_yield();
    delay(1000);
    debug_printf("User Task #%u Shared Value=%u\n", taskId, shared_value);

-   Kernel_unlock_sem();
+   //Kernel_unlock_sem();
}

세마포어 해제함수를 주석처리했다. 크리티컬 섹션에 진입하면서, 세마포어를 잠그기만하고 해제하지 않는다. 

빌드후 테스트해보자.

// ... 중략

User Task #2 Send=3
User Task #2 Shared Value=3
									// <---- 여기서 멈춤. U키를 누르면...
User Task #2 Send=3
User Task #2 Shared Value=3
									// <---- 여기서 멈춤. U키를 누르면...
User Task #2 Send=3
User Task #2 Shared Value=3
									// <---- 여기서 멈춤.

Task2가 크리티컬 섹션에 진입해서 출력하긴 하지만 세마포어의 잠금을 해제하지 않았다. 

다시 스케줄링 받아서 크리티컬 섹션에 진입했을때, Task2 자신이 잠갔던 세마포어에 걸려서 크리티컬 섹션에 진입하지 못하게된다. 그래서 QEMU의 출력 상 멈춰있는것으로 보인다. 이 상태에서 U를 입력하면 Task1이 세마포어를 푼다. 따라서 Task2가 크리티컬 섹션을 한 번 실행하고 다시 또 멈추는 것이다.

 

이처럼 세마포어는 소유의 개념이 없다. 잠그는 주체와 푸는 주체가 달라도 된다. 잠금 횟수와 순서만 맞으면 된다.

 

뮤텍스를 테스트해보자. 2가지 경우로 테스트 할것이다. 우선 다른 태스크에서 뮤텍스 해제하는 케이스로 작성해보자.

boot/Main.c

// ...중략

static uint32_t shared_value;
static void Test_critical_section(uint32_t p, uint32_t taskId)
{
-	Kernel_lock_sem();
+	Kernel_lock_mutex();

	debug_printf("User Task #%u Send=%u\n", taskId, p);
	shared_value = p;
	Kernel_yield();
	delay(1000);
	debug_printf("User Task #%u Shared Value=%u\n", taskId, shared_value);
    
-	//Kernel_unlock_sem();
}

// ...중략

void User_task1(void)
{
// ...중략
        case KernelEventFlag_Unlock:
        	Kernel_unlock_mutex();
            break;
        }
        Kernel_yield();
    }
}

 

빌드 후 테스트해보자.

// ...중략

User Task #2 Send=3
User Task #2 Shared Value=3
//									<----- 출력 없음

크리티컬 섹션의 내용이 한 번 출력되고 반응이 없다. U를 눌러도 직전 예제처럼 동작하지 않는다. 소유의 개념때문이다.

 

정상동작하는 케이스로 작성하자.

boot/Main.c

// ...중략

static uint32_t shared_value;
static void Test_critical_section(uint32_t p, uint32_t taskId)
{
	Kernel_lock_mutex();

	debug_printf("User Task #%u Send=%u\n", taskId, p);
	shared_value = p;
	Kernel_yield();
	delay(1000);
	debug_printf("User Task #%u Shared Value=%u\n", taskId, shared_value);
    
+	Kernel_unlock_mutex();
}

// ...중략

void User_task1(void)
{
// ...중략
        case KernelEventFlag_Unlock:
-        	Kernel_unlock_mutex();
            break;
        }
        Kernel_yield();
    }
}

빌드 후 테스트해보자.

 

// ...중략

User Task #2 Send=3
User Task #2 Shared Value=3
User Task #2 Send=3
User Task #2 Shared Value=3
User Task #0 Send=5
User Task #0 Shared Value=5
User Task #2 Send=3
User Task #2 Shared Value=3
User Task #0 Send=5
User Task #0 Shared Value=5
User Task #2 Send=3

Task2 출력 중간에 X를 눌러 Task0이 크리티컬 섹션에 끼어들도록 시도했다. 그럼에도, 뮤텍스로 크리티컬 섹션을 보호하므로 Task2가 잠근 뮤텍스는 Task2가 풀고 나오고 Task0이 잠근 뮤텍스는 Task0이 풀고 나온다. 

의도한대로 동작한다.

스핀락(spin lock)

  • 스핀락(spin lock)은 바쁜 대기(busy waiting) 개념의 크리티컬 섹션 보호 기능이다.
  • 바쁜 대기란 스케줄링을 하지 않고 CPU를 점유한 상태, 즉 CPU가 여전히 바쁜 상태에서 락이 풀리는 것을 대기한다는 의미이다.
  • 스케줄링을 하지 않고 짧은 시간 동안 CPU를 점유하면서 잠금이 풀리는 것을 기다리는 아이디어이므로 멀티코어 환경에서 유용하게 쓰이기도 한다.
  • 하지만 싱글 코어 환경에서는 다른 태스크가 잠금을 풀려면 어차피 스케줄링을 해야하므로 스핀락의 개념을 사용할수가 없다
  • QEMU의 RealViewPB도 싱글코어로 에뮬레이팅되므로 스핀락을 사용할 수 없다. 
  • 따라서 간단한 코드로 개념만 보자
    • 실제 스핀락 구현은 바쁜 대기 자체가 완전히 아토믹해야한다
      • 배타적 메모리 연산을 지원하는 어셈블리어 명령으로 구현한다.
      • C언어 코드로 의사코드를 작성한다. 그럼에도 코드를 이해할때는 해당 함수가 아토믹하게 동작한다고 생각하자
static bool sSpinLock = false;

void spin_lock(void)
{
    while(sSpinLock); // 대기
    sSpinLock = true; // 잠금
}

void spin_unlock(void)
{
    sSpinLock = false; // 해제
}

세마포어와 대략 비슷한 느낌이다. 다만 스핀락 변수가 boolean 타입이라 바이너리 세마포어와 같은 동작을 한다.

 

대기 시, 스케줄러를 호출하지 않고 while 반복문으로 CPU를 점유한 채로 대기한다.

멀티코어 환경이라면 아마 다른 코어에서 동작 중인 스핀락을 잠갔던 태스크가 spin_unlock() 함수를 호출해서 공유변수인 sSpinLock 변수를 false로 바꿔줄 것이므로 while loop에 바쁜 대기 중인 코어의 대기가 풀리면서 크리티컬 섹션에 진입할 수 있다.


후기

'컴퓨팅 시스템' 카테고리의 다른 글

CPU from scratch  (1) 2024.04.22
How To Make A CPU  (1) 2024.04.21
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함