실습 목표#
cansend,candump의 동작을 C++ 코드로 직접 구현하고canfd_frame,flags, MTU의 의미를 확인한다.- CAN FD 소켓 옵션과 프레임 필드 구성, 수신 프레임 종류 판별 로직을 완성하고 실제 송수신 결과를 검증한다.
- BRS를 직접 켜고 끄며 버스 부하율과 전달 시간 변화를 비교한다.
- 수신 필터와 64바이트 길이 제한 등 SocketCAN의 추가 동작을 확인한다.
실습 전 준비. 파일 구조와 실행 흐름 확인#
can_send.cpp와can_recv.cpp는 1타임에서 사용한cansend,candump의 동작을 C++ 코드로 구현한 파일이다.- 두 프로그램이 함께 사용하는 소켓 개방, 데이터 변환, 프레임 출력 기능은
common.h에 들어 있다.
| 파일 | 역할 |
|---|---|
common.h |
소켓 열기 · 16진 문자열 변환 · 프레임 화면 출력 |
can_send.cpp |
인자 읽기 → 구조체 채우기 → write() 전송 |
can_recv.cpp |
소켓 열기 → read() 반복 → 화면 출력 |
- 송신 프로그램은 소켓 개방 → 프레임 구성 →
write()전송 → 소켓 종료 순서로 동작한다. - 수신 프로그램도 같은 구조이며
write()대신read()를 반복 호출한다.
CAN FD 프레임과 구조체의 대응#
- 코드에서 직접 값을 넣는 것은 ID, 길이, BRS, 데이터 네 부분이다.
- SOF, CRC, ACK, EOF 등 나머지 필드는 CAN 컨트롤러가 전송 과정에서 구성한다.
| 프레임 필드 | 구조체 멤버 | 채우는 쪽 |
|---|---|---|
| ID | can_id |
코드 |
| DLC(길이) | len |
코드 |
| BRS | flags |
코드 |
| Data | data |
코드 |
| ESI | flags |
컨트롤러 |
| SOF · CRC · ACK · EOF | 없음 | 컨트롤러 |
canfd_frame은 직접 정의한 구조체가 아니라 리눅스 커널 헤더linux/can.h에 정의되어 있다.len은 데이터 길이,flags는 CAN FD 플래그,__res0,__res1은 예약 영역이다.
라즈베리파이·터미널
grep -n -A 8 "struct canfd_frame" /usr/include/linux/can.h
flags 필드와 MTU#
flags는 1바이트이며 각 비트가 서로 다른 의미를 가진다.- 이번 실습에서 코드가 직접 설정하는 것은
CANFD_BRS하나이다.
| 비트 | 상수 | 값 | 의미 |
|---|---|---|---|
| bit 0 | CANFD_BRS |
0x01 |
데이터 구간을 빠른 속도로 전송 |
| bit 1 | CANFD_ESI |
0x02 |
송신 노드가 Error Passive 상태 |
| bit 2 | CANFD_FDF |
0x04 |
CAN FD 프레임 표시 |
flags를0으로 두면 BRS가 꺼진 상태로 전송된다.- Classical CAN의
can_frame은 16바이트, CAN FD의canfd_frame은 72바이트이다.
linux/can.h
#define CAN_MTU (sizeof(struct can_frame))
#define CANFD_MTU (sizeof(struct canfd_frame))
코드 해설. 명령줄 인자와 구조체 초기화#
- 실행 시 전달한 값은
argv배열에 저장되고argc는 인자 개수를 나타낸다.
실행 예시
./can_send can0 123 DEADBEEF
can_send.cpp
const char *ifname = (argc > 1) ? argv[1] : "can0";
unsigned int can_id = (argc > 2) ? std::strtoul(argv[2], nullptr, 16) : 0x123;
const char *hex = (argc > 3) ? argv[3] : "DEADBEEF";
write()에는 프레임 데이터의 주소와 구조체 크기를 전달한다.- 실제 데이터 길이와 상관없이
sizeof(frame)은 항상72바이트이다.
can_send.cpp
write(sock, &frame, sizeof(frame));
- 구조체는
memset()으로 전체를0으로 초기화한다. - 따라서 별도로 설정하지 않은
flags는0으로 남아 BRS가 비활성 상태가 된다.
can_send.cpp
struct canfd_frame frame;
std::memset(&frame, 0, sizeof(frame));
시범 1. CAN FD 소켓 옵션 활성화#
common.h의 TODO 1에서CAN_RAW_FD_FRAMES옵션을 활성화한다.- 이 옵션을 켜지 않으면 소켓은 16바이트 Classical CAN 프레임만 처리한다.
common.h · TODO 1
int enable = 1;
if (setsockopt(sock, SOL_CAN_RAW, CAN_RAW_FD_FRAMES,
&enable, sizeof(enable)) < 0) {
perror("setsockopt CAN_RAW_FD_FRAMES");
close(sock);
return -1;
}
관찰 포인트
setsockopt()는 설정값 자체가 아니라 설정값의 주소 &enable과 크기 sizeof(enable)를 전달함
시범 2. 송신 프레임 필드 구성#
can_send.cpp의 TODO 2에서 보낼 프레임의 ID, 길이, 데이터를 채운다.parse_hex()가 문자열을 바이트 배열로 바꾸고 실제로 채운 바이트 수를 반환한다.- 반환값을 그대로
frame.len에 넣으므로 데이터와 길이가 어긋나지 않는다.
can_send.cpp · TODO 2
frame.can_id = can_id;
frame.len = parse_hex(hex, frame.data, CANFD_MAX_DLEN);
"DEADBEEF"는parse_hex()를 거쳐DE AD BE EF네 바이트로 변환된다.CANFD_MAX_DLEN은 최대 64바이트를 넘겨 쓰지 못하게 제한한다.
시범 3. 수신 프레임 종류 판별#
can_recv.cpp의 TODO 3에서 받은 프레임이 CAN FD인지 판별한다.- 프레임 내부의 FDF 비트를 직접 보는 것이 아니라
read()가 반환한 크기로 구분한다.
can_recv.cpp · TODO 3
bool is_fd = (nbytes == (ssize_t)CANFD_MTU);
CANFD_MTU는 72바이트이므로read()가 72를 반환하면 CAN FD 프레임으로 판단한다.
실습 준비. CAN 인터페이스 되돌리기#
- 1타임 마지막에
can0,can1을 Classical CAN 모드로 두었으므로 CAN FD 모드로 다시 올린다. canfd_frame은 72바이트이기 때문에 인터페이스가 Classical CAN 모드이면write()에서Invalid argument가 발생한다.
방법 1·스크립트 사용
~/canup.sh
방법 2·직접 설정
sudo ip link set can0 down
sudo ip link set can1 down
sudo ip link set can0 up type can bitrate 500000 dbitrate 2000000 fd on restart-ms 0
sudo ip link set can1 up type can bitrate 500000 dbitrate 2000000 fd on restart-ms 0
설정 확인
~/precheck.sh
관찰 포인트
mtu 72와 dbitrate 2000000이 보이면 CAN FD 모드로 정상 설정된 상태
실습 1. 구현하고 주고받기#
- 앞에서 확인한 TODO 세 곳을 직접 채우고 프로그램을 빌드한다.
- 터미널 두 개를 열어 한쪽은 수신, 다른 쪽은 송신에 사용한다.
빌드
cd ~/can_lab/2time && make
터미널 1·수신
./can_recv can1
터미널 2·송신
./can_send can0 123 DEADBEEF
./can_send can0 456 112233445566778899AABBCC

