Embedded HandbookПриложения
Все разделы › III. Периферия
STM32F103, STM32G474 · уровень средний · 30 мин чтения
Открыть в интерактивном справочнике

19л. LIN: дешёвая шина кузовной электроники — break, sync, PID и расписание кадров

Раздел 19к разбирал K-line — один провод и USART, которым прибор будит блок и расспрашивает его. LIN похожа на неё снаружи: тот же один провод с подтяжкой к бортовой сети, тот же USART, то же эхо своих байтов. Но задача другая. LIN — не диагностика, а рабочая шина: один ведущий по жёсткому расписанию опрашивает несколько простых ведомых, и каждый кадр приходит в свой слот времени. CAN (раздел 19) для таких узлов дорог, а LIN обходится USART и дешёвым генератором без кварца.

Здесь — линия и скорости, кадр (разрыв, байт 0x55, защищённый идентификатор), ответ и две контрольные суммы, расписание и виды кадров, подстройка ведомого по байту 0x55, режим LIN у USART F103 и G474. В конце — прогон ведущего и двух ведомых на модели шины с шагом 100 нс. Про UART и делитель — раздел 16, про однопроводный USART и эхо — 19к.

Опоры: со спецификацией LIN раздел не сверялся. Режим LIN у USART — по RM0008 (раздел 27.3.8) и RM0440 (разделы 37.5.13 и 37.5.9, таблица 343). Формулы PID и суммы, номера 0x3C/0x3D и множитель 1,4 сверены с двумя открытыми проектами: драйвером LIN из libopeninv (src/linbus.cpp и его тесты test/test_linbus.cpp, где разобраны примеры из спецификации LIN 2.1) и драйвером NXP из дерева Zephyr (hal/nxp/.../drivers/lin: lin_driver.c, fsl_lin.c, fsl_lin_lpuart.h). Лицензии: libopeninv — GPL v3 в заголовке файла и LGPL v3 в LICENSE репозитория, драйвер NXP — BSD-3-Clause; их код пересказан, в проверку не скопирован: проверка вырезает функции из исходников этих проектов и собирает рядом с нашими. Ядро ведущего и ведомого — _verify/lin19l/lin.c, модель шины — lin_host.c, ведущий на USART2 — lin_stm32.c, прогон — lin19l.py.

Как это называется в документации

Оригинал Перевод
LIN (local interconnect network) локальная шина кузова
master / slave node ведущий / ведомый узел
break field, break delimiter разрыв (не меньше 13 нулей), разделитель
sync field (0x55) байт синхронизации
protected identifier (PID) защищённый идентификатор: ID + два бита чётности
header / response заголовок / ответ
classic / enhanced checksum классическая / расширенная контрольная сумма
schedule table, frame slot расписание, слот кадра
unconditional / event triggered / sporadic frame безусловный / по событию / спорадический кадр
master request (0x3C), slave response (0x3D) диагностический запрос / ответ
NAD (node address for diagnostic) адрес узла для диагностики
LINEN, LBDL, LBD / LBDF режим LIN, длина разрыва для приёма, флаг «принят разрыв»
SBK / SBKRQ послать разрыв
auto baud rate detection (ABREN, ABRMOD) автоподбор скорости

19л.1 Где живёт LIN и как устроена линия

LIN собирает мелочь кузова в одну цепь: ведущий — в блоке кузова, ведомые — приводы и датчики, которым хватает пары байт раз в десятки миллисекунд. Так её обычно описывают; в исходниках живой пример один — тесты libopeninv разбирают настоящие кадры LIN электрического отопителя салона Volkswagen ID.3. В нашей модели два ведомых: стеклоподъёмник s1 и датчик дождя s2.

Линия — один провод. Как у K-line, это монтажное И: единицу держит подтяжка к +12 В, ноль ставит тот, кто передаёт, а каждый передатчик слышит себя эхом. Подтяжка ведущего сильнее, у ведомых — слабая. Между выводом STM32 и линией стоит трансивер (называют, например, TJA1020 и TJA1021): он переводит 3,3 В в уровни линии и обратно. С документами на трансиверы раздел не сверялся — номиналы подтяжек и уровни берите из документа на тот, что поставите.

Скорости. Драйвер libopeninv в описании принимает 9600 или 19200 бод, драйвер NXP при автоподборе узнаёт 19200, 14400, 9600, 4800 и 2400. Бит на 19200 — 52,08 мкс, на 9600 — 104,17 мкс, на 2400 — 416,67 мкс. Дальше все числа — для 19200.

Сверху блок кузова с ведущим: STM32 с USART2 в режиме LINEN, где SBK шлёт разрыв, а LBD сообщает о принятом разрыве, через трансивер LIN выходит на один провод, который сильная подтяжка ведущего держит у +12 В; на линии два ведомых — стеклоподъёмник s1 с частотой на 2,5 процента выше и датчик дождя s2 на 7 процентов ниже, оба подстраиваются по байту 0x55. Посередине кадр 0x10 на 19200 бод: разрыв 13 бит, 677 мкс, разделитель 1 бит, синхро 0x55, PID 0x50, данные 01 80 и сумма 2E; заголовок 34 бита, 1770,83 мкс, ответ 10 × (N + 1) бит; формулы P0 и P1. Внизу четыре слота по 10 мс из прогона: 0x10 от ведущего без ошибок, 0x21 с неверной суммой FB вместо FA, 0x21 без ответа — отказ через 4667 мкс, 0x30 по событию, где s1 и s2 ответили разом и столкнулись


