Zephyr v3.7.0 · CAN Controller & ISO-TP

CAN 한 프레임과 ISO-TP 한 메시지는 어디서 갈라질까?

CAN controller는 ID·DLC·payload로 이루어진 프레임을 버스에 송수신하고 필터로 관심 프레임을 고릅니다. 더 긴 메시지의 분할과 상대의 전송 속도 조절은 ISO-TP가 맡습니다. 둘을 한 함수의 성공·실패로 뭉치면 timeout과 버퍼 수명을 잘못 읽기 쉽습니다.

본문 기준: Zephyr 3.7.0 LTS3 · 이후 유지보수 패치는 별도입니다.

기준은 Zephyr v3.7.0 태그다. 이 글의 controller API는 <zephyr/drivers/can.h>, ISO-TP API는 <zephyr/canbus/isotp.h>를 뜻한다. 기존 Networking 글의 IP·socket 흐름과 달리 CAN controller의 직접 프레임 API와 ISO-TP context에 초점을 둔다. 그림은 원본 코드의 경계를 새로 구성한 설명도이며 특정 보드의 측정 결과가 아니다.

1. CAN ID로 고르는 프레임, ISO-TP 주소로 조립하는 메시지

struct can_frame은 표준 11비트 또는 확장 29비트 ID, DLC, flags와 payload를 담는다. 클래식 CAN의 프레임 payload는 최대 8바이트이고, CAN FD는 빌드 설정과 controller 지원이 함께 있을 때 최대 64바이트다. CAN 필터는 ID·mask·IDE 조건으로 개별 수신 프레임을 고른다. ISO-TP는 그 프레임 payload 안에 PCI를 두고 Single Frame(SF) 또는 First Frame(FF)·Consecutive Frame(CF)·Flow Control(FC)로 한 메시지를 주고받는다.

그림 1. 두 계층이 처리하는 단위와 책임

↔ 좁은 화면에서는 왼쪽 애플리케이션에서 오른쪽 버스까지 이동하세요.

애플리케이션

의미 있는 한 payload를 보내거나 읽는다.

직접 CAN: 한 can_frame씩 다룸ISO-TP: isotp_send / isotp_recv로 메시지 다룸

ISO-TP · 선택 계층

길이에 따라 SF 또는 FF·CF로 분할하고, 수신 측 FC의 BS·STmin을 따른다.

RX/TX 주소 쌍과 context 유지필요한 CAN 필터를 내부에서 설치

CAN controller + 버스

한 프레임의 송신 완료와 ID·mask 수신 필터를 처리한다.

can_send → TX mailbox → 버스버스 → RX filter → callback 또는 msgq
표 1. CAN 프레임과 ISO-TP 메시지
질문CAN controller 직접 APIISO-TP
애플리케이션의 단위한 can_frame분할될 수 있는 한 payload
수신 선택can_filter의 ID·mask·IDERX/TX 주소 쌍의 context가 필요한 filter를 설치
긴 payload호출자가 프레임 단위 정책을 직접 마련SF 또는 FF/FC/CF 상태 기계가 분할·조립
완료의 의미프레임 전송/오류ISO-TP 상태 기계의 완료/오류. v3.7.0 하위 CAN TX 오류는 빠질 수 있음

근거: v3.7.0 can_frame·can_filter, v3.7.0 ISO-TP SF/FF/CF/FC.

2. can_send()의 timeout은 전송 전체 시간이 아니다

device가 준비됐는지 확인하고 controller를 시작한 뒤 프레임을 보낸다. can_send(dev, &frame, timeout, callback, user_data)의 timeout은 빈 TX mailbox를 기다리는 시간이다. callback을 주면 0 반환은 전송 완료가 아니라 큐잉 성공이며, 완료 또는 오류는 callback의 error를 본다. callback에 NULL을 주면 호출이 프레임 전송 완료까지 기다린다. 사용자 모드에서 이 API를 부를 때는 callback이 NULL이어야 한다.

그림 2. 버스 결과 뒤에 오는 송신 완료와 독립된 수신 경로

↔ 좁은 화면에서는 송신 준비부터 결과까지 오른쪽으로 이동하세요. 아래 점선은 별도로 들어오는 수신 프레임입니다.

① 시작 확인device_is_readycan_start 결과와 물리 transceiver 구성을 확인
② mailbox 획득can_sendtimeout은 여기까지. 0 반환은 비동기 호출의 전송 완료가 아님
③ 버스 전송·오류ID + DLC + data상대 ACK, 중재, bus-off 등 실제 버스 조건이 영향
④ TX 결과callback(error)버스 결과 뒤 완료·오류 통지. NULL이면 호출이 완료까지 대기
별도 RX 경로버스의 수신 프레임 → ID·mask·IDE 필터 → ISR callback 또는 msgq. 송신 callback의 다음 단계가 아니다.

같은 ID의 순서. v3.7.0 헤더는 동일 우선순위 프레임의 큐잉 순서를 하드웨어 의존으로 둔다. 여러 CAN 프레임으로 자체 분할하면서 FIFO를 기대한다면 한 번에 하나씩 큐잉하도록 동기화하거나 ISO-TP를 사용한다. CAN_MODE_ONE_SHOT 및 FD 등 지원 mode는 controller마다 다르므로 can_get_capabilities()와 실제 드라이버를 확인한다.

