티스토리 뷰

반응형

nRF 과 어플리케이션을 연동해서 사용 중에, 특정상황에서 nRF 칩이 먹통되는 경험을 하신 경우가 있다면, 어떻게 해결 할 수 있을까요? 단순한 디바이스면 큰 문제없을수도 있지만 나아가 세탁기, 건조, 식기 세척기, 더 크게는 로봇, 자동차, 로켓도 분명이 그런 상황이 있을 수 있을텐데요. 복잡한 로직이 되어가면 되어갈 수록 이란 'DeadLock' 상태는 발생할 가능성이 높아집니다.

DeadLock의 개념

프로세스가 자원을 얻지 못해 다음 처리를 하지 못하는 상태로 '교착 상태' 라고도 하며 시스템적으로 한정된 자원을 여러 곳에서 사용하려고 할 때 발생합니다. 

DeadLock 발생조건

- 교착 상태는 한 시스템 내에서 다음의 조건이 모두 성립될 때 발생합니다. 따라서 아래의 네 가지 조건 중 하나라도 성립하지 않도록 만든다면 교착 상태를 해결 할 수 있습니다.

  1. 상호 배제 : 자원은 한번에 한 프로세스만이 사용할 수 있어야 한다.
  2. 점유 대기 : 최소한 하나의 자원을 점유하고 있으면서 다른 프로세스에 할당되어 사용하고 있는 자원을 추가로 점유하기 위해 대기하는 프로세스가 있어야 한다.
  3. 비선점 : 다른 프로세스에 할당된 지원은 사용이 끝날 때까지 강제로 빼앗을 수 없어야 한다.
  4. 순환대기 : 프로세스의 집한에서는 P0는 P1이 점유한 자원을 대기하고 P1은 P2가 점유한 자원을 대기하고 P2--- Pn-1은 Pn이 점유한 자원을 대기하며 Pn은 P0가 점유한 자원을 요구해야 한다.

DeadLock의 처리

  1. 교착 상태 예방 법 및 회피 : 교착 상태 발생 조건 중 하나를 제거함으로써 해결하는 방법 - 자원의 낭비가 심하다.
    • 예방 법
      • 상호 배제 부정 : 어러 개의 프로세스가 공유 자원을 사용할 수 있도록 한다.
      • 점유 대기 부정 : 프로세스가 실행되기 전 필요한 모든 자원을 할 당한다.
      • 비선점 부정 : 자원을 점유하고 있는 프로세스가 다른 자원을 요구할 때 점유하고 있는 자원을 반납하고, 요구한 자원을 사용하기 위해 기다리게 한다.
      • 순환 대기 부정 : 자원에 고유한 번호를 할당하고, 번호 순서대로 자원을 요구하도록 한다.
    • 회피 법 
      • 교착 상태가 발생하면 피해나가는 방법 : 프로세스가 자원을 요구할 때 시스템은 자원을 할당한 후에도 안정 상태로 남아있게 되는지를 사전에 거사하여 교착 상태를 회피하는 기법
        1. 안전 상태에 있으면 자원을 할당하고, 그렇지 않으면 다른 프로세스들이 자원을 해지할 때까지 대기함
        2. 교착 상태가 되지 않도록 보장하기 위하여 교착 상태를 예방하거나 회피하는 프로토콜을 이용하는 벙법
    • 교착 상태 탐지 및 회복
      • 교착 상태를 일으킨 프로세스를 종료하거나, 할당된 자원을 해제함으로써 회복하는 것을 의미한다.
        1. 프로세스를 종료하는 방법
        2. 교착 상태가 제거될 때까지 한 프로세스씩 중지
    • 자원을 선점하는 방법
      • 교착 상태의 프로세스가 점유하고 있는 자원을 선점하여 다른 프로세스에게 할당하며, 해당 프로세스를 일시 정지 시키는 방법
      • 우선 순위가 낮은 프로세스, 수행된 횟수가 적은 프로세스 등을 위주로 프로세스의 자원을 선점한다.

