요구사항 명세(SR) 학습 노트 목차

09. 소프트웨어 구조 — 요구사항이 코드 어디에 앉는가

앞의 00~08편은 SR이 무엇을 요구하는가였다. 이 편부터는 그래서 무엇을 만드는가다. ⚠️ 이 편의 내용은 SR에 없다. §9가 "Controller 간 내부 명령 전달, 상태 교환, 세부 중재 및 실행 메커니즘은 SysRS 또는 설계 문서에서 정의한다"고 미뤄 둔 영역을, SR 요구사항에서 역산해 구성한 설계 초안이다. 확정 전에 팀 합의가 필요하다.


1. 전체 구조를 한 장으로

다이어그램 로딩 중…

이 그림에서 반드시 봐야 할 세 가지

(1) P_SAFEP_DRV 화살표가 Central을 안 거친다. Anti-pinch는 Power Window 노드 안에서 감지·정지가 닫혀 있다. §6.2.5의 "통신 오류가 발생하더라도 로컬 Anti-pinch 보호 기능을 유지"가 이 화살표 하나로 표현된다. 이 경로에 CAN이 끼면 요구사항 위반이다.

(2) 판단은 전부 C_ARB(Central)로 모이고, 실행은 전부 아래로 흩어진다. Sensing은 "덥다"를 말하지 않는다. "온도 27.3 °C, 유효"를 말한다. "공조가 필요하다"는 판단은 Central이 한다. 이 분리가 유지돼야 기능 추가 시 센싱 노드를 안 건드린다.

(3) Vision은 Sensing을 거쳐 올라간다. RPi가 CAN에 직접 붙지 않는다(§7.1 기준 구성 + TBD-003). Sensing ControllerUART/SPI로 받아 CAN으로 중계한다. 즉 Sensing 노드는 센서 수집기이자 Vision 게이트웨이라는 두 역할을 갖는다.


2. 계층 구조 — 모든 노드에 공통

노드마다 하는 일은 다르지만 층은 같게 간다. 그래야 코드를 옮겨 붙이거나 공통 모듈을 공유할 수 있다.

┌─────────────────────────────────────────────┐
│  app/     기능 로직 — SR 요구사항이 여기 산다   │  ← 노드마다 다름
├─────────────────────────────────────────────┤
│  svc/     공통 서비스                         │  ← 전 노드 공유
│           trust(유효성) · fault(오류) ·       │
│           req(요청 추적) · sched(주기)        │
├─────────────────────────────────────────────┤
│  com/     통신 — CAN/UART 프레임 · E2E ·      │  ← 전 노드 공유
│           heartbeat · 신호 인코딩             │
├─────────────────────────────────────────────┤
│  drv/     장치 드라이버 — 모터·LED·센서·오디오  │  ← 노드마다 다름
├─────────────────────────────────────────────┤
│  hal/     칩 주변장치 — GPIO/ADC/PWM/FTM/     │  ← S32K 공용, ESP32 별도
│           LPUART/FlexCAN/타이머               │
└─────────────────────────────────────────────┘

svc/를 따로 두는가

00편의 공통 패턴 5종이 전부 svc/에 산다. 기능마다 따로 구현하면 반드시 어긋난다.

공통 패턴모듈전 기능이 공유하는 이유
A. 신뢰 못 하면 새로 시작 안 함svc/trust유효성 판정 규칙이 기능마다 다르면 §6.5.3의 일관된 표시가 불가능
B. 기능 오류 격리svc/fault기능별 상태 배열이 하나여야 §6.6을 만족
C. 자동 재개 금지svc/req요청 생애주기가 하나의 모델이어야 §6.5.1과 §6.2.6이 일치
D. stale 표시 금지svc/trustage 판정이 한 군데
E. 멱등성svc/req중복 요청 판정이 한 군데

3. svc/trust — 유효성의 단일 창구

03편 §4에서 본 3축(범위·신선도·복구)을 모든 신호가 통과하는 하나의 래퍼로 만든다.

/* svc/trust.h — 전 노드 공통 */

