AEGLOSLAB Arduino UNO Q 완전 가이드

제8부 실시간 시스템 응용

8.2 인터럽트·타이머와 타이밍 크리티컬 태스크 설계

8.1에서 MCU(STM32U585)가 왜 결정론적인지를 개념으로 정리했다. 이 장은 그 결정론을 실제 코드로 구현하는 도구 — 인터럽트와 타이머 — 를 Zephyr 기반 Arduino Core API로 다루는 법을 설명하고, "어떤 작업을 MCU에 놓고 어떤 작업을 MPU에 놓을지"를 판단하는 구체적 기준을 제시한다.

8.2.1 STM32U585의 인터럽트·타이머 하드웨어

Zephyr 프로젝트의 UNO Q 보드 포트 문서에 따르면 STM32U585 MCU 쪽에는 다음 페리퍼럴이 디바이스 트리로 기술되어 있다.

  • NVIC(ARMv8-M Nested Vectored Interrupt Controller) — 고정 우선순위·고정 벡터의 하드웨어 인터럽트 컨트롤러
  • 범용 타이머 11개 인스턴스(STM32 timers)
  • 저전력 타이머(LPTIM) 4개 인스턴스
  • ARMv8-M SysTick — 커널 틱(시스템 시간 기준)에 쓰이는 표준 Cortex-M 타이머
  • 외부 인터럽트 컨트롤러(EXTI 계열) — GPIO 핀 상태 변화를 인터럽트로 받는 경로
  • RCC(Reset and Clock Controller) — 클럭 트리 관리(HSE/MSI/LSE, PLL)

실제로 UNO Q의 8×13 LED 매트릭스(2.2.6)는 타이머 인터럽트로 주기적으로 리프레시되는 것으로 보인다 — 즉 "MCU의 타이머 인터럽트가 초당 수천 번 규칙적으로 어떤 작업을 수행하고 있다"는 것은 이 보드에서 현실적인 동작 방식이다. 다만 어떤 타이머 인스턴스(예: TIM17)가 실제로 이 용도에 쓰이는지, 정확히 몇 Hz로 리프레시되는지, 그리고 어떤 타이머가 사용자 스케치에서 몇 개까지 자유롭게 쓸 수 있고 어떤 것이 시스템(LED 매트릭스, SysTick 등)에 예약되어 있는지의 상세 매핑은 Arduino 공식 문서·arduino-core-zephyr 소스에서 보드별로 확인이 필요하다 — 확인 필요.

8.2.2 인터럽트 다루기 — attachInterrupt()

Arduino Core(Zephyr 기반)는 표준 Arduino API인 attachInterrupt() / detachInterrupt()를 제공한다. GPIO 핀 하나를 인터럽트 소스로 지정하고, 신호가 바뀔 때(RISING/FALLING/CHANGE) 지정한 함수(ISR)를 호출한다.

// 엔코더 A상 핀을 인터럽트로 받는 예
const uint8_t ENCODER_A_PIN = 2;
const uint8_t ENCODER_B_PIN = 3;

volatile long encoderCount = 0;

void onEncoderA() {
  // ISR 내부 — 반드시 짧게. 방향 판별만 하고 카운트만 갱신한다.
  if (digitalRead(ENCODER_B_PIN) == HIGH) {
    encoderCount++;
  } else {
    encoderCount--;
  }
}

void setup() {
  pinMode(ENCODER_A_PIN, INPUT_PULLUP);
  pinMode(ENCODER_B_PIN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(ENCODER_A_PIN), onEncoderA, RISING);
}

8.2.2.1 ISR 작성 원칙

