임베디드를 좋아하는 조금 특이한 개발자?

[RaspberryPI4] Bare metal에서 C언어로 GPIO 제어 본문

Embedded/Raspberry PI

[RaspberryPI4] Bare metal에서 C언어로 GPIO 제어

Gordon_ 2025. 7. 16. 10:20

- 개발 환경

개발 보드 : Raspberrypi 4

WSL2 (Ubuntu 22.04 LTS)

toolchain : aarch64-linux-gnu-gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0


- 선행 포스트

https://littlebitodd-developer.tistory.com/60

 

[RaspberryPI4] Bare metal에서 Assembly 언어로 GPIO 제어

- 개발 환경개발 보드 : Raspberrypi 4WSL2 (Ubuntu 22.04 LTS)toolchain : aarch64-linux-gnu-gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0- 참고 자료- 라즈베리파이 보드에 대한 설명 및 부팅 순서에 대한 소개https://www.raspberrypi.

littlebitodd-developer.tistory.com

GPIO 와 관련된 레지트터에 대한 설명 및 링커 스크립드(Linker.ld)에 대한 설명을 위 포스터에서 미리 확인하는 것이 좋습니다.

 

- 예제 코드

https://github.com/MainForm/RaspberryPI4_Baremetal_Firmware/tree/d8dfb4b5e59aff33c4dda792a23c59022ea7599e

 

GitHub - MainForm/RaspberryPI4_Baremetal_Firmware

Contribute to MainForm/RaspberryPI4_Baremetal_Firmware development by creating an account on GitHub.

github.com


1. 서론

  지난 포스터에서 어셈블리 언어로 GPIO를 제어하여 21번 Pin에 연결된 LED를 껏다 켰다 하는 간단한 기능을 하는 프로그램을 개발하였습니다. 하지만 어셈블리 언어로 개발하는 것은 개발 속도를 늦추며 코드를 깔끔하게 정리하기 힘들다는 단점이 있습니다. 이러한 단점을 보안하고 우리에게 익숙한 C언어로 개발하는 방법을 이번 포스트에서 알아보도록 하겠습니다.

 

2. 스택(Stack) 섹션 추가하기

  어셈블리 언어에서도 스택 레지스터를 사용하긴 하지만 GPIO와 같은 간단한 기능을 제어할 때는 사용하지 않았습니다. 하지만, C언어에서는 스택 메모리를 좀더 적극적으로 사용합니다. 지역 변수, 함수의 매개변수, 함수 호출시 복구 주소의 저장등 많은 용도에 사용합니다. 심지어 간단한 프로그램에서 조차도 main 함수에 진입하기 위해 스택 메모리를 사용합니다. 그렇기에 우리는 C언어를 사용하기 전 미리 스택 메모리를 확보하고 스택 레지스터를 초기화 하여야 합니다.

  그리고 프로그램의 메모리 구조를 구성하기 위해 링커 스크립트를 작성해야 합니다. 해당 링크 스크립트는 이전 포스트에서 소개한 링커 스크립트와 크게 다르지 않지만 BSS 섹션 뒤에 스택 섹션을 추가할 것입니다. 스택 메모리는 64Kb로 설정할 예정입니다. 단순히 GPIO을 제어하는 작은 프로그램은 64Kb만으로 충분할 것으로 예상됩니다.

  ARMv8-a 아키텍쳐에서 스택 메모리를 사용하기 위해서는 몇가지 조건이 있습니다. 해당 조건을 아래에 설명하도록 하겠습니다.

