https://www.yes24.com/Product/Goods/84909414
공부 목적으로 이만우님의 저서 "임베디드 OS 개발 프로젝트"를 따라가며, RTOS "Navilos"를 개발하는 포스트입니다. 모든 내용은 "임베디드 OS 개발 프로젝트"에 포함되어 있습니다.
개발 목적
임베디드 시스템에 많이 사용되는 RTOS를 직접 개발해 보며 다음과 같은 지식을 학습하고자 합니다.
- RTOS의 핵심 개념에 대해 학습한다.
- 운영체제 핵심 기능을 설계해 보며 학습한다.
- 펌웨어에 대한 진입장벽을 낮춘다.
- ARM 아키텍처에 대해 학습한다.
- 하드웨어에 대한 지식을 학습한다.(펌웨어가 어떻게 하드웨어를 제어하는지)
이번 챕터에선 메시징을 다룰 것입니다.
메시징이란?
임의의 데이터를 메시지라는 이름으로 전송하는 기능입니다.
예를 들어 이벤트가 벨로 생각하고 메시징을 사서함으로 생각한다면, 메시지가 들어올 때 마다 벨을 울릴 수 있습니다.
이벤트와 메시징을 함께 사용하면 위와 같이 데이터를 다룰 수 있습니다. 예를 들어 인터럽트 핸들러에서 이벤트 외에 별도 데이터를 더 보내고 싶으면 메시징 기능을 이용해서 데이터를 보내고 이벤트를 보냅니다. 그러면 태스크에서 이벤트를 처리할 때 이벤트 핸들러에서 메시지를 읽어와서 처리할 수 있습니다. 이것이 바로 이번 챕터의 목표입니다.
우선 kernel 디렉터리 하위에 msg.h와 msg.c를 만들겠습니다.
메시지 큐
메시징 기능의 설계는 메시지를 어떻게 관리할 것이냐부터 시작됩니다. 여기서는 큐(Queue)를 사용하기로 했습니다.
큐를 구현하는 방법은 일반적으로 연결 리스트를 사용하는 방법과 배열을 사용하는 방법이 있습니다. 연결 리스트로 구현하면 유연하고 장점도 많으나 메모리를 동적으로 할당해주어야 합니다. 임베디드 시스템에서는 동적 할당을 피하기 위해 주로 배열을 활용합니다.
//msg.h
#ifndef KERNEL_MSG_H_
#define KERNEL_MSG_H_
#define MSG_Q_SIZE_BYTE 512
typedef enum KernelMsgQ_t
{
KernelMsgQ_Task0,
KernelMsgQ_Task1,
KernelMsgQ_Task2,
KernelMsgQ_Num
} KernelMsgQ_t;
typedef struct KernelCirQ_t
{
uint32_t front;
uint32_t rear;
uint8_t Queue[MSG_Q_SIZE_BYTE];
} KernelCirQ_t;
void Kernel_msgQ_init(void);
bool Kernel_msgQ_is_empty(KernelMsgQ_t Qname);
bool Kernel_msgQ_is_full(KernelMsgQ_t Qname);
bool Kernel_msgQ_enqueue(KernelMsgQ_t Qname, uint8_t data);
bool Kernel_msgQ_dequeue(KernelMsgQ_t Qname, uint8_t* out_data);
#endif /* KERNEL_MSG_H_ */
메시징 큐의 크기를 512바이트로 만들었고, 큐의 인덱스 처리를 위해 front와 rear를 선언했습니다.
해당 프로젝트에서는 각 태스크 당 메시지 큐를 하나씩 배정하여 처리하였습니다.
전체 시스템에 메시지 큐가 하나만 있어도 되는 시스템도 있을 수 있고, 메시지 큐가 필요없는 시스템도 있습니다.
다음은 msg.c입니다.
#include "stdint.h"
#include "stdbool.h"
#include "stdlib.h"
#include "msg.h"
KernelCirQ_t sMsgQ[KernelMsgQ_Num];
void Kernel_msgQ_init(void)
{
for (uint32_t i = 0 ; i < KernelMsgQ_Num ; i++)
{
sMsgQ[i].front = 0;
sMsgQ[i].rear = 0;
memclr(sMsgQ[i].Queue, MSG_Q_SIZE_BYTE);
}
}
bool Kernel_msgQ_is_empty(KernelMsgQ_t Qname)
{
if (Qname >= KernelMsgQ_Num)
{
return false;
}
if (sMsgQ[Qname].front == sMsgQ[Qname].rear)
{
return true;
}
return false;
}
bool Kernel_msgQ_is_full(KernelMsgQ_t Qname)
{
if (Qname >= KernelMsgQ_Num)
{
return false;
}
if (((sMsgQ[Qname].rear + 1) % MSG_Q_SIZE_BYTE) == sMsgQ[Qname].front)
{
return true;
}
return false;
}
bool Kernel_msgQ_enqueue(KernelMsgQ_t Qname, uint8_t data)
{
if (Qname >= KernelMsgQ_Num)
{
return false;
}
if (Kernel_msgQ_is_full(Qname))
{
return false;
}
sMsgQ[Qname].rear++;
sMsgQ[Qname].rear %= MSG_Q_SIZE_BYTE;
uint32_t idx = sMsgQ[Qname].rear;
sMsgQ[Qname].Queue[idx] = data;
return true;
}
bool Kernel_msgQ_dequeue(KernelMsgQ_t Qname, uint8_t* out_data)
{
if (Qname >= KernelMsgQ_Num)
{
return false;
}
if (Kernel_msgQ_is_empty(Qname))
{
return false;
}
sMsgQ[Qname].front++;
sMsgQ[Qname].front %= MSG_Q_SIZE_BYTE;
uint32_t idx = sMsgQ[Qname].front;
*out_data = sMsgQ[Qname].Queue[idx];
return true;
}
- sMsgQ 배열는 실제 메시지 큐를 메모리에 할당한 변수입니다.
- KernelMsgQ_Num은 enum 형으로 선언한 값으로 전체 메시지 큐의 개수를 표시하는 값입니다. 지금은 3입니다.
- Kernel_msgQ_init()은 sMsgQ를 0으로 초기화하는 함수입니다.
- Kernel_msgQ_is_empty()는 메시지 큐가 비었는 지 확인하는 함수입니다.
- Kernel_msgQ_is_full()은 메시지 큐가 꽉 차 있는지 확인하는 함수입니다.
- Kernel_msgQ_enqueue()는 메시지 큐에 데이터를 넣는 함수입니다.
- kernel_msgQ_dequeue()는 메시지 큐에서 데이터를 빼는 함수입니다.
이제 Kernel.c에 메시지 보내고 받는 api를 추가하겠습니다.
//Kernel.c에 함수 추가.c에 함수 추가
bool Kernel_send_msg(KernelMsgQ_t Qname, void* data, uint32_t count)
{
uint8_t* d = (uint8_t*)data;
for (uint32_t i = 0 ; i < count ; i++)
{
if (false == Kernel_msgQ_enqueue(Qname, *d))
{
for (uint32_t j = 0 ; j < i ; j++)
{
uint8_t rollback;
Kernel_msgQ_dequeue(Qname, &rollback);
}
return false;
}
d++;
}
return true;
}
uint32_t Kernel_recv_msg(KernelMsgQ_t Qname, void* out_data, uint32_t count)
{
uint8_t* d = (uint8_t*)out_data;
for (uint32_t i = 0 ; i < count ; i++)
{
if (false == Kernel_msgQ_dequeue(Qname, d))
{
return i;
}
d++;
}
return count;
}
- send_msg()→ 일부만 들어간 불완전한 데이터를 다시 빼내야 메시지 큐의 무결성을 보장!
- : 데이터를 큐에 넣는 중 큐가 꽉 차버리는 상태에 대한 예외 처리
- rcv_msg()→ 10바이트를 읽는 중 메시지 큐에 7바이트 밖에 없었다면, 함수를 한번 더 호출해 3바이트를 추가로 읽기 위함.
- : 데이터를 메시지 큐에서 읽는 도중 더 읽을 것이 없는 상태에 대한 예외 처리
여기까지 해서 커널의 메시징 관련 기능을 모두 구현했습니다.
태스크 간 데이터 전달
이제 앞서 만든 메시징 기능을 이용해 예제를 만들어보겠습니다. UART 인터럽트 핸들러에서 이벤트와 함께 UART를 통해 들어온 키보드 입력 값을 메시지 큐로 보내겠습니다.
인터럽트 핸들러를 수정해서 키보드 입력을 메시지로 보내고 입력이 들들어왔다는 소식을 이벤트로 보내겠습니다.
//Uart.c 인터럽트 핸들러 수정
static void interrupt_handler(void)
{
uint8_t ch = Hal_uart_get_char();
Hal_uart_put_char(ch);
Kerenl_send_msg(KernelMsgQ_Task0, &ch, 1);
Kernel_send_events(KernelEventFlag_UartIn);
}
Task0의 메시지 큐에 입력된 값을 넣고, 이벤트를 발생시킵니다.
Task0에서 UartIn 인터럽트가 발생하면 메시지 큐에서 1바이트를 읽어서 내부 버퍼에 읽은 값을 쌓아둡니다. 그러다 엔터가 입력되면 버퍼에 쌓아두었던 값을 Task1의 메시지 큐에 넣고 CmdIn 이벤트를 보낼 것입니다.
//Main.c task0, task1 변경
void User_task0(void)
{
uint32_t local = 0;
debug_printf("User Task #0 SP=0x%x\\n", &local);
uint8_t cmdBuf[16];
uint32_t cmdBufIdx = 0;
uint8_t uartch = 0;
while(true)
{
KernelEventFlag_t handle_event = Kernel_wait_events(KernelEventFlag_UartIn|KernelEventFlag_CmdOut);
switch(handle_event)
{
case KernelEventFlag_UartIn:
Kernel_recv_msg(KernelMsgQ_Task0, &uartch, 1);
if (uartch == '\\r')
{
cmdBuf[cmdBufIdx] = '\\0';
Kernel_send_msg(KernelMsgQ_Task1, &cmdBufIdx, 1);
Kernel_send_msg(KernelMsgQ_Task1, cmdBuf, cmdBufIdx);
Kernel_send_events(KernelEventFlag_CmdIn);
cmdBufIdx = 0;
}
else
{
cmdBuf[cmdBufIdx] = uartch;
cmdBufIdx++;
cmdBufIdx %= 16;
}
break;
case KernelEventFlag_CmdOut:
debug_printf("\\nCmdOut Event by Task0\\n");
break;
}
Kernel_yield();
}
}
void User_task1(void)
{
uint32_t local = 0;
debug_printf("User Task #1 SP=0x%x\\n", &local);
uint8_t cmdlen = 0;
uint8_t cmd[16] = {0};
while(true)
{
KernelEventFlag_t handle_event = Kernel_wait_events(KernelEventFlag_CmdIn);
switch(handle_event)
{
case KernelEventFlag_CmdIn:
memclr(cmd, 16);
Kernel_recv_msg(KernelMsgQ_Task1, &cmdlen, 1);
Kernel_recv_msg(KernelMsgQ_Task1, cmd, cmdlen);
debug_printf("\\nRecv Cmd: %s\\n", cmd);
break;
}
Kernel_yield();
}
}
빌드 후 테스트하니 다음과 같습니다.
마치며
이번엔 큐 자료구조를 활용해서 메시징 기능을 만들었습니다. 태스크나 인터럽트 간 데이터를 전달하고 싶을 때는 큐에 데이터를 넣고 꺼내기만 하면 됩니다. 이벤트와 조합해 필요한 정보를 어떤 태스크가 보내고 받는지를 제어합니다.
참고
RealViewPB 데이터시트 https://developer.arm.com/documentation/dui0417/d/?lang=en
저자 이만우님 깃허브 https://github.com/navilera/Navilos
'임베디드' 카테고리의 다른 글
Arm 아키텍처 (1) | 2024.12.02 |
---|---|
[RTOS 개발하기] 임베디드 OS 개발 프로젝트ch.13 (3) | 2024.11.18 |
[RTOS 개발하기] 임베디드 OS 개발 프로젝트ch.11 (1) | 2024.11.16 |
[RTOS 개발하기] 임베디드 OS 개발 프로젝트ch.10 (0) | 2024.11.13 |
[RTOS 개발하기] 임베디드 OS 개발 프로젝트ch.9 (0) | 2024.11.12 |