Embedded HandbookApps
All sections › III. Peripherals
STM32F103, STM32F407 · level advanced · 39 min read
Open in the interactive handbook

19к. K-line: diagnostics of older cars via ISO 9141 and KWP2000 over UART

Section 19г covered OBD-II on CAN and listed the ELM327 adapter protocols in a table. Three of them are not CAN: ISO 9141-2 and two variants of ISO 14230-4 (KWP2000). All three run over the K-line — a single wire with serial coding, almost an ordinary UART. This kind of diagnostics was fitted to cars built before the switch to CAN, and it can still be found in garages: a device for it is a USART, one transistor or a transceiver chip, and careful handling of timing.

Here: the line and the echo, the frame and the checksum, two ways to wake a unit (an address at 5 baud and a 25 ms pulse), exchange timing and keeping the link alive. At the end — a device on USART2 of the F103 and F407 in single-wire mode and a run of the device against a unit simulator on a line model with a one-microsecond step. The services and PID formulas are the same as in 19г and are not repeated here; for UART and the baud rate divider see section 16.

Sources: the section was not checked against the text of the ISO 9141-2 and ISO 14230 standards. The speed, the wake-up address, the 55 08 08 response, the KWP frame format, the checksum and the echo were checked against three open-source projects: the ELM327 emulator 4.0.0 (elm/kline.py, which also plays the role of a unit on the K-line), python-obd 0.7.3 (obd/protocols/protocol_legacy.py) and rusEFI (firmware/hw_layer/kline.cpp, kline.h). The USART single-wire mode follows RM0008 (section 27.3.10) and RM0090 (section 30.3.10). Licenses: ELM327-emulator — CC BY-NC-SA 4.0, python-obd — GPL v2, rusEFI — GPL v3; their code is retold, not copied into the check. The device and the simulator are _verify/kline19k/kline.c, the line model is kline_host.c, the USART glue is kline_stm32.c, the run is kline19k.py.

Terms used in the documentation

Term Meaning
K-line a single-wire diagnostic line
tester the diagnostic device (scanner)
ECU an electronic control unit
5-baud init, slow init slow start: the address at 5 baud
fast init, wake-up pattern fast start: a wake-up pulse
synchronization byte (0x55) the synchronization byte
key bytes (KB1, KB2) the key bytes
StartCommunication (0x81) the "start communication" service
TesterPresent (0x3E) the "tester is present" service
format byte the format byte: address mode and length
checksum the checksum
single-wire half-duplex (HDSEL) USART single-wire half-duplex
echo the echo: your own byte coming back from the line
P1…P4, W1…W5 exchange and initialization timings

19к.1 The line: one wire and the echo

The K-line is one wire from the device to all the units hanging on it. At rest the line is held high by a pull-up to the +12 V vehicle supply; a zero is driven by whoever is transmitting: an open-collector output pulls the line to ground. This is a wired AND — high only while nobody pulls it low. So two nodes cannot transmit at the same time unnoticed: one node's zero corrupts the other's one.

This leads to the main feature: everyone hears themselves. The device's transmitter and receiver look at the same wire, and every byte sent immediately comes back. The ELM327 emulator says so in its description: every byte of the application comes back as an echo, as with a real adapter, because there is only one line. rusEFI has the same comment in its receive loop: because of the single wire, we read everything we transmitted ourselves. The echo is not interference but a check: if what came back is not what went out, someone else is on the line (19к.6).

Between the 12 V line and a 3.3 V pin you need a transceiver. There are ready-made chips — for example, L9637, MC33290 and SI9241 are mentioned; there is also a circuit with a transistor: it pulls the line low on the TX signal, while a divider or a second transistor brings the line to the RX input level. The section was not checked against the datasheets of these chips — check the circuit and the pull-up value (510 Ω is often quoted) against the documentation of whatever you fit.

On the left an STM32F103 or F407: pin PA2 works either as USART2 in HDSEL single-wire mode, where TX and RX are connected internally, or as GPIO for the wake-up; through a 3.3 V to 12 V open-collector transceiver it goes out to the K-line, which a 510 Ω pull-up holds at +12 V; on the same line is the engine unit with address 0x10 and, in red, a possible second tester — it is given away by a foreign echo. In the middle, the slow start: address 0x33 at 5 baud, 10 bits of 200 ms each, 2 s, then after W1 60 ms the unit sends 55, after 10 ms 08 and 08, after 30 ms the tester answers F7, after another 30 ms the unit answers CC. At the bottom, the fast start: 25 ms low, 25 ms high, the request C1 33 F1 81 with checksum 66 and the response 83 F1 10 C1 8F E9 with checksum BD


19к.2 Speed and frame

The speed after initialization is 10400 baud, 8 bits, no parity, one stop bit. The number 10400 appears both in the emulator (KLINE_BAUDRATE = 10400) and in rusEFI (KLINE_BAUD_RATE 10400 with the note "standard speed"). A bit lasts 96.15 µs, a 10-bit byte 961.54 µs (in the model with whole microseconds, 962 µs).

The KWP2000 frame. The first byte is the format: the two upper bits are the address mode, the six lower bits the data length. Then the receiver, the source, the data and the checksum. The tester is 0xF1, the functional OBD address is 0x33, the engine unit is 0x10. If there are more than 63 data bytes, the length in the format byte is zero and the real length goes in a separate byte after the addresses. This is how both the emulator and our kl_frame_kwp build the frame. An example from the emulator description: the request C2 33 F1 01 06 ED — format 0xC2 (functional request, 2 data bytes), and the response 83 F1 33 41 06 84 72.

The ISO 9141-2 frame. Three header bytes without a length, the data, the checksum. The engine's response in python-obd starts with 48 6B 10; the parser discards the three header bytes and the checksum and accepts frames 6 to 11 bytes long. The tester's request in our model is 68 6A F1 (this is how it is described for ISO 9141-2; not checked against the standard). There is no length in the header, so the end of the message is found by a pause — the emulator waits for 20 ms of silence.

The checksum is a simple sum of all bytes modulo 256. The emulator computes it as sum(message[:-1]) & 0xFF; we do it like this:

uint8_t kl_checksum(const uint8_t *b, size_t n)
{
    uint8_t s = 0;
    while (n--)
        s = (uint8_t)(s + *b++);   /* сумма по модулю 256 */
    return s;
}

The RPM request over ISO 9141-2: 68 6A F1 01 0C D0 — 0x68 + 0x6A + 0xF1 + 0x01 + 0x0C = 0x1D0, D0 goes into the byte. The simulator's response is 48 6B 10 41 0C 14 5F 83: the data are the same as on CAN in 19г, 1303.75 RPM.


19к.3 Slow start: the address at 5 baud

A unit on the K-line sleeps until it is woken up. The first way is ISO 9141-2 and ELM327 protocol 4 (in python-obd, "ISO 14230-4 (KWP 5BAUD)"): the tester sends the address 0x33 at 5 baud. A bit is 200 ms, a byte with start and stop is 10 bits, 2 s. Least significant bit first, 0x33 is start, 1 1 0 0 1 1 0 0, stop. The model recorded three low periods on the line: 300–500, 900–1300 and 1700–2100 ms (for the first 300 ms the line is silent — the W5 time).