19л.2 Кадр: разрыв, 0x55 и защищённый идентификатор

Кадр начинает всегда ведущий. Заголовок — три части:

  1. Разрыв — не меньше 13 битов нуля подряд и за ним единица-разделитель. Обычный байт 0x00 даёт только 9 нулей (старт и 8 бит), поэтому разрыв с данными не спутать. На 19200 13 битов — 677,08 мкс; в модели с шагом 100 нс ноль на шине продержался 677,00 мкс.
  2. Синхро — байт 0x55, в двоичном виде нули и единицы по очереди. По его спадам ведомый без кварца меряет длину бита (19л.5).
  3. PID — защищённый идентификатор: 6 бит ID (0…63) и два бита чётности.

Биты чётности — такие же в libopeninv и в драйвере NXP:

uint8_t lin_pid(uint8_t id)
{
    id &= 0x3Fu;
    unsigned p0 = BIT(id, 0) ^ BIT(id, 1) ^ BIT(id, 2) ^ BIT(id, 4);
    unsigned p1 = !(BIT(id, 1) ^ BIT(id, 3) ^ BIT(id, 4) ^ BIT(id, 5));
    return (uint8_t)(id | (p0 << 6) | (p1 << 7));
}

P0 = ID0 ^ ID1 ^ ID2 ^ ID4 — в бит 6, P1 = ¬(ID1 ^ ID3 ^ ID4 ^ ID5) — в бит 7. Все 64 значения (строка — старшая часть ID, столбец — прибавка):

ID +0 +1 +2 +3 +4 +5 +6 +7
0x00 80 C1 42 03 C4 85 06 47
0x08 08 49 CA 8B 4C 0D 8E CF
0x10 50 11 92 D3 14 55 D6 97
0x18 D8 99 1A 5B 9C DD 5E 1F
0x20 20 61 E2 A3 64 25 A6 E7
0x28 A8 E9 6A 2B EC AD 2E 6F
0x30 F0 B1 32 73 B4 F5 76 37
0x38 78 39 BA FB 3C 7D FE BF

Таблицу построил наш lin_pid; функции Parity из libopeninv и LIN_ProcessParity из драйвера NXP, собранные g++ рядом, дали те же 64 из 64, а четыре примера из тестов libopeninv (0x00 → 0x80, 0x17 → 0x97, 0x2B → 0x2B, 0x3B → 0xFB) сошлись. Из 256 возможных байтов верными PID оказываются только 64 — остальные 192 ведомый отвергает и молчит. Диагностические ID 0x3C и 0x3D дают PID 0x3C и 0x7D — именно эти числа стоят в коде NXP.


19л.3 Ответ и контрольная сумма

За заголовком идёт ответ: 1…8 байт данных и контрольная сумма. Шлёт его тот, кому принадлежит этот ID: сам ведущий (команда) или один из ведомых (состояние). Длину ответа знают заранее оба — в кадре её нет.

Сумма — сложение байтов с переносом: если вышло больше 0xFF, из суммы вычитают 0xFF (перенос возвращается в младший бит), в конце всё инвертируют. Две разновидности:

uint8_t lin_checksum(uint8_t pid, const uint8_t *d, unsigned n, int enhanced)
{
    unsigned s = enhanced ? pid : 0u;
    while (n--) {
        s += *d++;
        if (s > 0xFFu)
            s -= 0xFFu;             /* перенос из старшего бита — обратно в младший */
    }
    return (uint8_t)~s;
}

int lin_enhanced_for(uint8_t id, int lin2)
{
    return lin2 && id != LIN_ID_MREQ && id != LIN_ID_SRESP;
}

Кадры 0x3C и 0x3D всегда идут с классической суммой — так пишет в комментарии драйвер NXP, и так делает его LIN_MakeChecksumByte. Остальные кадры в нашем ядре — с расширенной (это LIN 2.x; LIN 1.x считала классическую для всех — по спецификации это не сверено).

Пример из прогона: ведущий шлёт кадр 0x10 (PID 0x50) с данными 01 80. Классическая сумма — 0x7E, расширенная — 0x2E; на шину ушло 01 80 2E. Пример из спецификации LIN 2.1, который приводят тесты libopeninv: PID 0x4A, данные 55 93 E5 — сумма 0xE6; наш код даёт то же, как и ещё четыре вектора тех же тестов (0x72, 0x05, 0x82, 0x5A).


19л.4 Расписание и виды кадров

Ведущий не ждёт событий — он идёт по расписанию: таблице кадров, где у каждого свой слот. Слот должен вместить самый длинный кадр. Заголовок — 34 бита (13 разрыв + 1 разделитель + 10 синхро + 10 PID), ответ — 10 × (N + 1) бит, где N — число байт данных. Номинал на 19200 и предел 1,4 × номинала — по формулам из комментариев драйвера NXP (LIN_CalcMaxHeaderTimeoutCnt, LIN_CalcMaxResTimeoutCnt):

N Номинал Не дольше (1,4 ×)
2 3333,33 мкс 4667 мкс
3 3854,17 мкс 5396 мкс
4 4375,00 мкс 6125 мкс
8 6458,33 мкс 9042 мкс

Заголовок сам по себе — 1770,83 мкс. Кадр из 8 байт помещается в слот 10 мс — это проверяет _Static_assert в lin.h. Не пришёл ответ за 1,4 номинала — кадр потерян:

    if (m->busy) {
        if (PAST(now, m->f.t_start + m->t_max)) {       /* 1,4 × номинала вышло */
            int r = m->f.n ? LIN_INCOMPLETE : LIN_NO_RESPONSE;
            if (m->tab[m->idx].kind == LIN_EVENT)        /* по событию молчать можно */
                r = m->f.n ? LIN_COLLISION : LIN_NO_NEWS;
            master_finish(m, r, now);
        }
        return;
    }