인터럽트가 "결정론적"이려면 ISR 자체가 결정론을 훼손하지 않아야 한다. 아무리 NVIC가 정확히 제때 ISR을 호출해도, ISR 안에서 실행 시간이 불규칙한 코드를 실행하면 다른 인터럽트·태스크가 그만큼 지연된다. 원칙:

  • ISR은 짧게. 상태만 갱신하고 바로 리턴한다. 부하가 큰 연산(PID 계산, 부동소수점 필터, 문자열 처리)은 loop()나 별도 태스크로 미룬다.
  • ISR과 공유하는 변수는 volatile로 선언한다. 컴파일러가 캐시된 값을 재사용하지 않도록 강제한다(위 예제의 encoderCount).
  • ISR 안에서 delay()를 호출하지 않는다. 다른 모든 인터럽트·타이밍이 그만큼 지연된다.
  • ISR 안에서 Serial.print() 같은 블로킹 I/O를 피한다. 시리얼 버퍼가 차면 대기가 걸릴 수 있다.
  • ISR 안에서 Bridge RPC(3.4·8.3)를 호출하지 않는다. RPC는 직렬화·전송을 수반하는 상대적으로 느린 경로다 — ISR에서 호출하면 그 순간 결정론이 보장되지 않는다. RPC 호출은 항상 loop()의 일반 코드 경로에서, 그것도 주기적으로 배치해서 수행한다.
  • 여러 인터럽트가 우선순위를 다툴 수 있다. 예를 들어 엔코더 인터럽트와 타이머 인터럽트가 동시에 발생하면 NVIC 우선순위가 어느 쪽을 먼저 처리할지 정한다. 기본 우선순위로 충분한지, 명시적으로 조정해야 하는지는 애플리케이션의 데드라인 요구에 따라 다르다.

8.2.3 주기적 태스크 만들기 — 소프트 스케줄 vs 하드 스케줄

제어 루프는 대개 "정확히 매 T밀리초마다" 실행되어야 한다. 이를 만드는 방법은 크게 두 가지다.

8.2.3.1 소프트 스케줄 — millis()/micros() 기반

loop() 안에서 경과 시간을 직접 확인해 주기를 맞추는 방식이다. 구현이 간단하지만, loop()의 다른 코드가 실행되는 동안은 시간을 체크할 수 없으므로 loop() 안의 다른 작업 실행 시간만큼 지터가 생긴다.

const unsigned long LOOP_PERIOD_US = 1000;  // 1kHz 목표
unsigned long lastTick = 0;

void loop() {
  unsigned long now = micros();
  if (now - lastTick >= LOOP_PERIOD_US) {
    lastTick += LOOP_PERIOD_US;
    controlStep();  // PID 등 실제 제어 연산
  }
  // 다른 작업(Bridge 폴링, 로깅 등)이 여기 섞이면 controlStep() 호출 시점이 지연될 수 있다
}

loop() 안에 다른 부하가 큰 코드가 없다면 이 방식으로도 마이크로초 단위 지터 안에 들어올 수 있지만, 보장은 아니다 — loop()에 한 줄이라도 실행 시간이 긴 코드(예: 문자열 포맷팅, RPC 호출)가 섞이면 그만큼 다음 제어 스텝이 지연된다.

8.2.3.2 하드 스케줄 — 타이머 인터럽트 기반

하드웨어 타이머가 정확히 T마다 인터럽트를 발생시키고, ISR에서 제어 연산의 "트리거" 플래그만 설정한 뒤 실제 연산은 최우선 순위로 처리하는 방식이다. loop()에서 무엇을 하고 있든 상관없이 타이머 인터럽트가 실행을 선점해 정확한 시점을 보장한다.

volatile bool controlTick = false;

// 개념 예시 — 실제 타이머 인터럽트 등록 API(HardwareTimer류)의
// 정확한 이름과 시그니처는 Arduino Core(Zephyr 기반) 버전에 따라
// 다를 수 있어 공식 레퍼런스로 재확인 필요(확인 필요).
void onTimerISR() {
  controlTick = true;   // 부하가 큰 연산은 하지 않는다 — 플래그만 설정한다
}