A USART cannot go that slow: on the F103 the divider is 16 bits, and at 36 MHz the lowest speed is 36 MHz / 65535 = 549.3 baud, at 72 MHz 1098.6 baud. So the address is sent by bit-banging the pin: the pin is switched to open-drain GPIO and the bits are set by a timer.

    case T_SLOW:                                      /* адрес на 5 бод ногой GPIO */
        while (PAST(now, t->t0 + (uint32_t)(t->bit + 1) * KL_SLOW_BIT_US)) {
            if (++t->bit <= 9) {
                t->hw.pin_gpio(t->hw.ctx, slow_level(t, t->bit));
            } else {
                kl_note("ADDR5 %02X %lu %lu", t->addr, (unsigned long)t->t0, (unsigned long)now);
                t->hw.pin_uart(t->hw.ctx);            /* дальше — USART, 10400 */
                t->state = T_SYNC;
                t->t_next = now + KL_W1_MAX_US;
                break;
            }
        }
        break;

After that the unit answers at 10400 baud, and each timing is bounded by a window:

Step Who Byte Window In the run
W1: end of address → sync unit 55 20…300 ms 60.001 ms
W2: 55 → KB1 unit 08 5…20 ms 9.951 ms
W3: KB1 → KB2 unit 08 0…20 ms 9.951 ms
W4: KB2 → ~KB2 tester F7 25…50 ms 29.951 ms
W4: ~KB2 → ~address unit CC 25…50 ms 29.951 ms

The sync byte 0x55 in binary is 01010101, zeros and ones alternating. The key bytes say which protocol follows: the emulator answers 55 08 08; for ISO 9141-2 the pair 94 94 is also seen — the tester in the run accepted both. The tester returns the second key byte inverted (~0x08 = 0xF7), the unit confirms with the inverted address (~0x33 = 0xCC). The whole slow start in the run takes 2444.568 ms from the beginning, of which 2 s is the address itself. The W1…W5 windows follow ISO 9141-2 and ISO 14230-2 and were not checked against the text of the standards: in the model they are parameters, and the run shows that the tester and the simulator fit within them.

No unit — no 0x55: the tester waits up to 300 ms for W1 and gives up. In the run the failure came 300.000 ms after the end of the address.


19к.4 Fast start and StartCommunication

KWP2000 can wake up faster — ELM327 protocol 5, in python-obd "ISO 14230-4 (KWP FAST)", the adapter command ATFI. After the W5 silence the tester pulls the line low for 25 ms, releases it for 25 ms — 50 ms in total — and immediately, at 10400 baud, sends the StartCommunication service:

Who Frame What it is
tester C1 33 F1 81 66 format 0xC1 (functional, 1 byte), to all, from the tester, 0x81, checksum
unit 83 F1 10 C1 8F E9 BD 3 bytes, to the tester, from the engine, 0xC1 = 0x81 + 0x40, key 8F E9

In the model the low period fell at 300.000–325.000 ms, the first request byte at 350.001 ms: the wake-up took 50.001 ms. The tester is ready 467.343 ms from the start — five times faster than the slow start.

The key bytes disagree here even within a single project. The emulator description promises the response 83 F1 33 C1 8F E9 E0, while the code sends C1 8F 8F: the run gave 83 F1 33 C1 8F 8F 86. A comment in the code says that testers check the second key byte, 0x8F. Our simulator answers as in the emulator description, and the tester only stores the key bytes.

A USART cannot produce a 25 ms pulse either. LIN mode sends, on the SBK bit, a break of 13 zero bits (RM0008, section 27.3.8; RM0090, 30.3.8) — at 10400 baud that is 1.25 ms, and besides, in LIN mode the HDSEL bit must be cleared. rusEFI enables LIN (USART_CR2_LINEN), but its kline.cpp has no wake-up: the file, as stated at its beginning, is about Honda's own protocol from the early 2000s. The pulse, like the 5 baud address, is bit-banged on a GPIO pin.


19к.5 Exchange timing and keeping the link alive

After initialization four timings apply:

Timing What Window In the run
P1 between bytes of the unit's response 0…20 ms 0.951 ms
P2 end of request → start of response 25…50 ms 29.951 ms
P3 end of response → new request 55 ms…5 s at least 55.047 ms
P4 between bytes of the tester's request 5…20 ms 5.047 ms

The windows follow the same ISO 9141-2 and ISO 14230 and were not checked against the standards. P4 makes a request slow: the 6 bytes of the RPM request took 31.007 ms, although the bytes themselves take less than 6 ms; the 8 bytes of the response took 14.353 ms. The tester waits for a response until the end of P2 — 50 ms; if nothing arrives, it is a timeout. For the unsupported PID 0xA6 the simulator stays silent, and the tester gave up 49.953 ms after the end of the request (the count starts from the echo of the last byte, which arrives in the middle of the stop bit).

Keeping the link alive. If the tester is silent for longer than P3max — 5 s — the unit considers the session over and goes back to sleep. To keep the session, the tester sends TesterPresent every 2 s: C1 33 F1 3E 23, and the unit answers 81 F1 10 7E 00. In the run the 3E requests went out at 2694.172 and 4757.687 ms, 2063.515 ms apart (the 2 s are counted from the end of the previous exchange), and a request after 6 s of silence got an answer. Without them the simulator closed the session 4999.951 ms after the last response, the next request ended in a timeout, and only a new fast start helped. For ISO 9141-2 our tester repeats the request 01 00 instead of 3E. The emulator did not answer 3E at all.


19к.6 On STM32: USART with HDSEL and a bit-banged wake-up

One pin. In USART single-wire mode (the HDSEL bit in USART_CR3) TX and RX are connected internally, the RX pin is no longer used, and TX is released when there is nothing to send — so it is configured as an input or as an open-drain output (RM0008, 27.3.10; the same in RM0090, 30.3.10). The LINEN, CLKEN, SCEN and IREN bits must be cleared. In this mode the echo arrives by itself. With a separate transceiver (TX and RX separate) you can do without HDSEL — the echo then comes back through the transceiver.

Pin PA2 has two roles: GPIO for the wake-up and USART2 afterwards:

static void pin_gpio(void *c, int level)
{
    (void)c;
    GPIOA->BSRR = level ? (1u << 2) : (1u << 18);
#if defined(STM32F10X_MD)
    GPIOA->CRL = (GPIOA->CRL & ~(0xFu << 8)) | (0x7u << 8);   /* выход, открытый сток */
#else
    GPIOA->MODER = (GPIOA->MODER & ~(3u << 4)) | (1u << 4);   /* выход; открытый сток — в OTYPER */
#endif
}

When switching back to USART, pin_uart reads SR and DR — discarding what the receiver picked up during the wake-up: it sees the 200 ms low periods as bytes with a framing error. The tester and the simulator discard such bytes (in the model there were three of them per slow start).

The divider. USART2 sits on APB1; the formula is from 16.2:

Port Clock BRR Speed Error
F103 USART2 36 MHz 3462 10398.6 baud −0.013 %
F103 USART1 72 MHz 6923 10400.1 baud +0.001 %
F407 USART2 42 MHz 4038 10401.2 baud +0.011 %

