임베디드 학습 노트 목차

신뢰성 — 현장에서 멈추지 않게

책상 위에서는 완벽히 도는 펌웨어가 현장에서 일주일에 한 번 멈춘다. 원인은 대개 코드 논리가 아니라 전기적 방해, 전원 이상, 그리고 예상 못 한 입력이다.

신뢰성 설계는 "고장이 안 나게" 가 아니라 "고장 나도 회복하게" 만드는 일이다.


워치독 — 마지막 방어선

IWDG->KR  = 0x5555;              // 쓰기 허용
IWDG->PR  = 4;                   // 분주
IWDG->RLR = 1250;                // 약 1초
IWDG->KR  = 0xCCCC;              // 시작 — 한 번 켜면 끌 수 없다

독립 워치독(IWDG)은 별도 저속 클럭으로 돈다. 메인 클럭이 죽어도 살아 있어, 클럭 고장까지 잡을 수 있다.

어디서 먹이느냐가 설계다

// ✗ 타이머 ISR 에서 무조건 — 메인 루프가 멈춰도 계속 살아 있다
void TIM2_IRQHandler(void) { IWDG->KR = 0xAAAA; }

// ✓ 모든 작업이 돌았음을 확인한 뒤에만
static uint32_t alive_mask;
#define TASK_ALIVE(bit) (alive_mask |= (1u << (bit)))

void main_loop(void) {
    task_a();  TASK_ALIVE(0);
    task_b();  TASK_ALIVE(1);
    task_c();  TASK_ALIVE(2);

    if (alive_mask == 0b111) {           // 셋 다 돌았다
        alive_mask = 0;
        IWDG->KR = 0xAAAA;
    }
}

워치독이 확인해야 할 것은 "인터럽트가 돌고 있다" 가 아니라 "시스템이 일을 하고 있다" 이다. 첫 번째 방식은 멈춘 시스템을 영원히 살려 둔다.

왜 리셋됐는지 남긴다

void check_reset_cause(void) {
    uint32_t csr = RCC->CSR;
    if (csr & RCC_CSR_IWDGRSTF) { reset_log.wdt++;   }   // 워치독
    if (csr & RCC_CSR_BORRSTF)  { reset_log.brown++; }   // 전압 강하
    if (csr & RCC_CSR_PINRSTF)  { reset_log.pin++;   }   // 리셋 핀
    if (csr & RCC_CSR_SFTRSTF)  { reset_log.soft++;  }   // 소프트웨어
    RCC->CSR |= RCC_CSR_RMVF;                             // 플래그 지우기
}

이 카운터가 현장 진단의 출발점이다. 워치독 리셋이 쌓이면 소프트웨어가 멈추는 것이고, 브라운아웃이 쌓이면 전원 문제다 — 완전히 다른 방향으로 조사해야 한다.

전원 이상 — 브라운아웃

// 전압이 기준 아래로 내려가면 리셋시킨다
PWR->CR |= PWR_CR_PLS_LEV7 | PWR_CR_PVDE;      // 전압 감지기
// 또는 옵션 바이트로 BOR 레벨 설정

전압이 애매하게 낮은 상태가 가장 위험하다. CPU 는 도는데 Flash 쓰기가 실패하거나 명령이 잘못 해석돼, 예측 불가능한 동작을 한다. 차라리 리셋되는 편이 안전하다.

// Flash 쓰기 중 전원이 나가면 절반만 쓰인다 — 이중화로 막는다
typedef struct { uint32_t magic, seq, data[16], crc; } Record;

void save_config(const Config *c) {
    Record r = { MAGIC, ++current_seq, ... };
    r.crc = crc32(&r, sizeof r - 4);

    // 두 슬롯을 번갈아 쓴다 — 하나가 깨져도 다른 하나가 남는다
    flash_write(slot_to_write(), &r, sizeof r);
}

const Record *load_config(void) {
    const Record *a = SLOT_A, *b = SLOT_B;
    bool ok_a = a->magic == MAGIC && crc32(a, sizeof(*a)-4) == a->crc;
    bool ok_b = b->magic == MAGIC && crc32(b, sizeof(*b)-4) == b->crc;

    if (ok_a && ok_b) return (int32_t)(a->seq - b->seq) > 0 ? a : b;  // 최신
    if (ok_a) return a;
    if (ok_b) return b;
    return &defaults;                          // 둘 다 깨졌으면 기본값
}

"쓰는 중에 전원이 나가도 이전 값은 살아 있다" 가 핵심이다. 순번(seq)과 CRC 로 어느 쪽이 유효하고 최신인지 판단한다. 이 패턴 없이 단일 슬롯에 쓰면, 하필 그 순간 전원이 나갔을 때 설정이 통째로 날아간다.

펌웨어 업데이트를 안전하게

[부트로더 32KB] [슬롯 A 240KB] [슬롯 B 240KB]
       │              ↑              ↑
       └── 어느 쪽이 유효한지 확인해 부팅한다
