2020년 7월 21일 화요일

Linux Kernel Programming - GPIO Interrupt and Bottom Halves

이번 시간에는 GPIO interrupt and Bottom Halves(Deferring Work)라는 주제로 몇가지 kernel programming 예제(linux 4.19.94 기준)를 소개해 보도록 하겠다. 😎



목차
1. STM32MP157C Discovery Kit 환경 설정
2. GPIO Interrupt Platform Driver
  - Device tree and platform driver, GPIO Interrupt(top half)
3. Bottom Halves and Deferring Work
  - Kernel timer
  - Tasklet
  - Workqueue
  - Threaded Interrupt(IRQ Thread)
  - Kernel Thread
4. References


1. STM32MP157C Discovery Kit 환경 설정
DK2 보드와 관련해서는 이미 이전 blog post를 통해 여러 차례 소개한 바가 있다. DK2 보드가 생소한 분들은 먼저 아래 blog post의 내용을 참조하기 바란다.


지금부터는 아래 ST wiki page의 내용을 기초로 하여 (이번 blog post에서 사용할) ARM cross toolchain(정확하게는 Yocto SDK) 및 linux kernel 등을 설치해 보도록 하겠다.


<Yocto SDK 설치>
$ ./st-image-weston-openstlinux-weston-stm32mp1-x86_64-toolchain-3.1-openstlinux-5.4-dunfell-mp1-20-06-24.sh 
ST OpenSTLinux - Weston - (A Yocto Project Based Distro) SDK installer version 3.1-openstlinux-5.4-dunfell-mp1-20-06-24
=======================================================================================================================
Enter target directory for SDK (default: /opt/st/stm32mp1/3.1-openstlinux-5.4-dunfell-mp1-20-06-24): 
You are about to install the SDK to "/opt/st/stm32mp1/3.1-openstlinux-5.4-dunfell-mp1-20-06-24". Proceed [Y/n]? y
[sudo] chyi의 암호: 
Extracting SDK................................................................................................................................................................................................................done
Setting it up...done
SDK has been successfully set up and is ready to be used.
Each time you wish to use the SDK in a new shell session, you need to source the environment setup script e.g.

[Tip] (설치 후 확인해 보니) ARM용 gcc compiler는 아래 디렉토리에 존재한다.
 /opt/st/stm32mp1/3.1-openstlinux-5.4-dunfell-mp1-20-06-24/sysroots/x86_64-ostl_sdk-linux/usr/bin/arm-ostl-linux-gnueabi 

 $ . /opt/st/stm32mp1/3.1-openstlinux-5.4-dunfell-mp1-20-06-24/environment-setup-cortexa7t2hf-neon-vfpv4-ostl-linux-gnueabi
  -> 이 파일에는 cross-compile과 관련한 모든 환경 변수(SYSROOT, compile path 등)가 담겨 있다. 이 명령을 매번 실행하는 것 보다는 ~/.bashrc에 넣어 두면 편리하다.
  -> 설치된 gcc(arm-ostl-linux-gnueabi-gcc) version이 9.3.0임을 알 수 있다.



<kernel download & build>
$ tar xvf en.SOURCES-kernel-stm32mp1-openstlinux-20-02-19.tar.xz
$ cd stm32mp1-openstlinux-20-02-19/sources/arm-ostl-linux-gnueabi/linux-stm32mp-4.19-r0
$ tar xvf linux-4.19.94.tar.xz
$ cd linux-4.19.94

$ make menuconfig
$ make uImage vmlinux dtbs LOADADDR=0xC2000040 -j8
$ make modules
$ mkdir -p $PWD/install_artifact/
$ make INSTALL_MOD_PATH="$PWD/install_artifact" modules_install

이 정도로 해서 간단하게 device driver(or kernel programming) 개발 환경 설정 작업을 마무리하도록 하겠다. 보다 자세한 사항은 참고 문헌 [1]을 참조하기 바란다.


2. GPIO Interrupt Platform Driver
필자는 이미 Linux kernel programming(or device driver programming)에 관하여 (3년 전에) 상세히 정리한 바가 있다.


and

하지만 Linux kernel은 나날이 발전(오늘 현재 5.7.9가 release되어 있음)하고 있어서, 위의 내용으로는 충분치 못하다는 생각이 들었다. 따라서 이번 posting에서는 여러 주제 중 interrupt 관련 부분만을 뽑아 최신 kernel (linux 4.19.94) 에 맞게 재 정리해 보고자 한다.

2.1) DK2 보드의 User Button
DK2 보드에는 사용자용 버튼(User1, User2)이 두개 있다. 이 두개의 버튼은 본래 u-boot에서 firmware writing mode(User1: USB programming mode, User2: Android fastboot mode)로  진입하기 위해 만들어졌지만, 필자는 이를 gpio interrupt 처리용으로 잠시 용도변경해 볼 생각이다.

[그림 2.1] DK2 보드의 사용자 버튼(User1, User2)
[Tip] 위의 그림에서 User2 버튼이 B2로 표기된 부분은 오타인 듯 보인다. 즉, B2 -> B4로 표기되는 것이 맞을 것 같다.

[그림 2.2] DK2 보드의 사용자 버튼(User1, User2)의 pinmap


[그림 2.3] DK2 보드의 block도 중 User button(우측) 발췌


2.2) Device Tree 내용 추가
앞서 언급한 User1 버튼은 아래 그림과 같이 PA14 pin(GPIO A bank 14번째 pin)에 연결되어 있다.

[그림 2.4] User1 버튼에 대한 회로도 - push-pull 회로