void setup() {
  // 예: 1kHz 하드웨어 타이머 인터럽트 등록
  // setupHardwareTimer(1000 /* Hz */, onTimerISR);
}

void loop() {
  if (controlTick) {
    controlTick = false;
    controlStep();  // 실제 PID 연산은 loop() 컨텍스트에서, 최대한 빨리
  }
}

STM32U585에 물리 타이머 페리퍼럴이 11개나 있다는 것(8.2.1)은 이 방식을 쓸 여력이 충분하다는 뜻이다. 다만 Arduino Core(Zephyr 기반)가 이 타이머들을 사용자 스케치에 어떤 API 이름으로 노출하는지 — 예를 들어 STM32duino 계열의 HardwareTimer 클래스가 그대로 있는지, 아니면 Zephyr 네이티브 k_timer를 직접 호출해야 하는지 — 는 공식 Arduino UNO Q API 레퍼런스로 재확인이 필요하다. 이 책에서는 개념(하드웨어 타이머 인터럽트로 정확한 주기를 만든다)까지를 확정 사실로 제시하고, 정확한 함수명은 확인 필요로 남긴다. 8.4의 실전 예제에서는 소프트 스케줄과 엔코더 인터럽트를 조합한, API 확정도가 더 높은 구성으로 코드를 제시한다.

8.2.4 어디에 둘 것인가 — MCU vs MPU 배치 기준

제8부(그리고 이후 실전 장들)에서 반복적으로 쓰는 판단 기준을 표로 정리한다.

기준MCU(STM32U585)에 배치MPU(QRB2210)에 배치
데드라인 성격Hard real-time(8.1.2.2) — 놓치면 발산·오작동Soft/firm real-time — 늦어도 품질 저하 수준
루프 주기마이크로초~수 밀리초 (예: PID 1kHz)수십 밀리초 이상 (예: UI 30fps, 로그 1Hz)
허용 지터아주 작음(예측 가능해야 함)상대적으로 큼
연산 성격정수·고정소수점 위주, 소량의 부동소수점(단정밀도 FPU 지원)부동소수점 대량 연산, 행렬·비전·신경망 추론
필요한 라이브러리GPIO·PWM·ADC·UART 저수준 드라이버네트워크 스택, 파일시스템, ROS2(7.1)·Zenoh(7.2), Python/AI 프레임워크
상태 유지 요구짧고 로컬(엔코더 카운트, 적분항 등)장기 저장(7.3·7.4 DB), 대시보드 상태
예시엔코더 읽기, PWM 모터 출력, MAVLink setpoint 스트리밍(8.5)경로 계획, 비전 추론(제9부), 웹 UI, 클라우드 연동

경험칙: "이 작업이 늦게 실행되면 물리적으로 문제가 생기는가?"가 예라면 MCU, "느려지면 사용자 경험만 저하되는가?"가 예라면 MPU. 두 브레인 사이의 데이터는 Bridge(3.4)로 오가는데, 이 경로 자체에 지연·지터가 있으므로(8.3) hard real-time 루프의 "안"에서 Bridge 호출을 기다리는 설계는 피한다 — Bridge는 MCU 루프에 설정값을 흘려주거나 상태를 흘려보내는 비동기 경로로 취급한다.

예 아니오 아니오 작업 후보 놓치면 물리적 문제? MCU에 배치 hard real-time 주기가 밀리초 이하? MPU에 배치 soft/firm real-time