Виды кадров. Драйвер NXP перечисляет четыре: безусловный, по событию, спорадический и диагностический.

Смысл кадров по событию и спорадических, раскладка NAD и PCI взяты не из документа — в исходниках есть только названия видов у NXP и поле NAD в его структурах. В прогоне так устроены наши ведущий и ведомые.

Прогон. Расписание из четырёх кадров по 10 мс: 0x10 (команда стеклоподъёмнику, 2 байта от ведущего), 0x20 (состояние s1, 4 байта), 0x21 (датчик дождя s2, 2 байта), 0x30 (по событию, 3 байта). Частота s1 выше нормы на 2,5 %, s2 — ниже на 7 %. Ведомый начинает ответ через 60 мкс после приёма PID — пауза перед ответом на шине вышла 34 мкс. Четыре круга, в каждом своя беда:

Круг Что сделали Что увидел ведущий
1 всё исправно 0x10, 0x20, 0x21 — ответ; 0x30 — никто не ответил через 5396 мкс
2 s2 портит сумму, у s1 новость 0x21: пришло 07 9C FB, должно быть FA — ошибка суммы на 3341 мкс; 0x30: 20 41 12 9B от s1
3 s2 отключён 0x21: нет ответа через 4667 мкс, остальные кадры в порядке
4 s2 вернулся, новости у обоих 0x21 снова отвечает; 0x30: на шине 20 00 10 09 — столкновение

Кадр 0x10 занял 3333,04 мкс (номинал 3333,33; время — по эху, с точностью до микросекунды), 0x20 — 4409,04 мкс, 0x21 — 3367,04 мкс: номинал плюс пауза перед ответом. Столкновение в круге 4 поучительно: s1 слал 20 41 12 9B, s2 — 61 08 9C 09, а монтажное И дало 20 00 10 09. Первый байт 0x20 выглядит как ответ одного s1 — выдала только сумма. После расписания ведущий перешёл на таблицу диагностики: запрос 02 03 22 01 02 FF FF FF с суммой D5 (NAD 2, PCI 03, служба 0x22, DID 0x0102) и ответ s2 02 04 62 01 02 2A FF FF с суммой 6A, оба кадра — с классической суммой. Кадр из 8 байт занял 6458,04 мкс.


19л.5 Ведомый без кварца: подстройка по 0x55

Дешёвый ведомый работает от внутреннего генератора, а тот уходит на проценты. Обычному UART этого не пережить — в 16.1 предел назван в 2–3 %. LIN выручает байт 0x55: в нём пять спадов (старт и биты 1, 3, 5, 7), от первого до пятого — ровно 8 бит. Ведомый меряет этот промежуток своим таймером, делит на 8 и ставит себе такую длину бита до конца кадра.

В модели так делает приёмник ведомого сразу после разрыва. s2, у которого частота ниже на 7 %, до подстройки считал бит в 56,004 мкс, после — 52,087 мкс; s1 — 50,813 → 52,087 мкс. Драйвер NXP делает то же самое: меряет пары бит в 0x55, соседние замеры не должны расходиться больше чем на 14 % (BIT_RATE_TOLERANCE_UNSYNC = 14), а найденная скорость — отстоять от одной из известных больше чем на 2 % (AUTOBAUD_BAUDRATE_TOLERANCE).

Насколько это расширяет допуск, показал прогон кадров 0x10 и 0x20 с одним ведомым при уходе его частоты от −20 до +20 % через 1 %:

Ведомый Разделитель после разрыва Кадры прошли
без подстройки 1 бит от −4 до +5 %
с подстройкой по 0x55 1 бит (F103) от −17 до +20 %, кроме −11 %
с подстройкой по 0x55 2 бита (G474) от −19 до +20 %

Без подстройки кадр рвётся уже на −5 и +6 %. С подстройкой отказы остались только при медленном ведомом, и причина одна: его отсчёты длиннее бита, и разделитель в один бит может проскочить между двумя отсчётами — тогда старт байта 0x55 принят за продолжение разрыва, и подстройка меряет не те спады. На −11 % так и вышло. G4 после разрыва держит на линии единицу два бита (RM0440, 37.5.13; F103 — один, RM0008, 27.3.8), и провал на −11 % исчез. Выше +20 % прогон не заходил.

Автоподбор у G4. Новые USART умеют мерить скорость сами (RM0440, 37.5.9): бит ABREN в CR2 и режим ABRMOD = 3 — «байт 0x55»: скорость уточняется по стартовому биту, по биту 0 и по биту 6, об окончании говорит флаг ABRF. Таблица 343 RM0440: режим LIN и автоподбор есть у USART1–3 и UART4/5, у LPUART1 — нет. В нашем прогоне этот путь не проверялся — подстройку делает модель приёмника, а не модель USART G4.


19л.6 На STM32: USART в режиме LIN

Режим. Бит LINEN в USART_CR2. В режиме LIN должны быть сброшены STOP[1:0] и CLKEN, а в CR3 — SCEN, HDSEL и IREN; слово — 8 бит (M = 0). Значит, однопроводного HDSEL из 19к здесь нет: TX и RX идут на трансивер раздельно. Эхо при этом остаётся: libopeninv после передачи заголовка ищет в приёмном буфере свой же PID.

