기술 가이드

CAN FD 길이 변환: DLC·바이트 수·패딩을 구분하는 Python 검증

CAN FD의 DLC와 실제 바이트 길이를 구분하고, 0~64바이트 변환·명시적 패딩·잘못된 입력을 Python으로 검증합니다. SocketCAN 길이 필드 적용 시 주의점도 정리합니다.

목차

CAN FD로 9바이트를 보내려는데 분석기에는 12바이트가 보인다면, 먼저 길이 필드가 무엇을 뜻하는지 확인해야 합니다. CAN FD의 DLC(Data Length Code)는 4비트 코드이고 실제 데이터 바이트 수와 항상 같지 않습니다. MCU 컨트롤러 설정, Linux SocketCAN 구조체, 응용 프로토콜의 유효 길이를 구분하면 뒤쪽 데이터 오해를 줄일 수 있습니다.

이 글은 길이 변환과 명시적 패딩을 다룹니다. 기존 SocketCAN ID·EFF·RTR 필터 가이드와는 별개의 송수신 데이터 길이 문제입니다.

1. 9바이트와 DLC 9는 다릅니다

CAN FD의 대응 관계는 다음과 같습니다. Linux CAN 드라이버의 공식 길이 변환 구현에서도 이 표를 확인할 수 있습니다.

DLC 코드 CAN FD 데이터 길이(바이트)
0~8 코드와 동일한 0~8
9 12
10 16
11 20
12 24
13 32
14 48
15 64

유효 데이터가 9바이트이면 이를 담을 최소 길이는 12바이트이고 DLC는 9입니다. 유효 데이터가 13바이트라면 길이 16, DLC 10을 선택합니다. DLC 12를 “12바이트”로 읽으면 실제 24바이트 프레임을 잘못 해석하게 됩니다.

Classical CAN에 이 표를 적용해서도 안 됩니다. Classical CAN 데이터는 최대 8바이트이고 원시 DLC 9~15를 다루는 경우도 CAN FD의 12~64바이트 의미와 다릅니다. 컨트롤러의 프레임 형식부터 확인하십시오.

2. API가 요구하는 단위를 먼저 확인합니다

위치 확인할 값 9바이트 데이터 예
응용 프로토콜 의미 있는 데이터 길이 9
버스상의 CAN FD DLC 4비트 길이 코드 9
명시적으로 패딩한 데이터 실제 전송 데이터 길이 12
SocketCAN canfd_frame.len 데이터 길이, 바이트 단위 이 글의 패딩 예에서는 12

Linux SocketCAN 공식 문서의 CAN FD 절은 canfd_frame.len을 0~64의 바이트 길이로 정의합니다. 따라서 이 필드에 길이 64를 뜻하는 DLC 코드 15를 넣으면 15바이트라는 다른 요청이 됩니다. 버스용 DLC 변환은 드라이버 계층에서 이뤄집니다.

MCU HAL과 레지스터도 이름만 보고 판단하지 마십시오. 어떤 인터페이스는 바이트 길이, 다른 인터페이스는 DLC 코드나 미리 시프트된 상수를 요구합니다. 사용하는 칩과 HAL 버전의 함수 설명을 확인해야 합니다. 아래 Python 함수의 DLC 정수를 특정 MCU의 HAL 인수에 그대로 대입하는 예제가 아닙니다.

3. 패딩은 응용 프로토콜과 함께 정합니다

수신 측은 12바이트 데이터만 보고 원래 유효 데이터가 9·10·11·12바이트 중 무엇인지 알아낼 수 없습니다. 프로토콜에 고정 메시지 길이나 별도 유효 길이 필드를 정의하고, 패딩 바이트의 값도 양쪽에서 합의하십시오.

특히 rstrip(b"\x00")으로 끝의 0을 전부 지우면 실제 데이터인 0까지 잃습니다. 길이 9가 프로토콜에서 검증된 경우에만 received[:9]를 의미 있는 데이터로 사용합니다. 이때 프레임 길이가 요구 길이 이상인지, 패딩 규칙이 맞는지도 검사하십시오. 기존 프로토콜에 임의의 길이 필드를 끼워 넣으면 호환성이 깨지므로 새 형식은 송수신 양쪽을 함께 설계합니다.

다음 예제는 원본을 보존하고 남는 바이트를 지정한 값으로 채웁니다. 0xCC는 눈으로 확인하기 위한 실습값이며 실제 제품 규칙은 아닙니다. 숫자가 범위를 넘으면 하위 4비트로 잘라 수용하지 않고 거부합니다.

4. PC에서 실행하는 완전한 길이 검증 예제

Python 3.10 이상에서 아래 내용을 canfd_length_demo.py로 저장합니다. 외부 패키지나 CAN 장치 없이 실행할 수 있습니다.

# Python 3.10+ / no third-party packages
FD_LENGTHS = (0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64)


def checked_int(value, upper):
    if type(value) is not int or value not in range(upper + 1):
        raise ValueError("integer out of range")
    return value


def dlc_to_length(dlc):
    return FD_LENGTHS[checked_int(dlc, 15)]


def length_to_dlc(length):
    checked_int(length, 64)
    for dlc, capacity in enumerate(FD_LENGTHS):
        if length in range(capacity + 1):
            return dlc
    raise AssertionError("unreachable")


