Devin.KR

신뢰성 - 워치독·결함 처리

개발자KR 조회 4

이 장에서 배우는 것

앞 장에서 메모리 사용량을 예측할 수 있도록 만들었다. 그러나 메모리 사용이 일정한 프로그램도 멈출 수 있다. 센서 처리가 진행되지 않거나, 전원 전압이 내려가거나, 재시작 직후 출력이 예상과 다르게 켜지는 문제가 남는다. 신뢰성을 높이려면 고장을 발견하는 방법과 발견한 뒤의 동작을 함께 설계해야 한다.

이 장에서는 컨베이어의 센서 처리와 모터 제어가 실제로 진행했는지 확인한 뒤 워치독(watchdog)을 갱신한다. 전압 저하에 따른 브라운아웃(brownout)을 별도로 다루고, 소프트웨어 정지와 전원 불안정 모두에서 같은 안전 출력으로 돌아오도록 만든다. 예제는 시간을 직접 전진시키는 PC 시뮬레이션이므로 매번 같은 결과를 얻는다.

  • 주기적으로 실행됐다는 사실과 필요한 작업을 완료했다는 사실을 구분한다.
  • 여러 작업의 진행 보고를 모아 워치독을 갱신하는 조건을 작성한다.
  • 브라운아웃 검출과 전원 회복 후 재시작 조건을 구분한다.
  • 결함 발견, 안전 출력, 워치독 재설정의 순서를 코드와 출력으로 확인한다.

문제 상황

컨베이어에는 물체 감지 센서와 구동 모터, 전원이 끊기면 체결되는 브레이크가 있다. 센서 작업은 입력을 읽고 유효성을 확인한다. 제어 작업은 운전 요청과 센서 결과를 이용해 모터 허가 출력을 갱신한다. 두 작업은 각각 20밀리초마다 실행된다.

현장에서 제어 작업의 오류 경로가 계속 반복되는 문제가 발생했다고 가정한다. 스케줄러와 센서 작업은 계속 실행되지만 제어 작업은 출력을 계산하는 지점에 도달하지 못한다. 그런데 타이머 인터럽트가 워치독을 갱신하고 있다면 MCU는 재설정되지 않는다. 모터 허가 출력은 마지막 값을 유지할 수 있다. 워치독이 켜져 있다는 사실만으로 필요한 기능의 진행을 보장할 수 없는 사례다.

다른 날에는 모터 기동 순간 공급 전압이 내려간다. 프로그램의 잘못된 분기를 찾는 것만으로는 이 문제를 설명할 수 없다. 전압이 동작 범위를 벗어나면 CPU 실행, 주변장치 동작, 메모리 쓰기를 정상이라고 가정하기 어렵다. 이때는 전압 감시와 출력 회로가 함께 대응해야 한다.

이 장의 장치에서는 안전 상태를 모터 허가 해제와 브레이크 해제 신호 차단으로 정의한다. 실제 설비에서는 갑작스러운 제동이 적재물을 떨어뜨릴 수도 있으므로 같은 정의를 그대로 적용하지 않는다. 예제의 정의는 정지 시 체결되는 브레이크를 사용하는 소형 수평 컨베이어를 전제로 한다.

진행을 확인한 뒤 워치독을 갱신한다

워치독은 정해진 시간 안에 갱신되지 않으면 재설정 등의 동작을 일으키는 감시 장치다. 여기서는 마지막 갱신 이후 100밀리초가 지나면 MCU가 재설정되는 것으로 모델링한다. 실제 MCU에서는 워치독의 클록 공급원, 저전력 상태에서의 동작, 재설정 범위가 제품마다 다르다.

갱신을 담당하는 감독 작업은 센서와 제어 작업의 진행 보고를 확인한다. 각 작업은 진입하자마자 보고하지 않는다. 센서 작업은 입력 검사 뒤에, 제어 작업은 출력 계산과 반영 뒤에 보고한다. 보고의 의미를 이렇게 정해야 작업이 호출됐지만 필요한 처리를 끝내지 못한 경우를 구별할 수 있다.

