Skip to content

실습 목표#


  • Classic CAN에서 8바이트를 넘는 데이터를 ISO-TP가 어떻게 SF, FF, CF, FC로 나누어 전송하는지 확인한다.
  • isotpsend, isotprecv, candump를 이용해 분할 전송 흐름과 PCI의 의미를 직접 관찰한다.
  • isotp_recv.cpp의 SF, FF 처리 코드를 완성하고 CF 순번을 확인하여 여러 프레임으로 나뉜 데이터를 재조립한다.
  • CAN FD의 2바이트 SF PCI와 STmin 동작까지 확인하여 이후 진단 응답 수신에 사용할 ISO-TP 수신 구조를 이해한다.

ISO-TP 분할 전송 구조#


  • Classic CAN 프레임에는 데이터를 최대 8바이트까지 담을 수 있다.
  • ISO-TP에서는 그중 일부를 PCI(Protocol Control Information)로 사용하므로 한 프레임에 담을 수 있는 실제 데이터가 줄어든다.
  • 7바이트를 넘는 진단 응답처럼 한 프레임에 담을 수 없는 데이터는 여러 프레임으로 나누고 다시 합쳐야 한다.
  • 이 분할·재조립 규격이 ISO 15765-2이며 ISO-TP(ISO Transport Protocol)라고 부른다.

PCI : 첫 바이트가 말하는 네 가지#

  • PCI의 상위 니블은 프레임 종류, 하위 니블은 길이나 순번을 나타낸다.
상위 니블 종류 의미
0x SF 짧은 데이터를 한 프레임에 저장
1x FF 긴 데이터의 첫 조각, 전체 길이를 예고
2x CF 순번이 부여된 연속 조각
3x FC 받는 쪽이 보내는 흐름 제어 신호
  • 예를 들어 0x10의 상위 니블 1은 FF를 의미하고, 하위 니블 0은 전체 길이의 윗자리에 사용된다.
FF 예시
10 14 01 02 03 04 05 06
  • 10 14를 길이로 합치면 0x014 = 20바이트이다.

CAN FD의 SF : PCI 두 바이트#

  • Classical CAN의 SF는 첫 바이트 하위 니블에 길이를 적기 때문에 최대 7바이트까지 표현할 수 있다.
  • CAN FD는 한 프레임에 더 많은 데이터를 담으므로 SF에서 첫 바이트를 00으로 두고 둘째 바이트에 길이를 적는다.
Classical CAN SF · 5바이트
05 01 02 03 04 05
CAN FD SF · 20바이트
00 14 01 02 03 04 05 06 ... 13 14

시범 1. 받는 쪽이 없을 때#


  • 20바이트 데이터를 전송하고 버스에 어떤 프레임이 나가는지 확인한다.
  • 받는 쪽이 없으면 FF 한 장만 나가고 이후 CF는 전송되지 않는다.
터미널 1·관찰
candump can1
터미널 2·송신
isotpsend -s 123 -d 321 -D 20 can0
관찰 결과
can1  123  [8]  10 14 01 02 03 04 05 06
  • 송신 측은 FF를 보낸 뒤 수신 측의 FC를 기다리기 때문에 여기서 멈춘다.

시범 2. 받는 쪽이 있을 때#


  • 수신 프로그램을 먼저 실행하면 FF를 받은 뒤 수신 측이 FC를 보내고, 이후 CF가 이어서 전송된다.
  • FC는 수신 측이 보내므로 데이터 프레임과 CAN ID 방향이 반대이다.
터미널 1·관찰
candump can1
터미널 2·수신 대기
isotprecv -s 321 -d 123 can1
터미널 3·송신
isotpsend -s 123 -d 321 -D 20 can0
관찰 결과
can1  123  [8]  10 14 01 02 03 04 05 06     FF
can1  321  [3]  30 00 00                    FC
can1  123  [8]  21 07 08 09 0A 0B 0C 0D     CF 1
can1  123  [8]  22 0E 0F 10 11 12 13 14     CF 2

