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

Sijin·2025년 6월 7일

UART로 입력 받기

  • 출력은 출력버퍼가 비어있는지 확인하고(TXFE, Transmit FIFO Empty), 비어있으면 레지스터를 통해 출력버퍼로 데이터를 보냈다
  • 입력은 그 반대로, 입력버퍼가 채워져 있는지 확인하고(RXFE, Receive FIRO Empty), 데이터가 있으면 레지스터를 통해 데이터를 읽어오면 된다

우선 만들어보기

uint8_t Hal_uart_get_char(void)
{
    uint8_t data;

    while(Uart->uartfr.bits.RXFE);

    if(Uart->uartdr.bits.BE || Uart->uartdr.bits.FE ||
        Uart->uartdr.bits.OE || Uart->uartdr.bits.PE)
    {
        Uart->uartrsr.bits.BE = 1;
        Uart->uartrsr.bits.FE = 1;
        Uart->uartrsr.bits.OE = 1;
        Uart->uartrsr.bits.PE = 1;
        return 0;
    }

    data = Uart->uartdr.bits.DATA;
    return data;
}
  • 가장 직관적이고 읽기 쉬운 코드이다
  • 그런만큼, 레지스터에 대한 접근과 비트 연산이 매우 많다
    • BE, FE, OE, PE 에러 플래그를 개별적으로 접근
    • BE, FE, OE, PE 에러 플래그를 개별적으로 클리어
  • 위 코드를 역어셈블해보자
sijin@Sijin:~/embedded_os/build$ arm-none-eabi-objdump -d Uart.o