Передача разрыва. На F103 — бит SBK в CR1: 13 нулей и единица (RM0008, 27.3.8). Бит сам сбрасывается во время стопового бита разрыва. На G474 — запись SBKRQ в USART_RQR: 13 нулей и две единицы (RM0440, 37.5.13), флаг SBKF в ISR показывает, что разрыв ещё не ушёл. В обоих случаях разрыв идёт после текущего байта, поэтому ведущий ставит запрос и сразу даёт передатчику 0x55 и PID — так же, по SBK, делает libopeninv:

static void hw_break(void *c)
{
    (void)c;
#if defined(STM32F10X_MD)
    USART2->CR1 |= USART_CR1_SBK;                /* 13 нулей и единица-разделитель */
#else
    USART2->RQR = USART_RQR_SBKRQ;               /* 13 нулей и две единицы */
#endif
}

Приём разрыва. Схема обнаружения разрыва работает независимо от приёмника: после старта она считает нули с отсчётом в середине бита, и если их набралось 10 (LBDL = 0) или 11 (LBDL = 1), а за ними пришла единица, ставит LBD в SR (на G4 — LBDF в ISR) и, при LBDIE, прерывание. Сам разрыв приёмник видит как байт с ошибкой кадра; ведущий такой байт пропускает, ведомому это знак начать кадр. Включение — три строки:

    USART2->BRR = LIN_BRR;
    USART2->CR2 = USART_CR2_LINEN | USART_CR2_LBDL | USART_CR2_LBDIE;     /* LIN, разрыв 11 бит */
    USART2->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_RXNEIE | USART_CR1_UE;

В модели порог тоже 11. Байт 0x00 в ответе s1 (40 12 00 05) разрывом не стал: за 16 кадров расписания ведущий и s1 насчитали по 16 разрывов, s2 — 12 (круг 3 он был отключён).

Делитель — по формуле 16.2, USART2 на APB1:

Порт Частота 19200 9600 2400
F103 USART2 36 МГц 1875, точно 3750, точно 15000, точно
F103 USART1 72 МГц 3750, точно 7500, точно 30000, точно
G474 USART2 170 МГц 8854: 19200,4 бод, +0,002 % 17708: 9600,2 бод, +0,002 % 70833 — в 16 бит не помещается

BRR — 16 бит, поэтому на 170 МГц ниже 2594,0 бод без предделителя не опуститься: для 2400 нужен PRESC (RM0440, регистр USART_PRESC), о нём — 16.9. На F103 при 36 МГц предел — 549,3 бод.

Сборка. arm-none-eabi-gcc 12.2.1 (stm-arm:1, -Os) собрал всё без предупреждений: ядро lin.c (PID, суммы, ведущий и ведомый) — 1228 байт кода и на F103, и на G474; ведущий на USART2 с прерыванием и временем от счётчика тактов DWT, lin_stm32.c, — 624 байта на F103 и 616 на G474 плюс 312 байт ОЗУ. _Static_assert проверяет делители 1875 и 8854.

QEMU. Модель USART в QEMU 7.2.22 (плата stm32vldiscovery) режим LIN не знает: после SBK бит так и стоял через 100000 опросов, на последовательный порт ушли только байты 55 50, разрыва не было, SR в конце — 0xC0 (только TXE и TC). Поэтому логику проверяли на модели шины, а прошивку — только сборкой.


19л.7 Полный код ядра и ведущего

Ведущий — три своих файла: заголовок lin.h, ядро lin.c (PID, суммы, ведущий по расписанию и ведомый) и ведущий на USART2 lin_stm32.c. Ниже все три целиком: lin.c и lin_stm32.c разбиты на куски подряд, без пропусков — склеенные через пустую строку, они дают файлы байт в байт. Куски из 19л.2, 19л.3, 19л.4 и 19л.6 повторяются внутри своих функций. Чужого кода нет.

Файла запуска с таблицей векторов (в ней нужен USART2_IRQHandler), скрипта компоновщика и настройки тактов в исходниках нет: lin_stm32.c собирается только в объектный файл и считает такты уже настроенными — 72 МГц на F103, 170 МГц на G474. Ведомого на STM32 в исходниках тоже нет: lin_slave_break() и lin_slave_byte() прогонялись только на модели шины lin_host.c — она, как и проба QEMU qemu_lin.c, стенд проверки, а не часть прибора.

Сборка — та же, что в проверке: каждый файл отдельно под F103 и G474. CMSIS_F1 — каталог Device/Include пака Keil STM32F1xx_DFP 2.4.1 (в нём stm32f10x.h), CMSIS_G4 — Drivers/CMSIS/Device/ST/STM32G4xx/Include пака STM32G4xx_DFP 1.6.0, CMSIS_CORE — CMSIS/Core/Include из CMSIS 5.9.0 (github.com/ARM-software/CMSIS_5):

for f in lin lin_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 -DSTM32G474xx \
    -I. -I$CMSIS_G4 -I$CMSIS_CORE -c $f.c -o G4_$f.o
done

Заголовок. Скорость, служебные ID 0x3C и 0x3D, наибольшая длина кадра (1,4 × номинала) с проверкой при сборке, связь с железом, расписание и состояния ведущего и ведомого:

/* Раздел 19л: LIN — ведущий с расписанием и ведомый, общее ядро для ПК и STM32.
 * Время — в микросекундах (uint32_t, с переполнением). Ядро не знает про USART:
 * всё железо — через lin_hw_t. */
#ifndef LIN_H
#define LIN_H
#include <stdint.h>

