ISO-TP (ISO 15765-2)
The transport layer that carries messages longer than 8 bytes over CAN. A one or two byte header per frame, a sequence counter, and a flow control frame the receiver must send or the transfer dies after the first frame.
ISO-TP (ISO 15765-2, "transport protocol and network layer services") is the segmentation layer that lets a message longer than a single Wiki: can-bus frame travel over CAN. A classic CAN frame carries at most 8 data bytes, and a diagnostic reply is routinely 20 or 200. ISO-TP spends the first byte or two of each frame on a header, splits the payload, numbers the pieces, and gives the receiver a way to say "slow down".
The four frame types
The high nibble of the first byte is the PCI (protocol control information) and names the frame type. This is the whole protocol in one table:
| Type | PCI | Layout | Meaning |
|---|---|---|---|
| Single Frame | 0 |
0L + up to 7 bytes |
L is the payload length, 1 to 7 |
| First Frame | 1 |
1L LL + 6 bytes |
12-bit total length, up to 4095 bytes |
| Consecutive Frame | 2 |
2S + up to 7 bytes |
S is a sequence counter, 1 to 15 then wrapping to 0 |
| Flow Control | 3 |
3F BS ST |
The receiver's permission to continue |
A payload of 7 bytes or fewer is one Single Frame and nothing else happens. Anything longer becomes a First Frame, then a Flow Control from the receiver, then Consecutive Frames.
The sequence counter starts at 1 in the first Consecutive Frame (the First Frame implicitly holds index 0), counts up to 15, and wraps to 0. It is 4 bits, so it detects a dropped frame but says nothing about which one; reassembly means concatenating the First Frame's 6 payload bytes with each Consecutive Frame's 7, in order, then truncating to the length the First Frame declared.
Flow Control, the part people skip
3F BS ST carries three fields:
- FlowStatus (low nibble of the first byte):
0ContinueToSend,1Wait (send another Flow Control before I accept more),2Overflow (I cannot buffer this, abort). - BlockSize: how many Consecutive Frames the sender may send before it must stop and wait for another Flow Control.
0x00means "send them all, do not wait again". - STmin, the separation time minimum:
0x00to0x7Fis 0 to 127 milliseconds between frames;0xF1to0xF9is 100 to 900 microseconds. Every other value is reserved, and a receiver that gets one is required to treat it as0x7F.
30 00 00 is therefore the common "send everything, as fast as you like" answer, and it is what a Linux ISO-TP socket sends by default.
The consequence for anyone with a sniffer: if you request a long reply with cansend and then watch with candump, you see the First Frame and nothing more. The ECU is waiting for a Flow Control that nobody sent, and it aborts about a second later. Raw capture works for reading somebody else's session; it does not work for driving one yourself.
Addressing
Normal addressing uses the CAN id alone: the whole 8 bytes are ISO-TP. Extended addressing spends the first data byte on a target address, leaving 6 for a Single Frame and 6 for a Consecutive Frame, and it is common on trucks and on buses with many ECUs behind one id. Mixed addressing does the same on 29-bit ids. A tool configured for the wrong mode reads every length field one byte off, which looks like corruption rather than a configuration mistake.
Unused bytes in the last frame are usually padded to a full 8, conventionally with 0x00 or 0xAA. Some ECUs insist on it and drop short frames.
On Linux
The can-isotp kernel module has been in mainline since Linux 5.10 and gives you a socket that does all of the above:
sudo modprobe can-isotp
sudo ip link set can0 up type can bitrate 500000
The can-utils tools take the same two flags, and the meaning is the same in both: -s is the CAN id you transmit with, -d is the CAN id you receive on, both from your own point of view. Talking to an ECU whose request id is 0x7E0 and response id is 0x7E8 therefore looks identical on each side:
isotprecv -s 7E0 -d 7E8 can0 | xxd # listens on 0x7E8, answers Flow Control on 0x7E0
isotpsend -s 7E0 -d 7E8 can0 <<< "22 F1 90"
Start the receiver first. It is the process that will send the Flow Control frame, and if it is not bound when the ECU answers, the reply dies on the First Frame.
From Python the same socket is one import away, and it is what udsoncan sits on:
import isotp
s = isotp.socket()
s.set_fc_opts(stmin=5, bs=10) # optional, before bind
s.bind("can0", isotp.Address(rxid=0x7E8, txid=0x7E0))
s.send(bytes([0x22, 0xF1, 0x90]))
print(s.recv(4095).hex())
For analysis after the fact, Wireshark dissects ISO 15765-2 and reassembles multi-frame messages, but only once you tell it which CAN ids to treat as ISO-TP (Preferences, Protocols, ISO15765, the CAN id mapping table). Without that it shows you raw CAN and leaves the reassembly to you.
Pitfalls
- Reassembling by hand off a raw capture. Perfectly doable, and the only option when your capture tool has no ISO-TP layer: concatenate, then truncate to the declared length. Forgetting the truncation leaves padding bytes glued to the end of the message.
- A sequence gap is a lost frame, not a protocol variant. If the counter jumps, your capture missed a frame or the bus dropped one.
- STmin is advisory in practice. Plenty of ECUs ignore the value they were sent and plenty of testers ignore the value they receive. When a long transfer fails halfway, raising STmin is the first thing to try.
- The 4095 byte limit is classic CAN only. CAN FD adds escape forms (a Single Frame with a zero length nibble and a real length in the next byte, a First Frame with
10 00and a 32-bit length) that older tools do not parse. - ISO-TP is not a protocol you attack. It carries Wiki: uds, and that is where the gates and the interesting services live.