실습 목표#
- 지난 시간에 조사한 SID, DID, NRC를 실제 ECU 코드에 적용해 읽을 수 있는 항목과 예외 처리 조건을 확장한다.
- DID
0xF195소프트웨어 버전 응답을 추가하고, 응답 길이에 따라 ISO-TP 프레임 구성이 어떻게 달라지는지 확인한다. - TesterPresent 하위 기능 오류, ReadDataByIdentifier 길이 오류, ECUReset 조건 오류를 각각 다른 NRC로 거절하도록 구현한다.
- 숫자형 DID와 세션 상태를 추가해 값 표현 방식과 ECU 상태에 따라 응답이 달라지는 구조를 확인한다.
실습 준비. 이번에 사용할 항목 확인#
- 이번 실습에서는 지난 시간에 조사한 SID, DID, NRC 중 아래 일곱 항목을 사용한다.
- SID는 어떤 동작을 할지, DID는 어떤 데이터를 다룰지 정하고, 요청을 처리할 수 없으면 NRC로 이유를 알려 준다.
| 항목 | 이름 | 의미 |
|---|---|---|
SID 0x3E |
TesterPresent | 진단기가 아직 연결되어 있음을 알림 |
SID 0x11 |
ECUReset | ECU 재시작 요청 |
DID 0xF190 |
VIN | 차대번호 |
DID 0xF195 |
systemSupplierECUSoftwareVersionNumber | 소프트웨어 버전 번호 |
NRC 0x12 |
subFunctionNotSupported | 지원하지 않는 하위 기능 |
NRC 0x13 |
incorrectMessageLengthOrInvalidFormat | 요청 길이 또는 형식 오류 |
NRC 0x22 |
conditionsNotCorrect | 현재 조건에서 처리 불가 |
실습 폴더
cd ~/can_lab/5time
make
시범. 소프트웨어 버전 번호 읽기#
- DID
0xF195에 대한 긍정 응답을 추가한다. - 차대번호 응답과 구조는 같고, 값만
FAKE_SW_VERSION으로 바뀐다. - 실제 ECU도 같은 형태의 DID 응답 블록을 여러 개 가지고 있다.
uds_ecu.cpp · 시범 자리
resp[0] = (unsigned char)(sid + 0x40);
resp[1] = req[1];
resp[2] = req[2];
int n = (int)std::strlen(FAKE_SW_VERSION);
std::memcpy(resp + 3, FAKE_SW_VERSION, n);
resp_len = 3 + n;
실습 1. DID를 늘려 확인하기#
- VIN과 소프트웨어 버전 번호를 각각 읽고 값 길이가 달라질 때 ISO-TP 프레임 구성이 어떻게 달라지는지 확인한다.
터미널 1·ECU
make
./uds_ecu can1
터미널 3·버스 관찰
candump can1
터미널 2·진단기
./uds_client can0 22F190
./uds_client can0 22F195
실습 1 결과 : 길이가 달라도 프레임 수는 같음#
- VIN 응답은 20바이트, 소프트웨어 버전 응답은 14바이트이다.
- 두 응답 모두 FF 1장과 CF 2장을 사용하며, 차이는 마지막 CF 길이뿐이다.
| 요청 | 값 길이 | 응답 길이 | FF 예고 | CF 수 | 마지막 CF |
|---|---|---|---|---|---|
22F190 |
17자 | 20 | 10 14 |
2 | [08] |
22F195 |
11자 | 14 | 10 0E |
2 | [02] |
버스 실측
can1 7E8 [08] 10 14 62 F1 90 4B 4E 4D
can1 7E8 [08] 22 50 31 32 33 34 35 36
can1 7E8 [08] 10 0E 62 F1 95 53 57 20
can1 7E8 [02] 22 31
14 = 6 + 7 + 1이므로 마지막 CF에는 데이터가 1바이트만 남아[02]프레임으로 전송된다.- FF의 둘째 바이트
0x14,0x0E가 각각 전체 UDS 길이 20바이트와 14바이트를 나타낸다.
예외 처리 전 상태 확인#
- 현재 코드는 잘못된 요청도 일부 정상 처리하거나 응답하지 않는다.
- 아래 세 요청을 먼저 보내 수정 전 동작을 확인한다.
터미널 1·ECU
./uds_ecu can1
터미널 3·버스 관찰
candump can1
터미널 2·수정 전 상태 확인
./uds_client can0 3E01
./uds_client can0 22F1
./uds_client can0 1101
3E01은 지원하지 않는 하위 기능인데도 성공 응답이 나온다.22F1은 길이가 부족하지만 그대로 처리된다.1101은 아직 응답이 없다.- 이후 세 도전 과제에서 각각 다른 NRC로 거절하도록 수정한다.
도전 과제 1. TesterPresent 하위 기능 검사#
- SID
0x3E의 둘째 바이트는 하위 기능이다. - 이번 실습에서는
0x00만 허용하고 다른 값은 NRC0x12로 거절한다. uds_ecu.cpp의 도전 과제 1 자리에서if (false)조건과 부정 응답 부분을 완성한다.
uds_ecu.cpp · 도전 과제 1 자리
if (false) {
} else {
resp[0] = (unsigned char)(sid + 0x40);
...
확인
./uds_client can0 3E00
./uds_client can0 3E01
기대 결과
3E00 -> 7E 00
3E01 -> 7F 3E 12
도전 과제 2. ReadDataByIdentifier 요청 길이 검사#
- SID
0x22요청은 SID 1바이트와 DID 2바이트를 합쳐 정확히 3바이트여야 한다. - 요청 길이는
req_len에 들어 있으며, 길이가 3이 아니면 NRC0x13으로 거절한다. - 길이 검사를 통과한 경우에만 DID를 읽도록 한다.
uds_ecu.cpp · 도전 과제 2 자리
if (false) {
} else {
unsigned short did = ...;
확인
./uds_client can0 22F190
./uds_client can0 22F1
기대 결과
22F190 -> 62 F1 90 + 값
22F1 -> 7F 22 13
도전 과제 3. ECUReset 요청 거절#
- SID
0x11은 ECU를 재시작하는 요청이다. - 실습 환경에서는 실제로 ECU를 재시작하지 않고 NRC
0x22로 거절한다. - 앞 두 과제와 달리 골격이 비어 있으므로 부정 응답 세 바이트를 직접 만든다.
uds_ecu.cpp · 도전 과제 3 자리
} else if (sid == SID_RESET) {
// 여기를 채운다
} else {
확인
./uds_client can0 1101
기대 결과
1101 -> 7F 11 22
도전 과제의 답#
도전 과제 1. 하위 기능 검사#
uds_ecu.cpp
if (req_len >= 2 && req[1] != 0x00) {
resp[0] = NEGATIVE;
resp[1] = sid;
resp[2] = NRC_SUBFUNC_NOT_SUPPORTED;
resp_len = 3;
}
도전 과제 2. 요청 길이 검사#
uds_ecu.cpp
if (req_len != 3) {
resp[0] = NEGATIVE;
resp[1] = sid;
resp[2] = NRC_WRONG_LENGTH;
resp_len = 3;
}
도전 과제 3. ECUReset 거절#
uds_ecu.cpp
resp[0] = NEGATIVE;
resp[1] = sid;
resp[2] = NRC_CONDITIONS_NOT_CORRECT;
resp_len = 3;
도전 과제 결과 : 거절 응답은 항상 세 바이트#
- 세 요청은 서로 다른 이유로 거절되지만 부정 응답 형식은 모두
7F SID NRC로 같다.
| 요청 | 응답 | NRC 이름 | 이유 |
|---|---|---|---|
3E 01 |
7F 3E 12 |
subFunctionNotSupported | 0x01 하위 기능 미지원 |
22 F1 |
7F 22 13 |
incorrectMessageLengthOrInvalidFormat | 요청 길이 부족 |
11 01 |
7F 11 22 |
conditionsNotCorrect | 현재 재시작 조건 불충족 |
버스 실측
can1 7E0 [03] 02 3E 01
can1 7E8 [04] 03 7F 3E 12
can1 7E0 [03] 02 22 F1
can1 7E8 [04] 03 7F 22 13
can1 7E0 [03] 02 11 01
can1 7E8 [04] 03 7F 11 22
- 부정 응답은 UDS 데이터가 3바이트이므로 ISO-TP SF 한 장에 들어간다.
- 따라서 요청 1장과 응답 1장으로 끝나며 FC와 CF가 필요하지 않다.
실습 2. 모든 분기 한 번씩 확인하기#
- 세 도전 과제를 모두 구현한 뒤 일곱 가지 요청을 차례로 전송한다.
- 성공 3가지, 거절 4가지가 나오며 ECU 코드의 주요 분기를 모두 한 번씩 지나간다.
터미널 1·ECU
make && ./uds_ecu can1
터미널 3·버스 관찰
candump can1
터미널 2·진단기
./uds_client can0 22F190
./uds_client can0 22F195
./uds_client can0 22F1FF
./uds_client can0 22F1
./uds_client can0 3E00
./uds_client can0 3E01
./uds_client can0 1101
실습 2 결과#
| 요청 | 의미 | 응답 | 결과 |
|---|---|---|---|
22F190 |
차대번호 | 62 F1 90 + 17바이트 |
성공 |
22F195 |
소프트웨어 버전 | 62 F1 95 + 11바이트 |
성공 |
22F1FF |
없는 DID | 7F 22 31 |
거절 · 범위 밖 |
22F1 |
길이 부족 | 7F 22 13 |
거절 · 길이 |
3E00 |
TesterPresent 하위 기능 00 |
7E 00 |
성공 |
3E01 |
TesterPresent 하위 기능 01 |
7F 3E 12 |
거절 · 하위 기능 |
1101 |
ECUReset | 7F 11 22 |
거절 · 조건 |
실습 2 결과 : NRC에 따라 대응이 달라짐#
| 요청 | 응답 | 문제 | 진단기가 다음에 할 일 |
|---|---|---|---|
3E01 |
7F 3E 12 |
하위 기능 값 오류 | 값을 고쳐 다시 요청 |
22F1 |
7F 22 13 |
요청 길이 부족 | 길이를 맞춰 다시 요청 |
1101 |
7F 11 22 |
현재 조건에서 처리 불가 | 조건을 갖춘 뒤 재요청 |
22F1FF |
7F 22 31 |
존재하지 않는 DID | 다른 DID로 요청 |
- 모든 오류를 같은 NRC로 처리하면 진단기는 원인을 구분할 수 없다.
실습 3. 옆자리 ECU에 물어보기#
- 두 사람이 각각 만든 ECU가 같은 UDS 표준을 따랐다면 같은 요청에 같은 응답이 나와야 한다.
- 두 라즈베리파이의
can0을 CAN_H, CAN_L로 연결하고 한쪽은 진단기, 다른 쪽은 ECU 역할을 맡는다.
옆자리 파이·ECU
./uds_ecu can0
내 파이·진단기
./uds_client can0 22F190
./uds_client can0 3E01
./uds_client can0 1101
22F190은 VIN 긍정 응답,3E01은7F 3E 12,1101은7F 11 22가 동일하게 나와야 한다.- 결과가 다르면 두 구현 중 하나의 조건이나 표준 해석을 다시 확인한다.
정리#
- 읽을 수 있는 DID를 늘리는 것은 같은 긍정 응답 패턴을 반복해서 추가하는 방식이다.
- 잘못된 요청은 원인에 맞는 NRC를 붙여 거절해야 진단기가 다음 행동을 결정할 수 있다.
| 문제 | NRC | 이름 |
|---|---|---|
| 하위 기능 값 오류 | 0x12 |
subFunctionNotSupported |
| 요청 길이 오류 | 0x13 |
incorrectMessageLengthOrInvalidFormat |
| 현재 처리 조건 불충족 | 0x22 |
conditionsNotCorrect |
| 존재하지 않는 DID | 0x31 |
requestOutOfRange |
- UDS 명령 체계는 그대로 유지되고, 다음 단계에서는 전송 계층만 ISO-TP + CAN에서 DoIP + Ethernet으로 바뀐다.
- 전송 계층이 바뀌어도
22 F1 90,62 F1 90 ...,7F 22 13같은 UDS 데이터 자체는 그대로 사용할 수 있다.
심화 과제 1. 숫자형 DID 만들기#
- 지금까지의 VIN과 소프트웨어 버전은 문자열이었지만 실제 ECU에는 온도, 전압, 주행거리처럼 숫자형 데이터도 많다.
- 값의 범위에 맞춰 필요한 바이트 수를 정해 저장한다.
| 항목 | 실제 값 | 전송 값 | 바이트 수 |
|---|---|---|---|
| 온도 | 90°C | 0x5A |
1 |
| 전압 | 12.4V | 10배 후 124 = 0x007C |
2 |
| 주행거리 | 100000km | 0x0186A0 |
3 |
uds_ecu.cpp · 사용할 값
static const unsigned char FAKE_TEMP[] = { 0x5A };
static const unsigned char FAKE_VOLT[] = { 0x00, 0x7C };
static const unsigned char FAKE_ODO[] = { 0x01, 0x86, 0xA0 };
터미널 1·ECU
./uds_ecu can1
터미널 3·버스 관찰
candump can1
터미널 2·확인
./uds_client can0 22F191
./uds_client can0 22F192
./uds_client can0 22F193
심화 과제 1 결과 : 바이트만으로는 의미를 알 수 없음#
| 요청 | 응답 | 실제 의미 | 현재 진단기 출력 |
|---|---|---|---|
22F191 |
62 F1 91 5A |
90°C | "Z" |
22F192 |
62 F1 92 00 7C |
12.4V | ".|" |
22F193 |
62 F1 93 01 86 A0 |
100000km | "..." |
주행거리 실측
[응답] (6바이트) 62 F1 93 01 86 A0
성공 (요청 22 + 0x40 = 62)
항목 F193 의 값 : ...
- 같은 바이트라도 DID마다 해석 방식이 다르다.
- 진단기는 해당 DID가 문자열인지 숫자인지, 몇 바이트인지, 배율이 있는지를 알고 있어야 한다.
- 실제 차량에서는 이런 정보를 ODX 같은 진단 데이터 파일로 관리한다.
심화 과제 2. 세션에 따라 응답 달리하기#
- 실차에서는 특정 DID를 아무 세션에서나 읽을 수 없는 경우가 있다.
- 주행거리 DID
0xF193을 기본 세션에서는 거절하고 확장 세션에서만 읽을 수 있게 만든다. 10 03은 확장 세션,10 01은 기본 세션으로 전환하는 요청이다.
순서대로 확인
./uds_client can0 22F193
./uds_client can0 1003
./uds_client can0 22F193
./uds_client can0 1001
./uds_client can0 22F193
심화 과제 2 결과 : ECU가 상태를 기억함#
버스 실측
can1 7E0 [04] 03 22 F1 93
can1 7E8 [04] 03 7F 22 22
can1 7E0 [03] 02 10 03
can1 7E8 [03] 02 50 03
can1 7E0 [04] 03 22 F1 93
can1 7E8 [07] 06 62 F1 93 01 86 A0
can1 7E0 [03] 02 10 01
can1 7E0 [04] 03 22 F1 93
can1 7E8 [04] 03 7F 22 22
| 현재 세션 | 22F193 응답 |
이유 |
|---|---|---|
| 기본 세션 | 7F 22 22 |
읽을 조건이 충족되지 않음 |
| 확장 세션 | 62 F1 93 01 86 A0 |
조건이 충족되어 응답 |
- 같은
22F193요청이어도 이전에 받은10 03,10 01요청에 따라 응답이 달라진다. - ECU가 이전 요청으로 바뀐 상태를 기억하는 것이 진단 세션의 핵심이다.
- 실제 차량에서는 일정 시간이 지나면 기본 세션으로 돌아가기 때문에 TesterPresent
0x3E를 주기적으로 보내 연결 상태를 유지한다.
부록. 심화 과제 1의 답 (1/2)#
- 숫자형 DID와 값을 상수로 선언한다.
uds_ecu.cpp
static const unsigned short DID_TEMP = 0xF191;
static const unsigned short DID_VOLT = 0xF192;
static const unsigned short DID_ODO = 0xF193;
static const unsigned char FAKE_TEMP[] = { 0x5A };
static const unsigned char FAKE_VOLT[] = { 0x00, 0x7C };
static const unsigned char FAKE_ODO[] = { 0x01, 0x86, 0xA0 };
부록. 심화 과제 1의 답 (2/2)#
uds_ecu.cpp · 숫자형 DID 응답
} else if (did == DID_TEMP) {
resp[0] = (unsigned char)(sid + 0x40);
resp[1] = req[1];
resp[2] = req[2];
std::memcpy(resp + 3, FAKE_TEMP, sizeof(FAKE_TEMP));
resp_len = 3 + sizeof(FAKE_TEMP);
std::printf(" -> 온도 요청. %d 도로 답한다\n", FAKE_TEMP[0]);
} else if (did == DID_VOLT) {
resp[0] = (unsigned char)(sid + 0x40);
resp[1] = req[1];
resp[2] = req[2];
std::memcpy(resp + 3, FAKE_VOLT, sizeof(FAKE_VOLT));
resp_len = 3 + sizeof(FAKE_VOLT);
std::printf(" -> 전압 요청. 2 바이트로 답한다\n");
} else if (did == DID_ODO) {
resp[0] = (unsigned char)(sid + 0x40);
resp[1] = req[1];
resp[2] = req[2];
std::memcpy(resp + 3, FAKE_ODO, sizeof(FAKE_ODO));
resp_len = 3 + sizeof(FAKE_ODO);
std::printf(" -> 주행거리 요청. 3 바이트로 답한다\n");
}
부록. 심화 과제 2의 답 (1/2)#
main()진입 후while(true)이전에 현재 세션 상태를 저장할 변수를 둔다.0x10세션 요청이 들어오면0x01은 기본 세션,0x03은 확장 세션으로 상태를 갱신한다.
uds_ecu.cpp · 세션 상태 관리
bool extended_session = false;
uds_ecu.cpp · 세션 요청 처리
} else if (sid == SID_SESSION && req_len >= 2) {
extended_session = (req[1] == 0x03);
resp[0] = (unsigned char)(sid + 0x40);
resp[1] = req[1];
resp_len = 2;
std::printf(" -> 세션 시작 요청 (%02X). 받아들인다\n", req[1]);
}
부록. 심화 과제 2의 답 (2/2)#
- 주행거리 DID에서 현재 세션을 확인해 기본 세션이면 NRC
0x22로 거절하고 확장 세션이면 값을 반환한다.
uds_ecu.cpp · 세션별 DID_ODO 응답
} else if (did == DID_ODO) {
if (!extended_session) {
resp[0] = NEGATIVE;
resp[1] = sid;
resp[2] = NRC_CONDITIONS_NOT_CORRECT;
resp_len = 3;
std::printf(" -> 주행거리 요청이지만 기본 세션. 거절한다\n");
} else {
resp[0] = (unsigned char)(sid + 0x40);
resp[1] = req[1];
resp[2] = req[2];
std::memcpy(resp + 3, FAKE_ODO, sizeof(FAKE_ODO));
resp_len = 3 + sizeof(FAKE_ODO);
std::printf(" -> 주행거리 요청. 확장 세션에서 응답\n");
}
}