Embedded firmware

Communication Protocols on STM32 (Part 5): LIN – Break field, PID and checksum

LIN on STM32 using the USART, from the frame structure (Break, Sync, PID, data, checksum) and step-by-step PID and checksum examples to frame timing and HAL code for master and slave. Part 5 of the Communication Protocols on STM32 series.

10 min read
Communication Protocols on STM32 (Part 5): LIN – Break field, PID and checksum

Note: the diagrams in this article were drawn by Capitlab for illustration. Table values are calculated from formulas; the "Measured" column will be filled in once measurements are made on hardware. PID and checksum examples were checked with a program. The code was written from the HAL documentation and datasheets and has not yet been run on hardware, so verify it before using it in a product.

LIN (Local Interconnect Network) is a cheap, slow, single-wire bus used in cars for auxiliary devices such as mirrors, seats, windows and rain sensors, the places where CAN would be overkill. At heart LIN is a UART with a frame structure and master-slave rules on top, so the STM32 runs it naturally on a USART. Most of the groundwork is in part 1 on UART.

A LIN network has one master and several slaves (typically up to 16 nodes), one data wire plus GND, and runs at 1 to 20 kbit/s, most often 19.2 kbit/s. Slaves only speak when the master asks them to.

1. How it works

1.1. Network and physical layer

The master coordinates the network with a schedule table: a list of frames, in order, each with its own time slot. Slaves cannot transmit on their own; they only answer when a header carries an ID they are responsible for. Since nobody competes for the bus, LIN needs no arbitration like CAN. The price is low speed, which suits auxiliary devices only.

Bus levels follow the battery, around 12 V, so a transceiver between the MCU and the LIN wire is mandatory.

LIN wiring: STM32 connected to a transceiver via TX and RX, the transceiver drives a single LIN wire with a 1 kΩ pull-up to VBAT, plus two slaves

Figure 1. The MCU connects to the LIN transceiver through the USART TX and RX pins. The single LIN wire has a pull-up to VBAT at the master node. Labels in the figure are in Vietnamese.

1.2. The LIN frame

Each frame consists of a header sent by the master and a response sent by the master or one slave, depending on the ID:

LIN frame structure: Break of 13 low bits, delimiter, Sync 0x55, PID, 1 to 8 data bytes, checksum

Figure 2. A LIN frame. The Break is long enough for slaves to recognise the start of a frame, Sync (0x55) lets them re-measure the bit rate, and the PID says what the frame is.

Field Content
Break Line held low for at least 13 bit times, then released high for at least 1 bit time (delimiter). Marks the start of a frame
Sync The byte 0x55. Slaves time this run of alternating bits to synchronise their baud rate
PID 6 ID bits plus 2 parity bits
Data 1 to 8 bytes
Checksum 1 byte

Nominal timing: THeader = 34 bits and TResponse = 10 × (N + 1) bits, where N is the number of data bytes. The specification allows a frame to stretch up to 1.4 times the nominal value. Calculated values, excluding the gaps between frames:

Data bytes Total bits 9.6 kbit/s 19.2 kbit/s Maximum allowed at 19.2 kbit/s (× 1.4)
1 54 5.63 ms 2.81 ms 3.94 ms
2 64 6.67 ms 3.33 ms 4.67 ms
4 84 8.75 ms 4.38 ms 6.13 ms
8 124 12.92 ms 6.46 ms 9.04 ms

Each slot in the schedule table must fit the maximum frame time (× 1.4), not the nominal one. Otherwise the next frame runs into the previous one.

1.3. PID and parity

The ID is 6 bits (0 to 63). The two parity bits are computed from the ID bits:

  • P0 = ID0 ⊕ ID1 ⊕ ID2 ⊕ ID4
  • P1 = NOT (ID1 ⊕ ID3 ⊕ ID4 ⊕ ID5)

PID = ID (bits 0 to 5) | P0 << 6 | P1 << 7. Slaves check the parity and ignore the whole header if it is wrong.

Bit-by-bit PID calculation: ID 0x01 gives 0xC1 and ID 0x0D gives 0x0D

Figure 3. Two PID examples. ID 0x01 has both parity bits set, so its PID is 0xC1. ID 0x0D has both clear, so its PID stays 0x0D.