예제는 두 보고를 비트로 모은다. 감독 작업은 40밀리초마다 두 비트가 모두 있는지 검사하고, 검사한 보고는 지운다. 보고를 지우지 않으면 시작 직후의 성공 기록이 이후의 정지를 가린다. 결함을 발견하면 결함 표시를 유지하고 워치독 갱신을 중단한다. 다른 작업이 나중에 보고하더라도 자동으로 갱신을 재개하지 않는다.

필수 작업의 완료 보고가 모두 모였을 때만 감독 작업이 워치독을 갱신한다

감독 작업의 첫 검사는 부팅 후 30밀리초에 수행한다. 이후 검사 시점은 70, 110, 150밀리초다. 센서와 제어 작업은 부팅 직후부터 20밀리초 간격으로 실행되므로 첫 검사 전에 보고할 기회가 있다. 초기화가 더 오래 걸리는 제품에서는 준비 완료 조건과 시작 제한 시간을 따로 정해야 한다. 준비가 안 됐다는 이유로 무기한 갱신하는 방식은 피한다.

예제에서 시간과 진행 보고가 뜻하는 것
항목설정설계 의미
센서·제어 주기20밀리초검사 구간마다 완료 보고를 만든다
감독 주기40밀리초필수 보고가 모두 있는지 검사한다
첫 감독 시점부팅 후 30밀리초초기 작업의 실행 기회를 확보한다
워치독 제한100밀리초마지막 정상 갱신부터 재설정까지의 시간이다

이 방식은 각 작업이 검사 구간에 한 번 이상 완료했는지를 확인한다. 20밀리초 주기의 모든 실행이 제시간에 끝났음을 증명하지는 않는다. 한 번 보고한 직후 작업이 멈추면 다음 검사는 통과하고 그다음 검사에서 누락이 드러날 수도 있다. 더 촘촘한 검출이 필요하면 마지막 완료 시각이나 완료 횟수를 검사해야 한다.

워치독 제한 시간을 정할 때는 감독 주기보다 크다는 조건만 확인하지 않는다. 정상 상태에서 가능한 감독 지연과 워치독 클록 오차를 포함해야 한다. 동시에 그 제한 시간이 공정에서 허용하는 무응답 시간보다 길어서는 안 된다. 두 조건을 함께 만족시키기 어렵다면 주기나 정지 회로를 다시 설계해야 한다.

브라운아웃은 전압 조건으로 다룬다

브라운아웃 재설정 회로는 공급 전압이 설정한 기준 아래로 내려가면 MCU를 재설정 상태로 만든다. 워치독이 작업 진행을 시간으로 감시한다면, 이 회로는 전압으로 실행 가능 조건을 감시한다. 전압이 불안정한 동안 소프트웨어가 정상 실행될 것이라고 기대하고 처리 루틴을 호출하는 구조와는 다르다.

전압 감시 인터럽트가 있더라도 저장 작업을 끝낼 시간이 항상 남는 것은 아니다. 전압 하강 속도, 저장 소자의 쓰기 시간, 남은 에너지에 따라 결과가 달라진다. 예제에서는 저전압이 발생하면 기록을 저장하려고 시도하지 않고, 하드웨어에 의해 출력이 안전해지고 CPU 실행이 중단된 것으로 모델링한다.

실제 보드에서는 검출 전압과 허용 오차, MCU 주파수별 최소 동작 전압, 재설정 해제 조건을 확인한다. 전압이 경계 부근을 오가며 반복 재설정되는 현상을 줄이려면 검출 회로의 히스테리시스와 전원 안정화 시간을 살핀다. MCU 전압이 회복됐다고 모터 드라이버와 센서까지 준비됐다고 볼 수도 없다.

