Switching threads, Periodic Thread, Semaphores, Mutex

Seungyun Lee·2026년 2월 23일

RTOS

목록 보기
7/14

Context Switch

; ---------------------------------------------------------
; 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

1단계: 기존 스레드 잠재우기 (Save)

현재 열심히 실행 중이던 스레드(예: Task1)를 일시 정지하고, 현재 상태를 스택에 안전하게 백업하는 과정입니다.

  • 하드웨어의 자동 백업 (1번): SysTick 타이머 인터럽트가 터지는 순간, CPU 하드웨어는 우리가 코드를 짜지 않아도 아주 똑똑하게 핵심 레지스터 8개(R0~R3, R12, LR, PC, PSR)를 현재 스레드의 스택에 자동으로 Push합니다.

  • 인터럽트 차단 (2번 - CPSID I): 레지스터를 옮기는 아주 민감한 작업 중에 다른 인터럽트가 끼어들면 스택이 엉켜버립니다. 그래서 잠시 시스템의 모든 인터럽트를 차단(Critical Section 진입)합니다.

  • 나머지 수동 백업 (3번 - PUSH {R4-R11}): 하드웨어가 백업해주지 않은 나머지 8개의 레지스터를 소프트웨어(어셈블리어)로 직접 스택에 밀어 넣습니다. (이제 총 16개의 레지스터가 스택에 얌전히 쌓였습니다!)

  • 현재 SP 저장 (4, 5번 - STR SP, [R1]): 이 스레드가 짐을 다 쌌으니, 현재 스택 포인터(SP)가 어디를 가리키고 있는지 명부(TCB)의 sp 항목에 적어둡니다. 그래야 나중에 다시 깨울 때 어디서부터 시작할지 알 수 있습니다.

2단계: 다음 차례 스레드 고르기 (Switch)