000000d4 <Hal_uart_get_char>:
  d4:   e52db004        push    {fp}            @ (str fp, [sp, #-4]!)
  d8:   e28db000        add     fp, sp, #0
...
  20c:   e49db004        pop     {fp}            @ (ldr fp, [sp], #4)
 210:   e12fff1e        bx      lr
  • 0x00d4에서 0x0210까지이므로, 입력 함수가 320byte, 80줄이다

[참고] UART 레지스터 약어

  • DR (Data Register)
    • 송신/수신한 데이터를 저장
  • SR (Status Register)
    • UART의 에러, 송신/수신 상태 등 상태를 저장
  • FR (Flag Register)
    • UART의 플래그 상태(버퍼가 비었는지, 가득찼는지)를 나타냄
  • PR (Prescaler Register)
    • UART 하드웨어 클럭 속도 설정에 사용
  • CR (Control Register)
    • UART의 활성화, 송신/수신 활성화 등 제어
  • RSR (Receive Status Register)
    • 수신 데이터에 대한 상태 정보 제공

최적화하기

  • 아래 두가지 요소에 대해 최적화 할 수 있다
    1. 비트마스킹을 활용
      • 레지스터 접근, 비트연산, 비교연산을 줄일 수 있다
    2. HW 레지스터값을 로컬 변수로 저장
      • 하드웨어 레지스터에 접근하는 것보다 스택 혹은 레지스터에 저장되는 로컬변수에 접근하는게 훨씬 빠름
uint8_t Hal_uart_get_char(void)
{
    uint8_t data;

    while(Uart->uartfr.bits.RXFE);

    data = Uart->uartdr.all;

    if(Uart->uartdr.all & 0x0F00)
    {
        Uart->uartrsr.all = 0xF;
        return 0;
    }

    return (uint8_t)(data & 0xFF);
}
  • 동일하게 역어셈블리해서 최적화되었는지 확인해보자
sijin@Sijin:~/embedded_os/build$ arm-none-eabi-objdump -d Uart.o

000000d4 <Hal_uart_get_char>:
  d4:   e52db004        push    {fp}            @ (str fp, [sp, #-4]!)
  d8:   e28db000        add     fp, sp, #0
...
 15c:   e49db004        pop     {fp}            @ (ldr fp, [sp], #4)
 160:   e12fff1e        bx      lr
  • 0x00d4에서 0x0160까지이므로, 입력함수가 144byte, 36줄이되었다

main에서 입력/출력

void main(void)
{
    Hw_init();

    int i = 100;
    while(i--)
    {
        uint8_t ch = Hal_uart_get_char();
        Hal_uart_put_char(ch);
    }
}
  • 문자를 입력받고 그대로 출력해보자
  • 키보드로 입력한 문자가 그대로 출력하는것을 볼 수 있다

printf 만들기

  • 일반 char 출력과 달리 printf는 format을 받을 수 있는점이 다르다

구현

  • format 문자열을 읽어 변수를 알맞게 넣어주어야 한다
    • 이 부분은 단순 구현이므로 생략
  • format에 대한 파라미터는 가변적인데, 이런 경우 __builtin_va 관련 함수들을 사용할 수 있다
    • 가변인자함수를 위한 컴파일러 내장 함수이다
함수 종류설명
__builtin_va_start가변 인자 목록을 초기화, va_list 타입의 변수를 인자로 받아, 가변 인자 목록의 시작 위치를 설정
__builtin_va_argva_list에서 다음 인자의 값을 지정한 타입으로 읽음
__builtin_va_end가변 인자 처리가 끝난 후, va_list를 정리
__builtin_va_copy한 va_list의 상태를 다른 va_list로 복사
#define va_start(v, l)      __builtin_va_start(v, l)
#define va_end(v)           __builtin_va_end(v)
#define va_arg(v, l)        __builtin_va_arg(v, l)

uint32_t debug_printf(const char* format, ...)
{
    va_list args;
    va_start(args, format);
    vsprintf(printf_buf, format, args);
    va_end(args);
    
    return putstr(printf_buf);
}
  • printf라는 함수 이름을 그대로 쓰면 컴파일러가 최적화 과정에 puts로 바꿔버릴 수 있어 debug_printf 함수이름을 사용했다

빌드

  • 여기서는 생략했지만, printf format을 처리할 때 %d 혹은 %x같은 format을 처리하기 위해 utoa 함수를 만들었다
    • 이 과정에서 나눗셈 연산이 들어갔다
    • 나눗셈 연산 때문에 아래와 같은 오류가 발생한다
/home/sijin/embedded_os/lib/stdio.c:92:(.text+0x374): undefined reference to `__aeabi_uidivmod'
arm-none-eabi-ld: /home/sijin/embedded_os/lib/stdio.c:98:(.text+0x3c8): undefined reference to `__aeabi_uidiv'
  • arm에서는 기본적으로 나머지(%), 나누기(/)연산을 지원하는 HW가 없는 것을 가정한다
  • HW가 없다면 gcc가 이를 SW로 구현한 라이브러리 함수로 자동으로 링킹한다
  • HW가 있다면 __aeabi_uidivmod, __aeabi_uidiv 함수를 만들어 HW를 사용하는 코드를 작성해 링킹해야 한다
LD=arm-none-eabi-gcc

LDFLAGS = -nostartfiles -nostdlib -nodefaultlibs -static -lgcc

$(navilos): $(ASM_OBJS) $(C_OBJS) $(LINKER_SCRIPT)
	$(LD) -n -T $(LINKER_SCRIPT) -o $(navilos) $(ASM_OBJS) $(C_OBJS) -Wl,-Map=$(MAP_FILE) $(LDFLAGS)
	$(OC) -O binary $(navilos) $(navilos_bin)
  • 링커 명령어가 arm-none-eabi-ld에서 arm-none-eabi-gcc로 변경했다
    • gcc도 링킹이 가능하고 내부적으로 gcc가 ld를 호출한다
    • 이 과정에서 몇가지 번거로운 옵션을 알아서 추가해준다
  • LDFLAGS를 정의해, 표준 라이브러리를 빼고 -lgcc만 추가해주었다
    • 이 옵션으로 빌드에러가 사라진다

0개의 댓글