19к. K-line: диагностика старых машин по ISO 9141 и KWP2000 на UART
Раздел 19г разбирал OBD-II на CAN и таблицей перечислил протоколы адаптера ELM327. Три из них — не CAN: ISO 9141-2 и два варианта ISO 14230-4 (KWP2000). Все три ходят по K-line — одному проводу с последовательным кодом, почти обычным UART. Такую диагностику ставили на машины, выпущенные до перехода на CAN, и она до сих пор встречается в гаражах: прибор для неё — это USART, один транзистор или микросхема-преобразователь и аккуратная работа со временем.
Здесь — линия и эхо, кадр и контрольная сумма, два способа разбудить блок (адрес на 5 бод и импульс 25 мс), времена обмена и поддержка связи. В конце — прибор на USART2 F103 и F407 в однопроводном режиме и прогон прибора против имитатора блока на модели линии с шагом в микросекунду. Службы и формулы PID — те же, что в 19г, здесь их не повторяем; про UART и делитель — раздел 16.
Опоры: с текстом стандартов ISO 9141-2 и ISO 14230 раздел не сверялся. Скорость, адрес
пробуждения, ответ 55 08 08, формат кадра KWP, сумма и эхо сверены с тремя
открытыми проектами: эмулятором ELM327 4.0.0 (elm/kline.py, он же
изображает блок на K-line), python-obd 0.7.3 (obd/protocols/protocol_legacy.py)
и rusEFI (firmware/hw_layer/kline.cpp, kline.h). Однопроводный режим
USART — по RM0008 (раздел 27.3.10) и RM0090 (раздел 30.3.10). Лицензии:
ELM327-emulator — CC BY-NC-SA 4.0, python-obd — GPL v2, rusEFI — GPL v3;
их код пересказан, в проверку не скопирован. Прибор и имитатор —
_verify/kline19k/kline.c, модель линии — kline_host.c, связь с USART —
kline_stm32.c, прогон — kline19k.py.
Как это называется в документации
Оригинал Перевод K-line однопроводная линия диагностики tester прибор (сканер) ECU электронный блок 5-baud init, slow init медленный старт: адрес на 5 бод fast init, wake-up pattern быстрый старт: импульс пробуждения synchronization byte (0x55) байт синхронизации key bytes (KB1, KB2) ключевые байты StartCommunication (0x81) служба «начать связь» TesterPresent (0x3E) служба «прибор на месте» format byte байт формата: режим адреса и длина checksum контрольная сумма single-wire half-duplex ( HDSEL)однопроводный полудуплекс USART echo эхо: свой байт, вернувшийся с линии P1…P4, W1…W5 времена обмена и инициализации
19к.1 Линия: один провод и эхо
K-line — один провод от прибора ко всем блокам, которые на нём висят. В покое линию держит в единице подтяжка к +12 В бортовой сети, ноль ставит тот, кто передаёт: выход с открытым коллектором прижимает линию к корпусу. Это монтажное И — единица, только пока никто не тянет вниз. Значит, двое одновременно не передают незаметно: ноль одного портит единицу другого.
Отсюда главная особенность: каждый слышит себя. Передатчик и приёмник прибора смотрят на один и тот же провод, и каждый отправленный байт тут же приходит обратно. Эмулятор ELM327 в своём описании так и пишет: каждый байт приложения возвращается эхом, как у настоящего адаптера, потому что линия одна. В rusEFI в цикле приёма — тот же комментарий: из-за одного провода читаем всё, что сами передали. Эхо — не помеха, а проверка: пришло не то, что ушло, — на линии кто-то ещё (19к.6).
Между 12 В линии и выводом на 3,3 В нужен преобразователь. Бывают готовые микросхемы — называют, например, L9637, MC33290 и SI9241; бывает схема на транзисторе: он прижимает линию по сигналу TX, а делитель или второй транзистор приводит линию к уровню входа RX. С документами на эти микросхемы раздел не сверялся — схему и номинал подтяжки (часто пишут 510 Ом) сверяйте с документацией на то, что поставите.
19к.2 Скорость и кадр
Скорость после инициализации — 10400 бод, 8 бит, без чётности, один
стоп. Число 10400 стоит и в эмуляторе (KLINE_BAUDRATE = 10400), и в rusEFI
(KLINE_BAUD_RATE 10400 с пометкой «стандартная скорость»). Бит длится
96,15 мкс, байт из 10 бит — 961,54 мкс (в модели с целыми микросекундами —
962 мкс).
Кадр KWP2000. Первый байт — формат: два старших бита — режим адреса,
шесть младших — длина данных. Дальше приёмник, источник, данные и сумма.
Прибор — 0xF1, общий адрес OBD — 0x33, блок двигателя — 0x10. Если данных
больше 63, длина в формате — ноль, а настоящая идёт отдельным байтом после
адресов. Так кадр собирают и эмулятор, и наш kl_frame_kwp. Пример из
описания эмулятора: запрос C2 33 F1 01 06 ED — формат 0xC2 (общий запрос,
2 байта данных), и ответ 83 F1 33 41 06 84 72.
Кадр ISO 9141-2. Три байта заголовка без длины, данные, сумма. Ответ
двигателя в python-obd начинается с 48 6B 10; разборщик отбрасывает три
байта заголовка и сумму и принимает кадры длиной от 6 до 11 байт. Запрос
прибора в нашей модели — 68 6A F1 (так его описывают для ISO 9141-2; со
стандартом не сверено). Длины в заголовке нет, поэтому конец сообщения
находят по паузе — эмулятор ждёт 20 мс тишины.
Сумма — простая сумма всех байтов по модулю 256. Эмулятор считает её
как sum(message[:-1]) & 0xFF, у нас — так:
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;
}
Запрос оборотов по ISO 9141-2: 68 6A F1 01 0C D0 — 0x68 + 0x6A + 0xF1 + 0x01 +
0x0C = 0x1D0, в байт идёт D0. Ответ имитатора — 48 6B 10 41 0C 14 5F 83:
данные те же, что на CAN в 19г, 1303,75 об/мин.
19к.3 Медленный старт: адрес на 5 бод
Блок на K-line спит, пока его не разбудят. Первый способ — ISO 9141-2 и протокол 4 у ELM327 (в python-obd — «ISO 14230-4 (KWP 5BAUD)»): прибор передаёт адрес 0x33 со скоростью 5 бод. Бит — 200 мс, байт со стартом и стопом — 10 бит, 2 с. Младшим битом вперёд 0x33 — это старт, 1 1 0 0 1 1 0 0, стоп. Модель записала три нуля на линии: 300–500, 900–1300 и 1700–2100 мс (первые 300 мс линия молчит — время W5).
USART так медленно не умеет: делитель на F103 — 16 бит, и при 36 МГц самая низкая скорость — 36 МГц / 65535 = 549,3 бод, при 72 МГц — 1098,6 бод. Поэтому адрес передают ногой: вывод переключают в GPIO с открытым стоком и выставляют биты по таймеру.
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;
Дальше блок отвечает уже на 10400 бод, и каждое время ограничено окном:
| Шаг | Кто | Байт | Окно | В прогоне |
|---|---|---|---|---|
| W1: конец адреса → синхро | блок | 55 | 20…300 мс | 60,001 мс |
| W2: 55 → KB1 | блок | 08 | 5…20 мс | 9,951 мс |
| W3: KB1 → KB2 | блок | 08 | 0…20 мс | 9,951 мс |
| W4: KB2 → ~KB2 | прибор | F7 | 25…50 мс | 29,951 мс |
| W4: ~KB2 → ~адрес | блок | CC | 25…50 мс | 29,951 мс |
Байт синхронизации 0x55 в двоичном виде — 01010101, нули и единицы по
очереди. Ключевые байты говорят, какой протокол дальше:
эмулятор отвечает 55 08 08; для ISO 9141-2 встречается и пара 94 94 —
прибор в прогоне принял обе. Прибор возвращает второй ключевой байт
инвертированным (~0x08 = 0xF7), блок подтверждает инвертированным адресом
(~0x33 = 0xCC). Весь медленный старт в прогоне — 2444,568 мс от начала,
из них 2 с — сам адрес. Окна W1…W5 — по ISO 9141-2 и ISO 14230-2,
с текстом стандартов не сверены: в модели это параметры, и прогон показывает, что
прибор и имитатор в них укладываются.
Нет блока — нет и 0x55: прибор ждёт W1 до 300 мс и сдаётся. В прогоне отказ пришёл через 300,000 мс после конца адреса.
19к.4 Быстрый старт и StartCommunication
KWP2000 умеет будить быстрее — протокол 5 у ELM327, в python-obd «ISO
14230-4 (KWP FAST)», у адаптера команда ATFI. После тишины W5 прибор
прижимает линию на 25 мс, отпускает на 25 мс — всего 50 мс — и сразу на
10400 бод шлёт службу StartCommunication:
| Кто | Кадр | Что это |
|---|---|---|
| прибор | C1 33 F1 81 66 | формат 0xC1 (общий, 1 байт), всем, от прибора, 0x81, сумма |
| блок | 83 F1 10 C1 8F E9 BD | 3 байта, прибору, от двигателя, 0xC1 = 0x81 + 0x40, ключ 8F E9 |
В модели ноль лёг на 300,000–325,000 мс, первый байт запроса — на 350,001 мс: пробуждение 50,001 мс. Готов прибор через 467,343 мс от начала — впятеро быстрее медленного старта.
Ключевые байты здесь расходятся даже внутри одного проекта. Описание
эмулятора обещает ответ 83 F1 33 C1 8F E9 E0, а код шлёт C1 8F 8F:
прогон дал 83 F1 33 C1 8F 8F 86. В комментарии кода сказано, что второй
ключевой байт, 0x8F, приборы проверяют. Наш имитатор отвечает, как в
описании эмулятора, а прибор ключевые байты только запоминает.
Импульс 25 мс USART тоже не даст. Режим LIN шлёт по биту SBK разрыв из 13
нулей (RM0008, раздел 27.3.8; RM0090, 30.3.8) — на 10400 бод это 1,25 мс,
к тому же в режиме LIN бит HDSEL должен быть сброшен. rusEFI включает LIN
(USART_CR2_LINEN), но пробуждения в его kline.cpp нет: файл, как сказано
в его начале, — о своём протоколе Honda начала 2000-х. Импульс, как и 5 бод, —
ногой GPIO.
19к.5 Времена обмена и поддержка связи
После инициализации действуют четыре времени:
| Время | Что | Окно | В прогоне |
|---|---|---|---|
| P1 | между байтами ответа блока | 0…20 мс | 0,951 мс |
| P2 | конец запроса → начало ответа | 25…50 мс | 29,951 мс |
| P3 | конец ответа → новый запрос | 55 мс…5 с | не меньше 55,047 мс |
| P4 | между байтами запроса прибора | 5…20 мс | 5,047 мс |
Окна — по тем же ISO 9141-2 и ISO 14230, со стандартами не сверены. P4 делает запрос медленным: 6 байт запроса оборотов шли 31,007 мс, хотя сами байты занимают меньше 6 мс; 8 байт ответа — 14,353 мс. Прибор ждёт ответ до конца P2 — 50 мс; не пришло ничего — таймаут. На неподдержанный PID 0xA6 имитатор молчит, и прибор сдался через 49,953 мс после конца запроса (отсчёт идёт от эха последнего байта, а оно приходит на середине стопового бита).
Поддержка связи. Если прибор молчит дольше P3max — 5 с, — блок считает
сеанс законченным и снова засыпает. Чтобы держать сеанс, прибор каждые
2 с шлёт TesterPresent: C1 33 F1 3E 23, блок отвечает 81 F1 10 7E 00.
В прогоне запросы 3E ушли на 2694,172 и 4757,687 мс, через 2063,515 мс
(2 с отсчитываются от конца предыдущего обмена), и запрос через 6 с
тишины получил ответ. Без них имитатор закрыл сеанс через 4999,951 мс после
последнего ответа, следующий запрос кончился таймаутом, и помог только
новый быстрый старт. Для ISO 9141-2 наш прибор вместо 3E повторяет запрос
01 00. Эмулятор на 3E не ответил вовсе.
19к.6 На STM32: USART с HDSEL и пробуждение ногой
Один вывод. В однопроводном режиме USART (бит HDSEL в USART_CR3) TX
и RX соединены внутри, вывод RX больше не используется, а TX отпускается,
когда нечего передавать, — поэтому его настраивают как вход или выход
с открытым стоком (RM0008, 27.3.10; то же в RM0090, 30.3.10). Биты LINEN,
CLKEN, SCEN и IREN при этом должны быть сброшены. Эхо в этом режиме
приходит само. С отдельным преобразователем (TX и RX раздельно) можно
обойтись и без HDSEL — эхо тогда приходит через преобразователь.
Вывод PA2 работает в двух ролях: GPIO на время пробуждения и 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
}
Возвращаясь в USART, pin_uart читает SR и DR — сбрасывает то, что
приёмник наловил за время пробуждения: 200-миллисекундные нули он видит как
байты с ошибкой кадра. Прибор и имитатор такие байты отбрасывают (в модели
их набралось по три на медленный старт).
Делитель. USART2 сидит на APB1, формула — из 16.2:
| Порт | Частота | BRR | Скорость | Ошибка |
|---|---|---|---|---|
| F103 USART2 | 36 МГц | 3462 | 10398,6 бод | −0,013 % |
| F103 USART1 | 72 МГц | 6923 | 10400,1 бод | +0,001 % |
| F407 USART2 | 42 МГц | 4038 | 10401,2 бод | +0,011 % |
Эхо как проверка. Прибор шлёт байт и ждёт его эхо, только потом — следующий, через P4. Эхо не совпало или пришло с ошибкой кадра — на линии второй передатчик:
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;
Эхо приходит, когда приёмник отсчитал середину стопового бита, — на
полбита раньше конца байта. Поэтому P3 и P4 прибор отсчитывает с запасом
в один бит (KL_BIT_US, 96 мкс), иначе P4 вышло бы короче 5 мс.
Прогон. Ядро kline.c (прибор и имитатор блока) собрано gcc в контейнере
stm-sys:1 с -Wall -Wextra -Werror -pedantic и запущено на модели линии
kline_host.c: шаг 1 мкс, монтажное И трёх узлов (прибор, блок, второй
прибор), у каждого свой приёмник UART с отсчётом в середине бита.
Шесть сценариев, весь обмен — в kline19k_host.txt:
| Сценарий | Что вышло |
|---|---|
| ISO 9141-2, ключ 08 08 | 01 00, 01 0C, 01 05, 03 и 09 02 (пять сообщений VIN) — ответ; испорченная сумма …5F 82 — отказ по сумме, повтор — ответ; 01 A6 — таймаут |
| ISO 9141-2, ключ 94 94 | старт и 01 0C — ответ |
| KWP2000, быстрый старт | 01 0C и 01 0D — ответ; 6 с тишины с TesterPresent — сеанс жив |
| KWP2000 без поддержки | через 6 с — таймаут, новый быстрый старт — ответ |
| второй прибор на линии | он начал байт C2 через 300 мкс после нашего C2: эхо пришло 02, запрос брошен, повтор — ответ |
| блока нет | отказ через 300 мс после адреса |
Кадры имитатора разобрал python-obd своим разборщиком ISO 9141-2 и KWP:
обороты 1303,75 об/мин, температура 55 °C, скорость 10 км/ч, коды P0133
и P0301. VIN из пяти сообщений он собрал как WP0ZZZ99ZTS39 — 13 знаков из
17: та же обрезка краёв строки, что в 19г.5. Эмулятор ELM327 на те же запросы
KWP ответил теми же данными (41 0C 14 5F, 41 0D 0A, 41 05 5F,
41 00 BE 3F A8 13), только от адреса 0x33, а не 0x10: он меняет местами
адреса запроса.
Размеры. arm-none-eabi-gcc 12.2.1 (stm-arm:1, -Os) собрал всё без
предупреждений: ядро с прибором и имитатором — 2800 байт кода на F103 и
2810 на F407; связь с USART2 и главный цикл kline_stm32.c — 458 и 490 байт
кода плюс 678 байт ОЗУ (состояние прибора и кольцо приёма на 16 элементов).
_Static_assert проверяет при сборке делители 3462 и 4038, 200 мс на бит
5 бод и что период поддержки связи меньше P3max. В QEMU и на железе прошивка
не запускалась.
19к.7 Полный код прибора и имитатора
Прибор — три своих файла: заголовок kline.h, ядро kline.c (кадры,
прибор и имитатор блока) и связь с USART2 kline_stm32.c. Ниже все три
целиком; kline.c разбит на шесть кусков подряд, без пропусков — склеенные
через пустую строку, они дают файл байт в байт. Куски из 19к.2, 19к.3 и
19к.6 здесь повторяются внутри своих функций. Чужого кода нет.
Файла запуска с таблицей векторов, настройки тактов и main() в исходниках
нет: kline_stm32.c собирается только в объектный файл, даёт kl_port_init()
и шаг главного цикла kl_app_step() и считает такты уже настроенными —
HCLK 72 МГц на F103 и 168 МГц на F407. Модель линии kline_host.c — стенд
проверки, а не часть прибора, её здесь нет.
Сборка — та же, что в проверке: каждый файл отдельно под F103 и F407.
CMSIS_F1 — каталог Device/Include пака Keil STM32F1xx_DFP 2.4.1 (в нём
stm32f10x.h), CMSIS_F4 — Drivers/CMSIS/Device/ST/STM32F4xx/Include
пака STM32F4xx_DFP 2.17.1, CMSIS_CORE — CMSIS/Core/Include из 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
Заголовок. Скорости, окна W и P в микросекундах, адреса, сообщение, три функции связи с железом и состояния прибора и имитатора:
/* 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
Кадры. PAST сравнивает время с учётом переполнения счётчика.
Сборка KWP и ISO 9141-2, разбор с проверкой длины и суммы:
/* 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;
}
Прибор: пуск и запрос. Состояния прибора, пробуждение (сначала тишина W5), отправка кадра с отсчётом P3 от конца прошлого обмена, отказ инициализации и разбор принятого сообщения:
/* ════ прибор ════ */
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;
}
Прибор: приём байта. Синхробайт и ключ при медленном старте, своё эхо при передаче, сборка ответа — у KWP по длине в байте формата:
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;
}
}
Прибор: шаг по времени. Адрес на 5 бод и импульс 25 + 25 мс ногой GPIO, сроки W1…W4, поддержка связи, байты запроса через P4, конец ответа по паузе P1 и таймаут P2:
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;
}
Имитатор: пробуждение и ответы. Свои сроки блока, распознавание импульса 25 мс и ответы на 81, 3E, режим 01 (PID 00, 05, 0C, 0D), 03 и 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;
}
}
Имитатор: приём и шаг. Разбор запроса, эхо своих байтов, приём ~KB2, адрес на 5 бод с отсчётом в середине бита и закрытие сеанса после 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. Время в микросекундах из SysTick, вывод PA2 в двух ролях, приём в прерывании через кольцо на 16 элементов и шаг главного цикла:
/* 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 Грабли
12 В на выводе. Линия подтянута к бортовой сети, а там не 12 В ровно: заведённый двигатель, пуск и выбросы. Вывод STM32 на 3,3 В — только через преобразователь или транзистор, никогда напрямую. Питание прибора — тоже через стабилизатор (19г.8).
Два прибора на одной линии. Сканер уже подключён, а вы подключаете свой — или в машине стоит противоугонный блок, который тоже говорит по K-line. Монтажное И смешает байты: в прогоне наш C2 и чужой C2, сдвинутый на 300 мкс, дали эхо 02. Сравнивайте каждое эхо и при расхождении бросайте запрос, а не ждите ответа на испорченный.
Эхо приняли за ответ. Если не вычитать свои байты, прибор примет собственный запрос за начало ответа. Считайте эхо байт в байт — или отключайте приём на время передачи, если преобразователь эха не даёт.
Инвертированный ключ приняли за кадр. Эмулятор ELM327 после 55 08 08
получает от прибора F7 (~KB2) и понимает его как байт формата KWP с длиной
55: следующий запрос C2 33 F1 01 0C F3 он проглотил как часть этого
кадра — в буфере 7 байт, ответа нет. Приёмник блока, который не знает
о медленном старте, так и сломается. Разбирайте инициализацию отдельным
состоянием, а не общим сборщиком кадров.
Разрыв LIN вместо импульса. 13 нулей по биту SBK — 1,25 мс, а быстрому старту нужно 25 мс. Импульс и 5 бод — только ногой GPIO, и после них сбросить мусор в приёмнике.
Сеанс тихо закрылся. Пауза больше 5 с — и блок снова спит; прибор видит только таймауты. Шлите TesterPresent раньше, у нас — раз в 2 с, а после таймаута повторяйте инициализацию.
Сумма и паузы. Испорченная сумма — повод выбросить сообщение и повторить запрос, не больше: эмулятор на кадр с неверной суммой просто молчит, как и наш имитатор. Сообщение ISO 9141-2 кончается паузой — не дописывайте в один буфер ответы разных блоков.
19к.9 Итоги
- K-line — один провод с подтяжкой к +12 В и открытым коллектором: каждый слышит своё эхо, второй передатчик выдаёт себя чужим эхом.
- 10400 бод 8N1: бит 96,15 мкс, байт 961,54 мкс; BRR 3462 на 36 МГц (−0,013 %), 4038 на 42 МГц (+0,011 %).
- Кадр KWP: формат (режим | длина), 0x33 или 0x10, 0xF1, данные, сумма по
модулю 256; ISO 9141-2 —
68 6A F1и ответ48 6B 10. - Медленный старт: 0x33 на 5 бод — 2 с; 55, ключ 08 08 (или 94 94), F7, CC; готово через 2444,568 мс.
- Быстрый старт: 25 + 25 мс,
C1 33 F1 81 66→83 F1 10 C1 8F E9 BD; готово через 467,343 мс. - P2 до 50 мс — потом таймаут; P3 больше 5 с — сеанс закрыт; TesterPresent
3Eкаждые 2 с держит сеанс. - USART ниже 549,3 бод при 36 МГц не опускается, разрыв LIN — 1,25 мс:
пробуждение — ногой GPIO, обмен — USART с
HDSEL. - Ядро прибора и имитатора — 2800 байт кода на F103, 2810 на F407.
Дальше: раздел 19л «LIN: дешёвая шина кузовной электроники — break, sync, PID и расписание кадров» — тот же один провод и USART, но уже рабочая шина кузова: ведущий по расписанию и дешёвые ведомые.