근거: v3.7.0 can_send 계약·순서·사용자 모드, controller 시작·mode·capabilities.

3. 필터 callback과 msgq는 받은 프레임을 다루는 장소가 다르다

can_add_rx_filter()는 일치한 프레임을 callback으로 건네며, 이 callback은 인터럽트 문맥에서 실행된다. 여기서 블로킹 작업을 하지 말고 필요한 데이터를 복사하거나 다른 실행 흐름으로 넘긴다. can_add_rx_filter_msgq()는 초기화된 k_msgq에 프레임을 넣어 thread에서 꺼내게 한다. 다만 큐가 넘치면 이 wrapper는 버린 프레임을 조용히 무시하므로 손실을 세려면 직접 callback과 k_msgq_put() 결과를 조합한다. 두 방식 모두 성공 반환값은 filter ID이며 해제할 때 can_remove_rx_filter()에 사용한다.

표 2. callback·msgq 수신 경로
경로실행 위치프레임을 오래 쓸 때놓치기 쉬운 조건
can_add_rx_filterRX callback은 ISR 문맥callback 안에서 필요한 값 복사·전달블로킹 금지; 중복 필터 매치 우선순위는 하드웨어 의존
can_add_rx_filter_msgq내부 callback이 큐에 넣고 thread가 k_msgq_get꺼낸 can_frame 값 사용큐 초과 프레임은 경고 없이 버려짐

예를 들어 11비트 ID 0x123을 정확히 고르면 .id=0x123, .mask=CAN_STD_ID_MASK, .flags=0이다. 확장 ID는 CAN_FILTER_IDE와 CAN_EXT_ID_MASK를 함께 사용한다. 필터 수가 없으면 -ENOSPC, 유형이 맞지 않거나 지원되지 않으면 해당 오류를 처리한다.

근거: v3.7.0 RX filter·msgq·오류 계약, ID와 mask 정의.

4. ISO-TP는 수신자의 FC를 기다리며 긴 메시지를 나눈다

클래식 CAN의 일반 주소 형식에서 7바이트 이하 payload는 SF 한 장으로 갈 수 있다. 그보다 큰 payload는 FF에 전체 길이와 앞부분을 넣고, 수신자가 FC로 허용 여부·block size(BS)·최소 프레임 간격(STmin)을 보낸 뒤 CF를 이어 보낸다. 주소 형식과 CAN FD 프레임 길이에 따라 SF의 실제 데이터 여유는 바뀌므로 “모든 ISO-TP 메시지의 SF 한계가 7바이트”라고 일반화하지 않는다.

그림 3. 분할 전송에서 데이터와 흐름 제어가 오가는 방향

↔ 좁은 화면에서는 ①부터 ⑥까지 오른쪽으로 이동하세요.

① 양쪽 사전 준비수신 측 isotp_bind수신 filter/context 등록; 송신 측은 data/context 확보
② First Frame →isotp_send + FF전체 길이와 첫 data 전달
③ 수신 버퍼FF 수신길이를 확인하고 후속 CF용 버퍼 확보
④ ← Flow ControlFC: BS, STmin받을 수 있는 CF 개수와 최소 간격 지정
⑤ CF →SN + data순번을 확인하며 block 단위로 전송
⑥ 수신/완료isotp_recv수신 버퍼 처리; 송신 ISO-TP 상태 확인

isotp_bind()는 수신 context에 RX/TX 주소 조합을 묶고 CAN filter를 설치한다. 이 함수의 timeout은 첫 FF/SF 수신 버퍼 할당을 기다리는 시간이다. isotp_recv()는 context FIFO에서 호출자 버퍼로 복사하며 timeout 또는 프로토콜 오류를 음수 값으로 돌려준다. isotp_send()에 완료 callback을 주면 비동기로 실행되고, NULL이면 완료까지 기다린다. ISO-TP의 결과 코드는 CAN controller의 음수 errno와 다른 ISOTP_N_* 범주이므로 그대로 동일하게 해석하지 않는다.

v3.7.0 구현상 완료의 한계. isotp.c의 send_can_tx_cb(dev, error, arg)는 전달받은 CAN 송신 error를 검사하지 않고 backlog와 작업 상태만 진행한다. 따라서 ISOTP_N_OK만으로 하위 CAN 프레임 송신·ACK 성공을 입증할 수 없다. 버스 문제를 분리할 때는 controller 상태와 오류 카운터를 확인하고, 같은 장치에서 직접 can_send()로 프레임 경로를 별도 시험한다.

근거: v3.7.0 ISO-TP 개요, v3.7.0 bind/recv/send API, v3.7.0 구현.

5. callback의 완료와 버퍼 소유권은 따로 추적한다

