-
Notifications
You must be signed in to change notification settings - Fork 1
스케줄러 이해를 위한 example
#APM에서 이용하고 있는 Scheduler
Pixhawk는 Nuttx 라는 RTOS를 사용한다. 그리고 RTOS를 사용하는 이유는 일반적으로 멀티쓰래드 이용을 위해서이다. 따라서 본인은 Pixhawk에 APM 이 이용된다고 하였을 때, 맨 처음에는 pthread를 이용한 멀티쓰래드로 구성했을 것이라 생각했다. 그렇지만 실제로 pthread로 구성하기 보다, ArduCopter 쓰래드 하나만 돌아간다.
이는 Pixhawk 쪽 프로젝트와 다른 접근방법인데, Pixhawk는 RTOS가 주는 멀티쓰래드 API 를 적극적으로 이용한다. 반면 APM은 단순히 프로그램을 돌리기 위한 장치로 RTOS를 이용하는 것이다. 이와 비슷한 예로 OROCA 카페에서 진행하였던 MultiWii 기반 프로젝트가 있다.
이에 대해서는 이해가 되는것이 본래 APM 프로젝트는 8bit MCU라는 제약적인 조건에서 구현된 프로젝트이고, 기존에 FreeRTOS와 같은 RTOS를 이용하지 않았다. 내부적으로 Scheduler를 구현해서 쓰래드 비스무리하게 간단한 구현이 있을 뿐이었고, 이를 멀티쓰래드로 바꾸기에는 구조가 커져버린 결과일 것이다.
쨋든, Pixhawk에서 APM을 이용하기 위해서는 Pixhawk의 멀티쓰래드 이용법 뿐만 아니라 APM쪽의 Scheduler쪽도 알아보아야 한다. 물론 APM내부 코드중 APM에 특징적인 부분만 수정한다면 APM쪽 코드만 보아도 되겠지만.
이전 예제와 마찬가지로 libraries 폴더 내부에 들어가 있다.
/libraries/AP_Scheduler/examples/Scheduler_test/
// -*- tab-width: 4; Mode: C++; c-basic-offset: 4; indent-tabs-mode: nil -*-
//
// Simple test for the AP_Scheduler interface
//
#include <AP_HAL/AP_HAL.h>
#include <AP_InertialSensor/AP_InertialSensor.h>
#include <AP_Scheduler/AP_Scheduler.h>
const AP_HAL::HAL& hal = AP_HAL::get_HAL();
class SchedTest {
public:
void setup();
void loop();
private:
AP_InertialSensor ins;
AP_Scheduler scheduler;
uint32_t ins_counter;
static const AP_Scheduler::Task scheduler_tasks[];
void ins_update(void);
void one_hz_print(void);
void five_second_call(void);
};
static SchedTest schedtest;
#define SCHED_TASK(func, _interval_ticks, _max_time_micros) SCHED_TASK_CLASS(SchedTest, &schedtest, func, _interval_ticks, _max_time_micros)
/*
scheduler table - all regular tasks are listed here, along with how
often they should be called (in 20ms units) and the maximum time
they are expected to take (in microseconds)
*/
const AP_Scheduler::Task SchedTest::scheduler_tasks[] = {
SCHED_TASK(ins_update, 50, 1000),
SCHED_TASK(one_hz_print, 1, 1000),
SCHED_TASK(five_second_call, 0.2, 1800),
};
void SchedTest::setup(void)
{
ins.init(scheduler.get_loop_rate_hz());
// initialise the scheduler
scheduler.init(&scheduler_tasks[0], ARRAY_SIZE(scheduler_tasks));
}
void SchedTest::loop(void)
{
// wait for an INS sample
ins.wait_for_sample();
// tell the scheduler one tick has passed
scheduler.tick();
// run all tasks that fit in 20ms
scheduler.run(20000);
}
/*
update inertial sensor, reading data
*/
void SchedTest::ins_update(void)
{
ins_counter++;
ins.update();
}
/*
print something once a second
*/
void SchedTest::one_hz_print(void)
{
hal.console->printf("one_hz: t=%lu\n", (unsigned long)AP_HAL::millis());
}
/*
print something every 5 seconds
*/
void SchedTest::five_second_call(void)
{
hal.console->printf("five_seconds: t=%lu ins_counter=%u\n", (unsigned long)AP_HAL::millis(), ins_counter);
}
/*
compatibility with old pde style build
*/
void setup(void);
void loop(void);
void setup(void)
{
schedtest.setup();
}
void loop(void)
{
schedtest.loop();
}
AP_HAL_MAIN();
이번에도 짧다면 짧다고 할 수 있는 예제다. 특징적인 것은 스케줄러의 테스트용 코드를 class로 구현하였고, setup과 loop 또한 테스트용으로 빼내고, 진짜 setup과 loop에서 호출하고 있다는 점이다.
이러한 설계 방법으로 각 기능별로 초기화(setup)와 반복(loop) 파트를 구성하고, 한 setup과 loop에서 다른 setup, loop들을 호출하는 설계를 가능하게 한다. 개인적으로는 꽤 마음에 듬.
class SchedTest {
public:
void setup();
void loop();
private:
AP_InertialSensor ins;
AP_Scheduler scheduler;
uint32_t ins_counter;
static const AP_Scheduler::Task scheduler_tasks[];
void ins_update(void);
void one_hz_print(void);
void five_second_call(void);
};
static SchedTest schedtest;
매써드 명에서 알 수 있듯이 ins_update나 one_hz_print나 five_second_call 등은 테스크(쓰래드로 봐도 무방할 것이다.) 들이다.
static const AP_Scheduler::Task scheduler_tasks[];
위 구문에서 AP_Scheduler::Task는 테스크 등록용 변수임을 예측할 수 있고, 배열로 선언된 것을 보아 여러 태스크를 등록할 것임을 알 수 있다. 그 외 다른 필요한 클레스의 인스턴스를 선언하는 부분은 넘기기로 하자.
#define SCHED_TASK(func, _interval_ticks, _max_time_micros) SCHED_TASK_CLASS(SchedTest, &schedtest, func, _interval_ticks, _max_time_micros)
위 전처리 구문에서 SCHED_TASK_CLASS라는 테스크 등록용 메크로가 있는 듯 함을 알 수 있는데, 이용은 앞서 말한 AP_Scheduler::Task 형 배열을 같이 이용하는 듯 하다. 실제 사용한 예는 다음과 같다.
/*
scheduler table - all regular tasks are listed here, along with how
often they should be called (in 20ms units) and the maximum time
they are expected to take (in microseconds)
*/
const AP_Scheduler::Task SchedTest::scheduler_tasks[] = {
SCHED_TASK(ins_update, 50, 1000),
SCHED_TASK(one_hz_print, 1, 1000),
SCHED_TASK(five_second_call, 0.2, 1800),
};
SCHED_TASK_CLASS(SchedTest, &schedtest, func, _interval_ticks, _max_time_micros)
에서 본래 테스크 등록은 클래스 단위로 묶어 이용해야 함을 알 수 있다. 따라서 인자는 왼쪽부터 순서대로
- 클래스 이름
- 인스턴스의 포인터
- 등록할 매써드명
- 실행할 인터벌 (Hz)
- 아무래도 타임아웃용? 정도 일 것이다.
등록 후에는 내부 스케줄러가 알아서 스케줄링을 해 줄 것을 기대할 수 있을것이다.
void SchedTest::setup(void)
{
ins.init(scheduler.get_loop_rate_hz());
// initialise the scheduler
scheduler.init(&scheduler_tasks[0], ARRAY_SIZE(scheduler_tasks));
}
간단하게 관성센서(IMU라고 해야 하려나?)와 스케줄러를 초기화(또는 이용하겠다는 의사표현)하고 있다.
scheduler.get_loop_rate_hz()
관성센서의 초기화 인자에 실행할 인터벌 값을 인자로 넣는 부분인 것 같은데, _loop_rate_hz 라는 것을 반환하도록 되어있고, 이 값은 Scheduler 라이브러리의 _loop_rate_hz.set(SCHEDULER_DEFAULT_LOOP_RATE); 구문에 의해 내부적으로 보통으로 지정된 값을 쓰나 보다. (자세히 보려고 추적하려면 귀찮을 것 같은,,,)
scheduler.init(&scheduler_tasks[0], ARRAY_SIZE(scheduler_tasks)); 구문에서 등록할 테스크들의 배열의 처음 주소값과 전체 배열 크기를 인자로 주는것은 사실 크게 놀라운 일이 아니다.
void SchedTest::loop(void)
{
// wait for an INS sample
ins.wait_for_sample();
// tell the scheduler one tick has passed
scheduler.tick();
// run all tasks that fit in 20ms
scheduler.run(20000);
}
관성센서의 값이 갱신될때까지 기다리고, scheduler.tick(); 이란 녀석을 호출한다. 단순히 _tick_counter++; 할 뿐인 레퍼함수다. 이 _tick_counter 란 녀석은 어떤 작업이 시간을 너무 잡아먹는지 등을 판별하기 위한 일종의 장치인 것 같은데, counter 란 이름을 꽤나 많이 사용하는 것 같다. 이 counter 들이 어디서 어떻게 사용되는지는 소스를 좀 뒤적뒤적 해봐야 될 듯.
scheduler.run(20000); 은 스케줄러를 20000us (20ms) 단위로 실행시키겠다는 의미다. 이게 뭔소리냐면, 20ms에 한번씩 돌릴 테스크가 있는지 체크하면서, 돌릴 테스크가 있으면 돌리겠다는 의미다.
/*
update inertial sensor, reading data
*/
void SchedTest::ins_update(void)
{
ins_counter++;
ins.update();
}
/*
print something once a second
*/
void SchedTest::one_hz_print(void)
{
hal.console->printf("one_hz: t=%lu\n", (unsigned long)AP_HAL::millis());
}
/*
print something every 5 seconds
*/
void SchedTest::five_second_call(void)
{
hal.console->printf("five_seconds: t=%lu ins_counter=%u\n", (unsigned long)AP_HAL::millis(), ins_counter);
}
테스크(쓰래드) 함수들이다.
void SchedTest::ins_update(void)
{
ins_counter++;
ins.update();
}
마찬가지로 무슨 counter를 ++ 하는데, 자세한건 counter가 이용되는 경로를 찾아봐야 할 것 같다. 다음 두개중 하나일 것 같다.
- 관성센서가 update 된 횟수
- 관성센서를 update 해야 하는지 판별하는 기준(이 경우 어느 순간 0으로 만드는 구간이 있을것이다.)
void SchedTest::one_hz_print(void)
{
hal.console->printf("one_hz: t=%lu\n", (unsigned long)AP_HAL::millis());
}
millis 함수는 시스템이 시작한 때부터 현제까지 흐른 시간을 ms 단위로 알려주는 함수다.
void SchedTest::five_second_call(void)
{
hal.console->printf("five_seconds: t=%lu ins_counter=%u\n", (unsigned long)AP_HAL::millis(), ins_counter);
}
이 경우 흐른 시간과, ins_counter 라는 녀석을 같이 띄운다. ins_counter는 앞서 말했듯이 관성센서가 업데이트 되는 구간에서 1씩 더해진다. 역시 업데이트 된 횟수를 의미하는것인지,,,
/*
compatibility with old pde style build
*/
void setup(void);
void loop(void);
void setup(void)
{
schedtest.setup();
}
void loop(void)
{
schedtest.loop();
}
AP_HAL_MAIN();
이후 원래 있던 setup, loop 문으로 레핑한다.
내가 사용하던 FreeRTOS와 사용법이 비슷하면서 조금 다른 부분이 있는 것 같다. APM을 본격적으로 개발하려면 반드시 능숙해저야 할 부분인 것으로 보이는데, 실제 APM쪽 문서를 보면 위 예제와 조금 다른 부분이 있다(내 생각엔 그쪽 문서가 오래된 거 같다). 그런 부분은 직접 해보거나 유추하면서 알아내야 할 텐데,,, 프로젝트 양도 꽤 되고 귀찮은 작업이 될 것 같다 ㅡㅡ;