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

10. 노드 간 인터페이스 — 신호 목록과 프레임 설계 초안

⚠️ SR에 없는 내용이다. §2가 "네트워크 프레임의 상세 Byte/Bit 배치 … 는 별도의 네트워크 인터페이스 명세서에서 정의한다"고 미뤄 둔 부분을, 요구사항에서 역산해 만든 초안이다. TBD-001·002·003이 확정되면 여기부터 고친다. 09편의 interface/ 디렉터리에 들어갈 내용이다.


1. 왜 인터페이스를 먼저 정하는가

노드가 6개다. 각자 만들다가 나중에 붙이면 반드시 어긋난다. 인터페이스는 코드보다 먼저 합의돼야 하는 계약이다.

그리고 이 시스템에서는 인터페이스가 곧 요구사항이다. 예를 들어 §6.5.3의 "센서 오류, 통신 오류 및 기능 자체 오류를 구분하여 표시"는 — 프레임에 오류 종류 필드가 없으면 애초에 불가능하다. 신호 목록을 짜면서 요구사항을 하나씩 대조하면 빠진 게 드러난다.

다이어그램 로딩 중…

2. 물리 계층 — 무엇이 무엇에 붙나

링크방식상태근거
Central ↔ Power WindowCAN (FD 여부 미정)TBD-002§4.1 그림 1
Central ↔ FunctionCANTBD-002
Central ↔ SensingCANTBD-002
Central ↔ ESP32미정UART / CAN / Wi-FiTBD-001§4.1 "물리 통신 TBD"
Sensing ↔ Raspberry Pi미정 — UART / SPITBD-003§4.1 "UART/SPI TBD"
엔진룸 카메라 ↔ ?미정 — Sensing 경유 or 별도 Vision 경로TBD-033§8

2-1. TBD-001을 CAN으로 정할 때의 이점과 제약

이점

  • 프로토콜이 하나로 통일된다. 신호 정의·E2E·heartbeat 코드를 그대로 재사용.
  • 브로드캐스트가 공짜다. Central이 상태를 한 번 쏘면 ESP32도 같이 받는다. 07편 §6-2의 "이벤트 + 주기 재전송"이 자연스럽다.
  • 노이즈 내성이 UART보다 훨씬 낫다(차동 신호).

제약

  • 07편 §6-1에서 본 대로 채택할 ESP32 변종의 TWAICAN FD를 지원하는지 먼저 확인해야 한다. 지원하지 않는데 TBD-002에서 FD를 택하면 붙일 수 없다.
  • CAN 트랜시버 칩이 ESP32 보드에 추가로 필요하다.

Classic CAN으로 통일하는 안이 가장 단순하다. 이 프로토타입에서 FD가 필요할 만큼 데이터가 많지 않다 — 아래 신호 목록을 보면 전 노드 합산 부하가 낮다.

2-2. TBD-003 — UART vs SPI

UARTSPI
배선2선 (TX/RX)4선 (SCK/MOSI/MISO/CS)
속도~1 Mbps 실용수~수십 Mbps
마스터/슬레이브대칭 — RPi가 자발적으로 보낼 수 있다마스터(S32K)가 물어봐야 슬레이브(RPi)가 답한다
프레이밍직접 구현 (SOF·길이·CRC)직접 구현
RPi 지원안정적안정적 (SPI 슬레이브는 까다로움)

비전 결과는 4~8바이트에 불과하다(03편 §4-3). 속도가 전혀 문제되지 않으므로 UART가 낫다. 결정적 이유는 방향성이다 — 엔진룸 동물 진입은 RPi가 판정하는 즉시 올려보내야 하는 이벤트인데, SPI 슬레이브는 마스터가 물어볼 때까지 기다려야 한다. 폴링 주기만큼 지연이 붙는다.


3. 신호 목록 — 요구사항에서 역산

09편 §7의 상태 소유권 표를 신호로 펼친 것이다. 송신 노드 = 원본 노드다.

