예제로 보는 Pixhawk - whdlgp/look_into_pixhawk_with_apm GitHub Wiki

예제

예제를 보는 이유

APM쪽 문서를 보면 처음에는 간단히 구조를 언급하면서 각 부분이 의미하는 것이 무엇인지 따지는 방식으로 정리되어 있다.
대부분의 문서가 그렇지만 나도 보통 내 프로젝트를 문서화 할땐 이런식으로 한다.

그런데 이렇게 문서가 정리되어 있어도, 각 파트가 눈에 안들어오는게 문제다. 뭔 소리냐면 경상도 사투리랑 전라도 사투리 같은 느낌?
그래서 일단 그쪽 코딩 컨밴션이나 백그라운드 등을 알아볼 필요가 있는 것이다. 내가 만드는게 아니라 들어가는 쪽이니, 그쪽 전통을 따라줘야 하지 않는가?

쨌든 익숙해지기에는 그쪽 사람들이 간단히 만든 예제를 한번 보는게 갑이다. 짧은 예제에서도 의외로 많은 것을 알 수 있다.

다음은 ardupilot/libraries/AP_AHRS/examples/AHRS_Test 에 있는 예제다.

// -*- tab-width: 4; Mode: C++; c-basic-offset: 4; indent-tabs-mode: nil -*-

//
// Simple test for the AP_AHRS interface
//

#include <AP_ADC/AP_ADC.h>
#include <AP_AHRS/AP_AHRS.h>
#include <AP_HAL/AP_HAL.h>

const AP_HAL::HAL& hal = AP_HAL::get_HAL();

// INS and Baro declaration
AP_InertialSensor ins;

Compass compass;

AP_GPS gps;
AP_Baro baro;
AP_SerialManager serial_manager;

// choose which AHRS system to use
AP_AHRS_DCM  ahrs(ins, baro, gps);



#define HIGH 1
#define LOW 0

void setup(void)
{
    ins.init(100);
    ahrs.init();
    serial_manager.init();

    if( compass.init() ) {
        hal.console->printf("Enabling compass\n");
        ahrs.set_compass(&compass);
    } else {
        hal.console->printf("No compass detected\n");
    }
    gps.init(NULL, serial_manager);
}

void loop(void)
{
    static uint16_t counter;
    static uint32_t last_t, last_print, last_compass;
    uint32_t now = AP_HAL::micros();
    float heading = 0;

    if (last_t == 0) {
        last_t = now;
        return;
    }
    last_t = now;

    if (now - last_compass > 100*1000UL &&
        compass.read()) {
        heading = compass.calculate_heading(ahrs.get_rotation_body_to_ned());
        // read compass at 10Hz
        last_compass = now;
    }

    ahrs.update();
    counter++;

    if (now - last_print >= 100000 /* 100ms : 10hz */) {
        Vector3f drift  = ahrs.get_gyro_drift();
        hal.console->printf(
                "r:%4.1f  p:%4.1f y:%4.1f "
                    "drift=(%5.1f %5.1f %5.1f) hdg=%.1f rate=%.1f\n",
                        ToDeg(ahrs.roll),
                        ToDeg(ahrs.pitch),
                        ToDeg(ahrs.yaw),
                        ToDeg(drift.x),
                        ToDeg(drift.y),
                        ToDeg(drift.z),
                        compass.use_for_yaw() ? ToDeg(heading) : 0.0f,
                        (1.0e6f*counter)/(now-last_print));
        last_print = now;
        counter = 0;
    }
}

AP_HAL_MAIN();

위 예제를 고른 이유는 분석하기 만만해 보여서다.

전처리

#include <AP_ADC/AP_ADC.h>
#include <AP_AHRS/AP_AHRS.h>
#include <AP_HAL/AP_HAL.h>

AP_ADC 해더의 경우 설명상으로는 아두이노 메가 보드의 ADC용 이라고 되있다. Pixhawk에서도 이용되는가? 잘 모르겠다.
AP_AHRS 해더는 이름에서 추측하건데 AHRS, 즉 자세추정이나 위치 추정 등에 이용되는 알고리즘 들이 들어있는 것 같다. AP_HAL의 경우 APM쪽에서 만든 아랫단 드라이버라고 한다. 이것때문에 포팅이 잘된다나 뭐라나,,,

#define HIGH 1
#define LOW 0

이는 아두이노에서 따온듯한 네이밍이다. 실제로 아두이노에서 digitalWrite(1, HIGH) 와 같이 HIGH와 LOW를 쓰기 때문, APM은 아두이노 개발환경에서 프로젝트를 시작하였기 때문에 아두이노에서 사용하던 흔적이 이곳저곳 많이 묻어있다. 아두이노의 대한 사전 지식이 있다면 수월해질 순 있겠다(그렇다고 진입장벽이 낮아지는건 아니다.)