Some common PIDs, checked with a program: ID 0x00 gives 0x80, 0x10 gives 0x50, 0x3C (master diagnostic frame) gives 0x3C, 0x3D (slave diagnostic frame) gives 0x7D.

1.4. Checksum

The LIN checksum is an inverted sum with carry wrap-around: add the bytes, subtract 255 whenever the sum exceeds 255 (which adds the carry back in), then take the bitwise NOT. There are two variants:

  • Classic: only the data bytes are summed. This is the LIN 1.x method.
  • Enhanced: the PID is summed together with the data bytes. LIN 2.x added it, but diagnostic frames (IDs 0x3C and 0x3D) still use classic.

Some devices only support classic, so when connecting an unfamiliar device, check its documentation for the variant it uses.

Enhanced checksum example with PID 0x0D and data AB BC CD DE EF giving 0xED

Figure 4. Enhanced checksum step by step. The result is 0xED. The same data with the classic method (no PID) gives 0xFA.

You can check it yourself with the Capitlab CRC / checksum calculator by choosing LIN enhanced or classic.

2. STM32 configuration

Many STM32 families have a LIN mode built into the USART: the hardware generates the break when sending and detects it when receiving. HAL provides HAL_LIN_Init and HAL_LIN_SendBreak.

Setting Value Note
Mode LIN (Asynchronous with LIN) Select in CubeMX if the part supports it
Baud Rate 19200 The usual LIN rate
Word Length 8 Bits
Parity None LIN parity lives in the PID, not in the UART
Stop Bits 1
Break detect length 10 or 11 bits Break detection threshold on the receive side
Clock Use HSE LIN has a tight speed tolerance

If the part has no LIN mode, a plain UART still works. Drop the baud rate to about 9/13 of the normal rate, send 0x00, then restore the baud rate. The start bit plus eight 0 bits make 9 low bits at the slow rate, which is longer than 13 bits at the normal rate.

3. Source code

3.1. Computing the PID and checksum

static uint8_t lin_pid(uint8_t id)
{
    uint8_t b[6];
    for (int i = 0; i < 6; i++) b[i] = (id >> i) & 1;
    uint8_t p0 = b[0] ^ b[1] ^ b[2] ^ b[4];
    uint8_t p1 = !(b[1] ^ b[3] ^ b[4] ^ b[5]);
    return (id & 0x3F) | (p0 << 6) | (p1 << 7);
}

static uint8_t lin_checksum(uint8_t pid, const uint8_t *d, uint8_t n, int enhanced)
{
    uint16_t sum = enhanced ? pid : 0;
    for (uint8_t i = 0; i < n; i++) {
        sum += d[i];
        if (sum > 0xFF) sum -= 0xFF;
    }
    return (uint8_t)(~sum);
}

Quick check: lin_pid(0x01) must equal 0xC1, and lin_checksum(0x0D, {AB,BC,CD,DE,EF}, 5, 1) must equal 0xED (0xFA for classic).

3.2. The master sends a frame

void lin_master_send(uint8_t id, const uint8_t *data, uint8_t n)
{
    uint8_t pid = lin_pid(id);
    uint8_t frame[2 + 8 + 1];
    uint8_t k = 0;
    frame[k++] = 0x55;                    // Sync
    frame[k++] = pid;                     // PID
    for (uint8_t i = 0; i < n; i++) frame[k++] = data[i];
    frame[k++] = lin_checksum(pid, data, n, 1);

    HAL_LIN_SendBreak(&huart2);           // Break generated by the hardware
    HAL_UART_Transmit(&huart2, frame, k, 50);
}

For frames the slave has to answer, the master sends only Break, Sync and PID, then switches to receiving and waits for the response within the allowed time, at most 1.4 times the nominal value from section 1.2.

3.3. The slave receives the header

The slave has to recognise the Break, read Sync, read the PID and check its parity, then receive or send the response depending on the ID. The simplest structure is a state machine in the receive interrupt:

typedef enum { WAIT_BREAK, WAIT_SYNC, WAIT_PID, RX_DATA } lin_state_t;
static volatile lin_state_t st = WAIT_BREAK;
static uint8_t rx_byte, rx_buf[10], rx_n, rx_pid, want;