관찰 포인트
수신 터미널에서 ID 123의 4바이트 데이터와 ID 456의 12바이트 데이터가 정상적으로 출력되는지 확인
실습 2. 소켓 옵션을 끄고 보내기#
common.h의setsockopt()블록을 삭제하지 말고 주석 처리한다.

- 다시 빌드한 뒤 4바이트와 12바이트 데이터를 각각 보내고 결과를 비교한다.
- 실행 전에 어느 경우가 실패할지 먼저 예측한다.
터미널 2·송신
./can_send can0 123 DEADBEEF
./can_send can0 456 112233445566778899AABBCC

실습 2 결과 : 전송 실패 원인#
- 데이터 크기와 관계없이 두 경우 모두 실패한다.
- 원인은 데이터 길이가 아니라
write()에 넘기는canfd_frame구조체 전체 크기가 72바이트이기 때문이다. - CAN FD 소켓 옵션을 끄면 소켓은 16바이트 프레임만 받으므로 72바이트 구조체를 거절한다.
can_send.cpp
write(sock, &frame, sizeof(frame));
- 정상 통신을 위해서는 인터페이스와 소켓이 모두 CAN FD 모드여야 한다.
- 둘 중 하나라도 Classical CAN 모드이면
Invalid argument가 발생한다.
도전 과제. BRS 켜고 끄기#
- 현재는
flags를 설정하지 않아 BRS가 항상 꺼진 상태이다. - 네 번째 인자가
nobrs이면 BRS를 끄고, 그 외에는 BRS를 켜도록frame.flags를 설정한다.
기대 동작
./can_send can0 456 112233445566778899AABBCC
./can_send can0 456 112233445566778899AABBCC nobrs
- 인자 판별 결과는 이미
use_brs변수에 들어 있다.
can_send.cpp · 인자 판별
bool use_brs = !(argc > 4 && std::strcmp(argv[4], "nobrs") == 0);
관찰 포인트
use_brs가 true이면 CANFD_BRS, false이면 0을 frame.flags에 넣으면 됨

