일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- QEMU
- AVR
- platformio
- nucleo
- buildroot
- BeagleBone
- Arduino
- STM32
- Visual Studio
- vscode
- C++
- Debugging
- 디버깅
- Visual Studio Code
- atmel
- raspberrypi
- USART
- Debug
- Linux
- bare metal
- GPIO
- 아두이노
- UART
- 리눅스
- avr-gcc
- AArch64
- yocto
- esp32
- 라즈베리파이
- Raspberry
- Today
- Total
임베디드를 좋아하는 조금 특이한 개발자?
[RaspberryPI4] Bare metal에서 Assembly 언어로 GPIO 제어 본문
- 개발 환경
개발 보드 : Raspberrypi 4
WSL2 (Ubuntu 22.04 LTS)
toolchain : aarch64-linux-gnu-gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
- 참고 자료
- 라즈베리파이 보드에 대한 설명 및 부팅 순서에 대한 소개
https://www.raspberrypi.com/documentation/computers/raspberry-pi.html
- 라즈베리파이4에 사용된 프로세스에 대한 설명
https://www.raspberrypi.com/documentation/computers/processors.html#bcm2711
- 라즈베리파이을 위한 Linux kernel 빌드 방법 소개
https://www.raspberrypi.com/documentation/computers/linux_kernel.html
- BCM2711에 대한 Datasheet
https://datasheets.raspberrypi.com/bcm2711/bcm2711-peripherals.pdf
- 라즈베리파이3 bare metal로 GPIO 제어 방법 소개
https://www.youtube.com/watch?v=jN7Fm_4ovio
1. 서론
Raspberrypi은 Linux 기반의 많은 OS가 포팅되어 다양한 목적의 OS를 탑재할 수 있습니다. 하지만, 저는 AVR, STM32와 같이 OS 없이 firmware를 직접 개발하는 방식에 익숙 하기에 Linux에서 동작하는 임베디드 시스템은 익숙치 않습니다. 그렇기에 Raspberrypi에서 OS가 없는 환경에서 동작하는 Firmware를 개발하고 싶은 생각이 들어 도전하게되었습니다. 그리고 더 나아가 Linux가 아닌 저만의 커널을 개발하여 새로운 OS를 개발하는 것이 목표입니다.
2. Toolchain 설치
제가 개발하고 있는 환경의 컴퓨터는 x64 아키텍쳐를 기반으로 동작하고 있습니다. 하지만, RaspberryPI4는 64bit ARMv8 아키텍쳐를 가지고 있습니다. 그렇기 때문에 해당 하는 아키텍쳐에 맞는 Toolchain를 사용하여야 합니다. 그리고 그 Toolchain은 Raspberrypi에서 Linux Kernel를 빌드할 때 사용하는 Toolchain를 사용하도록 하겠습니다.
https://www.raspberrypi.com/documentation/computers/linux_kernel.html
Cross-compile the kernel 항목 참조
sudo apt install crossbuild-essential-arm64
3. Datasheet에서 GPIO 관련 내용 확인
BCM2711 Datasheet : https://datasheets.raspberrypi.com/bcm2711/bcm2711-peripherals.pdf
Datasheet에는 BCM2711에서 지원하는 Perpharal에 대한 설명 및 레지스터에 대해 적혀 있습니다. 여기서 우리가 중심적으로 봐야 하는 것은 GPIO를 사용하기 위해 필요한 레지스터와 해당 레지스터의 주소를 알아야합니다. 먼저 어떤 레지스터가 필요한지 확인 해보도록 하겠습니다.
먼저 우리는 21번 핀을 On, Off를 반복하며 Blink할 예제를 작성할 예정입니다. 그러므로 우리는 21번 핀을 Output으로 설정하는 레지스터와 21핀을 High로 변경하는 레지스터와 21번 핀을 Low로 변경하는 레지스터를 찾아야합니다. 아래에 각 레지스터에 대한 설명을 시작하겠습니다.
3.1. GPIO 레지스터 확인
3.1.1. GPFSEL2 레지스터
GPFSEL2 레지스터는 20~29 Pin에 대해서 기능 설정을 하는 레지스터입니다. 우리는 21번 핀을 Output으로 설정해야하므로 해당 레지스터의 5:3 Bit를 001로 설정해야합니다.
3.1.2. GPSET0 레지스터
GPSET0 레지스터는 출력으로 설정한 Pin을 High로 출력하기 위한 레지스터입니다. 우리는 21번 Pin를 High로 출력하기 위해 GPSET0의 21번 bit를 1로 설정하여야 합니다.
3.1.3. GPCLR0 레지스터
GPCLR0 레지스터는 GPSET0 레지스터의 반대입니다. 21번 Pin을 Low로 설정하기 위해 GPCLR0의 21번 bit를 1로 설정해야합니다.
3.2. GPIO 레지스터의 주소 확인
먼저 확인해야하는 것은 GPIO의 베이스 주소입니다. 그 이유는 GPIO을 담당하는 레지스터의 주소는 서로 맞붙어 있습니다. 그러므로 GPIO을 담당하는 레지스터의 베이스 주소(시작 주소)을 확인 한 다음 각 레지스터가 베이스 주소로 부터 얼만큼 떨어져 있는지를 확인하여 각 레지스터의 주소를 알 수 있습니다.
Datasheet를 확인해보니 GPIO의 베이스 주소가 0x7e200000이라고 적혀 있습니다. 하지만 해당 주소는 "Legacy Master view" 관점에서의 주소입니다. "Legacy Mater view"라는 것은 Raspberrypi 1,2,3에서 사용되었던 주소의 관점에서 보는 주소 입니다. 즉, 이전 보드 개발자들에서 보기 쉽게하기 위한 관점입니다. 하지만, RaspberryPI4에 대한 물리적인 주소를 알아야합니다.
그러므로 우리는 "Legacy Mater view"가 아닌 "ARM view"에서의 메모리 주소를 사용해야합니다.
위 내용을 확인해보면 Legacy Master Address를 ARM Address로 변경하는 내용이 적혀있습니다. 우리가 사용할 GPIO의 베이스 주소는 "Legacy Mater Address"에서 0x7E20 0000 입니다. 해당 주소를 "ARM Address"로 변경하면 0xFE20 0000 입니다. 이제 물리적인 베이스 주소를 알았으므로 나머지 GPFSEL2, GPSET0, GPCLR0에 대한 주소도 확인 할 수 있습니다.
GPFSEL2는 베이스 주소인 0xFE20 0000에서 0x08 떨어져 있으므로 0xFE20 0008, GPSET0는 0xFE20 001C, GPCLR0는 0xFE20 0028임을 확인 할 수 있습니다. 주소를 정리하면 아래와 같습니다.
- GPFSEL2 레지스터 : 0xFE20 0008
- GPSET0 레지스터 : 0xFE20 001C
- GPCLR0 레지스터 : 0xFE20 0028
4. 어셈블리 작성
GitHub - MainForm/RaspberryPI4_Baremetal_Firmware
Contribute to MainForm/RaspberryPI4_Baremetal_Firmware development by creating an account on GitHub.
github.com
위 Git repo에는 제가 작성한 예제가 있습니다. 앞으로 하는 실습에서 문제가 있다면 참고 부탁드립니다.
// 프로젝트 폴더 생성
mkdir -p rpi4_gpio_baremetal && cd rpi4_gpio_baremetal
// 어셈블리를 작성하기 위한 start.S 파일 생성
touch start.S
- start.S
// Linker Script에서 사용하기 위해 .global로 _start 선언
.global _start
// GPIO 관련 레지스터의 베이스 주소
.equ GPIO_BASE, 0xFE200000
.equ GPFSEL2, 0x08 // 0xFE200008 = 0x08 + 0xFE200000
.equ GPSET0, 0x1c // 0xFE20001c = 0x1c + 0xFE200000
.equ GPCLR0, 0x28 // 0xFE200028 = 0x28 + 0xFE200000
.equ GPIO_21_OUTPUT, 0x08 // 0b0000 1000 -> 5~3 bit : 001
.equ GPIOVAL, 0x200000 // 0x200000 == (1 << 21)
_start:
// GPIO_BASE(0xFE200000) 값을 0번 레지스터에 저장
ldr w0, =GPIO_BASE
// GPIO_21_OUTPUT(0x08) 값을 1번 레지스터에 저장
ldr w1, =GPIO_21_OUTPUT
// 1번 레지스터의 값(0x08)를 GPFSEL2(0xFE200008 == 0번 레지스터(0xFE200000) + GPFSEL2(0x08))에 저장
str w1, [x0, #GPFSEL2]
// 2번 레지스터에 0x800000을 저장
// 2번 레지스터을 통해 딜레이 시간을 조절
ldr w2, =0x800000
loop:
// 1번 레지스터에 GPIOVAL의 값(0x200000)를 저장
ldr w1, =GPIOVAL
// 1번 레지스터의 값(0x200000)를 GPSET0(0xFE20001c == 0번 레지스터(0xFE200000) + GPSET0(0x1c))에 저장
str w1, [x0, #GPSET0]
// 딜레이
// 10번 레지스터의 값을 0으로 설정
eor w10, w10, w10
delay1:
// 10번 레지스터의 값을 1증가
add w10, w10, #1
// 10번 레지스터와 2번 레지스터의 값이 같은지 비교
cmp w10, w2
// 만약 10번 레지스터와 2번 레지스터가 같지 않다면 delay1로 이동
bne delay1
// 1번 레지스터에 GPIOVAL의 값(0x200000)를 저장
ldr w1, =GPIOVAL
// 1번 레지스터의 값(0x200000)를 GPSET0(0xFE200028 == 0번 레지스터(0xFE200000) + GPCLR0(0x28))에 저장
str w1, [x0, #GPCLR0]
eor w10, w10, w10
delay2:
add w10, w10, #1
cmp w10, w2
bne delay2
// loop로 이동하여 loop를 무한 반복
b loop
각 코드에 주석을 달아 각 코드가 어떠한 동작을 하는지 적었습니다. 그래도, 한번 중요한 코드들에 대해서 살펴 보도록 하겠습니다.
4.1. GPIO 레지스터에 데이터 저장
// GPIO 관련 레지스터의 베이스 주소
.equ GPIO_BASE, 0xFE200000
.equ GPFSEL2, 0x08 // 0xFE200008 = 0x08 + 0xFE200000
.equ GPSET0, 0x1c // 0xFE20001c = 0x1c + 0xFE200000
.equ GPCLR0, 0x28 // 0xFE200028 = 0x28 + 0xFE200000
.equ GPIO_21_OUTPUT, 0x08 // 0b0000 1000 -> 5~3 bit : 001
.equ GPIOVAL, 0x200000 // 0x200000 == (1 << 21)
위 코드는 자주 사용하는 상수을 선언하였습니다. C언어에서 #define를 통해 상수를 선언하는 것과 같습니다.
// GPIO_BASE(0xFE200000) 값을 0번 레지스터에 저장
ldr w0, =GPIO_BASE
// GPIO_21_OUTPUT(0x08) 값을 1번 레지스터에 저장
ldr w1, =GPIO_21_OUTPUT
// 1번 레지스터의 값(0x08)를 GPFSEL2(0xFE200008 == 0번 레지스터(0xFE200000) + GPFSEL2(0x08))에 저장
str w1, [x0, #GPFSEL2]
- ldr w0, =GPIO_BASE
위 코드는 GPIO_BASE의 값을 0번 레지스터에 저장하는 명령어 입니다. 즉, 0번 레지스터에 0xFE200000값이 저장됩니다.
- ldr w1, =GPIO_21_OUTPUT
위 코드는 GPIO_21_OUTPUT 의 값을 1번 레지스터에 저장하는 명령어 입니다. 즉, 1번 레지스터에 0x08값이 저장됩니다.
- str w1, [x0, #GPFSEL2]
위 코드는 1번 레지스터에 있는 값(0x08)의 값을 GPFSEL2(0xFE200008)에 저장하는 코드입니다.
[x0, #GPFSEL2]의 의미는 0번 레지스터에 저장되어 있는 값(0xFE200000)과 GPFSEL2(0x08)을 더한 위치입니다. 즉, 0xFE200008의 주소를 가르키게 됩니다.
w와 x의 차이점은 w은 32bit을 의미하고, x는 64bit를 의미합니다. C언어에서 int와 long long의 차이와 비슷합니다. 여기서 w1를 사용한 이유는 GPFSEL2의 크기가 32bit이기 때문이며, x0를 사용한 이유는 ARMv8가 64bit이므로 기본적으로 주소는 64bit을 사용합니다.
4.2. 일정 시간동안 Delay
// 2번 레지스터에 0x800000을 저장
// 2번 레지스터을 통해 딜레이 시간을 조절
ldr w2, =0x800000
// 딜레이
// 10번 레지스터의 값을 0으로 설정
eor w10, w10, w10
delay1:
// 10번 레지스터의 값을 1증가
add w10, w10, #1
// 10번 레지스터와 2번 레지스터의 값이 같은지 비교
cmp w10, w2
// 만약 10번 레지스터와 2번 레지스터가 같지 않다면 delay1로 이동
bne delay1
위 코드는 간단히 10번 레지스터가 0x0부터 0x800000 까지 증가하는 동안 반복하는 코드입니다.
// start.S를 빌드하는 명령어
aarch64-linux-gnu-as -c -g -o start.o start.S
이제 위 명령어를 입력하여 start.S를 빌드합니다.
5. Linker 스크립트 작성
Linker 스크립트를 작성을 하는 가장 큰 이유는 라즈베리4가 부팅을 하고 나면 첫 명령어를 실행하는 주소에 맞추어 프로그램을 실행하기 위함입니다. 기본적으로 라즈베리파이4는 정상 부팅 후 0x0008 0000 부터 명령어를 실행하기 시작합니다. 그러므로 저희가 작성한 프로그램의 첫 명령어를 0x0008 0000에 위치하도록 설정하여야 합니다.
// linker 스크립트를 작성할 파일 생성
touch linker.ld
- Linker.ld
ENTRY(_start)
SECTIONS
{
. = 0x80000;
.text : { *(.text*) }
.rodata : { *(.rodata*) }
.data : { *(.data*) }
.bss : { *(.bss*) *(COMMON) }
}
- ENTRY(_start)
작성한 어셈블리 코드에서 처음 실행 할 라벨을 선언합니다. - . = 0x80000;
SECTION의 첫 주소을 0x80000로 설정합니다.
그리고 SECTION의 처음이 .text 섹션이므로 .text섹션의 첫 주소가 0x80000가 됩니다.
// linker.ld를 바탕으로 start.o파일로 kernel.elf를 생성
aarch64-linux-gnu-ld -T linker.ld -o kernel.elf start.o
5.1. _start의 시작 위치 확인
이제 실제로 우리가 작성한 코드가 0x80000에서 시작하는지 확인해보도록 해보겠습니다.
aarch64-linux-gnu-objdump -D kernel.elf
6. 최종 실행 파일 생성
이제 마지막 단계입니다. 마지막으로 라즈베리파이4에서 실행하기 위한 실행파일을 생성하도록 하겠습니다. 우리는 kernel.elf파일에서 binary 부분만을 출력하여 라즈베리파이4에 실행해야합니다.
// kernel.elf파일에서 binary 부분을 추출한 image 파일 생성
aarch64-linux-gnu-objcopy -O binary kernel.elf image
위 코드를 실행하면 최종적으로 image라는 파일이 생성됩니다. 해당 image파일이 드디어 라즈베리파이4에서 실행할 수 있는 파일입니다.
7. 후기
이제, 실제 image 파일을 라즈베리파이4에서 실행하는 방법은 내용이 방대하기에 다음 포스트에서 다루도록 하겠습니다.
https://littlebitodd-developer.tistory.com/61
[RaspberryPI4] Bare metal에서 개발한 image 실행
- 개발 환경개발 보드 : Raspberrypi 4WSL2 (Ubuntu 22.04 LTS)SDcard 64Gb1. 서론 리눅스가 아닌 Bare metal 환경에서 직접 개발한 image를 라즈베리파이에서 실행하기 위한 방법을 확인 할 것 입니다. 2. SD카드 파
littlebitodd-developer.tistory.com
이번 내용을 통해 라즈베리파이도 결국 AVR 및 STM32와 같은 MCU라는 것을 배웠습니다. 다만, 방법이 까다롭고 리눅스라는 운영체제를 통해 쉽게 다른 기능들을 사용할 수 있기에 굳이 bare metal로 개발하는 사람은 없을 것입니다. 하지만 bare metal로 개발함을 통해 실제 GPIO가 동작하는 방법을 확인하고 더 나아가 bare metal환경에서 C언어를 통한 GPIO 제어를 도전 해볼 수 있다고 생각합니다.
'Embedded > Raspberry PI' 카테고리의 다른 글
[RaspberryPI4] Bare metal에서 C언어로 GPIO 제어 (2) | 2025.07.16 |
---|---|
[RaspberryPI4] Bare metal에서 개발한 image 실행 (2) | 2025.07.12 |
[Raspberrypi4] BCM2711의 UART 입력 주파수(UARTCLK) 초기 설정값 (0) | 2025.07.10 |
이전 Raspberrypi OS 이미지 다운로드 (0) | 2025.04.05 |
RaspberryPI Assembly Language: 1. 기본 구조 (0) | 2022.09.09 |