NetCAN

NetCAN is the PF_CAN protocol family for FreeBSD - kernel modules, a generic interface layer, a hardware driver, and a full userland toolkit. Modeled after Linux SocketCAN, written native to FreeBSD: NETISR_CAN dispatch, ifnet integration.

Frame formats

A single CAN_RAW socket opts into whichever generations it needs via setsockopt(SOL_CAN_RAW, …). Default is Classical CAN for binary compatibility; FD and XL are independent opt-ins.

Classical CAN 2.0B - struct can_frame, MTU 16
11/29-bit identifiers, RTR frames, error frames.
CAN FD - struct canfd_frame, MTU 72
Up to 64-byte payloads, Bit Rate Switching, Error State Indicator.
CAN XL - struct canxl_frame, MTU 2060
Up to 2048-byte payloads, VCID handling via CAN_RAW_XL_VCID_OPTS.

Kernel modules

ModuleRole
can.koCore domain. Registers PF_CAN via DOMAIN_SET(can), provides the NETISR_CAN handler, the generic can_if interface layer, the ioctl handler, and the bit-timing calculator.
can_raw.koCAN_RAW for SOCK_RAW sockets. Up to 512 filters per socket, loopback control, recv_own_msgs semantics, CAN FD / CAN XL support, epoch-based PCB synchronization.
can_nmea2000.koNMEA 2000 / SAE J1939 for SOCK_DGRAM sockets. Fast Packet reassembly, Address Claim (PGN 60928), Product Information (PGN 126996).
vcan.koVirtual CAN driver. Cloned loopback interfaces (vcanX) for testing without hardware. CAN FD and CAN XL MTUs.
ahc0512.koHardware driver for the AHC0512 (RDC A9610LITOA) Flexcan-compatible controller found on Advantech industrial platforms.

Userland

CommandRole
canconfig(8)Configure CAN interfaces - bitrate, data-bitrate (FD), control modes (FD, XL, loopback, listen-only, one-shot, …), filters.
candump(1)Capture and display CAN frames. Ported from Linux can-utils; kqueue-driven.
cansend(1)Send a CAN frame from the command line.
slcand(1)Serial Line CAN daemon. Bridges a serial SLCAN device to a CAN ifnet.
n2kdump(1)Decode NMEA 2000 messages from a CAN bus. Configurable PGN catalog, JSON output.

API

Standard socket(2) family. If you’ve written SocketCAN code on Linux, this is what you already know.

/* Open a raw CAN socket on vcan0. */
int s = socket(PF_CAN, SOCK_RAW, CAN_RAW);

struct sockaddr_can addr = {
    .len         = sizeof addr,   /* bind() validates this */
    .can_family  = AF_CAN,
    .can_ifindex = if_nametoindex("vcan0"),
};
bind(s, (struct sockaddr *)&addr, sizeof addr);

/* Opt into CAN FD (72-byte MTU). */
int on = 1;
setsockopt(s, SOL_CAN_RAW, CAN_RAW_FD_FRAMES, &on, sizeof on);

/* Filter two arbitration IDs. */
struct can_filter f[2] = {
    { .can_id = 0x123,                       .can_mask = CAN_SFF_MASK },
    { .can_id = 0x18EEFF01 | CAN_EFF_FLAG, .can_mask = CAN_EFF_MASK },
};
setsockopt(s, SOL_CAN_RAW, CAN_RAW_FILTER, f, sizeof f);

/* Send. */
struct canfd_frame fr = {
    .can_id = 0x123,
    .len    = 8,
    .data   = { 0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xba, 0xbe },
};
write(s, &fr, CANFD_MTU);

Socket options

CAN_RAW_FILTER
Up to 512 struct can_filter entries per socket.
CAN_RAW_JOIN_FILTERS
Switch the filter aggregator from OR (default) to AND.
CAN_RAW_FD_FRAMES / CAN_RAW_XL_FRAMES
Opt into CAN FD or CAN XL frames respectively. Independent switches.
CAN_RAW_LOOPBACK
Software loopback. On by default.
CAN_RAW_RECV_OWN_MSGS
Receive your own transmissions. Off by default.
CAN_RAW_ERR_FILTER
Receive error frames matching a mask.
CAN_RAW_XL_VCID_OPTS
CAN XL Virtual CAN Identifier handling.

Inside the kernel

Two-phase netisr dispatch
NETISR_CAN snapshots the registered protocol list under a brief NET_EPOCH hold, then delivers each protocol’s input function outside epoch. Prevents grace-period starvation under sustained frame rates.
Per-interface backpressure
can_if_transmit() maintains an atomic netisr_inflight counter per interface. Above the backlog limit, transmits are dropped rather than overrunning the netisr. The limit is dev.can.netisr_backlog_max globally, and dev.<driver>.<unit>.netisr_backlog_max per device.
ISR + taskqueue, no shortcuts
Hardware ISRs never touch mbufs or the stack. They snapshot pending hardware state under a spin lock and schedule a taskqueue. Allocation and delivery happen in the taskqueue.
Bus-off recovery + TX watchdog
Automatic restart (sysctl restart_ms), TX watchdog for stuck transmissions, synthetic error frames (CAN_ERR_FLAG) delivered to sockets on state transitions.

Locking hierarchy

The documented acquisition order. Not every pair below is exercised: the netisr walks the protocol list under NET_EPOCH without taking can_proto_list_mtx, and registration takes the mutex without entering epoch.

CAN_LOCK (per-interface mtx, MTX_DEF)
  +- can_proto_list_mtx (global, MTX_DEF)
       +- NET_EPOCH (read-only, brief)
            +- CAN_RAW_LOCK (global, MTX_DEF)
                 +- socket buffer locks (sb_mtx)

AHC0512_INTR_LOCK (per-device, MTX_SPIN) -- ISR-only, never nested

Writing a driver

Embed a struct can_softc, populate hw_transmit_locked / set_bittiming / set_ctrlmode / set_powermode_locked / get_bus_state, call can_if_attach(). The ahc0512 driver under sys/dev/can/ahc0512/ is a complete reference implementation - ACPI probe, ISR + taskqueue, full state machine. It attaches to the ACPI bus only; PCI and platform-bus attachment are not implemented.

Try it

# virtual CAN - no hardware needed
kldload vcan
ifconfig vcan create
ifconfig vcan0 up            # the interface is ifconfig's job
candump  vcan0  &
cansend  vcan0  123#deadbeef
# on an AHC0512
kldload ahc0512
canconfig can0 bitrate 250000
canconfig can0 listenonly on
ifconfig  can0 up
n2kdump   can0 -j            # interface first, then options

A full CAN edge

NetCAN is the kernel floor. A real telemetry edge stacks a decoder, a message bus, persistence, and dashboards on top of it. The MarlinOS racing platform is that worked example - NMEA 2000 capture over native CAN, NATS routing, TimescaleDB, and onboard dashboards, in a five-jail platform that runs offline for days. Swap the decoder and the same shape is any industrial or automotive CAN edge - a factory floor, a test bench, a vehicle gateway.