실습 목표#
- 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으로 바꾸며 프레임 수를 센다.
클래식#





CAN FD#





실습 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);

시범 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);

실습 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

도전 과제. 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

정리#
- PCI의 첫 바이트만 보면 SF, FF, CF, FC 종류와 CF 순번을 구분할 수 있다.
- 수신 측은
expected와next_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 결과 : 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이므로 기존 실습 동작에는 영향을 주지 않는다.