본문 바로가기

임베디드

[RTOS 개발하기] 임베디드 OS 개발 프로젝트ch.7

https://www.yes24.com/Product/Goods/84909414

 

임베디드 OS 개발 프로젝트 - 예스24

나만의 임베디드 운영체제를 만들어 보자.이 책은 펌웨어 개발 과정을 실시간 운영체제(RTOS)를 만들어 가며 설명한다. 임베디드 운영체제를 개발 환경 구성에서 시작해 최종적으로 RTOS를 만드는

www.yes24.com

 

공부 목적으로 이만우님의 저서 "임베디드 OS 개발 프로젝트"를 따라가며, RTOS "Navilos"를 개발하는 포스트입니다. 모든 내용은 "임베디드 OS 개발 프로젝트"에 포함되어 있습니다.

개발 목적

임베디드 시스템에 많이 사용되는 RTOS를 직접 개발해 보며 다음과 같은 지식을 학습하고자 합니다.
 - RTOS의 핵심 개념에 대해 학습한다.
 - 운영체제 핵심 기능을 설계해 보며 학습한다.
 - 펌웨어에 대한 진입장벽을 낮춘다.
 -  ARM 아키텍처에 대해 학습한다.
 - 하드웨어에 대한 지식을 학습한다.(펌웨어가 어떻게 하드웨어를 제어하는지)


이번 챕터는 타이머를 다룹니다. 임베디드 시스템에서는 시간에 의존해 제어하는 경우가 많이 있습니다. 그 기능을 구현하기 위해서는 타이머가 필요합니다.

타이머 구현
일반적으로 타이머는 목표 카운터 레지스터와 측정 카운트 레지스터를 조합하여 활용합니다.
목표 카운트 레지스터 값을 지정하고 측정 카운트 레지스터를 증가 혹은 감소로 설정합니다. 측정 카운트가 증가할 때는 0부터 시작해서 목표 카운트 값과 같아지면 인터럽트를 발생시킵니다. 반대로 측정 카운트가 감소할때는 목표 카운트부터 시작해서 0이 되면 인터럽트를 발생시킵니다.

 

저희가 사용하는 RealViewPB는 SP804라는 타이머 하드웨어를 가지고 있습니다. 이 타이머는 측정 카운터가 감소하는 형식입니다.

이번 목표는 delay()함수 구현입니다.

 


타이머 하드웨어 초기화

항상 새로운 하드웨어를 추가하는 첫 작업은 해당 하드웨어의 레지스터를 구조체로 추상화하여 hal에 추가하는 형식입니다.

 

//hal/rvpb/Timer.h   -> SP804 하드웨어의 레지스터
#ifndef HAL_RVPB_TIMER_H_
#define HAL_RVPB_TIMER_H_

typedef union TimerXControl_t
{
        uint32_t all;
        struct {
                uint32_t OneShot:1;             // 0
                uint32_t TimerSize:1;   // 1
                uint32_t TimerPre:2;    // 3:2
                uint32_t Reserved0:1;   // 4
                uint32_t IntEnable:1;   // 5
                uint32_t TimerMode:1;   // 6
                uint32_t TimerEn:1;             // 7
                uint32_t Reserved1:24;  // 31:8
        }bits;
}TimerXControl_t;

typedef union TimerXRIS_t
{
        uint32_t all;
        struct {
                uint32_t TimerXRIS:1;   // 0
                uint32_t Reserved:31;   // 31:1
        }bits;
}TimerXRIS_t;

typedef union TimerXMIS_t
{
        uint32_t all;
        struct {
                uint32_t TimerXMIS:1;   // 0
                uint32_t Reserved:31;   // 31:1
        }bits;
}TimerXMIS_t;

typedef struct Timer_t
{
        uint32_t                timerxload;             // 0x00
        uint32_t                timerxvalue;    // 0x04
        TimerXControl_t timerxcontrol;  // 0x08
        uint32_t                timerxintclr;   // 0x0C
        TimerXRIS_t             timerxris;              // 0x10
        TimerXMIS_t             timerxmis;              // 0x14
        uint32_t                timerxbgload;   // 0x18
}Timer_t;

