Skip to content

LIN Bus

The Local Interconnect Network bus is a simple low-speed bus to ... as the name says: interconnect different different devices. It's used in the automobile industry as low-end alternative for the faster and much more complex CAN bus.

It turns out that even the simplest STM µCs have support for LIN in their USART hardware. And that you can use the LIN protocol without bus drivers: just an internal pull-up and setting the GPIO pin to open-drain mode is enough.

The official LIN bus runs at speeds up to 19,200 baud, with packets payloads of only up to 8 bytes. That gives it very limited throughput, but still snappy response times for things which happen at a "human" timescale, i.e. down to tenths of seconds. The maximum number of devices ("IDs") on one bus is 64.

It should be enough for a decent set of DOBB modules if those IDs are properly managed.

The nice thing about using a standard bus, is that some scopes have a built-in decoder. Including filters for packets with a specific ID or a payload matching specific bytes.

The plan

What I have in mind is sending measurements around and controlling some parameter settings via the LIN bus. With a single bus line shared between modules, combined modules start to make sense: one generates some signal, another one measures, a third one controls the settings, and yet more for live displays. Fun when combined with breadboards.

Just as with CAN, the key idea is that devices "shout": they publish whatever they want, using small 2..8 byte messages. The other modules can see this and pick up what they like. It all hinges on proper use of the packet's 64 IDs: a mismatch means nothing happens.

Unlike CAN, the LIN bus does not have bus arbitration. Instead, it relies on a master node which queries IDs according to some time schedule. The schedule can be as simple as querying each ID in rapid succession, or it can divide the limited time in more subtle ways.

Bus master

The first task is to set up a bus master which periodically sends out the queries. This is what the LIN Viewer does (despite its name): it has a fixed schedule for now, just to get started: polling IDs 0..7, 32..39, and 48..55 once every 500 ms. A more sophisticated schedule will make buttons and knobs more responsive, but for now a simple fixed 0.5 sec poll will do.

LIN bus packets are very limited:

  • IDs 0..31 have a 2-byte payload
  • IDs 32..47 have a 4-byte payload
  • IDs 48..63 have an 8-byte payload

That's it. No other sizes, no more IDs. It's not a protocol for sending out lots of text or firmware images, although that could be done by sending multiple packets to the same ID.

The intial implementation is a module with a small OLED to show the packets on the bus. There can only (and must always) be one bus master.

Three wires

The "official" LIN bus uses 12V signaling and slew-controlled transceiver interface chips, because its main use is in noisy automotive environments. For DoBB, standard logic levels are fine, and simple open-drain outputs with internal pull-ups to 3.3V work well. As more devices are added to the bus, the pull-ups will counteract the increased capacitive load.

The BUS-A line on the expansion bases will be used for LIN, as first informal convention. It's automatically shared between modules, so it's just a matter of plugging one in and making sure it's set up as LIN "slave".

But there's another option: if a circuit only uses a LIN bus connection, then all it needs is 3 wires: +5V, LIN bus, and GND. One example is the rotary module shown here: two rotary knobs and some buttons as "controller", plus an OLED which can indicate what the knobs do.

The cable uses a standard 2.5 mm audio plug: small and widely available.

Bus monitor

Although the LIN bus only runs at 19,200 baud, there could be quite a bit going on when more devices are active. The bus master will poll each device every 10 to 2500 milliseconds.

Since there are only 64 possible IDs and each one can only send 2, 4, or 8 bytes, there's a way to (just barely!) fit all the packets from each device on a 240x240 LCD screen. Only the last packet is shown for each ID, with new ones in red, slowly fading to yellow, green, blue, and then grey.

It's cramped and super tiny, but this is what the LIN Monitor module is for. It fits on a 7x3 cm proto board and plugs into an expansion base. And the colours make all the difference!

Demo setup

Here's a complete LIN-enabled combination of modules:

From left to right:

  • the DM105 Rotary module, connected to 5V power + LIN with a 3-wire cable
  • an STM32C011 board, currently configured as Lin Viewer and bus master
  • a hack to bring out the 5V + LIN + GND connections to a 2.5 mm audio jack
  • an STM32G431 board, configured as Lin Monitor - with activity on ID 1 (in green)

Info

The viewer and monitor are both connected to the LIN bus via the BUS-A line on the expansion base;

Background LIN

There's a LIN responder implementation in JeeH which runs as task in the background. It's convenient because it stays out of the way of the rest of the application. Everything is done via an dedicated UART in async DMA mode (the uart::Trig driver), and with STM's UART support there's just one interrupt per device poll and when a complete device packet has been received. The prority of these interrupts could be lowered, since there won't be more than one or two every 10 ms, which is the maximum scheduling rate of the LIN bus master.

Bus master demo

Here is a demonstration of a LIN "bus master", which polls slave IDs to leet them send out packets on the bus without contention or further arbitration:

linmaster.cpp
// DM107 - LIN bus master, polling IDs according to a fixed schedule.

#include <jee.h>
#include <jee/hal.h>
#include <jee/util/lin.h>
using namespace jeeh;
#include "defs.hpp"

Ticker ticker;
TICKER_TRIGGER(ticker)

util::Lin linTask (linBus);

uint8_t* util::LinPoll (uint8_t) {
    led.toggle();
    return nullptr;
}

void util::LinRecv (const uint8_t* ptr, int len) {
    logDump(ptr, len);
}

// for each ID: the time between polls in steps of 10 ms (0 = disabled)
// ID 1 is polled every 20 ms and half of the rest are polled once every 0.5 s
uint8_t schedules [64] = {
   50,  2, 50, 50, 50, 50, 50, 50,  0,  0,  0,  0,  0,  0,  0,  0,
   50, 50, 50, 50, 50, 50, 50, 50,  0,  0,  0,  0,  0,  0,  0,  0,
   50, 50, 50, 50, 50, 50, 50, 50,  0,  0,  0,  0,  0,  0,  0,  0,
   50, 50, 50, 50, 50, 50, 50, 50,  0,  0,  0,  0,  0,  0,  0,  0,
};

int main () {
    initBoard();

    // init all tasks in decreasing priority
    ticker.init();
    linTask.init(schedules);

    while (true)
        asm ("wfi");
}

This example code implements a LIN "bus master", sending queries according to a fixed schedule. It does not have it's own module build but runs just fine on the LIN Viewer board.

The schedule table passed to the LinTask::init call specifies the timing to periodically poll the bus. Each ID is polled at its own rate, from 10 milliseconds up to 2.55 seconds. Each LIN bus poll takes between 3 and 9 ms, so if the bus is busy the fastest polls may get delayed at times, as seen in this scope capture:

The zoomed section shows a packet reception for poll ID 1, sandwich'ed between polls 21 (0x15) and 22 (0x16).

ID 1 is polled every 20 ms, but since polls cycle around the entire ID range and are handled on a first-come first-serve bases, its polls slow down when the other ID polls get scheduled. This could be improved by dynamically adjusting rates, based on actual bus activity.