DK2 보드용 device tree 파일 내용을 살펴보면, STMP157C 칩(SoC)에는 대략 10개(gpio A ~ I, Z) 정도의 GPIO bank가  존재하는 것 같다. 또한 하나의 GPIO bank에는 16개의 GPIO pin이 포함되어 있음을 알 수 있다.

[그림 2.5] STMP157C의 GPIO controller에 대한 device tree 표현 - stm32mp157cac-pinctrl.dtsi 파일에서 발췌


[그림 2.6] kernel booting message


<여기서 잠깐 !>
    -> DK2 보드의 device tree에 관하여
1장에서 사용한 kernel source를 기준으로 device tree 파일(linux-4.19.94/arch/arm/boot/dts/stmp32mp*)의 상관 관계를 파악해 보면 대략 다음과 같다.

stm32mp157c.dtsi
stm32mp157cac-pinctrl.dtsi  -> stm32mp157-pinctrl.dtsi
stm32mp157c-m4-srm.dtsi
^
|
stm32mp157a-dk1.dts
^
|
stm32mp157c-dk2.dts
[그림 2.7] DK2 보드의 device tree 상관 관계도

그런데, 이 내용은 이전 blog post에서 분석한 내용(buildroot 기반)과는 사뭇 차이가 난다. 왜 그럴까 ?

[그림 2.8] DK2 보드의 device tree 상관 관계도 - buildroot 기준

이는 아마도, Yocto 환경(linux-4.19.94)과 buildroot 환경(linux-5.7.1)에서 오는 차이인 듯하다. 아니, kernel 버젼이 다른 것이 더 큰 이유가 될 것 같다.

어찌됐든 우리는 이 중에서 User1 button을 활용한 GPIO interrupt 부분에 관심이 있으니, 이와 관련해서 좀 더 분석해 보기로 하자.

<gpio controller & device 상호 관계>
intc(GIC) <---- gpioa controller <--- User1 button
                               (interrupt-parent)      (interrupt device)


<gpioa controller node>

[그림 2.9] gpioa bank(gpio a controller) - stm32mp157-pinctrl.dtsi

[그림 2.10] gpioa bank(gpio a controller) - stm32mp157cac-pinctrl.dtsi

GPIO 속성 설정과 관련해서는 아래 문서를 참조할 필요가 있다.
Documentation/devicetree/bindings/gpio/gpio.txt

User1 button 용 device tree node는 대략 다음과 같다(예상해 볼 수  있다).

<User1 button - gpio device node>
user1_button {
    compatible = "blahblahblah";
    user1-gpios = <&gpioa 14 GPIO_ACTIVE_LOW>;
    interrupt-parent = <&gpioa>;
    interrupts = <14 IRQ_TYPE_EDGE_FALLING>;
};

<속성 정보 설명>
[<name>-]gpios 속성 지정자
   => bank(phandle), pin 번호, active-high or active-low value
[참고] <name>-gpios 형태로 사용해야 함(new style). <name>- 없이 gpios, gpio도 사용 가능(하위 버젼 호환성 차원에서 지원)

interrupt-parent 속성
  => interrupt controller(여기서는 gpioa)에 대한 phandle 값

interrupts 속성
  => interrupt signal을 내보내는 device(gpio pin/pad)의 pin 값(여기서는 14)과, IRQ type(include/dt-bindings/interrupt-controller/irq.h 내용 참조) 지정
-- -- -- -- --

GPIO pin을 제대로 활용하기 위해서는 pin control 설정에 대해서도 정확히 알고 있어야 한다. 이와 관련해서는 아래 문서를 참조하도록 하자.
Documentation/devicetree/bindings/pinctrl/st,stm32-pinctrl.txt

[그림 2.11] Documentation/devicetree/bindings/pinctrl/st,stm32-pinctrl.txt
---------------

이상의 내용을 기초로 device tree 코드를 구현해 보도록 하자.

<stm32mp157-pinctrl.dtsi에 추가할 내용>
pinctrl_user2_button_a: user2-pb-0 {
    pins {
        pinmux = <STM32_PINMUX('A', 14, GPIO)>;
        drive-push-pull;
    };
};
[Tip] STM32_PINMUX macro는 include/dt-bindings/pinctrl/stm32-pinfunc.h 파일에 정의되어 있다.
[Tip] 위에서 drive-push-pull을 지정한 이유는 그림 2.4의 회로도를 유심히 살펴보면 알 수가 있다. Push-pull 출력은 2개의 TR(transistor)로 구성되어 있는데, 아래 스위치(TR)를 누르면 출력 핀(PA14)이 GND와 연결되어 LOW가 출력(전류 싱크)되고, 위쪽 스위치를 누르면 출력 핀은 VCC(3.3V)와 연결되어 HIGH가 출력(전류 소스)된다. Push-pull과 대비되는 개념으로 open-drain/collector가 있는데, 얘는 전류 싱크로만 동작 가능하다. 즉, LOW 상태와 회로가 open된 상태인 하이 임피던스만 존재한다.

[그림 2.12] include/dt-bindings/pinctrl/stm32-pinfunc.h 파일

<stm32mp157a-dk1.dts에 추가할 내용>
my_button_interrupt {
    compatible = "eagle,int-button";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_user2_button_a>;      /* stm32mp157-pinctrl.dtsi에서 정의 */
    mybtn-gpios = <&gpioa 14 GPIO_ACTIVE_LOW>;    /* new style gpio specifier */
    interrupt-parent = <&gpioa>;
    interrupts = <14 IRQ_TYPE_EDGE_FALLING>;
};

