Hardware Abstraction Layer

안태욱·2023년 1월 31일
2
post-thumbnail
post-custom-banner

1. HAL이란?

Hardware Abstraction Layer

하드웨어 추상화 계층(HAL, Hardware Abstraction Layer)은 컴퓨터의 물리적인 하드웨어와 컴퓨터에서 실행되는 소프트웨어 사이의 추상화 계층이다. 이것은 하드웨어의 차이를 숨겨서 응용 프로그램이 작동할 수 있는 일관된 플랫폼을 제공한다.

임베디드 SW를 개발할 때는 target HW의 데이터 시트를 참고하며 개발을 진행하기 때문에 HW에 의존적인 코드가 작성될 수 밖에 없다. 만약 기능을 구현하는 코드와 HW에 의존적인 코드가 강하게 결합되어 있다면, HW가 변경되었을 때 SW를 이식하는데 큰 비용이 발생할 것이다.

HAL이 없을 때

예를 들어 동일한 기능을 수행하는 HW가 2개 있다고 가정한다. 이 두 HW는 수행하는 기능은 같지만 레지스터의 수나 제어 방법에서 차이가 있을 수 있다. 이런 경우에는 각각의 HW에 의존적인 코드가 작성된다. 만약 기능을 구현하는 코드가 이들 중 HW 1에 의존하는 코드와 강하게 결합되어 있다면, 해당 코드는 HW 2와 호환되지 않는다. 만약 사용하고자 하는 HW가 1에서 2로 변경된다면 기능 구현 코드도 HW 2에 호환되도록 변경해야 하기 때문에 수정해야 할 코드가 많아진다.

HAL이 있을 때

위와 같은 경우를 방지하기 위해 HAL을 사용할 수 있다. 이를 구현하기 위해서는 HW의 기능을 사용하기 위한 공용 인터페이스를 정의해야 한다. 그리고 기능 구현 코드는 이 공용 인터페이스만을 이용하도록 코드를 작성해야 한다. 공용 인터페이스는 헤더 파일에 정의할 수 있으며, 각 HW에 의존하는 코드들은 소스 파일에서(.c) 이 헤더 파일을 include 하여 인터페이스를 구현할 수 있다. 이러한 방식을 사용하면 사용하고자 하는 HW가 변경되더라도 기능 구현 코드를 수정하지 않아도 된다.



2. 레지스터의 추상화

HW를 제어하려면 데이터 시트를 참고하여 HW가 사용하고 있는 레지스터들과 이들의 역할을 파악해야 한다. 그리고 이에 따라 레지스터들을 제어하여 원하는 동작을 구현할 수 있다. 이를 위해서는 각각의 레지스터들을 코드로 제어할 수 있게 해줄 방법이 필요하다. 단순히 포인터를 통해 레지스터에 해당하는 주소의 값을 변경할 수도 있지만, 이는 가독성이 떨어질 수 있으며 사용하고자 할 레지스터의 주소를 매번 확인해봐야 하는 번거로움이 있다. 이를 해결하기 위해서 C언어의 구조체와 공용체를 활용하여 레지스터를 추상화 할 수 있다.

PL011 UART Device

데이터 시트에 따르면, realview-pb-a8 디바이스는 PL011이라는 UART 디바이스를 사용하고 있다.

PL011의 데이터 시트를 살펴보면 위와 같은 레지스터들을 확인할 수 있다. offset 정보도 함께 기록되어 있는데, realview-pb-a8 데이터 시트에 적힌 UART 디바이스 base 주소를 이용하여 각 레지스터들의 실제 위치를 파악할 수 있다.

추가로 0x0000의 offset을 가지는 UARTDR 레지스터를 살펴보면 레지스터가 여러개의 비트 필드로 나누어져 있음을 알 수 있다. 이처럼 각각의 레지스터들은 모두 다른 역할을 수행하며 각 레지스터들 내부의 비트가 의미하는 바가 다르기 때문에, C언어를 통해 각각의 레지스터들과 내부의 비트들에 접근할 방법이 필요하다.

구조체와 공용체의 활용

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;
UARTDR_t* uartdr = (UARTDR_t*) (UART0_BASE_ADDR + UARTDR_OFFSET);