다만, 대부분의 시스템은 교착 상태가 실제로는 잘 발생하지 않으며, 교착 상태 예방, 회피, 탐지, 복구하는 것은 비용이 많이 들게 됩니다. 교착상태가 발생하는 것은 개발 중일 때가 실제 상용화 될때는 최소화 되어있어야 하는데 그 스스로 또는 외부와의 연결시에 예측하지 못하는 상황이라고 한다면 어쨋든 최소한이의 조치라도 넣어놔야 시장의 문제점을 예방할 수 있는데요, 저 역시 개발 중에 우연치 않게 발생한 문제를 예사롭게 보지 않고, 시장에서 나올 수 있다 판단했습니다.

nRF에서 DeadLock  발생 사례

실제로 개발하면서 발생했던 사례를 하나 소개하도록 할텐데요. 나오기 쉽지 않은 상황이었어도 나올 수 있구나하는 생각이 들어 방심하면 안되겠다는 생각이 든 케이습니다. 우선 BLE 연결을 통해 iOS 앱과 연동을 하는 과정에서 문제가 발생하였습니다. 상황은 이렇습니다. 디바이스와 모바일 앱이 연결을 하고 미리 정의해 놓은 프로토콜에 따라 요청과 응답을 받는 구조로 UART 방식으로 프로토콜을 자체 설계해서 한번에 20바이트 미만으로 하는데, 연결을 끊고, 해제하고, 거리가 멀어져서 자동해제되고 함에 따라 디바이스가 Deadlock에 걸렸던 것인데요. 안드로이드 앱과는 나왔던 현상이 아니라 앱에서 무엇인가 처리에 문제가 있을것 이라는 추측을 하게 되었습니다. 다만 이 과정에서도 그럴지라도 디바이스는 Deadlock에 걸려있으면 안될 것이구요. 우선 문제는 크게 2가지로 정의했습니다.

  • 왜 Deadlock이 걸리는가?
  • Deadlock에 걸리면 어떻게 탈출하는게 적당할까?

 

왜 DeadLock이 걸리는가?

DeadLock이 걸리는 이유는 공유자원을 엑세스하려다 보니 발생하는 교착상태라고 설명드렸었는데요, 연결을 끊고, 해제하고 하는 과정에서 어떤 자원의 동시접근으로 교착상태에 빠진것으로 생각되었습니다. 우선 BLE를 통해 연결되는 과정은 단순히 연결하고 땡 하는것이 아니라, Connecting-Establashing-Establashed-Connected 와 같은 세부적인 연결상태를 지닙니다. 그러다보니, Establashing 과정에서 먼가를 요청했다면 아직 연결이 완성되기 전이라 문제가 일어 날 수 있겠죠? 

결론적으로 제가 세팅한 ble 스택의 초기화 과정 중 하나로 MTU 23 byte로 세팅하는데 그이유는 다음과 같습니다.  MTU는 Maximum Transmission Unit으로, BLE 를 통해 전송을 어느정도 가능한지 정하는 것입니다. maximum 보통 254byte를 하는 경우가 많은데 이렇게 크게 보낼 수도 있는거였는지 보면, BLE에서도 한번 보내는 패킷을 중간에 끊지않고 업데이트하여 한방에 다량의 데이터를 보낼 수 도 있습니다. 다만 장단점이 있는데 BLE 자체는 프로토콜의 설계 원리가 간단한 데이터를 주고 받는 것으로 출발해서 워치와 무선 이어폰의 경우 별도 프로파일을 만들어 사용하는게 그 이유이구요. 그래서 동영상같은 데이터를 보낼때는 wifi direct를 주로 쓰고 있습니다. 그래서 20 Byte 정도로 하는 이유가 한번에 심플하게 보내는 사이즈로 보면 되고, mtu가 23 인 것은 내부에서는 20byte만 쓰나 ble 자체적으로 opcode나 다른 필수적인 데이터가 붙어서 23byte로 표기되는 것입니다. 다만 이 과정에서 Chracteristic  까지 완전히 세팅된 것이 아니므로 요청하게 되면 ble stack 내부에서 crash가 발생하게 되었던 것이죠.