typedef enum {
    TRUST_OK = 0,
    TRUST_OUT_OF_RANGE,   /* §6.3.4 유효 범위 이탈 */
    TRUST_STALE,          /* §6.3.4 갱신 기준 미달 — TBD-022 */
    TRUST_RECOVERING,     /* §6.3.4 복구 조건 미충족 — TBD-038 */
    TRUST_NO_DATA,        /* 한 번도 받은 적 없음 */
} trust_t;

typedef struct {
    int32_t   value;
    int32_t   lo, hi;         /* 유효 범위 */
    uint32_t  max_age_ms;     /* TBD-022 — 신호별로 다르다 */
    uint8_t   recover_streak; /* TBD-038 — 복구에 필요한 연속 정상 횟수 */
    uint8_t   ok_streak;
    uint32_t  updated_ms;
    trust_t   state;
} trusted_t;

void    trust_init(trusted_t *t, int32_t lo, int32_t hi,
                   uint32_t max_age_ms, uint8_t recover_streak);
void    trust_feed(trusted_t *t, int32_t raw);   /* 새 값 수신 시 */
trust_t trust_eval(const trusted_t *t);          /* 사용 직전 평가 — age 는 여기서 */
bool    trust_ok(const trusted_t *t);            /* == (trust_eval() == TRUST_OK) */

trust_eval()trust_feed()와 분리된 게 핵심이다. age는 시간이 흐르면 저절로 나빠지므로, 값이 들어올 때가 아니라 쓸 때 판정해야 한다. feed에서만 판정하면 갱신이 멈춘 신호가 영원히 OK로 남는다.

신호별 설정값 — 한 곳에 모은다

/* svc/trust_config.h — TBD-022 · TBD-038 확정 시 여기만 고친다 */
/*                          lo      hi   max_age  recover  근거 */
#define TRUST_LUX        { 0,    100000,   2000u,    3u }  /* 조도: 느리게 변함 */
#define TRUST_TEMP_C10   { -400,    850,   3000u,    3u }  /* -40.0~85.0 °C */
#define TRUST_HUMID_P10  { 0,      1000,   3000u,    3u }  /* 0~100.0 %RH */
#define TRUST_DIST_MM    { 20,     5000,    300u,    2u }  /* 초음파: 빨리 낡음 */
#define TRUST_OCCUPANT   { 0,        15,   2000u,    5u }  /* 인원 수 */
#define TRUST_ANIMAL     { 0,         1,  60000u,    3u }  /* 트리거 기반이라 길다 */
#define TRUST_WIN_POS    { 0,      1000,    200u,    2u }  /* 0~100.0 % 개도 */
#define TRUST_DOOR_LOCK  { 0,         1,   1000u,    2u }

max_age가 신호마다 다르다는 게 07편 §3-1의 결론이다. 전역 상수 하나로 하면 초음파가 위험해지거나 온도가 항상 stale로 뜬다. 이 표가 곧 TBD-022의 산출물 형태다.


4. svc/fault — 기능 × 오류종류 2차원 레지스트리

§6.6의 "기능별 동작 상태를 서로 구분", "센서 오류통신 오류를 서로 구분"을 자료구조로 강제한다.

/* svc/fault.h */

typedef enum {                 /* 00편 §1 노드 표와 §6 기능 도메인이 그대로 온다 */
    FUNC_VSS, FUNC_POWER_WINDOW, FUNC_SENSING, FUNC_VISION,
    FUNC_SMART_ACCESS, FUNC_CLIMATE, FUNC_AMBIENT, FUNC_HMI,
    FUNC_COUNT
} func_id_t;

typedef enum {                 /* §5 용어 정의 + §6.5.3 */
    ERR_SENSOR, ERR_COMM, ERR_FUNCTION,
    ERR_KIND_COUNT
} err_kind_t;

typedef enum {                 /* 01편 VSS 상태 기계에서 일반화 */
    FST_INIT, FST_READY, FST_ACTIVE, FST_DEGRADED, FST_FAULT, FST_DISABLED
} func_state_t;

typedef struct {
    func_state_t state;
    uint16_t     err_detail[ERR_KIND_COUNT];  /* 종류별 세부 코드 (0 = 없음) */
    uint32_t     since_ms;
} func_status_t;