// 부트로더
void boot(void) {
    Header *a = SLOT_A_HDR, *b = SLOT_B_HDR;

    if (b->magic == MAGIC && crc_ok(b) && b->version > a->version) {
        if (b->trial_count < 3) {              // 새 펌웨어를 시험 부팅
            b->trial_count++;
            jump_to(SLOT_B);
        }
        // 3번 시도해도 못 올라왔으면 이전으로 되돌린다
    }
    jump_to(SLOT_A);
}

// 애플리케이션이 정상 기동하면 스스로 확정한다
void confirm_boot(void) {
    if (running_from_b) { b_hdr->trial_count = 0; b_hdr->confirmed = 1; }
}

"부팅에 성공해야 확정" 이 A/B 업데이트의 핵심이다. 새 펌웨어가 부팅 자체에 실패하면 자동으로 이전 것으로 돌아간다. 이것이 없으면 원격 업데이트 실패가 곧 벽돌(brick)이다.

EMC — 전자기 간섭

방출(emission)      우리가 남을 방해한다   → 인증 시험에서 떨어진다
내성(immunity)      남이 우리를 방해한다   → 현장에서 오작동한다

소프트웨어로 할 수 있는 것

// ① 슬루율을 낮춘다 — 가장 쉽고 효과가 크다
GPIOA->OSPEEDR &= ~(0b11 << (5*2));          // 최저 속도

급격한 엣지가 곧 고주파 성분이다. LED·릴레이처럼 느려도 되는 핀을 최고 속도로 두면 불필요한 방출이 생긴다. 앞의 GPIO 편에서 본 그 이야기다.

// ② 스펙트럼 확산 — 클럭 주파수를 미세하게 흔들어 피크를 낮춘다
RCC->SSCGR = RCC_SSCGR_SSCGEN | (modperiod << 0) | (incstep << 13);

에너지 총량은 같지만 한 주파수에 몰리지 않아 피크가 낮아진다. 인증 시험은 피크를 보므로 이것만으로 통과하는 경우가 있다.

// ③ 중요한 입력은 여러 번 확인한다
bool read_critical_input(void) {
    int high = 0;
    for (int i = 0; i < 5; i++) { if (gpio_read()) high++; delay_us(10); }
    return high >= 3;                          // 다수결
}
// ④ 통신에 CRC 를 반드시 넣는다 — UART 는 오류 검출이 약하다
typedef struct { uint8_t cmd, len, data[64]; uint16_t crc; } Packet;
if (crc16(&p, p.len + 2) != p.crc) { rx_error++; return; }

패리티만으로는 부족하다. 짝수 개 비트가 뒤집히면 통과한다. CRC 는 그런 오류까지 잡는다.

하드웨어가 더 중요하다

디커플링 커패시터   전원 핀마다 100nF 를 최대한 가깝게
접지면              연속된 접지 평면 — 리턴 경로가 짧아야 한다
페라이트 비드       케이블로 나가는 고주파를 막는다
TVS 다이오드        정전기(ESD)로부터 핀을 보호
차동 신호           CAN·RS-485 — 잡음이 상쇄된다

소프트웨어로 못 고치는 문제가 훨씬 많다. EMC 시험에서 떨어지면 대개 기판 설계를 손봐야 한다. 다만 슬루율·스펙트럼 확산·필터링은 코드 몇 줄이라 먼저 시도할 값어치가 있다.

메모리 손상에 대비한다

// 중요한 변수는 이중화 + 반전 저장
typedef struct { uint32_t v, inv; } SafeU32;

void safe_set(SafeU32 *s, uint32_t v) { s->v = v; s->inv = ~v; }
bool safe_get(const SafeU32 *s, uint32_t *out) {
    if (s->v != ~s->inv) return false;         // 손상 감지
    *out = s->v; return true;
}
// Flash 무결성을 주기적으로 확인한다
bool verify_flash(void) {
    uint32_t crc = crc32((void*)APP_START, app_size);
    return crc == stored_crc;
}

우주선이나 의료기기 이야기가 아니다. 강한 전자기장이나 방사선이 없어도, 불안정한 전원과 노후 Flash 로 비트가 뒤집히는 일이 실제로 있다. 안전 관련 제품에서는 이런 확인이 규격 요구사항이기도 하다.

안전한 기본 상태

// 통신이 끊기면 어떻게 할 것인가 — 반드시 정해 둔다
void check_comm_timeout(void) {
    if ((int32_t)(tick_ms - last_rx) > 1000) {
        motor_stop();                          // ✓ 안전한 쪽으로
        set_state(STATE_SAFE);
        // ✗ 마지막 명령을 계속 유지하면 위험하다
    }
}

"명령이 안 오면 마지막 값을 유지" 가 가장 위험한 기본값이다. 통신이 끊긴 채 모터가 계속 도는 상황이 여기서 나온다. 안전한 상태(정지·차단)로 가는 것이 원칙이다.