#define LIN_BAUD          19200u
#define LIN_MAX_DATA      8u
#define LIN_ID_MREQ       0x3Cu   /* диагностика: запрос ведущего */
#define LIN_ID_SRESP      0x3Du   /* диагностика: ответ ведомого */
#define LIN_HEADER_BITS   34u     /* 13 разрыв + 1 разделитель + 10 синхро + 10 PID */

/* наибольшая длина кадра: 1,4 × (34 + 10 × (N + 1)) бит, в мкс */
#define LIN_FRAME_MAX_US(n, baud) \
    ((uint32_t)((14ull * (LIN_HEADER_BITS + 10u * ((n) + 1u)) * 1000000ull + 10ull * (baud) - 1u) / (10ull * (baud))))

_Static_assert(LIN_FRAME_MAX_US(8u, 19200u) == 9042u, "8 байт на 19200: не длиннее 9042 мкс");
_Static_assert(LIN_FRAME_MAX_US(8u, 19200u) < 10000u, "кадр из 8 байт помещается в слот 10 мс");

uint8_t lin_pid(uint8_t id);                      /* 6 бит ID + P0, P1 */
int lin_pid_check(uint8_t pid);                   /* ID или -1 при ошибке чётности */
uint8_t lin_checksum(uint8_t pid, const uint8_t *d, unsigned n, int enhanced);
int lin_enhanced_for(uint8_t id, int lin2);       /* 0x3C и 0x3D — всегда классическая */

typedef struct {
    void (*send_break)(void *ctx);                        /* 13 нулей + разделитель */
    void (*send_bytes)(void *ctx, const uint8_t *b, unsigned n);
    void *ctx;
} lin_hw_t;

/* ── ведущий ── */
enum { LIN_PUB_MASTER, LIN_PUB_SLAVE, LIN_EVENT };
enum { LIN_OK, LIN_NO_RESPONSE, LIN_INCOMPLETE, LIN_CHECKSUM, LIN_READBACK, LIN_NO_NEWS, LIN_COLLISION };

typedef struct {
    uint8_t id, len, kind;
    uint16_t slot_ms;
} lin_entry_t;

typedef struct {
    uint8_t id, pid, len, n, result, cs;
    uint8_t data[LIN_MAX_DATA + 1];
    uint32_t t_start, t_end;
} lin_frame_t;

typedef struct {
    lin_hw_t hw;
    const lin_entry_t *tab;
    unsigned tab_n, idx;
    uint32_t baud;
    int lin2;
    int busy;
    uint32_t slot_start, t_max;
    lin_frame_t f;
    uint8_t tx[LIN_MAX_DATA + 3];
    unsigned tx_n, echo_i;
    int (*get_data)(void *ctx, uint8_t id, uint8_t *buf);   /* данные кадров ведущего */
    void (*on_frame)(void *ctx, const lin_frame_t *f);
} lin_master_t;

void lin_master_start(lin_master_t *m, uint32_t now);
void lin_master_poll(lin_master_t *m, uint32_t now);
void lin_master_byte(lin_master_t *m, uint8_t b, int fe, uint32_t now);

/* ── ведомый ── */
typedef struct {
    uint8_t id, len, publish;   /* publish: 1 — отвечает, 0 — слушает, 2 — по событию */
    uint8_t data[LIN_MAX_DATA];
    uint8_t news;               /* для кадра по событию: есть что сказать */
} lin_sframe_t;

typedef struct {
    lin_hw_t hw;
    lin_sframe_t *tab;
    unsigned tab_n;
    int lin2;
    uint8_t nad;
    int state;
    lin_sframe_t *cur;
    uint8_t pid, rx[LIN_MAX_DATA + 1];
    unsigned rx_n, echo_left;
    uint8_t diag[LIN_MAX_DATA];
    int diag_ready;
    int bad_cs_once, mute;      /* порча для прогона */
    unsigned n_ok, n_bad;
    void (*on_frame)(void *ctx, uint8_t id, const uint8_t *d, unsigned n, int ok);
} lin_slave_t;

void lin_slave_break(lin_slave_t *s);
void lin_slave_byte(lin_slave_t *s, uint8_t b, int fe);

#endif

PID и суммы. Два бита чётности, проверка PID, сумма с переносом и выбор классической или расширенной:

/* Раздел 19л: ядро LIN — PID, контрольные суммы, ведущий с расписанием, ведомый. */
#include "lin.h"
#include <string.h>

#define PAST(now, t) ((int32_t)((now) - (t)) >= 0)
#define BIT(x, n) (((x) >> (n)) & 1u)

uint8_t lin_pid(uint8_t id)
{
    id &= 0x3Fu;
    unsigned p0 = BIT(id, 0) ^ BIT(id, 1) ^ BIT(id, 2) ^ BIT(id, 4);
    unsigned p1 = !(BIT(id, 1) ^ BIT(id, 3) ^ BIT(id, 4) ^ BIT(id, 5));
    return (uint8_t)(id | (p0 << 6) | (p1 << 7));
}

int lin_pid_check(uint8_t pid)
{
    return lin_pid(pid & 0x3Fu) == pid ? (int)(pid & 0x3Fu) : -1;
}

uint8_t lin_checksum(uint8_t pid, const uint8_t *d, unsigned n, int enhanced)
{
    unsigned s = enhanced ? pid : 0u;
    while (n--) {
        s += *d++;
        if (s > 0xFFu)
            s -= 0xFFu;             /* перенос из старшего бита — обратно в младший */
    }
    return (uint8_t)~s;
}