Deadlock에 걸리면 어떻게 탈출하는게 적당할까?

이제 본론으로 돌아와서, 이런 DeadLock 상황은 어떻게 탈출하는게 현 시점에서 가장 적절할까요? 여러가지 방법들이 있지만, 저는 아무래도 고스펙이 칩이 아니고, 사용성 자체도 DeadLock 발생시 터지는 제품은 아니지만, 배터리가 있고 LED가 있기 때문에 열을 오랫동안 발생시키지 않도록 타임아웃 동안 특정 Task가 돌지 않으면 Watch Dog Reset이 걸리는 구조로 설계했습니다. 리셋이 되면 문제가 되는 쓰레드들도 자연스럽게 종료가 되고 다시 정상적으로 실행될 수 있기 때문인데요. 가장 최소의 비용으로 처리하고자 했습니다.

그럼 nRF에서 WatchDog을 어떻게 사용하는지 알아봐야겠지요? nRF에서는 아래처림 SDK에서 지원하고 있고 이에 따라 샘플도 존재합니다. 샘플의 위치는 samples/peripheral/wdt 가 되겠네요. 주요 코드는 아래와 같습니다.

int main(void)
{
    uint32_t err_code = NRF_SUCCESS;

    //BSP configuration for button support: button pushing will feed the dog.
    err_code = nrf_drv_clock_init();
    APP_ERROR_CHECK(err_code);
    nrf_drv_clock_lfclk_request(NULL);

    err_code = app_timer_init();
    APP_ERROR_CHECK(err_code);

    err_code = bsp_init(BSP_INIT_BUTTONS, bsp_event_callback);
    APP_ERROR_CHECK(err_code);

    //Configure all LEDs on board.
    bsp_board_init(BSP_INIT_LEDS);

    //Configure WDT.
    nrf_drv_wdt_config_t config = NRF_DRV_WDT_DEAFULT_CONFIG;
    err_code = nrf_drv_wdt_init(&config, wdt_event_handler);
    APP_ERROR_CHECK(err_code);
    err_code = nrf_drv_wdt_channel_alloc(&m_channel_id);
    APP_ERROR_CHECK(err_code);
    nrf_drv_wdt_enable();

    //Indicate program start on LEDs.
    for (uint32_t i = 0; i < LEDS_NUMBER; i++)
    {   nrf_delay_ms(200);
        bsp_board_led_on(i);
    }
     err_code = bsp_buttons_enable();
     APP_ERROR_CHECK(err_code);

    while (1)
    {
        __SEV();
        __WFE();
        __WFE();
    }
}

 핵심은 WDT를 쵝화 하는 부분인데, LED가 차례로 켜지는 과정에서 2초 안에 BSP_EVENT_KEY_0 버튼 누르지 않으면 Reset 되게 됩니다. 

//Configure WDT.
    nrf_drv_wdt_config_t config = NRF_DRV_WDT_DEAFULT_CONFIG;
    err_code = nrf_drv_wdt_init(&config, wdt_event_handler);
    APP_ERROR_CHECK(err_code);
    err_code = nrf_drv_wdt_channel_alloc(&m_channel_id);
    APP_ERROR_CHECK(err_code);
    nrf_drv_wdt_enable();
define FEED_BUTTON_ID          0                           /**< Button for feeding the dog. */

nrf_drv_wdt_channel_id m_channel_id;

void wdt_event_handler(void)
{
    bsp_board_leds_off();

    //NOTE: The max amount of time we can spend in WDT interrupt is two cycles of 32768[Hz] clock - after that, reset occurs
}


void app_error_fault_handler(uint32_t id, uint32_t pc, uint32_t info)
{
    bsp_board_leds_off();
    while (1);
}


