FreeRTOS V2 OS Timer 삽질기

기존 프로젝트는 None-OS 모델로 개발이 진행되어, OS 없이 개발된 코드는 인수 인계 받아 프로젝트를 진행하였지만, 이번 새로 진행하는 프로젝트는 LCD 모듈이 추가되어 OS 없이 개발이 가능하지만 개발의 효율성을 높이기 위해 STM32에서 제공하는 FreeRTOS를 적용하여 진행하기로 하였다.

 

OS를 사용하면 쓰레드 생성 및 공유자원 접근 그리고 쓰레드간 IPC를 보다 편하게 처리할 수 있으며, tick 타임을 직접 계산하여 타이머처럼 코딩하는 것이 아니라 OS에서 제공하는 SW Timer를 사용하면 보다 편하게 코딩할 수 있다.

 

그런데, 이번에 FreeRTOS를 처음 접해서 케바케로 다양한 삽질을 진행중에 있다. 우선 FreeRTOS 타이머를 생성하여 500ms 마다 주기적으로 타이머 콜백함수가 실행되도록 하였는데, 타이머 생성에는 문제가 없는데 콜백함수가 실행되지 않아 FreeRTOS 커널을 분석해보니 정말 어처구니 없는 나의 실수(?)로 콜백함수가 실행되지 않는 것이었다.

 

우선 main thread를 하나 생성하고 monit 및 ui thread를 생성하여 UI 및 각종 Peripheral 데이터를 수집하는 정도 작성한 코드이다.

static task_t main_thread;

static void main_task(void* args)
{
  warn("Version: v%s [%s]", FW_VERSION_STRING, FW_BUILD_TYPE);
  warn("Build at %s %s", __DATE__, __TIME__);

  st7789_init();

  led_init();
  monit_init();
  ui_init();

  console_init();
  while (1) {
    wdg_feed();
    console_step();
  }
}

int main(void)
{
#ifdef NUCLEO
  nucleo_init();
#elif defined(MINI)
  mini_init();
#endif

  wdg_init();
  gpio_init();
  uart_init();
  adc_init();
  spi_init();
  i2c_init();
  timer_init();

  os_init();
  const task_attr_t main_attr = {
      .name       = "main_task",
      .stack_size = 512 * 4,
      .priority   = priorityNormal - 1,
      .task_func  = main_task,
  };
  if ((main_thread = os_task_create(&main_attr)) == NULL) {
    error("main_task create fail");
  }
  os_start_schedule();
}

monit_init() 함수에서 타이머를 생성하여 500ms 마다 i2c 통신을 하여 온도 값을 읽어오도록 하는 타이머를 등록하였는데, i2c 통신을 담당하는 타이머 콜백함수가 호출되지 않는 문제가 발생하여 FreeRTOS 커널을 리뷰한 결과 FreeRTOS의 타이머 동작은 커널 initial 과정에서 아래 이미지와 같이 타이머를 관리하는 prvTimerTask 쓰레드(테스크)가 생성된다.

prvTimerTask는 등록된 타이머가 타임아웃이 발생하면 콜백함수가 실행되도록 구현되어 있는데, 여기서 콜백함수가 호출되지 않는 이유를 알수가 없었다. 500ms 마다 실행되는 타이머을 생성한 코드는 아래와 같다.

static os_timer_t timer_thermal;

void timer_read_thermal(void* args)
{
  ntc75_read_temperature();
}

void thermal_collect(monit_t* m)
{
  m->temperature.board = ntc75_get_temperature();
}

void thermal_init(void)
{
  timer_attr_t attr = {
      .name     = "timer_thermal",
      .type     = timer_periodic,
      .timer_cb = timer_read_thermal,
  };
  if ((timer_thermal = os_timer_create(&attr)) == NULL) {
    error("%s() : thermal_timer create fail", __func__);
    return;
  }

  ntc75_init();
  os_timer_start(timer_thermal, TEMPERATURE_READ_INTERVAL);
}

타이머 생성과정도 문제가 없었고, 타이머를 실행하는 prvTimerTask도 동작하는데 왜 타이머 콜백함수가 실행 안되는지 이유를 알 수가 없어서 prvTimerTask에 디버그 메시지를 출력하여 실행하는데.... prvTimerTask가 전혀 동작하지 않는 것이 아닌가...?


prvTimerTask가 동작하지 않는 원인은 아주 단순한 이유였다. CubeIDE에서 FreeRTOS Timer Task의 Priority의 기본값이 2로 설정되어 발생한 문제였다. Timer Task Priority를 main thread priority와 같은 23(osPriorityNormal - 1)로 설정하면 prvTimerTask가 동작하는 것을 확인할 수 있다.

 

여기서 하나 헷갈리는 점이 main thread에서 console_step에서 uart read timeout 값이 설정되어 있어, uart read할 때 대기시간 동안 OS Scheduler가 Task Switching을 할 것이라고 생각하였는데, FreeRTOS는 테스트 전환이 일어나지 않았다. OS 특징인지 아니면 내가 잘못 알고 있는 것인지 모르겠지만, 위와 같은 OS Timer Task Priority가 기본 값이 2인로 설정된 상태라면 main thread에서  명시적으로 Task Switching을 하기 위해 os_delay() 함수를 사용해야 되는 것 같다.

 

원인을 찾았다. 여러 Peripheral 정보를 수집하는 monit_task priority가 24로 설정되어 있고, 여기서 os_delay()를 실행하지 않아, 타이머 콜백함수를 실행시키는 FreeRTOS의 prvTimerTask Priority가 2로 설정되어 있어 작업 전환이 일어나지 않은 것이다.

 

별 것아닌 문제인데, 몇 시간동안 헤맸지만 이참에 FreeRTOS 코드도 살펴보았고 내가 알고 있던 OS 상식과 다르다는 것을 알게 되었고, 어쨌든 타이머 콜백함수가 동작되지 않는 정확한 원인을 찾아 위안을 삼는다. ㅎ