
멀티스레딩 환경에서 가장 중요한 문제 중 하나는 여러 스레드가 하나의 자원(전역 변수, I/O 포트 등)을 공유할 때 발생하는 충돌입니다. 이를 방지하기 위한 보호 메커니즘이 필수적입니다.
RTOS 환경에서 스레드는 크게 메인 스레드(Main Threads)와 이벤트 스레드(Event Threads) 두 가지로 나뉩니다.
특징:
OS 유무에 따른 차이:
void mainThread(void){
int();
while(1){
Body();
}
}
void inputThread(void){
data = ReadInput();
Send(data);
}
void outputThread(void){
data = Recv();
WriteOutput(data);
}
void periodicThread(void){
PerformTask();
}

이전내용 복습
Special Registers: R13-R15
이 레지스터들은 시스템의 흐름을 제어하는 핵심적인 역할을 합니다.
R13: Stack Pointer (SP)
현재 스택의 주소를 가리킵니다.
Cortex-M은 MSP(Main Stack Pointer)와 PSP(Process Stack Pointer) 두 가지를 가집니다.
RTOS 관점: 커널이나 ISR은 MSP를 사용하고, 각 태스크(Thread)는 독립적인 PSP를 사용하여 시스템의 안정성을 높입니다.
R14: Link Register (LR)
함수 호출(BL 명령어) 시, 함수가 끝난 후 돌아갈 복귀 주소를 저장합니다.
ISR 실행 시에는 특별한 값(EXC_RETURN)이 저장되어 예외 처리가 끝난 후 올바른 모드로 복구되도록 돕습니다.
R15: Program Counter (PC)
다음에 실행할 명령어의 주소를 가리킵니다.
Cortex-M의 파이프라인 구조 때문에 실제 실행 중인 명령어보다 몇 단계 앞선 주소를 가리키는 특징이 있습니다.
🟢 왼쪽: 활활 타오르는 '현재 실행 중인 스레드 (Running thread)'
이 스레드는 지금 당장 CPU의 주인공입니다. VIP 대접을 받고 있죠.
Real Registers (진짜 CPU 레지스터): R0~R12, SP, PC, PSR 같은 하드웨어 레지스터들이 이 스레드의 명령어를 처리하느라 쉴 새 없이 빙글빙글 돌고 있습니다.
PC (Program Counter): 화살표를 보세요! 현재 실행 중인 프로그램 코드(초록색 박스)의 특정 위치를 정확히 가리키고 있습니다. "나 지금 이 줄 읽고 있어!"라는 뜻입니다.
SP (Stack Pointer): 스택 메모리의 맨 위(노란색 끝부분)를 가리키며 짐을 넣을 준비를 하고 있습니다.
핵심 포인트: 이때 TCB 안에 있는 sp 변수는 별로 안 중요합니다. 진짜 하드웨어 CPU의 SP 레지스터가 알아서 일하고 있기 때문입니다. (그래서 그림에서도 화살표가 없습니다)
🔴 오른쪽: 처참하게 쫓겨난 '기절한 스레드 (Not running thread)'
타이머 인터럽트(SysTick)가 "땡!" 하고 울려서 CPU 마이크를 빼앗기고 강제로 대기열로 쫓겨난 스레드입니다.
Stack (노란색으로 꽉 찬 보따리): 쫓겨나기 직전에 CPU에서 쓰던 모든 레지스터 값들을 자기만의 개인 스택 메모리에 몽땅 쑤셔 넣었습니다(Push).
시험 핵심: 밑에 깔린 PSR, PC, LR, R12, R3~R0는 타이머가 울리는 순간 하드웨어가 자동으로(자동 반사로) 밀어 넣은 짐입니다.
그 위에 있는 R11~R4는 회원님이 직접 어셈블리 파일에 PUSH {R4-R11}이라고 써서 손으로 싸맨 짐입니다. (기억나시죠?)
TCB의 sp (가장 중요!): 짐 보따리를 다 싼 다음, "다음에 나 깨울 때 내 짐 보따리 입구가 여기 스택 주소니까 잘 기억해 둬!" 하고 TCB의 sp 변수에 짐의 꼭대기 주소를 적어놓고(STR SP, [R1]) 기절한 겁니다. (화살표가 스택 맨 위를 가리키고 있죠!)
스택 안의 PC (Program Counter): 스택 안쪽에 저장된 PC의 화살표를 보세요. 초록색 박스의 중간을 가리키고 있죠? "나 아까 여기까지 실행하다 끊겼어..." 하고 기억해 둔 이정표입니다.
중간고사 실전 요약
결국 이 그림은 "스레드가 교체될 때 어떤 마법이 일어나는가?"를 보여줍니다.
스케줄러가 빙글빙글 돌다가 오른쪽의 '기절한 스레드'를 다시 RunPt로 지목하면 어떻게 될까요?
역순으로 TCB의 sp에 적힌 주소를 진짜 CPU의 SP 레지스터로 가져온 뒤(LDR SP, [R1]), 노란색 보따리를 풀어헤쳐서(POP {R4-R11}) 레지스터를 복구하고 왼쪽의 'Running thread' 상태로 부활시킵니다.
#include <stdint.h>
#define NUMTHREADS 3 // Maximum number of threads
#define STACKSIZE 100 // Number of 32-bit words in each stack
// Thread Control Block (TCB) structure definition
struct tcb {
int32_t *sp; // Pointer to stack (valid for threads not running)
struct tcb *next; // Linked-list pointer to the next thread
};
typedef struct tcb tcbType;
//==================================LAB2================================
// Global variables for OS
tcbType tcbs[NUMTHREADS]; // Array of TCBs for 3 threads
tcbType *RunPt; // Pointer to the currently running thread
int32_t Stacks[NUMTHREADS][STACKSIZE]; // 2D Array for stacks (3 threads, 400 bytes each)
//======================================================================
// External/Hardware specific definitions (Assuming they exist in BSP/Hardware headers)
extern void DisableInterrupts(void);
extern void StartCritical(void);
extern void EndCritical(int32_t status);
extern void StartOS(void);
extern void Profile_Toggle0(void);
extern void Profile_Toggle1(void);
extern void Profile_Toggle2(void);
// Initialize the stack for a specific thread to simulate a context switch
void SetInitialStack(int i) {
// Set the stack pointer to point to the top of the simulated stack frame
tcbs[i].sp = &Stacks[i][STACKSIZE-16];
// Fill the simulated stack with dummy values and necessary initial states
Stacks[i][STACKSIZE-1] = 0x01000000; // xPSR: Set Thumb bit (Mandatory for Cortex-M)
// Stacks[i][STACKSIZE-2] will be set to the Program Counter (PC) in OS_AddThreads
Stacks[i][STACKSIZE-3] = 0x14141414; // R14 (LR) dummy value
Stacks[i][STACKSIZE-4] = 0x12121212; // R12 dummy value
Stacks[i][STACKSIZE-5] = 0x03030303; // R3 dummy value
Stacks[i][STACKSIZE-6] = 0x02020202; // R2 dummy value
Stacks[i][STACKSIZE-7] = 0x01010101; // R1 dummy value
Stacks[i][STACKSIZE-8] = 0x00000000; // R0 dummy value
Stacks[i][STACKSIZE-9] = 0x11111111; // R11 dummy value
Stacks[i][STACKSIZE-10] = 0x10101010; // R10 dummy value
Stacks[i][STACKSIZE-11] = 0x09090909; // R9 dummy value
Stacks[i][STACKSIZE-12] = 0x08080808; // R8 dummy value
Stacks[i][STACKSIZE-13] = 0x07070707; // R7 dummy value
Stacks[i][STACKSIZE-14] = 0x06060606; // R6 dummy value
Stacks[i][STACKSIZE-15] = 0x05050505; // R5 dummy value
Stacks[i][STACKSIZE-16] = 0x04040404; // R4 dummy value
}
// Add three tasks to the OS and configure them as a circular linked list
int OS_AddThreads(void(*task0)(void), void(*task1)(void), void(*task2)(void)) {
int32_t status;
// 중요!! Lock the door (start of Critical Section)
status = StartCritical(); // Disable interrupts during configuration
// 중요!! TCB 포인터가 다음 스레드로 넘어가는 지점
// 1. Link the TCBs in a circular manner for Round-Robin scheduling
tcbs[0].next = &tcbs[1]; // Thread 0 points to Thread 1
tcbs[1].next = &tcbs[2]; // Thread 1 points to Thread 2
tcbs[2].next = &tcbs[0]; // Thread 2 points back to Thread 0
// 중요!! 스택에 pc를 집어 넣는 지점
// 가짜 스택에 내가 만든 함수 주소 몰래 넣기
// 2. Initialize stacks and set the Program Counter (PC) for each thread
SetInitialStack(0);
Stacks[0][STACKSIZE-2] = (int32_t)(task0); // Set PC to Task0 address
SetInitialStack(1);
Stacks[1][STACKSIZE-2] = (int32_t)(task1); // Set PC to Task1 address
SetInitialStack(2);
Stacks[2][STACKSIZE-2] = (int32_t)(task2); // Set PC to Task2 address
// 3. Set the first thread to run
RunPt = &tcbs[0];
//중요!! Unlock the door (end of critical section)
EndCritical(status); // Restore interrupt state
return 1; // Return success
}
// Basic OS Initialization
void OS_Init(void) {
DisableInterrupts();
BSP_Clock_InitFastest(); // Set processor clock to maximum speed
}
// Hardware Registers for SysTick (Usually defined in tm4c123gh6pm.h)
#define STCTRL (*((volatile uint32_t *)0xE000E010))
#define STRELOAD (*((volatile uint32_t *)0xE000E014))
#define STCURRENT (*((volatile uint32_t *)0xE000E018))
#define SYSPRI3 (*((volatile uint32_t *)0xE000ED20))
// Configure SysTick and launch the OS
void OS_Launch(uint32_t theTimeSlice) {
STCTRL = 0; // Disable SysTick during setup
STCURRENT = 0; // Clear current value
// Set SysTick priority to 7 (Lowest priority) to avoid blocking hardware interrupts
SYSPRI3 = (SYSPRI3 & 0x0FFFFFFF) | 0xE0000000;
STRELOAD = theTimeSlice - 1; // Set reload value for the time slice
STCTRL = 0x00000007; // Enable SysTick, core clock, and interrupts
StartOS(); // Trigger the first context switch (Assembly function)
}
uint32_t Count0 = 0;
uint32_t Count1 = 0;
uint32_t Count2 = 0;
// User Task 0
void Task0(void) {
Count0 = 0;
while(1) {
Count0++;
Profile_Toggle0(); // Toggle bit for debugging/profiling
}
}
// User Task 1
void Task1(void) {
Count1 = 0;
while(1) {
Count1++;
Profile_Toggle1();
}
}
// User Task 2
void Task2(void) {
Count2 = 0;
while(1) {
Count2++;
Profile_Toggle2();
}
}
#define THREADFREQ 500 // Time slice frequency in Hz
int main(void) {
OS_Init(); // Initialize OS and disable interrupts
Profile_Init(); // Initialize digital I/O for profiling
// Register the three user tasks to the OS
OS_AddThreads(&Task0, &Task1, &Task2);
// Launch the OS with the calculated time slice
OS_Launch(BSP_Clock_GetFreq() / THREADFREQ);
return 0; // The program should never reach this point
}
OS_Init():
void OS_Init(void){
DisableInterrupts();
BSP_Clock_InitFastest();// set processor clock to fastest speed
// initialize any global variables as needed
}
SysTick의 역할:
AREA |.text|, CODE, READONLY, ALIGN=2
THUMB
REQUIRE8
PRESERVE8
; Import the global variable from OS.c
EXTERN RunPt
; Export the functions so C code can call them
EXPORT StartOS
EXPORT SysTick_Handler
; ***************** StartOS *******************
; Starts the first thread by restoring its initial stack
StartOS
LDR R0, =RunPt ; 1) R0 = address of RunPt pointer
LDR R1, [R0] ; 2) R1 = current RunPt (points to the first TCB)
LDR SP, [R1] ; 3) Load initial SP (SP = RunPt->sp)
POP {R4-R11} ; 4) Restore R4-R11 (dummy values)
POP {R0-R3} ; 5) Restore R0-R3 (dummy values)
POP {R12} ; 6) Restore R12 (dummy value)
ADD SP, SP, #4 ; 7) Discard LR from initial stack (skip 4 bytes)
POP {LR} ; 8) Restore PC into LR (This is the thread's start address!)
ADD SP, SP, #4 ; 9) Discard xPSR (skip 4 bytes)
CPSIE I ; 10) Enable interrupts
BX LR ; 11) Jump to the first thread's starting address
; ---------------------------------------------------------
; SysTick_Handler: Performs the preemptive thread switch
; 1) Hardware automatically saves R0-R3, R12, LR, PC, xPSR
; ---------------------------------------------------------
SysTick_Handler
; --- Step 1: Suspend current thread ---
CPSID I ; 2) Disable interrupts to make context switch atomic
PUSH {R4-R11} ; 3) Save remaining registers R4-R11 to the stack
LDR R0, =RunPt ; 4) R0 = address of the RunPt pointer
LDR R1, [R0] ; R1 = current RunPt (points to the old TCB)
STR SP, [R1] ; 5) Save current SP into the 'sp' field of the old TCB
; --- Step 2: Choose next thread (Round-Robin) ---
LDR R1, [R1, #4] ; 6) R1 = RunPt->next (Load the address of the next TCB)
STR R1, [R0] ; Update RunPt to point to the new TCB
; --- Step 3: Launch new thread ---
LDR SP, [R1] ; 7) Load the new thread's SP (SP = RunPt->sp)
POP {R4-R11} ; 8) Restore remaining registers R4-R11 from the new stack
CPSIE I ; 9) Enable interrupts so the new thread can run
BX LR ; 10) Return from interrupt: Hardware restores R0-R3, R12, LR, PC, xPSR
PUSH {R4-R11}
STR SP, [R1]
LDR R1, [R1, #4]
POP {R4-R11}
StartOS: OS.c에서 RunPt에 첫 번째 스레드를 지정해 두었죠? 이 함수는 그 첫 번째 스레드의 가짜 스택을 열어서 쓰레기값들을 차례대로 버리고(ADD SP, SP, #4), 함수의 시작 주소를 꺼내와서 출발(BX LR)시키는 역할을 합니다.
SysTick_Handler: 1ms마다 타이머가 울릴 때마다 실행되며, 현재 하던 일을 스택에 밀어 넣고(PUSH), 다음 스레드의 스택 포인터를 가져와서(LDR SP, [R1]), 짐을 풀고(POP) 다시 출발(BX LR)시키는 가장 중요한 문맥 전환(Context Switch) 마법입니다.