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

[RaspberryPI4] Bare metal에서 UART통신 구현 본문

Embedded/Raspberry PI

[RaspberryPI4] Bare metal에서 UART통신 구현

Gordon_ 2025. 7. 18. 18:09

- 개발 환경

개발 보드 : Raspberrypi 4

WSL2 (Ubuntu 22.04 LTS)

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


- 선행 포스트

 UART를 사용하기 위한 레지스터 확인

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

 

[RaspberryPI4] Bare metal에서 UART통신을 위한 레지스터 확인

- 개발 환경개발 보드 : Raspberrypi 4WSL2 (Ubuntu 22.04 LTS)toolchain : aarch64-linux-gnu-gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.01. 서론 지금까지 C언어로 GPIO를 제어하는 간단한 예제까지 진행하였습니다. 하지만, GPIO

littlebitodd-developer.tistory.com

 

 Bare metal 에서 C언어로 GPIO 제어하기

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

 

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

- 개발 환경개발 보드 : Raspberrypi 4WSL2 (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 언어로 GPI

littlebitodd-developer.tistory.com

- 예제 코드

https://github.com/MainForm/RaspberryPI4_Baremetal_Firmware/tree/5d01ffc1b02c7c8d4a777b6e79f372aea1c585d5

 

GitHub - MainForm/RaspberryPI4_Baremetal_Firmware

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

github.com


1. 서론

  지금까지 Bare metal 환경에서 C언어로 GPIO를 제어하였으며 UART를 사용하기 위한 레지스터까지 확인하였습니다. 이제 직접 프로그래밍하여 UART를 사용해보도록 하겠습니다. 먼저, GPIO와 UART의 기능을 한번에 main.c의 파일에서 코딩하는 것은 main.c파일이 너무 커지므로 먼저 프로젝트를 정리하고, 그 다음 UART 초기화하는 함수를 작성한 다음 데이터 송신(Tx)기능을 통해 PC에 "Hello world"를 출력하는 예제를 해보도록 하겠습니다.

 

2. 프로젝트 정리

  먼저 지금까지 많은 기능들을 추가하면서 main.c 함수만으로 프로그래밍하기에 코드수가 많아졌습니다. 이제, 각 기능에 따라 파일로 나누도록 하겠습니다. 또한 너무 복잡하게 폴더를 구성하면 여기서 설명하는데 많은 내용을 적어야 하기에 폴더 구성은 간단히 하도록 하겠습니다. 만약 아래 프로젝트를 구성하는데 어려움이 있다면 Github을 참고해주시기 바랍니다.

 

각 폴더에 대한 설명을 드리도록하겠습니다.

2.1. firmware 폴더

  해당 폴더는 라즈베리파이4의 펌웨어를 실행시키기 위한 최소한의 파일이 들어있는 폴더입니다. 해당 폴더는 빌드로 생성되는 "image" 파일과 같이 SD카드에 넣어줘야하는 파일들이 들어있습니다. 자세한 내용은 아래의 포스트를 참고해주세요.

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

2.2. include 폴더

  include 폴더는 header 파일을 저장하는 폴더입니다. header 파일에는 레지스터의 주소에 대한 선언 및 각 소스파일에서 공유하는 함수를 선언하는 파일입니다.

2.3. src 폴더

  실제 실행되는 프로그램을 개발하기 위한 소스 파일을 저장하는 폴더입니다. 자세한 내용은 아래에서 설명하며 각 파일에서 프로그래밍 하도록 하겠습니다.

2.4. Makefile 파일

  복잡해진 프로젝트를 make 명령어를 통해 빌드 과정을 자동화 해주는 파일입니다.

2.5. Linker.ld 파일

  펌웨어에 대한 각 섹션을 구성하는 파일입니다.

 

3. UART를 사용하기 위한 프로그래밍

  모든 폴더에 대한 설명을 추가하기에는 내용이 너무 많아지므로 필요한 내용에 대해서만 설명하도록 하겠습니다. 만약 해당 파일에 대한 설명이 없다면 github에서 참고 해주시기 바랍니다.

3.1. bcm2711_peripheral.h

#ifndef _BCM2711_PERIPHERAL_H
#define _BCM2711_PERIPHERAL_H

// uint32_t를 사용하기 위한 헤더 선언
#include <stdint.h>

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

// BCM2711 GPIO register definitions
#define BCM2711_GPIO_BASE        (0xFE200000)
#define BCM2711_GPIO_GPFSEL0     ((volatile uint32_t *)(BCM2711_GPIO_BASE + 0x00))
#define BCM2711_GPIO_GPFSEL1     ((volatile uint32_t *)(BCM2711_GPIO_BASE + 0x04))
#define BCM2711_GPIO_GPFSEL2     ((volatile uint32_t *)(BCM2711_GPIO_BASE + 0x08))
#define BCM2711_GPIO_GPFSEL3     ((volatile uint32_t *)(BCM2711_GPIO_BASE + 0x0C))
#define BCM2711_GPIO_GPFSEL4     ((volatile uint32_t *)(BCM2711_GPIO_BASE + 0x10))
#define BCM2711_GPIO_GPFSEL5     ((volatile uint32_t *)(BCM2711_GPIO_BASE + 0x14))
#define BCM2711_GPIO_GPSET0      ((volatile uint32_t *)(BCM2711_GPIO_BASE + 0x1C))
#define BCM2711_GPIO_GPSET1      ((volatile uint32_t *)(BCM2711_GPIO_BASE + 0x20))
#define BCM2711_GPIO_GPCLR0      ((volatile uint32_t *)(BCM2711_GPIO_BASE + 0x28))
#define BCM2711_GPIO_GPCLR1      ((volatile uint32_t *)(BCM2711_GPIO_BASE + 0x2C))

// BCM2711 UART0 register definitions
#define BCM2711_UART0_BASE       (0xFE201000)
#define BCM2711_UART0_DR         ((volatile uint32_t *)(BCM2711_UART0_BASE + 0x00))
#define BCM2711_UART0_FR         ((volatile uint32_t *)(BCM2711_UART0_BASE + 0x18))
#define BCM2711_UART0_IBRD       ((volatile uint32_t *)(BCM2711_UART0_BASE + 0x24))
#define BCM2711_UART0_FBRD       ((volatile uint32_t *)(BCM2711_UART0_BASE + 0x28))
#define BCM2711_UART0_LCRH       ((volatile uint32_t *)(BCM2711_UART0_BASE + 0x2C))
#define BCM2711_UART0_CR         ((volatile uint32_t *)(BCM2711_UART0_BASE + 0x30))
#define BCM2711_UART0_IFLS       ((volatile uint32_t *)(BCM2711_UART0_BASE + 0x34))
#define BCM2711_UART0_IMSC       ((volatile uint32_t *)(BCM2711_UART0_BASE + 0x38))
#define BCM2711_UART0_MIS        ((volatile uint32_t *)(BCM2711_UART0_BASE + 0x40))
#define BCM2711_UART0_ICR        ((volatile uint32_t *)(BCM2711_UART0_BASE + 0x44))

#endif

 

  "bcm2711_peripheral.h"은 각 레지스터의 주소를 선언과 레지스터에 접근하기 위한 "REG_32" 메크로를 선언를 하는 header 파일입니다. 

 

위 내용에 따르면 UART0의 Base 주소는 0x7E20 1000 로 보이지만 실제는 그렇지 않습니다. 해당 주소는 "Legacy Master view" 에 해당하는 주소이며 실제로는 "ARM view"에 해당하는 주소를 사용해야합니다. 

 

위 내용에 따르면 "Legacy Master view"의 주소를 "ARM view"의 주소로 변경하려면 앞자리 7E 를 FE로 변경하라고 적혀있습니다. 

 더 자세한 내용은 아래 포스트의 "3.2. GPIO 레지스터의 주소 확인"를 참고해주시 바랍니다.

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

 

 또한 각 레지스터의 Offset은 "BCM2711 ARM Peripherals"의 "Table 172. UART Registers"를 참고하여 작성하였습니다.

 

3.2. uart.h

#ifndef __BCM2711_UART_H__
#define __BCM2711_UART_H__

#include <stdint.h>

// Default UART0 clock
#define UART0_CLK               (48000000)

// DR(Data Register) Register bits
#define UART_DATA               (0xFF)

// CR(Control Register) Register bits
#define UART_RXE                (0x01 << 9)
#define UART_TXE                (0x01 << 8)
#define UART_UARTEN             (0x01)

// FR(Flag Register) Register bits
#define UART_TXFF               (0x01 << 5)
#define UART_RXFE               (0x01 << 4)

// LCRH(Line Control Register) Register bits
#define UART_WLEN_8BIT          (0x03 << 5)

void UART_Initialize();
void UART_SendWord(uint8_t data);

#endif

 

"uart.h" 파일은  UART의 각 레지스터에 대한 bit에 대한 선언과 "main.c"에서 호출 할 함수를 선언하였습니다.

 

3.3. uart.c

#include "bcm2711_peripheral.h"
#include "gpio.h"
#include "uart.h"

void UART_Initialize(){
    // 14 pin 과 15 pin를 UART의 Tx와 Rx로 사용하기 위한 GPIO 설정
    GPIO_SelectFunction(14,GPIO_FUNC_ALT0);
    GPIO_SelectFunction(15,GPIO_FUNC_ALT0);

    // UART설정을 위해 UART을 비활성화
    REG_32(BCM2711_UART0_CR) = 0x0;

    // Baudrate를 115200으로 설정
    REG_32(BCM2711_UART0_IBRD) = 26;
    REG_32(BCM2711_UART0_FBRD) = 3;

    // Parity bit 사용 안함, Stop bit는 1개, Word 길이는 8bit으로 설정
    REG_32(BCM2711_UART0_LCRH) = UART_WLEN_8BIT;

    // UART의 송신 수신 활성화 및 UART 통신 활성화
    REG_32(BCM2711_UART0_CR) = UART_TXE | UART_RXE | UART_UARTEN;
}

void UART_SendWord(uint8_t data){
	// UART의 송신 버퍼에 송신할 데이터을 넣을 메모리가 없다면 무한 반복
    while(REG_32(BCM2711_UART0_FR) & UART_TXFF);
	
    // DR에 레지스터의 DATA bits에 데이터 비트를 저장함으로써 UART 송신
    REG_32(BCM2711_UART0_DR) = data;
}

 

 

3.3.1. UART_Initialize 함수 (UART 초기화)

UART 초기화 프로세스

3.3.1.1. Tx, Rx 에 대한 GPIO 설정

    // 14 pin 과 15 pin를 UART의 Tx와 Rx로 사용하기 위한 GPIO 설정
    GPIO_SelectFunction(14,GPIO_FUNC_ALT0);
    GPIO_SelectFunction(15,GPIO_FUNC_ALT0);

   "BCM2711 ARM Peripherals"의 "Table 171. UART Assignment on the GPIO Pin map"를 확인하면 UART의 Rx,Tx으로 사용하기 위한 Pin을 어떻게 설정해야할지 언급되어 있습니다.

 

  그러므로 14 Pin과 15 Pin를 ALT0로 설정하여야 Tx, Rx로 설정 할 수 있기에 GPIO_SelectFunction를 사용해 설정하였습니다. GPIO_SelectFunction은 gpio.c에서 제가 구현한 함수입니다. GPIO_SelectFunction 의 구현 방법은 Uart와 관련없으므로 Github에서 코드를 참고 해주시기 바랍니다.

 

3.3.1.2. UART 비활성화

    // UART설정을 위해 UART을 비활성화
    REG_32(BCM2711_UART0_CR) = 0x0;

 

  CR 레지스터의 값을 전부 0으로 클리어하여 UART 뿐만이 아니라 모든 기능에 대해서 비활성화 하고 있습니다.

 

3.3.1.3. Baudrate 설정

    // Baudrate를 115200으로 설정
    REG_32(BCM2711_UART0_IBRD) = 26;
    REG_32(BCM2711_UART0_FBRD) = 3;

 

 Baudrate를 설정하기 위해 IBRD, FBRD 레지스터를 설정하는 코드입니다. Baudrate를 설정하는 방법은 아래 포스트의 2.2. 부분을 참고해주시 바랍니다. 

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

 

[RaspberryPI4] Bare metal에서 UART통신을 위한 레지스터 확인

- 개발 환경개발 보드 : Raspberrypi 4WSL2 (Ubuntu 22.04 LTS)toolchain : aarch64-linux-gnu-gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.01. 서론 지금까지 C언어로 GPIO를 제어하는 간단한 예제까지 진행하였습니다. 하지만, GPIO

littlebitodd-developer.tistory.com

 

3.3.1.4. Baudrate 설정

    // Parity bit 사용 안함, Stop bit는 1개, Word 길이는 8bit으로 설정
    REG_32(BCM2711_UART0_LCRH) = UART_WLEN_8BIT;

  이 코드에서 Parity bit와 Stop bit에 대한 설정을 하지 않은 이유는 디폴드 설정이 "Parity bit 사용안함, Stop bit 1개" 이기 때문입니다. 그러므로 Word의 길이에 대해서만 설정하여도 충분합니다.

 

3.3.1.5. UART에 대한 기능 활성화

    // UART의 송신 수신 활성화 및 UART 통신 활성화
    REG_32(BCM2711_UART0_CR) = UART_TXE | UART_RXE | UART_UARTEN;

 

  이제 UART에 대한 설정을 완료했으므로 마지막으로 UART에서 사용할 기능을 활성화함으로써 UART의 초기화를 마무리합니다.

 

3.3.2. UART_SendWord 함수

// UART의 송신 버퍼에 송신할 데이터을 넣을 메모리가 없다면 무한 반복
while(REG_32(BCM2711_UART0_FR) & UART_TXFF);

 

  위 코드는 UART의 송신 버퍼에 남은 메모리가 생길 때까지 무한 반복하면 기다리는 코드입니다. 이 코드가 필요한 이유는 만약 송신 버퍼을 확인하지 않고 데이터를 버퍼에 강제로 넣는다면 데이터을 손실될 가능성이 있기 때문입니다.

 

// DR에 레지스터의 DATA bits에 데이터 비트를 저장함으로써 UART 송신
REG_32(BCM2711_UART0_DR) = data;

위 코드가 실제로 UART의 송신 버퍼에 data를 넣어 데이터의 송신을 유도하는 코드입니다. data의 데이터 형은 uint8_t입니다. 그렇기에 자동적으로 DR의 0~7비트에 data 데이터가 저장되게 됩니다.

 

3.4. main.c

#include "uart.h"

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

int main(void){
	// PC 출력하기 위한 테스트 문자열
    char * message = "hello world\n";
    
    // UART를 사용하기 전 초기화
    // Baudrate : 115200
    // Stop bit : 1
    // Parity bit : None
    // Word Length : 8 bits
    UART_Initialize();

    while(1){
    	// 널 문자가 나올때까지 모든 문자 송신
        for(int i = 0;message[i] != '\0';++i){
            UART_SendWord(message[i]);
        }
        
        // 다음 문제를 출력할때 까지 대기
        delay(0x200000);
    }

    return 0;
}

 

4. 프로젝트 빌드

이미 빌드해야하는 소스파일이 많아져 일일이 타이핑하여 빌드하기에는 무리가 있습니다. 그렇기에 Makefile를 통해 빌드 과정을 자동화 하였습니다. Makefile이 있는 디렉토리에서 "make" 명령어만 입력하면 빌드가 자동적으로 진행될 것입니다.

 

Makefile의 내용을 설명하는 것은 이번 포스트와 관련없으므로 넘어가도록 하겠습니다. 궁금하신 분은 github를 참고 부탁드립니다.

https://github.com/MainForm/RaspberryPI4_Baremetal_Firmware/blob/5d01ffc1b02c7c8d4a777b6e79f372aea1c585d5/Makefile

 

 

5. 라즈베리파이에서 펌웨어 실행

"image" 파일은 "./build/output 폴더에서 찾을 수 있습니다. 해당 파일를 SD카드에 넣고 TTL USB를 Tx, Rx 핀을 연결해야합니다. 이제 라즈베리파이에서 실행하면 정상적으로 "Hello world"가 출력됨을 확인 할 수 있습니다. 

 

 

6. 후기

  이제 GPIO와 UART까지 구현하였습니다. 이를 통해 이제 대부분의 Peripheral를 제어하는 방법을 숙지 할 수 있었습니다. 아직 구현해야할 기능은 많이 있습니다. 특히 Interrupt와 Timer등 이 있지만 지금까지 방법을 통해 똑같이 데이터 시트를 확인한다면 큰 문제없이 구현가능 할 것입니다.

  UART 통신을 사용할 수 있다는 뜻은 꽤나 큰 의미를 가집니다. 이제는 특정 변수에 대한 값을 확인 할 수 있으며 이를 통해 디버깅이 가능해 졌다는 이야기입니다. 물론 디테일한 디버깅을 하는데는 무리가 없지만 취미로 하고 있는 우리는 굳이 JTAG를 통한 디버깅까지는 필요 없다고 생각합니다.

  이제 데이터를 송신(Tx) 하는 기능을 구현하였으므로 다음은 수신(Rx) 기능을 구현하여 데이터를 Echo(메아리)하는 코드를 구현해보도록 하겠습니다.