Skip to content

실습 목표#


  • UDS 진단 통신의 요청·응답 구조와 SID, DID, NRC의 의미를 이해한다.
  • isotp.h 위에 UDS 진단 로직을 구현하고 차대번호 조회, 세션 시작, TesterPresent 요청의 긍정 응답을 확인한다.
  • 없는 DID와 지원하지 않는 SID에 대해 서로 다른 부정 응답을 만들고 실제 버스 프레임을 ISO-TP와 UDS 두 계층으로 나누어 분석한다.
  • ECU가 응답하지 않는 상황과 진단기 수신 타임아웃을 확인하고, 다음 실습에 사용할 SID·DID·NRC 항목을 조사한다.

UDS 진단 대화의 구조#


  • UDS는 진단기가 요청하고 ECU가 응답하는 구조이다.
  • ECU가 먼저 요청을 시작하지 않는다.
  • 응답은 긍정 응답과 부정 응답 두 가지이며, 부정 응답에는 거절 이유가 함께 들어간다.
  • 응답이 오지 않으면 진단기는 시간 제한이 끝날 때까지 기다리게 된다.

SID · DID · NRC#

  • UDS 요청과 응답은 여러 바이트로 구성되며 각 바이트의 의미가 정해져 있다.
항목 이름 의미 예
SID Service Identifier 요청 서비스 종류 0x22 = 값 읽기
DID Data Identifier 읽거나 쓰려는 데이터 항목 0xF190 = 차대번호
NRC Negative Response Code 부정 응답 이유 0x31 = 요청 범위 밖
  • 차대번호를 읽을 때는 SID 0x22와 DID 0xF190을 함께 보낸다.
차대번호 읽기 요청
22 F1 90

긍정 응답과 부정 응답#

  • 요청을 정상 처리하면 응답 SID는 요청 SID에 0x40을 더한 값이 된다.
요청 의미 긍정 응답
0x22 ReadDataByIdentifier 0x62
0x10 DiagnosticSessionControl 0x50
0x3E TesterPresent 0x7E
  • 부정 응답은 항상 세 바이트로 구성된다.
부정 응답 형식
7F SID NRC
  • 0x7F는 부정 응답 표시, 둘째 바이트는 거절한 요청 SID, 셋째 바이트는 거절 이유 NRC이다.

isotp.h와 UDS 진단용 CAN ID#


  • 3타임에서 구현한 ISO-TP 분할·재조립 기능은 isotp.h에 함수로 묶여 있다.
  • 이번 실습에서는 ISO-TP 위에서 UDS 진단 로직만 구현한다.
isotp.h 제공 API
isotp_open(t, ifname, 받을 ID, 보낼 ID)
isotp_send(t, data, len)
isotp_recv(t, out, max_len)

UDS 진단용 CAN ID#

CAN ID 용도 송신 주체
0x7DF 모든 ECU에 기능 요청 진단기
0x7E0 ~ 0x7E7 특정 ECU에 요청 진단기
0x7E8 ~ 0x7EF 해당 ECU의 응답 ECU
  • 이번 실습에서는 첫 번째 ECU 자리를 사용한다.
ECU
isotp_open(t, ifname, 0x7E0, 0x7E8);
진단기
isotp_open(t, ifname, 0x7E8, 0x7E0);

시범. 차대번호 긍정 응답 만들기 TODO 1#


  • uds_ecu.cpp의 TODO 1에서 DID 0xF190 차대번호 요청에 대한 긍정 응답을 만든다.
  • 첫 바이트는 요청 SID 0x22 + 0x40 = 0x62가 된다.
  • 둘째·셋째 바이트는 요청 DID F1 90을 그대로 넣는다.
  • 그 뒤에 17바이트의 가상 VIN 문자열을 붙이고 전체 길이를 resp_len에 저장한다.
uds_ecu.cpp · TODO 1
resp[0] = (unsigned char)(sid + 0x40);  // 22 -> 62
resp[1] = req[1];                       // F1
resp[2] = req[2];                       // 90
int n = (int)std::strlen(FAKE_VIN);     // 17
std::memcpy(resp + 3, FAKE_VIN, n);
resp_len = 3 + n;                       // 20