3-1. Sensing → Central (그리고 Function·ESP32가 함께 수신)

신호타입범위 · 단위주기근거
luxu170~100000 lux100 ms§6.3.1
lux_trustu3trust_t§6.3.4
temp_c10s12−400~850 (0.1 °C)100 ms§6.3.1
temp_trustu3
humid_p10u100~1000 (0.1 %RH)100 ms§6.3.1
humid_trustu3
dist_mmu1320~5000 mm50 ms§6.3.1
dist_trustu3
prox_levelu30~5 단계§6.3.3 · TBD-013
prox_risku1근접 위험 상태§6.3.3
occupant_presentu1200 ms§6.3.1
occupant_countu40~15§6.3.1
occupant_trustu3§6.3.4
over_capacityu1정원 초과§6.3.2
animal_presentu1엔진룸 대상 동물200 ms§6.3.5
animal_confu80~255 신뢰도§6.3.5 · TBD-030
animal_trustu3§6.3.5
sensing_faultu8센서별 오류 비트맵200 ms§6.6

*_trust 필드가 값마다 따라붙는 게 핵심이다. 09편 규칙 2 — 사본은 항상 trusted_t로 감싼다 — 를 프레임 레벨에서 보장한다. 값만 보내고 유효성을 안 보내면 수신 측이 §6.5.3을 만족할 방법이 없다.

trust_t를 3비트로 보내는 이유: TRUST_OK / OUT_OF_RANGE / STALE / RECOVERING / NO_DATA 5값이라 3비트면 충분하고, 수신 측이 §6.5.3의 "센서 오류 vs 통신 오류" 구분을 할 수 있다. bool valid 1비트로 줄이면 왜 무효인지가 사라진다.

3-2. Power Window → Central

신호타입주기근거
win_stateu47상태 (완전열림/완전닫힘/열림중/닫힘중/중간정지/Anti-pinch/오류)이벤트 + 200 ms§6.2.6
win_pos_p10u100~1000 (0.1 % 개도)§6.2.3
win_trustu3위치 신뢰도§6.2.6
pinch_eventu1보호 동작 발생이벤트 즉시§6.2.4
pinch_doneu1보호 동작 완료이벤트 즉시§6.2.4
pinch_latchedu1재개 금지 래치200 ms§6.2.4
antipinch_availu1Anti-pinch 사용 가능200 ms§6.2.2
req_id / req_state / req_rejectu16/u3/u4요청 생애주기이벤트§6.2.6 · §6.5.1
pw_faultu8오류 종류 · 세부200 ms§6.6

pinch_event는 주기 전송을 기다리지 않는다. 01편 §5 시퀀스에서 봤듯 경고음이 최대한 빨리 나야 하므로 이벤트 즉시 송신하고, CAN ID도 낮게(높은 우선순위) 배정한다.

antipinch_avail이 별도 신호인 이유: §6.2.2의 "Anti-pinch 보호 기능을 사용할 수 없는 경우 닫힘 동작을 시작하지 않아야 한다"를 Central·HMI가 미리 알아야 한다. 사용자가 닫힘 버튼을 눌렀을 때 비로소 거부하는 것보다, 버튼을 비활성화해 보여주는 게 낫다.

3-3. Function → Central

신호타입주기근거
door_locku2LOCKED / UNLOCKED / UNKNOWN이벤트 + 200 ms§6.4.1
door_ajaru2CLOSED / OPEN / UNKNOWN§6.4.1
door_reportu35상태 (정상3 + INCONSISTENT + UNTRUSTED)§6.4.1
fan_owneru3NONE / AUTO / MANUAL / PRECOND200 ms§6.4.2
fan_requ2OFF/LOW/MED/HIGH (요구)§6.4.2
fan_actu2OFF/LOW/MED/HIGH (실제)§6.4.2
precond_stateu2IDLE / RUNNING / ENDED§6.4.2
vent_stateu3자동 환기 동작 상태§6.4.2
vent_completeu2정상완료 / 미완료(닫힘 거부)이벤트§6.4.2 ★
amb_activeu3현재 표시 종류 6종200 ms§6.4.3
amb_dutyu70~100 %§6.4.3
fn_faultu16기능별 × 오류종류200 ms§6.6

