티스토리 뷰
RTOS 뿐만 아니라 모든 OS는 커널에서 사용하기 위한 타이머가 있다. 그리고 OS에서 사용할 수 있도록 많은 CPU에는 전용의 타이머를 제공하기도 한다.
이번 글에서는 커널에서 사용하는 system timer에 대해서 설명하도록 하겠다.
먼저 os.h 파일에 필요한 내용을 추가해 보도록 하겠다.
OS_TICKS_PER_SEC은 1초에 몇번의 tick을 발생 시킬것인지 설정하는 역할을 한다. OS 타이머는 이 tick을 기준으로 동작한다. 디폴트값으로 100으로 설정되도록 하였으므로 10msec마다 한번씩 타이머 인터럽트가 발생하게 된다. 만약 1msec 마다 타이머 인터럽트를 발생시키고자 한다면 BOARD_TICKS_PER_SEC를 이용하여 OS_TICKS_PER_SEC값을 1000으로 설정해 주면 된다. 초당 발생하는 tick count값이 크면 클수록 해상도가 높아지게 되므로 정밀한 동작을 수행할 수 있게 된다. 그렇다고 해서 tick count를 무작정 크게 하는것이 항상 좋은 것은 아니다. 그만큼 자주 타이머 인터럽트가 발생하게 된다는 뜻이된다. 인터럽트가 발생할 때 어떤 동작을 수행하여야 하는지 이미 앞에서 설명하였기 때문에 여기에서는 굳이 설명할 필요가 없을 것이다.
만약 실제로 만드는 시스템이 그다지 정밀한 시간을 다루지 않는다면 오히려 tick count를 낮게 하는것이 더 좋은 방법일 수 있다.
OS_SCHED_TIMESLICE_TICKS는 같은 우선순위를 가지는 타스크들끼리는 일정한 시간이 경과되면 CPU 점유권을 양보해주어야 한다. 즉, 라운드 로빈 방식으로 동작 시켜줘야 한다는 뜻이다. megaOS에서는 100msec 동안 동작하게 되면 같은 우선순위의 다른 타스크가 running 상태로 있는 경우, CPU 점유권을 넘겨주도록 하고 있다.
System TImer를 지원하기 위하여 TCB 구조체도 조금 수정해 주어야 한다.
10번 줄은 위에서 설명하였듯이 타스크가 얼마만큼 CPU를 점유하고 있는지 기록하기 위하여 timeslice_count를 각각의 타스크가 가지고 있어야 한다.
13번은 타스크가 일정 시간동안 sleep 상태로 전환될 때 사용하기 위하여 timer 알람을 가지고 있어야 한다.
17~18은 타스크가 sleep 상태로 들어간 이유와 sleep 상태에서 깨어나게된 이유를 저장하기 위하여 reason 필드를 필요로 한다.
타이머 설정과 관련된 inline 함수도 os.h 파일에 추가한다.
코드가 복잡하지 않으므로 쉽게 이해할 것이다.
TCB 구조체가 수정되었으므로 당연히 os.task.c 파일도 몇가지 기능이 추가 되어야 할 것이다.
21번줄에 시스템 타이머에 예약해 놓은 시간이 종료되었을때 이를 알려주기 위하여 alarm을 등록해 놓고 있다.
timer_alarm() 함수는 다음과 같이 만든다.
10번은 타스크가 sleep 상태로 들어간 이유가 일정 시간 delay를 주기위한 경우였기 때문에 시간이 종료되었다는 것을 알려주는것이고,
14번은 어떤 이벤트를 기다리고 있는데, 그 시간동안 이벤트가 발생하지 않아 timeout 되었다는 것을 알려주는 코드이다.
22번은 이유야 어떻든 시간이 종료되었으므로 sleep 상태의 타스크를 깨우도록 하는것이다.
타스크를 깨우기 위한 task_wake()와 타스크가 sleep 상태로 들어가기 위한 task_sleep() 함수의 모습이다.
일정 시간 동안 delay를 주기 위하여 os_task_delay() 함수를 사용하면 된다.
os_task_delay()의 인자는 얼마만큼의 시간동안 delay를 할 것인지 알려주기 위한 tick count값이다. 예를 들어 OS_TICKS_PER_SEC의 값이 100일때 os_task_delay(1); 의 경우 1tick = 10msec가 되므로 10msec동안 sleep 상태로 있다가 다시 깨어 나게 된다.
인자로 들어가는 값이 tick count이므로 실제로 시간으로는 얼마가 될지는 잘 알수 없게 되는 경우가 있다. 예를 들어 250msec 동안 delay를 주고 싶을 때 이에 해당되는 tick 값으로 변환하여야 한다. 이런 필요에 의하여 다음과 같은 매크로를 제공한다.
MS_TO_TICKS()에 msec값을 넣어주면 OS_TICKS_PER_SEC에 따른 tick 값으로 변환해 주게 된다.
가장 중요한 스케줄러도 시스템 타이머를 지원하기 위하여 조금의 코드 수정이 필요하다.
라운드 로빈을 지원하기 위하여 timeslice 기능이 추가되었다. os_sched.c 파일이 이전과 어느 부분이 달라졌는지 한줄씩 비교해 가면 분석해 보기 바란다.
sched_timeslice()는 시스템 타이머 인터럽트가 발생할 때마다 한번씩 불리워지도록 되어 있다. 어떤 타스크가 일정 시간동안 계속 점유하고 있을때 필요한 경우 같은 우선순위의 타스크와 강제로 타스크 스위칭이 되도록 한다.
시스템 타이머를 담당하는 코드를 만들어 보도록 하겠다.
kernel/os_timer.c 파일을 만들고 다음과 같은 코드를 추가한다.
16번은 카운트를 하나 만들어 system_timer에 저장한다.
17번은 타이머가 동작할 때 이를 처리하기 위하여 인터럽트를 등록하는 코드이다.
20번은 CPU의 타이머를 설정하기 위한 코드이다.
타이머 인터럽트가 발생하면 sys_timer_isr()로 들어오는데 여기에서는 특별히 하는게 없고, 실제 일은 sys_timer_dsr()에서 처리한다.
10번은 system_timer 카운트에 연결된 알람들을 처리한다.
11번은 위에서 설명했듯이 라운드로빈을 지원하기 위하여 스케줄러에게 tick이 발생했음을 알려준다.
이렇게 megaOS에서 사용하는 시스템 타이머에 대해 간단히 알아보았으므로 실제 타이머가 제대로 동작되는지 확인해 보도록 하겠다.
os_init() 함수에 system_timer_init();를 추가하여 시스템 타이머를 설정하도록 한다.
os_start() 함수에서 첫번째 타스크인 idle task를 수행하기 위한 arch_load_context()을 부르기 직전에 os_interrupt_unmask(SYS_TIMER_IRQ);를 넣어서 타이머가 인터럽트를 발생시키도록 한다.
main_task()를 다음과 같이 만든다.
프로젝트를 빌드하고 보드에서 실행 시켜보자.
LED가 1초 주기로 깜빡이는것을 볼 수 있을것이다.
RTOS를 사용하지 않는 프로그램이라면 일정 시간 delay를 주기 위해서는 루프함수를 사용하여 일정 시간이 경과될 때까지 계속 반복코드를 수행한다거나, 아니면 CPU에 있는 타이머를 설정하여 타이머 레지스터의 카운트값이 어느 값에 도달했는지 모니터링하거나, flag 레지스터를 계속 polling 하고 있어야 한다. 방법이야 어떻든 그 시간동안 다른일은 수행하지 못하고 시간만 지나기를 기다리고 있는것이나 다름없게 된다.
하지만, RTOS에서는 지정된 시간이 될때까지 다른 작업을 수행하다가, 시간이 되면 다시 깨어나 필요한 동작을 수행한 뒤 다시 다른 작업을 계속 수행할 수 있게 된다.
이것이 바로 RTOS를 사용하는 이유일 것이다.
이번글에서 타이머를 사용하기 전에는 idle task와 main task 두개가 있었지만 실제로는 main task가 CPU를 100% 독점하고 있었고, idle task는 단 한번도 CPU를 사용해 보지 못하고 있었다.
하지만, 이제는 99%이상 idle task가 CPU를 사용하게 되고, 아주 잠깐동안만 main task가 CPU를 사용하여 LED를 점멸하게 되는 것이다.
delay 값으로 OS_TICKS_PER_SEC/2를 사용하였으므로 정확하게 1초의 절반은 LED가 꺼져 있게 되고, 나머지 절반은 켜져 있게 된다.
만약 900msec 동안 LED가 꺼져있고, 100msec 동안만 LED를 켜지게 하려면 어떻게 하면 될까?
코드를 위와 같이 변경하여 보드에서 어떻게 동작되는지 확인해 보자.
마지막으로 타스크를 좀더 많이 만들어 여러개의 타스크가 각각의 동작을 잘 수행하는지 확인해 보도록 하겠다.
25번은 main_task()가 시작되면서 다른 타스크를 생성 시키기 위한 함수를 부르는 것이다.
19번은 spin_task를 생성 시키는데 동일한 코드의 타스크를 6개를 만드는 것이다. 타스크 함수의 인자로 1~6을 주게 된다.
2~11이 spin_task() 함수로써 인자로 들어온 값만큼 delay되었다가 spin[]에 있는 글자를 한번씩 출력하도록 한다.
4번에 선언된 count 변수는 함수내에서 선언되었으므로 타스크의 스택 메모리에 위치하게 된다. 그러므로 여섯개의 count 변수가 메모리상에 존재하게 된다.
참고로 printf() 함수를 사용할 때 스택메모리를 많이 사용하므로 최소 스택 사이즈를 128바이트로 변경하여야 한다.
AVR과 같이 SRAM의 크기가 작을때는 가급적 stdio를 사용하지 않는것이 좋다. 쓸때없이 자원만 많이 소모하기 때문이다.
프로그램을 실행 시켰을때의 결과가 위의 그림처럼 나온다.
두번째 줄에 여섯개의 글자가 보이는데, 글자 하나가 spin_task 하나가 된다. 그러므로 모두 여섯개의 spin_task가 동작되는것을 확인 할 수 있다.
제일 앞에 있는 spinner는 1초에 한번씩 회전한다. 두번째는 2초에 한번씩 회전하고, 나머지 spinner들도, 3초,4초,5초,6초에 한번씩 회전하게 된다.
물론 LED는 변함없이 900msec 꺼져 있다가, 100msec 동안만 켜졌다가 다시 꺼지게 된다.
위의 동영상은 PORTC에 여섯개의 LED를 연결하여 각각의 타스크가 LED를 점멸하는 것을 보여주고 있다. 초록색 LED가 한번 점멸할 때마다 아래 보이는 여섯개의 LED가 왼쪽에서부터 차례대로 1초,2초,3초,4초,5초,6초에 한번씩 상태가 변하는것을 확인할 수 있다. 여섯개의 LED 타스크와 main task, idle task 이렇게 모두 8개의 타스크가 독립적으로 동작되고 있다.
지금까지 megaOS의 기본 기능에 대한 설명을 진행하였다.
다음으로는 타스크간에 정보를 주고 받기 위한 synchronization에 대해서 설명하도록 하겠다.
'심화' 카테고리의 다른 글
megaOS - 12. Flag (0) | 2016.11.08 |
---|---|
megaOS - 11. Semaphore (0) | 2016.11.07 |
megaOS - 9. Counter (0) | 2016.10.29 |
megaOS - 8. Interrupt (0) | 2016.10.29 |
megaOS - 7. Context(Task) Switching (0) | 2016.10.27 |