Skip to content

실습 목표#


  • 지난 시간에 조사한 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만 허용하고 다른 값은 NRC 0x12로 거절한다.
  • 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이 아니면 NRC 0x13으로 거절한다.
  • 길이 검사를 통과한 경우에만 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 긍정 응답, 3E017F 3E 12, 11017F 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");
    }
}