vent_complete가 05편 §5-3의 요구사항을 신호로 만든 것이다. "닫힘이 거부되어 정상 완료가 아님"을 상위로 전달할 통로가 없으면 그 요구사항은 구현될 수 없다. 신호 목록을 짜면서 발견되는 전형적인 누락이다.

3-4. Central → 각 실행 노드

신호수신주기근거
veh_power전 노드OFF/ACC/ON/START100 ms§5 · TBD-027
win_cmdPower WindowOPEN/CLOSE/STOP + 출처 + one_touch이벤트§6.2.1
win_cmd_sequ8 순환 카운터§6.2.5 유령 명령 방지
lock_cmdFunctionLOCK/UNLOCK + 출처이벤트§6.4.1
climate_setFunctionenable + target_c10이벤트 + 1 s§6.4.2
fan_cmdFunction수동 출력 수준이벤트§6.4.2
vent_enableFunction자동 환기 사용 설정이벤트 + 1 s§6.4.2
amb_setFunctionenable + brightness이벤트 + 1 s§6.4.3
amb_safetyFunctionWarning/Fault/Emergency 요구이벤트 즉시§6.4.3
vision_triggerSensing → RPi엔진룸 판정 트리거이벤트§6.3.5 · TBD-036

설정성 신호(climate_set, vent_enable, amb_set)에 1초 주기 재전송을 붙인 이유: 이벤트만 보내면 그 프레임이 유실됐을 때 노드 간 설정이 영구히 어긋난다. 07편 §6-2의 "이벤트 + 주기"를 설정에도 적용한 것이고, 주기가 느린 건 자주 안 바뀌기 때문이다.

3-5. ESP32 ↔ Central

방향신호근거
ESP32 → Centraluser_req (기능 + 명령 + req_id)§6.5.1
ESP32 → Centraldk_authenticated, dk_prox_state, dk_departed§6.4.1 · TBD-016
ESP32 → Centralmobile_authorized (인증 + 차량별 권한)§6.5.2
ESP32 → Centraluser_settings (무음, 야간, 목표온도, 밝기 …)§6.1.3 · §6.4.2,3
Central → ESP32위 3-1~3-3의 모든 상태 신호 (중계 또는 브로드캐스트 수신)§6.5.3
Central → ESP32req_result (req_id + state + reject)§6.5.1
Central → ESP32fault_summary (기능 × 오류종류)§6.5.3 · §6.6
Central → ESP32alert_event (ROA, 동물 진입 등 중요 경고)§6.3.5 · §6.5.4

alert_event가 상태와 별개인 이유: §6.5.4의 "사용자가 확인하지 않은 중요 경고가 존재하는 경우 해당 경고 상태를 확인할 수 있도록 해야 한다"는 이벤트 이력을 요구한다. 현재 상태만 보내면 "잠깐 발생했다 사라진 경고"가 사라진다.


4. CAN 프레임 배치 초안

4-1. ID 배정 원칙

CANID가 작을수록 아비트레이션에서 이긴다. 그래서 ID 배정이 곧 우선순위 설계다.

ID 대역용도
0x000~0x0FF안전 이벤트 — 즉시성이 요구되는 것0x010 pinch_event, 0x020 amb_safety, 0x030 alert_event
0x100~0x1FF제어 명령0x110 win_cmd, 0x120 lock_cmd, 0x130 fan_cmd
0x200~0x2FF고빈도 상태 (50~100 ms)0x210 sensing_fast, 0x220 window_state
0x300~0x3FF저빈도 상태 (200 ms~1 s)0x310 sensing_slow, 0x320 function_state
0x400~0x4FF설정0x410 climate_set, 0x420 amb_set
0x500~0x5FFheartbeat · 진단0x510~0x513 노드별 heartbeat

