[임베디드 OS 개발 프로젝트] 12. 메시징

Sijin·2025년 7월 22일

메세지 큐

  • 이벤트만으로는 많은 정보를 보낼 수 없다
  • 이벤트와 함께 데이터를 보내기 위해서는 메시징 기능을 이용해야 한다
  • 메시징 기능은 메세지를 어떻게 관리할지 결정하는 것에서 시작한다. 우리는 큐를 사용할 것이다
# 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 포인터 출력이 더이상 되지 않는 것을 볼 수 있다

0개의 댓글