#define TIMER_CPU_BASE  0x10011000
#define TIMER_INTERRUPT 36

#define TIMER_FREERUNNING       0
#define TIMER_PERIOIC           1

#define TIMER_16BIT_COUNTER 0
#define TIMER_32BIT_COUNTER 1

#define TIMER_1MZ_INTERVAL              (1024 * 1024)

#endif /* HAL_RVPB_TIMER_H_ */

Timer_t 타입의 구조체를 보면 레지스터 7개가 정의되어 있습니다.

  • timerxload : 카운터의 목표 값을 지정하는 레지스터
  • timerxvalue : 감소하는 레지스터, timerxload의 값을 복사하고 감소함
  • timerxcontrol : 타이머 하드웨어의 속성을 설정하는 레지스터
  • timerxintclr : 인터럽트 처리가 완료되었음을 타이머 하드웨어에 알려주는 레자스터
  • 나머지 3개는 현재 사용하지 않음

하드웨어 속성 설정 → timerXControl_t

  • OneShot : 1이면 타이머 인터럽트가 한 번 발생하고 타이머가 바로 꺼짐
  • TimerSize : timerxload와 timerxvalue의 크기를 설정
  • Timepre는 클럭마다 카운터를 1번, 16번, 256번마다 줄일지를 설정 → 우리는 클럭 1번에 카운터 1씩 줄이는 걸로 하겠습니다.
  • intEnable : 타이머 하드웨어의 인터럽트를 켬
  • TimerMode : timerxload를 사용할지 말지를 경정

TIMER_CPU_BASE(0x10011000) → 타이머 하드웨어 레지스터가 할당되어 있는 주소

타이머 인터럽트 번호 36

 

이제 레지스터 헤더 파일을 만들었으니 초기화 코드를 작성하겠습니다.

이전과 마찬가지로 hal/HalTimer.h파일을 만들어 공용 인터페이스 API를 확정하고 그것에 맞춰 구현 코드를 만들겠습니다.

//hal/HalTimer.h
#ifndef HAL_HALTIMER_H_
#define HAL_HALTIMER_H_

void    Hal_timer_init(void);

#endif /* HAL_HALTIMER_H_ */

아직까진 타이머 관련 어떤 API가 필요한지 몰라 init()만 선언하였습니다. 앞으로 필요한 내용을 추가해 나갈 예정입니다.

이전(UART, GIC)과 마찬가지로 공용 API를 구현하겠습니다.

//hal/rvpb/Timer.c
#include "stdint.h"
#include "Timer.h"
#include "HalTimer.h"
#include "HalInterrupt.h"

extern volatile Timer_t* Timer;

static void interrupt_handler(void);

static uint32_t internal_1ms_counter;

void Hal_timer_init(void)
{
        // interface reset
        Timer->timerxcontrol.bits.TimerEn = 0;
        Timer->timerxcontrol.bits.TimerMode = 0;
        Timer->timerxcontrol.bits.OneShot = 0;
        Timer->timerxcontrol.bits.TimerSize = 0;
        Timer->timerxcontrol.bits.TimerPre = 0;
        Timer->timerxcontrol.bits.IntEnable = 1;
        Timer->timerxload = 0;
        Timer->timerxvalue = 0xFFFFFFFF;

        // set periddic mode
        Timer->timerxcontrol.bits.TimerMode = TIMER_PERIOIC;
        Timer->timerxcontrol.bits.TimerSize = TIMER_32BIT_COUNTER;
        Timer->timerxcontrol.bits.OneShot = 0;
        Timer->timerxcontrol.bits.TimerPre = 0;
        Timer->timerxcontrol.bits.IntEnable = 1;

        uint32_t interval_1ms = TIMER_1MZ_INTERVAL / 1000;

        Timer->timerxload = interval_1ms;
        Timer->timerxcontrol.bits.TimerEn = 1;

        internal_1ms_counter = 0;

        // Register Timer interrupt handler
        Hal_interrupt_enable(TIMER_INTERRUPT);
        Hal_interrupt_register_handler(interrupt_handler, TIMER_INTERRUPT);
}

