실습 목표#
- 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와 DID0xF190을 함께 보낸다.
차대번호 읽기 요청
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에서 DID0xF190차대번호 요청에 대한 긍정 응답을 만든다.- 첫 바이트는 요청 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를 요청받았을 때
0x31RequestOutOfRange 부정 응답을 만든다.
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이 된다.