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.
19л.2 Кадр: разрыв, 0x55 и защищённый идентификатор
Кадр начинает всегда ведущий. Заголовок — три части:
- Разрыв — не меньше 13 битов нуля подряд и за ним единица-разделитель. Обычный байт 0x00 даёт только 9 нулей (старт и 8 бит), поэтому разрыв с данными не спутать. На 19200 13 битов — 677,08 мкс; в модели с шагом 100 нс ноль на шине продержался 677,00 мкс.
- Синхро — байт 0x55, в двоичном виде нули и единицы по очереди. По его спадам ведомый без кварца меряет длину бита (19л.5).
- 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 (перенос возвращается в младший бит), в конце всё инвертируют. Две разновидности:
- классическая — только байты данных;
- расширенная — данные и PID.
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 перечисляет четыре: безусловный, по событию, спорадический и диагностический.
- Безусловный — у ID ровно один хозяин, ответ приходит всегда.
- По событию — на один заголовок могут ответить несколько ведомых, но только те, у кого что-то изменилось; первым байтом ответа ведомый кладёт PID своего безусловного кадра, чтобы ведущий понял, чей он. Никто не ответил — это не ошибка; ответили двое — столкновение.
- Спорадический — слот, который ведущий занимает кадром по своему выбору, когда ему есть что сказать.
- Диагностический — пара 0x3C (запрос ведущего, 8 байт) и 0x3D (ответ ведомого, 8 байт), классическая сумма. Первый байт — NAD, адрес узла, за ним байт PCI, как у одиночного кадра ISO-TP (19в.2), и служба UDS (19д).
Смысл кадров по событию и спорадических, раскладка 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 Итоги
- LIN — один провод, монтажное И, один ведущий по расписанию; бит на 19200 — 52,08 мкс, на 9600 — 104,17, на 2400 — 416,67.
- Заголовок: разрыв 13 бит (677,08 мкс) и разделитель, 0x55, PID — всего 34 бита, 1770,83 мкс.
- PID = ID + P0 (ID0^ID1^ID2^ID4) + P1 (¬(ID1^ID3^ID4^ID5)): 64 верных байта из 256, 0x3C → 0x3C, 0x3D → 0x7D; таблица совпала с libopeninv и NXP 64 из 64.
- Сумма — с переносом и инверсией; расширенная включает PID, для 0x3C и
0x3D — только классическая:
01 80в кадре 0x10 дают 0x7E и 0x2E. - Кадр не длиннее 1,4 номинала: 4667 мкс для 2 байт, 9042 мкс для 8 — слот 10 мс вмещает любой.
- Подстройка по 0x55 расширила допуск ведомого с −4…+5 % до −19…+20 % (с разделителем в 2 бита).
- STM32:
LINEN,LBDL,LBD/LBDF,SBK/SBKRQ; BRR 1875 на 36 МГц и 8854 на 170 МГц; ядро — 1228 байт кода, ведущий — 624 байта на F103.
Дальше: раздел 19м «rusEFI изнутри: искра и впрыск по углу коленвала — накопление, опережение и очередь событий на одном таймере».