pinch_event0x010인 게 요구사항 대응이다. §6.1.2의 "안티핀치 비상 경고음을 다른 음향보다 최우선"을 만족하려면, 그 이벤트가 CAN 버스에서도 최우선이어야 한다. 버스가 혼잡할 때 다른 프레임에 밀리면 경고음이 늦는다.

4-2. 프레임 예시 — 0x210 sensing_fast (8 byte, 50 ms)

byte  bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
 0    [        dist_mm [12:5]                ]
 1    [ dist_mm [4:0]      ][ dist_trust     ]
 2    [ prox_level  ][prox_risk][ reserved   ]
 3    [        temp_c10 [11:4]                ]
 4    [ temp_c10[3:0]      ][ temp_trust     ]
 5    [ seq_counter (0~255)                   ]
 6    [ e2e_crc8 [7:0]                        ]
 7    [ alive_counter[3:0] ][ data_id[3:0]    ]
필드크기역할
seq_counter8b중복·역순 검출 (§6.2.5)
e2e_crc88b애플리케이션 레벨 무결성 — CAN 자체 CRC와 별개
alive_counter4b송신 노드가 살아 있는지 (프레임은 오는데 값이 안 바뀌는 경우 검출)
data_id4b프레임 종류 식별 — ID 오배선/오설정 검출

alive_counterseq_counter를 둘 다 두는 이유가 헷갈릴 수 있다.

  • seq_counter: 이 데이터가 새 것인가 — 같은 값이 재전송된 건지 새 측정인지
  • alive_counter: 송신 태스크가 도는가 — 태스크가 멈춰 DMA가 옛 버퍼를 계속 쏘는 경우 검출

두 번째가 실제로 잡기 어려운 고장이다. 송신 노드의 애플리케이션이 hang 됐는데 CAN 컨트롤러가 마지막 프레임을 자동 재전송하면, 수신 측은 정상 프레임을 계속 받는다. alive_counter가 안 변하는 것으로만 알 수 있다.

4-3. E2E 보호를 왜 또 하는가

CAN은 이미 15비트 CRC를 갖고 있다. 그런데 왜 애플리케이션 CRC를 또 넣나?

CAN CRC는 버스 구간만 보호한다.

[송신 앱] → [송신 버퍼] → [CAN 컨트롤러] ══CRC 보호══ [CAN 컨트롤러] → [수신 버퍼] → [수신 앱]
     ↑            ↑                                                    ↑          ↑
   여기서       여기서                                              여기서    여기서
   깨지면       깨지면                                              깨지면    깨지면
   CAN CRC 가 못 잡는다 ─────────────────────────────────────────────────────────┘

메모리 손상, DMA 오설정, 버퍼 인덱스 버그, 신호 인코딩 실수 — 전부 CAN CRC 범위 밖이다. E2E는 앱에서 앱까지를 보호한다. ISO 26262가 요구하는 이유이고, §6.2.5의 "의도하지 않은 윈도우 구동이 발생하지 않아야 한다"를 진지하게 구현하려면 필요하다.

/* com/e2e.c — 송신 */
void e2e_protect(uint8_t *data, uint8_t len, uint8_t data_id, e2e_ctx_t *ctx)
{
    data[len - 3] = ctx->seq++;                     /* seq_counter */
    data[len - 1] = (uint8_t)((ctx->alive++ & 0x0F) | (data_id << 4));
    data[len - 2] = crc8_sae_j1850(data, len - 2);  /* CRC 자리 제외하고 계산 */
}