Device tree 작업을 마쳤으니, 관련 내용을 target board에 반영시켜 볼 차례이다^^.

$ cd linux-4.19.94
$ make dtbs
  ->  지금까지 작업한 내용을 compile해 보자.

[그림 2.13] compile 후, dtb 파일 생성

다음으로, compile 결과 파일(dtb 파일)을 target board에 올려 보도록 하자.

<Target board>
root@stm32mp1:~# ifconfig eth0 192.168.1.100 netmask 255.255.255.0 up

<Desktop PC>
$ cd arch/arm/boot
$ scp ./uImage root@192.168.1.100:~/workspace/boot
  -> device tree 파일만 수정했으나, 현재 동작중인 kernel과 새로 build한 kernel 간에 차이가 있을 수 있으므로, kernel image(uImage)도 함께 교체하도록 한다.
$ cd dts
scp ./stm32mp157c-dk2.dtb root@192.168.1.100:~/workspace/boot
scp ./stm32mp157c-dk2-a7-examples.dtb root@192.168.1.100:~/workspace/boot
scp ./stm32mp157c-dk2-m4-examples.dtb root@192.168.1.100:~/workspace/boot
[Tip] DK2 보드는 현재 microSD로 부팅한 상태이며, /boot 디렉토리에 kernel과 dtb 파일 등이 위치하고 있다.

[그림 2.14] target board /boot 디렉토리

<Target board>
root@stm32mp1:~# cd ~/workspace/boot
root@stm32mp1:~# cp ./uImage /boot
root@stm32mp1:~# cp ./*.dtb /boot
root@stm32mp1:~# sync; sync; reboot
  -> 파일 교체 후에는 반드시 부팅을 하도록 한다.

2.3) Platform driver 구현
Device tree 작업이 끝났으니, 이제 부터는 이를 사용하는 platform driver(gpio interrupt driver)를 구현해 볼 차례이다.

원래는 driver 작성 과정을 상세히 설명할 계획이었으나, 시간 관계상 source code의 위치를 소개하는 것으로 이를 대신하고자 한다.


[그림 2.15] GPIO interrupt - platform driver probe 함수 소개

그럼, driver가 제대로 동작하는지 확인해 보도록 하자.

<Desktop PC>
$ git clone https://github.com/ChunghanYi/linux_kernel_hacks
   -> 위의 코드는 필자의 github에 올려 두었으니, 이를 먼저 내려 받도록 한다.
$ cd linux_kernel_hacks/writing_device_drivers_by_coopj/s_08
$ ../genmake
   -> Makefile을 자동으로 생성해준다.
$ make clean
$ make 
$ file lab3_interrupt.ko
   -> cross-compile이 제대로 되었는지 확인해 본다.


$ scp ./lab3_interrupt.ko root@192.168.1.100:~/workspace
  -> scp 명령을 사용하여 target board로 kernel module을 올린다.

<Target board>
root@stm32mp1:~/workspace# insmod ./lab3_interrupt.ko 
  -> insmod 명령을 사용하여 kernel module을 구동시킨다.
[ 3769.676181] int-button my_button_interrupt: my_probe() function is called.
[ 3769.681772] stm32mp157-pinctrl soc:pin-controller@50002000: pin PA14 already requested by my_button_interrupt; cannot claim for GPIOA:14
[ 3769.694580] stm32mp157-pinctrl soc:pin-controller@50002000: pin-14 (GPIOA:14) status -22
[ 3769.701946] int-button my_button_interrupt: gpio get failed
[ 3769.707667] int-button: probe of my_button_interrupt failed with error -22

어랍쇼~ 에러가 발생한다. 어디가 문제일까 ?  "pin-controller ... pin PA14 already requested"라는 부분이 보이는데 ?!

이상하다. GPIOA 14 pin에 대한 설정을 다른 곳에서 이미 하고 있나 ? 그렇다면, 일단 pinctrl 설정 부분을 막고 테스트해 보자.

[그림 2.16] device tree 수정 버젼(stm32mp157a-dk1.dts) - pinctrl 부분 제거

<Target board>
root@stm32mp1:~/workspace# insmod ./lab3_interrupt.ko
  -> 수정 후, 다시 테스트(반복되는 과정은 생략)해 보니, 정상 동작한다.
  -> User1 버튼을 누르면, 한줄씩 메시지가 출력된다.

[그림 2.17] lab3_interrupt.ko 모듈 정상 동작 모습
[Tip] 왜 PA14 already requested 메시지가 출력되는지를 알기 위해서는 전체 device tree 부분을 면밀히 검토해 보아야 할 듯하다.

<여기서 잠깐 !>
  ->  devm_request_irq( ) 함수에 대해...
-------------------------------------------------------------------------------------------------------------------
int devm_request_irq(struct device *dev,
                                    unsigned int irq,
                                    irq_handler_t handler,
                                    unsigned long irq_flags,
                                    const char *devname,
                                    void *dev_id);