실습 1. 네 줄을 읽어 보기#


  • 위 네 프레임의 PCI를 직접 읽어 종류와 의미를 확인한다.
  • 상위 니블은 종류, 하위 니블은 길이나 순번을 나타낸다.
  • FF는 첫 두 바이트를 합쳐 전체 길이를 표현한다.

실습 1 결과 : 첫 바이트 읽는 법#

첫 바이트 종류
10 14 FF 1은 FF, 0x014는 전체 길이 20바이트
30 00 00 FC 3은 FC, BS=0, STmin=0
21 CF 순번 1, 뒤 7바이트가 데이터
22 CF 순번 2, 여기까지 20바이트가 모두 모임
  • FC만 수신 측이 보내기 때문에 CAN ID가 321로 반대 방향이다.

실습 2. 길이를 바꿔 프레임 수 세기#


  • 전송 길이를 바꿔 가며 Classical CAN과 CAN FD에서 몇 개의 프레임으로 나뉘는지 확인한다.
  • FF 이후 CF를 계속 받으려면 수신 프로그램이 실행되어 있어야 한다.
터미널 1·관찰
candump can1
터미널 2·수신 대기
isotprecv -s 321 -d 123 can1              # 클래식
isotprecv -s 321 -d 123 -L 72:64:0 can1   # CAN FD   
터미널 3·송신
isotpsend -s 123 -d 321 -D 20 can0              # 클래식
isotpsend -s 123 -d 321 -L 72:64:0 -D 20 can0   # CAN FD
  • -D 뒤의 값을 7, 8, 20, 62, 63으로 바꾸며 프레임 수를 센다.

클래식#

실습 2 결과사진1 실습 2 결과사진2


실습 2 결과사진3 실습 2 결과사진4


실습 2 결과사진5 실습 2 결과사진6


실습 2 결과사진7 실습 2 결과사진8


실습 2 결과사진9 실습 2 결과사진10

CAN FD#

실습 2 결과사진11 실습 2 결과사진12


실습 2 결과사진13 실습 2 결과사진14


실습 2 결과사진15 실습 2 결과사진16


실습 2 결과사진17 실습 2 결과사진18


실습 2 결과사진19 실습 2 결과사진20

실습 2 결과#

  • 한 프레임에 담을 수 있는 데이터는 프레임 크기에서 PCI 크기를 뺀 값이다.
구분 프레임 크기 PCI 크기 한 프레임 데이터 분할 시작
Classical CAN 8 1 7바이트 8바이트부터
CAN FD 64 2 62바이트 63바이트부터
  • 경계값 자체보다 프레임 크기 - PCI 크기로 계산하는 원리를 이해하는 것이 중요하다.

  • 20바이트를 보낼 때 Classical CAN은 FF, FC, CF 2개가 필요하지만 CAN FD는 SF 한 장으로 끝난다.

Classical CAN · 프레임 4개
can1  123  [ 8]  10 14 01 02 03 04 05 06        FF
can1  321  [ 3]  30 00 00                       FC
can1  123  [ 8]  21 07 08 09 0A 0B 0C 0D        CF 1
can1  123  [ 8]  22 0E 0F 10 11 12 13 14        CF 2
CAN FD · 프레임 1개
can1  123  [24]  00 14 01 02 03 ... 13 14       SF
  • CAN FD에서는 분할과 FC 대기 과정이 없어져 프레임 수와 통신 절차가 줄어든다.

수신 프로그램 : 조립 상태를 들고 있기#


  • 조각을 다시 합치려면 이전 프레임에서 어디까지 받았는지 상태를 유지해야 한다.
  • Assembly 구조체에 전체 길이, 현재까지 받은 길이, 다음 CF 순번과 데이터를 저장한다.
isotp_recv.cpp
struct Assembly {
    bool active;
    unsigned int expected;
    unsigned int received;
    int next_seq;
    unsigned char buf[4095];
};
멤버 의미
active 현재 조립 중인지 여부
expected FF가 예고한 전체 데이터 길이
received 지금까지 모은 데이터 길이
next_seq 다음에 와야 할 CF 순번
buf 재조립 데이터를 저장하는 버퍼