/* com/e2e.c — 수신 */
e2e_result_t e2e_check(const uint8_t *d, uint8_t len, uint8_t data_id, e2e_ctx_t *ctx)
{
    if (d[len - 2] != crc8_sae_j1850(d, len - 2))       return E2E_CRC_ERR;
    if (((d[len - 1] >> 4) & 0x0F) != data_id)          return E2E_WRONG_ID;

    uint8_t alive = d[len - 1] & 0x0F;
    if (alive == ctx->last_alive)                        return E2E_STALE_ALIVE;  /* 태스크 정지 */
    ctx->last_alive = alive;

    uint8_t delta = (uint8_t)(d[len - 3] - ctx->last_seq);
    if (delta == 0)                                      return E2E_REPEATED;
    if (delta > E2E_MAX_SEQ_JUMP)                        return E2E_LOST;         /* 프레임 유실 */
    ctx->last_seq = d[len - 3];
    return E2E_OK;
}

반환값이 bool이 아니라 열거형인 게 §6.5.3 대응이다. E2E_CRC_ERR(전기적 문제)와 E2E_STALE_ALIVE(송신 노드 hang)는 사용자에게 다른 오류로 보여야 한다.


5. Heartbeat와 타임아웃 — TBD-020

5-1. 무엇을 감시하나

감시 대상방법실패 시
노드가 살아 있는가전용 heartbeat 프레임 (0x510~)해당 노드 관련 전 신호를 ERR_COMM
특정 신호가 갱신되는가신호별 max_age (svc/trust)그 신호만 TRUST_STALE
송신 태스크가 도는가alive_counterERR_COMM (원인 세부: 태스크 정지)

세 층이 다른 고장을 잡는다. heartbeat만 있으면 "노드는 살아 있는데 센서 태스크만 죽은" 경우를 못 잡고, max_age만 있으면 "값이 안 변할 뿐 정상인" 신호(도어 상태 등)를 오탐한다.

5-2. 주기와 타임아웃 결정

heartbeat 주기      = 100 ms          (제안)
heartbeat 타임아웃  = 350 ms          (3회 연속 유실 + 여유)
신호별 max_age      = 생산 주기 × 2~3  (09편 §8 검산)
복구 판정           = 연속 N회 정상    (TBD-038)

타임아웃 = 주기 × 3 + α가 관행이다. 2배면 한 번 유실 + 지터로 오탐이 나고, 5배면 검출이 너무 늦다.

/* com/heartbeat.c */
#define HB_PERIOD_MS    100u
#define HB_TIMEOUT_MS   350u
#define HB_RECOVER_N      3u    /* TBD-038 — 진입은 1회, 복구는 3회 */

typedef struct {
    uint32_t last_rx_ms;
    uint8_t  ok_streak;
    bool     alive;
} hb_node_t;

static hb_node_t g_hb[NODE_COUNT];

void hb_on_rx(node_id_t n) {
    g_hb[n].last_rx_ms = hal_now_ms();
    if (g_hb[n].ok_streak < 255) g_hb[n].ok_streak++;
    if (!g_hb[n].alive && g_hb[n].ok_streak >= HB_RECOVER_N) {
        g_hb[n].alive = true;
        fault_clear(node_to_func(n), ERR_COMM);
        /* §6.4.4 — 복구했다고 자동 재개하지 않는다. 재평가만 요청 */
        arbiter_request_reevaluate(node_to_func(n));
    }
}

void hb_tick(void) {   /* 10 ms 주기 */
    for (node_id_t n = 0; n < NODE_COUNT; n++) {
        if (g_hb[n].alive && (hal_now_ms() - g_hb[n].last_rx_ms) > HB_TIMEOUT_MS) {
            g_hb[n].alive = false;         /* 진입은 즉시 */
            g_hb[n].ok_streak = 0;
            fault_set(node_to_func(n), ERR_COMM, ERRD_HEARTBEAT_LOST);
        }
    }
}

arbiter_request_reevaluate()가 §6.4.4의 구현이다. 복구 시 바로 동작을 재개하지 않고 "다시 판단해라"만 요청한다. 05편 §8-1의 흐름도가 이 한 줄로 표현된다.


6. Sensing ↔ Raspberry Pi UART 프레임 — TBD-003