int lin_enhanced_for(uint8_t id, int lin2)
{
    return lin2 && id != LIN_ID_MREQ && id != LIN_ID_SRESP;
}

Ведущий. Слот расписания: заголовок (разрыв, 0x55, PID), свой ответ для кадров ведущего, эхо байт в байт, приём ответа и итог кадра — по сумме или по сроку 1,4 × номинала:

/* ════════ ведущий ════════ */

static void master_finish(lin_master_t *m, int result, uint32_t now)
{
    m->f.result = (uint8_t)result;
    m->f.t_end = now;
    m->busy = 0;
    if (m->on_frame)
        m->on_frame(m->hw.ctx, &m->f);
    m->slot_start += (uint32_t)m->tab[m->idx].slot_ms * 1000u;
    m->idx = (m->idx + 1u) % m->tab_n;
}

void lin_master_start(lin_master_t *m, uint32_t now)
{
    m->idx = 0;
    m->busy = 0;
    m->slot_start = now;
}

void lin_master_poll(lin_master_t *m, uint32_t now)
{
    if (m->busy) {
        if (PAST(now, m->f.t_start + m->t_max)) {       /* 1,4 × номинала вышло */
            int r = m->f.n ? LIN_INCOMPLETE : LIN_NO_RESPONSE;
            if (m->tab[m->idx].kind == LIN_EVENT)        /* по событию молчать можно */
                r = m->f.n ? LIN_COLLISION : LIN_NO_NEWS;
            master_finish(m, r, now);
        }
        return;
    }
    if (!PAST(now, m->slot_start))
        return;
    const lin_entry_t *e = &m->tab[m->idx];
    memset(&m->f, 0, sizeof m->f);
    m->f.id = e->id;
    m->f.pid = lin_pid(e->id);
    m->f.len = e->len;
    m->f.t_start = now;
    m->t_max = LIN_FRAME_MAX_US(e->len, m->baud);
    m->tx[0] = 0x55u;
    m->tx[1] = m->f.pid;
    m->tx_n = 2;
    if (e->kind == LIN_PUB_MASTER) {                     /* ответ шлёт сам ведущий */
        m->get_data(m->hw.ctx, e->id, &m->tx[2]);
        m->tx[2 + e->len] = lin_checksum(m->f.pid, &m->tx[2], e->len, lin_enhanced_for(e->id, m->lin2));
        m->tx_n += e->len + 1u;
    }
    m->echo_i = 0;
    m->busy = 1;
    m->hw.send_break(m->hw.ctx);
    m->hw.send_bytes(m->hw.ctx, m->tx, m->tx_n);
}

void lin_master_byte(lin_master_t *m, uint8_t b, int fe, uint32_t now)
{
    if (!m->busy || fe)
        return;                                   /* сам разрыв приходит ошибкой кадра */
    if (m->echo_i < m->tx_n) {                    /* своё эхо: читаем обратно */
        if (b != m->tx[m->echo_i]) {
            master_finish(m, LIN_READBACK, now);
            return;
        }
        if (++m->echo_i == m->tx_n && m->tab[m->idx].kind == LIN_PUB_MASTER) {
            memcpy(m->f.data, &m->tx[2], m->f.len + 1u);
            m->f.n = (uint8_t)(m->f.len + 1u);
            m->f.cs = m->tx[2 + m->f.len];
            master_finish(m, LIN_OK, now);
        }
        return;
    }
    m->f.data[m->f.n++] = b;
    if (m->f.n < m->f.len + 1u)
        return;
    m->f.cs = b;
    int enh = lin_enhanced_for(m->f.id, m->lin2);
    int ok = lin_checksum(m->f.pid, m->f.data, m->f.len, enh) == b;
    if (m->tab[m->idx].kind == LIN_EVENT)
        master_finish(m, ok ? LIN_OK : LIN_COLLISION, now);
    else
        master_finish(m, ok ? LIN_OK : LIN_CHECKSUM, now);
}

Ведомый. Разрыв приходит байтом с ошибкой кадра и сбрасывает автомат; дальше 0x55, PID с проверкой чётности, ответ или приём, диагностика 0x3C/0x3D со службой 0x22:

/* ════════ ведомый ════════ */

enum { S_BREAK, S_SYNC, S_PID, S_DATA, S_SEND };

void lin_slave_break(lin_slave_t *s)
{
    s->state = S_SYNC;                            /* всё прежнее — забыть */
}

static void slave_send(lin_slave_t *s, const uint8_t *d, unsigned n, int enh)
{
    uint8_t out[LIN_MAX_DATA + 1];
    memcpy(out, d, n);
    out[n] = lin_checksum(s->pid, d, n, enh);
    if (s->bad_cs_once) {
        out[n] ^= 0x01u;
        s->bad_cs_once = 0;
    }
    s->echo_left = n + 1u;
    s->state = S_SEND;
    s->hw.send_bytes(s->hw.ctx, out, n + 1u);
}