해더파일에서 봤듯이 이 사람들은 이름 짓는 스타일부터 짝대기 '_'를 쓴다는 것을 알 수 있다. 낙타형식으로 apAdc와 같이 쓰는 나와는 차이가 나는데, 이후 코드에서도 이러한 형식은 자주 보이며, 내가 익숙해져야 할 파트중 하나다.
(사소하면 사소한데, 나는 개인적으로 이 짝대기 쓰는게 정말 마음에 안든다)

선언부

// INS and Baro declaration
AP_InertialSensor ins;

Compass compass;

AP_GPS gps;
AP_Baro baro;
AP_SerialManager serial_manager;

// choose which AHRS system to use
AP_AHRS_DCM  ahrs(ins, baro, gps);

아두이노의 객체를 활용하는 부분에서 클래스명은 대문자로 시작하고, 인스턴스는 소문자로 시작하는 경우가 많다.
이는 아두이노에서 시작한 APM에서도 뭍어나온다.

아두이노의 경우 '_'를 거의 사용하지 않는다. 대부분 digitalWrite 처럼 낙타 형식으로 구분되는 부분만 대문자로 표현한다. APM은 이와 다르게 구분되는 부분을 거의 무조건 '_'를 이용해 띄워놓는다. 때문에 아두이노식 네이밍과 '_'를 사용하는 APM식 네이밍이 공존하고 있다.

ADC라든지 AHRS라든지 어떤 단어의 줄임말은 무조건 대문자로 표현하고 있고, 이 부분에 관해서는 딱히 이상하진 않다.

초기화

void setup(void)
{
    ins.init(100);
    ahrs.init();
    serial_manager.init();

    if( compass.init() ) {
        hal.console->printf("Enabling compass\n");
        ahrs.set_compass(&compass);
    } else {
        hal.console->printf("No compass detected\n");
    }
    gps.init(NULL, serial_manager);
}

setup 이라는 네이밍은 아두이노에서 그대로 따온듯. 이후 나오는 loop 와 같이 아두이노 에서는 처음 한번만 실행되는 'setup' 파트와 무한 반복되는 'loop' 문이 있다. 이는 multiwii 오픈소스 쪽에서도 마찬가지로 활용하고 있는 구분이기도 하다~~(누가 생각했는지 네이밍이 참)~~.

드라이버를 다루는 부분에서도 사소한 차이가 있는데, 아두이노의 경우 다음과 같은 드라이버 원칙을 지키는 편이다.

  • begin 초기화
  • end 닫기
  • read 읽기
  • write 쓰기

그 외 설정용의 함수를 빼두기 보다 begin 등에서 전부 처리한다. 에초에 유저가 설정에 접근하는것을 거의 막아놓은 편

APM에서의 드라이버 함수의 이름은 일반적인 UNIX나 POSIX, 여타 펌웨어 개발자들이 쓰는 드라이버 관행을 따르는 듯 하다. 예로

  • init 초기화
  • close 닫기
  • get_* 읽기

쓰기는 예제에 없어서 잘 모르겠으며, 그외 ioctr 같은 네이밍도 사용되는지는 좀더 봐야 하겠지만, 아두이노에서 온 개발자들이 점점 줄어든건지, 여타 펌웨어를 개발하다 참가한 인원이 많아진건지 아두이노 스타일에서 조금씩 벗어나고 있는 경향이 보인다. ('_' 도 그렇고,,,)

if (~~~~~~~){
...
}

코딩 스타일의 경우 완벽한 K&R은 아니지만 K&R 스타일의 코딩 스타일을 따르고 있다. 문제는 필자가 Allman에 상당히 익숙해져서 K&R을 싫어한다는 점,,, 이 사람들이 이걸 좋아한다는데 어쩔수 없다. (후,,,)

    if( compass.init() ) {
        hal.console->printf("Enabling compass\n");
        ahrs.set_compass(&compass);
    } else {
        hal.console->printf("No compass detected\n");
    }

compass 센서의 초기화를 진행하면서 compass 센서가 있을경우와 없을 경우를 에러 처리 해놓았다. 에러처리 형식이 시스템 프로그래밍 하는 인간들이 하는 스타일. printf 와 같은 이름을 사용하는 것을 봐서 거의 확정이다.

무한반복문

void loop(void)
{
    static uint16_t counter;
    static uint32_t last_t, last_print, last_compass;
    uint32_t now = AP_HAL::micros();
    float heading = 0;

    if (last_t == 0) {
        last_t = now;
        return;
    }
    last_t = now;

    if (now - last_compass > 100*1000UL &&
        compass.read()) {
        heading = compass.calculate_heading(ahrs.get_rotation_body_to_ned());
        // read compass at 10Hz
        last_compass = now;
    }

    ahrs.update();
    counter++;

    if (now - last_print >= 100000 /* 100ms : 10hz */) {
        Vector3f drift  = ahrs.get_gyro_drift();
        hal.console->printf(
                "r:%4.1f  p:%4.1f y:%4.1f "
                    "drift=(%5.1f %5.1f %5.1f) hdg=%.1f rate=%.1f\n",
                        ToDeg(ahrs.roll),
                        ToDeg(ahrs.pitch),
                        ToDeg(ahrs.yaw),
                        ToDeg(drift.x),
                        ToDeg(drift.y),
                        ToDeg(drift.z),
                        compass.use_for_yaw() ? ToDeg(heading) : 0.0f,
                        (1.0e6f*counter)/(now-last_print));
        last_print = now;
        counter = 0;
    }
}