The echo as a check. The tester sends a byte and waits for its echo, and only then sends the next one, after P4. If the echo does not match or arrives with a framing error, there is a second transmitter on the line:

    case T_SEND:
        if (!fe && t->echo_pending && b == t->echo_byte) {
            t->echo_pending = 0;
            if (++t->tx_i == t->tx_n) {           /* запрос ушёл целиком */
                t->state = T_WAIT;
                t->t0 = now;
                t->msg_end = now;
                t->rx_n = 0;
                t->rx_need = 0;
                t->nresp = 0;
                t->nbad = 0;
            } else {
                t->t_next = now + KL_P4_MIN_US + KL_BIT_US;
            }
        } else {                                  /* эхо не своё: на линии кто-то ещё */
            kl_note("COLLISION sent %02X got %02X", t->echo_byte, b);
            t->result = KL_R_COLLISION;
            t->state = T_READY;
            t->t_last = now;
            t->ev = (t->purpose == P_REQ) ? KL_EV_DONE : KL_EV_KEEPALIVE;
        }
        break;

The echo arrives when the receiver has counted to the middle of the stop bit — half a bit before the end of the byte. So the tester counts P3 and P4 with a margin of one bit (KL_BIT_US, 96 µs); otherwise P4 would come out shorter than 5 ms.

Test run. The kline.c core (the tester and the unit simulator) was built by gcc in the stm-sys:1 container with -Wall -Wextra -Werror -pedantic and run on the line model kline_host.c: a 1 µs step, a wired AND of three nodes (tester, unit, second tester), each with its own UART receiver sampling in the middle of the bit. Six scenarios; the whole exchange is in kline19k_host.txt:

Scenario What happened
ISO 9141-2, key 08 08 01 00, 01 0C, 01 05, 03 and 09 02 (five VIN messages) — answered; a corrupted checksum …5F 82 — rejected on checksum, a retry — answered; 01 A6 — timeout
ISO 9141-2, key 94 94 start and 01 0C — answered
KWP2000, fast start 01 0C and 01 0D — answered; 6 s of silence with TesterPresent — the session is alive
KWP2000 without keep-alive after 6 s — timeout, a new fast start — answered
a second tester on the line it started a C2 byte 300 µs after our C2: the echo came back as 02, the request was dropped, a retry — answered
no unit failure 300 ms after the address

The simulator's frames were decoded by python-obd with its ISO 9141-2 and KWP parser: RPM 1303.75, temperature 55 °C, speed 10 km/h, codes P0133 and P0301. It assembled the VIN from five messages as WP0ZZZ99ZTS39 — 13 characters of 17: the same trimming of the string ends as in 19г.5. The ELM327 emulator answered the same KWP requests with the same data (41 0C 14 5F, 41 0D 0A, 41 05 5F, 41 00 BE 3F A8 13), only from address 0x33 rather than 0x10: it swaps the request addresses.

Sizes. arm-none-eabi-gcc 12.2.1 (stm-arm:1, -Os) built everything without warnings: the core with the tester and the simulator is 2800 bytes of code on the F103 and 2810 on the F407; the USART2 glue and the main loop kline_stm32.c are 458 and 490 bytes of code plus 678 bytes of RAM (the tester state and a 16-element receive ring). _Static_assert checks at build time the divisors 3462 and 4038, the 200 ms per bit at 5 baud, and that the keep-alive period is shorter than P3max. The firmware was not run in QEMU or on hardware.


19к.7 Full code of the tester and the simulator

The tester is three files of our own: the header kline.h, the core kline.c (frames, tester and unit simulator) and the USART2 glue kline_stm32.c. All three are below in full; kline.c is split into six consecutive pieces with no gaps — joined with a blank line, they give the file byte for byte. The pieces from 19к.2, 19к.3 and 19к.6 are repeated here inside their functions. There is no third-party code.

The sources have no startup file with a vector table, no clock setup and no main(): kline_stm32.c is built only into an object file, provides kl_port_init() and the main loop step kl_app_step(), and assumes the clocks are already configured — HCLK 72 MHz on the F103 and 168 MHz on the F407. The line model kline_host.c is a verification bench, not part of the tester, and is not shown here.

The build is the same as in the check: each file separately for the F103 and the F407. CMSIS_F1 is the Device/Include directory of the Keil STM32F1xx_DFP 2.4.1 pack (it contains stm32f10x.h), CMSIS_F4 is Drivers/CMSIS/Device/ST/STM32F4xx/Include of the STM32F4xx_DFP 2.17.1 pack, CMSIS_CORE is CMSIS/Core/Include from CMSIS 5.9.0 (github.com/ARM-software/CMSIS_5):

for f in kline kline_stm32; do
  arm-none-eabi-gcc -std=c11 -Os -Wall -Wextra -Werror -ffunction-sections -fdata-sections \
    -mcpu=cortex-m3 -mthumb -DSTM32F10X_MD -I. -I$CMSIS_F1 -I$CMSIS_CORE -c $f.c -o F1_$f.o
  arm-none-eabi-gcc -std=c11 -Os -Wall -Wextra -Werror -ffunction-sections -fdata-sections \
    -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard -DSTM32F407xx \
    -I. -I$CMSIS_F4 -I$CMSIS_CORE -c $f.c -o F4_$f.o
done

The header. Speeds, the W and P windows in microseconds, addresses, the message, three hardware glue functions and the states of the tester and the simulator:

/* kline.h — K-line (ISO 9141-2, ISO 14230 KWP2000): прибор и имитатор блока.
 * Раздел 19к справочника. Ядро без железа: время — мкс от платформы,
 * вывод и USART — через три функции kl_hw. Собирается gcc на ПК и arm-none-eabi-gcc. */
#ifndef KLINE_H
#define KLINE_H
#include <stdint.h>
#include <stddef.h>

/* ── скорости ── */
#define KL_BAUD          10400u                    /* бод после инициализации */
#define KL_BIT_US        ((1000000u + KL_BAUD / 2u) / KL_BAUD)  /* 96 мкс */
#define KL_SLOW_BAUD     5u                        /* адрес при медленном старте */
#define KL_SLOW_BIT_US   (1000000u / KL_SLOW_BAUD) /* 200 мс на бит */

/* ── времена инициализации, мкс (ISO 9141-2, ISO 14230-2) ── */
#define KL_W1_MIN_US      20000u   /* конец адреса → 0x55 */
#define KL_W1_MAX_US     300000u
#define KL_W2_MIN_US       5000u   /* 0x55 → KB1 */
#define KL_W2_MAX_US      20000u
#define KL_W3_MAX_US      20000u   /* KB1 → KB2 */
#define KL_W4_MIN_US      25000u   /* KB2 → ~KB2 и ~KB2 → ~адрес */
#define KL_W4_MAX_US      50000u
#define KL_W5_MIN_US     300000u   /* тишина на линии перед инициализацией */
#define KL_TINIL_US       25000u   /* быстрый старт: 25 мс ноль… */
#define KL_TWUP_US        50000u   /* …и ещё 25 мс единица, всего 50 мс */