실습 1. 채운 것을 돌려 보기#


  • TODO 1을 빌드한 뒤 차대번호 읽기, 세션 시작, TesterPresent 요청을 차례로 보낸다.
  • 차대번호가 문자열로 출력되고 나머지 두 서비스도 긍정 응답이 나오면 정상이다.
터미널 1·ECU
make
./uds_ecu can1
터미널 3·버스 관찰
candump can1
터미널 2·진단기
./uds_client can0 22F190
./uds_client can0 1001
./uds_client can0 3E00
기대 출력
[응답] (20바이트) 62 F1 90 4B 4E 4D 41 54 32 4D 54 38 4C 50 31 32 33 34 35 36
  성공 (요청 22 + 0x40 = 62)
  항목 F190 의 값 : KNMAT2MT8LP123456

[응답] (2바이트) 50 01
  성공 (요청 10 + 0x40 = 50)

[응답] (2바이트) 7E 00
  성공 (요청 3E + 0x40 = 7E)
  • KNMAT2MT8LP123456은 형식만 맞춘 가상의 차대번호이다.

실습 2. 오가는 프레임 읽기#


  • 차대번호 응답은 20바이트이므로 Classical CAN 한 프레임에 담기지 않아 ISO-TP로 분할된다.
  • 같은 요청을 다시 보내고 이번에는 candump에서 버스 프레임을 확인한다.
  • 한 CAN 프레임 안에는 ISO-TP 계층과 UDS 계층이 함께 들어 있다.
터미널 1·ECU
./uds_ecu can1
터미널 3·버스 관찰
candump can1
터미널 2·진단기
./uds_client can0 22F190
관찰 결과
7E0 [04] 03 22 F1 90
7E8 [08] 10 14 62 F1 90 4B 4E 4D
7E0 [03] 30 00 00
7E8 [08] 21 41 54 32 4D 54 38 4C
7E8 [08] 22 50 31 32 33 34 35 36

실습 2 결과 : 프레임 안의 두 계층#

프레임 ISO-TP 부분 UDS 부분 의미
요청 03 22 F1 90 SF, 3바이트 UDS 요청
응답 FF 10 14 62 F1 90 4B 4E 4D 전체 20바이트 예고 + 앞 6바이트
FC 30 00 00 없음 ISO-TP 흐름 제어
CF 1·2 21, 22 나머지 데이터 순번 + 이어지는 UDS 데이터
  • ISO-TP는 데이터를 나누고 합치는 역할만 하며 내부 UDS 의미는 해석하지 않는다.
  • FC는 ISO-TP 흐름 제어용 프레임이므로 UDS 데이터가 없다.
  • VIN의 17바이트 값은 ASCII 문자를 그대로 옮긴 것이다.
VIN 데이터
4B 4E 4D 41 54 32 4D 54 38 4C 50 31 32 33 34 35 36
 K  N  M  A  T  2  M  T  8  L  P  1  2  3  4  5  6

도전 과제. 부정 응답 만들기#


  • 지원하는 서비스 0x22로 존재하지 않는 DID를 요청했을 때 부정 응답을 직접 만든다.
  • 현재는 해당 코드가 비어 있어 진단기가 응답을 받지 못하고 기다린다.
  • 부정 응답은 0x7F, 거절한 SID, NRC 세 바이트로 구성한다.
넣을 위치 사용할 값 의미
resp[0] NEGATIVE 0x7F
resp[1] sid 거절한 요청 SID
resp[2] NRC_OUT_OF_RANGE 0x31
resp_len 3 응답 전체 길이

관찰 포인트

resp_len을 설정하지 않으면 응답 데이터는 만들어도 실제로 전송되지 않음

도전 과제의 답#


  • 존재하지 않는 DID를 요청받았을 때 0x31 RequestOutOfRange 부정 응답을 만든다.
uds_ecu.cpp
resp[0] = NEGATIVE;
resp[1] = sid;
resp[2] = NRC_OUT_OF_RANGE;
resp_len = 3;
std::printf(" -> 모르는 항목 %04X. 범위 밖이라고 답한다\n", did);