반복문 내 변수 선언

    static uint16_t counter;
    static uint32_t last_t, last_print, last_compass;
    uint32_t now = AP_HAL::micros();
    float heading = 0;

지역변수 선언의 경우 함수 내 최상단에 위치하고 있다. 당연한 사람한태는 당연하겠지만, 보통 C89 정도 되는 C표준으로 배운 사람들이 변수 선언을 최상단에 밀어넣는다. 필자의 경우 최상단에는 비교적 함수 전체적으로 사용되고 재활용 가능성이 있는 변수를, 중간 중간에 부분적으로만 간혹 사용되는 변수는 그 부분에서 선언한다(기억하기로는 C99부터 이런게 된다).

micros 와 같은 이름의 경우 아두이노에서 따온것이다. 아두이노는 라이브러리나 유틸리티만 이용하고, 직접 개발할 때는 다른 펌웨어 개발을 하던 스타일로 작성한 것으로 보인다.

전역변수를 거의 사용하지 않고 loop내에서 static으로 처리한 것으로 보아, '전역변수보단 지역변수'와 같은 개념을 어느정도 잡고 있는 것 같다. 이상하게 펌웨어 개발하는 사람들중 전역변수로 선언하고 아무나 가져다 쓰라는, 레이스 컨디션 과 같은 문제를 고려하지 않은 코드를 작성할 때가 많은데, 나름 컴퓨터 공학적인 생각이 있다는 것을 볼 수 있다.

다른 반복 내용물들

 if (last_t == 0) {
        last_t = now;
        return;
    }
    last_t = now;

뭐야. 이건 왜 넣은거야.

    if (now - last_compass > 100*1000UL &&
        compass.read()) {
        heading = compass.calculate_heading(ahrs.get_rotation_body_to_ned());
        // read compass at 10Hz
        last_compass = now;
    }

일정 주기로 실행 시키기 위한 방법. 그다지 새로운 방법은 아니다. __일정주기__라는 개념을 사용하는 것 자체가 "이 개발보드는 1초동안 최대 몇번이나 센서읽는게 가능한가" 라는 질문을 던지는 물리하던 사람이나, 센서 드라이버를 개발하던 사람 등이 자주 쓰는 개념이다. 적분하고 할려면 읽는 시간을 일정하게 할 필요가 있고, 1초에 몇번이나 읽을 수 있는지를 생각하는게 머리로 생각하기 편하기 때문이다. 항공쪽 인간들도 "초당 100번 연산한다" 같은 소리를 자주 한다.

 ahrs.update();

복잡한 알고리즘은 보여주기 보다 아예 가려버렸다. 암묵적으로 이 부분에 대해서는 유저 입장에서 이용만 하라는 표시. 그 외 다른거나 건들이라는 말이다. 내 직감도 이 부분은 머리아플것 같으니 피하라고 하고있다.

    counter++;

    if (now - last_print >= 100000 /* 100ms : 10hz */) {
        Vector3f drift  = ahrs.get_gyro_drift();
        hal.console->printf(
                "r:%4.1f  p:%4.1f y:%4.1f "
                    "drift=(%5.1f %5.1f %5.1f) hdg=%.1f rate=%.1f\n",
                        ToDeg(ahrs.roll),
                        ToDeg(ahrs.pitch),
                        ToDeg(ahrs.yaw),
                        ToDeg(drift.x),
                        ToDeg(drift.y),
                        ToDeg(drift.z),
                        compass.use_for_yaw() ? ToDeg(heading) : 0.0f,
                        (1.0e6f*counter)/(now-last_print));
        last_print = now;
        counter = 0;
    }

마찬가지로 일정한 주기라는 개념을 이용하고 있다. 계산을 용이하기 하기 위해서인지 Deg 같은 용어에 집착을 보이고 있다. 러프하게 개발하는 사람들 한태는 귀찮아 보이지만, 이런식으로 단위를 명확하게 해주면 계산하는 사람 입장에서 이용하기 편리하다.

AP_HAL_MAIN();

setup과 loop라는 개념으로 나눠놓았던 것들을 main 내부로 집어넣는 과정이 들어있을 것임을 추측할 수 있다.

각 로직이 의미하는 바 보다 어떤 스타일/느낌으로 개발하는지를 중점으로 살펴봤다. 대충 어떤식으로 개발을 진행하는지 알 수 있었다.

그 이외 Nuttx OS라는 환경과 여타 개발용 환경들을 어떤식으로 이용하고 있으며, 자신들의 환경은 어떻게 구축했는지 알 필요가 있다. 예로 멀티스래드를 하는 방법이라든지, 돌아가는 순서라든지,,, 일단 이정도로 하자.