/* ── времена обмена, мкс ── */
#define KL_P1_MAX_US      20000u   /* между байтами ответа блока */
#define KL_P2_MIN_US      25000u   /* конец запроса → начало ответа */
#define KL_P2_MAX_US      50000u
#define KL_P3_MIN_US      55000u   /* конец ответа → новый запрос */
#define KL_P3_MAX_US    5000000u   /* тишина дольше — сеанс закрыт */
#define KL_P4_MIN_US       5000u   /* между байтами запроса прибора */
#define KL_KEEPALIVE_US 2000000u   /* наш период «я здесь»: меньше P3max */

_Static_assert(KL_SLOW_BIT_US == 200000u, "5 бод = 200 мс на бит");
_Static_assert(KL_TWUP_US == 2u * KL_TINIL_US, "25 мс ноль + 25 мс единица");
_Static_assert(KL_KEEPALIVE_US < KL_P3_MAX_US, "поддержка связи чаще P3max");

/* ── адреса ── */
#define KL_ADDR_OBD      0x33u   /* OBD: «всем блокам» */
#define KL_TESTER        0xF1u   /* прибор */
#define KL_ECU_ENGINE    0x10u   /* двигатель */

enum { KL_ISO9141 = 1, KL_KWP = 2 };             /* что ответил блок */
enum { KL_SLOW = 1, KL_FAST = 2 };               /* как будим */
enum { KL_R_OK = 0, KL_R_TIMEOUT, KL_R_CHECKSUM, KL_R_COLLISION };
enum { KL_EV_NONE = 0, KL_EV_CONNECTED, KL_EV_INIT_FAIL, KL_EV_DONE, KL_EV_KEEPALIVE };

#define KL_MSG_MAX 72

typedef struct {
    uint8_t fmt, tgt, src, n;   /* n — байт данных */
    uint8_t d[64];
} kl_msg;

typedef struct {
    void (*pin_gpio)(void *ctx, int level);  /* вывод — GPIO с открытым стоком: 0 прижать, 1 отпустить */
    void (*pin_uart)(void *ctx);             /* вывод — снова USART, 10400 бод */
    void (*uart_send)(void *ctx, uint8_t b); /* начать передачу байта */
    void *ctx;
} kl_hw;

uint8_t kl_checksum(const uint8_t *b, size_t n);
size_t  kl_frame_kwp(uint8_t mode, uint8_t tgt, uint8_t src, const uint8_t *d, uint8_t n, uint8_t *out);
size_t  kl_frame_9141(uint8_t h0, uint8_t h1, uint8_t h2, const uint8_t *d, uint8_t n, uint8_t *out);
int     kl_parse(const uint8_t *b, size_t n, kl_msg *m);  /* 0, -1 коротко, -2 сумма, -3 длина */

/* ── прибор ── */
typedef struct {
    kl_hw hw;
    int wake, proto, state, purpose, keepalive;
    uint32_t t0, t_next, t_last;     /* t_last — конец последнего обмена */
    uint8_t addr, kb1, kb2;
    int bit;
    uint8_t tx[KL_MSG_MAX];
    uint8_t tx_n, tx_i, echo_pending, echo_byte;
    uint8_t rx[KL_MSG_MAX];
    uint8_t rx_n, rx_need;
    uint32_t rx_last;
    kl_msg resp[6];
    uint8_t nresp, nbad;
    int result, ev;
    uint32_t msg_end;
} kl_tester;

void kl_tester_init(kl_tester *t, const kl_hw *hw);
void kl_tester_connect(kl_tester *t, int wake, uint32_t now);
int  kl_tester_request(kl_tester *t, const uint8_t *d, uint8_t n, uint32_t now);
void kl_tester_rx(kl_tester *t, uint8_t b, int framing_error, uint32_t now);
int  kl_tester_poll(kl_tester *t, uint32_t now);
int  kl_tester_ready(const kl_tester *t);

/* ── имитатор блока двигателя ── */
typedef struct {
    kl_hw hw;
    int state, line, proto;
    uint32_t t_fall, t0, t_last_req, t_next;
    uint8_t kb1, kb2, addr;
    int bit;
    uint8_t rx[KL_MSG_MAX];
    uint8_t rx_n, rx_need;
    uint32_t rx_last;
    uint8_t out[6][KL_MSG_MAX];
    uint8_t out_n[6], nout, out_k, out_i, echo;
    int corrupt_next;                 /* испортить сумму следующего ответа */
} kl_ecu;

void kl_ecu_init(kl_ecu *e, const kl_hw *hw, uint8_t kb1, uint8_t kb2);
void kl_ecu_line(kl_ecu *e, int level, uint32_t now);   /* фронт на линии */
void kl_ecu_rx(kl_ecu *e, uint8_t b, int framing_error, uint32_t now);
void kl_ecu_poll(kl_ecu *e, uint32_t now);

#ifdef KL_HOST
void kl_note(const char *fmt, ...);
#else
#define kl_note(...) ((void)0)
#endif

#endif

Frames. PAST compares times taking counter overflow into account. Building KWP and ISO 9141-2 frames, parsing with length and checksum checks:

/* kline.c — K-line: кадры, прибор и имитатор блока. Раздел 19к. */
#include "kline.h"
#include <string.h>

#define PAST(now, t) ((int32_t)((uint32_t)(now) - (uint32_t)(t)) >= 0)

static uint8_t inv8(uint8_t x)
{
    return (uint8_t)(x ^ 0xFFu);                  /* инверсия байта: 0x08 → 0xF7, 0x33 → 0xCC */
}

/* ════ кадры ════ */

uint8_t kl_checksum(const uint8_t *b, size_t n)
{
    uint8_t s = 0;
    while (n--)
        s = (uint8_t)(s + *b++);   /* сумма по модулю 256 */
    return s;
}

/* KWP2000: байт формата (режим адреса | длина), приёмник, источник, данные, сумма */
size_t kl_frame_kwp(uint8_t mode, uint8_t tgt, uint8_t src, const uint8_t *d, uint8_t n, uint8_t *out)
{
    size_t k = 0;
    out[k++] = (uint8_t)(mode | (n <= 63u ? n : 0u));
    out[k++] = tgt;
    out[k++] = src;
    if (n > 63u)
        out[k++] = n;              /* длинное сообщение: отдельный байт длины */
    memcpy(&out[k], d, n);
    k += n;
    out[k] = kl_checksum(out, k);
    return k + 1u;
}

/* ISO 9141-2: три байта заголовка без длины, данные, сумма */
size_t kl_frame_9141(uint8_t h0, uint8_t h1, uint8_t h2, const uint8_t *d, uint8_t n, uint8_t *out)
{
    out[0] = h0;
    out[1] = h1;
    out[2] = h2;
    memcpy(&out[3], d, n);
    out[3u + n] = kl_checksum(out, 3u + n);
    return 4u + n;
}

int kl_parse(const uint8_t *b, size_t n, kl_msg *m)
{
    size_t hdr = 3u, len;
    if (n < 4u)
        return -1;
    if (b[0] & 0x80u) {                       /* KWP: длина в байте формата */
        len = b[0] & 0x3Fu;
        if (len == 0u) {
            if (n < 5u)
                return -1;
            len = b[3];
            hdr = 4u;
        }
        if (n != hdr + len + 1u)
            return -3;
    } else {                                  /* ISO 9141-2: длина — по паузе */
        len = n - 4u;
    }
    if (len > sizeof m->d)
        return -3;
    if (kl_checksum(b, n - 1u) != b[n - 1u])
        return -2;
    m->fmt = b[0];
    m->tgt = b[1];
    m->src = b[2];
    m->n = (uint8_t)len;
    memcpy(m->d, &b[hdr], len);
    return 0;
}