시범 3. SF 처리 TODO 1#


  • pci == 0x0이면 데이터가 한 프레임에 모두 들어 있는 SF이다.
  • Classical CAN SF에서는 첫 바이트 하위 니블이 데이터 길이이다.
  • 데이터는 PCI 한 바이트 뒤인 frame.data + 1부터 시작하며 FC는 보내지 않는다.
isotp_recv.cpp · TODO 1
unsigned int len = frame.data[0] & 0x0F;
std::printf("[SF]   길이 %u\n", len);
std::memcpy(a.buf, frame.data + 1, len);
a.received = len;
a.active   = false;
print_pdu(a);

TODO 1 코드 위치사진

시범 4. FF 처리 TODO 2#


  • pci == 0x1이면 긴 데이터의 첫 조각인 FF이다.
  • 첫 두 바이트에서 전체 길이를 계산하고, PCI 두 바이트 뒤의 데이터를 버퍼에 복사한다.
  • FF를 받은 뒤 FC를 보내야 송신 측이 CF를 계속 전송한다.
isotp_recv.cpp · TODO 2
a.expected = ((frame.data[0] & 0x0F) << 8) | frame.data[1];
a.next_seq = 1;
a.active   = true;
unsigned int n_copy = frame.len - 2;
std::memcpy(a.buf, frame.data + 2, n_copy);
a.received = n_copy;
send_fc(sock, tx_id);

TODO 2 코드 위치사진

실습 3. 채운 코드 실행하기#


  • TODO 1과 TODO 2를 채운 뒤 빌드하여 SF와 FF 수신을 확인한다.
  • 긴 데이터도 끝까지 조립되지만 아직 CF 순번을 읽지 않았기 때문에 순번이 모두 0으로 출력된다.
터미널 1·수신
make
./isotp_recv can1 123 321
터미널 3·버스 관찰
candump can1
터미널 2·송신
./isotp_send can0 123 321 5
./isotp_send can0 123 321 20
기대 출력
[SF]   길이 5
[완성] 5 바이트 : 01 02 03 04 05
[FC]   321 : 30 00 00   보냄
[CF 0] 13 / 20
[CF 0] 20 / 20
[완성] 20 바이트 : 01 02 ... 14

실습3 결과사진 실습3 결과사진

도전 과제. CF 순번 확인하기#


  • CF의 첫 바이트 하위 니블에는 순번이 들어 있다.
  • 현재 코드의 int seq = 0;을 수정하여 실제 CF 순번을 꺼낸다.
  • 받은 순번이 a.next_seq와 다르면 조립을 중단하고 다음 프레임으로 넘어가도록 한다.