6-1. 프레임 포맷

+------+------+------+----------+------+------+
| 0xAA | 0x55 | LEN  | PAYLOAD  | CRC16 (LE)  |
+------+------+------+----------+------+------+
  SOF1   SOF2   1B     LEN B       2B

2바이트 SOF를 쓰는 이유: UART는 프레임 경계가 없다. 1바이트 SOF면 페이로드 안의 우연한 0xAA에 동기가 잘못 걸린다. 2바이트면 확률이 크게 준다. 그래도 완전하지 않으므로 CRC 실패 시 1바이트 밀고 재동기해야 한다.

6-2. 페이로드 — 비전 결과 (RPi → Sensing)

/* interface/uart_vision.h — 양쪽이 같은 헤더를 본다 */
#define VIS_MSG_RESULT   0x01

typedef struct __attribute__((packed)) {
    uint8_t  msg_type;        /* VIS_MSG_RESULT */
    uint8_t  flags;           /* bit0 occupant_present
                                 bit1 animal_present
                                 bit2 occupancy_valid
                                 bit3 animal_valid
                                 bit4 over_capacity
                                 bit5~7 reserved */
    uint8_t  occupant_count;  /* 0~15 */
    uint8_t  occupancy_conf;  /* 0~255 */
    uint8_t  animal_conf;     /* 0~255 — TBD-030 */
    uint8_t  quality_code;    /* 0=ok, 1=too_dark, 2=too_bright, 3=blurred, 4=saturated */
    uint16_t age_ms;          /* 이 판정의 나이 — TBD-022 */
    uint8_t  alive;           /* 추론 루프가 도는지 */
} vis_result_t;               /* 9 byte — 영상 대비 수십만 분의 1 */

이 구조체에 이미지 데이터가 없다는 것이 §6.3.4 개인정보 요구사항 충족의 증거다. 코드 리뷰에서 확인 가능한 형태로 요구사항이 구현에 박혀 있다.

quality_code를 따로 보내는 이유: *_valid 비트만 보내면 "왜 무효인지"를 모른다. §6.5.3이 오류 종류 구분을 요구하므로, 렌즈 오염(blurred)과 야간 저조도(too_dark)를 사용자에게 다르게 보여줄 수 있어야 한다.

6-3. 트리거 (Sensing → RPi)

#define VIS_MSG_TRIGGER  0x02

typedef struct __attribute__((packed)) {
    uint8_t msg_type;         /* VIS_MSG_TRIGGER */
    uint8_t target;           /* bit0 occupancy, bit1 engine_bay */
    uint8_t reason;           /* 0=periodic, 1=ignition_attempt, 2=door_open — TBD-036 */
} vis_trigger_t;

reason을 보내는 이유는 RPi가 추론 파라미터를 다르게 할 수 있게 하기 위해서다. 시동 시도 트리거면 지연이 급하므로 낮은 해상도로 빨리, 주기 트리거면 배터리를 아끼되 정확하게.


7. interface/signals.yaml — 단일 정의 파일

신호를 코드에 직접 쓰면 노드마다 어긋난다. 하나의 정의 파일에서 각 노드용 헤더를 생성하는 게 정석이다.

