ARM 콘솔 입출력용 UART 하드웨어 모듈, PL011.
Datasheet: https://developer.arm.com/documentation/ddi0183/latest/

UARTDR : Data Register, R/W
이러한 UART Register 를 C 언어 구조체로 옮길 것.
UART 의 기본 주소는 0x10009000이다.
우리가 사용하고 있는 하드웨어는 RealViewPB 이므로 RealViewPB 용 Uart 레지스터 구조체이다. 라즈베리파이와 같은 다른 하드웨어를 사용 시, 또 다른 구조체 파일을 생성해야 한다.
hal/rvpb/Uart.h
8 #ifndef HAL_RVPB_UART_H_
9 #define HAL_RVPB_UART_H_
10
11 typedef union UARTDR_t
12 {
13 uint32_t all;
14 struct {
15 uint32_t DATA:8; // 7:0
16 uint32_t FE:1; // 8
17 uint32_t PE:1; // 9
18 uint32_t BE:1; // 10
19 uint32_t OE:1; // 11
20 uint32_t reserved:20;
21 } bits;
22 } UARTDR_t;
...
213 typedef struct PL011_t
214 {
215 UARTDR_t uartdr; //0x000
216 UARTRSR_t uartrsr; //0x004
217 uint32_t reserved0[4]; //0x008-0x014
218 UARTFR_t uartfr; //0x018
219 uint32_t reserved1; //0x01C
220 UARTILPR_t uartilpr; //0x020
221 UARTIBRD_t uartibrd; //0x024
222 UARTFBRD_t uartfbrd; //0x028
223 UARTLCR_H_t uartlcr_h; //0x02C
224 UARTCR_t uartcr; //0x030
225 UARTIFLS_t uartifls; //0x034
226 UARTIMSC_t uartimsc; //0x038
227 UARTRIS_t uartris; //0x03C
228 UARTMIS_t uartmis; //0x040
229 UARTICR_t uarticr; //0x044
230 UARTDMACR_t uartdmacr; //0x048
231 } PL011_t;
232
233 #define UART_BASE_ADDRESS0 0x10009000
이렇게 구조체를 만들었다면, PL011.uartdr.bits.DATA .. 이런식으로 접근이 가능해진다.
UART 변수를 생성하자. 기본적으로 UART 에 접근하기 위한 volatile 변수이다.
(임베디드에서 volatile 사용하는 이유? 일반적으로 컴파일러는 같은 주솟값에 이루어진 여러 명령을 생략하고 마지막 것만 실행시키는 것과 같은 최적화를 수행한다. 하지만 레지스터 주솟값에 쓰기 를 실행한다는것은 그 자체로 특정 명령을 실행 시키는 것이므로 자동 최적화에 의한 예외 상황을 방지 하기 위해서 volatile 를 사용하여 해당 변수에 대한 컴파일러 자동 최적화를 비활성화한다.)
"automatically optimized"
int a;
a = 0;
a = 1;
->
a = 1;
"with volatile"
volatile int a;
a = 0;
a = 1;
->
a = 0;
a = 1;
hal/rvpb/Regs.c
1 #include "stdint.h"
2 #include "Uart.h"
3
4
5 volatile PL011_t *Uart = (PL011_t*)UART_BASE_ADDRESS0;
Hardware Abstraction Layer
여러 하드웨어에서 사용할 수 있는 공용 인터페이스를 사용하여 기능 코드를 작성하는 API 의 설계를 HAL 이라고 함.
기능코드 -> 특정 그룹의 디바이스 공용 인터페이스 -> 개별 디바이스 코드
UART 의 HAL 공용 인터페이스 hal/HalUart.h
1 #ifndef HAL_HALUART_H
2 #define HAL_HALUART_H
3
4 void Hal_uart_init(void); //초기화 루틴
5 void Hal_uart_put_char(uint8_t ch); //터미널 출력 루틴
6 uint8_t Hal_uart_get_char(void); //터미널 입력 루틴
7
8 #endif //HAL_HALUART_H
이러한 루틴의 실제 동작 코드는 하드웨어 별로 다르게 짜야한다. 컴파일시 하드웨어별로 다르게 링킹 시킴으로써 구현함.
상위 계층에서 사용할 기능코드는 하드웨어별 코드를 생각하지 않고 공용 인터페이스만 사용하면 됨.
우리는 rvpb 구현 중이므로 hal/rvpb/Uart.c 에 실제 동작 코드를 작성하겠음.
위에서 설명한 것 처럼 구조체 접근법을 사용하여 각 레지스터에 접근할 수 있다.
레지스터에 접근하는 시간보다 변수의 값에 접근하는것이 더 짧다는 것을 활용해서 최적화를 수행하자.
hal/rvpb/Uart.c
1 #include "stdint.h"
2 #include "Uart.h"
3 #include "HalUart.h"
4
5 extern volatile PL011_t *Uart; //defined in Regs.c
6
7 void Hal_uart_init(void) {
8 // Enable Uart
9 Uart->uartcr.bits.UARTEN = 0; //disable UART
10 Uart->uartcr.bits.TXE = 1; //enable Transmit
11 Uart->uartcr.bits.RXE = 1; //enable Receive
12 Uart->uartcr.bits.UARTEN = 1; //re-enbale UART
13 }
14
15 void Hal_uart_put_char(uint8_t ch) {
16 while(Uart->uartfr.bits.TXFF); //wait until the FIFO buffer is empty
17 Uart->uartdr.all = (ch & 0xFF); //write to Data bit in data register size of 8 bit
18 }
19
20
21 uint8_t Hal_uart_get_char(void) {
22 uint8_t data;
23
24 while(Uart->uartfr.bits.RXFE);
25
26 data = Uart->uartdr.all;
27
28 //Check for an errfor flag
29 if(data & 0xFFFFFF00)
30 {
31 Uart->uartrsr.all = 0xFF; //write to the RSR register will clear the errors
32 return 0;
33 }
34 return (uint8_t)(data & 0xFF);
35 }
Makefile 수정
4 TARGET = rvpb
...
17 VPATH = boot \ #when a source file cannot be found, makefile automatically will try to find it from these directories.
18 hal/$(TARGET) \
...
21 C_SRCS = $(notdir $(wildcard boot/*.c)) #boot/main.c -> main.c DIR 경로가 boot와 hal 두개 이상이 되었으므로, notdir 을 사용하여 dir 경로를 삭제한다.
22 C_SRCS += $(notdir $(wildcard hal/$(TARGET)/*.c))
24 C_OBJS = $(patsubst %.c, build/%.o, $(C_SRCS)) #notdir 로 경로가 삭제된 SRCS 를 패턴에 매칭 시킬 수 있도록 boot/%.c 가 아닌 %.c 으로 수정하였다.
...
57 build/%.o: %.S #이것 또한 boot/%.S 가 아닌 %.S 으로 수정하였다. VPATH 에 의해 자동으로 소스 파일을 찾을 수 있음.
58 mkdir -p $(shell dirname $@)
59 $(CC) -march=$(ARCH) -mcpu=$(MCPU) $(INC_DIRS) $(CFLAGS) -o $@ $<
60
61 build/%.o: %.c
62 mkdir -p $(shell dirname $@)
63 $(CC) -march=$(ARCH) -mcpu=$(MCPU) $(INC_DIRS) $(CFLAGS) -o $@ $<
앞서 만든 하드웨어에 직접 접근하는 함수인 Hal_uart_put_char 를 사용하여 문자 1개를 출력할 수 있게 되었다. 문자열을 출력하기 위해서는 이 함수를 사용하는 기능코드를 만들어야 한다.
lib/stdio.h
1 #include "stdarg.h" //가변 인자를 위한 헤더파일.
2
3 #ifndef LIB_STDIO_H
4 #define LIB_STDIO_H
5
6 #define NULL 0
7
8 typedef enum utoa_t {
9 utoa_dec = 10,
10 utoa_hex = 16,
11 } utoa_t;
12
13 uint32_t putstr(const char *ch);
14 uint32_t debug_printf(const char *format, ...); // ... 은 가변 인자 지정이다.
15 uint32_t vsprintf(char *printf_buf, const char *format, va_list arg);
16 uint32_t utoa(char *buf, uint32_t val, utoa_t base);
17
18 #endif //LIB_STDIO_H
먼저 printf 를 만들것인데, printf 는 3단계로 이루어져 있다.
printf -> vsprintf(가변인자 처리) -> utoa(unsigned int 를 printable character로 변환)
debug_printf 는 %s, %c와 같은 파라미터의 개수를 가변으로 처리하기 위해서 "..." 을 써서 가변 인자를 지정한다.
(가변 인자 함수(Variabic Arguments), 가변 인자는 함수의 고정 인자 값 이후에 "..." 으로 표시된다. 가변 인자들은 각각 va_~~ 매크로를 사용하여 접근 가능하다. 이 함수들은 컴파일러 built-in 함수들이므로, define 을 사용하여 따로 매크로를 만드는것이 간편하다.
va_list: va_start,copy 등에 의해 저장된 가변 변수가 저장된 전용 list 이다.
va_start(ap, param_n): va_list ap 를 initializing 한다.
ap - va_list 타입의 오브젝트, param_n - 첫번째 가변 인자 앞의 고정 인자
va_arg(ap, T): 지정된 위치에서 T type 값을 리턴하고 ap가 다음 파라미터를 가르키게 한다.
T - 다음 파라미터의 타입.)
lib/stdarg.h
1 #ifndef LIB_STDARG_H
2 #define LIB_STDARG_H
3
4 typedef __builtin_va_list va_list;
5
6 #define va_start(v,l) __builtin_va_start(v,l)
7 #define va_end(v) __builtin_va_end(v)
8 #define va_arg(v,l) __builtin_va_arg(v,l)
9
10 #endif //LIB_STDARG_H
lib/stdio.c
1 #include "stdint.h"
2 #include "stdio.h"
3 #include "stdarg.h"
4 #include "HalUart.h"
5
6 #define PRINTF_BUF_LEN 1024
7
8 static char printf_buf[PRINTF_BUF_LEN]; //1KB 최종 값이 저장될 버퍼
9
10 uint32_t putstr(const char *s) { //UART HAL 공용 인터페이스를 사용하여 string 을 출력하는 함수
11 uint32_t c = 0;
12 while(*s) {
13 Hal_uart_put_char(*s++);
14 c++;
15 }
16 return c;
17 }
18
19 uint32_t debug_printf(const char *format, ...) {
20 va_list args;
21 va_start(args, format);
22 vsprintf(printf_buf, format, args); //vsprintf 를 사용하여 "%" 가 포함된 문자열을 최종 문자열로 변환한 값을 printf_buf 에 저장함.
23 va_end(args);
24
25 return putstr(printf_buf);
26 }
여기 vsprintf 에서는 많은 포맷을 처리하지 않을 것이고, %c, %s, %u, %x 만 처리할 것이다.
lib/stdio.c
char, string, dec(10진수), hex(16진수)
28 uint32_t vsprintf(char *buf, const char *format, va_list args) {
29 uint32_t c = 0;
30
31 char ch;
32 char *str;
33
34 uint32_t uint;
35 uint32_t hex;
36
37 for(uint32_t i = 0; format[i]; i++) {
38 if(format[i] == '%') {
39 i++;
40 switch(format[i]) {
41 case 'c':
42 ch = (char)va_arg(args, int32_t);
43 buf[c++] = ch;
44 break;
45 case 's':
46 str = (char*)va_arg(args, char*);
47 if (str == NULL) {
48 str = "(null)";
49 }
50 while(*str) {
51 buf[c++] = (*str++);
52 }
53 break;
54 case 'u': //convert unsigned into readable string
55 uint = (uint32_t)va_arg(args, uint32_t);
56 c += utoa(&buf[c], uint, utoa_dec);
57 break;
58 case 'x':
59 hex = (uint32_t)va_arg(args, uint32_t);
60 c += utoa(&buf[c], hex, utoa_hex);
61 break;
62 }
63 }
64 else {
65 buf[c++] = format[i];
66 }
67
68 if(c >= PRINTF_BUF_LEN) {
69 buf[i] = '\0';
70 return 0;
71 }
72 }
73
74 buf[c] = '\0';
75 return c;
76 }
utoa 는 10진수 및 16진수를 문자열로 표현할 수 있게 해준다.
78 uint32_t utoa(char *buf, uint32_t val, utoa_t base)
79 {
80 uint32_t c = 0;
81 int32_t idx = 0;
82 char tmp[11];
83
84 do //만약 val 값이 0 이라면 이 루프는 실행조차 되지 않는 것을 방지하기 위해서 do while 문을 사용함.
85 {
86 uint32_t t = val % (uint32_t)base; //extract first digit, if the base is 16, it is possible to extract the number over 10.
87 if(t >= 10) {
88 t = t - 10 + 'A' - '0'; // 12 - 10 + 65 -> 67 in ascii code, it is converted to letter "C" which is hex, -0 for skipping next step
89 }
90
91 tmp[idx] = (t + '0'); //if the extracted number is lower than 10, it should be converted to decimal letter which is represented from number 48('0').
92 val /= base; //jump into next digit
93 idx++;
94 } while(val);
95
96 idx--;
97 while(idx >= 0) {
98 buf[c++] = tmp[idx];
99 idx--;
100 }
101
102 return c;
103 }
utoa 함수에서 % 와 / 연산자를 사용했는데, ARM 은 기본적으로 이를 지원하는 하드웨어가 없다고 간주한다. 따라서 gcc 에서 제공하는 라이브러리로 링킹해야한다.
8 LD = arm-none-eabi-gcc //gcc 내부에서 자동으로 링킹을 수행해준다.
...
32 LDFLAGS = -nostartfiles -nostdlib -nodefaultlibs -static -lgcc
//stdandard library 들을 사용하지 않겠다....lgcc 옵션을 사용해서 gcc에서 사용하는 library 만은 사용하겠다고 설정함.
...
53 $(navilos): $(ASM_OBJS) $(C_OBJS) $(LINKER_SCRIPT)
54 $(LD) -n -T $(LINKER_SCRIPT) -o $(navilos) $(ASM_OBJS) $(C_OBJS) -Wl,-Map=$(MAP_FILE) $(LDFLAGS) //-Wl 는 링커 옵션.
55 $(OC) -O binary $(navilos) $(navilos_bin)
여기까지 완료했다면 debug_printf 함수를 사용하여 포맷을 사용한 출력이 가능해진다.