PC에서는 실제 전압을 낮추지 않는다. 시뮬레이션 시각 210밀리초에 저전압 사건을 주입하고, 240밀리초까지 애플리케이션을 실행하지 않는다. 이 시간 동안 출력이 꺼지는 동작은 CPU가 실행한 안전 함수의 효과가 아니라, 회로가 보장해야 할 동작을 모형으로 표현한 것이다.

안전 출력과 재가동 조건을 나눈다

워치독 만료를 기다리는 동안에도 컨베이어가 계속 움직여도 되는 것은 아니다. 감독 작업이 살아 있고 보고 누락을 발견했다면 먼저 안전 출력을 적용한다. 그다음 워치독 갱신을 중단해 재설정을 유도한다. 안전 출력은 물리적 동작을 제한하고, 재설정은 소프트웨어를 초기 상태로 되돌리는 서로 다른 조치다.

반대로 CPU 전체가 멈췄다면 안전 함수를 실행할 수 없다. 워치독 재설정 중의 핀 상태, 외부 풀다운, 드라이버의 허가 입력, 브레이크 회로가 정지 동작을 책임져야 한다. MCU 재설정이 실제 모터 정지로 이어지는지 보드에서 확인해야 한다.

예제의 출력은 모두 활성 수준이 높다. motor_enable이 참이면 모터를 허가하고, brake_release가 참이면 브레이크를 푼다. 두 값을 거짓으로 만드는 것이 안전 출력이다. 실제 드라이버에서는 허가 해제와 브레이크 체결 사이에 필요한 순서나 지연이 있을 수 있으며, 그 조건은 장치 규격에 맞춰 구현한다.

재설정 뒤에는 안전 출력을 유지하며 새로운 운전 명령을 기다린다

재설정 뒤에는 이전 운전 요청을 복원하지 않는다. 입력을 다시 확인한 뒤 새로운 운전 명령이 들어와야 움직인다. 실제 버튼을 사용하는 경우에는 부팅할 때 이미 눌린 버튼을 새 명령으로 받아들이지 않도록 해제 후 재입력 등의 조건을 정한다. 예제에서는 20밀리초에 한 번만 발생하는 명령 사건으로 이를 단순화한다.

PC 예제의 역할을 FreeRTOS에 옮길 때의 대응
예제 요소FreeRTOS 대응옮길 때 확인할 점
주기 작업 호출vTaskDelayUntil()기준 시각을 유지하되 지연 가능성을 포함한다
완료 보고 비트xEventGroupSetBits()각 작업의 유효한 완료 지점에서 보고한다
보고 수집과 소비xEventGroupWaitBits()전체 비트 조건과 소비 시점을 일관되게 정한다
워치독 갱신·전압 재설정MCU 제조사 HAL 또는 레지스터FreeRTOS 공통 API가 제공하는 기능은 아니다

표는 역할의 대응이다. PC의 비트 읽기와 지우기를 선점 가능한 태스크에 그대로 복사하면 그 사이의 보고가 사라질 수 있다. 전체 보고를 기다리고 소비하는 정책으로 옮긴다면 이벤트 그룹의 대기 조건과 비트 해제 옵션을 함께 사용한다. 검사 구간의 시간 기준도 새 실행 환경에 맞춰 정해야 한다.

API의 동작 확인에는 FreeRTOS 주기 지연 API 문서와 이벤트 그룹 대기 API 문서를 참고할 수 있다. 실제 워치독과 전압 검출 조건은 사용하는 MCU의 데이터시트와 참조 설명서에서 확인한다.

완성 코드

다음 프로그램을 watchdog_sim.c로 저장한다. 하나의 파일 안에 hal_sim 역할의 함수와 협동형 스케줄러를 둔다. 운영체제의 실제 시간이나 스레드 실행 순서에 의존하지 않으므로 macOS와 Linux에서 같은 출력이 나온다. 지정한 빌드 옵션의 -pthread는 사용하지만 프로그램 자체는 단일 스레드다.