8.2.5 엣지케이스와 함정

  • 인터럽트 폭주(interrupt storm): 노이즈가 많은 엔코더·스위치를 디바운스 없이 CHANGE 모드로 연결하면 인터럽트가 초당 수만 번 발생해 다른 작업을 기아 상태로 만들 수 있다. 하드웨어 디바운스(RC 필터)나 소프트웨어 디바운스(최소 간격 체크)를 함께 쓴다.
  • ISR 안에서 우선순위 역전: 낮은 우선순위 ISR이 공유 자원(예: 같은 변수)을 오래 점유하고 있으면 높은 우선순위 인터럽트가 대기해야 하는 경우가 생길 수 있다 — 임계구역은 최소화한다.
  • volatile만으로는 원자성이 보장되지 않는다. 32비트 이상 변수를 인터럽트와 메인 루프가 동시에 읽고 쓸 때는 레이스 컨디션이 생길 수 있다(예: long 카운터를 읽는 도중 인터럽트가 발생해 상위·하위 바이트가 섞이는 경우). 필요하면 인터럽트를 짧게 비활성화(noInterrupts()/interrupts())한 구간에서 읽는다.
  • 타이머 리소스 충돌: LED 매트릭스가 이미 TIM17을 쓰고 있듯, 시스템이 예약한 타이머를 사용자 코드가 재설정하면 다른 기능이 깨질 수 있다. 어떤 타이머가 비어 있는지 보드 문서로 확인한다(확인 필요).
  • Zephyr 커널 틱과의 상호작용: millis()/micros()는 내부적으로 Zephyr의 시스템 틱(SysTick)에 의존한다. 매우 정밀한 타이밍이 필요하면 커널 틱 해상도 자체가 하한이 될 수 있다 — 기본 틱 설정값은 확인 필요.

8.2.6 다음 장으로

이 장에서 다진 인터럽트·타이머 다루는 법은 8.4(PID 모터 제어 루프)에서 바로 코드로 이어진다. 그 전에 8.3에서는 MCU 루프가 MPU와 통신할 때 생기는 지연을 어떻게 측정하고 설계에 반영할지를 다룬다.

8.2.7 하드웨어 워치독 — IWDG

8.2.3에서 만든 주기적 태스크도, 8.2.5에서 다룬 인터럽트 폭주·우선순위 역전 같은 함정도 결국 하나의 전제 위에 서 있다 — "MCU가 계속 정상적으로 코드를 실행하고 있다"는 전제다. 그 전제가 성립하지 않는 경우, 즉 버그로 무한루프에 빠지거나 하드폴트로 코어가 멈추는 경우를 감지하고 복구하는 마지막 안전장치가 하드웨어 워치독(IWDG, Independent Watchdog)이다.

8.2.7.1 왜 하드웨어 워치독이 필요한가

loop() 안의 버그, 잘못 작성한 ISR(8.2.2.1에서 경고한 "ISR 안에서 블로킹 호출"이 실제로 발생하는 경우), 또는 예상 못 한 하드폴트로 MCU가 특정 지점에 멈춰 버리면 어떻게 될까. 이 상태는 loop()도, attachInterrupt()로 등록한 ISR도, millis()도 — 코드로 작성한 그 어떤 감시 로직도 더는 실행되지 않는다는 뜻이다. 소프트웨어로 구현한 감시 장치는 감시 대상과 같은 CPU·같은 실행 흐름 위에서 동작하기 때문에, CPU 자체가 멈추면 감시 장치도 같이 멈춘다.

이 문제를 원천적으로 피하려면 감시 로직이 감시 대상과 완전히 분리된 하드웨어에서 독립적으로 동작해야 한다. STM32U585의 IWDG가 바로 그 역할이다 — 별도의 저속 클록(LSI)으로 구동되는 독립 다운카운터가 계속 0을 향해 줄어들고, 정상 동작 중인 소프트웨어가 주기적으로 그 카운터를 리로드("feed"·"kick")해 주지 않으면 카운터가 0에 도달하는 순간 IWDG가 무조건 MCU를 리셋시킨다. 소프트웨어가 feed를 멈췄다는 것 자체가 "이상이 발생했다"는 신호이므로, IWDG는 정상/비정상을 판별할 필요 없이 단순히 "주기적으로 신호가 왔는가"만 본다.

8.2.7.2 소프트웨어 워치독과의 차이