extern func_status_t g_func[FUNC_COUNT];      /* 전역 단일 플래그 금지 */

void         fault_set(func_id_t f, err_kind_t k, uint16_t detail);
void         fault_clear(func_id_t f, err_kind_t k);
bool         fault_any(func_id_t f);
func_state_t fault_state(func_id_t f);

err_detail을 배열로 둔 이유

한 기능이 동시에 여러 종류의 오류를 가질 수 있다. Predictive Climate는 습도 센서 고장(ERR_SENSOR) + Sensing 노드 통신 두절(ERR_COMM)을 동시에 겪을 수 있고, §6.5.3은 이 둘을 사용자가 구분할 수 있게 표시하라고 한다. 단일 필드면 나중에 쓴 게 앞을 덮는다.

DEGRADED와 FAULT의 판정은 기능이 한다

svc/fault는 저장만 한다. "습도만 죽었으니 DEGRADED"인지 "Fan이 안 도니 FAULT"인지는 각 기능 모듈이 결정한다. 05편 §8의 표가 그 규칙이다.

/* app/climate/climate_health.c */
static func_state_t climate_eval_state(void)
{
    if (fan_mismatch_latched())            return FST_FAULT;      /* 실행 불가 */
    if (!trust_ok(&g_temp))                return FST_DEGRADED;   /* 온도 없으면 자동공조 불가 */
    if (!trust_ok(&g_humid))               return FST_DEGRADED;   /* 습도만 없으면 환기 판단만 불가 */
    if (!climate_enabled())                return FST_DISABLED;
    return fan_owner_is(FAN_OWNER_NONE) ? FST_READY : FST_ACTIVE;
}

5. svc/req — 요청 생애주기의 단일 모델

07편 §1-2의 6단계를 전 노드가 공유한다. 이게 있어야 §6.2.6(윈도우)·§6.5.1(HMI)·§6.5.4(모바일)가 같은 상태 집합을 쓴다.

/* svc/req.h */

typedef enum {
    REQ_ACCEPTED, REQ_IN_PROGRESS, REQ_COMPLETED,
    REQ_REJECTED, REQ_ABORTED, REQ_FAILED,
} req_state_t;

typedef enum {                          /* 거부 사유 — §6.2.2 · §6.5.1 */
    RJ_NONE = 0,
    RJ_SAFETY_UNAVAILABLE,              /* Anti-pinch 사용 불가 등 */
    RJ_STATE_UNRELIABLE,                /* 위치·도어 상태 신뢰 불가 */
    RJ_COMM_UNRELIABLE,                 /* 통신 정보 신뢰 불가 */
    RJ_NOT_ALLOWED,                     /* 운영·안전 조건 미충족 */
    RJ_PROTECTION_ACTIVE,               /* 보호 래치 걸림 */
    RJ_LOWER_PRIORITY,                  /* 상위 요청 진행 중 */
    RJ_TARGET_ALREADY,                  /* 멱등 — 이미 목표 상태 */
    RJ_NOT_AUTHORIZED,                  /* 인증·권한 미확인 */
} reject_t;

typedef struct {
    uint16_t    id;                     /* 발신자별 증가 — 멱등 판정용 */
    uint8_t     src;                     /* win_src_t 등 발신 출처 */
    func_id_t   target;
    req_state_t state;
    reject_t    reason;
    uint32_t    created_ms;
} req_t;

bool req_is_duplicate(uint8_t src, uint16_t id);   /* 패턴 E */
void req_transition(req_t *r, req_state_t s, reject_t why);

reject_t가 공용인 게 중요하다. 사유 열거형을 기능마다 따로 만들면 HMI가 기능별로 다른 메시지 테이블을 들고 있어야 하고, 새 기능이 생길 때마다 HMI를 고쳐야 한다.


6. 노드별 모듈 구성과 담당 요구사항

6-1. S32K344-WB — Central Controller + VSS