a) interrupt handler를 등록한다.
b)기존에 많이 사용하던 request_irq( ) 함수와 비교해, 첫번째 argument가 struct device pointer라는 점에서 차이가 있는데, 이런 함수를 일컬어 managed resource API(workqueue로 유명한 허태준씨가 2007년 kernel 2.6.21부터 제안함)라고 부른다.
c) request_irq()는 사용 후, free_irq()를 반드시 해 주어야 하나, devm_request_irq()는 driver 제거 or probe 실패 시 자동으로 memory를 해제시켜 주는 특징이 있다.
d) 자세한 사항은 참고문헌 [4]를 참고하도록 하자.

  - dev: device나 module이 해제될 때에 자동으로 제거되는 장치에의 pointer
  - irq: IRQ number. platform device의 경우는 platform_get_irq() 함수를 통해 얻은 값을 사용하면 됨.
  - handler: IRQ handler 함수 pointer
  - irq_flags:  IRQ flag 값(아래 값을 사용할 수 있음)
          #define IRQF_TRIGGER_NONE   0x00000000
          #define IRQF_TRIGGER_RISING 0x00000001
          #define IRQF_TRIGGER_FALLING    0x00000002
          #define IRQF_TRIGGER_HIGH   0x00000004
          #define IRQF_TRIGGER_LOW    0x00000008
          #define IRQF_TRIGGER_MASK   (IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | \
                                                               IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)
          #define IRQF_TRIGGER_PROBE  0x00000010
  - devname: interrupt를 위해 사용하는 이름(/proc/interrupts로 확인 가능)
  - dev_id: device별 data structure를 가리키는 pointer를 전달하기 위해서 사용함.
-------------------------------------------------------------------------------------------------------------------

이상으로 기본적인 GPIO interrupt driver(platform driver)와 관련 device tree 작업을 진행해 보았다.


3. Bottom Halves and Deferring Work
이제부터는 2장에서 작성한 platform driver의 내용을 조금씩 변경해 가면서, kernel timer, tasklet, threaded interrupt, work queue, kernel thread 등에 관하여 살펴 보도록 하자. Kernel이 upgrade되면서 많은 부분이 달라졌다. 따라서 예전에 정리해 둔 내용으로는 문제가 있다(하지만, 그림은 나름 쓸만하여 copy & paste해 보았다 😅).


[그림 3.1] Linux interrupt(top half)와 Bottom halves 개념도


[그림 3.2] Interrupt와 interrupt handler 개요도

본격적인 내용 설명에 앞서 (좀 오래된 문서이기는 하지만) 필자가 오래 전에 작성해 둔 참고 문헌 [3]을 반드시 훑어 볼 것을 권한다. 지면 & 시간 관계상 모든 것을 다시 설명을 할 수는 없으니 말이다. 😂

3.1) Kernel Timer
<개요>

[그림 3.3] kernel timer 개요도

[Tip] kernel timer API는 예전 버젼(3.x 이하, 시점은 정확하지 않음)과 많은 차이를 보인다. 특히 초기화 함수인 init_timer() 대신 timer_setup()를 사용해야 한다. 그 밖에도 많은 부분에 변화가 있다. 아래 예제 코드를 통해 직접 확인해 보기 바란다.

<Example code>


[그림 3.4] kernel timer 예제 코드

<실행 모습>
root@stm32mp1:~/workspace# insmod ./lab4_periodic_timers_alt.ko
  -> 구동해 보니, 두개의 kernel timer가 서로 다른 주기로 message를 출력함을 알 수 있다.

[그림 3.5] kernel timer 예제 실행 모습
[Tip] 편의 상, kernel timer example은 2장에서 소개한 platform driver랑은 무관하게 작성하였다.

3.2) Tasklet
<개요>

[그림 3.6] tasklet 개요도

<Example code>


[그림 3.7] tasklet 예제 코드

<실행 모습>
root@stm32mp1:~/workspace# insmod ./lab_platform_interrupt_tasklet.ko
  -> 구동 후, User1 button을 누르면 tasklet code(아래 그림 하단의 message 출력)가 정상 동작한다.

[그림 3.8] tasklet 예제 실행 모습

3.3) Workqueue
<개요>

[그림 3.9] workqueue 개요도

<Example code>


[그림 3.10] workqueue 예제 코드

<실행 모습>
root@stm32mp1:~/workspace# insmod ./lab_platform_interrupt_workqueue.ko
  -> 구동 후, User1 button을 누르면 workqueue code(아래 그림 하단의 message)가 정상 동작한다.

[그림 3.11] workqueue 예제 실행 모습

3.4) Threaded Interrupt
<개요>

[그림 3.12] threaded interrupt(threaded irq) 개요도

<Example code>



[그림 3.13] threaded interrupt 예제 코드

<실행 모습>
root@stm32mp1:~/workspace# insmod ./lab_platform_interrupt_threaded_irq.ko
  -> 구동 후, User1 button을 누르면 irq thread routine(아래 그림 하단의 message)이 정상 동작한다.

[그림 3.14] threaded interrupt 예제 실행 모습

3.5) Kernel Thread
<개요>

[그림 3.15] kernel thread 개요도

<Example code>



[그림 3.16] kernel thread 예제 코드

<실행 모습>
root@stm32mp1:~/workspace# insmod ./lab_platform_interrupt_thread.ko
[24771.478353] int-button my_button_interrupt: The irq number is 68.
[24771.483115] int-button my_button_interrupt: platform_get_irq(): 68
[24771.489646] int-button my_button_interrupt: Successfully loading ISR handler.
  -> 구동 후, User1 button을 누르면 thread routine(아래 그림 하단의 message)이 정상 동작한다.

[그림 3.17] kernel thread 예제 실행 모습


이상으로 kernel 4.19.94 환경에서 GPIO interrupt & bottom halves 관련 platform driver & device tree 작성에 관하여 소개해 보았다. 대부분 간단한(?) 수준의 코드(하지만, 누군가에게는 꼭 필요한 코드)이므로 이해하는데는 크게 무리가 없었을 것으로 믿는다. 😋

May the source be with you~ 