80밀리초부터 제어 작업의 완료를 막는다. 이 결함은 함수가 반환은 하지만 유효한 작업을 완료하지 못하는 상황이다. 실제 무한 반복을 넣지 않는 이유는 PC에서 MCU 내부 프로그램과 독립적인 워치독 시간을 한 스레드로 모사하기 위해서다. 하드웨어 모델을 먼저 진행하고 그 뒤에 애플리케이션을 호출한다.

#include <stdbool.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdio.h>

/* [1] Time units are milliseconds. */
enum {
    STEP_MS = 10,
    TASK_MS = 20,
    SUPERVISOR_MS = 40,
    FIRST_CHECK_MS = 30,
    WATCHDOG_MS = 100,
    SENSOR_BIT = 1u << 0,
    CONTROL_BIT = 1u << 1,
    REQUIRED_BITS = SENSOR_BIT | CONTROL_BIT
};

typedef enum {
    RESET_POWER,
    RESET_WATCHDOG,
    RESET_BROWNOUT
} ResetReason;

/* [2] This object belongs to the simulated hardware. */
typedef struct {
    uint32_t now;
    uint32_t last_feed;
    bool watchdog_armed;
    bool low_voltage;
    bool motor_enable;
    bool brake_release;
    unsigned watchdog_resets;
    unsigned brownouts;
} HalSim;

typedef struct {
    uint32_t boot_time;
    uint32_t last_sensor;
    uint32_t last_control;
    uint32_t last_check;
    unsigned reports;
    bool sensor_started;
    bool control_started;
    bool check_started;
    bool sensor_ok;
    bool run_requested;
    bool fault;
} App;

/* [3] Safe outputs are also the reset defaults. */
static void hal_safe(HalSim *h)
{
    h->motor_enable = false;
    h->brake_release = false;
}

static const char *reset_name(ResetReason reason)
{
    switch (reason) {
    case RESET_POWER:
        return "power";
    case RESET_WATCHDOG:
        return "watchdog";
    case RESET_BROWNOUT:
        return "brownout";
    }
    return "unknown";
}

static void boot(HalSim *h, App *a, ResetReason reason)
{
    hal_safe(h);
    *a = (App){ .boot_time = h->now };
    h->last_feed = h->now;
    h->watchdog_armed = true;
    printf("t=%03" PRIu32 " boot=%s safe; new START required\n",
           h->now, reset_name(reason));
}

static void hal_watchdog_feed(HalSim *h)
{
    h->last_feed = h->now;
    printf("t=%03" PRIu32 " watchdog feed\n", h->now);
}

/* [4] Hardware time advances before application work. */
static bool hal_step(HalSim *h, App *a, uint32_t now)
{
    h->now = now;

    if (now == 210u) {
        h->low_voltage = true;
        h->watchdog_armed = false;
        ++h->brownouts;
        hal_safe(h);
        printf("t=%03" PRIu32 " brownout; reset held; safe\n",
               h->now);
    }

    if (h->low_voltage) {
        if (now < 240u) {
            return false;
        }
        h->low_voltage = false;
        boot(h, a, RESET_BROWNOUT);
    }

    if (h->watchdog_armed &&
        (uint32_t)(now - h->last_feed) >= WATCHDOG_MS) {
        ++h->watchdog_resets;
        hal_safe(h);
        printf("t=%03" PRIu32 " watchdog reset; safe\n",
               h->now);
        boot(h, a, RESET_WATCHDOG);
    }
    return true;
}

/* [5] Report only after useful work has completed. */
static void sensor_task(HalSim *h, App *a)
{
    a->sensor_ok = !h->low_voltage;
    a->reports |= SENSOR_BIT;
}

static void control_task(HalSim *h, App *a)
{
    bool injected_stall = h->now >= 80u && h->now < 170u;

    if (injected_stall) {
        return;
    }

    if (a->run_requested && a->sensor_ok && !a->fault) {
        h->motor_enable = true;
        h->brake_release = true;
    } else {
        hal_safe(h);
    }
    a->reports |= CONTROL_BIT;
}