def pad_payload(payload, pad=0):
    if not isinstance(payload, bytes):
        raise TypeError("payload must be bytes")
    checked_int(pad, 255)
    dlc = length_to_dlc(len(payload))
    capacity = dlc_to_length(dlc)
    return dlc, payload + bytes([pad]) * (capacity - len(payload))


def must_reject(function, *args):
    try:
        function(*args)
    except (ValueError, TypeError):
        return
    raise AssertionError("invalid input accepted")


if __name__ == "__main__":
    # Independent range boundaries from the published DLC mapping.
    ranges = [(0, 8, None), (9, 12, 9), (13, 16, 10),
              (17, 20, 11), (21, 24, 12), (25, 32, 13),
              (33, 48, 14), (49, 64, 15)]
    for low, high, expected in ranges:
        for n in range(low, high + 1):
            assert length_to_dlc(n) == (n if expected is None else expected)
            original = bytes(range(n))
            for pad in (0x00, 0xCC):
                dlc, wire = pad_payload(original, pad)
                assert len(wire) == dlc_to_length(dlc)
                assert wire[:n] == original
                assert wire[n:] == bytes([pad]) * (len(wire) - n)
                assert dlc == 0 or n not in range(FD_LENGTHS[dlc - 1] + 1)
    for dlc in range(16):
        assert length_to_dlc(dlc_to_length(dlc)) == dlc
    for bad in (-1, 65, 256, True, 1.5, "9", None):
        must_reject(length_to_dlc, bad)
    for bad in (-1, 16, 255, True, 1.5, "9", None):
        must_reject(dlc_to_length, bad)
    must_reject(pad_payload, bytes(65))
    must_reject(pad_payload, b"abc", -1)
    must_reject(pad_payload, b"abc", 256)
    must_reject(pad_payload, bytearray(b"abc"))
    print("PASS: 65 lengths, 130 padding cases, 16 DLC round trips, 18 invalid inputs")
    dlc, wire = pad_payload(bytes.fromhex("01 02 03 04 05 06 07 08 09"), 0xCC)
    print(f"DLC={dlc}, wire_length={len(wire)}, payload={wire.hex(' ')}")

Windows PowerShell에서는 py -3 .\canfd_length_demo.py, Linux에서는 python3 canfd_length_demo.py로 실행합니다. -O 옵션은 assert 검사를 비활성화하므로 사용하지 않습니다.

확인한 출력은 다음과 같습니다.

PASS: 65 lengths, 130 padding cases, 16 DLC round trips, 18 invalid inputs
DLC=9, wire_length=12, payload=01 02 03 04 05 06 07 08 09 cc cc cc

0~64바이트 길이 전체, 0x00·0xCC 두 패딩값, DLC 16개 왕복, 범위 밖 숫자·잘못된 자료형을 검사했습니다. 테스트는 길이 변환과 바이트 배열만 검증합니다. 실제 CAN 컨트롤러·배선·타이밍·ACK를 시험한 결과는 아닙니다.

5. SocketCAN에 적용할 때 확인할 사항

명시적 패딩 예를 C에 옮길 때는 struct canfd_frame frame = {0};처럼 구조체를 초기화하고, frame.len에는 패딩된 바이트 수를 넣으며, frame.data에 같은 수의 바이트를 복사합니다. 소켓에 쓰는 구조체 전체 크기와 유효 데이터 길이는 별개입니다. Linux에서 CANFD_MTU는 72바이트, 데이터 영역의 최대 길이는 64바이트입니다.

CAN FD 처리를 활성화하는 CAN_RAW_FD_FRAMES 옵션과 FD를 지원하는 인터페이스 설정도 필요합니다. FD 소켓은 Classical CAN과 CAN FD를 모두 받을 수 있으므로 수신 반환 크기를 먼저 확인하고 해당 구조체의 길이를 검사합니다. 구조체 전체를 9바이트만 쓰거나, 읽은 구조체 크기 72를 데이터 길이로 사용하는 것은 잘못된 처리입니다.

6. 장치 시험에서 남길 관측값

격리된 시험 버스에서 8·9·12·13·48·49·64바이트 경계를 차례로 확인하십시오. 각 경우에 응용 유효 길이, 선택 DLC, 패딩 뒤 길이, 실제 뒤쪽 바이트를 함께 기록하면 문제가 생기는 계층을 좁힐 수 있습니다.

  • 분석기의 표시가 DLC 코드인지 바이트 길이인지 확인합니다.
  • 송수신 노드가 같은 CAN FD 형식과 비트 타이밍을 사용하는지 확인합니다.
  • BRS 사용 여부, 트랜시버의 FD 지원, 종단·배선 조건을 별도로 확인합니다.
  • 패딩까지 정상인데 값만 다르면 응용 데이터의 부호·바이트 순서·유효 길이 규칙을 확인합니다.

이 길이 계산이 통과해도 실제 버스 동작을 보장하지 않습니다. 가상 인터페이스나 PC 배열 테스트로 물리 신호 품질까지 확인할 수는 없습니다.

공식 참고 자료

문서 및 예제 확인일: 2026-10-22. 예제는 공식 대응 관계를 참고해 이 글을 위해 작성한 독립적인 Python 실습 코드입니다.

관련 서비스

관련 글

댓글

첫 댓글을 남겨 보세요.

관리자 확인 후 게시됩니다.

처리 중입니다...

잠시만 기다려주세요.