Tester: start and request. The tester states, the wake-up (first the W5 silence), sending a frame with P3 counted from the end of the previous exchange, initialization failure and parsing of a received message:

/* ════ прибор ════ */

enum { T_OFF, T_W5, T_SLOW, T_FAST_LOW, T_FAST_HIGH, T_SYNC, T_KB1, T_KB2, T_W4, T_NKB2, T_NADDR,
       T_READY, T_SEND, T_WAIT };
enum { P_REQ, P_START, P_KEEP };

void kl_tester_init(kl_tester *t, const kl_hw *hw)
{
    memset(t, 0, sizeof *t);
    t->hw = *hw;
    t->addr = KL_ADDR_OBD;
    t->keepalive = 1;
}

int kl_tester_ready(const kl_tester *t)
{
    return t->state == T_READY;
}

void kl_tester_connect(kl_tester *t, int wake, uint32_t now)
{
    t->wake = wake;
    t->proto = 0;
    t->hw.pin_gpio(t->hw.ctx, 1);             /* линию отпустить и выждать W5 */
    t->state = T_W5;
    t->t_next = now + KL_W5_MIN_US;
}

static void send_frame(kl_tester *t, const uint8_t *d, uint8_t n, int purpose, uint32_t now)
{
    if (t->proto == KL_ISO9141)
        t->tx_n = (uint8_t)kl_frame_9141(0x68u, 0x6Au, KL_TESTER, d, n, t->tx);
    else
        t->tx_n = (uint8_t)kl_frame_kwp(0xC0u, KL_ADDR_OBD, KL_TESTER, d, n, t->tx);
    t->tx_i = 0;
    t->nresp = 0;
    t->nbad = 0;
    t->echo_pending = 0;
    t->purpose = purpose;
    t->state = T_SEND;
    /* эхо приходит на середине стопового бита: P3 и P4 отсчитываем с запасом в бит */
    t->t_next = t->t_last + KL_P3_MIN_US + KL_BIT_US;
    if (PAST(now, t->t_next))
        t->t_next = now;
    if (purpose == P_START)
        t->t_next = now;
    kl_note("REQ %s", purpose == P_START ? "start" : purpose == P_KEEP ? "keepalive" : "obd");
}

int kl_tester_request(kl_tester *t, const uint8_t *d, uint8_t n, uint32_t now)
{
    if (t->state != T_READY)
        return -1;
    send_frame(t, d, n, P_REQ, now);
    return 0;
}

static int fail(kl_tester *t, const char *why)
{
    (void)why;
    kl_note("INIT FAIL %s", why);
    t->state = T_OFF;
    t->hw.pin_uart(t->hw.ctx);
    return KL_EV_INIT_FAIL;
}

static void finish_msg(kl_tester *t)
{
    kl_msg m;
    int r = kl_parse(t->rx, t->rx_n, &m);
    kl_note("MSG %d", r);
    if (r == 0 && t->nresp < 6u)
        t->resp[t->nresp++] = m;
    else
        t->nbad++;
    t->msg_end = t->rx_last;
    t->rx_n = 0;
    t->rx_need = 0;
}

Tester: receiving a byte. The sync byte and the key during the slow start, its own echo during transmission, assembling the response — for KWP by the length in the format byte:

void kl_tester_rx(kl_tester *t, uint8_t b, int fe, uint32_t now)
{
    if (fe && t->state != T_SEND)
        return;                               /* обрывки пробуждения — не байты */
    switch (t->state) {
    case T_SYNC:
        if (b == 0x55u) {
            t->state = T_KB1;
            t->t_next = now + KL_W2_MAX_US;
        }
        break;
    case T_KB1:
        t->kb1 = b;
        t->state = T_KB2;
        t->t_next = now + KL_W3_MAX_US;
        break;
    case T_KB2:
        t->kb2 = b;
        t->state = T_W4;
        t->t_next = now + KL_W4_MIN_US + 5000u;       /* 30 мс: внутри 25…50 */
        break;
    case T_NKB2:
        if (t->echo_pending && b == t->echo_byte) {   /* своё эхо */
            t->echo_pending = 0;
            t->state = T_NADDR;
            t->t_next = now + KL_W4_MAX_US;
        }
        break;
    case T_NADDR:
        if (b == inv8(t->addr)) {                       /* ~адрес */
            t->proto = (t->kb2 == 0x8Fu) ? KL_KWP : KL_ISO9141;
            t->state = T_READY;
            t->t_last = now;
            t->ev = KL_EV_CONNECTED;
            kl_note("CONNECTED slow %02X %02X", t->kb1, t->kb2);
        }
        break;
    case T_SEND:
        if (!fe && t->echo_pending && b == t->echo_byte) {
            t->echo_pending = 0;
            if (++t->tx_i == t->tx_n) {           /* запрос ушёл целиком */
                t->state = T_WAIT;
                t->t0 = now;
                t->msg_end = now;
                t->rx_n = 0;
                t->rx_need = 0;
                t->nresp = 0;
                t->nbad = 0;
            } else {
                t->t_next = now + KL_P4_MIN_US + KL_BIT_US;
            }
        } else {                                  /* эхо не своё: на линии кто-то ещё */
            kl_note("COLLISION sent %02X got %02X", t->echo_byte, b);
            t->result = KL_R_COLLISION;
            t->state = T_READY;
            t->t_last = now;
            t->ev = (t->purpose == P_REQ) ? KL_EV_DONE : KL_EV_KEEPALIVE;
        }
        break;
    case T_WAIT:
        if (t->rx_n < KL_MSG_MAX)
            t->rx[t->rx_n++] = b;
        t->rx_last = now;
        if (t->rx_n == 1u && (b & 0x80u) && (b & 0x3Fu))
            t->rx_need = (uint8_t)(3u + (b & 0x3Fu) + 1u);
        if (t->rx_n == 4u && (t->rx[0] & 0x80u) && !(t->rx[0] & 0x3Fu))
            t->rx_need = (uint8_t)(4u + b + 1u);
        if (t->rx_need && t->rx_n == t->rx_need)
            finish_msg(t);
        break;
    default:
        break;
    }
}

Tester: time step. The 5-baud address and the 25 + 25 ms pulse bit-banged on GPIO, the W1…W4 deadlines, keep-alive, request bytes spaced by P4, end of response by the P1 pause and the P2 timeout:

static int slow_level(const kl_tester *t, int bit)
{
    if (bit == 0)
        return 0;                                     /* старт */
    if (bit <= 8)
        return (t->addr >> (bit - 1)) & 1;            /* младшим вперёд */
    return 1;                                         /* стоп */
}