app/
  arbiter/      기능 상태 종합, 자동 기능 트리거 판단      §6.3.2 판단부 · §6.6
  vss/
    engine.c    우선순위 슬롯 · 선점 · 중재               §6.1.2
    mixer.c     Ducking · Mute · Fade · 음량 하한          §6.1.3
    catalog.c   이벤트 → 음향 매핑 · 반복 횟수 · 최대시간   §6.1.1
    health.c    입력 불신/자체 오류 · READY/DEGRADED/FAULT §6.1.4
  power/        차량 전원 상태(OFF/ACC/ON/START) 관리      §5 · TBD-027
  hmi_link/     ESP32 와의 상태·요청 중계                  §6.5 · TBD-001
svc/  trust · fault · req · sched
com/  can_tx · can_rx · e2e · heartbeat
drv/  audio_codec · amp
hal/  flexcan · sai_i2s · edma · lpit

VSS 모듈이 4개로 쪼개진 이유는 01편의 4개 소절과 1:1이다. catalog(무엇을) → engine(누가 이기나) → mixer(얼마나 크게) → health(낼 수 있나). 요구사항 절과 소스 파일이 대응하면 변경 영향 분석이 쉬워진다.

6-2. S32K144 — Power Window

app/
  window/
    safety.c    끼임 감지 · 즉시 정지 · 보호 구동 · 래치   §6.2.4  ★ 로컬 폐루프
    position.c  펄스 카운트 · 끝단 학습 · UNKNOWN 관리     §6.2.3 · TBD-008
    arbiter.c   출처 4종 우선순위 · 정지 최우선 · 멱등      §6.2.1
    gate.c      6단계 관문 · 거부 사유 생성                §6.2.2
    motion.c    열림/닫힘/정지 · 끝단 정지 · 환기 위치      §6.2.3 · TBD-007
    report.c    7상태 + 신뢰도 축 · 요청 생애주기 통지      §6.2.6
drv/  hbridge · hall · current_sense
hal/  ftm_pwm · adc · gpio · lpit

safety.c는 다른 모듈에 의존하지 않는다position.c만 읽고, drv/hbridge를 직접 호출한다. arbiter·gate·com이 전부 죽어도 동작해야 하기 때문이다(§6.2.5).

/* app/window/safety.c — 의존성을 의도적으로 최소화 */
#include "drv/hbridge.h"       /* 직접 제어 */
#include "app/window/position.h"
/* com/ · svc/req 를 include 하지 않는다 — 통신 없이 동작해야 한다 */

#include 목록 자체가 §6.2.5를 지키는지 검증하는 수단이 된다. 코드 리뷰에서 safety.ccom/ 헤더가 추가되면 즉시 반려.

6-3. S32K144 — Function Controller

app/
  access/
    dk_gate.c   인증·근접 확인 결과 수신 후 허용 판정      §6.4.1-1
    lock.c      잠금/해제 폐루프 · 열린문 금지 · 멱등       §6.4.1-2 · TBD-017
    dstate.c    잠금축 × 개폐축 · 비정상 조합 노출          §6.4.1-3
    relock.c    Auto Re-lock                             §6.4.1-4
    autolock.c  이탈 기반 Auto Lock · 열린문 알림          §6.4.1-5
  climate/
    target.c    목표온도 자동공조 · 4단계 히스테리시스      §6.4.2-1 · TBD-012a
    circulate.c 사용자 직접 공기순환                       §6.4.2-2
    precond.c   이용 패턴 학습 · 예상 탑승 시점            §6.4.2-3
    fan_own.c   Fan 소유권 · 전환 · 재개 금지              §6.4.2-4  ★
    vent.c      Window 기반 자동환기 요청·종료 판정         §6.4.2-5 · TBD-012b
    fan_mon.c   요구 vs 실제 · 지속 방지 · 배터리 컷오프     §6.4.2-6,7 · TBD-018
  ambient/
    sources.c   6종 표시 요구 수집 · 수명 모델 2종          §6.4.3-1~4
    priority.c  우선순위 선택 · 선점 · 재개 금지            §6.4.3-5  ★
    dim.c       조도 기반 밝기 · 안전 하한 · slew          §6.4.3-6 · TBD-011
    out.c       출력 반영 · 전류 감시                      §6.4.3-7,8
drv/  lock_actuator · fan_pwm · led_pwm