구조체와 비트필드

구조체에서 비트필드를 사용하면 정수형 자료형을 비트 단위로 나누어 사용할 수 있다. 코드를 살펴보면 DATA 필드에 8비트, FE, PE, BE, OE 필드에 각각 1비트, reserved 필드에 20비트를 할당한 것을 볼 수 있는데, 이는 UARTDR 레지스터의 세부 비트필드들을 코드로 옮긴 것이다. 이렇게 총 32비트를 사용하고 있는데, 이는 unsigned int 자료형의 크기인 4바이트를 넘어가지 않기 때문에 해당 bits 구조체는 총 4byte의 크기를 가지게 된다. 이처럼 나눈 비트에 접근할 때는 bits.DATA와 같이 . 연산자를 통해 접근할 수 있다.

공용체

공용체는 멤버로 가지는 필드들 중에서 가장 크기가 큰 자료형 만큼 메모리를 확보하고 이를 모든 필드가 공유해서 사용한다. 위의 UARTDR_t 공용체를 살펴보면 uint32_t 타입의 all 필드와 비트필드를 사용한 bits 구조체 필드를 가지고 있음을 알 수 있다. 이들은 모두 4byte의 크기를 가지기 때문에 해당 공용체 또한 4byte의 크기를 가진다. 그리고 이 4byte의 메모리 공간을 all과 bits 필드가 공유하는데, 레지스터 값 전체에 접근할 때는 uartdr->all로 접근할 수 있으며, 해당 레지스터 내부의 일부 비트에만 접근할 때는 uartdr->bits.DATA와 같은 방식으로 접근할 수 있다.

이처럼 구조체와 공용체를 혼합하여 하나의 레지스터를 코드로 표현할 수 있다. 레지스터 구조체 포인터 변수를 선언하고 base 주소와 offset을 더한 값으로 초기화하고 나면 -> 룰 통해 포인터가 가리키는 곳 즉, 레지스터에 접근할 수 있다.

레지스터들을 모은 구조체 정의

위의 PL011의 데이터 시트를 살펴보면 UARTDR 레지스터 이외에도 UARTCR 등과 같은 여러개의 레지스터가 존재함을 알 수 있다. 이들 각각의 레지스터들을 위와 같은 방법을 사용하여 공용체로 정의하고, 각각의 offset에 맞도록 하나의 구조체로 모아두면 보다 쉽게 각 레지스터들에 접근할 수 있다.

PL011_t 라는 구조체를 정의하고 각각 레지스터들의 offset에 맞게 필드를 배치하였다.

#define UART_BASE_ADDRESS0       0x10009000
volatile PL011_t* Uart = (PL011_t*) UART_BASE_ADDRESS0;

Uart->uartdr.all;
Uart->uardcr.bits.DATA;

realview-pb-a8 데이터 시트에서 UART0 디바이스의 base address에 따라 위와 같이 매크로를 정의하였다. 그리고 PL011_t 포인터 타입의 변수를 선언하고 UART0의 base address 값을 갖도록 초기화하였다. 이후에는 각 레지스터에 접근할 때 Uart->uartdr.bits.DATA와 같은 코드를 사용할 수 있다. PL011_t 구조체를 정의할 때 각각의 레지스터들을 offset에 맞게 배치하였기 때문에 이와 같은 방식으로 구조체에 접근하였을 때 접근하고자 하는 레지스터의 주소가 자동으로 계산된다.

이와 같은 레지스터 추상화 소스는 일반적으로 모두 구현하는 것이 아니라 HW 제조사에서 제공하는 파일을 사용한다.


3. HAL 구현하기

공용 인터페이스 정의와 레지스터 추상화를 통해 HAL을 구현할 수 있다. 해당 포스팅에서는 UART를 예시로 들어 HAL을 구현한다. 우선은 작업 디렉터리에 hal 이라는 디렉터리를 생성하고 내부에 HalUart.h 파일을 생성한다.

HalUart.h

#ifndef HAL_HALUART_H_
#define HAL_HALUART_H_

#include "stdint.h"