void lin_slave_byte(lin_slave_t *s, uint8_t b, int fe)
{
    if (fe) {
        s->state = S_BREAK;
        return;
    }
    switch (s->state) {
    case S_SYNC:
        s->state = (b == 0x55u) ? S_PID : S_BREAK;
        break;
    case S_PID: {
        int id = lin_pid_check(b);
        s->state = S_BREAK;
        if (id < 0 || s->mute)
            return;                               /* чётность не сошлась — молчим */
        s->pid = b;
        s->cur = 0;
        for (unsigned i = 0; i < s->tab_n; i++)
            if (s->tab[i].id == id)
                s->cur = &s->tab[i];
        if (id == LIN_ID_SRESP && s->diag_ready) {
            s->diag_ready = 0;
            slave_send(s, s->diag, 8, 0);
            return;
        }
        if (id == LIN_ID_MREQ) {
            s->cur = 0;
            s->rx_n = 0;
            s->state = S_DATA;
            return;
        }
        if (!s->cur)
            return;
        if (s->cur->publish == 1 || (s->cur->publish == 2 && s->cur->news)) {
            s->cur->news = 0;
            slave_send(s, s->cur->data, s->cur->len, lin_enhanced_for((uint8_t)id, s->lin2));
        } else if (s->cur->publish == 0) {
            s->rx_n = 0;
            s->state = S_DATA;
        }
        break;
    }
    case S_DATA: {
        unsigned len = s->cur ? s->cur->len : 8u;
        s->rx[s->rx_n++] = b;
        if (s->rx_n < len + 1u)
            break;
        s->state = S_BREAK;
        uint8_t id = s->pid & 0x3Fu;
        int ok = lin_checksum(s->pid, s->rx, len, lin_enhanced_for(id, s->lin2)) == s->rx[len];
        ok ? s->n_ok++ : s->n_bad++;
        if (ok && s->cur)
            memcpy(s->cur->data, s->rx, len);
        if (ok && !s->cur && s->rx[0] == s->nad && s->rx[2] == 0x22u) {
            /* диагностика: NAD, PCI, служба 0x22 (19д) — готовим ответ на 0x3D */
            const uint8_t r[8] = {s->nad, 0x04u, 0x62u, s->rx[3], s->rx[4], 0x2Au, 0xFFu, 0xFFu};
            memcpy(s->diag, r, 8);
            s->diag_ready = 1;
        }
        if (s->on_frame)
            s->on_frame(s->hw.ctx, id, s->rx, len, ok);
        break;
    }
    case S_SEND:
        if (s->echo_left && --s->echo_left == 0)
            s->state = S_BREAK;                   /* своё эхо дослушали */
        break;
    default:
        break;
    }
}

USART2 в режиме LIN. Время из счётчика тактов DWT, выводы, делитель, LINEN и порог разрыва 11 бит:

/* Раздел 19л: ведущий LIN на USART2 (PA2 — TX, PA3 — RX) через трансивер.
 * STM32F103: PCLK1 = 36 МГц, HCLK = 72 МГц.  STM32G474: PCLK1 = HCLK = 170 МГц.
 * Ядро lin.c то же, что прогонялось на модели шины. */
#include "lin.h"
#if defined(STM32F10X_MD)
#include "stm32f10x.h"
#define PCLK1 36000000u
#define HCLK  72000000u
#else
#include "stm32g4xx.h"
#define PCLK1 170000000u
#define HCLK  170000000u
#endif

#define LIN_BRR ((PCLK1 + LIN_BAUD / 2u) / LIN_BAUD)
_Static_assert(LIN_BRR == (PCLK1 == 36000000u ? 1875u : 8854u), "делитель на 19200 бод");

static lin_master_t M;
static volatile uint16_t ring[16];               /* байт | ошибка кадра << 8 */
static volatile uint32_t ring_t[16];
static volatile unsigned rh, rt;
static const uint8_t *tx_p;
static volatile unsigned tx_n;
static uint32_t cyc_last, now_us;

static uint32_t micros(void)                     /* микросекунды из счётчика тактов DWT */
{
    uint32_t d = DWT->CYCCNT - cyc_last;
    uint32_t u = d / (HCLK / 1000000u);
    cyc_last += u * (HCLK / 1000000u);
    now_us += u;
    return now_us;
}

static void usart2_lin_init(void)
{
#if defined(STM32F10X_MD)
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    RCC->APB1ENR |= RCC_APB1ENR_USART2EN;
    GPIOA->CRL = (GPIOA->CRL & ~0xFF00u) | 0x4B00u;   /* PA2 — выход AF, PA3 — вход */
#else
    RCC->AHB2ENR |= RCC_AHB2ENR_GPIOAEN;
    RCC->APB1ENR1 |= RCC_APB1ENR1_USART2EN;
    GPIOA->AFR[0] = (GPIOA->AFR[0] & ~0xFF00u) | 0x7700u;               /* AF7 */
    GPIOA->MODER = (GPIOA->MODER & ~0xF0u) | 0xA0u;
#endif
    USART2->BRR = LIN_BRR;
    USART2->CR2 = USART_CR2_LINEN | USART_CR2_LBDL | USART_CR2_LBDIE;     /* LIN, разрыв 11 бит */
    USART2->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_RXNEIE | USART_CR1_UE;
    NVIC_EnableIRQ(USART2_IRQn);
}

Разрыв, передача и прерывание. Разрыв — SBK на F103 и SBKRQ на G474, байты — по TXE, принятые — в кольцо вместе с ошибкой кадра и временем:

static void hw_break(void *c)
{
    (void)c;
#if defined(STM32F10X_MD)
    USART2->CR1 |= USART_CR1_SBK;                /* 13 нулей и единица-разделитель */
#else
    USART2->RQR = USART_RQR_SBKRQ;               /* 13 нулей и две единицы */
#endif
}

static void hw_bytes(void *c, const uint8_t *b, unsigned n)
{
    (void)c;
    tx_p = b;                                    /* буфер ядра живёт до конца кадра */
    tx_n = n;
    USART2->CR1 |= USART_CR1_TXEIE;              /* байты пойдут после разрыва */
}