8.2.3.1에서 소개한 millis() 기반 소프트 스케줄과 같은 방식으로 "소프트웨어 워치독"도 만들 수 있다 — 예를 들어 각 태스크가 마지막으로 정상 실행된 타임스탬프를 기록해 두고, loop()의 다른 자리에서 그 타임스탬프가 너무 오래됐으면 스스로 재시작 로직을 트는 식이다. 이런 방식은 "이 특정 태스크가 논리적으로 데드라인을 놓쳤는가" 같은 세밀한 조건을 감지하는 데는 유용하지만, 근본적인 한계가 있다 — 그 감시 코드 자체가 loop()의 일부이므로, MCU가 완전히 멈추면(무한루프·하드폴트·클록 정지) 감시 코드도 함께 멈춰 아무 조치도 못 한다.

IWDG는 이 한계를 구조적으로 피한다. ST의 공식 HAL 문서에 따르면 IWDG는 메인 클록이 정지해도 계속 동작하도록("stays active even if the main clock fails") 설계되어 있고, CPU 코어와 별도의 VDD 전원 도메인에 구현되어 있어 STOP·STANDBY 같은 저전력 모드에서도 계속 카운트다운한다. 즉 감시 대상(CPU 실행 흐름)이 완전히 정지해도 감시자(IWDG)는 별도의 클록·전원으로 계속 동작해 리셋을 발생시킬 수 있다. 이것이 "하드웨어" 워치독과 "소프트웨어" 워치독의 근본적인 차이다 — 실무에서는 두 계층을 함께 쓰는 것이 이상적이다. 소프트웨어 워치독으로 세밀한 논리적 이상을 검출하고, IWDG로 "그마저도 실행이 안 될 만큼 완전히 멈춘" 최악의 경우를 검출한다.

아니오, 완전 정지 무한루프·하드폴트 예 MCU 정상 실행 중 loop()/ISR이 계속 실행되는가? 소프트웨어 워치독: 태스크별 타임스탬프 확인 스스로 이상 감지·복구 소프트웨어 워치독도 실행 못 됨 — 감지 불가 IWDG(별도 클록·전원) feed 신호 끊김 감지 IWDG 카운터 0 도달 → 무조건 MCU 리셋

8.2.7.3 STM32U585 IWDG 하드웨어 스펙

STM32U585의 IWDG는 ST의 STM32U5 시리즈 레퍼런스 매뉴얼(RM0456)과 HAL 문서 기준으로 다음과 같이 동작한다.

  • 클록 소스: LSI(Low-Speed Internal) 클록. 메인 시스템 클록(HSE/MSI/PLL)과 완전히 독립적이다 — 메인 클록 트리에 문제가 생겨도 LSI는 별도로 구동된다. 공칭(nominal) 주파수는 32kHz다. LSI는 저정밀 내장 오실레이터라 실제 주파수가 온도·개체별로 상당히 달라질 수 있다 — 정확한 min/typ/max 허용 오차는 STM32U585 데이터시트 전기적 특성(Electrical characteristics) 장의 "LSI oscillator characteristics" 표를 봐야 한다(이번 조사에서 PDF 원문의 정확한 수치까지는 확인하지 못했다 — 확인 필요). 실무적으로는 이 오차 때문에 워치독 타임아웃을 "정확히 맞춰" 설정하지 말고 여유를 두고 설정해야 한다.
  • 카운터·프리스케일러: 12비트 다운카운터(리로드 레지스터, 0~4095)를 프리스케일러(4~256 사이의 값, 통상 2^n 단계)로 나눈 LSI 클록이 감소시킨다. 타임아웃은 대략 t = (리로드값 + 1) × 프리스케일러 / f_LSI로 계산된다.
  • 타임아웃 범위: 공칭 32kHz LSI 기준으로 최소 프리스케일러·최소 리로드값에서는 대략 수백 마이크로초, 최대 프리스케일러(256)·최대 리로드값(4095)에서는 약 32.7초까지 설정할 수 있다(널리 인용되는 근사치는 최소 약 125μs, 최대 약 32.7초다). 정확한 경계값은 위와 같은 이유로 LSI 실측 주파수에 따라 달라진다.
  • 저전력 모드·디버그 동작: IWDG는 STOP·STANDBY 같은 저전력 모드에서도 계속 카운트다운하며, IWDG 리셋이 STANDBY에서 CPU를 깨울 수도 있다. 반대로 디버거로 코어를 정지시켰을 때 IWDG 카운터를 함께 멈출지(DBG_IWDG_STOP 설정 비트) 여부는 선택할 수 있다 — 디버깅 중에 의도치 않은 리셋을 피하려면 이 옵션을 알아 두는 것이 좋다.
  • 한 번 시작하면 끌 수 없다: IWDG는(장치군에 따라 다르지만 STM32 계열에서 일반적으로) 소프트웨어로 정지시킬 수 없는 것으로 알려져 있다 — 한 번 wdt_setup()(또는 HAL의 IWDG_Start 계열)을 호출하면 그 후로는 계속 feed해 주거나, 리셋을 감수해야 한다. 정확한 동작(예: 특정 Zephyr 드라이버 옵션으로 disable이 가능한지)은 확인 필요.