[2] Linux Device Drivers Development, John Madieu, Packt
[Tip] 책 내용 중에 몇가지 오류가 있기는 하나, 나름 최신 kernel 내용을 언급하고 있는 (아주) 좋은 책이다. 근데, 왜 Amazon은 Packt(늘 느끼는 거지만 출판 상태가 별로 좋지 못함) 책을 열심히 밀고 있는 걸까 ?






2020년 7월 1일 수요일

ARM Bare-Metal Programming - 1편

이번 blog post에서는 ARM(32bit) 용 bare metal programming(1편)에 관한 내용을 소개하고자 한다. Bare metal programming을 위한 target board로는 QEMU(processor emulator/가상 환경)을 활용(추후 STM32F103 Nucleo-64 보드를 대상으로 할 예정)하기로 하겠다.


<목차>
1. 개발 환경 준비
   - ARM GCC toolchain 설치
   - QEMU 설치
   - 간단한 예제 돌려 보기
2. Assembly Programming 기초
3. Linker & Linker Script
4. 리셋 벡터, Exception Handling 그리고 C code 진입하기
5. References


"Bringing up an ARM embedded system from scratch"


1. 개발 환경 준비
이 장에서는 bare metal programming을 위한 기본 단계로, ARM cross toolchain과 QEMU(processor emulator)를 설치한 후, (맛뵈기 차원에서) 간단한 assembly program를 하나 만들어 돌려 보도록 하겠다. 아래에서 설명하는 모든 작업은 Ubuntu 18.04 desktop에서 실시하였다.

sudo apt install gcc-arm-none-eabi
[Tip] ARM용 raw 실행 파일을 만들기 위해서는 arm-none-eabi-gcc가 설치되어야 한다.

arm-none-eabi-gcc --version
arm-none-eabi-gcc (15:6.3.1+svn253039-1build1) 6.3.1 20170620
Copyright (C) 2016 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

sudo apt install qemu-system-arm

qemu-system-arm --version
QEMU emulator version 2.11.1(Debian 1:2.11+dfsg-1ubuntu7.28)
Copyright (c) 2003-2017 Fabrice Bellard and the QEMU Project developers

qemu-system-arm -M ?
  => qemu가 지원하는 machine의 목록을 출력해 준다. 이 중 적당한 machine을 하나 선택하도록 하자.

[그림 1.1] QEMU에서 지원하는 machine 종류 출력

다음으로 (아래 site 내용을 참조하여) arm-none-eabi-gdb를 설치해 보자.
https://acassis.wordpress.com/2018/12/27/adding-arm-none-eabi-gdb-to-ubuntu-18-04/

$ sudo dpkg -i ./libreadline6_6.3-8ubuntu2_amd64.deb 
Selecting previously unselected package libreadline6:amd64.
(데이터베이스 읽는중 ...현재 318299개의 파일과 디렉터리가 설치되어 있습니다.)
Preparing to unpack .../libreadline6_6.3-8ubuntu2_amd64.deb ...
Unpacking libreadline6:amd64 (6.3-8ubuntu2) ...
libreadline6:amd64 (6.3-8ubuntu2) 설정하는 중입니다 ...
Processing triggers for libc-bin (2.27-3ubuntu1) ...

$ sudo dpkg -i ./gdb-arm-none-eabi_7.10-1ubuntu3+9_amd64.deb 
Selecting previously unselected package gdb-arm-none-eabi.
(데이터베이스 읽는중 ...현재 318311개의 파일과 디렉터리가 설치되어 있습니다.)
Preparing to unpack .../gdb-arm-none-eabi_7.10-1ubuntu3+9_amd64.deb ...
Unpacking gdb-arm-none-eabi (7.10-1ubuntu3+9) ...
gdb-arm-none-eabi (7.10-1ubuntu3+9) 설정하는 중입니다 ...
Processing triggers for man-db (2.8.3-2ubuntu0.1) ...

[Tip] 위의 방식이 마음에 안들면 arm 개발 site에서 보다 최신의 arm toolchain을 내려 받아 설치하는 방법도 있다. 자세한 사항은 필자의 아래 blog 2장 a 절 내용을 참조하기 바란다.

Toolchain과 QEMU가 준비되었으니, 간단한 assembly program(덧셈 예제)을 하나 만들어 돌려 보도록 하자.

$ vi add.S
  => assembly code는 확장자로 .S 혹은 .s를 사용한다.
  => 아래 코드는 r0 register에 숫자 5를 r1 register에 숫자 4를 대입하고, 둘을 더해 r2 register에 저장한 후, b stop 명령을 계속 실행하면서 무한 loop에 빠지는 program이다.
  => .text는 assembler directive라고 하며, 이하의 명령이 code section에 포함됨을 알려 준다. 아래 코드에는 없으나 변수 영역(data section)을 표시하는 .data라는 directive도 있다.

[그림 1.2] add.S 파일

<여기서 잠깐 ~>
assembly code는 보통 "label:      instruction    @ comment" 형태로 구성된다.

label    메모리 상에서의 명령어(instruction)의 위치(번지)를 표시함.
instruction    ARM instruction(32 bit or 16 bit) 이나 assembler directive가 올 수 있다. assembler directive는 assembler에게 알려주는 명령에 해당하며 반드시 .(점)으로 시작한다.
@comment    글자그대로 주석이다.
------

arm-none-eabi-as -o add.o add.S
  => assembly code compile(이 경우는 compile 대신 assemble 한다고 함)은 as(assembler)를 이용한다.
  => 결과 파일은 add.o 임.