비동기 isotp_send()에 넘긴 data와 isotp_send_ctx는 전체 전송이 끝날 때까지 유효해야 한다. isotp_bind()의 isotp_recv_ctx도 isotp_unbind() 전까지 유지한다. isotp_recv()는 호출자 버퍼에 복사한다. 반면 isotp_recv_net()으로 받은 net_buf는 사용 후 net_buf_unref()해야 한다. BS가 0이 아닌 수신은 block 단위로 조각을 건넬 수 있으므로 반환값과 뒤따르는 수신을 함께 확인한다.

표 3. 전송 완료와 객체 수명
API·객체무엇을 확인하나수명·해제
can_send + callback즉시 반환값은 큐잉, callback의 error는 프레임 결과callback 인자에 넘긴 애플리케이션 상태는 완료 관찰까지 유지
CAN RX callback의 frame*필터와 ID, DLC, payloadcallback 뒤 포인터 보존을 가정하지 않고 필요한 값 복사
isotp_send + callbackISOTP_N_*는 ISO-TP 상태 결과; v3.7.0에서 하위 CAN TX 오류를 모두 반영하지 않음data와 send context를 완료까지 유지
isotp_recv복사한 바이트 수 또는 음수 오류호출자 버퍼 소유
isotp_recv_netblock 단위 수신과 남은 길이받은 net_buf를 net_buf_unref

수신 필터와 부분 읽기. 직접 등록한 filter는 filter ID를 보관했다가 can_remove_rx_filter()로 제거한다. isotp_unbind()는 자신이 설치한 filter, FIFO에 남은 버퍼와 rctx->buf를 정리한다. 다만 v3.7.0 구현은 isotp_recv()의 부분 읽기로 남은 rctx->recv_buf를 이 함수에서 해제하지 않는다. 따라서 중간에 수신을 버리는 경로까지 자동 정리된다고 단정하지 말고, 종료 전에 수신 처리와 동기화하고 부분 읽기 버퍼의 소진 여부를 확인해야 한다. 직접 필터와 ISO-TP 필터를 같은 ID로 임의 해제하지 않는다.

근거: v3.7.0 ISO-TP API 수명·반환 계약, CAN RX 필터 계약.

6. 실제 보드에서는 프레임 경로부터 확인한다

controller가 존재해도 device 준비 상태, transceiver, bus 배선과 bitrate, 상대 ACK 조건이 맞지 않으면 전송은 성립하지 않는다. DT_CHOSEN(zephyr_canbus)는 그 경로로 device를 선택하는 애플리케이션에서만 필요하다. 이미 유효한 dev를 직접 넘기는 can_send(dev, ...) 자체의 필수 조건은 아니다. CAN FD도 CONFIG_CAN_FD_MODE 하나만으로 모든 controller가 지원하는 것은 아니며 mode·timing·상대 노드를 함께 확인해야 한다. 아래 순서로 controller 계층을 먼저 검증한 뒤 ISO-TP context와 흐름 제어를 본다.

① 장치·구성

zephyr.dts의 CAN 노드와 .config 옵션, device_is_ready()를 확인한다. 앱이 DT_CHOSEN(zephyr_canbus)를 쓰면 chosen도 확인한다.

② 프레임 송신

can_start(), 한 프레임 can_send(), 즉시 반환과 완료 callback 또는 blocking 결과를 분리해 기록한다.

③ 수신 필터

표준/확장 ID와 mask, filter ID 또는 -ENOSPC, msgq 깊이와 손실 여부를 확인한다.

④ ISO-TP 왕복

양쪽 RX/TX 주소와 FC 방향, BS·STmin, buffer pool, timeout 코드를 각각 기록한다.

표 4. 계층별 실패 진단 출발점
관찰한 신호먼저 볼 계층확인할 사실
can_send == -EAGAINCAN controllerTX mailbox 대기 timeout; 버스 전체 timeout으로 단정하지 않기
-ENETDOWN / -ENETUNREACHCAN controller각각 stopped / bus-off 상태를 can_get_state()로 확인
RX filter -ENOSPCCAN controller필터 슬롯과 중복 직접/ISO-TP 등록 확인
ISO-TP ISOTP_N_TIMEOUT_BS / _CRISO-TPFC 또는 후속 CF 수신 방향·상대 설정 확인

이 글은 v3.7.0의 API·구현을 읽어 만든 설계 지도다. 특정 SoC의 CAN controller를 빌드하거나 두 보드로 실제 ISO-TP 왕복을 측정한 결과는 포함하지 않는다.

근거: CAN 송신·상태 오류, ISO-TP timeout 코드.

7. Zephyr v3.7.0 고정 근거와 이어 읽기

API 명칭과 반환 의미는 v3.7.0 태그 및 같은 버전 공식 문서에 맞췄다. 최신 개발판의 추가 callback API나 controller 기능을 이 글에 소급하지 않았다.

표 5. CAN·ISO-TP 고정 버전 근거
근거확인한 범위
v3.7.0 can.hframe/filter, send timeout·callback, RX ISR·msgq, state/recovery·FD 가드
v3.7.0 ISO-TP 문서SF/FF/CF/FC, bind/send/recv/recv_net 수명·반환값
v3.7.0 isotp.c수신 상태 기계와 buffer pool·FC 전송 위치