( https://cs140e.sergio.bz/docs/AArch64-Procedure-Call-Standard.pdf : 5.2.2 The Stack 참조 )

  • 높은 메모리 주소에서 낮은 메모리 주소로 이동(full-descending)
      __stack_top과 __stack_bottom 은 각각 스택 메모리의 범위를 나타내는 라벨입니다. 스택 메모리는 다른 메모리와 다르게 사용할 수록 메모리의 주소가 낮아지는 특징을 가지고 있습니다.

 

  • 스택 메모리의 주소는 16Byte의 배수로 설정
      ARMv8-a은 ARM 64bit 을 기반으로 하는 아키텍쳐 입니다. 그렇기에 해당 문서를 확인 해보면 스택의 주소가 16Byte(Quad-word) 배수로 설정해야한다고 적혀 있습니다.

 

- Linker.ld

ENTRY(_start)

SECTIONS
{
    . = 0x80000;

    .text : { 
        *(.text*) 
    }

    .rodata : { 
        *(.rodata*) 
    }

    .data : { 
        *(.data*) 
    }

    .bss : { 
        *(.bss*) *(COMMON) 
    }

    /* ALIGN(16)으로 스택 섹션의 시작 주소를 16Byte의 배수로 설정 */
    .stack : ALIGN(16) {
        /*  __stack_bottom :  스택의 마지막 주소 */
        __stack_bottom = .;
        /* stack의 크기 : 64Kb */
        . = . + (64K);
        /*  __stack_top :  스택의 시작 주소 */
        __stack_top = .;
    }
}

 

3. Main 함수을 호출 하기 위한 어셈블리 언어 작성

  이번에 작성할 어셈블리 언어의 목적은 C언어로 작성한 main함수를 호출 하는 것 그리고 C언어를 사용할 수 있도록 스택 메모리를 설정하는 것입니다. GPIO를 제어하는 대부분의 언어는 C언어로 작성할 것이므로 어셈블리 언어의 코드가 길지 않습니다.

 

- start.S

// Linker Script에서 사용하기 위해 .global로 _start 선언
.global _start

_start:
    // Linker Script에서 __stack_top의 값을 0번 레지스터에 저장
    LDR x0, =__stack_top
    // 0번 레지스터의 값(__stack_top)을 SP(Stack Pointer) 레지스터에 저장
    MOV sp, x0

    // main.c 에 작성한 main 함수로 분기
    BL  main

    B   .

 

4. C언어로 main 함수 작성

- main.c

// 32bit을 저장하기 위한 자료형 선언
typedef unsigned int uint32_t;

// BCM2711 GPIO 레지스터 주소
#define BCM2711_GPIO_BASE       (0xFE200000)
#define BCM2711_GPIO_GPFSEL2    (BCM2711_GPIO_BASE + 0x08)
#define BCM2711_GPIO_GPSET0     (BCM2711_GPIO_BASE + 0x1C)
#define BCM2711_GPIO_GPCLR0     (BCM2711_GPIO_BASE + 0x28)

// 레지스터 주소에 접근하기 위한 매크로
#define REG_32(SOURCE)          (*((volatile uint32_t*)SOURCE))

// 특정 시간동안 멈추기 위한 함수
void delay(volatile int val){
    while(val-- > 0);
}

int main(void){
    // GPFSEL2 레지스터에서 5~3번 레지스터를 0b001로 설정하여 21 Pin를 Output으로 설정
    REG_32(BCM2711_GPIO_GPFSEL2) |= 0x01 << 3;

    while(1){
        // GPSET0에서 21번 bit를 1로 설정, 21 Pin를 High로 출력
        REG_32(BCM2711_GPIO_GPSET0) |= (0x01 << 21);
        // 적당한 시간 동안 딜레이
        delay(0x200000);

        // GPCLR0에서 21번 bit를 1로 설정, 21 Pin를 Low로 출력
        REG_32(BCM2711_GPIO_GPCLR0) |= (0x01 << 21);
        delay(0x200000);
    }

    return 0;
}

 

중요한 코드에 대해서 설명하도록 하겠습니다.

 

4.1. GPIO 관련 레지스터 주소 설정

// BCM2711 GPIO 레지스터 주소
#define BCM2711_GPIO_BASE       (0xFE200000)
#define BCM2711_GPIO_GPFSEL2    (BCM2711_GPIO_BASE + 0x08)
#define BCM2711_GPIO_GPSET0     (BCM2711_GPIO_BASE + 0x1C)
#define BCM2711_GPIO_GPCLR0     (BCM2711_GPIO_BASE + 0x28)

 

GPIO의 Base 주소로 각 레지스터에 접근 하기 위해 offset 만큼 더하여 레지스터 주소를 설정하였습니다.

 

4.1. 레지스터 접근 메크로

// 레지스터 주소에 접근하기 위한 매크로
#define REG_32(SOURCE)          (*((volatile uint32_t*)SOURCE))

 

  위 코드는 레지스터에 접근하기 위해 사용되는 복잡한 형변환을 간단하게 접근하기 위한 매크로 입니다. 위 매크로를 좀더 간단히 풀어 쓴다음 다음과 같습니다.

// 접근하려는 주소를 uint32_t*로 형변환하여 레지스터 주소를 가리키고 있는 포인터 변수로 변환
uint32_t * source = (uint32_t *)BCM2711_GPIO_GPFSEL2;
// uint32_t* 포인터 변수 앞에 *를 붙여 해당 레지스터 주소의 값에 접근
*source = 0x08;

// 매크로를 사용한 더 간단한 코드
REG_32(BCM2711_GPIO_GPFSEL2) = 0x08;

 

volatile 키워드에 대해서 알아 보겠습니다.

  컴파일러는 개발자가 작성한 코드를 컴파일 하면서 필요 없다고 여겨지는 코드를 삭제하면서 프로그램을 최적화합니다. 하지만, 개발자의 의도와 맞지 않게 컴파일러가 제멋대로 최적화하는 경우가 있습니다. 특히, 임베디드 환경에서 레지스터를 접근하면서 이러한 상황이 많이 발생합니다. 

    uint32_t * source1 = (uint32_t *)BCM2711_GPIO_GPFSEL2;
    
    // 컴파일러의 관점에서 결국 *source의 값은 0x20이기에 위 2줄은 최적화로 인해 삭제
    *source1 = 0x08; // 컴파일러에 의해 삭제
    *source1 = 0x10; // 컴파일러에 의해 삭제
    *source1 = 0x20;
    
    volatile uint32_t * source2 = (volatile uint32_t *)BCM2711_GPIO_GPFSEL3;
    // volatile 키워드가 있으므로 아래 코드가 순차적으로 실행
    *source2 = 0x08;
    *source2 = 0x10;
    *source2 = 0x20;

 

  위와 같은 상황에서 컴파일러에 의해 0x08의 값과 0x10의 값을 *source에 저장하는 코드가 생략됩니다. 왜냐하면 결국 *source의 값은 컴파일러가 보기에 최종값인 0x20으로 한번 변경하면 결과론적으로 결과가 같기 때문입니다. 하지만, 임베디드에서는 주변기기를 사용하기 위해 해당 주변기기를 설정하는 레지스터를 여러번 순차적으로 접근 해야하는 경우가 발생합니다. 하지만, 컴파일러에 의해 코드가 생략되어 주변기기를 쓸수 없는 상황이 발생하는 것을 방지 하기 위해 해당 변수를 최적화 하지 말라고 컴파일러에게 알려주는 키워드 입니다.

5. 빌드 과정

aarch64-linux-gnu-gcc -c -g -ostart.os start.S
aarch64-linux-gnu-gcc -c -g -Wall -o main.o main.c
aarch64-linux-gnu-ld -T linker.ld -o kernel.elf start.os main.o
aarch64-linux-gnu-objcopy -O binary kernel.elf image

 

 

위 결과로 나온 image파일을 SD카드에 넣어 라즈베리파이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