int kl_tester_poll(kl_tester *t, uint32_t now)
{
    static const uint8_t start_comm[] = { 0x81u };
    static const uint8_t tp_kwp[] = { 0x3Eu };
    static const uint8_t tp_9141[] = { 0x01u, 0x00u };
    int ev = t->ev;
    t->ev = KL_EV_NONE;
    if (ev)
        return ev;
    switch (t->state) {
    case T_W5:
        if (!PAST(now, t->t_next))
            break;
        t->t0 = now;
        t->hw.pin_gpio(t->hw.ctx, 0);
        if (t->wake == KL_SLOW) {
            t->bit = 0;
            t->state = T_SLOW;
        } else {
            t->state = T_FAST_LOW;
        }
        break;
    case T_SLOW:                                      /* адрес на 5 бод ногой GPIO */
        while (PAST(now, t->t0 + (uint32_t)(t->bit + 1) * KL_SLOW_BIT_US)) {
            if (++t->bit <= 9) {
                t->hw.pin_gpio(t->hw.ctx, slow_level(t, t->bit));
            } else {
                kl_note("ADDR5 %02X %lu %lu", t->addr, (unsigned long)t->t0, (unsigned long)now);
                t->hw.pin_uart(t->hw.ctx);            /* дальше — USART, 10400 */
                t->state = T_SYNC;
                t->t_next = now + KL_W1_MAX_US;
                break;
            }
        }
        break;
    case T_FAST_LOW:
        if (PAST(now, t->t0 + KL_TINIL_US)) {
            t->hw.pin_gpio(t->hw.ctx, 1);
            t->state = T_FAST_HIGH;
        }
        break;
    case T_FAST_HIGH:
        if (PAST(now, t->t0 + KL_TWUP_US)) {
            t->hw.pin_uart(t->hw.ctx);
            t->proto = KL_KWP;
            send_frame(t, start_comm, 1u, P_START, now);
        }
        break;
    case T_SYNC:
        if (PAST(now, t->t_next))
            return fail(t, "no sync");
        break;
    case T_KB1:
    case T_KB2:
        if (PAST(now, t->t_next))
            return fail(t, "no key byte");
        break;
    case T_W4:
        if (PAST(now, t->t_next)) {
            t->echo_byte = inv8(t->kb2);                  /* ~KB2 */
            t->echo_pending = 1;
            t->hw.uart_send(t->hw.ctx, t->echo_byte);
            t->state = T_NKB2;
        }
        break;
    case T_NADDR:
        if (PAST(now, t->t_next))
            return fail(t, "no inverted address");
        break;
    case T_READY:
        if (t->keepalive && PAST(now, t->t_last + KL_KEEPALIVE_US)) {
            if (t->proto == KL_ISO9141)
                send_frame(t, tp_9141, 2u, P_KEEP, now);
            else
                send_frame(t, tp_kwp, 1u, P_KEEP, now);
        }
        break;
    case T_SEND:
        if (!t->echo_pending && PAST(now, t->t_next)) {
            t->echo_byte = t->tx[t->tx_i];
            t->echo_pending = 1;
            t->hw.uart_send(t->hw.ctx, t->echo_byte);
        }
        break;
    case T_WAIT:
        if (t->rx_n && (uint32_t)(now - t->rx_last) > KL_P1_MAX_US)
            finish_msg(t);                            /* ISO 9141-2: конец — по паузе */
        if (t->rx_n == 0u && (uint32_t)(now - t->msg_end) > KL_P2_MAX_US) {
            t->result = t->nresp ? KL_R_OK : t->nbad ? KL_R_CHECKSUM : KL_R_TIMEOUT;
            t->state = T_READY;
            t->t_last = t->nresp || t->nbad ? t->msg_end : now;
            if (t->purpose == P_START) {
                if (t->result == KL_R_OK && t->resp[0].n == 3u && t->resp[0].d[0] == 0xC1u) {
                    t->kb1 = t->resp[0].d[1];
                    t->kb2 = t->resp[0].d[2];
                    kl_note("CONNECTED fast %02X %02X", t->kb1, t->kb2);
                    return KL_EV_CONNECTED;
                }
                return fail(t, "no StartCommunication answer");
            }
            return t->purpose == P_KEEP ? KL_EV_KEEPALIVE : KL_EV_DONE;
        }
        break;
    default:
        break;
    }
    return KL_EV_NONE;
}

Simulator: wake-up and responses. The unit's own deadlines, recognition of the 25 ms pulse and responses to 81, 3E, mode 01 (PID 00, 05, 0C, 0D), 03 and 09 02:

/* ════ имитатор блока двигателя ════ */

enum { E_SLEEP, E_LOW, E_SLOW, E_W1, E_SYNC, E_NKB2, E_W4, E_FASTWAIT, E_AWAKE, E_REPLY };

#define ECU_W1_US  60000u   /* блок: пауза перед 0x55 */
#define ECU_W2_US  10000u   /* между 0x55, KB1, KB2 */
#define ECU_W4_US  30000u   /* ~KB2 → ~адрес */
#define ECU_P1_US   1000u   /* между байтами ответа */
#define ECU_P2_US  30000u   /* запрос → ответ и между ответами */

void kl_ecu_init(kl_ecu *e, const kl_hw *hw, uint8_t kb1, uint8_t kb2)
{
    memset(e, 0, sizeof *e);
    e->hw = *hw;
    e->kb1 = kb1;
    e->kb2 = kb2;
    e->line = 1;
}

void kl_ecu_line(kl_ecu *e, int level, uint32_t now)
{
    e->line = level;
    if (e->state == E_SLEEP && level == 0) {
        e->t_fall = now;
        e->state = E_LOW;
    } else if (e->state == E_LOW && level == 1) {
        uint32_t dur = now - e->t_fall;
        if (dur >= KL_TINIL_US - 1000u && dur <= KL_TINIL_US + 1000u) {
            e->state = E_FASTWAIT;                     /* 25 мс ноль: быстрый старт */
            e->t_last_req = now;
            e->rx_n = 0;
            kl_note("ECU fast wake %lu", (unsigned long)dur);
        } else {
            e->state = E_SLEEP;                        /* помеха */
        }
    }
}

static void ecu_add(kl_ecu *e, const uint8_t *d, uint8_t n)
{
    if (e->nout >= 6u)
        return;
    if (e->proto == KL_ISO9141)
        e->out_n[e->nout] = (uint8_t)kl_frame_9141(0x48u, 0x6Bu, KL_ECU_ENGINE, d, n, e->out[e->nout]);
    else
        e->out_n[e->nout] = (uint8_t)kl_frame_kwp(0x80u, KL_TESTER, KL_ECU_ENGINE, d, n, e->out[e->nout]);
    e->nout++;
}