도전 과제 결과 : 구현 확인#
- 인자 없이 보내면 수신 로그에
(CAN FD, BRS)가 표시된다. nobrs를 붙이면(CAN FD)만 표시된다.- 빌드할 때
use_brs관련 미사용 경고가 사라지는지도 확인한다.
BRS 켬
[수신 1] can1 456 [12] 11 22 ... CC (CAN FD, BRS)
BRS 끔
[수신 2] can1 456 [12] 11 22 ... CC (CAN FD)
실습 3. BRS에 따른 버스 부하율 측정#
- 동일한 데이터를 반복 전송하면서 BRS ON/OFF에 따른 버스 부하율을 비교한다.
- 다섯 번째 인자로 반복 횟수를 지정할 수 있다.
- 측정값 자체보다 두 조건의 상대적 차이를 비교한다.
터미널 1·부하 측정
canbusload can0@500000,2000000 -r -t -b -c
터미널 2·송신
./can_send can0 456 112233445566778899AABBCC brs 3000
./can_send can0 456 112233445566778899AABBCC nobrs 3000

실습 3 결과 : BRS에 따른 전송 시간 차이#
- BRS를 끄면 동일한 12바이트 데이터라도 버스 점유 시간이 길어져 부하율이 증가한다.
- BRS ON에서는 데이터 구간이
2 Mbit/s, BRS OFF에서는 데이터 구간도500 kbit/s로 전송된다. - 프레임 크기는 같지만 데이터 구간의 전송 시간이 달라진다.
주요 오류 유형#
| 오류 메시지 | 원인 | 조치 |
|---|---|---|
Network is down |
인터페이스가 내려가 있음 | ~/canup.sh 실행 |
Invalid argument |
인터페이스와 소켓 모드가 어긋남 | 양쪽 설정 확인 |
No buffer space available |
송신 큐가 가득 참 | 잠시 기다렸다 재시도 |
No buffer space available은 완성 코드의 재시도 로직에서 자동 처리된다.
정리#
- SocketCAN 송신은 소켓 개방 → 프레임 구조체 구성 →
write()전송 순서로 이루어진다. - 애플리케이션이 직접 설정하는 주요 필드는 ID, 길이, 데이터, BRS이다.
- 정상적인 CAN FD 통신을 위해서는 인터페이스와 소켓이 모두 CAN FD를 지원하도록 설정되어야 한다.
- BRS를 활성화하면 데이터 구간만 더 빠른 속도로 전송되어 버스 점유 시간과 전달 시간이 줄어든다.
- 이번에 구현한 송수신 구조는 이후 ISO-TP와 같은 대용량 데이터 분할 전송 구현의 기반이 된다.
구현 결과
./can_send can0 ID 데이터 [brs|nobrs] [반복]
./can_recv can1
심화 과제 1. 특정 ID만 받기#
- 소켓에 필터를 걸어 원하는 CAN ID만 수신하도록 만든다.
- 먼저 필터 없이
123,456두 ID를 보내 보고, 필터를 적용한 뒤 결과를 비교한다. - 코드는
common.h의bind()블록이 끝난 뒤return sock;앞에 넣는다.
터미널 3·준비와 빌드
~/canup.sh
cd ~/can_lab/2time && make clean && make
터미널 1·수신
./can_recv can1
터미널 2·송신
./can_send can0 123 DEADBEEF
./can_send can0 456 DEADBEEF