도전 과제. 구현 검증#


  • 없는 DID와 지원하지 않는 SID를 각각 요청하여 서로 다른 NRC가 나오는지 확인한다.
터미널 1·ECU
make
./uds_ecu can1
터미널 3·버스 관찰
candump can1
터미널 2·진단기
./uds_client can0 22F1FF
./uds_client can0 1900
기대 출력
[응답] (3바이트) 7F 22 31
  거절됨. 요청 22, 이유 31 (요청 범위 밖)

[응답] (3바이트) 7F 19 11
  거절됨. 요청 19, 이유 11 (지원하지 않는 서비스)

도전 과제 결과 : 같은 거절, 다른 이유#

요청 응답 거절 이유 진단기가 다음에 할 일
22 F1 FF 7F 22 31 서비스는 있지만 DID가 없음 다른 DID로 다시 요청
19 00 7F 19 11 SID 자체를 지원하지 않음 ECU가 지원하는 서비스 확인
  • 같은 0x7F 부정 응답이라도 NRC에 따라 진단기가 다음에 취할 동작이 달라진다.
  • 실제 ECU에서 여러 NRC를 구분해 사용하는 이유이다.

실습 3. 답이 없을 때#


  • 도전 과제에서 만든 부정 응답 코드 중 resp_len = 3; 줄만 잠시 제거하고 다시 빌드한다.
  • 없는 DID 22F1FF를 요청하여 ECU, 진단기, 버스 화면을 각각 확인한다.
  • 확인 후에는 반드시 resp_len = 3;을 다시 복구한다.
터미널 1·ECU
make
./uds_ecu can1
터미널 3·버스 관찰
candump can1
터미널 2·진단기
./uds_client can0 22F1FF

실습 3 결과 : 부정 응답이 필요한 이유#

  • 응답이 없으면 진단기는 오류 이유를 알 수 없고 계속 기다리게 된다.
  • 부정 응답이 오면 거절된 SID와 NRC를 통해 원인을 판단하고 다음 동작을 결정할 수 있다.
진단기가 받은 것 알 수 있는 것 다음에 할 수 있는 일
아무것도 없음 원인 알 수 없음 기다릴 뿐
7F 22 31 0x22 요청, DID 범위 밖 다른 DID로 다시 요청
7F 19 11 0x19 서비스 자체 미지원 지원 서비스 확인
  • 실제 ECU는 처리하지 못하는 요청에도 응답을 보내야 한다.
  • 응답이 아예 오지 않는 경우에 대비해 진단기에는 별도의 시간 제한이 필요하다.

실습 4. 다음에 쓸 SID · DID · NRC 조사하기#


  • 지금까지 사용한 SID, DID, NRC 값은 ISO 14229-1에 정의되어 있다.
  • 다음 실습에 사용할 항목과 주변 항목을 코드와 표준 문서에서 찾아 표를 채운다.
코드에서 찾기
grep -n -A 12 "nrc_text" uds_client.cpp
grep -n "SID_\|DID_\|NRC_" uds_ecu.cpp

실습 4 결과 : SID와 DID#

항목 이름 하는 일 다음 시간
SID 0x11 ECUReset ECU를 다시 시작시킴 거절 응답을 만듦
SID 0x14 ClearDiagnosticInformation 저장된 고장 기록을 지움 —
SID 0x19 ReadDTCInformation 저장된 고장 기록을 읽음 —
SID 0x3E TesterPresent 진단기가 아직 연결되어 있음을 알림 하위 기능을 검사함
DID 0xF186 ActiveDiagnosticSession 현재 진단 세션 정보 —
DID 0xF18B ECUManufacturingDate ECU 제조 날짜 —
DID 0xF18C ECUSerialNumber ECU 일련번호 —
DID 0xF195 systemSupplierECUSoftwareVersionNumber ECU 소프트웨어 버전 번호 새로 만듦
  • 0xF1로 시작하는 DID 중 일부는 ECU 자체 정보에 사용되며 표준에 값이 정의되어 있다.

실습 4 결과 : NRC#