/* [6] A detected fault stays latched until reset. */
static void supervisor_task(HalSim *h, App *a)
{
    unsigned seen = a->reports;
    a->reports = 0u;

    if (a->fault) {
        return;
    }

    if ((seen & REQUIRED_BITS) != REQUIRED_BITS || !a->sensor_ok) {
        a->fault = true;
        a->run_requested = false;
        hal_safe(h);
        printf("t=%03" PRIu32
               " fault: progress/input check; safe; feed stopped\n",
               h->now);
        return;
    }
    hal_watchdog_feed(h);
}

/* [7] Fixed-order cooperative scheduling. */
static void scheduler_step(HalSim *h, App *a)
{
    uint32_t now = h->now;

    if (!a->sensor_started ||
        (uint32_t)(now - a->last_sensor) >= TASK_MS) {
        a->sensor_started = true;
        a->last_sensor = now;
        sensor_task(h, a);
    }

    if (now == 20u && a->sensor_ok && !a->fault) {
        a->run_requested = true;
        printf("t=%03" PRIu32 " START accepted\n", now);
    }

    if (!a->control_started ||
        (uint32_t)(now - a->last_control) >= TASK_MS) {
        a->control_started = true;
        a->last_control = now;
        control_task(h, a);
    }

    bool check_due = a->check_started
        ? (uint32_t)(now - a->last_check) >= SUPERVISOR_MS
        : (uint32_t)(now - a->boot_time) >= FIRST_CHECK_MS;

    if (check_due) {
        a->check_started = true;
        a->last_check = now;
        supervisor_task(h, a);
    }
}

/* [8] The test harness survives simulated MCU resets. */
int main(void)
{
    HalSim h = {0};
    App a = {0};

    boot(&h, &a, RESET_POWER);

    for (uint32_t now = 0u; now <= 280u; now += STEP_MS) {
        if (hal_step(&h, &a, now)) {
            scheduler_step(&h, &a);
        }
    }

    printf("t=%03" PRIu32
           " final: motor_enable=%u brake_release=%u"
           " watchdog_resets=%u brownouts=%u\n",
           h.now,
           (unsigned)h.motor_enable,
           (unsigned)h.brake_release,
           h.watchdog_resets,
           h.brownouts);
    return 0;
}

줄별 해설

[1] 시간과 보고 비트. 시간 단위는 밀리초다. STEP_MS는 시뮬레이터가 시간을 관찰하는 간격이다. 센서와 제어의 비트는 서로 다른 위치를 사용하며, REQUIRED_BITS는 감독 작업이 요구하는 보고 집합이다. 새 필수 작업을 추가하면 해당 작업의 완료 보고와 요구 비트를 함께 추가한다.

[2] 하드웨어와 애플리케이션의 수명. HalSim은 PC 시험 장치의 상태이고 App은 재설정될 MCU 프로그램의 상태다. 재설정 횟수는 시험 장치가 보관하므로 애플리케이션 초기화 뒤에도 남는다. 실제 MCU의 일반 RAM에 같은 구조체를 두면 이 보존 성질을 얻는 것은 아니다. 재설정 원인은 제조사가 제공하는 상태 레지스터의 읽기·해제 규칙에 맞춰 취득한다.

[3] 안전 출력을 먼저 적용한다. hal_safe()는 두 허가 신호를 모두 내린다. 여러 번 호출해도 결과가 같다. boot()는 이 함수를 먼저 호출하고 구조체 복합 리터럴로 애플리케이션 전체를 초기화한다. 지정하지 않은 멤버는 0 또는 거짓으로 초기화되므로 운전 요청과 완료 보고도 지워진다.

boot()의 last_feed 설정은 부팅 시점부터 워치독 제한 시간을 새로 세는 모델이다. 실제 장치에서 재설정 뒤 워치독이 계속 동작하는지, 부팅 코드가 설정을 바꿀 수 있는지는 별도 확인 대상이다. PRIu32는 uint32_t를 출력하기 위한 형식을 제공하며, 불리언 출력은 unsigned로 변환해 %u와 맞춘다.

