메세지 큐
- 이벤트만으로는 많은 정보를 보낼 수 없다
- 이벤트와 함께 데이터를 보내기 위해서는 메시징 기능을 이용해야 한다
- 메시징 기능은 메세지를 어떻게 관리할지 결정하는 것에서 시작한다. 우리는 큐를 사용할 것이다
# kernel/msg.h
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);
- 메세지 큐는 3개를 만들 것이다
- 메세지 큐는 front, rear를 index로 circular queue 형태로 만들 것이다
- 메세지 큐 하나의 크기는 512byte이다
- 메세지 큐를 태스크, 이벤트처럼 커널 API로 만들어보자
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 (!Kernel_msgQ_enqueue(Qname, *d))
{
for (uint32_t j = 0; j < i; j++)
{
uint8_t rollback;
Kernel_msg0_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 (!Kernel_msgQ_dequeue(Qname, d))
{
return i;
}
d++;
}
return count;
}
- Kernel_send_msg에서는 데이터를 메세지 큐에 넣는다
- 메세지 큐가 도중에 다 차버려서
Kernel_msgQ_enqueue에서 false를 리턴하면 다시 데이터를 빼낸다
- Kernel_recv_msg는 큐에서 데이터를 읽는다
- 메세지를 읽는 도중에 메세지 큐가 비어버리면 현재까지 읽은 byte 수만 리턴한다
- 나머지는 호출하는 쪽에서 알아서 처리해야 한다
태스크 간 데이터 전달
- 만든 메세징 기능을 이용해 Uart 입력값을 메세지큐로 보내보자
- Uart interrupt handler에서 아래처럼 이벤트 호출 전에 메세지 큐에 입력된 1byte를 넣자
static void interrupt_handler(void)
{
uint8_t ch = Hal_uart_get_char();
Hal_uart_put_char(ch);
Kernel_send_msg(KernelMsgQ_Task0, &ch, 1);
Kernel_send_events(KernelEventFlag_UartIn);
}
- Uart 이벤트를 처리하는 Task0을 만들어보자
void task_0(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';
while (true)
{
Kernel_send_events(KernelEventFlag_CmdIn);
if (false == Kernel_send_msg(KernelMsgQ_Task1, &cmdBufIdx, 1))
{
Kernel_yield();
}
else if (false == Kernel_send_msg(KernelMsgQ_Task1, cmdBuf, cmdBufIdx))
{
uint8_t rollback;
Kernel_recv_msg(KernelMsgQ_Task1, &rollback, 1);
Kernel_yield();
}
else
{
break;
}
}
cmdBufIdx = 0;
}
else
{
cmdBuf[cmdBufIdx] = uartch;
cmdBufIdx++;
cmdBufIdx %= 16;
}
break;
case KernelEventFlag_CmdOut:
debug_printf("\nCmdOut Event by Task0\n");
break;
}
Kernel_yield();
}
}
- 오류처리때문에 코드가 조금 복잡하다
- UartIn 이벤트에서는 엔터키가 눌러지기 전까지 cmdBuf에 1byte씩 추가한다
- 엔터키가 눌러져 Uart를 통해 들어오면 메세지 큐로 데이터를 넣는다
- 우선 CmdIn 이벤트를 보낸다
- 다음으로 cmdBuf의 길이를 넣는다, 큐가 가득 차 실패한 경우 다음 task로 스케줄링해서 큐를 비워주길 기다린다
- 길이를 넣은 후, cmdBuf 데이터를 넣다가 큐가 가득 차 실패한 경우, 먼저 넣은 cmdBuf 1byte를 다시 빼주고 다음 task로 스케줄링해 큐를 비워주길 기다린다
- 데이터를 모두 보낸 후 cmdBufIdx를 0으로 만들어 cmdBuf를 초기화한다
void task_1(void)
{
uint32_t local = 0;
debug_printf("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();
}
}
- Task0으로 부터 CmdIn을 받는 Task1을 코드는 위와 같다
- 이벤트가 발생한 경우, 메세지 큐에서 데이터를 받아온다
동작 확인

- 의도한 동작을 확인할 수 있었다
- Kernel_yield가 각 task의 while문 안에서 호출되어서, 스케줄링 후 각 태스크 함수 진입점에 있는 SP 포인터 출력이 더이상 되지 않는 것을 볼 수 있다