$ arm-none-eabi-ld -Ttext=0x0 -o add.elf add.o
  => 실행 가능한 파일을 생성하기 위해서는 ld(linker)를 사용해야 한다.
  => -Ttext=0x0 은 label(위의 경우는 start)의 주소를 지정하는 역할을 한다. 즉 start label은 0x0 번지에서 시작한다는 뜻이다.
   => 위 명령 실행 결과, add.elf라는 실행 파일이 만들어지게 된다.

[그림 1.3] arm-none-eabi-nm add.elf 실행 모습
[Tip] ELF(Executable and Linkable Format)실행, 오브젝트 파일, 공유 라이브러리, 또는 코어 덤프를 할 수 있게 하는 바이너리 파일(UNIX/Linux 실행 파일 format)을 말한다.

[그림 1.4] ELF 파일 형식
--------

qemu-system-arm -M realview-pb-a8 -kernel ./add.elf -S -gdb tcp::1234,ipv4
  => qemu(processor emulator)를 돌려 보자.
  => machine으로 realview-pb-a8를 선택하자.
  => -kernel option으로 add.elf를 지정하자.
  => -S option은 QEMU가 시작하자마자 일시 정지(suspend) 시키는 역할을 한다.
  => -gdb tcp::1234,ipv4는 외부에서 gdb로 연결(tcp 1234 번 port로 연결)해 올 수 있도록 해준다(내부에 gdbserver가 포함되어 있음)
[Tip] machine type은 다른 것을 선택해도 무방하나, 참고문헌 [2]를 기초로 realview-pb-a8을 일단 선택해 보았다.
[Tip] tcp port 1234는 다른 값으로 지정해도 된다.

[그림 1.5] qemu-system-arm 실행 모습

$ arm-none-eabi-gdb
  => 다른 터미널 창에서 arm 용 gdb를 실행하자.

arm-none-eabi-gdb <=======> gdbserver @ QEMU(add.elf 실행 대기 중)

(gdb) target remote:1234
  => 앞서 실행한 qemu 내의 gdbserver와 연결한다.
(gdb) c
  => 코드(add.elf)를 실행한다.
  => add.elf는 무한 loop에 빠지도록 되어 있으므로 Ctrl-C로 강제 중단하자.
(gdb) info registers
  => 현재 register 정보를 확인해 본다.
  => r0, r1, r2 register 내용을 보면, 각각 0x5, 0x4, 0x9로 add.S code에서 의도한 대로 내용이 저장되어 있다.
(gdb) x/4x 0
  => 0x00000000 메모리 번지 부터 4개의 item(4byte씩 4개)를 출력한다.

[그림 1.6] qemu-system-arm 실행 모습

(gdb) x/4iw 0x0
  => i option을 사용하면 memory에 올라와 있는 instruction을 disassemble할 수도 있다.
   0x0: mov r0, #5
   0x4: mov r1, #4
   0x8: add r2, r1, r0
=> 0xc: b 0xc


이상으로 간단한 arm toolchain & QEMU 설치 후, assembly program을 하나 작성하고 QEMU 상에서 돌려보는 과정을 소개해 보았다. 이어지는 장에서는 assembly program 작성 방법을 좀 더 깊이있게 소개(2장) 보고, linker와 linker script의 역할(3장)에 대해 소개해 보도록 하겠다.


2. Assembly Programming (정말) 기초
이번 장에서는 2개의 assembly program 예제를 가지고, Assembly Programming의 가장 기초적인 부분을 짚고 넘어가 보기로 하겠다.

a) Array의 합을 계산하는 program
코드를 바로 제시하면 다음과 같다. 우측의 주석이 잘 정리되어 있으므로 코드 흐름을 이해하는 것은 그리 어렵지 않다. 아래 program을 실행하고 나면 최종적으로 r3 register에 55가 저장되어야 한다.

[그림 2.1] Array의 합을 계산하는 program - sum-data.S

Program build에 앞서 .byte.align assembler directive의 의미를 먼저 알아보도록 하자.

<.byte directive>
1 byte(8 bits) 크기의 값들을 메모리에 연속으로 배치하고자 할 때 사용한다(byte array로 이해하면 됨). 이와 유사한 표현으로 .2byte, .4byte 등이 있는데, 각각이 의미하는 바는 각각 16 bits, 32bits array로 이해하면 된다. .byte 뒤에 기술하는 argument의 형식으로는 다음과 같은 것들이 올 수 있다.

pattern:  .byte 0b01010101, 0b00110011, 0b00001111
npattern: .byte npattern - pattern
halpha:   .byte 'A', 'B', 'C', 'D', 'E', 'F'
dummy:    .4byte 0xDEADBEEF
nalpha:   .byte 'Z' - 'A' + 1

<.align directive>
.align directive는 4 bytes(32bits) 단위로 ARM 명령어를 배치하기 위해 사용된다. 이는 instruction만 죽 나열되어 있는 상황에서는 불필요하며, instruction 중간 중간에 data bytes나 half words(16 bites)가 선언되어 있을 경우에만 의미가 있다. 4bytes 단위로 align하기 위해 필요한 경우에 padding bytes가 삽입되게 된다.

이제 code를 실행해 보자.
$ vi sum.ld
  => 먼저, 아래와 같이 간단한 linker script(의미는 3장에서 자세히 설명)를 하나 만든다.

[그림 2.2] 간단한 linker script - sum.ld

arm-none-eabi-as -o sum-data.o sum-data.S
  => assemble하여 sum-data.o object 파일을 생성한다.

arm-none-eabi-ld -T sum.ld -o sum-data.axf ./sum-data.o
  => ld 명령으로 sum-data.axf(ELF/DWARF 파일)을 생성해 낸다.
  => -T option은 linker script를 지정할 때 사용한다.