[4] 저전압 동안에는 작업을 호출하지 않는다. 210밀리초가 되면 안전 출력을 적용하고 워치독을 비활성으로 표시한다. 이는 브라운아웃이 재설정을 유지하는 동안 워치독 사건을 따로 세지 않는 모델의 선택이다. false 반환을 받은 주 실행부는 스케줄러를 건너뛴다. 240밀리초에 회복하면 브라운아웃 원인으로 새로 부팅한다.

워치독 만료 검사는 스케줄러보다 먼저 수행한다. 제한 시간에 도달한 뒤 애플리케이션이 뒤늦게 갱신해 만료를 없애지 못하도록 순서를 정한 것이다. 이 예제에서 만료 시각은 10밀리초 격자와 일치한다. 다른 값을 사용하면 검출은 다음 관찰 시점까지 늦어질 수 있다.

(uint32_t)(now - last_feed)는 부호 없는 경과 시간 계산이다. 누적 시각을 직접 비교하는 방식보다 시각 값의 순환을 다루기 쉽다. 다만 검사를 매우 오래 중단해 한 바퀴 이상의 경과를 잃어버리는 상황까지 해결하지는 않는다. 예제의 실행 시간은 이러한 범위에 훨씬 못 미친다.

[5] 호출과 완료를 구별한다. 센서 작업의 입력 검사는 시뮬레이션을 위해 단순화했다. 실제로는 통신 오류, 입력 범위, 최신성 등의 검사가 들어갈 자리다. 제어 작업은 주입된 결함 구간에서 완료 비트를 남기지 않고 반환한다. 그 외에는 안전 조건을 확인하고 출력을 반영한 다음 보고한다. 작업이 실행됐다는 표시를 함수 첫 줄에 두면 이 시험은 결함을 발견하지 못한다.

[6] 검사한 보고를 소비한다. seen에 보고를 복사하고 즉시 지워 다음 구간을 시작한다. 모든 함수가 한 스레드에서 순서대로 실행되므로 여기에는 동시 접근이 없다. 결함이 한 번 설정되면 이후 감독 호출도 갱신하지 않는다. 보고 누락과 입력 이상을 같은 안전 처리 경로로 보내되, 제품에서는 원인을 구별해 기록하는 편이 진단에 유리하다.

[7] 같은 시각의 처리 순서를 고정한다. 센서 검사, 운전 명령 수신, 제어, 감독 순으로 실행한다. 부팅 직후에는 sensor_started와 control_started가 거짓이므로 두 작업이 바로 호출된다. 첫 감독 검사만 부팅 후 30밀리초를 기다린다. 이 스케줄러는 지연된 호출을 여러 번 따라잡지 않고 한 번만 수행하는 단순 모델이다.

[8] 실제 대기 없이 시간을 전진시킨다. 반복문이 시간을 10밀리초씩 올린다. MCU 재설정은 PC 프로세스 종료가 아니라 App의 초기화로 표현한다. 따라서 같은 시험에서 워치독 재설정과 브라운아웃을 연달아 관찰할 수 있다. 이 프로그램은 정책을 검토하는 예제이며, 실제 클록 독립성이나 전압 임계값을 측정하는 시험을 대신하지 않는다.

실행 결과

다음 명령으로 C11 프로그램을 빌드하고 실행한다.

cc -std=c11 -Wall -Wextra -pthread watchdog_sim.c -o watchdog_sim
./watchdog_sim

예상 표준 출력은 다음과 같다.

t=000 boot=power safe; new START required
t=020 START accepted
t=030 watchdog feed
t=070 watchdog feed
t=110 fault: progress/input check; safe; feed stopped
t=170 watchdog reset; safe
t=170 boot=watchdog safe; new START required
t=200 watchdog feed
t=210 brownout; reset held; safe
t=240 boot=brownout safe; new START required
t=270 watchdog feed
t=280 final: motor_enable=0 brake_release=0 watchdog_resets=1 brownouts=1