출처: STMicroelectronics STM32U5 시리즈 레퍼런스 매뉴얼(RM0456) 및 STM32U5 HAL 드라이버 공식 문서("HAL IWDG How to Use", dev.st.com), ST 공식 STM32 IWDG 제품 교육 자료(STM32WB-WDG_TIMERS-Independent-Watchdog-IWDG).

8.2.7.4 Arduino API로 노출되는가 — 직접 조사 결과

이 장·이 절에서 가장 중요하게 확인해야 할 것은 "STM32U585에 IWDG 하드웨어가 있다"는 사실이 아니라 "UNO Q의 Arduino 스케치에서 그것을 실제로 쓸 수 있는가"다. 이를 확인하기 위해 arduino/ArduinoCore-zephyr 공식 저장소(GitHub, arduino:zephyr:unoq 코어)를 직접 클론해 전체 소스를 검색했다. 결과는 다음과 같다.

  • 저장소 전체(코어 소스·라이브러리·샘플·변형(variant) 설정 포함)를 watchdog·wdt·iwdg 키워드로 대소문자 구분 없이 검색한 결과 일치하는 파일이 하나도 없었다. libraries/ 디렉터리에는 Arduino_LED_Matrix, Camera, CAN, Ethernet, PDM, RTC, SocketWrapper, SPI, Storage, WiFi, Wire 등은 있지만 워치독 관련 라이브러리는 없다.
  • UNO Q 보드의 Zephyr 설정 파일(variants/arduino_uno_q_stm32u585xx/arduino_uno_q_stm32u585xx.conf)에도 CONFIG_WATCHDOG·CONFIG_IWDG_STM32 같은 옵션이 없다 — 즉 IWDG 드라이버 자체가 이 보드의 기본 빌드에 컴파일되어 들어가지 않는다.
  • Zephyr 프로젝트 자체에는 STM32용 IWDG 드라이버(drivers/watchdog/wdt_iwdg_stm32.c, 디바이스트리 바인딩 st,stm32-watchdog)와 표준화된 워치독 API(wdt_setup(), wdt_install_timeout(), wdt_feed(), wdt_disable())가 이미 존재한다(Zephyr 공식 문서·소스로 확인). 문제는 이것이 ArduinoCore-zephyr 쪽에서 활성화·연결되어 있지 않다는 점이다.
  • ArduinoCore-zephyr의 스케치는 사전에 빌드된 Zephyr "로더(loader)" 위에 llext(linkable loadable extension)로 적재되어 실행되는 구조다. 스케치에서 어떤 Zephyr 함수를 부르려면 그 함수가 loader/llext_exports.c에 명시적으로 내보내져(export) 있어야 링크된다 — 이 파일도 함께 검색했지만 wdt_* 함수는 내보내져 있지 않았다.
  • 저장소의 공식 README(FAQ)는 정확히 이 상황을 위한 안내를 담고 있다: "Q: 컴파일에 포함되지 않은 Zephyr 서브시스템을 쓰고 싶다" → "A: 보드의 .conf 파일을 열어 필요한 CONFIG_를 추가하고, 로더를 재컴파일·재업로드하라." 즉 사용자가 원한다면 (1) 보드 .conf에 CONFIG_WATCHDOG=y·CONFIG_IWDG_STM32=y를 추가하고, (2) llext_exports.c에 필요한 wdt_* 함수를 내보내도록 추가한 뒤, (3) Zephyr 로더 자체를 다시 빌드·플래시해야 한다 — 이는 일반 .ino 스케치 코드만 고쳐서 되는 일이 아니라 Arduino Core 자체를 커스텀 빌드하는 범위의 작업이다.

