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.
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
- The K-line is one wire with a pull-up to +12 V and an open collector: everyone hears their own echo, a second transmitter gives itself away with a foreign echo.
- 10400 baud 8N1: a bit is 96.15 µs, a byte 961.54 µs; BRR 3462 at 36 MHz (−0.013 %), 4038 at 42 MHz (+0.011 %).
- The KWP frame: format (mode | length), 0x33 or 0x10, 0xF1, data, checksum modulo
256; ISO 9141-2 —
68 6A F1and the response48 6B 10. - Slow start: 0x33 at 5 baud — 2 s; 55, key 08 08 (or 94 94), F7, CC; ready after 2444.568 ms.
- Fast start: 25 + 25 ms,
C1 33 F1 81 66→83 F1 10 C1 8F E9 BD; ready after 467.343 ms. - P2 up to 50 ms — then a timeout; P3 over 5 s — the session is closed; TesterPresent
3Eevery 2 s keeps the session alive. - The USART cannot go below 549.3 baud at 36 MHz, a LIN break is 1.25 ms:
the wake-up is bit-banged on GPIO, the exchange uses the USART with
HDSEL. - The tester and simulator core is 2800 bytes of code on the F103, 2810 on the F407.
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.