void USART2_IRQHandler(void)
{
#if defined(STM32F10X_MD)
    uint32_t sr = USART2->SR;
    if (sr & USART_SR_LBD)
        USART2->SR = (uint16_t)~USART_SR_LBD;    /* разрыв ведущему не нужен: только сброс */
    if (sr & USART_SR_RXNE) {
        ring[rt % 16u] = (uint16_t)((USART2->DR & 0xFFu) | ((sr & USART_SR_FE) ? 0x100u : 0u));
        ring_t[rt % 16u] = micros();
        rt++;
    }
    if ((USART2->CR1 & USART_CR1_TXEIE) && (sr & USART_SR_TXE)) {
        if (tx_n) { USART2->DR = *tx_p++; tx_n--; }
        else USART2->CR1 &= ~USART_CR1_TXEIE;
    }
#else
    uint32_t isr = USART2->ISR;
    if (isr & USART_ISR_LBDF)
        USART2->ICR = USART_ICR_LBDCF;
    if (isr & USART_ISR_RXNE) {
        ring[rt % 16u] = (uint16_t)((USART2->RDR & 0xFFu) | ((isr & USART_ISR_FE) ? 0x100u : 0u));
        ring_t[rt % 16u] = micros();
        rt++;
        USART2->ICR = USART_ICR_FECF | USART_ICR_ORECF;
    }
    if ((USART2->CR1 & USART_CR1_TXEIE) && (isr & USART_ISR_TXE)) {
        if (tx_n) { USART2->TDR = *tx_p++; tx_n--; }
        else USART2->CR1 &= ~USART_CR1_TXEIE;
    }
#endif
}

Расписание и главный цикл. Четыре кадра по 10 мс, данные кадра 0x10, последние ответы — в массив last для отладчика:

static const lin_entry_t TAB[] = {
    {0x10, 2, LIN_PUB_MASTER, 10}, {0x20, 4, LIN_PUB_SLAVE, 10},
    {0x21, 2, LIN_PUB_SLAVE, 10},  {0x30, 3, LIN_EVENT, 10},
};

static int get_data(void *c, uint8_t id, uint8_t *buf)
{
    (void)c;
    if (id == 0x10) { buf[0] = 0x01; buf[1] = 0x80; return 2; }
    return 0;
}

volatile lin_frame_t last[4];                    /* последние ответы: смотреть отладчиком */
static void on_frame(void *c, const lin_frame_t *f)
{
    (void)c;
    last[(f->id == 0x10) ? 0 : (f->id == 0x20) ? 1 : (f->id == 0x21) ? 2 : 3] = *f;
}

int main(void)
{
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
    usart2_lin_init();
    M.hw.send_break = hw_break;
    M.hw.send_bytes = hw_bytes;
    M.tab = TAB;
    M.tab_n = 4;
    M.baud = LIN_BAUD;
    M.lin2 = 1;
    M.get_data = get_data;
    M.on_frame = on_frame;
    lin_master_start(&M, micros());
    for (;;) {
        while (rh != rt) {
            uint16_t v = ring[rh % 16u];
            uint32_t t = ring_t[rh % 16u];
            rh++;
            lin_master_byte(&M, (uint8_t)v, v >> 8, t);
        }
        __disable_irq();                         /* micros() зовут и из прерывания */
        uint32_t t = micros();
        __enable_irq();
        lin_master_poll(&M, t);
    }
}

19л.8 Грабли

Разделитель проскочил. Медленный ведомый с подстройкой по 0x55 на −11 % потерял кадр: отсчёты шли через 1,12 бита, и единица-разделитель в один бит легла между ними. Два бита разделителя, как у G4, закрыли дыру. На F103 разделитель можно удлинить программно: дождаться, пока SBK сбросится, и только потом класть 0x55 (в модели это тот же случай, что два бита у G4; на железе не проверялось).

Столкновение выглядит как ответ. На кадр по событию двое ответили разом, и первый байт 0x20 совпал с PID кадра s1. Не верьте содержимому, пока не сошлась сумма; при столкновении опросите по очереди безусловные кадры всех, кто мог ответить.

Не та сумма для диагностики. Функция Checksum из libopeninv всегда прибавляет PID. На запрос 0x3C она дала 0x99, а драйвер NXP и наш код — 0xD5; ответ 0x3D — 0xEC вместо 0x6A. Для 0x3C и 0x3D — только классическая сумма.

Ведомый молчит на свой ID. Проверьте PID: из 256 байтов верны 64, и ведомый с неверной чётностью не отвечает вовсе. Частая ошибка — положить в заголовок ID без битов чётности.

Молчание по событию — не ошибка. Кадр по событию без ответа — норма: ведущий ждёт 1,4 номинала (для 3 байт — 5396 мкс) и идёт дальше. А вот безусловный кадр без ответа — узел пропал, и это надо показать.

Слот короче кадра. Кадр из 8 байт может тянуться до 9042 мкс. Слот 10 мс его вмещает, слот 5 мс — нет: следующий заголовок наедет на ответ.

Режим LIN и однопроводный USART вместе. HDSEL в режиме LIN должен быть сброшен (RM0008, RM0440) — TX и RX ведите на трансивер раздельно, как libopeninv.

2400 бод на G4. При 170 МГц делитель 70833 не влезает в 16 бит BRR — включайте PRESC.


19л.9 Итоги

Дальше: раздел 19м «rusEFI изнутри: искра и впрыск по углу коленвала — накопление, опережение и очередь событий на одном таймере».

Открыть в интерактивном справочнике