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_RCVTIMEOread() 한 번에 대한 제한이며 전체 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이 된다.