static void interrupt_handler(void)
{
        internal_1ms_counter++;

        Timer->timerxintclr = 1;
}
더보기

Hal_timer_init()

초기화 → 데이터 시트에 설명된 인터페이스 초기화 절차를 따릅니다

  1. 타이머를 끕니다(TimerEn = 0)
  2. 프리-러닝 모드로 설정해 놓습니다(TimerMode = 0, OneShot = 0)
  3. 16비트 카운터 모드로 설정합니다(TimerSize = 0)
  4. 프리스케일러 분주(divider)는 1로 설정합니다(TimerPre = 0)
  5. 인터럽트를 켭니다(IntEnable = 1)
  6. 로드 레지스터를 켭니다
  7. 카운터 레지스터는 0xFFFFFFFF로 설정합니다

25-34, 는 코드가 피리오딕 모드로 1밀리초 간격으로 인터럽트를 발생하게 타이머를 설정하는 코드입니다.

이 코드에서 가장 중요한 부분은 31번째 줄의 interval 변수의 값을 정하는 코드입니다. 이 interval 변수의 값이 로드 레지스터로 들어갑니다. 따라서 이 값이 타이머 인터럽트의 발생 간격을 지정하는 것입니다.

#define TIMER_1MZ_INTERVAL (1024 * 1024)
TIMER_1MZ_INTERVAL / 1000;

이 코드를 이해해야 왜 31번째 줄에서 간격을 1ms로 설정했는지 알 수 있습니다.

RealViewPB는 타이머 클럭 소스로 1MHz 클럭을 받거나 32.768KHz짜리 크리스탈 오실레이터를 클럭으로 받을 수 있습니다.

그럼 QEMU의 RealViewPB는 무엇을 타이머 클럭으로 쓸까요?

Real-ViewPB의 데이터시트에 따르면 시스템 컨트롤 레지스터0의 15번 비트가 타이머 클럭을 어떤 것으로 설정했는지 알려준다고 합니다.

→ SYSCTRL0(시스템 컨트롤 레지스터0, 주소 0x10001000)의 값을 읽어보면 됩니다. debug_printf()를 이용하자

Main.c의 printf_test() 변경
make 에러

make가 안되는 오류를 발견했습니다. Timer가 정의되어 있지 않다는데 위 코드들을 보면 알 수 있듯이 Timer는 잘 정의해두었습니다.

hal/rvpb/Regs.c에 Timer가 정의되어 있지 않았기 때문이다.
hal/rvpb/Regs.c
#include "stdint.h"
#include "Uart.h"
#include "Interrupt.h"
#include "Timer.h"

volatile PL011_t* Uart = (PL011_t*)UART_BASE_ADDRESS0;
volatile GicCput_t* GicCpu      = (GicCput_t*)GIC_CPU_BASE;
volatile GicDist_t* GicDist = (GicDist_t*)GIC_DIST_BASE;

volatile Timer_t* Timer = (Timer_t*)TIMER_CPU_BASE;​

이후 빌드가 잘 된다.

test 결과

Test 결과를 보면 SYSCTRL0 레지스터의 모든 비트 값은 0입니다. 따라서 QEMU의 RealViewPB는 타이머 클럭으로 1MHz를 쓴다는 것을 알 수 있습니다. 하지만 QEMU는 소프트웨어 에뮬레이터이므로 클럭이 정확하게 동작하진 않습니다.

PL804의 데이터시트를 읽어보면 더 자세히 이해할 수 있습니다.

 


타이머 카운터 오버플로

1미리초 단위로 타이머 카운터를 만들었습니다. 이제 우리가 고려할 점은 카운터 변수가 32비트이기 때문에 오버플로우를 고려해야 합니다. 32비트 변수의 최댓값인 0xFFFFFFFFF까지 증가하면 다시 0으로 만들어주어야 합니다.

 

delay()

타이머 하드웨어를 활성화하고 타이머 카운터를 만들어서 시간 측정을 하는 이유는 시간 지연 함수를 만들어 사용하기 위해서 입니다. 가장 간단한 형태의 시간 지연 함수인 delay()를 구현해보겠습니다.