void Hal_uart_init(void);
void Hal_uart_put_char(uint8_t ch);
uint8_t Hal_uart_get_char(void);

#endif

이는 UART HW를 사용하기 위한 공용 인터페이스이다.

  • 하드웨어 초기화
  • 문자 입력
  • 문자 출력

이와 같은 3가지 인터페이스를 제공한다. 기능을 구현할 때 UART HW를 사용하기 위해서는 이러한 공용 인터페이스를 통해 접근해야 한다.

HW 플랫폼 별 소스 분리

공용 인터페이스를 정의한 이후에는 hal 디렉터리 내부에 HW 플랫폼 디렉터리를 생성한다. 해당 실습에서는 rvpb (realview-pb-a8) 라는 이름을 가지는 디렉터리를 생성하였다. 그리고 해당 디렉터리 내부에 다음과 같은 코드를 작성한다.

Uart.h

#ifndef HAL_RVPB_UART_H_
#define HAL_RVPB_UART_H_

#include "stdint.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 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_ */

레지스터를 추상화하여 정의한 구조체와 공용체들이 해당 파일에 위치한다.

Regs.c

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

volatile PL011_t* Uart = (PL011_t*) UART_BASE_ADDRESS0;

realview-pb-a8 디바이스에서 사용하는 여러 HW의 레지스터 구조체 포인터를 선언하고 base address를 매핑하는 파일이다. 현재는 UART 밖에 없으나 추후 프로젝트가 진행됨에 따라 여러 포인터를 추가로 작성한다.

Uart.c

#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; // enable uart output
    Uart->uartcr.bits.RXE - 1; // enable uart input
    Uart->uartcr.bits.UARTEN = 1;
}

void Hal_uart_put_char(uint8_t ch) {
    while(Uart->uartfr.bits.TXFF); // wait until output buffer is empty
    Uart->uartdr.all = (ch & 0xFF);
}

uint8_t Hal_uart_get_char(void) {

    uint8_t data;

    while(Uart->uartfr.bits.RXFE); // wait until input buffer is filled

    data = Uart->uartdr.all;

    // check 4 error flag
    if (data & 0xFFFFFF00) {

		...

        // clear all error flag
        Uart->uartdr.all = 0xFF;
        return 0;
    }

    return (uint8_t)(data & 0xFF);
}

해당 파일은 HalUart.h에서 정의한 공용 인터페이스를 구현한다. 이는 rvpb 플랫폼의 UART를 위한 코드로 PL011 HW를 추상화해놓은 Uart.h 까지 include 한다. 그리고 전역 변수로 선언해놓은 Uart를 활용하여 기능을 구현하고 있다.

로직의 구현은 데이터 시트를 보고 구현하면 되는데, QEMU 환경에서는 최소한의 코드만 있어도 동작한다. 실제 HW를 제어할 때는 이보다 더 복잡한 코드가 필요하다.

프로젝트 구조

해당 포스팅에서 작업한 내역을 트리 구조로 나타낸 그림이다. 예를 들어 아두이노와 같은 다른 플랫폼을 지원해야 한다면 hal 디렉터리 내부에 arduino 디렉터리를 생성하고 rvpb 하위에 소스를 작성했던 것 처럼 arduino의 데이터 시트를 참고하여 코드를 작성하면 된다. 그리고 빌드 시 경로를 다르게 설정하여 다른 플랫폼에서 실행할 최종 실행 파일을 생성할 수 있다.


4. 기능 구현 및 확인

문자열 출력 구현

#include "HalUart.h"

uint32_t putstr(const char* s) {

    uint32_t count = 0;

    while(*s) {
        Hal_uart_put_char(*s++);
        count++;
    }

    return count;
}

지금까지 작성한 HAL UART를 통해서 터미널로 문자를 출력하는 기능을 구현할 수 있다. 이와 같이 HalUart.h에서 정의한 인터페이스를 활용하여 기능을 구현한다.

main() 작성

#include "HalUart.h"

static void Hw_init(void);

void main(void) {

    Hw_init();

    putstr("Hello World!\n");
    putstr("This is UART test.\n");

}

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

실행 결과

profile
책 읽는 개발자
post-custom-banner

0개의 댓글