$ qemu-system-arm -M realview-pb-a8 -kernel ./sum-data.axf -S -gdb tcp::1234,ipv4
  => qemu를 사용하여 실행 파일을 구동시킨다.

다른 창(terminal)에서 arm-none-eabi-gdb를 실행해 보면 다음과 같다. 반독되는 내용이므로 구체적인 설명은 생략한다. 다만, (info register 명령 실행 결과) r3 register에 55가 저장된 것은 눈여겨 볼 부분이다.

[그림 2.3] sum-data.axf에 대해 arm-none-eabi-gdb 실행 모습

b) String의 길이를 구하는 program
역시 코드를 바로 살펴 보도록 하자. 코드 우측의 주석을 보면 알 수 있듯이 "Hello World" string의 각 byte를 하나씩 체크하면서 nul(= 0)이 나올때까지 반복하는 program이다. 물론 program의 이름에 걸맞게  loop를 돌면서 1씩 값을 증가시키고 이를 r1 register에 저장한다. Program 끝 부분에서 r1 -=1을  하는 이유는 맨 마지막 문자(nul 값)을 제외시켜야 하기 때문이다. 따라서 최종적으로 r1 register의 값이 11이면 정답이다.

[그림 2.4] string length를 계산하는 program - strlen.S

이 program에서는 두가지의 assembler directive 즉,  .asciz.equ가 눈에 띈다.

<.asciz directive>
.asciz는 string literal을 정의하기 위해 사용된다. 이와 비슷한 것으로 .ascii가 있는데, 둘간의 차이는 string 맨 끝에 null charactor(= 0)가 있고 없고이다. 즉, 이름에서 유추할 수 있듯이 .asciz는 string 맨 끝에 0이 추가되고, .ascii는 추가되지 않는다.

<.equ directive>
Assembler는 symbol table이라는 것을 관리하는데, .equ는 symbol table에 항목을 추가할 때 사용한다. 즉, 위의 예에서 ".equ  nul, 0"이 하는 일은 symbol table에 nul이라는 이름을 추가하고, 그 값이 0이라는 것을 알려준다.

그럼, 이제부터 코드를 돌려 보도록 하자. 앞서와 반복되는 내용이니 같은 설명을 반복하지는 않겠다.
$ arm-none-eabi-as -o strlen.o strlen.S
$ arm-none-eabi-ld -T strlen.ld -o strlen.axf strlen.o
$ qemu-system-arm -M realview-pb-a8 -kernel ./strlen.axf -gdb tcp::5678,ipv4
  => 이번에는 gdbserver port를 5678로 변경해 보았다.
$ arm-none-eabi-gdb
  => r1 register 값이 0xb(= 11)로 제대로 들어갔음을 알 수 있다.

[그림 2.5] string length를 계산하는 program 실행 모습

이상으로 2개의 assembly code를 예로 들어, arm assembly programming의 기초 사항을 정리해 보았다. 다음 장에서는 실행 program 생성에 중대한 역할을 하는 linker에 대해 알아 보도록 하자.

<여기서 잠깐 !>
  => ARM Assembly Programming에 대해 보다 심도있는 내용을 원하는 분은 아래 site를 참조하기 바란다.

-------------


3. Linker & Linker Script
이번 장에서는 linker와 linker script에 관해서 소개하고자 한다.

a) Linker가 하는일
먼저 linker는 아래 그림에서와 같이 여러개의 object 파일을 하나로 묶어 실행 가능한 파일(ELF)을 생성하는 역할을 한다. 이 과정에서 symbol resolution(A)relocation(B)이라는 두가지 일이 진행되게 된다.
[Tip] ld는 link editor or loader를 의미한다.

[그림 3.1] linker의 역할

Linker의 역할을 구체적으로 설명하기 위해 main.S와 sub.S로 구성된 예를 하나 들어 보자. 이 두개의 program이 하는 일은 아주 간단하다. 즉, main.S에 정의되어 있는 byte array(10, 20, 25)의 값을 더해서 그 결과를 r3 register에 저장하는 것이다. 단, 더하는 routine은 main.S가 아니라 sub.S에 정의되어 있어, main.S에서 sub.S의 (일종의)함수를 호출하는 코드라고 볼 수 있다.

[그림 3.2] main.S 코드

[그림 3.3] sub.S 코드

<.global directive>
Assembly 언어에서 label은 모두 외부에서 사용 불가하도록 되어있다(C 언어로 생각해 본다면 static 함수 선언로 선언된 경우는 외부에서 호출 불가하다는 내용과 유사하다고 볼 수 있음). 따라서 이를 외부에서 사용 가능하게 하려면 해당 label을 .global로 선언해 주어야 한다. 위의 예에서는 ".global sum" 이라고 선언되어 있으므로, main.S에서 해당 label을 사용(호출)할 수 있는 것이다.

이 상태에서 두개의 파일을 build(assemble)한 후, nm 명령으로 symbol table의 상태를 확인해 보도록 하자.

arm-none-eabi-as -o main.o main.S
arm-none-eabi-as -o sub.o sub.S

[그림 3.4] nm 명령으로 main.o와 sub.o의 symbol table 확인

위의  nm 명령 결과 중, t는 symbol이 정의되어 있음을 뜻하고, u는 정의되어 있지 않음을 뜻한다. 또한 TU 처럼 대문자로 표기된 경우는 해당 symbol이 .global로 선언되어 있을 때이다. 내용을 보니 main.o에서 sum label은 역시나 U로 표기되어 있다.

그렇다면, 위의 두 파일을 linker로 연결해 보면 어떻게 될까 ?
$ arm-none-eabi-ld -Ttext=0x0 -o sum.axf main.o sub.o