항목 이름 의미 우리 코드에서
NRC 0x12 subFunctionNotSupported 지원하지 않는 하위 기능 아직 사용 안 함
NRC 0x13 incorrectMessageLengthOrInvalidFormat 길이 또는 형식 오류 0x22 길이가 3이 아닐 때
NRC 0x22 conditionsNotCorrect 현재 조건에서 처리할 수 없음 아직 사용 안 함
NRC 0x78 requestCorrectlyReceivedResponsePending 요청은 받았으나 응답 준비 중 아직 사용 안 함
  • 0x78은 최종 거절이 아니라 시간이 더 필요하다는 중간 응답이다.
  • 오래 걸리는 작업에서 진단기가 응답을 기다리도록 만드는 데 사용된다.

정리#


  • UDS 진단은 SID와 필요에 따라 DID·하위 기능 등을 조합해 ECU에 요청하는 구조이다.
  • 응답 첫 바이트를 보면 성공, 거절, 무응답을 구분할 수 있다.
첫 바이트 의미 뒤에 오는 값
SID + 0x40 긍정 응답 요청한 항목과 값
0x7F 부정 응답 거절한 SID와 NRC
아무것도 없음 응답 없음 시간 제한까지 대기
0x22 요청 예
62 ...        긍정 응답
7F 22 31      부정 응답
  • 처리할 수 없는 요청에도 부정 응답을 보내야 진단기가 원인을 알고 다음 동작을 결정할 수 있다.

심화 과제. 진단기에 시간 제한 두기#


  • 실습 3에서는 ECU가 응답하지 않아 진단기가 계속 기다렸다.
  • 이번에는 uds_client.cpp의 수신 소켓에 1초 타임아웃을 설정하여 스스로 대기를 끝내도록 만든다.
  • ECU를 실행하지 않은 상태에서 요청을 보내어 수정 전후 동작을 비교한다.
터미널 3·버스 관찰
candump can1
터미널 2·진단기
./uds_client can0 22F190
사용할 것 의미
struct timeval tv 초와 마이크로초를 저장하는 구조체
tv.tv_sec = 1; 1초까지만 대기
SO_RCVTIMEO 소켓 수신 시간 제한 옵션
SOL_SOCKET 소켓 계층 옵션

심화 과제 결과 : 요청은 정상적으로 전송됨#

  • 타임아웃을 추가하면 ECU가 없을 때 약 1초 뒤 read()가 종료된다.
  • 버스를 보면 ECU가 없는 경우에도 요청 프레임은 정상적으로 전송되었다.
  • 문제는 요청 전송이 아니라 응답이 오지 않는 것이었다.
버스 관찰
can1 7E0 [04] 03 22 F1 90
can1 7E0 [04] 03 22 F1 90
can1 7E0 [04] 03 22 F1 90
can1 7E8 [08] 10 14 62 F1 90 4B 4E 4D
can1 7E0 [03] 30 00 00
can1 7E8 [08] 21 41 54 32 4D 54 38 4C
can1 7E8 [08] 22 50 31 32 33 34 35 36
상태 진단기 화면 걸린 시간
수정 전 계속 기다림 끝나지 않음
수정 후 · ECU 없음 응답이 없다 약 1.017초
수정 후 · ECU 있음 차대번호 출력 즉시
  • SO_RCVTIMEO는 read() 한 번에 대한 제한이며 전체 UDS 대화 시간 제한과는 다르다.

부록. 심화 과제의 답#


  • struct timeval을 사용하기 위해 sys/time.h를 추가한다.
uds_client.cpp · 파일 위쪽
#include "isotp.h"
#include <cstdlib>
#include <sys/time.h>
  • isotp_open() 바로 다음에 수신 타임아웃을 설정한다.
uds_client.cpp · isotp_open 다음
struct timeval tv;
tv.tv_sec = 1;
tv.tv_usec = 0;
setsockopt(t.sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
  • ECU 없이 요청하면 약 1초 뒤 다음과 같이 종료된다.
실행 결과
[요청] (3바이트) 22 F1 90
read: Resource temporarily unavailable
응답이 없다
  • 시간이 끝나면 read()가 -1을 반환하고 isotp_recv()도 -1이 된다.