70밀리초 검사 뒤에는 보고가 비워진다. 80밀리초부터 제어가 완료되지 않으므로 110밀리초 검사에는 센서 보고만 남는다. 감독 작업은 이때 안전 출력을 적용한다. 마지막 워치독 갱신은 70밀리초였으므로 100밀리초 뒤인 170밀리초에 재설정된다. 결함 검출 시점부터 다시 100밀리초를 세는 것이 아니다.

170밀리초 부팅 뒤에는 운전 요청이 없지만 센서와 제어 작업은 정상적으로 완료된다. 따라서 200밀리초에 워치독을 갱신한다. 안전 대기 중에도 정상 진행을 감시할 수 있다는 뜻이다. 240밀리초 부팅 뒤에도 새 운전 명령은 주입되지 않으므로 마지막 출력은 모두 0이다.

실무에서 자주 틀리는 것

감독 함수가 호출됐다는 이유로 갱신한다

다음은 감독 함수 내부의 잘못된 발췌다. 검사 결과와 관계없이 갱신하면 제어 작업이 멈춘 뒤에도 워치독이 동작하지 않는다.

unsigned seen = a->reports;
a->reports = 0u;
hal_watchdog_feed(h);

if ((seen & REQUIRED_BITS) != REQUIRED_BITS) {
    hal_safe(h);
}

갱신은 필요한 조건을 모두 통과한 경로의 끝에 둔다. 결함 기록과 안전 출력을 먼저 처리하고 반환한다.

unsigned seen = a->reports;
a->reports = 0u;

if (a->fault) {
    return;
}
if ((seen & REQUIRED_BITS) != REQUIRED_BITS || !a->sensor_ok) {
    a->fault = true;
    a->run_requested = false;
    hal_safe(h);
    return;
}
hal_watchdog_feed(h);

이전 구간의 성공 보고를 계속 사용한다

다음 검사에는 보고를 소비하는 과정이 없다. 한 번 모인 두 비트가 남아 있으면 이후 작업이 멈춰도 같은 성공 기록을 반복해서 사용한다.

if (!a->fault && a->sensor_ok &&
    (a->reports & REQUIRED_BITS) == REQUIRED_BITS) {
    hal_watchdog_feed(h);
}

협동형 예제에서는 복사 후 지우는 것으로 검사 구간을 분리한다. 다음 발췌는 완료 코드의 감독 검사와 함께 사용한다. 선점 가능한 환경에서는 복사와 지우기를 각각 수행하는 방식의 경쟁 조건도 해결해야 한다.

unsigned seen = a->reports;
a->reports = 0u;

if ((seen & REQUIRED_BITS) != REQUIRED_BITS) {
    a->fault = true;
    a->run_requested = false;
    hal_safe(h);
    return;
}

재부팅을 운전 재개 명령으로 취급한다

다음 초기화는 재설정 직후 새 명령 없이 출력을 켠다. 저전압이 반복되면 짧은 운전과 재설정이 반복될 수 있다.

*a = (App){ .boot_time = h->now };
a->run_requested = true;
h->motor_enable = true;
h->brake_release = true;

부팅 경로에서는 안전 출력을 유지하고 운전 요청을 지운다. 준비 확인과 새 명령 수신은 이후의 별도 조건이다. 이 코드는 부팅 함수의 해당 부분을 보여 준다.

hal_safe(h);
*a = (App){ .boot_time = h->now };

소프트웨어 초기화가 시작되기 전에도 출력은 안전해야 한다. 핀의 재설정 기본값과 외부 저항만으로 드라이버가 어떤 상태가 되는지 확인하고, 전원이 다른 순서로 들어오는 경우도 시험한다.

한눈에 보기

