Skip to content

실습 목표#


  • cansend, candump의 동작을 C++ 코드로 직접 구현하고 canfd_frame, flags, MTU의 의미를 확인한다.
  • CAN FD 소켓 옵션과 프레임 필드 구성, 수신 프레임 종류 판별 로직을 완성하고 실제 송수신 결과를 검증한다.
  • BRS를 직접 켜고 끄며 버스 부하율과 전달 시간 변화를 비교한다.
  • 수신 필터와 64바이트 길이 제한 등 SocketCAN의 추가 동작을 확인한다.

실습 전 준비. 파일 구조와 실행 흐름 확인#


  • can_send.cppcan_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 프레임 표시
  • flags0으로 두면 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으로 초기화한다.
  • 따라서 별도로 설정하지 않은 flags0으로 남아 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 72dbitrate 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

실습1 결과사진

관찰 포인트

수신 터미널에서 ID 123의 4바이트 데이터와 ID 456의 12바이트 데이터가 정상적으로 출력되는지 확인

실습 2. 소켓 옵션을 끄고 보내기#


  • common.hsetsockopt() 블록을 삭제하지 말고 주석 처리한다.

실습2 코드 주석처리한 사진

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

실습2 결과사진

실습 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_brstrue이면 CANFD_BRS, false이면 0frame.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 결과사진1 실습3 결과사진2

실습 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.hbind() 블록이 끝난 뒤 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 결과사진

심화 과제 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 결과사진

심화 과제 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 결과사진1 심화과제3 결과사진2 심화과제3 결과사진3

심화 과제 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 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 2 코드 위치 사진

TODO 3. can_recv.cpp#

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

TODO 3 코드 위치 사진

부록. 도전 과제의 답#


  • 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.hbind() 블록이 끝난 뒤 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;
}

심화과제 코드 넣는 곳 위치사진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);

심화과제 코드 넣는 곳 위치사진2

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

심화과제 코드 넣는 곳 위치사진3