static void ecu_answer(kl_ecu *e, const kl_msg *m)
{
    static const char vin[] = "WP0ZZZ99ZTS390000";
    const uint8_t *q = m->d;
    uint8_t r[8];
    e->nout = 0;
    if (q[0] == 0x81u && m->n == 1u) {
        r[0] = 0xC1u; r[1] = e->kb1; r[2] = e->kb2;  /* StartCommunication: да, и ключ */
        ecu_add(e, r, 3u);
    } else if (q[0] == 0x3Eu && m->n == 1u) {
        r[0] = 0x7Eu;                                  /* TesterPresent: да */
        ecu_add(e, r, 1u);
    } else if (q[0] == 0x01u && m->n == 2u) {
        r[0] = 0x41u; r[1] = q[1];
        switch (q[1]) {
        case 0x00: r[2] = 0xBEu; r[3] = 0x3Fu; r[4] = 0xA8u; r[5] = 0x13u; ecu_add(e, r, 6u); break;
        case 0x05: r[2] = 0x5Fu; ecu_add(e, r, 3u); break;                   /* 55 °C */
        case 0x0C: r[2] = 0x14u; r[3] = 0x5Fu; ecu_add(e, r, 4u); break;     /* 1303,75 об/мин */
        case 0x0D: r[2] = 0x0Au; ecu_add(e, r, 3u); break;                   /* 10 км/ч */
        default: break;                                                      /* не знаю — молчу */
        }
    } else if (q[0] == 0x03u && m->n == 1u) {
        r[0] = 0x43u; r[1] = 0x01u; r[2] = 0x33u; r[3] = 0x03u; r[4] = 0x01u; r[5] = 0; r[6] = 0;
        ecu_add(e, r, 7u);                                                   /* P0133, P0301 */
    } else if (q[0] == 0x09u && m->n == 2u && q[1] == 0x02u) {
        for (uint8_t k = 0; k < 5u; k++) {                                   /* VIN — пять сообщений */
            r[0] = 0x49u; r[1] = 0x02u; r[2] = (uint8_t)(k + 1u);
            for (uint8_t j = 0; j < 4u; j++) {
                int c = (int)k * 4 + (int)j - 3;
                r[3u + j] = (uint8_t)(c < 0 ? 0 : vin[c]);
            }
            ecu_add(e, r, 7u);
        }
    }
    if (e->nout && e->corrupt_next) {
        e->out[0][e->out_n[0] - 1u] ^= 0x01u;           /* испорченная сумма */
        e->corrupt_next = 0;
    }
}

Simulator: reception and step. Parsing the request, the echo of its own bytes, receiving ~KB2, the 5-baud address sampled in the middle of the bit, and closing the session after P3:

static void ecu_request(kl_ecu *e, uint32_t now)
{
    kl_msg m;
    int r = kl_parse(e->rx, e->rx_n, &m);
    e->rx_n = 0;
    e->rx_need = 0;
    if (r != 0) {
        kl_note("ECU drop %d", r);
        return;
    }
    if (e->proto == KL_ISO9141 ? m.tgt != 0x6Au : (m.tgt != KL_ADDR_OBD && m.tgt != KL_ECU_ENGINE))
        return;
    if (e->state == E_FASTWAIT && m.d[0] != 0x81u)
        return;
    e->t_last_req = now;
    ecu_answer(e, &m);
    if (e->nout) {
        e->state = E_REPLY;
        e->out_k = 0;
        e->out_i = 0;
        e->t_next = e->rx_last + ECU_P2_US;          /* P2 — от конца запроса */
        if (!PAST(e->t_next, now))
            e->t_next = now;
    } else if (e->state == E_FASTWAIT) {
        e->state = E_AWAKE;
    }
}

void kl_ecu_rx(kl_ecu *e, uint8_t b, int fe, uint32_t now)
{
    if (fe) {
        if (e->echo && e->state == E_REPLY) {         /* эхо испорчено: ответ бросить */
            e->echo = 0;
            e->state = E_AWAKE;
        }
        return;
    }
    if (e->echo) {                                    /* своё эхо */
        e->echo = 0;
        if (e->state == E_SYNC) {
            e->out_i++;
            if (e->out_i == 3u) {
                e->state = E_NKB2;
                e->t0 = now;
            } else {
                e->t_next = now + ECU_W2_US;
            }
        } else if (e->state == E_W4) {
            e->state = E_AWAKE;
            e->t_last_req = now;
            kl_note("ECU awake slow");
        } else if (e->state == E_REPLY) {
            if (++e->out_i == e->out_n[e->out_k]) {
                e->out_i = 0;
                if (++e->out_k == e->nout) {
                    e->state = E_AWAKE;
                    e->t_last_req = now;
                } else {
                    e->t_next = now + ECU_P2_US;
                }
            } else {
                e->t_next = now + ECU_P1_US;
            }
        }
        return;
    }
    if (e->state == E_NKB2) {
        if (b == inv8(e->kb2)) {                        /* ~KB2 */
            e->state = E_W4;
            e->t_next = now + ECU_W4_US;
            e->proto = (e->kb2 == 0x8Fu) ? KL_KWP : KL_ISO9141;
        }
        return;
    }
    if (e->state != E_AWAKE && e->state != E_FASTWAIT)
        return;
    if (e->state == E_FASTWAIT)
        e->proto = KL_KWP;
    if (e->rx_n < KL_MSG_MAX)
        e->rx[e->rx_n++] = b;
    e->rx_last = now;
    if (e->rx_n == 1u && (b & 0x80u) && (b & 0x3Fu))
        e->rx_need = (uint8_t)(3u + (b & 0x3Fu) + 1u);
    if (e->rx_need && e->rx_n == e->rx_need)
        ecu_request(e, now);
}

void kl_ecu_poll(kl_ecu *e, uint32_t now)
{
    switch (e->state) {
    case E_LOW:
        if (PAST(now, e->t_fall + KL_SLOW_BIT_US / 2u)) {
            e->state = E_SLOW;                        /* ноль дольше 100 мс — это 5 бод */
            e->bit = 0;
            e->addr = 0;
        }
        break;
    case E_SLOW:                                      /* адрес: отсчёт в середине битов */
        while (e->bit <= 9 && PAST(now, e->t_fall + (uint32_t)e->bit * KL_SLOW_BIT_US + KL_SLOW_BIT_US / 2u)) {
            if (e->bit == 0 && e->line != 0) {
                e->state = E_SLEEP;
                return;
            }
            if (e->bit >= 1 && e->bit <= 8)
                e->addr |= (uint8_t)(e->line << (e->bit - 1));
            if (e->bit == 9) {
                if (e->line == 1 && e->addr == KL_ADDR_OBD) {
                    e->state = E_W1;
                    e->t_next = e->t_fall + 10u * KL_SLOW_BIT_US + ECU_W1_US;
                    kl_note("ECU addr5 %02X", e->addr);
                } else {
                    e->state = E_SLEEP;
                }
                return;
            }
            e->bit++;
        }
        break;
    case E_W1:
        if (PAST(now, e->t_next)) {
            e->state = E_SYNC;
            e->out_i = 0;
            e->t_next = now;
        }
        break;
    case E_SYNC:
        if (!e->echo && PAST(now, e->t_next)) {
            const uint8_t seq[3] = { 0x55u, e->kb1, e->kb2 };
            e->echo = 1;
            e->hw.uart_send(e->hw.ctx, seq[e->out_i]);
        }
        break;
    case E_NKB2:
        if (PAST(now, e->t0 + 2u * KL_W4_MAX_US))
            e->state = E_SLEEP;
        break;
    case E_W4:
        if (!e->echo && PAST(now, e->t_next)) {
            e->echo = 1;
            e->hw.uart_send(e->hw.ctx, inv8(KL_ADDR_OBD));   /* ~адрес = 0xCC */
        }
        break;
    case E_FASTWAIT:
    case E_AWAKE:
        if (e->rx_n && (uint32_t)(now - e->rx_last) > KL_P1_MAX_US) {
            if (e->rx_need)
                e->rx_n = 0, e->rx_need = 0;          /* KWP оборвался */
            else
                ecu_request(e, now);                  /* ISO 9141-2: конец по паузе */
        }
        if (PAST(now, e->t_last_req + KL_P3_MAX_US)) {
            e->state = E_SLEEP;
            kl_note("ECU session closed");
        }
        break;
    case E_REPLY:
        if (!e->echo && PAST(now, e->t_next)) {
            e->echo = 1;
            e->hw.uart_send(e->hw.ctx, e->out[e->out_k][e->out_i]);
        }
        break;
    default:
        break;
    }
}