# interface/signals.yaml (발췌)
frames:
  - name: sensing_fast
    id: 0x210
    dlc: 8
    period_ms: 50
    tx: sensing
    rx: [central, function, hmi]
    signals:
      - { name: dist_mm,    start: 0,  size: 13, type: u, min: 20,   max: 5000, unit: mm }
      - { name: dist_trust, start: 13, size: 3,  type: enum, values: [OK, RANGE, STALE, RECOVER, NODATA] }
      - { name: prox_level, start: 16, size: 3,  type: u, min: 0, max: 5 }
      - { name: prox_risk,  start: 19, size: 1,  type: bool }
      - { name: temp_c10,   start: 24, size: 12, type: s, min: -400, max: 850, unit: "0.1degC" }
      - { name: temp_trust, start: 36, size: 3,  type: enum }
    e2e: { seq: 40, crc: 48, alive: 56, data_id: 0x1 }

  - name: window_state
    id: 0x220
    dlc: 8
    period_ms: 200
    event: true
    tx: power_window
    rx: [central, hmi]
    signals:
      - { name: win_state,       start: 0,  size: 4, type: enum,
          values: [FULL_OPEN, FULL_CLOSED, OPENING, CLOSING, MID_STOPPED, ANTIPINCH, ERROR] }
      - { name: win_pos_p10,     start: 4,  size: 10, type: u, min: 0, max: 1000 }
      - { name: win_trust,       start: 14, size: 3, type: enum }
      - { name: pinch_latched,   start: 17, size: 1, type: bool }
      - { name: antipinch_avail, start: 18, size: 1, type: bool }

빌드 시 이 파일에서 signals.h(인코딩·디코딩 함수)를 생성하면:

  • 노드 간 배치 불일치가 원천 차단된다
  • data_id가 자동 부여되어 오배선 검출이 공짜로 따라온다
  • period_ms를 읽어 max_age 검산 스크립트를 만들 수 있다 (09편 §8 경고)
# tools/check_age.py — max_age 가 생산 주기보다 충분히 큰지 검산
import yaml
CFG = yaml.safe_load(open("interface/signals.yaml"))
TRUST = yaml.safe_load(open("interface/trust_config.yaml"))
for f in CFG["frames"]:
    for s in f["signals"]:
        age = TRUST.get(s["name"], {}).get("max_age_ms")
        if age is None:
            continue
        if age < f["period_ms"] * 2:
            print(f"경고 {s['name']}: max_age {age}ms < 주기 {f['period_ms']}ms × 2 — 항상 stale 위험")

이 스크립트를 CI에 넣으면 09편에서 지적한 모순이 자동으로 잡힌다.


8. 버스 부하 개산

Classic CAN 500 kbps 기준, 표준 ID 8바이트 프레임은 스터핑 포함 대략 130 비트 ≈ 0.26 ms다.

프레임주기초당 프레임초당 비트
sensing_fast (0x210)50 ms202,600
sensing_slow (0x310)200 ms5650
window_state (0x220)200 ms5650
function_state (0x320)200 ms5650
veh_power (0x2xx)100 ms101,300
heartbeat × 4100 ms405,200
설정 3종1 s3390
이벤트 (여유)~202,600
합계~108~14,000 bps

500 kbps 대비 약 2.8 %. 대단히 여유롭다.

이 계산의 함의:

  1. CAN FD가 전혀 필요 없다. TBD-002에서 Classic CAN을 택해도 부하 문제가 없고, 그러면 TBD-001에서 ESP32를 CAN에 붙이는 선택지가 살아난다(07편 §6-1).
  2. 250 kbps로 낮춰도 5.6 %다. 배선 길이 여유를 벌 수 있다 — 비트레이트가 낮을수록 버스를 길게 뽑을 수 있다.
  3. 이벤트 프레임에 큰 여유가 있으므로, pinch_event 같은 안전 이벤트가 지연될 걱정이 없다.

다만 이건 정상 상태 계산이다. 고장 시 오류 프레임이 폭주하거나, 버스 오프 복구 반복으로 부하가 치솟는 시나리오는 별도 검토가 필요하다. 그래서 TBD-002가 "트랜시버, 종단 및 메시지 정책 확정"까지 포함한다.


9. 한 줄 요약