★ 표시한 fan_own.cpriority.c이 노드의 핵심 중재기다. 05편·06편에서 본 "소유권 모델"과 "매 주기 최우선 재선택"이 각각 여기 산다.

6-4. S32K144 — Sensing Controller

app/
  sense/
    lux.c · temp.c · humid.c · ultrasonic.c   원시 수집 + 온도보상   §6.3.1
    validity.c   trust 래핑 · 센서별 max_age 적용                  §6.3.4
    proximity.c  후진 AND 거리 → 위험 생성 / OR → 해제 · 단계 산출   §6.3.3 · TBD-013
  vision_gw/
    link.c       RPi UART/SPI 프레이밍 · CRC                     TBD-003
    relay.c      비전 결과를 CAN 신호로 중계                       §6.3.1,5
drv/  adc_mux · hcsr04 (또는 채택 초음파 모듈) · i2c_th_sensor

proximity.c가 Sensing에 있는 게 논쟁 지점이다. 판단은 Central이 한다는 원칙과 어긋나 보이지만, §6.3.3이 "근접 위험 상태를 생성해야 한다"를 센싱 절에 두었고 거리 → 단계 변환은 센서 특성에 밀착한 계산이다. 원 거리와 단계를 둘 다 올려보내고(03편 §3-4), Central은 그걸 VSS 2단계로 다시 매핑한다.

6-5. Raspberry Pi 3 B — Vision Module

vision/
  capture.py      두 카메라 스트림 · drop-oldest 큐(깊이 1~2)
  quality.py      밝기·블러·포화 게이트                    §6.3.4
  occupancy.py    탑승자 존재/인원 — 상시 · 높은 우선순위    §6.3.1 · TBD-014
  engine_bay.py   동물 판정 — 트리거/주기 · 낮은 우선순위    §6.3.5 · TBD-030,036
  link.py         Sensing 으로 결과 송신 (영상은 안 나간다)  §6.3.4 개인정보

occupancy.pyengine_bay.py를 별도 프로세스/스레드로 분리해야 §6.3.4의 "엔진룸 판정이 지연·실패해도 실내 인식을 방해하지 않아야 한다"를 만족한다. 같은 루프에 넣으면 구조적으로 위반이다.

6-6. ESP32 — HMI / Digital Key

app/
  ui/
    render.c     상태 렌더 · stale/오류 배지 · 안전 오버레이  §6.5.3
    input.c      터치 입력 → 요청 생성                      §6.5.1
    reqview.c    요청 생애주기 6단계 표시 · 거부 사유        §6.5.1
  ble/
    dk_auth.c    Digital Key 챌린지-응답                    §6.4.1-1
    proximity.c  RSSI 필터 · 히스테리시스 · 유지시간         TBD-016
  mobile/
    session.c    인증 + 차량별 권한                         §6.5.2
    alerts.c     미확인 중요 경고 관리                      §6.5.4
    history.c    최근 요청 결과 이력                        §6.5.4
  link/          Central 과의 통신                          TBD-001

7. 상태 소유권 — 가장 중요한 표

분산 시스템에서 같은 상태를 두 노드가 각자 갖고 있으면 반드시 어긋난다. 각 상태의 원본(source of truth)이 어디인지를 못 박아야 한다.

상태원본 노드사본을 갖는 노드왜 여기가 원본인가
윈도우 위치 · 이동 상태Power WindowCentral, ESP32(표시)센서가 여기 붙어 있고, Anti-pinch가 이 값에 의존
Anti-pinch 래치Power WindowCentral(경고음 트리거)로컬 안전 상태 — 통신과 무관해야 함
도어 잠금 상태FunctionCentral, ESP32액추에이터와 피드백 센서가 여기
도어 물리 개폐FunctionCentral, ESP32도어 스위치 입력이 여기
Fan 소유자 · 출력 수준FunctionCentral, ESP3205편 소유권 모델이 이 노드 안에서 닫힘
Ambient 현재 표시FunctionESP32(표시)06편 우선순위 선택이 이 노드 안에서 닫힘
조도·온도·습도·거리SensingCentral, Function, ESP32센서 위치
근접 위험 단계SensingCentral(VSS), ESP32§6.3.3
탑승자 존재·인원Vision → SensingCentral, ESP32RPi가 판정, Sensing이 중계
엔진룸 동물 진입Vision → SensingCentral, ESP32, 모바일
Digital Key 인증·근접ESP32CentralBLE 라디오가 여기
차량 전원 상태Central전 노드TBD-027 — 생성 주체 미정, 잠정 Central
기능별 오류 상태각 기능 노드Central(집계), ESP32(표시)§6.6 격리 원칙
표시용 종합 상태CentralESP32§6.5.3 공통 표시 원칙을 한 곳에서 적용