USART2 glue. Time in microseconds from SysTick, pin PA2 in two roles, reception in an interrupt through a 16-element ring, and the main loop step:

/* kline_stm32.c — прибор K-line на USART2 (вывод PA2) в однопроводном режиме, F103 и F407.
 * Пробуждение — тем же выводом в режиме GPIO, обмен — USART с битом HDSEL. Раздел 19к. */
#include "kline.h"
#if defined(STM32F10X_MD)
#include "stm32f10x.h"
#define PCLK1 36000000u
#define HCLK  72000000u
#elif defined(STM32F407xx)
#include "stm32f4xx.h"
#define PCLK1 42000000u
#define HCLK  168000000u
#endif

#define KL_BRR ((PCLK1 + KL_BAUD / 2u) / KL_BAUD)
_Static_assert(KL_BRR == (PCLK1 == 36000000u ? 3462u : 4038u), "делитель на 10400 бод");

static volatile uint32_t ms;
void SysTick_Handler(void) { ms++; }

static uint32_t now_us(void)
{
    uint32_t m, v;
    do {
        m = ms;
        v = SysTick->VAL;
    } while (m != ms);
    return m * 1000u + (HCLK / 1000u - 1u - v) / (HCLK / 1000000u);
}

/* PA2: GPIO — пробуждение, AF — USART2 */
static void pin_gpio(void *c, int level)
{
    (void)c;
    GPIOA->BSRR = level ? (1u << 2) : (1u << 18);
#if defined(STM32F10X_MD)
    GPIOA->CRL = (GPIOA->CRL & ~(0xFu << 8)) | (0x7u << 8);   /* выход, открытый сток */
#else
    GPIOA->MODER = (GPIOA->MODER & ~(3u << 4)) | (1u << 4);   /* выход; открытый сток — в OTYPER */
#endif
}

static void pin_uart(void *c)
{
    (void)c;
#if defined(STM32F10X_MD)
    GPIOA->CRL = (GPIOA->CRL & ~(0xFu << 8)) | (0xFu << 8);   /* USART, открытый сток */
#else
    GPIOA->MODER = (GPIOA->MODER & ~(3u << 4)) | (2u << 4);   /* AF7 = USART2 */
#endif
    (void)USART2->SR;                                         /* сбросить обрывки пробуждения */
    (void)USART2->DR;
}

static void uart_send(void *c, uint8_t b)
{
    (void)c;
    USART2->DR = b;
}

/* приём: прерывание кладёт байт и признак ошибки кадра в кольцо, разбор — в главном цикле */
static volatile uint16_t ring[16];
static volatile uint8_t head, tail;

void USART2_IRQHandler(void)
{
    uint32_t sr = USART2->SR;
    if (sr & (USART_SR_RXNE | USART_SR_FE | USART_SR_ORE)) {
        uint16_t b = (uint16_t)(USART2->DR & 0xFFu);          /* чтение DR сбрасывает флаги */
        if (sr & USART_SR_FE)
            b |= 0x100u;
        ring[head & 15u] = b;
        head++;
    }
}

static kl_tester T;

void kl_port_init(void)
{
    static const kl_hw hw = { pin_gpio, pin_uart, uart_send, 0 };
#if defined(STM32F10X_MD)
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
#else
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
    GPIOA->OTYPER |= 1u << 2;
    GPIOA->AFR[0] = (GPIOA->AFR[0] & ~(0xFu << 8)) | (7u << 8);
#endif
    RCC->APB1ENR |= RCC_APB1ENR_USART2EN;
    USART2->BRR = KL_BRR;
    USART2->CR3 = USART_CR3_HDSEL;             /* один провод: TX и RX соединены внутри */
    USART2->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE | USART_CR1_RXNEIE;
    NVIC_EnableIRQ(USART2_IRQn);
    SysTick_Config(HCLK / 1000u);
    kl_tester_init(&T, &hw);
    pin_uart(0);
}

/* один шаг главного цикла: будим быстрым стартом и по кругу спрашиваем обороты */
int kl_app_step(void)
{
    static const uint8_t rpm[] = { 0x01u, 0x0Cu };
    static int started;
    uint32_t t = now_us();
    while (tail != head) {
        uint16_t b = ring[tail & 15u];
        tail++;
        kl_tester_rx(&T, (uint8_t)b, (b & 0x100u) != 0u, t);
    }
    if (!started) {
        kl_tester_connect(&T, KL_FAST, t);
        started = 1;
    }
    int ev = kl_tester_poll(&T, t);
    if (ev == KL_EV_CONNECTED || ev == KL_EV_DONE)
        kl_tester_request(&T, rpm, 2u, t);
    if (ev == KL_EV_INIT_FAIL)
        started = 0;
    return ev;
}

19к.8 Pitfalls

12 V on the pin. The line is pulled up to the vehicle supply, and that is not exactly 12 V: a running engine, cranking and transients. A 3.3 V STM32 pin connects only through a transceiver or a transistor, never directly. The device's power also goes through a regulator (19г.8).

Two testers on one line. A scanner is already connected and you connect yours — or the car has an immobilizer unit that also talks on the K-line. The wired AND will mix the bytes: in the run our C2 and a foreign C2, shifted by 300 µs, produced the echo 02. Compare every echo and on a mismatch drop the request instead of waiting for a response to a corrupted one.

The echo taken for a response. If you do not subtract your own bytes, the tester will take its own request for the start of the response. Count the echo byte for byte — or disable reception during transmission if the transceiver does not produce an echo.

The inverted key taken for a frame. After 55 08 08 the ELM327 emulator receives F7 (~KB2) from the tester and interprets it as a KWP format byte with length 55: it swallowed the next request C2 33 F1 01 0C F3 as part of that frame — 7 bytes in the buffer, no response. A unit receiver that does not know about the slow start will break in exactly this way. Handle initialization as a separate state, not with the general frame assembler.

A LIN break instead of a pulse. 13 zero bits on the SBK bit take 1.25 ms, while the fast start needs 25 ms. The pulse and the 5 baud address — only by bit-banging GPIO, and after them flush the garbage from the receiver.

The session closed silently. A pause longer than 5 s — and the unit is asleep again; the tester sees only timeouts. Send TesterPresent earlier, every 2 s in our case, and after a timeout repeat the initialization.

Checksum and pauses. A corrupted checksum is a reason to discard the message and repeat the request, nothing more: the emulator simply stays silent on a frame with a wrong checksum, as does our simulator. An ISO 9141-2 message ends with a pause — do not append responses from different units into one buffer.


19к.9 Summary

Next: section 19л "LIN: the cheap body-electronics bus — break, sync, PID and the frame schedule" — the same single wire and USART, but now a working body bus: a master on a schedule and cheap slaves.

Open in the interactive handbook