이 문서는 FreeRTOS 에서 문맥 전환(Context Switch)이 어떻게 일어나는 지에 관하여 설명합니다. 설명을 위해 Atmel AVR 마이크로컨트롤러의 FreeRTOS port를 예제로 사용할 것이며, 하나의 완전한 문맥전환을 보면서 한 단계 한 단계 세부적으로 분석하는 방식을 통해 진행됩니다.
Contents[hide] |
C 개발 도구
FreeRTOS 의 목표는 ‘간결함’, ‘이해하기 쉬움’ 이다. 이런 목적을 위해 RTOS 소스코드의 대부분은 어셈블리언어가 아니라 C 언어로 작성되었다. 여기서 제시되는 예제는 WinAVR 개발 도구를 사용한다. WinAVR 은 Windows 환경에서 AVR 바이너리 코드를 생성해주는 GCC 기반의 크로스 컴파일러이다. WinAVR 은 다음 사이트에서 무료로 다운로드 가능하다.
http://sourceforge.net/projects/winavr/index.html
RTOS 틱
태스크가 아무 일도 하지 않는 수면(sleep) 모드에 들어갈 경우, 태스크는 깨어날(wake) 시각을 명시한다. 그리고 태스크가 대기(block)모드로 될 경우에 태스크는 최대로 기다릴 수 있는 시간(waiting time)을 명시할 수 있다.
FreeRTOS 실시간 커널은 시간을 측정하는 수단으로 틱 이라는 카운트 변수를 사용한다. 타이머 인터럽트(RTOS tick interrupt)는 절대적인 시간적 정확성을 가지고 증가한다. 이것은 실시간 커널이 타이머 인터럽트의 빈도수를 분석함으로써 시간을 측정할 수 있도록 해 준다.
틱 카운트가 증가할 때마다 실시간 커널은 반드시 지금 태스크가 대기상태에서 벗어나야 하는지 수면 상태에서 깨어나야 하는지를 확인해야한다. 틱 ISR(Interrupt Service Routine)을 수행하는 동안 깨어나거나 재개된 태스크가 ISR에 의해 인터럽트 되기 전에 실행중인 태스크의 우선순위 보다 높을 경우도 발생할 수 있다. 이 경우 틱 ISR 은 새롭게 깨어나거나 재개된 태스크로 돌아가야 한다. 아래에 이 상황이 묘사되어 있다.
<그림1>
①에서 RTOS idle task 가 실행중이다. ②에서 RTOS 틱이 발생하고 제어가 ③의 틱 ISR 로 전환된다. RTOS 틱 ISR 은 vControlTask 가 RTOS idle task 보다 더 높은 우선순위를 가지고 있고 vControlTask 를 실행시킬 준비가 되었음을 확인한 후 문맥 vControlTask 로 전환한다. ④ 이제 실행중인 문맥이 vControlTask 이기 때문에 ISR 이 종료되면서 제어가 vControlTask 로 전환되고 실행을 시작한다.
이런 방식으로 일어나는 문맥 전환을 ‘선점형(preemptive)’ 이라고 한다. 인터럽트를 받은 태스크가 자발적으로 중지하지 않았음에도 불구하고 다른 태스크에 의해 선점(preempt) 되었기 때문이다.
FreeRTOS 의 AVR port 는 RTOS 틱을 생성하기 위해 timer 1 컴페어 매치 모드를 사용한다. 다음 장에서 RTOS 틱 ISR 이 WinAVR 개발 툴을 이용해서 어떻게 구현되었는지 살펴본다.
GCC 'signal' 속성
GCC 개발 툴을 이용하면 인터럽트를 C로 작성할 수 있다. AVR timer 1 주변장치에서의 컴페어 매치 이벤트는 다음과 같이 작성될 수 있다.
1. void SIG_OUTPUT_COMPARE1A( void ) __attribute__ ( ( signal ) ); 2. void SIG_OUTPUT_COMPARE1A( void ) 3. { 4. vPortYieldFromTick(); 5. }
3~4.에서 RTOS 틱을 위한 ISR C 코드가 들어간다.
함수원형에서 '__attribute__ ( ( signal ) )' 은 컴파일러에게 이 함수가 ISR 이란 것을 알려준다. 그리고 그 결과로 생성되는 코드에 2가지 중요한 변화가 있다.
- 'signal' 속성은 ISR 을 수행하는 동안 변경된 모든 프로세서 레지스터들이 ISR 이 종료될 때 원래의 값으로 복구 된다는 것을 보장 해 준다. 이것은 컴파일러가 언제 인터럽트가 들어올 지에 관한 어떠한 예측도 할 수 없기 때문에 필요하다. 그리고 그 이유 때문에 어떤 프로세서 레지스터가 저장이 필요하고 필요하지 않는지 알 수 없기에 코드를 최적화 할 수 없다.
- 또한 ‘signal' 속성은 보통의 함수에서 사용되는 'return' 명령어(RET) 대신에 'return from interrupt' 명령어(RETI) 가 사용되도록 한다. AVR 마이크로컨트롤러는 ISR 에 들어오면서 인터럽트를 비 활성화시키기 때문에 RETI 명령어는 ISR 이 종료될 때 인터럽트를 활성화시키기 위해 필요하다.
컴파일러가 생성하는 코드는 아래와 같다.
- { - Start of ISR
1. PUSH R1 2. PUSH R0 3. IN R0,0x3F 4. PUSH R0 5. CLR R1 6. PUSH R18 7. PUSH R19 8. PUSH R20 9. PUSH R21 10. PUSH R22 11. PUSH R23 12. PUSH R24 13. PUSH R25 14. PUSH R26 15. PUSH R27 16. PUSH R30 17. PUSH R31
ISR 동안 응용프로그램 코드에 의해 변경될 수 있는 레지스터들을 저장하기 위해 컴파일러가 생성하는 어셈블리 코드이다.
- vPortYieldFromTick();
18. CALL 0x0000029B
18. 서브루틴을 호출한다.
- } - End of ISR
19. POP R31 20. POP R30 21. POP R27 22. POP R26 23. POP R25 24. POP R24 25. POP R23 26. POP R22 27. POP R21 28. POP R20 29. POP R19 30. POP R18 31. POP R0 32. OUT 0x3F,R0 33. POP R0 34. POP R1 35. RETI
앞서 저장되었던 레지스터들을 불러오기 위해 컴파일러가 생성한 코드.
GCC 'naked' 속성
앞에서 'signal' 속성이 C언어를 이용하여 ISR 을 작성하는데 어떻게 사용되는지, 그리고 그 결과로써 실행중인 문맥의 일부분이 어떻게 자동적으로 저장되는지를 살펴보았다.(ISR에 의해 수정된 프로세서 레지스터만 저장된다) 하지만 문맥 전환을 수행할 때는 전체 문맥이 저장되어야만 한다.
사용자가 작성한 응용프로그램 코드가 ISR 에 들어가면서 명시적으로 모든 프로세서 레지스터를 저장할 수도 있을 것이다. 이렇게 할 경우 어떤 프로세서 레지스터는 두 번 저장하는 결과를 가져온다. 즉 원래 컴파일러에서 자동적으로 생성한 코드가 저장할 것이고 응용프로그램 코드에서 다시 한 번 레지스터 내용을 저장할 것이다. 이것은 바람직하지 못하며 'signal' 속성에 추가로 'naked' 속성을 사용함으로써 막을 수 있다.
1. void SIG_OUTPUT_COMPARE1A( void ) __attribute__ ( ( signal, naked ) ); 2. void SIG_OUTPUT_COMPARE1A( void ) 3. { 4. vPortYieldFromTick(); 5. }
3~4.에서 RTOS 틱을 위한 ISR C 코드가 들어간다.
'naked' 속성은 컴파일러가 어떤 함수의 시작지점 또는 종료지점에서 코드를 생성할 수 없도록 한다. 이를 이용하여 더 간단한 출력 코드를 얻을 수 있다.
output code 1. CALL 0x0000029B
1. 단순히 서브루틴 호출을 위한 코드만이 생성된다.
'naked' 속성을 사용하면 컴파일러가 함수 진입 코드 또는 함수 이탈 코드를 자동적으로 생성하지 않기 때문에 명시적으로 이러한 코드를 추가시켜 주어야 한다. portSAVE_CONTEXT() 와 portRESTORE_CONTEXT() 매크로를 이용해 실행 중인 문맥 전체를 저장하거나 복구할 수 있다.
1. void SIG_OUTPUT_COMPARE1A( void ) __attribute__ ( ( signal, naked ) ); 2. void SIG_OUTPUT_COMPARE1A( void ) 3. { 4. portSAVE_CONTEXT(); 5. vPortYieldFromTick(); 6. portRESTORE_CONTEXT(); 7. asm volatile ( "reti" ); 8. }
4. 매크로를 사용해서 실행중인 문맥을 명시적으로 저장한다. 5. RTOS 틱을 위한 ISR C 코드. 6. 매크로를 사용해서 이전에 실행중이던 문맥을 명시적으로 불러온다. 7. 인터럽트로부터 복귀 역시 명시적으로 추가되어야한다.
'naked' 속성은 사용자가 작성한 응용프로그램 코드에게 AVR 문맥이 언제, 어떻게 저장되어야 하는지에 대한 완벽한 제어능력을 준다. 응용프로그램 코드가 ISR 에 들어가면서 전체 문맥을 저장한다면 문맥 전환을 수행하기 전에 이것을 다시 반복해서 저장할 필요가 없다. 그래서 어떤 프로세서 레지스터들도 두 번 저장되는 일이 없다.
FreeRTOS 틱 코드
FreeRTOS AVR port에서 사용된 실제 소스코드는 앞에서 보여준 것과는 약간 다르다. vPortYieldFromTick() 은 'naked' 함수처럼 구현되어 있고 그 안에서 문맥이 저장되고 복구된다. 이것은 비 선점형 문맥 전환(태스크가 자발적으로 대기 상태로 되는 것 - 여기서는 자세히 설명하지 않음) 의 구현 때문에 이런 방식으로 수행된다.
그러므로 FreeRTOS에는 RTOS 틱을 아래와 같이 구현한다.
1. void SIG_OUTPUT_COMPARE1A( void ) __attribute__ ( ( signal, naked ) ); 2. void vPortYieldFromTick( void ) __attribute__ ( ( naked ) ); 3. void SIG_OUTPUT_COMPARE1A( void ) 4. { 5. vPortYieldFromTick(); 6. asm volatile ( "reti" ); 7. } 8. void vPortYieldFromTick( void ) 9. { 10. portSAVE_CONTEXT(); 11. vTaskIncrementTick(); 12. vTaskSwitchContext(); 13. portRESTORE_CONTEXT(); 14. asm volatile ( "ret" ); 15. }
3. RTOS 틱을 위한 ISR 코드. 5. 틱 함수를 호출한다. 6. 인터럽트에서 복귀한다. 만약 문맥 전환이 일어났다면 원래 호출이 일어났던 지점과는 다른 곳으로 복귀하게 된다. 10. 'naked' 로 선언된 함수이므로 문맥을 직접 저장해주어야 한다. 11. 틱 카운트를 증가시켜서 대기 상태에 있던 태스크가 깨어나서 준비 상태가 되는지의 여부를 판단한다. 12. 새로 준비 상태가 된 태스크 들 간의 우선순위를 살펴보고 문맥 전환이 필요한 지를 판단한다. 13. 다음에 실행되어야 할 태스크의 문맥을 복구한다. 14. 실행되어야 할 태스크로 복귀한다.
AVR 문맥
문맥 전환이 발생하면 전체의 실행중인 문맥이 저장되어야만 한다. AVR 마이크로컨트롤러 에서의 문맥은 다음과 같이 구성되어 있다.:
- 32개의 범용 프로세서 레지스터 : GCC 개발 툴은 레지스터 R1 이 0으로 설정되어 있다고 가정한다.
- 상태 레지스터 : 상태 레지스터의 값은 명령어의 실행에 영향을 미치기 때문에 문맥 전환이 일어날 때 반드시 보존되어야 한다.
- 프로그램 카운터 : 태스크가 재개될 때 태스크는 반드시 이전에 중지되었던 지점 이후부터 시작해야한다.
- 두 개의 스택 포인터 레지스터
문맥의 보관
각각의 실시간 태스크는 각자 자신만의 스택 메모리 영역을 가지고 있다. 그래서 단순히 프로세서 레지스터를 태스크 스택에 넣음(push)으로써 문맥을 저장할 수 있다. AVR 문맥을 저장하는 것은 불가피하게 어셈블리 코드를 사용해야만 하는 작업이다.
portSAVE_CONTEXT() 는 매크로로 구현이 되어 있고 소스코드는 아래와 같다.
- portSAVE_CONTEXT() - PORTMACRO.H
1. #define portSAVE_CONTEXT() 2. asm volatile ( 3. "push r0 \n\t" 4. "in r0, __SREG__ \n\t" 5. "cli \n\t" 6. "push r0 \n\t" 7. "push r1 \n\t" 8. "clr r1 \n\t" 9. "push r2 \n\t" 10. "push r3 \n\t" 11. "push r4 \n\t" 12. "push r5 \n\t" : : : 13. "push r30 \n\t" 14. "push r31 \n\t" 15. "lds r26, pxCurrentTCB \n\t" 16. "lds r27, pxCurrentTCB + 1 \n\t" 17. "in r0, __SP_L__ \n\t" 18. "st x+, r0 \n\t" 19. "in r0, __SP_H__ \n\t" 20. "st x+, r0 \n\t" 21. );
3. 프로세서 레지스터 R0 는 상태(status) 레지스터가 저장될 때 사용되므로 가장 먼저 저장된다. 그리고 반드시 원래의 값을 저장해야 한다. 4. 에서 상태 레지스터는 R0 로 들어온 후 6. 에서 스택에 저장된다. 5. 에서 프로세서 인터럽트가 비 활성화된다. 만약 portSAVE_CONTEXT() 가 ISR 내부에서 불러졌다면 AVR 이 이미 인터럽트를 비 활성화시켰기 때문에 따로 명시하지 않아도 된다. 반대로 portSAVE_CONTEXT() 매크로가 ISR 외부에서 사용될 때(태스크가 스스로 중지될 때) 인터럽트는 반드시 가능한 한 빠르게 명시적으로 비활성화 상태가 되어야 한다. 7~8. ISR C 소스코드를 컴파일 할 때 컴파일러는 R1 이 0으로 설정되어 있다고 가정한다. R1의 기존 값은 R1 이 지워지기 전에 저장된다. 9~15. 나머지 모든 프로세서 레지스터가 순서대로 저장된다. 15~16. 지금 중지된 태스크의 스택은 이제 실행중인 문맥의 복사본을 가지고 있다. 커널은 태스크 의 스택 포인터를 저장해서 태스크가 재개 될 때 문맥이 복구될 수 있도록 한다. X 프로세서 레지스터는 스택 포인터가 저장되어야 할 주소와 함께 로드 된다. 17~18. 스택 포인터는 먼저 하위 바이트가 저장된 다음 19~20. 상위 니블이 저장된다.
문맥의 복구
portRESTORE_CONTEXT() 는 portSAVE_CONTEXT() 의 역순이다. 재개되는 태스크의 문맥은 이전의 태스크 스택에 저장되어 있었다. 실시간 커널 은 태스크를 위해 스택 포인터를 가져오고 문맥을 스택에서 프로세서 레지스터로 다시 불러온다.(pop)
- portRESTORE_CONTEXT() - PORTMACRO.H
1. #define portRESTORE_CONTEXT() 2. asm volatile ( 3. "lds r26, pxCurrentTCB \n\t" \ (1) 4. "lds r27, pxCurrentTCB + 1 \n\t" \ (2) 5. "ld r28, x+ \n\t" \ 6. "out __SP_L__, r28 \n\t" \ (3) 7. "ld r29, x+ \n\t" \ 8. "out __SP_H__, r29 \n\t" \ (4) 9. "pop r31 \n\t" \ 10. "pop r30 \n\t" \ : : : 11. "pop r1 \n\t" \ 12. "pop r0 \n\t" \ (5) 13. "out __SREG__, r0 \n\t" \ (6) 14. "pop r0 \n\t" \ (7) 15. );
3~4. pxCurrentTCB 는 태스크의 스택 포인터를 가져올 수 있는 주소를 가지고 있다. 이것은 X 레지스터에 로드된다. 6, 8. 재개 되는 태스크의 스택 포인터는 AVR 스택 포인터로 로드된다. 먼저 하위 바이트가 로드되고 다음으로 상위 니블이 로드된다. 10~11. 프로세서 레지스터들은 스택에서 저장한 순서와는 반대로 꺼내어진다(pop) 13, 14. 상태 레지스터는 레지스터 R1 과 R0 사이에 스택에 저장되었다. 그래서 R1을 먼저 복구하고 그다음에 R0를 복구한다.
RTOS 문맥 전환의 전체 진행 과정
여기서는 이런 소스코드 모듈이 AVR 마이크로컨트롤러에서 RTOS 문맥 전환을 구현하기위해 어떻게 사용되는지 알아본다. 낮은 우선순위의 태스크 A와 높은 우선순위의 태스크 B를 전환하는 7단계 과정에 관해 설명한다.
아래의 소스코드는 WinAVR C 개발 툴에서 사용가능하다.
RTOS 틱 인터럽트가 발생하기 전
이 예제는 태스크 A가 실행을 시작하면서부터 시작한다. 태스크 B는 중지 상태에 있다고 가정한다. 그리고 그 문맥은 이미 태스크 B의 스택에 저장되어 있다고 하자.
태스크 A 는 아래 그림과 같은 문맥을 가지고 있다.
<그림>
각각의 레지스터 안에 있는 (A) 표시는 레지스터가 태스크 A의 문맥에 대한 값을 가지고 있다는 것을 의미한다.
RTOS 틱 인터럽트 발생
RTOS 틱은 태스크A 가 막 LDI 명령을 실행하려고 할 때 발생한다. 인터럽트가 발생하면 AVR 마이크로컨트롤러는 RTOS 틱 ISR 실행을 위해 실행지점을 변경하기 전에 자동적으로 현재의 프로그램 카운터(PC)를 스택에 올려놓는다.
RTOS 틱 인터럽트 실행 중
아래는 ISR의 소스코드이다. (~ 참조)
1. void SIG_OUTPUT_COMPARE1A( void ) 2. { 3. vPortYieldFromTick(); 4. asm volatile ( "reti" ); 5. } 6. void vPortYieldFromTick( void ) 7. { 8. portSAVE_CONTEXT(); 9. vTaskIncrementTick(); 10. vTaskSwitchContext(); 11. portRESTORE_CONTEXT(); 12. asm volatile ( "ret" ); 13. }
SIG_OUTPUT_COMPARE1A() 은 naked 함수이다. 그래서 첫 번째 명령어는vPortYieldFromTick() 을 호출한다. vPortYieldFromTick() 또한 naked 함수이다. 그래서 AVR 실행 문맥은 portSAVE_CONTEXT() 를 명시적으로 호출함으로써 저장된다.
portSAVE_CONTEXT() 는 전체 AVR 실행 문맥을 태스크 A의 스택에 넣는다. 이것은 아래 그림에 나와 있다. 태스크 A의 스택 포인터는 이제 태스크 A의 문맥 중 최상단을 가리킨다. portSAVE_CONTEXT() 는 스택 포인터의 복사본을 저장함으로써 완료된다. 실시간 커널은 이미 지난번에 태스크 B가 중지될 때 가져온 태스크 B의 스택 포인터의 복사본을 가지고 있다.
틱 카운트 증가
vTaskIncrementTick() 는 태스크 A의 문맥이 저장된 이후에 실행된다. 이 예제를 위해 틱 카운트를 증가시키면 태스크 B가 실행될 준비가 된다고 가정한다. 태스크 B는 태스크 A보다 높은 우선순위를 가지고 있기 때문에 vTaskSwitchContext() 는 ISR이 끝날 때 태스크 B에게 CPU 처리 시간을 준다.
태스크 B의 스택 포인터가 복구됨
태스크 B의 문맥은 반드시 복구되어야만 한다. portRESTORE_CONTEXT() 가 가장 먼저 하는 일은 중지될 때 저장 시켜 놓은 태스크 B의 스택 포인터를 다시 가져 오는 것이다. 태스크 B의 스택 포인터는 프로세서 스택 포인터로 로드된다. 그래서 이제 AVR 스택은 태스크 B의 문맥중 최상단을 가리키게 된다.
태스크 B의 문맥을 복구함
portRESTORE_CONTEXT() 는 스택에서 태스크 B의 문맥을 적절한 프로세서 레지스터로 복구함으로써 완성된다.
이제 프로그램 카운터만이 스택에 남아있다.
RTOS 틱 종료
vPortYieldFromTick() 는 마지막 명령어, 즉 return from interrupt (RETI) 를 만나면 SIG_OUTPUT_COMPARE1A() 로 돌아간다. RETI 명령어는 스택에 있는 다음 값이 앞에서 인터럽트가 발생했을 때 저장된 복귀 주소(return address)라고 가정한다. RTOS 틱 인터럽트가 시작됐을 때 AVR 은 자동적으로 태스크 A의 복귀 주소를 스택 에 넣는다. 이 복귀 주소는 태스크 A가 다음에 실행할 명령어의 주소이다. ISR 은 스택 포인터를 변경하여 이제 태스크 B의 스택을 가리키도록 한다. 그래서 RETI 명령어에 의해 스택으로부터 꺼내어진 복귀 주소는 실제로 태스크 B가 중지되기 직전에 실행하려고 했던 명령어의 주소이다.
RTOS 틱 인터럽트는 태스크 A가 실행중일 때 발생했지만 틱 인터럽트가 종료될 때는 태스크 B가 실행되게 된다. 이렇게 해서 문맥 전환이 끝난다.
'OS 포팅' 카테고리의 다른 글
FreeRTOS API (0) | 2013.06.20 |
---|---|
FreeRTOS 구조 (0) | 2013.06.20 |
FreeRTOS 소개 (0) | 2013.06.20 |
ucos-2 백업 파일 (0) | 2013.06.20 |
u-boot 파일 [이전] (0) | 2013.06.20 |