이제 바통을 넘겨받을 다음 스레드(예: Task2)를 찾아갈 차례입니다.

  • 다음 TCB로 이동 (6번 - LDR R1, [R1,#4]): 원형 연결 리스트(Circular Linked List)에서 다음 스레드의 TCB 주소를 가져와 RunPt 변수를 업데이트합니다.
    • 참고: 여기서 #4를 더하는 이유는 C언어 구조체(struct tcb)에서 첫 번째 항목(sp)이 4바이트를 차지하고, 두 번째 항목인 next 포인터를 가리키기 위해서 4칸을 건너뛰어야 하기 때문입니다.

3단계: 새로운 스레드 깨우기 (Restore)

새로 선택된 스레드(Task2)의 짐을 스택에서 풀고 실행을 시작합니다.

  • 새로운 SP 불러오기 (7번 - LDR SP, [R1]): CPU의 스택 포인터(SP)를 Task2의 TCB에 적혀 있던 주소로 바꿉니다. 이제부터 CPU는 Task2의 스택 메모리를 쳐다보게 됩니다.

  • 수동 복구 (8번 - POP {R4-R11}): Task2가 예전에 잠들 때 직접 밀어 넣었던 8개의 레지스터를 스택에서 빼옵니다.

  • 인터럽트 허용 (9번 - CPSIE I): 중요하고 위험한 교체 작업이 다 끝났으니, 다시 인터럽트가 발생할 수 있도록 잠금을 풉니다.

  • 마법의 귀환 (10번 - BX LR): 여기서 LR 레지스터 안에는 일반적인 함수 복귀 주소가 아니라 0xFFFFFFF9라는 특수한 값(EXC_RETURN)이 들어있습니다. CPU가 이 명령어를 만나면, "아! 인터럽트에서 복귀하라는 뜻이구나!" 하고 깨닫고, 나머지 8개의 핵심 레지스터(R0~R3, R12, LR, PC, PSR)를 하드웨어가 자동으로 Pop해 줍니다.

  • 이때 빠져나온 PC(Program Counter) 값의 주소로 점프하면서 자연스럽게 Task2가 실행을 재개합니다.


Periodic Thread

RTOS에서 정확한 시간에 맞춰 동작해야 하는 주기적 스레드(Periodic Thread) 처리에 대한 아주 중요한 개념입니다

1. 주기적 스레드를 스케줄링하는 2가지 방법

  • 하드웨어 타이머 사용 (Hardware Timer): 가장 직관적인 방법입니다. 스레드 개수가 적다면, 각 작업마다 개별 하드웨어 타이머(Timer0A, Timer1A 등)를 할당해서 그 타이머의 인터럽트(ISR)에서 실행하게 만듭니다.

  • 스케줄러 내부 삽입 (SysTick ISR 활용): 하드웨어 타이머는 개수가 한정되어 있으므로, 모든 스레드의 턴을 넘겨주는 가장 정확한 시계인 SysTick 스케줄러 안에서 주기적 함수를 직접 호출하는 소프트웨어적인 방식입니다.

2. 단일 주기적 스레드 처리

SysTick이 1ms마다 한 번씩 스케줄러를 부른다고 가정해 봅시다. 이때 특정 작업(PeriodicTask1)을 10ms마다 한 번씩 실행하고 싶다면 어떻게 해야 할까요?

가장 간단한 방법은 카운터를 세는 것입니다.

uint32_t Counter = 0;
#define NUM 10
void (*PeriodicTask1)(void); // Pointer to user function

void Scheduler(void) {
    // 1. Check if 10ms has passed
    if((++Counter) == NUM) {
        (*PeriodicTask1)();  // Execute the user task every 10ms
        Counter = 0;         // Reset the counter
    }
    
    // 2. Perform regular thread context switch
    RunPt = RunPt->next;     // Round Robin scheduler
}

3. Multiple Periodic Threads

만약 실행해야 할 주기적 작업이 2개 이상이라면 큰 문제가 발생합니다. 두 작업이 우연히 같은 1ms 틱(Tick)에 동시에 실행되면, 한 작업이 끝날 때까지 다른 작업이 밀려나서 시간 오차(Jitter)가 발생하기 때문입니다.

이를 막기 위한 완벽한 해결책이 바로 최소공배수(LCM)와 실행 시간 엇갈리게 하기(Non-overlapping)입니다.

상황: SysTick은 1ms 주기. Task1은 10ms 주기, Task2는 25ms 주기로 실행해야 함.

해결법:
10과 25의 최소공배수인 50을 주기로 카운터를 돌립니다 (0부터 49까지).
Task2는 카운터가 0, 25일 때 실행하게 만듭니다.
Task1은 카운터가 0, 10, 20...일 때 실행하면 0일 때 Task2와 겹치게 됩니다. 따라서 시작점을 1로 살짝 밀어서(Offset) 1, 11, 21, 31, 41일 때 실행하게 만듭니다!

uint32_t Counter = 0;

void Scheduler(void) {
    // 1. Update counter from 0 to 49 (LCM of 10 and 25 is 50)
    Counter = (Counter + 1) % 50; 
    
    // 2. Schedule Task1 every 10ms (Offset by 1 to prevent overlap)
    // Runs when Counter is 1, 11, 21, 31, 41
    if((Counter % 10) == 1) { 
        PeriodUserTask1();
    }
    
    // 3. Schedule Task2 every 25ms
    // Runs when Counter is 0, 25
    if((Counter % 25) == 0) { 
        PeriodUserTask2();
    }
    
    // 4. Perform regular thread context switch
    RunPt = RunPt->next; 
}

시험 포인트: "왜 Task1의 조건문이 == 0이 아니라 == 1인가요?"라는 질문이 자주 나옵니다. 정답은 "같은 SysTick 주기 안에서 여러 작업이 동시에 실행되어 발생하는 Jitter(시간 지연)를 막기 위해 의도적으로 실행 타이밍을 엇갈리게(Offset) 만든 것"입니다.


Semaphores

1. Semaphore의 기본 개념

세마포어는 1965년 에츠허르 데이크스트라(Edsger Dijkstra)가 고안한 개념으로, 스레드 간의 동기화(Synchronization), 자원 공유(Sharing), 통신(Communication)을 위해 사용하는 단순한 '숫자 카운터(Counter)'입니다.

세마포어(Semaphore)는 여러 스레드가 동시에 실행되는 운영체제에서, 공유 자원에 접근하는 것을 안전하게 제어하기 위해 사용하는 '신호등' 또는 '번호표' 역할의 정수형(Integer) 변수입니다.

가장 직관적인 비유인 '주차장 빈자리 전광판'으로 설명해 드릴게요.

  • 주차장에 빈자리가 3개 있다면 전광판(세마포어) 숫자는 3입니다.
  • 차가 1대 들어갈 때마다 차단기가 내려가고 전광판 숫자를 1씩 뺍니다. (OS_Wait)
  • 숫자가 0이 되면? 입구에 도착한 다음 차는 전광판 숫자가 1 이상으로 바뀔 때까지 꼼짝없이 기다려야 합니다.
  • 주차를 마친 차가 1대 빠져나오면 숫자를 1 더해주고 대기하던 차를 들여보냅니다. (OS_Signal)

주로 3가지 동작으로 이루어집니다.

  • Init (초기화): 처음에 열쇠(자원)를 몇 개 둘 것인지 정합니다.
  • Wait (대기): 자원을 가져가는 함수입니다. (다른 말로 Pend, P 연산이라고도 함)
    • 카운터가 0보다 크면: 값을 1 빼고 바로 자원을 씁니다.
    • 카운터가 0이면: 자원이 반납될 때까지 무한 대기(Spin)합니다.
  • Signal (신호): 자원을 다 쓰고 반납하는 함수입니다. (다른 말로 Post, V 연산이라고도 함)
    • 카운터 값을 1 증가시켜 다른 스레드가 쓸 수 있게 해줍니다.

2. Spin-lock 세마포어의 한계와 특징

이번에 배우는 방식은 가장 원시적인 형태인 스핀락(Spin-lock) 방식입니다.

1. 왼쪽 흐름도: OS_Wait (열쇠 획득하기)
스레드가 공유 자원(열쇠)을 쓰기 위해 기다리는 과정입니다.

- 빨간색 네모 (Disable interrupts): 가장 먼저 인터럽트를 꺼서 **임계 영역(Critical Section)**에 진입합니다.
  변수 s를 확인하는 도중에 다른 스레드가 끼어들지 못하게 문을 잠그는 것입니다.

- 노란색 마름모 (s == 0?): 현재 열쇠가 남아있는지(s) 확인합니다.

  경로 A (s == 0 인 경우 - 왼쪽 루프): 열쇠가 하나도 없습니다. 
    이때 아주 중요한 동작이 일어납니다. **초록색 네모(Enable interrupts)**를 실행하여 잠시 문을 열어줍니다.  
    문을 열어두는 찰나의 순간에 SysTick 인터럽트가 터지면, 다른 스레드로 차례가 넘어가게 됩니다. (그래야 다른 스레드가 열쇠를 다 쓰고 반납할 수 있으니까요!) 
    그 후 다시 **빨간색 네모(Disable interrupts)**로 문을 잠그고, 열쇠가 생겼는지 다시 확인하러 올라갑니다. 이것이 제자리걸음을 하는 Spin(스핀) 동작입니다.

   경로 B (s > 0 인 경우 - 아래쪽 방향): 다행히 남은 열쇠가 있습니다.
   주황색 네모 (s = s - 1): 열쇠를 하나 가져갑니다(카운터 감소).
   초록색 네모 (Enable interrupts): 볼일이 끝났으니 다시 인터럽트를 켜고 함수를 무사히 빠져나옵니다(return). 이제 스레드는 자원을 마음껏 사용할 수 있습니다.

2. 오른쪽 흐름도: OS_Signal (열쇠 반납하기)
스레드가 자원을 다 쓰고 나서 다른 스레드들을 위해 열쇠를 돌려주는 과정입니다.

빨간색 네모 (Disable interrupts): 반납하는 과정에서도 숫자가 꼬이지 않도록 인터럽트를 끕니다.
주황색 네모 (s = s + 1): 열쇠를 하나 추가합니다(카운터 증가).
초록색 네모 (Enable interrupts): 인터럽트를 다시 켜고 함수를 종료(return)합니다. (이때 왼쪽 루프에서 돌고 있던 다른 스레드가 이 열쇠를 낚아채게 됩니다!)

제자리돌기 (Spin): 열쇠가 없으면 다른 일을 하러 가는 게(Block/Sleep) 아니라, 화장실 문고리를 계속 덜컹거리며 while(s == 0) 루프를 무한정 돕니다.

CPU 낭비: 이 방식은 CPU 자원을 의미 없이 낭비합니다. 하지만 우리가 앞에서 배운 SysTick 스케줄러가 주기적으로 강제 문맥 전환(Context Switch)을 해주기 때문에, 영원히 멈춰있지 않고 결국 다른 스레드에게 턴이 넘어갑니다.

3. 핵심 코드 분석 및 시험 출제 포인트

// ******** OS_InitSemaphore ************
// Initialize counting semaphore
// Inputs:  pointer to a semaphore
//          initial value of semaphore
// Outputs: none
void OS_InitSemaphore(int32_t *semaPt, int32_t value){
	(*semaPt) = value;
}


// Wait: Try to take the resource (decrement)
void OS_Wait(int32_t *s) {
    DisableInterrupts(); // Enter critical section, lock the door
    
    while((*s) == 0) {
        // [CRITICAL POINT] Why do we enable and disable interrupts here?
        EnableInterrupts();  // Allow context switch (SysTick) to happen
        DisableInterrupts(); // Re-enter critical section to check the condition again
    }
    
    (*s) = (*s) - 1;     // Take the resource
    EnableInterrupts();  // Exit critical section
}

// Signal: Release the resource (increment)
void OS_Signal(int32_t *s) {
    DisableInterrupts(); // Enter critical section
    (*s) = (*s) + 1;     // Release the resource
    EnableInterrupts();  // Exit critical section
}

질문: "만약 while 루프 안에서 EnableInterrupts()와 DisableInterrupts()를 지워버리면 어떻게 될까?"

정답: 시스템이 완전히 다운(Crash)됩니다! * 이유: 인터럽트를 끈 상태로 while 루프에 갇히게 되는데, 인터럽트가 꺼져 있으니 스레드를 교체해 줄 SysTick 타이머조차 울리지 못합니다. 결국 열쇠를 가진 다른 스레드가 실행될 기회를 영영 잃어버려서 카운터가 평생 0에 머무는 데드락(Deadlock) 상태에 빠집니다.

4. 뮤텍스(Mutex = Mutual Exclusion)

세마포어의 특수한 형태로, 초기 카운터 값을 딱 '1'로 설정한 세마포어를 뮤텍스라고 부릅니다.

  • 목적: 오직 한 번에 하나의 스레드만 접근해야 하는 공유 자원(예: LCD 디스플레이, 전역 변수)을 보호할 때 씁니다.

  • 동작: * 1 (Free): 지금 아무도 안 쓰고 있음.

    • 0 (Busy): 누군가 쓰고 있음.
  • 가장 중요한 규칙: 뮤텍스는 화장실 열쇠와 같아서, OS_Wait으로 열쇠를 가져간 바로 그 스레드가 반드시 OS_Signal을 호출해서 열쇠를 반납해야 합니다. (다른 스레드가 대신 반납해 줄 수 없습니다.)

질문

1. 뮤텍스 세마포어의 초기값은 왜 '1'이어야 할까요?

공유 자원(예: LCD 화면, 특정 전역 변수)을 보호하기 위해 세마포어를 쓸 때, 그 초기값은 반드시 1이어야 합니다.

초기값이 1인 이유: 1은 "현재 아무도 안 쓰고 있음(Free)"을 의미하기 때문입니다. 처음 시작할 때는 당연히 자원이 비어있어야 첫 번째로 도착한 스레드가 OS_Wait을 통해 값을 0으로 만들고 자원을 사용할 수 있습니다.

만약 초기값이 0이라면? 시작하자마자 화장실 문이 안에서 잠겨있는 것과 같습니다. 아무 스레드도 자원에 접근하지 못하고, 모든 스레드가 무한정 대기(Spin)만 하다가 시스템이 시작과 동시에 멈춰버립니다.

2. OS_Wait (또는 그 안의 인터럽트 잠금)을 없애버리면 어떻게 될까요?

공유 자원 주변에 쳐놓은 안전 울타리를 치워버리는 것과 같습니다.

상호 배제(Mutual Exclusion) 실패: OS_Wait은 "지금 누가 쓰고 있으면 난 기다릴게!"라는 규칙입니다. 이 규칙이 없어지면, 스레드 A가 공유 변수에 값을 쓰고 있는 도중에 스레드 B가 난입해서 값을 덮어써 버립니다.

결과: 여러 스레드가 동시에 자원에 접근하면서 데이터가 박살 나거나(Data Corruption), LCD 화면에 글자가 겹쳐서 깨져 나오는 등 시스템이 완전히 엉망이 됩니다.

Synchronization

스레드 A가 특정 작업을 끝내야만 스레드 B가 다음 작업을 시작할 수 있을 때, 순서를 맞춰주는 역할입니다.

초기값은 반드시 '0': 뮤텍스(초기값 1)와 달리 동기화 세마포어의 초기값은 0입니다. 왜냐하면 기다리던 '이벤트(사건)'가 아직 발생하지 않았기 때문입니다.

동작 원리:

  • 기다리는 쪽 (Consumer): OS_Wait을 호출합니다. 초기값이 0이므로 이벤트가 발생할 때까지 여기서 멈춰서 기다립니다(Spin/Block).
  • 신호를 주는 쪽 (Producer): 작업이 끝나면 OS_Signal을 호출하여 값을 1로 만듭니다. 이 순간 기다리던 스레드가 깨어나서 다음 작업을 진행하게 됩니다.

Thread Communication Using Mail Box

전역 변수(Mail) 하나를 두고 데이터를 주고받는 완벽한 탁구(Ping-Pong) 게임입니다. 데이터가 덮어씌워 지거나 두 번 읽히는 것을 막기 위해 2개의 세마포어를 교차해서 사용합니다.

  • Send 세마포어 (초기값 0): 생산자가 "편지 왔어!"라고 소비자에게 알리는 용도입니다.
  • Ack 세마포어 (초기값 0): 소비자가 "편지 잘 받았어!"라고 생산자에게 알리는 용도입니다.

1. Two Main thread comm mailbox

// 1. Global variables for the Mailbox
uint32_t Mail;       // Shared data storage
int32_t  Send = 0;   // Semaphore: 1 means mail is unread, 0 means mail is empty
int32_t  Ack = 0;    // Semaphore: 1 means mail was received, 0 means waiting

// 2. Producer function: Put data into mailbox
void SendMail(uint32_t data) {
    Mail = data;        // 1) Write data to the shared variable
    OS_Signal(&Send);   // 2) Signal the Consumer: "You have mail!"
    OS_Wait(&Ack);      // 3) Wait here until the Consumer sends an "Ack" (Acknowledgment)
}

// 3. Producer thread execution
void Producer(void) {
    Init1();
    while(1) {
        uint32_t myData;
        myData = MakeData(); // Generate new data
        SendMail(myData);    // Send it to the mailbox
        Unrelated1();        // Do other tasks
    }
}

// 4. Consumer function: Read data from mailbox
uint32_t RecvMail(void) {
    uint32_t theData;
    OS_Wait(&Send);     // 1) Wait here until the Producer sends a "Send" signal
    theData = Mail;     // 2) Read the shared data
    OS_Signal(&Ack);    // 3) Signal the Producer: "I received the mail!"
    return theData;     // 4) Return the read data
}

// 5. Consumer thread execution
void Consumer(void) {
    Init2();
    while(1) {
        uint32_t thisData;
        thisData = RecvMail(); // Wait for and receive the mail
        Unrelated2();          // Do other tasks with the data
    }
}
  1. 대기 상태: 처음 시작하면 Send도 0, Ack도 0입니다. 소비자는 RecvMail() 안의 OS_Wait(&Send)에서 멈춰서(Spin) 기다립니다.

  2. 생산자 행동 개시: 생산자가 MakeData()로 데이터를 만들고 SendMail()을 부릅니다. 편지함(Mail)에 데이터를 넣고 OS_Signal(&Send)를 부릅니다.

  3. 바통 터치 (Ping): Send가 1이 되었으므로, 기다리던 소비자가 깨어납니다! 그와 동시에 생산자는 바로 밑에 있는 OS_Wait(&Ack)에 걸려서 스스로 멈춥니다. "내 편지 읽을 때까지 나 아무것도 안 하고 기다릴 거야!" 하는 상태가 됩니다.

  4. 소비자 수령 및 답장 (Pong): 깨어난 소비자가 Mail에서 데이터를 꺼내 읽습니다. 다 읽었으니 OS_Signal(&Ack)를 불러서 "편지 잘 받았어!"라고 답장을 보냅니다.

  5. 생산자 재가동: Ack가 1이 되었으므로 멈춰있던 생산자가 깨어나서 다음 루프로 돌아가 새로운 데이터를 만들기 시작합니다.

2. communication for interrupt(Drop & Run)

생산자가 하드웨어 인터럽트(Event Thread)일 때

이 부분이 시험에 정말 자주 나오는 함정입니다. 만약 버튼 입력이나 센서 타이머 같은 ISR(인터럽트 서비스 루틴)이 데이터를 만들어내는 생산자라면 어떻게 될까요?
절대 규칙: "ISR 안에서는 절대 OS_Wait을 호출할 수 없다!"

인터럽트는 시스템의 흐름을 끊고 들어온 긴급 상황이므로 무한 대기(Spin/Block) 상태에 빠지면 시스템 전체가 먹통이 됩니다.

해결책 (Ack 세마포어 제거):

  • 생산자(ISR)는 소비자가 데이터를 읽었는지 기다릴(OS_Wait(&Ack)) 시간이 없습니다. 따라서 Ack 세마포어를 아예 없애버리고 Send 세마포어 하나만 사용해야 합니다.

치명적인 부작용 (데이터 손실 - Lost):

  • 생산자(ISR)가 데이터를 편지함에 넣고 나갔는데, 소비자가 미처 읽기도 전에 또 인터럽트가 터져서 생산자가 새로운 데이터를 가져올 수 있습니다.

  • 이때 Send 값이 이미 1이라면(아직 안 읽은 편지가 있다면), 생산자는 기존 데이터를 어쩔 수 없이 덮어씌워 버립니다. 이때 데이터가 날아갔다는 것을 기록하기 위해 Lost라는 에러 카운터 변수를 1 증가시킵니다.

// 1. Global variables for the Mailbox
uint32_t Mail;     // Shared data
int32_t  Send = 0; // Semaphore to indicate new mail is available
uint32_t Lost = 0; // Counter for lost data when mailbox is full

// 2. Initialize the Mailbox
void OS_MailBox_Init(void) {
    Send = 0;      // Initially, there is no mail
    Lost = 0;      // Reset the lost error counter
}

// 3. Send data to the Mailbox (Producer / ISR)
void OS_MailBox_Send(uint32_t data) {
    Mail = data;           // Put the data into the mailbox
    
    if(Send) {             // If Send is already 1 (mail is not yet read)
        Lost++;            // Increment the lost counter (data overwritten)
    } else {
        OS_Signal(&Send);  // Signal the consumer that new mail has arrived
    }
}

// 4. Receive data from the Mailbox (Consumer)
uint32_t OS_MailBox_Recv(void) {
    OS_Wait(&Send);        // Wait until new mail arrives (Spin-lock)
    return Mail;           // Read and return the data from the mailbox
}
  1. 변수 구성:

    • 데이터를 담을 Mail
    • 편지가 왔음을 알리는 Send (초기값 0)
    • 잃어버린 데이터 횟수 세는 LOST counter
  2. SendMail (생산자/인터럽트):
    더 이상 소비자가 읽을 때까지 기다리지 않습니다. 편지함에 데이터를 툭 던져놓고 갑니다 (Mail = data;).

    • 만약 Send 세마포어가 이미 1이라면? (소비자가 아직 이전 편지를 안 읽었다면) 어쩔 수 없이 새 데이터로 덮어씌우고 Lost++를 하여 에러를 기록합니다.

    • Send가 0이었다면 정상적으로 OS_Signal(&Send)를 호출하여 편지가 왔음을 알립니다.

  3. RecvMail (소비자):
    첫 번째 사진과 동일하게 OS_Wait(&Send)로 편지를 기다렸다가 데이터를 꺼내갑니다. (답장을 보내지 않고 바로 return Mail;을 합니다.)

// ******** OS_AddPeriodicEventThreads ****************
// Add two periodic event threads to the OS
// Inputs: pointers to a void/void event thread functions
//         periods given in system time units
// Outputs: 1 if successful, 0 if this thread cannot be added
int OS_AddPeriodicEventThreads(void(*thread1)(void), uint32_t period1,
                               void(*thread2)(void), uint32_t period2) {
    
    // 1. Save the function pointers to global variables
    PeriodicTask1 = thread1;
    PeriodicTask2 = thread2;
    
    // 2. Save the periods to global variables
    Period1 = period1;
    Period2 = period2;
    
    // 3. Initialize the time counters to 0
    Counter1 = 0;
    Counter2 = 0;
    
    return 1; // Return 1 to indicate success
}

// ******** Scheduler ****************
// Run periodic tasks and advance RunPt to the next thread
// Inputs:  none
// Outputs: none
void Scheduler(void) {
    
    // 1. Manage the first periodic task
    Counter1 = Counter1 + 1;          // Increment the counter (1ms has passed)
    if(Counter1 == Period1) {         // Check if the target period is reached
        (*PeriodicTask1)();           // Execute the periodic task
        Counter1 = 0;                 // Reset the counter for the next cycle
    }
    
    // 2. Manage the second periodic task
    Counter2 = Counter2 + 1;          // Increment the counter
    if(Counter2 == Period2) {         // Check if the target period is reached
        (*PeriodicTask2)();           // Execute the periodic task
        Counter2 = 0;                 // Reset the counter
    }
    
    // 3. Advance the Round-Robin pointer for the main threads
    RunPt = RunPt->next;              // Move to the next main thread
}
profile
RTL, FPGA Engineer

0개의 댓글