먼저 stdlib.h 파일을 lib 디렉터리 밑에 만듭니다. delay()와 같은 유틸리티 관련 함수는 stdlib.h에 정의하겠습니다.

//lib/stdlib.h
#ifndef LIB_STDLIB_H_
#define LIB_STDLIB_H_

void delay(uint32_t ms);

#endif /* LIB_STDLIB_H_ */

 

타이머 카운터 변수는 로컬 전역 변수이므로 다른 파일에서 값을 이용하려면 글로벌 전역 변수로 바꾸던 값을 읽을 수 있는 인터페이스로 만들어야 합니다. 여기에서는 인터페이스 함수로 만들어 사용하겠습니다. 인터페이스 함수를 사용하는 장점은 스포트웨어의 구조를 유연하게 만들 수 있기 때문입니다.

글로벌 전역 변수는 공유 메모리에 전역 변수를 배치하는 등 번거러운 작업이 뒤따라 올 가능성이 있지만, 인터페이스 함수는 함수의 리턴값으로 필요한 정보를 전달할 수 있어 복잡한 작업을 줄일 수 있습니다.

 

공용 인터페이스에 API를 추가하겠습니다.

//hal/HalTimer.h
#ifndef HAL_HALTIMER_H_
#define HAL_HALTIMER_H_

void     Hal_timer_init(void);
uint32_t Hal_timer_get_1ms_counter(void);


#endif /* HAL_HALTIMER_H_ */
//hal/rvpb/Timer.c에 함수 정의
uint32_t Hal_timer_get_1ms_counter(void)
{
    return internal_1ms_counter;
}

이제 internal_1ms_counter()함수를 이용해 delay() 함수를 만들어보겠습니다.

//lib/stdlib.c
#include "stdint.h"
#include "stdbool.h"
#include "HalTimer.h"

void delay(uint32_t ms){
        uint32_t goal = Hal_timer_get_1ms_counter() + ms;

        while(goal != Hal_timer_get_1ms_counter());
}
더보기

goal 변수는 목적하는 시간입니다.

delay() 함수가 호출되는 순간 타이머 카운터 값이 100이고, delay() 함수를 통해서 10ms의 시간 지연을 구현한다면 타이머가 110이 될 때까지 기다리는 것입니다.

≠ 연산으로 비교하는 이유는 < 연산으로 비교하면 오버플로를 고려할 수 없기 때문입니다.

 

delay()함수는 준비되었습니다. 이제 테스트 해보겠습니다.

//Main.c에 test코드 추가
#include "stdint.h"
#include "stdbool.h"

#include "HalUart.h"
#include "HalInterrupt.h"
#include "HalTimer.h"

#include "stdio.h"
#include "stdlib.h"

static void Hw_init(void);
static void Printf_test(void);
static void Timer_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();
				Timer_test();
				
        while(true);
}

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


static void Printf_test(void){
        char* str = "printf pointer test";
        char* nullptr = 0;
        uint32_t i = 5;
        uint32_t* sysctrl0 = (uint32_t*)0x10001000;

        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, 0xff);
        debug_printf("print zero %u\n", 0);
        debug_printf("SYSCTRL0 %x\n", *sysctrl0);
}

static void Timer_test(void){
        while(true){
                debug_printf("current counter : %u\n", Hal_timer_get_1ms_counter());
                delay(1000);
        }
}

test 결과

잘 동작하는 것을 확인할 수 있습니다.

 


마치며

이번 챕터에선 타이머 레지스터를 활성화하고, delay() 함수를 구현해보았습니다. 타이머 기능을 활용할 수 있도록 API를 작성하는 것도 흥미로웠지만, 펌웨어에서 레지스터를 하드웨어를 다루는 방법에 조금 더 익숙해진 것 같습니다.

 


참고

RealViewPB 데이터시트 https://developer.arm.com/documentation/dui0417/d/?lang=en

 

Documentation – Arm Developer

 

developer.arm.com

 

저자 이만우님 깃허브 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