규칙 세 개

규칙 1 — 사본은 절대 수정하지 않는다. Central이 들고 있는 윈도우 위치는 읽기 전용이다. 사본을 고치는 코드가 생기는 순간 두 값이 갈라진다.

규칙 2 — 사본에는 항상 trusted_t가 따라붙는다. 원본 노드에서는 센서를 직접 읽으니 신선하지만, 사본은 CAN을 타고 왔으므로 낡을 수 있다. 사본을 int32_t로 들고 있으면 §6.5.3의 stale 판정이 불가능하다.

/* Central 이 들고 있는 윈도우 위치 — 값이 아니라 trusted_t 로 */
static trusted_t g_win_pos;        /* ✓ age 판정 가능 */
/* static int32_t g_win_pos; */    /* ✗ 언제 받은 값인지 모른다 */

규칙 3 — 판단은 원본을 가진 노드나 Central에서만. Function Controller가 윈도우 위치 사본을 보고 뭔가 판단하기 시작하면, 그 판단은 낡은 값에 기반한다. 자동 환기가 창문을 요청만 하고 직접 안 움직이는 이유가 이것이다(05편 §5-2).


8. 태스크와 주기 설계

8-1. S32K344-WB (Central + VSS)

태스크주기우선순위하는 일
audio_dma_isrI²S 버퍼 (예 1 ms)최고오디오 샘플 공급 · Fade 게인 적용
can_rx_isr이벤트높음프레임 수신 → 링버퍼
vss_tick1 ms높음반복 횟수 · 최대 경고 시간 · 선점
arbiter_tick10 ms중간기능 상태 종합 · 자동 기능 판단
com_tick10 ms중간heartbeat 송신 · timeout 감시
state_tx_tick이벤트 + 500 ms중간상태 송신 (TBD-021)
hmi_link_tick20 ms낮음ESP32 송수신
diag_tick100 ms낮음오류 집계 · 로깅

audio_dma_isr가 최고 우선순위인 이유: 버퍼가 비면 소리가 끊긴다(underrun). 그리고 00편 §1에서 지적한 "VSS가 Central을 굶기지 않게" 하려면 ISR짧게 유지해야 한다 — 게인 곱셈만 하고 판단은 vss_tick이 한다.

8-2. S32K144 (Power Window) — 안전이 스케줄을 지배한다

태스크주기우선순위하는 일
pinch_isr1 ms 타이머최고전류·속도 샘플 → 끼임 판정 → 하드웨어 브레이크
hall_isr펄스 이벤트최고위치 카운트 · 방향
motion_tick5 ms높음보호 구동 · 끝단 정지 · 목표 추종
gate_tick10 ms중간요청 관문 6단계
com_tick10 ms중간CAN 송수신 · heartbeat
report_tick이벤트 + 500 ms낮음상태 · 요청 결과 송신

pinch_isr의 1 ms는 TBD-009의 반응시간 요구에서 역산해야 하는 값이다. 지금은 잠정치다. 감지 지연 = 샘플 주기 + 필터 지연 + 판정 지연 + 브레이크 응답이므로, 벤치에서 전체를 재고 주기를 조정한다.

8-3. 나머지 노드