void bsp_event_callback(bsp_event_t event)
{
    switch (event)
    {
        case BSP_EVENT_KEY_0:
            nrf_drv_wdt_channel_feed(m_channel_id);
            break;

        default :
            //Do nothing.
            break;
    }
}

사실 명확하게 2초라는 계산이 app_timer_init 이라는 별도 예제속의 라이브러리에 포함되는 범주인데, 왠지 WatchDog을 제대로 모르면서 쓰는 기분이 듭니다. 조금더 Low하지만 직접 제어하면서 자신의 Task안에 접목하도록 아래와 같이 헤보도록 하겠습니다. 우선 공식 문서의 WDT 관련된 설명은 아래와 같이 참고해주시고 중앙에 Timeout Period 조금더 자세히 살펴보시면 됩니다.

https://infocenter.nordicsemi.com/index.jsp?topic=%2Fcom.nordic.infocenter.nrf52832.ps.v1.1%2Fwdt.html 

 

Nordic Semiconductor Infocenter

 

infocenter.nordicsemi.com

기본적으로 초기화를 하고 초기화시에 타임아웃 시간을 세팅하며, 그 시간안에 특정 레지스터를 업데이트 하지 않으면 WatchDog Reset이 걸리는 구조라 위에서 SDK를 이용할 때 말한 설명과 별반 다르지 않지만 실제 구현하면 아래와 같습니다.

void watchdog_init(uint32_t time_sec)
{
    NRF_WDT->TASKS_START = 0;
    NRF_WDT->CONFIG = ((WDT_CONFIG_HALT_Pause << WDT_CONFIG_HALT_Pos) | (WDT_CONFIG_SLEEP_Run << WDT_CONFIG_SLEEP_Pos));
    NRF_WDT->CRV = (time_sec * 32768);
    NRF_WDT->RREN |= WDT_RREN_RR0_Msk;      // Enable Reload Register 0
    NRF_WDT->RR[0] = WDT_RR_RR_Reload;      // Reload The Watchdog Counter  
    NRF_WDT->TASKS_START = 1;

    printf("[%s] CRV = %ld\r\n", _FN_, NRF_WDT->CRV);
}

void watchdog_update(void)
{
    NRF_WDT->RR[0] = WDT_RR_RR_Reload;  // Reload The Watchdog Counter
}

제일 첫 번째 watchdog_init 이라는 인터페이스에 만약 3을 입력변수로 받는 다면 NRF_WDT->CRV 에 3 * 32768 이 입력되겠지요? 공식문서에서 보았던 time_out = (crv - 1) / 32768 과 같습니다. crv에 넣어야할 값을 역으로 구할려면 timeout 시간에 32768을 곱해주어야 하겠지요? 나머지 세팅값은 레지스터 세팅의 조금 디테일한 내용이니 똑같이 따라하시고 이후 watdog_update를 타임아웃전에 업데이트 해준다면 watch dog reset이 걸릴 일이 없습니다. 저의 경운 watchdog_init은 main에서 제일 처음 초기화 할때 부르고 system_task를 생성해서 해당 태스크는 배터리 용량 체크 등 시스템을 모니터링 하는 역할을 하는 곳을 만들어 watchdog_update를 1초마다 콜 될 수 있도록 만들었고 만약 system_task가 돌지 못하는 상황이라면 그 상황은 DeadLock 상태로 정의하였습니다. 최소한의 비용으로 비정상적인 상황을 해결하기 적합한 방법이라고 판단 되었고, freeRTOS를 포팅하였다보니 별도 task를 만들어 관리할 수 있게 하는 점도 이상적이었습니다.

지금까지 DeadLock과 WatchDog 에 대해서 살펴보았는데요, 여러분의 시스템이 안정화되게 만드는 것이 가장 중요한 점이지만 예상치 못한 문제로 DeadLock에 빠지는 상황을 극복할 수 있는 방법을 마련하는 것은 상품성 있는 Product을 만드는데 있어서 가장 필수 적이라고 할 수 있습니다. 

반응형
댓글