isotp_recv.cpp · 수정할 부분
} else if (pci == 0x2) {
    if (!a.active) { continue; }
    int seq = 0; 

관찰 포인트

frame.data[0] & 0x0F로 하위 니블만 남기고, seq != a.next_seq이면 조립을 중단함

도전 과제의 답#


  • CF 첫 바이트의 하위 니블에서 순번을 꺼내고, 예상 순번과 다르면 조립을 중단한다.
isotp_recv.cpp
int seq = frame.data[0] & 0x0F;
if (seq != a.next_seq) {
    std::printf("[CF]   순번이 어긋남. %d 를 기다렸는데 %d 가 왔다\n",
                a.next_seq, seq);
    a.active = false;
    continue;
}

도전 과제. 구현 검증#


  • 20바이트와 150바이트 데이터를 보내 CF 순번이 제대로 출력되는지 확인한다.
  • 순번은 15 다음에 다시 0으로 돌아간다.
터미널 1·수신
make
./isotp_recv can1 123 321
터미널 3·버스 관찰
candump can1
터미널 2·송신
./isotp_send can0 123 321 20
./isotp_send can0 123 321 150
기대 출력
[CF 1] 13 / 20
[CF 2] 20 / 20
[완성] 20 바이트 : 01 02 03 ... 14
[CF 15] 111 / 150
[CF 0]  118 / 150

도전과제 결과사진1 도전과제 결과사진2

정리#


  • PCI의 첫 바이트만 보면 SF, FF, CF, FC 종류와 CF 순번을 구분할 수 있다.
  • 수신 측은 expectednext_seq를 이용해 데이터가 얼마나 더 와야 하는지와 중간 조각 유실 여부를 확인할 수 있다.
어디서 얻나 확인할 수 있는 것
expected FF의 두 PCI 바이트 전체 길이와 남은 데이터 양
next_seq CF의 하위 니블 CF가 순서대로 왔는지 여부
유실을 확인하는 예
[CF 1] 13 / 20
[CF]   순번이 어긋남. 2 를 기다렸는데 4 가 왔다
  • 여기서 만든 ISO-TP 수신 구조는 이후 진단 응답 재조립에 그대로 사용한다.

심화 과제 1. 우리 코드의 경계 찾기#


  • isotpsend 도구는 Classical CAN에서 7/8, CAN FD에서 62/63이 분할 경계였다.
  • 직접 만든 isotp_send의 경계가 어디인지 7, 8, 62, 63바이트를 보내며 확인한다.
터미널 1·수신
./isotp_recv can1 123 321
터미널 3·버스 관찰
candump can1
터미널 2·송신
./isotp_send can0 123 321 7
./isotp_send can0 123 321 8
./isotp_send can0 123 321 62
./isotp_send can0 123 321 63

심화 과제 1 결과 : 우리 코드의 경계는 7/8#

  • 직접 만든 송신기는 8바이트 프레임만 사용하므로 7/8에서 SF와 FF가 갈린다.
  • 62, 63바이트는 모두 FF이며 CF 개수만 다르다.
보낸 길이 프레임 수 첫 바이트 종류 CF 수
7 1 07 SF 0
8 3 10 08 FF 1
62 10 10 3E FF 8
63 11 10 3F FF 9
실측 발췌
can1  123  [08]  07 01 02 03 04 05 06 07
can1  123  [08]  10 08 01 02 03 04 05 06
can1  123  [08]  10 3E 01 02 03 04 05 06
can1  123  [08]  10 3F 01 02 03 04 05 06
can1  123  [02]  29 3F
  • 마지막 프레임 길이는 실제 남은 데이터에 맞춰 짧아질 수 있다.
  • 실차에서는 남는 공간을 00 또는 CC로 채워 8바이트로 맞추는 경우가 많다.

심화 과제 2. CAN FD로 보낸 SF 받기#


  • 만든 수신기로 CAN FD의 20바이트 SF를 받아 본다.
  • 기존 SF 처리에서는 첫 바이트 하위 니블만 길이로 보기 때문에 길이가 0으로 출력된다.
  • candump에서 첫 두 바이트를 확인하고 왜 그런지 분석한다.
터미널 1·수신
./isotp_recv can1 123 321
터미널 3·버스 관찰
candump can1
터미널 2·CAN FD 송신
isotpsend -s 123 -d 321 -L 72:64:0 -D 20 can0

심화과제2 결과사진1 심화과제2 결과사진2

심화 과제 2 결과 : CAN FD SF의 길이 위치#

  • CAN FD SF의 첫 바이트 00은 SF 종류를 나타내며 실제 길이는 둘째 바이트에 있다.
  • 따라서 00 14 ...를 Classical CAN 방식으로 읽으면 길이 0, CAN FD 방식으로 읽으면 0x14 = 20이 된다.
구분 Classical CAN 방식 CAN FD 방식
길이 위치 첫 바이트 하위 니블 둘째 바이트
길이 값 0 0x14 = 20
데이터 시작 data + 1 data + 2
수정 전후
수정 전
[SF]   길이 0
[완성] 0 바이트 :

수정 후
[SF]   길이 20
[완성] 20 바이트 : 01 02 03 ... 12 13 14
  • 수정 코드는 맨 아래 부록에서 확인한다.

심화 과제 3. STmin으로 CF 간격 조절하기#


  • FC 프레임 30 00 00의 셋째 바이트는 STmin이며 CF 사이의 최소 전송 간격을 지정한다.
  • 마지막 두 인자로 BS와 STmin을 입력할 수 있도록 수신기를 수정한다.
  • BS는 0으로 두고 STmin만 0, 20으로 바꿔 비교한다.
  • -L 옵션을 사용하여 CAN FD 소켓으로 열어야 한다.
터미널 1·수신
./isotp_recv can1 123 321 0 0
./isotp_recv can1 123 321 0 20
터미널 3·간격 관찰
candump -t d can1
터미널 2·송신
isotpsend -s 123 -d 321 -L 72:8:0 -D 150 can0

관찰 포인트

BS를 0이 아닌 값으로 두면 블록마다 FC를 추가로 보내야 하므로 이번 실습에서는 BS=0을 사용함

심화 과제 3 결과 : STmin이 전송 속도에 미치는 영향#

  • PPT의 실측에서는 STmin을 0에서 20으로 바꾸자 전체 전송 시간이 약 33배 증가했다.
STmin FC CF 사이 간격 전체 시간
0 30 00 00 0.57 ms 12.5 ms
20 30 00 14 20.56 ms 412.4 ms
실측 발췌
STmin 0
(000.000480) can1 321 [03] 30 00 00
(000.000629) can1 123 [08] 21 07 08 09 0A 0B 0C 0D
(000.000567) can1 123 [08] 22 0E 0F 10 11 12 13 14

STmin 20
(000.000454) can1 321 [03] 30 00 14
(000.000663) can1 123 [08] 21 07 08 09 0A 0B 0C 0D
(000.020626) can1 123 [08] 22 0E 0F 10 11 12 13 14
  • STmin은 CF와 CF 사이의 간격이므로 FC 다음 첫 번째 CF에는 적용되지 않는다.
  • 순번은 2F 다음 20으로 돌아가며 프로그램 출력도 [CF 15] 다음 [CF 0]이 된다.
  • 수정 코드는 맨 아래 부록에서 확인한다.

부록. 심화 과제 2의 답#


  • 기존 SF 처리에서 첫 바이트 하위 니블이 0이고 프레임 길이가 1보다 크면 CAN FD SF로 판단한다.
  • 이 경우 둘째 바이트를 길이로 읽고 데이터 시작 위치를 data + 2로 바꾼다.
isotp_recv.cpp · SF 처리
unsigned int len;
int off;
if ((frame.data[0] & 0x0F) == 0 && frame.len > 1) {
    len = frame.data[1];  off = 2;          // CAN FD 의 SF
} else {
    len = frame.data[0] & 0x0F;  off = 1;   // 클래식의 SF
}
std::printf("[SF]   길이 %u\n", len);
std::memcpy(a.buf, frame.data + off, len);
a.received = len;
a.active   = false;
print_pdu(a);

심화 과제 코드 위치사진

부록. 심화 과제 3의 답 (1/2)#


  • send_fc()가 BS와 STmin을 인자로 받아 FC의 두 번째, 세 번째 바이트에 넣도록 수정한다.
isotp_recv.cpp · send_fc
static void send_fc(int sock, unsigned int tx_id,
                    unsigned char bs, unsigned char stmin)
{
    struct canfd_frame fc;
    std::memset(&fc, 0, sizeof(fc));
    fc.can_id  = tx_id;
    fc.len     = 3;
    fc.data[0] = 0x30;
    fc.data[1] = bs;      // 한 번에 받을 프레임 수
    fc.data[2] = stmin;   // 프레임 사이 최소 간격 (ms)
    if (write(sock, &fc, sizeof(fc)) != (ssize_t)sizeof(fc)) {
        perror("write FC");
        return;
    }
    std::printf("[FC]   %03X : 30 %02X %02X   보냄\n", tx_id, bs, stmin);
}

부록. 심화 과제 3의 답 (2/2)#


  • main에서 BS와 STmin을 명령줄 인자로 읽는다.
isotp_recv.cpp · main
unsigned char bs    = (argc > 4) ? (unsigned char)std::strtoul(argv[4], nullptr, 0) : 0;
unsigned char stmin = (argc > 5) ? (unsigned char)std::strtoul(argv[5], nullptr, 0) : 0;
  • TODO 2의 FC 호출부에도 두 값을 넘긴다.
isotp_recv.cpp · TODO 2
send_fc(sock, tx_id, bs, stmin);
  • 인자를 주지 않으면 기본값이 0 0이므로 기존 실습 동작에는 영향을 주지 않는다.