노드주요 태스크주기
Functionaccess_tick / climate_tick / ambient_tick20 / 50 / 20 ms
Functionfan_safety_tick (지속 방지·배터리)100 ms
Functionactuator_watchdog_isr (도어락 최대 ON)1 ms 타이머
Sensingadc_scan (조도·온습도)100 ms
Sensingus_cycle_tick (초음파 순차 발신)60 ms × 센서 수
Sensingvision_link_tick50 ms
RPioccupancy 추론목표 FPS (TBD-014)
RPiengine_bay 추론트리거 + 저빈도 주기 (TBD-036)
ESP32ui_render33 ms (30 fps)
ESP32ble_scan100 ms

주기를 정할 때 반드시 확인할 것: 어떤 신호의 max_age그 신호를 만드는 태스크 주기의 2~3배 이상이어야 한다. 07편 §6-2에서 본 관계다. 위 표와 §3의 trust_config.h를 나란히 놓고 검산해야 한다. 예: 초음파 max_age = 300 ms인데 센서 4개 순차 발신이면 한 바퀴 240 ms — 여유가 60 ms뿐이라 위험하다. 센서 수를 줄이거나(TBD-013) max_age를 늘려야 한다. 이런 모순은 표를 나란히 놓아야 보인다.


9. 데이터 흐름 — 센서 한 값이 소리가 되기까지

초음파 거리 하나가 경고음이 되는 전 경로를 따라가면 구조 전체가 한 번에 보인다.

다이어그램 로딩 중…

이 경로에서 값이 버려질 수 있는 지점 4곳

지점조건근거
svc/trust범위 이탈 · age 초과 · 복구 미완§6.3.4
proximity.c후진 기어 아님§6.3.3
com/can_rxCRC 불일치 · seq 역행 · heartbeat 끊김§6.2.5 유령 명령 방지
arbiter시동 꺼짐§6.1.1

어느 지점에서 버려졌는지가 §6.5.3의 오류 종류 구분과 대응한다. trust에서 버려지면 ERR_SENSOR, can_rx에서 버려지면 ERR_COMM, arbiter 조건 미달이면 오류가 아니라 정상 억제. 이 매핑을 흐리면 HMI가 "센서 오류"와 "통신 오류"를 구분할 수 없다.


10. 디렉터리 구조 제안

vcs/
├── common/                     ← 전 S32K 노드가 공유 (git submodule 또는 심볼릭)
│   ├── svc/  trust.[ch] fault.[ch] req.[ch] sched.[ch]
│   ├── com/  can_if.[ch] e2e.[ch] heartbeat.[ch] signals.h
│   └── inc/  vcs_types.h        ← func_id_t · err_kind_t · 공용 열거형
│
├── node_central/               S32K344-WB
│   ├── app/{arbiter,vss,power,hmi_link}/
│   ├── drv/  hal/
│   └── cfg/  trust_config.h  can_map.h
│
├── node_window/                S32K144
├── node_function/              S32K144
├── node_sensing/               S32K144
│
├── node_vision/                Raspberry Pi 3 B (Python)
├── node_hmi/                   ESP32 (ESP-IDF)
│
├── interface/                  ← 노드 간 계약. 여기가 바뀌면 전원 재빌드
│   ├── signals.yaml            신호 정의 (10편)
│   ├── can_matrix.md           CAN ID · 주기 · 송수신
│   └── uart_vision.md          Sensing ↔ RPi 프레임 (TBD-003)
│
└── docs/
    ├── SR_v1.0.pdf
    ├── traceability.md         요구사항 → 모듈 → 시험 항목 (11편)
    └── tbd_status.md           TBD 32건 진행 상황

common/interface/를 최상위에 둔 게 핵심이다.

  • common/공통 패턴의 단일 구현을 보장한다. 노드마다 trust를 따로 짜면 max_age 판정이 미묘하게 달라지고, 그 차이는 통합 시험에서야 드러난다.
  • interface/노드 간 계약이다. 여기 파일이 바뀌면 관련 노드를 전부 다시 빌드·시험해야 한다는 신호가 된다. §9의 변경 영향 분석이 디렉터리 구조로 표현된 셈.

11. 한 줄 요약