결함의 종류에 따라 검출과 복구 조건을 구분한다
상황검출 수단즉시 동작재가동 조건
필수 작업의 진행 누락감독 작업의 완료 보고 검사안전 출력, 갱신 중단재설정 뒤 입력 확인과 새 명령
감독을 포함한 실행 정지독립적으로 동작하는 워치독재설정과 회로의 안전 기본값정지 원인 확인과 시작 조건 충족
공급 전압 저하브라운아웃 검출 회로재설정 유지, 출력 허가 차단전원·주변장치 안정과 새 명령
정상적인 운전 대기작업 진행과 입력 상태 검사안전 출력을 유지하며 갱신유효한 운전 명령

워치독 갱신은 프로그램이 원하는 기능을 계속 수행하고 있다는 제한된 증거다. 보고 조건이 부실하면 증거도 부실해진다. 또한 재설정 원인만으로 최초 결함의 위치를 알 수는 없다. 실제 제품에서는 가능한 범위의 진단 정보를 남기되, 진단 처리 때문에 안전 출력이 늦어지지 않도록 한다.

연습 문제

  1. 완성 코드에서 제어 작업이 80밀리초부터 완료되지 않는다. 안전 출력 적용 시각과 워치독 재설정 시각을 각각 구하고, 두 시각이 다른 이유를 설명하라.
  2. WATCHDOG_MS만 120으로 바꾸면 첫 워치독 재설정은 언제 발생하는가. 170밀리초에 제어 작업이 다시 정상 실행될 수 있는데도 재설정되는 이유를 설명하라.
  3. 센서와 제어 작업의 실행은 그대로 두고, 80밀리초부터 170밀리초 직전까지 scheduler_step() 호출 전체를 건너뛴다고 가정하라. 이때 110밀리초의 결함 메시지와 안전 출력 적용 시각은 어떻게 달라지는가.
  4. 작업이 검사 구간 시작 직후 완료 보고를 한 번 남기고 멈췄다. 비트 방식의 검출 지연이 감독 주기 하나보다 길어질 수 있는 이유와 이를 줄이는 방법을 설명하라.

정답과 해설

  1. 안전 출력은 110밀리초에 적용되고, 워치독 재설정은 170밀리초에 발생한다. 감독 작업은 70밀리초 이후 제어 보고가 없다는 사실을 110밀리초 검사에서 발견한다. 워치독은 마지막 갱신인 70밀리초부터 100밀리초를 센다. 안전 출력은 검출 즉시 적용하는 조치이며 재설정을 기다리지 않는다.
  2. 첫 재설정은 190밀리초다. 마지막 갱신 70밀리초에 제한 시간 120밀리초를 더한 값이다. 110밀리초에 설정된 결함 표시가 유지되므로 제어 작업이 나중에 완료돼도 갱신이 재개되지 않는다. 재설정 시각이 변하면 이후 작업의 위상도 새 부팅 시각을 기준으로 달라진다.
  3. 110밀리초의 결함 메시지는 나오지 않는다. 감독 작업도 호출되지 않기 때문이다. 마지막 갱신은 여전히 70밀리초이므로 하드웨어 모델은 170밀리초에 재설정하고 안전 출력을 적용한다. 그전까지 모터 허가가 유지될 수 있다. 이 지연이 공정에 허용되지 않으면 별도의 출력 차단 수단이나 더 짧은 검출 경로가 필요하다.
  4. 남아 있는 완료 비트 때문에 다음 검사는 통과한다. 그 검사가 비트를 지운 뒤 다음 구간에서 보고가 없다는 사실이 드러나므로, 위상에 따라 검출까지 거의 두 감독 주기가 걸릴 수 있다. 마지막 완료 시각과 허용 나이를 비교하거나 구간별 완료 횟수를 확인하면 더 구체적인 진행 조건을 만들 수 있다. 조건을 강화할 때는 정상적인 지연 때문에 결함을 오검출하지 않도록 허용 범위도 함께 정한다.

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.