Dušan Čorlija · Writing

STmin has to hold across block boundaries too

A small ISO-TP timing bug that no unit test would catch, found because a bus simulator made the transfer faster than it was allowed to be.

  • CAN
  • ISO-TP
  • UDS
  • Testing

ISO-TP (ISO 15765-2) is the transport layer under UDS diagnostics and ECU flashing. It cuts a message of up to 4095 bytes into 8-byte CAN frames: a first frame, then consecutive frames, with the receiver pacing the sender through flow-control frames. A flow control carries two numbers:

  • BS, block size: how many consecutive frames the sender may send before waiting for the next flow control (0 = no limit);
  • STmin: the minimum gap the sender must leave between consecutive frames, so a slow receiver can keep up.

While writing isotp-lite, a compact ISO-TP implementation in C, I also wrote a small discrete-time CAN bus simulator: TX mailboxes, arbitration, frame timing from the bit rate. It exists so that timing behaviour can be tested on a laptop. It found a bug I would not have found otherwise.

The symptom: a result that was too good

One of the examples sends a 4095-byte message over a simulated 500 kbit/s bus for a grid of BS and STmin values. With STmin = 1 ms the results looked like this:

Receiver BSReceiver STminTransfer time
0 (no limit)1 ms584.9 ms
81 ms555.6 ms

Adding a block size made the transfer faster. That can't be right. A block size adds a flow-control round trip every eight frames; it can only cost time. When a measurement improves after you add overhead, something is skipping a rule.

The cause

After a flow control arrived, my sender sent the first consecutive frame of the new block immediately. Inside a block it respected STmin correctly, but across the block boundary the gap was only the flow-control turnaround: the last consecutive frame, then the receiver's flow control (about 0.28 ms each on the wire at 500 kbit/s), plus processing. That came to roughly 0.6 ms, well under the 1 ms the receiver had asked for.

Timeline of a 100-byte transfer with block size 4 and STmin 1 ms
After the fix: consecutive frames stay 1 ms apart, including the first frame after each flow control.

A receiver that needs STmin usually needs it for a reason: a small RX buffer, or an interrupt that has to copy each frame out before the next arrives. Breaking the gap exactly at block boundaries is the worst place, because that is also where the receiver has just spent time building and sending its flow control.

The fix

Remember when the last consecutive frame went out, and on a flow control schedule the next one at last_cf + STmin. Only the very first consecutive frame after the first frame may go immediately:

l->tx_due_us = (l->tx_sn == 1 && l->tx_off == 6)
             ? now                                   /* first CF after the FF */
             : l->tx_last_cf_us + l->tx_st_us;       /* keep STmin across blocks */
if (reached(now, l->tx_due_us))
    l->tx_due_us = now;

After the fix, BS = 8 with STmin = 1 ms takes the same 584.9 ms as BS = 0: the flow-control round trip now fits inside the gap the receiver asked for anyway.

The test that guards it

The test suite now measures the actual gap between consecutive frames on the simulated wire, with BS = 0 and BS = 4, and fails if any gap is shorter than STmin:

if (f->id == TESTER_ID && (f->data[0] >> 4) == 2) {      /* consecutive frame */
    if (last_cf_us && b->now_us - last_cf_us < min_gap_us)
        min_gap_us = b->now_us - last_cf_us;
    last_cf_us = b->now_us;
}
...
CHECK(min_gap_us >= 2000);                               /* STmin = 2 ms */

A unit test of the state machine would not have caught this. Each function did what its author meant. The bug only exists in the timing between two code paths, and only shows up when you look at the bus the way a receiver sees it.

Takeaways

  • Simulate the medium, not just the protocol. A few hundred lines of bus model turned a timing requirement into something a test can measure.
  • Distrust improvements you can't explain. The bug announced itself as a better number.
  • Test properties, not traces. "No two consecutive frames closer than STmin" holds for every configuration. A recorded trace only covers the one it was recorded with.