항목한 줄
전체 구조판단은 Central로 모이고 실행은 아래로 흩어진다. 단 Anti-pinch만 로컬 폐루프
계층app / svc / com / drv / hal — 5층을 전 노드에 공통 적용
svc/trust유효성 3축의 단일 창구. feedeval을 분리해야 age가 제대로 늙는다
svc/fault기능 × 오류종류 2차원. 전역 단일 플래그는 §6.6 구조적 위반
svc/req요청 생애주기 6단계 + 공용 거부 사유. HMI가 기능별 테이블을 안 들게
상태 소유권원본은 하나. 사본은 읽기 전용이고 항상 trusted_t로 감싼다
태스크 주기모든 max_age는 생산 태스크 주기의 2~3배 이상 — 표를 나란히 놓고 검산
디렉터리common/(공통 구현)과 interface/(노드 간 계약)를 최상위로

설계 판단 체크포인트

구현에 들어가기 전에 팀이 답을 갖고 있어야 하는 것들이다. 답이 안 나오면 그 자리가 곧 설계 문의 또는 TBD 후보다.

  1. proximity.c(거리→단계 변환)를 Sensing에 둘 것인가 Central에 둘 것인가? Sensing에 두면 센서 특성과 밀착해 튜닝이 쉽고 CAN 부하가 준다. Central에 두면 "판단은 Central" 원칙이 깨끗하다. 이 노트는 Sensing 안을 택했지만, 거리 원값도 함께 올려보내는 것을 전제로 한다 — 그래야 Central이 VSS 2단계 매핑을 독자적으로 할 수 있다(03편 §3-4).

  2. common/을 git submodule로 할 것인가, 단일 저장소의 공유 디렉터리로 할 것인가? submodule은 노드별 버전 고정이 되지만 동기화 비용이 크다. 단일 저장소는 항상 최신이지만 한 노드의 변경이 전원에게 즉시 파급된다. 팀 규모가 작으면 단일 저장소 + 공유 디렉터리가 낫다.

  3. 초음파 max_age(300 ms)와 순차 발신 한 바퀴(센서 4개 × 60 ms = 240 ms)의 여유가 60 ms뿐이다. 어떻게 할 것인가? 선택지는 (a) 센서 수를 줄인다 (b) 그룹 동시 발신으로 한 바퀴를 줄인다 (c) max_age를 늘린다 (d) 센서별로 독립된 age를 관리한다. (d)가 가장 정확하다 — 각 센서는 자기 차례에 갱신되므로 개별 age를 갖는다. TBD-013·TBD-022를 함께 정할 때 이 계산을 근거로 써야 한다.

  4. safety.cposition.c에 의존하는데, 위치를 신뢰할 수 없으면 Anti-pinch가 성립하는가? 02편 체크포인트 4번과 같은 질문이고, TBD-009의 감지 방식에 달려 있다. 전류+속도만으로 감지 가능하게 설계하면 safety.cposition.c 없이도 동작하고, 그러면 §6.2.2의 "Anti-pinch를 사용할 수 없는 경우 닫힘 금지"가 발동할 상황이 줄어든다. 감지 방식 선택이 기능 가용성을 바꾼다.

  5. 차량 전원 상태(TBD-027)의 원본을 Central로 잠정 배치했다. ESP32 데모 입력으로 하면 무엇이 달라지나? ESP32가 원본이면 Central은 사본을 갖게 되고, TBD-001 통신이 끊기면 Central이 전원 상태를 모른다. 그런데 §6.1.1의 ROA 사이렌은 "차량 전원 상태와 관계없이" 울려야 하므로 그건 괜찮지만, 초음파 경고의 "시동 상태" 조건과 야간 감쇄 조건이 판정 불가가 된다. Central을 원본으로 두는 쪽이 통신 의존을 줄인다.

  6. audio_dma_isr를 최고 우선순위에 두면 pinch_isr은 어떻게 되나?ISR은 다른 칩에 있다 — 오디오는 S32K344, Anti-pinch는 S32K144. 경합하지 않는다. 이게 00편에서 본 "물리적 분리가 주는 이득"의 실제 사례다. 만약 두 기능이 한 칩에 있었다면 우선순위 설계가 훨씬 까다로웠을 것이다.

요구사항 관리 — SR 작성 원칙·TBD 32건·리뷰 결과노드 간 인터페이스 — 신호 목록·CAN 프레임·E2E·버스 부하