void USART2_IRQHandler(void)
{
    if (__HAL_UART_GET_FLAG(&huart2, UART_FLAG_LBD)) {   // break detected
        __HAL_UART_CLEAR_FLAG(&huart2, UART_CLEAR_LBDF); // flag name to clear depends on the family
        st = WAIT_SYNC;
    }
    HAL_UART_IRQHandler(&huart2);
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *h)
{
    switch (st) {
    case WAIT_SYNC: if (rx_byte == 0x55) st = WAIT_PID; else st = WAIT_BREAK; break;
    case WAIT_PID:
        if (lin_pid(rx_byte & 0x3F) == rx_byte) {         // parity OK
            rx_pid = rx_byte; rx_n = 0; want = response_length(rx_byte & 0x3F);
            st = RX_DATA;
        } else st = WAIT_BREAK;
        break;
    case RX_DATA:
        rx_buf[rx_n++] = rx_byte;
        if (rx_n == want + 1) {                           // data + checksum
            if (lin_checksum(rx_pid, rx_buf, want, 1) == rx_buf[want]) { /* valid frame */ }
            st = WAIT_BREAK;
        }
        break;
    default: break;
    }
    HAL_UART_Receive_IT(&huart2, &rx_byte, 1);
}

response_length is a table you define yourself, mapping each ID to its byte count. The code only illustrates the flow. The break flag name (LBD or LBDF) and how to clear it differ between STM32 families, so check the reference manual. A real slave also has to re-measure the bit rate from the Sync byte, which is fiddly, so many projects use an existing LIN stack rather than writing their own.

4. Design notes

LIN tolerates only a small speed mismatch between nodes, so the master must run from a crystal, not an RC oscillator.

Pull-ups are typically 1 kΩ at the master and about 30 kΩ at each slave. The 30 kΩ is usually already inside the transceiver or module.

The LIN checksum is only a simple sum. If the data matters enough to need stronger error detection, use CAN.

5. Common faults

Symptom Usual cause Fix
Slave does not recognise the header Break too short (under 13 bit times at the sender), or wrong break detection length Measure the Break with a logic analyzer, set 10 or 11 bits on the receiver
Checksum mismatch Classic and enhanced mixed up, PID left out Diagnostic frames use classic, normal frames use enhanced (LIN 2.x)
PID rejected Parity computed wrongly Check lin_pid: ID 0x01 must give 0xC1
No signal on the bus Transceiver has no 12 V supply, missing pull-up Check the supply, measure the level on the LIN wire
Frames come and go Speed mismatch between master and slave, unstable clock Use a crystal for the master
Response arrives too late Master closes the window too early Allow up to 1.4 times the nominal time

6. FAQ

How is LIN different from UART? The characters on the wire are identical to UART 8N1. LIN adds the Break, Sync, PID, checksum and the master-slave rules. The STM32 runs LIN on a USART, either with the hardware LIN mode or by generating the break manually.

Why is the Sync byte 0x55? 0x55 is 01010101b, full of consecutive edges. Slaves time the edges to work out the exact bit length and adjust their rate, which lets a slave running on an internal oscillator still receive correctly.

Can LIN replace RS-485? LIN is meant for short distances (inside a vehicle, about 40 m maximum per the specification) and low speeds. For longer or faster links, see RS-485 in part 6.

7. Measurement plan

The "Measured" column stays empty until the code runs on hardware.

Test Method Calculated Measured
Actual Break length Logic analyzer, measure how long the line is low At least 13 bit times = 677 µs at 19.2 kbit/s (to be filled)
PID and checksum check Compare the logic analyzer's LIN decode with calculated values ID 0x01 → PID 0xC1, checksum 0xED (example above) (to be filled)
Duration of an 8-byte frame Logic analyzer, from Break to end of checksum 6.46 ms at 19.2 kbit/s (to be filled)
Stable schedule table period Measure the time between consecutive headers As configured in the schedule (to be filled)
Waveform on the 12 V bus Oscilloscope, observe levels and slew rate Depends on the transceiver (to be filled)
Tolerated master/slave speed mismatch Detune the master gradually, note when the slave fails Depends on the slave (to be filled)

Summary

LIN is a UART plus a master-slave frame. The Break must be at least 13 low bits, Sync 0x55 lets slaves lock onto the bit rate, and both sides must use the same checksum variant.

Part 6 moves on to RS-485: a differential half-duplex physical layer that underpins many industrial protocols, Modbus in particular.

References