심화 과제 1 결과 : 필터는 커널에서 적용#
- 송신 프레임은 모두 버스로 나가지만 필터에서 걸러진 프레임은
read()까지 올라오지 않는다. - 커널은 아래 조건으로 ID를 비교한다.
필터 조건
(받은 ID & can_mask) == (can_id & can_mask)
can_mask = 0x7FF이면 11비트를 모두 비교하므로0x123만 통과한다.can_mask = 0x7F0이면 끝 4비트를 비교하지 않으므로0x120,0x12F는 통과하고0x130은 걸러진다.
계산 예
0x12F & 0x7F0 = 0x120
0x130 & 0x7F0 = 0x130
심화 과제 2. 64바이트와 그 너머#
- 64바이트를 정확히 보내고 65, 68, 72바이트 데이터를 각각 넣어 결과를 확인한다.
- 심화 과제 1의 필터를 넣었다면 먼저 필터 코드를 제거하고 다시 빌드한다.
터미널 1·수신
./can_recv can1
터미널 2·길이별 송신
for n in 64 65 68 72; do
DATA=$(printf 'AA%.0s' $(seq 1 $n))
printf "%2d 바이트 (%3d 자 ) -> " $n ${#DATA}
./can_send can0 123 $DATA | grep -o "\\[..\\]"
done

심화 과제 2 결과 : 길이가 잘리는 위치#
- 네 경우 모두
[64]로 전송되며 오류나 경고는 발생하지 않는다. - 64바이트를 넘는 데이터를 자르는 것은 커널이 아니라
parse_hex()의len < max_len조건이다.
can_send.cpp · parse_hex
for (int i = 0; text[i] != '\\0' && text[i + 1] != '\\0' && len < max_len; i += 2) {
max_len이 64이므로 64바이트를 넘는 입력은 오류 없이 잘린다.- 수신 측도
[64]만 보므로 원래 입력이 몇 바이트였는지는 알 수 없다.
심화 과제 3. 전달 시간 재기#
- 송신 시각과 수신 시각을 각각
CLOCK_MONOTONIC으로 기록하고 두 값을 비교한다. - 같은 라즈베리파이에서 실행하므로 두 시각을 직접 비교할 수 있다.
- 측정값은 왕복 시간이 아니라 프레임 하나가 한쪽에서 다른 쪽으로 전달되는 편도 시간이다.
- 수정 위치는
can_send.cpp,can_recv.cpp의 네 군데이며 구체적인 코드는 맨 아래 부록에서 확인한다.
터미널 3·빌드
cd ~/can_lab/2time && make clean && make
터미널 1·수신
./can_recv can1
터미널 2·세 경우 송신
./can_send can0 123 DEADBEEF
DATA=$(printf 'AA%.0s' {1..64})
./can_send can0 123 $DATA
./can_send can0 123 $DATA nobrs

심화 과제 3 결과 : 전달 시간 비교#
- PPT의 측정 예에서는 4바이트 BRS ON이 약
0.530 ms, 64바이트 BRS ON이 약0.839 ms, 64바이트 BRS OFF가 약1.639 ms로 측정되었다. - 64바이트는 512비트이므로 데이터 구간의 이론 전송 시간은 다음과 같다.
64바이트 데이터 구간 계산
2 Mbit/s : 512 / 2000000 = 0.256 ms
500 kbit/s : 512 / 500000 = 1.024 ms
차이 : 0.768 ms
- 실제 측정값 차이가 조금 더 큰 것은 CRC와 스터핑 비트도 데이터 구간 속도로 전송되기 때문이다.
- 고정 지연에는
write, SPI, 중재, SPI,read등이 포함되며 BRS를 켜고 끌 때 달라지는 것은 주로 버스의 데이터 구간이다.
부록. 시범 세 곳의 답#
- 앞에서 진행한 TODO 1~3의 완성 코드를 한 번에 확인한다.
TODO 1. common.h#
common.h
int enable = 1;
if (setsockopt(sock, SOL_CAN_RAW, CAN_RAW_FD_FRAMES,
&enable, sizeof(enable)) < 0) {
perror("setsockopt CAN_RAW_FD_FRAMES");
close(sock);
return -1;
}

TODO 2. can_send.cpp#
can_send.cpp
frame.can_id = can_id;
frame.len = parse_hex(hex, frame.data, CANFD_MAX_DLEN);

TODO 3. can_recv.cpp#
can_recv.cpp
bool is_fd = (nbytes == (ssize_t)CANFD_MTU);

부록. 도전 과제의 답#
- BRS 설정은 삼항 연산자 또는
if문으로 구현할 수 있다. - 두 방식은 동일하게 동작한다.
짧게 쓰는 방법
frame.flags = use_brs ? CANFD_BRS : 0;
조건문으로 쓰는 방법
if (use_brs) {
frame.flags = CANFD_BRS;
} else {
frame.flags = 0;
}

부록. 심화 과제의 답 (1/2)#
- 심화 과제 1의 특정 ID 수신 필터 코드는
common.h의bind()블록이 끝난 뒤return sock;앞에 넣는다.
common.h · 넣을 코드
struct can_filter flt;
flt.can_id = 0x123;
flt.can_mask = CAN_SFF_MASK;
if (setsockopt(sock, SOL_CAN_RAW, CAN_RAW_FILTER,
&flt, sizeof(flt)) < 0) {
perror("setsockopt CAN_RAW_FILTER");
close(sock);
return -1;
}

부록. 심화 과제의 답 (2/2)#
- 심화 과제 3의 전달 시간 측정을 위해
can_send.cpp,can_recv.cpp두 파일에 네 군데를 수정한다.
| 파일 | 넣을 자리 | 내용 |
|---|---|---|
can_send.cpp |
#include <cerrno> 아래 |
#include <ctime> |
can_send.cpp |
소켓에 write() 하기 전 |
송신 시각 기록 |
can_recv.cpp |
#include "common.h" 아래 |
#include <ctime> |
can_recv.cpp |
수신 로그 출력 전 | 수신 시각 기록 |
can_send.cpp 와 can_recv.cpp· 헤더추가
#include <ctime>
can_send.cpp · write() 전
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
std::printf("[ 보냄 %ld.%09ld]\n", ts.tv_sec, ts.tv_nsec);

can_recv.cpp · 수신 출력 전
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
std::printf("[ 받음 %ld.%09ld]\n", ts.tv_sec, ts.tv_nsec);
