[임베디드 OS 개발 프로젝트] 5-1. UART (출력)

Sijin·2025년 6월 7일

UART

  • UART는 Universal Asynchronous Receiver/Transmitter의 약자이다
  • 우리가 qemu를 통해서 에뮬레이팅하고 있는 RealViewPB에서는 PL011이라는 UART 하드웨어 모듈이 붙어있다
  • 역시 이 하드웨어 모듈 또한 arm 인포센터에 데이터시트가 있다

UART 역할

  • UART는 보통 콘솔 입출력용으로 사용된다
  • 즉, UART를 통해 터미널을 명령 인터페이스로 활용해 임베디드 시스템을 제어할 수 있다

UART 레지스터

  • UART를 사용하기 위해서는, 우선 UART 하드웨어의 레지스터를 코드로 만들어야 한다

  • UART 레지스터는 아래에서 확인할 수 있다

  • UART에는 상당히 많은 레지스터가 있다

    • 다양한 목적과 요구사항을 최대한 만족시키기 위해 여러 기능이 추가되어 많은 레지스터들이 들어가있다
  • 레지스터를 코드로 옮기는 방법은 아래 두가지가 있다

    • C언어 매크로를 사용

      #define UART_BASE_ADDR      0x10009000
      
      #define UARTDR_OFFSET       0x00
      #define UARTDR_DATA (0)
      #define UARTDR_FE (8)
      
      ...
      
      uint32_t *uartdr = (uint32_t*)(UART_BASE_ADDR + UARTDR_OFFSET);
      *uartdr = (data) << UARTDR_DATA
      
      bool fe = (bool)((*uartdr >> UARTDR_FE) & 0x1);
      
      if(fe)
      {
          // fe 오류 처리 코드
      }   
    • 구조체를 이용

      typedef union UARTDR_t
      {
          uint32_t all;
          struct
          {
              uint32_t DATA:8;
              uint32_t FE:1;
              ...
          } bits;
      } UARTDR_t;
      
      typedef union UARTCR_t
      {
          uint32_t all;
          struct
          {
              uint32_t UARTEN:1;
              uint32_t SIREN:1;
              ...
          } bits;
      } UARTCR_t;
      
      typedef struct PL011_t
      {
          UARTDR_t uartdr;
          UARTCR_t uartcr;
      } PL011_t;
      
      PL011_t Uart = (PL011_t*)UART_BASE_ADDR;
      Uart->uartdr.DATA = data & 0xFF;
      if (Uart->uartdr.FE)
      {
          // fe 오류 처리 코드
      }
  • hal/rvpb 디렉토리를 만들고 그 안에 Uart.h 파일을 만들어 PL011의 레지스터를 코드로 넣어보자

    • 이런 구조는 여러 플랫폼을 지원하는 것을 염두에 둔 디렉토리 구조이다
    • 예를 들어, 라즈베리파이로 확장하게 된다면 라즈베리파이 보드에 종속된 하드웨어 코드는 hal/rasppi 디렉토리에 들어갈 것 이다
    • Uart.h는 아래 링크에서 받자
  • 다음으로 UART 하드웨어를 제어할 수 있는 변수를 Regs.c 파일을 만들어 선언하자

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

volatile PL011_t* Uart = (PL011_t*)UART_BASE_ADDRESS0;
  • 초기화하는 하드웨어가 추가되면, 이런 형태로 해 당 하드웨어의 레지스터 구조체와 베이스 주소를 연결해서 추가할 것이다

[참고] Bit Field

  • 비트필드는 구조체의 멤버 변수를 비트 단위로 나누어 사용할 수 있도록 해주는 문법이다
  • 정수형 데이터의 일부 비트만 특정 용도로 지정해 사용할 수 있도록 해준다
  • 특히 하드웨어의 레지스터 제어, 플래그 값을 하나의 변수에 효율적으로 저장할 때 사용한다
  • 선언시 콜론 뒤에 비트 수를 지정한다