항목한 줄
물리 계층Classic CAN 통일이 가장 단순. Vision 링크는 이벤트 방향성 때문에 UARTSPI보다 낫다
신호 목록모든 값에 *_trust가 따라붙는다. 유효성 없는 값은 §6.5.3을 만족할 수 없다
누락 발견vent_complete, antipinch_avail 같은 신호는 요구사항 역산으로만 나온다
CAN IDID가 곧 우선순위. pinch_event0x010에 둬야 §6.1.2가 버스에서도 성립
E2ECAN CRC는 버스 구간만 보호. 앱-앱 보호는 별도. alive_counter가 태스크 정지를 잡는다
Heartbeat노드·신호·태스크 3층 감시. 타임아웃은 주기 × 3 + α. 복구는 재평가 요청까지만
부하500 kbps에서 약 2.8 % — FD 불필요. 이게 TBD-002를 Classic으로 밀 근거
관리signals.yaml 단일 정의 → 헤더 생성 + max_age 검산 스크립트를 CI에

설계 판단 체크포인트

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

  1. 버스 부하가 2.8 %면 CAN FD를 안 쓸 근거가 충분한가? 충분하다. 다만 확인할 것은 향후 확장이다. 카메라 메타데이터를 더 보내거나 진단 로그를 CAN으로 올릴 계획이 있으면 다시 계산해야 한다. 현재 SR 범위(§7.2에서 AUTOSAR·양산 제외)에서는 Classic으로 충분하다.

  2. *_trust 필드를 매 신호마다 붙이면 대역폭이 아깝지 않은가? 3비트 × 신호 수다. 위 부하 계산에 이미 포함돼 있고 2.8 %다. 반대로 이걸 빼면 §6.3.4·§6.5.3·§6.4.4를 구현할 방법이 없어져 요구사항 미충족이 된다. 비용 대비 명백히 남는 거래다.

  3. alive_counterheartbeat 중 하나만 두면 안 되나? 안 된다. heartbeat는 "노드가 프레임을 보내는가", alive_counter는 "그 데이터를 만드는 태스크가 도는가"를 본다. 센서 태스크만 hang 되고 통신 태스크는 도는 고장에서 heartbeat는 정상으로 보인다. 실제로 흔한 고장 모드다.

  4. signals.yaml 코드 생성을 도입할지, 헤더를 손으로 관리할지? 노드가 6개고 신호가 50개 이상이면 손 관리는 반드시 어긋난다. 다만 생성기를 만드는 비용이 있으므로, 최소한 interface/ 디렉터리의 헤더 하나를 전 노드가 공유하는 것이 하한선이다. 노드마다 복사본을 두는 것만은 피해야 한다.

  5. prox_level(단계)과 dist_mm(원값)을 둘 다 보내는 게 중복 아닌가? 중복이지만 의도적이다. 03편 §3-4에서 봤듯 VSS는 2단계, HMI는 5단계, 그리고 거리 비례 경고음 간격에는 원값이 필요하다. 단계만 보내면 Central이 VSS 매핑을 독자적으로 못 하고, 원값만 보내면 단계 히스테리시스를 두 곳에서 각각 구현하게 된다.

  6. Vision UART에서 CRC 실패 시 1바이트 밀고 재동기하는 방식의 한계는? 페이로드 안에 우연히 0xAA 0x55가 들어 있으면 잘못된 경계에서 반복 실패할 수 있다. 근본 해법은 **바이트 스터핑(COBS 등)**으로 페이로드에서 SOF 바이트를 제거하는 것이다. 프레임이 9바이트로 작으므로 스터핑 오버헤드도 미미하다. 재동기 실패가 관측되면 이쪽으로 가야 한다.

  7. veh_power의 송신 노드가 Central인데, TBD-027에서 ESP32 입력으로 정해지면? 신호 방향이 뒤집힌다(ESP32 → Central → 전 노드 중계, 또는 ESP32가 직접 브로드캐스트). 09편 체크포인트 5번에서 봤듯 통신 의존이 늘어나므로, Central을 원본으로 유지하고 ESP32는 입력만 전달하는 형태를 권한다. 이렇게 하면 TBD-027이 어느 쪽으로 정해져도 이 프레임 정의는 안 바뀐다.

소프트웨어 구조 — 계층·모듈·상태 소유권·태스크 주기구현 순서와 검증 — 마일스톤·벤치 실험·시험 항목 추적