DoIP를 처음 붙일 때 CAN 진단 습관 때문에 자주 나오는 오해가 하나 있다.
- UDS가 길어지면 Flow Control이 있어야 할 것 같다
- First Frame, Consecutive Frame처럼 나눠야 할 것 같다
- 큰 요청은 내가 여러 조각으로 쪼개 보내야 할 것 같다
그런데 DoIP에서는 이 그림으로 들어가면 구현이 금방 꼬인다.
오늘 메모는 DoIP에서 UDS를 ISO-TP처럼 자르면 왜 디버깅이 길어지는지다.
결론부터
DoIP에서 테스터가 직접 다뤄야 하는 메시지 경계는 보통 이쪽이다.
Generic Header(8 bytes)
+ Diagnostic Message payload
+ SA(2)
+ TA(2)
+ UDS bytes(N)
즉 애플리케이션 입장에서는
UDS 한 요청 = DoIP Diagnostic Message 하나로 보는 편이 안전하다.
CAN에서 보던 ISO-TP 조각 경계로 생각하면
송신 큐와 수신 파서가 불필요하게 복잡해진다.
왜 CAN 감각이 그대로 들어오나
CAN에서는 프레임 크기가 작다.
그래서 UDS payload가 길면 transport layer가 전면에 드러난다.
Single Frame
First Frame
Flow Control
Consecutive Frame
이 흐름을 늘 의식하게 된다.
반면 DoIP에서는 TCP가 이미 바이트 스트림 전달을 맡고 있고,
DoIP는 그 위에서 payload length로 메시지 경계만 정한다.
그래서 테스터 구현에서 먼저 신경 써야 하는 것은:
- ISO-TP block size
- CF sequence number
- Flow Control frame 생성
이 아니라
- TCP stream 누적 수신
- DoIP header parse
- payload length 기준 message reassembly
쪽이다.
자주 터지는 실수 1: UDS payload를 앱이 임의로 조각낸다
예를 들면 이런 구조다.
큰 UDS request 생성
-> 앱이 256바이트 단위로 나눔
-> 각 조각을 별도 DoIP Diagnostic Message로 전송
-> ECU 응답 이상
이건 ECU 입장에서는 같은 요청의 연속 조각이 아니라
서로 다른 UDS 요청으로 보일 수 있다.
그러면 흔히 이런 증상이 나온다.
- 첫 조각만 보고 NRC가 옴
- 뒤 조각은 길이 이상처럼 보임
- 어떤 ECU는 무응답
- 어떤 ECU는 세션이 꼬인 것처럼 보임
즉 “크니까 나눠 보내야지”가 아니라
UDS service 자체가 요구하는 바이트열을 한 번에 담아야 한다고 보는 편이 맞다.
자주 터지는 실수 2: recv() 한 번을 한 메시지로 간주한다
반대로 수신 쪽에서는 이런 착각이 자주 나온다.
recv()
-> 받은 바이트를 바로 한 응답으로 처리
하지만 DoIP는 TCP stream 위에 있다.
- 한 메시지가 여러
recv()에 나뉠 수 있다 - 여러 메시지가 한
recv()에 붙어 올 수도 있다
그래서 CAN-TP처럼 “프레임 이벤트”를 기대하면 안 되고,
앞 8바이트를 읽어 payload length를 얻은 뒤
그 길이만큼 누적해서 한 Diagnostic Message를 완성해야 한다.
이 부분은 ISO-TP 조립과 비슷해 보일 수 있지만,
실제로 조립 기준은 DoIP header다.
자주 터지는 실수 3: ACK/NACK를 Flow Control처럼 해석한다
DoIP Diagnostic ACK/NACK가 있으니
이걸 CAN의 Flow Control 비슷하게 오해하는 경우도 있다.
그런데 역할이 다르다.
- ISO-TP Flow Control: 다음 조각 전송 속도를 조절
- DoIP ACK/NACK: 방금 보낸 Diagnostic Message의 수락 여부를 알려줌
즉 ACK를 받았다고 해서
“다음 조각을 보내라”는 뜻이 아니다.
애초에 앱이 조각을 만들고 있다면
그 구조부터 다시 보는 편이 빠르다.
긴 UDS 요청은 어디서 다뤄야 하나
실무에서는 여기서 한 번 더 헷갈린다.
예를 들어 RequestDownload, TransferData, 대용량 DID 쓰기처럼
UDS payload 자체가 길어지는 서비스가 있다.
이때도 우선 기준은 같다.
UDS가 정의한 한 request PDU 생성
-> SA/TA 앞에 붙여 DoIP Diagnostic Message 구성
-> header payload length 계산
-> TCP stream으로 송신
즉 길이가 길다는 이유만으로
앱이 ISO-TP 같은 조각 계층을 새로 만들 필요는 없다.
물론 OEM 또는 ECU 구현에 따라
서비스 단위 block 분할 정책은 있을 수 있다.
하지만 그건 UDS 서비스 레벨의 block 설계이지,
DoIP transport를 ISO-TP처럼 다시 만드는 문제와는 다르다.
디버깅할 때 이렇게 보면 빨라진다
이 이슈는 패킷 덤프만 봐도 힌트가 바로 나온다.
- 한 UDS 요청이 DoIP Diagnostic Message 여러 개로 쪼개져 있는지 확인
- 각 DoIP message의
payload length가 SA/TA + UDS 길이와 맞는지 확인 recv()횟수와 message 개수를 같은 개념으로 보고 있지 않은지 확인- ACK/NACK를 다음 조각 제어 신호처럼 해석하는 코드가 있는지 확인
- 큰 서비스 요청일수록 앱 내부에 “분할 송신 레이어”가 숨어 있지 않은지 확인
구현 쪽에서 무난한 패턴
애플리케이션 경계를 단순하게 두는 편이 덜 아프다.
build complete UDS request
-> wrap into one DoIP Diagnostic Message
-> send bytes
receive TCP bytes
-> accumulate by DoIP payload length
-> decode one Diagnostic Message
-> pass UDS payload upward
핵심은 두 가지다.
- 송신은 UDS request 단위
- 수신은 DoIP message 경계 단위
이렇게 잡아두면
Routing Activation 이후의 UDS 흐름도 훨씬 읽기 쉬워진다.
빠른 체크리스트
- 큰 UDS 요청을 앱이 임의 크기로 쪼개 별도 DoIP message로 보내지 않는지 확인
recv()한 번을 한 응답으로 처리하는 경로가 없는지 확인payload length계산에 SA/TA 4바이트가 포함되는지 확인- ACK/NACK를 transport-level flow control처럼 쓰는 코드가 없는지 확인
- 장비 로그에
UDS length,DoIP payload length,TCP rx chunk length를 분리 기록하는지 확인
한 줄 요약
DoIP에서 UDS를 ISO-TP처럼 다시 쪼개기보다, UDS 한 요청을 Diagnostic Message 하나로 만들고 TCP stream에서는 Generic Header의 payload length 기준으로 메시지 경계를 복원하는 편이 안전하다.
추천 키워드
DoIP, UDS, ISO-TP, Diagnostic Message, payload length, Routing Activation
DevBJ | 오늘을살자, Log Today