// 센서 값도 범위를 확인한다
if (temp < -40 || temp > 125) {                // 물리적으로 불가능한 값
    sensor_fault++;
    use_last_valid_or_safe_default();
}

진단 정보를 남긴다

__attribute__((section(".noinit"))) struct {
    uint32_t magic;
    uint32_t boot_count, wdt_reset, brown_reset;
    uint32_t last_fault_pc, last_fault_cfsr;
    uint32_t comm_error, sensor_fault, queue_overflow;
    uint32_t uptime_max_s;
} health;

현장에서 "가끔 이상하다" 는 신고가 오면 이 숫자가 유일한 단서다.

wdt_reset 이 오른다        → 소프트웨어가 어딘가에서 멈춘다
brown_reset 이 오른다      → 전원 설계 문제
comm_error 만 오른다       → 배선·EMC
sensor_fault 만 오른다      → 그 센서 불량
아무것도 안 오르는데 이상   → 논리 버그. 이벤트 로그가 필요하다

분류가 되면 조사 방향이 정해진다. 이 카운터 없이 "가끔 멈춘다" 만으로는 어디서부터 봐야 할지 알 수 없다.

출시 전 점검

□ 워치독이 켜져 있고, 모든 작업 확인 후에만 먹인다
□ 디버그 비트를 껐다 (대기 전류)
□ 세미호스팅·개발용 printf 를 제거했다
□ 리셋 원인 카운터가 동작한다
□ 설정 저장이 이중화돼 있다
□ 통신 타임아웃 시 안전 상태로 간다
□ 센서 값 범위 검사가 있다
□ 펌웨어 업데이트가 실패해도 되돌아온다
□ 온도 양극단에서 시험했다
□ 전원을 반복해 껐다 켜도 정상 부팅한다 (100회)
□ 통신선을 뽑았다 꽂아도 복구한다
□ 장시간(72시간+) 연속 운전에서 메모리·카운터가 정상이다

마지막 셋이 특히 중요하다. 책상에서는 하지 않는 시험이고, 현장 고장의 대부분이 여기서 잡힌다.


한눈에 정리

  • 신뢰성은 "고장이 안 나게" 가 아니라 "고장 나도 회복하게" 만드는 일이다
  • 워치독은 "일을 하고 있음" 을 확인한 뒤 먹인다ISR 에서 무조건 먹이면 멈춘 시스템을 살려 둔다
  • 리셋 원인 카운터가 현장 진단의 출발점 — 워치독·브라운아웃·핀 리셋은 조사 방향이 다르다
  • 애매하게 낮은 전압이 가장 위험하다 — 차라리 리셋되는 편이 안전하다
  • 설정 저장은 이중 슬롯 + 순번 + CRC — 쓰는 중 전원이 나가도 이전 값이 산다
  • A/B 업데이트는 "부팅 성공해야 확정" — 없으면 원격 업데이트 실패가 곧 벽돌
  • 슬루율 낮추기와 스펙트럼 확산은 코드 몇 줄로 EMC 를 개선한다. 다만 하드웨어가 더 중요하다
  • UART 에 CRC 는 필수패리티는 짝수 비트 오류를 못 잡는다
  • "명령이 안 오면 마지막 값 유지" 가 가장 위험한 기본값안전 상태로 간다
  • 건강 카운터를 .noinit 에 남긴다 — 분류가 되면 조사 방향이 정해진다
  • 전원 반복·통신 단절·72시간 연속 — 책상에서 안 하는 이 세 시험이 현장 고장을 잡는다

꼬리질문 대비

  • "워치독을 어디서 먹여야 하나?" → 타이머 ISR 이 아니라 모든 작업이 돌았음을 확인한 뒤 — ISR 에서 먹이면 메인이 멈춰도 살아 있다
  • "독립 워치독(IWDG)이 별도 클럭을 쓰는 이유는?" → 메인 클럭이 죽어도 동작해야 클럭 고장까지 잡을 수 있다
  • "브라운아웃이 위험한 이유는?" → 전압이 애매하게 낮으면 CPU 가 오동작하면서 Flash 에 쓰기까지 할 수 있다 — BOR 로 아예 리셋시킨다
  • "현장에서 터진 원인을 어떻게 남기나?" → 폴트 핸들러에서 레지스터·PC 를 비휘발 영역에 기록하고 리셋 — 다음 부팅에 읽어 보고

출처 — STMicroelectronics, RM0090 STM32F4 Reference Manual §21 IWDG·§5 PWR(PVD·BOR)·§6 RCC(CSR 리셋 플래그) · STMicroelectronics, AN4435 STM32 EMC 설계 지침 · AN2606 부트로더 · IEC 61000-4-2/4-4(ESD·EFT 내성 시험) · IEC 60730-1 Annex H(가전 안전 — 자가 진단 요구)

저전력 설계 — 배터리로 몇 년을 버티기