struct Flags {
    unsigned int a : 1; // 1비트
    unsigned int b : 3; // 3비트
    unsigned int c : 7; // 7비트
};
  • 비트 필드는 내부적으로 컴파일러가 비트 연산 코드를 생성해서 사용하게 되는 것이다
  • 비트 필드는 선언된 기본 자료형의 단위로 묶여서 할당된다
    • 위 Flags에서는 선언된 기본 자료형인 unsigned int의 4byte를 크기로 갖는다
  • 실무에서는 호환성과 명확성을 위해 주로 unsigned int를 사용한다

UART HAL

  • 우리가 RealViewPB에서 PL011 UART를 사용하는 것 처럼, 다른 플랫폼은 다른 UART 하드웨어를 가질 것이다
  • 각 UART들의 공용 인터페이스 API를 정의해 범용적으로 사용할 수 있는 일종의 디바이스 드라이버가 필요하다
  • 각기 다른 하드웨어들을 중계해 주는 추상화 계층을 HAL(Hardware Abstraction Layer)이라고 한다
  • UART 공용 HAL 인터페이스 HalUart.h를 hal 디렉토리에 만들어보자
    • 각 하드웨어 HAL 구현체는 HalUart.h 파일을 include해서 각 하드웨어에 맞게 구현한다
    • 이후 펌웨어를 빌드할 때 타겟 하드웨어에 맞는 구현 코드를 컴파일에 포함시킨다
# 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 초기화, 출력 함수를 정의하였다
  • 이 API를 RealViewPB에 맞는 코드로 구현하는 코드를 hal/rvpb/Uart.c에 만들어보자
#include "stdint.h"
#include "Uart.h"
#include "HalUart.h"

extern volatile PL011_t* Uart;

void Hal_uart_init(void)
{
    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 하드웨어 출력 버퍼가 0이 될 때까지 기다림
    Uart->uartdr.all = (ch & 0xFF);     // 데이터 레지스터를 통해 알파벳 한 글자를 출력 버퍼로 보냄
}
  • 이제 실제 동작을 확인해보기 위해 main 함수에서 UART 관련 함수를 실행시켜보자
    • 제대로 동작한다면 화면에 N이 100개 출력될 것이다
#include "stdint.h"
#include "HalUart.h"

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

void main(void)
{
    Hw_init();

    uint32_t i = 100;
    while(i--)
    {
        Hal_uart_put_char('N');
    }
}
  • 빌드를 위해 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.navilos_bin

.PHONY: all clean run debug gdb

all: $(navilos)

clean:
	@rm -rf 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

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 $@ $<

  • 빌드 후 실행시켜보면 아래와 같이 N이 100개 출력된다
    • qemu에서 nographic 옵션으로 GUI를 출력하지 않고 시리얼 포트 입출력을 현재 호스트 콘솔에 연결한다, 즉 UART 입출력이 리눅스 터미널에 연결된다

추상화된 입출력 라이브러리

  • 문자열을 출력하는 함수는 UART 하드웨어를 신경쓰지 않고 사용할 수 있어야 한다
  • lib 디렉토리를 만들고 stdio 파일을 만들고 문자열을 UART로 출력하는 함수를 만들어보자
#ifndef LIB_STDIO_H_
#define LIB_STDIO_H_

uint32_t putstr(const char* s);

#endif /* LIB_STDIO_H_ */
#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;
}
  • 로직은 간단하게 문자열의 문자 수 만큼 UART에 문자를 출력한다
  • 이 함수를 main함수에 적용해서 확인해보자
#include "stdint.h"
#include "HalUart.h"
#include "stdio.h"

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

void main(void)
{
    Hw_init();

   putstr("Hello world\n");
}
  • 이후 빌드가 가능하도록 makefile을 수정하자
(...)
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
(...)
  • putstr함수에 의해 Hello world가 출력된 것을 볼 수 있다

0개의 댓글