arm-none-eabi-nm sum.axf
  => 얘상대로  sum 앞에 T가 보인다.

[그림 3.5] nm 명령으로 sum.axf의 symbol table 확인

지금까지 설명한 과정을 symbol resolution(A)이라고 한다.

그렇다면 relocation(B)이란 무엇일까 ?
아래와 같이 -Ttext option을 주어 text section의 시작을 0x0 => 0x100으로 변경한 상태에서 linking을 해보면, 아래와 같이 모든 symbol의 주소(예: 00000120 T sum)가 그에 맞게 조절됨을 알 수 있는데, 이것이 relocation이다.

[그림 3.6] nm 명령으로 sum.axf의 symbol table 확인
[Tip] 내용이 너무 장황해 질 듯하여, 아주 간단하게 설명하였으나, relocation을 위해서는 section merging(서로 다른 파일에 흩어져 있는 동일 section을 하나로 통합)과 section placement(앞서 설명한 시작 주소 변경에 따른 위치 조정)라는 과정이 동반된다.

b) Linker script
앞서 언급한 대로 linker는 section merging과 section placement 절차를 수행한다. 이때 어는 section이 merge(통합)되고, 어느 위치가 메모리에 재배치되는지에 관해서는 linker script라는 파일을 통해 세세히 기술해 줄 수가 있다. 즉, linker는 linker script의 내용을 토대로 section merging & placement 과정을 진행하게 된다.


[그림 3.7] linker script 구조 설명(1)

❶ SECTIONS 명령은 section이 어떻게 merge되고 어느 위치에 배치되는지를 기술한다.
❷ . = 0x00000000 은 location counter를 의미하며 첫번째 section(여기서는 text section)이 메모리 주소 0x00000000에 위치한다는 것을 알려 준다.
❸ ❹ 입력 파일인 abc.o와 def.o에 있는 text section은 output file의 text section으로 들어감을 의미한다.

위의 linker script는 아래와 같이 *를 사용하여 단순화해서 표현할 수도 있다(보통 이렇게 많이 쓴다).

[그림 3.8] linker script 구조 설명(2)

아래 예는 .text section과 .data section을 모두 포함한 경우의 linker script 예제이다. .text section은 종전과 같이 0x0에 위치하고 있지만, .data section은 0x400 부터 배치됨을 보여준다.

[그림 3.9] linker script 구조 설명(3)

위의 linker script를 사용하는 예제 program 하나 테스트해 보자. 앞서 [그림 2.1]과 동일한 예제코드이다.

[그림 3.10] Array의 합을 계산하는 program - sample2.S

$ arm-none-eabi-as -o sample2.o sample2.S
$ arm-none-eabi-ld -T sample2.ld -o sample2.axf sample2.o

nm 명령으로 symbol table 내용을 확인해 보면, text section과 data section의 memory 주소가 linker script에 지정한 값대로 출력됨을 알 수 있다.

[그림 3.11] arm-none-eabi-nm -n ./sample2.axf 실행 결과

이상으로 linker와 linker script의 역할에 대해 간략히 살펴 보았다. 다음 장에서는 지금까지 설명한 내용을 기초로 본격적인 bare metal programming에 관한 설명에 돌입하고자 한다.


4. 리셋 벡터, Exception Handling 그리고 C Code 진입하기
지금까지 1 ~ 3장을 통해 bare metal programming을 위한 준비 사항을 정리해 보았다. 그렇다면 이제 부터는 본격적인 bare metal programming에 들어가 보도록 하자. 이절에서 소개하는 내용은 아래 책을 기초로 하였음을 밝힌다. 

[그림 4.1] 임베디드 OS 개발 프로젝트 책(인사이트 이만우 저)

a) Reset vector와 Exception vector
ARM core에 전원이 인가되면 ARM core가 가장 먼저 하는 일은 Reset vector(0x00000000 번지)를 실행하는 것이다.

[그림 4.2] ARMv7 Processor Mode [참고문헌 5]


[그림 4.3] ARM Register Sets [참고문헌 5]


[그림 4.4] ARM Current Program Status Register [참고문헌 5]

[그림 4.5] ARM Vector Table [참고문헌 5]


[그림 4.6] ARM Exception Handing [참고문헌 4]


[그림 4.7] ARM Memory Layout [참고문헌 4]


b) 메모리 map 구성 및 스택 만들고, C main 함수 호출하기

[그림 4.8] Entry.S 파일 - reset handler 수행 후, C main( )  함수 호출
 

이번 장은 (시간이 없어) 정리를 하다가 중간에 중단을 하였다. 보다 상세한 사항은 참고 문헌 [2]를 참조해 주시기 바라며, 추후 다시 정리할 수 있는 기회가 있기를 희망해 본다. 😓


5. References
[1] http://www.bravegnu.org/gnu-eprog/index.html
  => 1~ 3장의 기초 자료로 사용함.

[2] 임베디드 OS 개발 프로젝트, 이만우, 인사이트
  => 4장의 기초 자료로 사용함.
  => RTOS의 동작 원리 파악에 도움이 되는 Good book

[3] Embedded programming with Android: Bringing Up and Android System from Scratch, Roger YE, Addison-Wesley

[4] ARM System Developer’s Guide - Designing and Optimizing System Software, Andrew N. Sloss, Dominic Symes, Chris Wright, Morgan Kaufmann Publishers
   => ARM architecture 소개(too old)

   => ARMv7 architecture 소개

  => ARM  Assembly Programming 상세 소개




Slowboot