결론: IWDG는 표준 Arduino API로는 노출되어 있지 않다. Zephyr 네이티브 워치독 API 자체는 STM32U585을 지원하는 드라이버로 이미 Zephyr 안에 존재하므로 이론적으로는 위 3단계(Kconfig 활성화 → llext export 추가 → 로더 재빌드)를 거치면 접근할 수 있을 것으로 보이지만, 이 경로를 실제로 끝까지 수행해 동작을 검증하지는 못했다 — 확인 필요로 남긴다. 아래 코드는 그 이론적 경로를 전제로 한 개념 예시이며, 표준 Arduino IDE로 스케치만 작성해서는 컴파일·링크되지 않는다는 점을 분명히 해 둔다.

// 개념 예시 — 표준 Arduino 스케치만으로는 동작하지 않는다(확인 필요).
// 전제 조건(이 책 시점 기준 미검증):
//   1) variants/arduino_uno_q_stm32u585xx.conf 에
//      CONFIG_WATCHDOG=y, CONFIG_IWDG_STM32=y 추가
//   2) loader/llext_exports.c 에 wdt_setup/wdt_install_timeout/wdt_feed 내보내기 추가
//   3) Zephyr 로더 재컴파일·재플래시
// 위 세 단계를 거쳤다는 전제 하에서만 아래와 같은 형태의 호출을 시도해 볼 수 있다.
#include <zephyr/drivers/watchdog.h>
#include <zephyr/device.h>

static const struct device *iwdg = DEVICE_DT_GET(DT_NODELABEL(iwdg));

void setup() {
  struct wdt_timeout_cfg cfg = {};
  cfg.window.max = 2000;            // 2초 안에 feed하지 않으면 리셋
  cfg.flags = WDT_FLAG_RESET_SOC;   // 타임아웃 시 SoC 전체 리셋

  wdt_install_timeout(iwdg, &cfg);
  wdt_setup(iwdg, 0);
}

void loop() {
  controlTick();       // 8.2.3의 제어 태스크가 정상적으로 실행되는 경로
  wdt_feed(iwdg, 0);    // 정상 동작을 증명하는 신호 — 여기가 실행되지 않으면 리셋된다
}

feed 호출의 위치도 설계의 일부다. loop()의 맨 앞이나 ISR에서 무조건 feed하면 워치독이 "항상 정상 동작 중이라고 오인"해 정작 loop() 뒷부분이 정지해도 검출하지 못한다 — feed는 8.2.5에서 경고한 함정들(인터럽트 폭주로 다른 작업이 기아 상태가 되는 상황, 우선순위 역전으로 특정 태스크가 오래 실행되지 못하는 상황)까지 실제로 통과했다는 것을 증명하는 자리, 즉 한 사이클의 "정상 완료 지점"에 두어야 워치독이 제 역할을 한다.

Sources: