https://www.yes24.com/Product/Goods/84909414
임베디드 OS 개발 프로젝트 - 예스24
나만의 임베디드 운영체제를 만들어 보자.이 책은 펌웨어 개발 과정을 실시간 운영체제(RTOS)를 만들어 가며 설명한다. 임베디드 운영체제를 개발 환경 구성에서 시작해 최종적으로 RTOS를 만드는
www.yes24.com
공부 목적으로 이만우님의 저서 "임베디드 OS 개발 프로젝트"를 따라가며, RTOS "Navilos"를 개발하는 포스트입니다. 모든 내용은 "임베디드 OS 개발 프로젝트"에 포함되어 있습니다.
개발 목적
임베디드 시스템에 많이 사용되는 RTOS를 직접 개발해 보며 다음과 같은 지식을 학습하고자 합니다.
- RTOS의 핵심 개념에 대해 학습한다.
- 운영체제 핵심 기능을 설계해 보며 학습한다.
- 펌웨어에 대한 진입장벽을 낮춘다.
- ARM 아키텍처에 대해 학습한다.
- 하드웨어에 대한 지식을 학습한다.(펌웨어가 어떻게 하드웨어를 제어하는지)
이번 챕터는 UART에 관한 내용을 다룹니다.
UART란?
UART는 Universal Asynchronous Receiver/Transmitter의 약자로, 범용 비동기화 송수신기입니다.
왜 UART 먼저 작업하는가?
가장 먼저 UART를 작업하는 이유는 UART가 보통 콘솔 입출력용으로 사용하기 때문입니다.
UART는 문자 통신 문자 통신용 프로토콜이 아닌 어떤 데이터든 주고받을 수 있습니다. 터미널 프로그램을 UART와 연결하면 UART를 통해 받은 아스키 코드를 그 코드에 해당하는 문자로 화면에 출력할 수 있습니다. 즉, 터미널 프로그램을 통해 CLI를 쓰듯 임베디드 시스템을 제어할 수 있습니다.
UART 하드웨어를 활성화하고, 이를 사용해야 합니다.
먼저 UARTDR: 데이터 레지스터(UART Data register)의 데이터 시트를 확인해보겠습니다.
데이터 시트를 보면, 0번 비트부터 7번 비트는 입출력 데이터가 사용하고, 8-11번 비트는 각각 프레임 에러, 패리티 에러, 브레이크 에러, 오버렌 에러를 감지하는 비트입니다. 즉, UARTDR은 1바이트씩 통신할 수 있는 하드웨어라고 볼 수 있겠습니다.
보통 2가지 방법으로 활용합니다. C언어 매크로를 사용하거나, 구조체를 사용합니다. 아래는 그 예시입니다.
C언어 매크로
#define UART_BASE_ADDR 0x10009000
#define UART_OFFSET 0x00
#define UARTDR_DATA (0)
... 중략 ...
#define UARTCR_OFFSET 0x30
//사용
uint32_t *uartdr = (uint32_t*)(UART_BASE_ADDR + UARTDR_OFFSET)
*uartdr = (data) << UARTDR_DATA;
bool fe = (bool)((*uartdr >> UARTDR_FE) & 0x1);
...중략...
if(fe || pe || be || oe){
//예외처리 코드
}
구조체
typedef union UARTDR_t{
uint32_t all;
struct{
uint32_t DATA:8; //7:0
uint32_t FE:1, //8
...
}bits;
}UARTDR_t;
typedef union UARTCR_t{
...
}
해당 프로젝트에선 구조체 방식을 사용하였습니다. hal 디렉터리를 만들고 rvpb란 하위 디렉터리에 Uart.h를 만들겠습니다.
//Uart.h
#ifndef HAL_RVPB_UART_H_
#define HAL_RVPB_UART_H_
typedef union UARTDR_t
{
uint32_t all;
struct {
uint32_t DATA:8; // 7:0
uint32_t FE:1; // 8
uint32_t PE:1; // 9
uint32_t BE:1; // 10
uint32_t OE:1; // 11
uint32_t reserved:20;
} bits;
} UARTDR_t;
typedef union UARTRSR_t
{
uint32_t all;
struct {
uint32_t FE:1; // 0
uint32_t PE:1; // 1
uint32_t BE:1; // 2
uint32_t OE:1; // 3
uint32_t reserved:28;
} bits;
} UARTRSR_t;
typedef union UARTFR_t
{
uint32_t all;
struct {
uint32_t CTS:1; // 0
uint32_t DSR:1; // 1
uint32_t DCD:1; // 2
uint32_t BUSY:1; // 3
uint32_t RXFE:1; // 4
uint32_t TXFF:1; // 5
uint32_t RXFF:1; // 6
uint32_t TXFE:1; // 7
uint32_t RI:1; // 8
uint32_t reserved:23;
} bits;
} UARTFR_t;
typedef union UARTILPR_t
{
uint32_t all;
struct {
uint32_t ILPDVSR:8; // 7:0
uint32_t reserved:24;
} bits;
} UARTILPR_t;
typedef union UARTIBRD_t
{
uint32_t all;
struct {
uint32_t BAUDDIVINT:16; // 15:0
uint32_t reserved:16;
} bits;
} UARTIBRD_t;
typedef union UARTFBRD_t
{
uint32_t all;
struct {
uint32_t BAUDDIVFRAC:6; // 5:0
uint32_t reserved:26;
} bits;
} UARTFBRD_t;
typedef union UARTLCR_H_t
{
uint32_t all;
struct {
uint32_t BRK:1; // 0
uint32_t PEN:1; // 1
uint32_t EPS:1; // 2
uint32_t STP2:1; // 3
uint32_t FEN:1; // 4
uint32_t WLEN:2; // 6:5
uint32_t SPS:1; // 7
uint32_t reserved:24;
} bits;
} UARTLCR_H_t;
typedef union UARTCR_t
{
uint32_t all;
struct {
uint32_t UARTEN:1; // 0
uint32_t SIREN:1; // 1
uint32_t SIRLP:1; // 2
uint32_t Reserved1:4; // 6:3
uint32_t LBE:1; // 7
uint32_t TXE:1; // 8
uint32_t RXE:1; // 9
uint32_t DTR:1; // 10
uint32_t RTS:1; // 11
uint32_t Out1:1; // 12
uint32_t Out2:1; // 13
uint32_t RTSEn:1; // 14
uint32_t CTSEn:1; // 15
uint32_t reserved2:16;
} bits;
} UARTCR_t;
typedef union UARTIFLS_t
{
uint32_t all;
struct {
uint32_t TXIFLSEL:3; // 2:0
uint32_t RXIFLSEL:3; // 5:3
uint32_t reserved:26;
} bits;
} UARTIFLS_t;
typedef union UARTIMSC_t
{
uint32_t all;
struct {
uint32_t RIMIM:1; // 0
uint32_t CTSMIM:1; // 1
uint32_t DCDMIM:1; // 2
uint32_t DSRMIM:1; // 3
uint32_t RXIM:1; // 4
uint32_t TXIM:1; // 5
uint32_t RTIM:1; // 6
uint32_t FEIM:1; // 7
uint32_t PEIM:1; // 8
uint32_t BEIM:1; // 9
uint32_t OEIM:1; // 10
uint32_t reserved:21;
} bits;
} UARTIMSC_t;
typedef union UARTRIS_t
{
uint32_t all;
struct {
uint32_t RIRMIS:1; // 0
uint32_t CTSRMIS:1; // 1
uint32_t DCDRMIS:1; // 2
uint32_t DSRRMIS:1; // 3
uint32_t RXRIS:1; // 4
uint32_t TXRIS:1; // 5
uint32_t RTRIS:1; // 6
uint32_t FERIS:1; // 7
uint32_t PERIS:1; // 8
uint32_t BERIS:1; // 9
uint32_t OERIS:1; // 10
uint32_t reserved:21;
} bits;
} UARTRIS_t;
typedef union UARTMIS_t
{
uint32_t all;
struct {
uint32_t RIMMIS:1; // 0
uint32_t CTSMMIS:1; // 1
uint32_t DCDMMIS:1; // 2
uint32_t DSRMMIS:1; // 3
uint32_t RXMIS:1; // 4
uint32_t TXMIS:1; // 5
uint32_t RTMIS:1; // 6
uint32_t FEMIS:1; // 7
uint32_t PEMIS:1; // 8
uint32_t BEMIS:1; // 9
uint32_t OEMIS:1; // 10
uint32_t reserved:21;
} bits;
} UARTMIS_t;
typedef union UARTICR_t
{
uint32_t all;
struct {
uint32_t RIMIC:1; // 0
uint32_t CTSMIC:1; // 1
uint32_t DCDMIC:1; // 2
uint32_t DSRMIC:1; // 3
uint32_t RXIC:1; // 4
uint32_t TXIC:1; // 5
uint32_t RTIC:1; // 6
uint32_t FEIC:1; // 7
uint32_t PEIC:1; // 8
uint32_t BEIC:1; // 9
uint32_t OEIC:1; // 10
uint32_t reserved:21;
} bits;
} UARTICR_t;
typedef union UARTDMACR_t
{
uint32_t all;
struct {
uint32_t RXDMAE:1; // 0
uint32_t TXDMAE:1; // 1
uint32_t DMAONERR:1;// 2
uint32_t reserved:29;
} bits;
} UARTDMACR_t;
typedef struct PL011_t
{
UARTDR_t uartdr; //0x000
UARTRSR_t uartrsr; //0x004
uint32_t reserved0[4]; //0x008-0x014
UARTFR_t uartfr; //0x018
uint32_t reserved1; //0x01C
UARTILPR_t uartilpr; //0x020
UARTIBRD_t uartibrd; //0x024
UARTFBRD_t uartfbrd; //0x028
UARTLCR_H_t uartlcr_h; //0x02C
UARTCR_t uartcr; //0x030
UARTIFLS_t uartifls; //0x034
UARTIMSC_t uartimsc; //0x038
UARTRIS_t uartris; //0x03C
UARTMIS_t uartmis; //0x040
UARTICR_t uarticr; //0x044
UARTDMACR_t uartdmacr; //0x048
} PL011_t;
#define UART_BASE_ADDRESS0 0x10009000
#define UART_INTERRUPT0 44
#endif /* HAL_RVPB_UART_H_ */
그리고 같은 hal/rvpb에 Regs.c란 코드를 넣어주겠습니다. RVPB의 하드웨어를 제어할 수 있는 변수들을 모아둔 코드입니다.
//Regs.c
#include "stdint.h"
#include "Uart.h"
volatile PL011+t* Uart = (PL011_t*)UART_BASE_ADDRESS0;
HAL이란?
각기 다른 하드웨어 A, B, C를 공용 API를 통해서 동일한 방법으로 접근하여 사용할 수 있게합니다. 기능 코드를 변경하지 않아도 펌웨어를 다른 하드웨어에 이식할 수 있습니다. 이런 공용 인터페이스 혹은 API 설계를 HAL(Hardware Abstraction Layer)라고 합니다.
이제 HAL 인터페이스를 정의하겠습니다.
HalUart.h는 hal에 위치해야 합니다. 위 그림의 HW A가 RealViewPB로 rvpb 디렉터리에 해당하기 때문입니다. 만약 라즈베리파이를 추가한다면 hal/rasppi가 되는 것입니다.
//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를 설계했으므로, 이제 API를 만족하는 코드를 구현하겠습니다.
//hal/rvpb/Uart.c
#include "stdint.h"
#include "Uart.h"
#include "HalUart.h"
extern volatile PL011_t* Uart;
void Hal_uart_init(void){
//Enable UART
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(Iart->uartfr.bits.TXFF);
Uart->uartdr.all = (ch & 0xFF);
}
설명
- 5번째 줄 hal/rvpb/Regs.c에서 선언하고 메모리 주소에 할당해 놓은 Uart 변수를 extern으로 불러오는 코드
- 7~14, UART 하드웨어를 초기화하는 코드 → 실제 실물 하드웨어를 초기화하려면 훨씬 복잡함
- 10, 컨트롤 레지스터를 변경하기 전에 하드웨어를 꺼놓는 코드
- 13, 아까 꺼내놨던 UART하드웨어 전체를 다시 켜는 코드
- 16~20, 알파벳 한 글자를 UART를 통해 출력하는 코드
- 18, UART 하드웨어 출력 버프가 0이 될때까지 기다리는 코드 → 출력 버퍼가 0이라는 것은 출력 버퍼가 빈 것, 버퍼가 비어야알파벳 하나를 넣을 수 있음
- 19, 데이터 레지스터를 통해 알파벳 한 글자를 출력 버퍼로 보내는 코드
이제 UART를 초기화하고 출력해보겠습니다.
//Main.c
#include "stdint.h"
#include "HalUart.h"
static void Hw_init(void);
void main(void)
{
Hw_init();
uint32_t i = 100;
while(i--){
Hal_uart_put_char('N');
}
}
static void Hw_init(void){
Hal_uart_init();
}
- 5, static함수로 Hw_init()을 선언했고, 18-21에서 Hw_init()구현
- 11-15, 알파벳 N을 100 번 루프를 돌며 UART로 보냄. 만약 UART가 제대로 초기화가 되었고 출력 코드가 정상이라면 N이 100개 출력되어야 함
소스 코드를 추가했으니 Makefile을 변경해야 합니다.
//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.bin
.PHONY: all clean run debug gdb
all: $(navilos)
clean:
@rm -fr 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:
arm-none-eabi-gdb
$(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 $@ $<
- 전체적으로 많이 수정됨
- 41번째 줄에 QEMU를 호출하는 옵션을 변경했음 → 마지막에 nographic을 추가함. 이 옵션을 추가하면, QEMU는 GUI를 출력하지 않고 시리얼 포트 입출력을 현재 호스트의 콘솔과 연결함
이제 빌드하고 실행해보겠습니다. 예상했던대로 N이 100개가 출력이 잘 되는 모습입니다.
QEMU와 현재 리눅스 터미널을 연결하면, 입력도 QEMU와 연결되어 Ctrl + c로도 QEMU가 종료되지 않습니다. 다음과 같은 두 가지 방법이 있습니다.
1. 별개의 터미널에서 kill 명령으로 종료
2. Ctrl + A -> x
Hello World
이제 프로그래밍 언어를 처음 공부할때 항상 가장 먼저하는 예제 “Hello World”를 출력하는 코드를 작성해보겠습니다. C언어를 처음 공부할때는 printf()를 사용하겠지만, 펌웨어에서는 printf()마저도 직접 구현해야 합니다.
어떻게 할까?
앞전에 작성한 UART를 이용해 문자 한개 출력하는 코드를 반복 호출해서 문자열을 출력하면 됩니다. 이를 정의한 소스 코드를 lib디렉터릴 만들고 그 안에 stdio.c와 stdio.h라는 파일로 생성하겠습니다.(c 표준 라이브러리와 같은 이름으로 만드는 것은 역할이 비슷하기 때문입니다.)
//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;
}
ssafy@ssafy-VirtualBox:~/RTOS/rtos-project$ cat ./lib/stdio.h
#ifndef LIB_STDIO_H_
#define LIB_STDIO_H_
uint32_t putstr(const char* s);
#endif /* LIB_STDIO_H_ */
const char* -> 포인터 파라미터를 읽기 전용으로 쓸 때 const를 붙이는 것은 좋은 코딩 습관입니다. 코딩 실수로 포인터를 변경하는 것을 컴파일러가 감시할 수 있기 때문입니다.
//stdio.c
#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;
}
다시 또 디렉터리를 새로 생성했으므로 Makefile을 수정하고 빌드해보겠습니다.
Main에서 putstr("Hello World")를 넣고 실행하니 Hello World가 잘 출력되는 것을 볼 수 있습니다.
UART로 입력 받기
출력은 어떻게 했나?
보내기 버퍼가 비어있는지 확인한 후, 비어있으면 데이터 레지스터를 통해 데이터를 보내기 버퍼로 보내면, 하드웨어가 나머지 처리를 해주어 하드웨어와 연결된 콘솔에 데이터가 나타납니다. 현재까지 다루는 데이터가 아스키 코드이기 때문에 알파벳이 보입니다.
그렇다면 입력은??
입력은 출력의 반대입니다. 받기 버퍼가 채워져 있는지 확인 후, 받기 버퍼에 데이터가 있으면 데이터 레지스터를 통해 데이터를 하나 읽어오면 됩니다.
//비효율적인 Hal_uart_get_char() 함수
uint8_t Hal_uart_get_char(void){
uint8_t data;
while(Uart->uartfr.bits.RXFE);
//Check for an error flag
if(Uart->uartdr.bits.BE || Uart->uartdr.bits.FE
|| Uart->Uartdr.bits.OE || Uart->uartdr.bits.PE){
//Clear the error
Uart->uartsr.bits.BE = 1;
Uart->uartsr.bits.FE = 1;
Uart->uartsr.bits.OE = 1;
Uart->uartsr.bits.PE = 1;
return 0;
}
data = Uart->uartdr.bits.DATA;
return data;
}
우선 기본적인 코드입니다. 하지만 이 코드는 비효율적이죠.
8~9번째 줄에서 if 문에서 각 에러 플래그 4개를 개별적으로 확인하기 때문에 비효율적인 코드입니다. 이 과정에서 레지스터 접근이 4번 일어나고, 각각 비트 플래그를 확인하므로, 비트 시프트 연산 네 번, 0이 아닌지 확인하는 연산 4번이 발생합니다. 또한, 12~15에서도 에러 플래그를 클리어하는 코드도 레지스터 접근, 비트 시프트, 데이터 복사가 각각 4번씩 발생하는 매우 비효율적인 코드입니다.
아래는 이를 최적화한 코드입니다.
//한 번 더 최적화한 Hal_uart_get_char()함수
uint8_t Hal_uart_get_char(void){
uint8_t data;
while(Uart->uartfr.bits.RXFE);
data = Uart->uartdr.all;
//Check for an error flag
if(data & 0xFFFFFF00){
//Clear the error
Uart->uartrsr.all = 0xFF;
return 0;
}
return (uint8_t)(data & 0xFF);
}
32비트 레지스터 자체로 접근하며 비트값을 직접 비교하는 방식으로 변경했고, UARTDR에 두 번 접근하는 것을 한 번 읽어와 재활용하는 방식으로 최적화하였습니다. 역어셈블했을 때 이전 방식 340바이트에서 140바이트로 최적화되었습니다.
Hal_uart_get_char()함수 코드는 ./hal/rvpb/Uart.c에 추가하고, ./hal/HalUart.h에 선언합니다.
위 함수를 main()에 추가하겠습니다.
//Main.c
#include "stdint.h"
#include "HalUart.h"
#include "stdio.h"
static void Hw_init(void);
void main(void)
{
Hw_init();
uint32_t i = 100;
while(i--){
Hal_uart_put_char('N');
}
Hal_uart_put_char('\n');
putstr("Hello World\n");
i = 100;
while(i--){
uint8_t ch = Hal_uart_get_char();
Hal_uart_put_char(ch);
}
}
static void Hw_init(void){
Hal_uart_init();
}
이제 빌드하고 실행해보겠습니다.
입력도 잘 되는 것을 확인할 수 있습니다.
어쩌다가 Ctrl + A → X로 종료가 안돼서 다른 터미널에서 qemu를 kill 해줬는데 갑자기 터미널 화면에 내가 입력한 명령이 보이지 않았습니다.
터미널 입력 비활성화 때문이었는데,
$ stty echo
를 입력하면 해결됩니다.
printf 만들기
printf()는 펌웨어에서도 로그나 디버깅 등에 매우 유용한 함수입니다.
이 printf()를 직접 구현해보겠습니다.
printf()라는 이름으로 구현하면 gcc를 포함한 많은 컴파일러가 최적화 과정에서 printf()를 puts()로 바꿔버림
→ 다른 이름으로 만들자. 여기선 debug_printf()
디버그 인터페이스 구현
//stdio.h에 debug_printf()선언
#ifndef LIB_STDIO_H_
#define LIB_STDIO_H_
uint32_t putstr(const char* s);
uint32_t debug_printf(const char* format, ...);
#endif /* LIB_STDIO_H_ */
→ debug_printf()의 마지막 파라미터 점 세 개(…)은 C언어 문법으로 가변 인자 지정입니다.
//stdio.c에 debug_printf()구현
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);
}
아직까진 간단합니다. 실제 %u, %x 등의 형식 문자 처리는 vsprintf() 함수에서 구현할 예정입니다.
두 번째 단계
//include/stdarg.h
#ifndef INCLUDE_STDARG_H_
#define INCLUDE_STDARG_H_
typedef __builtin_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)
세 번째 단계 vsprintf() 만들기
printf()는 실제로 엄청 복잡합니다. 여기선 극히 일부만 구현할 예정입니다.
- 길이 옵션과 채우기 옵션은 구분하지 않을 것임 → %3u와 %03u를 구별하지 않을 것
- %c, %u, %x, %s만 구현하겠음 → %d는 펌웨어 개발 과정에서 디버깅 중 부호 있는 숫자를 다룰 일이 많이 없음
//stdio.h에 vsprintf() 선언 추가
#ifndef LIB_STDIO_H_
#define LIB_STDIO_H_
#include "stdarg.h"
uint32_t putstr(const char* s);
uint32_t debug_printf(const char* format, ...);
uint32_t vsprintf(char* buf, const char* format, va_list arg);
#endif /* LIB_STDIO_H_ */
//vsprintf()함수 구현
uint32_t vsprintf(char* buf, const char* format, va_list arg)
{
uint32_t c = 0;
char ch;
char* str;
uint32_t uint;
uint32_t hex;
for (uint32_t i = 0 ; format[i] ; i++){
if (format[i] == '%')
{
i++;
switch(format[i])
{
case 'c':
ch = (char)va_arg(arg, int32_t);
buf[c++] = ch;
break;
case 's':
str = (char*)va_arg(arg, char*);
if(str == NULL)
{
str = "(null)";
}
while(*str)
{
buf[c++] = (*str++);
}
break;
case 'u':
uint = (uint32_t)va_arg(arg, uint32_t);
c += utoa(&buf[c], uint utoa_dec);
break;
case 'x':
hex = (uint32_t)va_arg(arg, uint32_t);
c += utoa(&buf[c], hex, utoa_hex);
break;
}
}
else
{
buf[c++] = format[i];
}
}
if (c >= PRINTF_BUF_LEN)
{
buf[0] = '\0';
return 0;
}
buf[c] = '\0';
return c;
}
- %c의 기능은 아스키 문자 한 개를 그대로 출력하는 것임 → va_list에서 파라미터 한 개를 그대로 읽어서 문자열로 바로 추가하면 끝
- %s는 while문을 통해 위를 반복하면 간단히 구현 가능
- 23-26, 그런데 어쩌다 널 포인터가 전달될 수 있음 → C 언어는 널 포인터를 제대로 처리하지 못 하면 심각한 일이 생김 → 그래서 에러처리 해둔 것
- 다음 %u, %x의 핵심 기능은 utoa()함수에서 처리함
마지막 단계
utoa() 함수를 만들어주겠습니다.
//utoa.h 선언
#ifndef LIB_STDIO_H_
#define LIB_STDIO_H_
#include "stdarg.h"
typedef enum utoa_t
{
utoa_dec = 10,
utoa_hex = 16,
}utoa_t;
uint32_t putstr(const char* s);
uint32_t debug_printf(const char* format, ...);
uint32_t vsprintf(char* buf, const char* format, va_list arg);
uint32_t utoa(char* buf, uint32_t val, utoa_t base);
#endif /* LIB_STDIO_H_ */
→ 첫번째 파라미터 : 리턴 포인터로 사용
→ 두번째 파라미터 : 문자열로 바꿀 원본 숫자 데이터
→ 세번째 파라미터 : 문자열을 10진수로 표현할지 16진수로 표현할지 결정
//utoa() 함수 구현
uint32_t utoa(char* buf, uint32_t val, utoa_t base)
{
uint32_t c = 0;
int32_t idx = 0;
char tmp[11];
do{
uint32_t t = val % (uint32_t)base;
if (t >= 10)
{
t += 'A' - '0' - 10;
}
tmp[idx] = (t + '0');
val /= base;
idx++;
}while(val);
//reverse
idx--;
while (idx >= 0)
{
buf[c++] = tmp[idx];
idx--;
}
return c;
}
위 코드는 직관적인 코드는 아님
위 함수는 크게 두 분으로 나누어짐
7-16과 20-24
- 7-16, val의 값을 기준으로 루프 탈출 조건을 걸어 뒀기 때문에 만약 debuf_printf(”%u”, 0)과 같은 코드라면 val이 시작부터 0이라서 화면에 ‘0’이 출력되지 않음 → 그래서 do-while문으로 수행한 것
- 문자열 버퍼 길이가 11인 것은 32비트 크기의 데이터를 10진수로 표현하면 11자리를 넘지 않기 때문
utoa() 함수의 목적은 숫자를 아스키 코드로 변환하는 것입니다. (ex, 0을 ‘0’으로)
이제 debug_pritnf()를 모두 만들었으니 테스트를 해보겠습니다. main() 함수를 수정해 debug_printf()가 잘 동작하는지 테스트 함수를 작성하겠습니다.
//debug_printf()함수의 테스트 코드가 추가된 main()함수
#include "stdint.h"
#include "HalUart.h"
#include "stdio.h"
static void Hw_init(void);
static void Printf_test(void);
void main(void)
{
Hw_init();
uint32_t i = 100;
while(i--){
Hal_uart_put_char('N');
}
Hal_uart_put_char('\n');
putstr("Hello World\n");
Printf_test();
i = 100;
while(i--){
uint8_t ch = Hal_uart_get_char();
Hal_uart_put_char(ch);
}
}
static void Hw_init(void){
Hal_uart_init();
}
static void Printf_test(void){
char* str = "printf pointer test";
char* nullptr = 0;
uint32_t i = 5;
debug_printf("%s\n", "Hello printf");
debug_printf("output string pointer: %s\n", str);
debug_printf("%s is null pointer, %u number\n", nullptr, 10);
debug_printf("%u = 5\n", i);
debug_printf("dec=%u hex=%x\n", 0xff, oxff);
debug_printf("print zero %u\n", 0);
}
여기까지 작업 후 빌드를 하면 위와 같은 에러가 나옵니다. 이유는 utoa() 함수에서 나머지(%)와 나누기(/) 연산자를 사용했는데 ARM은 기본적으로 나누기와 나머지를 지원하는 하드웨어가 없다고 간주하기 때문입니다. → gcc가 소프트웨어로 구현해 놓은 라이브러리 함수로 링킹을 합니다.
만약 나누기와 나머지 하드웨어가 있는 플랫폼 기반의 펌웨어라면 __aeabi_uidivmod와 ++aeabi_uidiv 함수를 만들고 그 함수에서 하드웨어를 사용하는 코드를 작성하여 링킹해야 합니다.
ARCH = armv7-a
MCPU = cortex-a8
TARGET = rvpb
CC = arm-none-eabi-gcc
AS = arm-none-eabi-as
LD = arm-none-eabi-gcc
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) \
lib
C_SRCS = $(notdir $(wildcard boot/*.c))
C_SRCS += $(notdir $(wildcard hal/$(TARGET)/*.c))
C_OBJS = $(patsubst %.c, build/%.o, $(C_SRCS))
C_SRCS += $(notdir $(wildcard lib/*.c))
INC_DIRS = -I include \
-I hal \
-I hal/$(TARGET) \
-I lib
CFLAGS = -c -g -std=c11 -mthumb-interwork
LDFLAGS = -nostartfiles -nostdlib -nodefaultlibs -static -lgcc
navilos = build/navilos.axf
navilos_bin = build/navilos.bin
.PHONY: all clean run debug gdb
all: $(navilos)
clean:
@rm -fr 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:
arm-none-eabi-gdb
$(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)
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 $@ $<
수정 후엔 빌드가 잘 됩니다. run을 해보면
출력이 잘 됩니다.
참고
저자 이만우님 깃허브 https://github.com/navilera/Navilos
GitHub - navilera/Navilos: RTOS for various embedded platform
RTOS for various embedded platform. Contribute to navilera/Navilos development by creating an account on GitHub.
github.com
RealViewPB 데이터시트 https://developer.arm.com/documentation/dui0417/d/?lang=en
Documentation – Arm Developer
developer.arm.com
마치며
이번엔 UART를 통해 입출력을 할 수 있도록 만들었습니다. UART 레지스터를 활성화하고 소스 코드를 추가하는 방식이 다른 레지스터를 사용하는 방법에 그대로 활용됩니다. 지금 만든 입출력을 통해 디버깅할 수 있기 때문에 앞으로는 좀 더 수월히 디버깅할 수 있을 것입니다.
c언어 프로그래밍을 하며 가장 익숙한 printf()를 직접 구현해본다는 점이 매우 재밌는 내용이었습니다.
'임베디드' 카테고리의 다른 글
[RTOS 개발하기] 임베디드 OS 개발 프로젝트ch.7 (1) | 2024.11.11 |
---|---|
[RTOS 개발하기] 임베디드 OS 개발 프로젝트ch.6 (3) | 2024.11.06 |
[RTOS 개발하기] 임베디드 OS 개발 프로젝트ch.4 (4) | 2024.10.31 |
[RTOS 개발하기] 임베디드 OS 개발 프로젝트ch.3 (1) | 2024.10.19 |
[RTOS 개발하기] 임베디드 OS 개발 프로젝트ch.0-ch.2 (0) | 2024.10.19 |