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

19и. J1939: CAN грузовиков и спецтехники — PGN, SPN и диагностика DM1

Разделы 19г–19з говорили о легковой машине: сканер спрашивает, блок отвечает, идентификаторы 11-битные. У грузовиков, автобусов, тракторов и спецтехники своя надстройка над CAN — J1939. Здесь блоки сами рассылают данные по расписанию, адреса занимают по заявке, а сообщения длиннее восьми байт режут на куски своим транспортом.

В разделе: как устроен 29-битный идентификатор и что такое PGN, как узел получает адрес, как читать параметры (SPN), как ходят длинные сообщения и как выглядит список неисправностей DM1. Стенд — маленький стек на C: он повторяет байт в байт кадры из документации can-utils, прогоняет обмен «двигатель — прибор» и собирается под STM32F103.

Опоры: can-utils — github.com/linux-can/can-utils (коммит cbbad5ee): заголовок ядра include/linux/can/j1939.h, описания can-j1939.md и can-j1939-kickstart.md, программы testj1939.c, j1939acd.c, библиотека libj1939.c, примеры j1939_vehicle_position и j1939_timedate, calc-bit-timing. Лицензия: testj1939.c, j1939acd.c, libj1939.c и can-calc-bit-timing.c — GPL-2.0-only, j1939.h — GPL-2.0-only WITH Linux-syscall-note, заголовки примеров — LGPL-2.0-only. Код can-utils в разделе пересказан, цитаты — по строке. Сами стандарты SAE J1939 платные и здесь не использованы. Всё, что взято не из can-utils, а по памяти о стандарте, помечено словами «не сверено».

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

Оригинал Перевод
PGN (parameter group number) номер группы параметров
SPN (suspect parameter number) номер параметра
PDU1 / PDU2 адресный / широковещательный формат
PF, PS (PDU format, PDU specific) формат, уточнение
SA, DA (source / destination address) адрес отправителя / получателя
NAME 64-битное имя узла
address claim заявка адреса
request PGN запрос группы
transport protocol, TP.CM / TP.DT транспорт: кадр управления / кадр данных
BAM (broadcast announce message) рассылка всем без подтверждений
RTS / CTS / EoMA «готов передать» / «готов принять» / «принял всё»
DM1, DTC активные неисправности, код неисправности
FMI (failure mode identifier), OC вид отказа, счётчик появлений

19и.1 Чем J1939 отличается от OBD-II

По can-j1939.md из can-utils J1939 — это:

Скорость в python-obd записана прямо в имени протокола ELM327 «A»: SAE J1939 (CAN 29/250) — 29 бит, 250 кбит/с. Класс SAE_J1939 в protocol_can.py — обычный 29-битный CAN-протокол, ничего своего: разбирать PGN и DM1 python-obd не умеет, это придётся делать самим.

В Linux J1939 — отдельный вид сокета: socket(AF_CAN, SOCK_DGRAM, CAN_J1939). Ядро само режет и собирает длинные сообщения, программа видит только PGN, адрес и данные; адрес по заявке занимает отдельная программа j1939acd. В ядре WSL2 (6.6.87.2-microsoft-standard-WSL2), где шёл прогон, модуля нет: собранный testj1939 падает с socket(j1939): Protocol not supported. Поэтому стенд не опирается на сокеты — он проверяет свой стек по кадрам, записанным в can-j1939-kickstart.md.


19и.2 Идентификатор, PDU1/PDU2 и PGN

29 бит делятся так (can-j1939.md, раскладка PGN — j1939.h):

Биты 28…26 25 24 23…16 15…8 7…0
Поле приоритет R (EDP) DP PF PS SA
0x18FECA00 6 0 0 0xFE 0xCA 0x00

PGN — 18 бит: PS (биты 0–7), PF (8–15), DP (16) и R (17). В заголовке ядра бит 17 назван R — reserved, в isotp.h Zephyr из раздела 19в — EDP.

Смысл PS зависит от PF:

На одной странице DP выходит 240 адресных групп и 16 × 256 = 4096 широковещательных. Пример из стенда — j1939_id_pack():

uint32_t id = ((uint32_t)(prio & 7u) << 26) | ((pgn & 0x3FFFFu) << 8) | sa;
if (j1939_pgn_is_pdu1(pgn))
    id = (id & ~0xFF00u) | ((uint32_t)da << 8);   /* PS = адрес получателя */

Вверху — 29-битный идентификатор 0x18FECA00, кадр DM1 от двигателя, разложенный на шесть полей: приоритет 3 бита равен 6, биты EDP и DP равны 0, PF 0xFE, PS 0xCA и адрес отправителя 0x00; подчёркнуто, что PGN из 18 бит складывается из EDP, DP, PF и PS; справа пояснение: при PF меньше 240 это PDU1 и PS — адрес получателя, в номере группы он обнуляется, как у 0xEA00, 0xEC00 и 0xEE00, а при PF от 240 это PDU2 для всех и PS входит в номер, 0xFECA равно 65226, 0xFEF3 равно 65267. Ниже — стенд из трёх узлов на шине 250 кбит/с: двигатель с адресом 0x00, прибор и регистратор со своими NAME. Первый обмен — заявка адреса PGN 60928: прибор заявляет 0x80, регистратор с меньшим NAME заявляет тот же адрес, прибор уступает и берёт 0x81. Второй — DM1 с тремя кодами, 14 байт, рассылкой BAM: кадр управления 18ECFF00 20 0E 00 02 FF CA FE 00 и два кадра данных через 50 мс, лампы MIL 1 и AWL 1, коды SPN 110, 100 и 190. Третий — запрос VIN прибором с адресом 0x81: 18EA0081 EC FE 00, ответ по RTS и CTS на 17 байт тремя пакетами и подтверждение 13

Сверка с can-utils. В can-j1939-kickstart.md девять команд testj1939 записаны вместе с кадром, который они выдают на шину. Стенд разбирает те же адреса настоящей функцией libj1939_parse_canaddr() из libj1939.c, упаковывает идентификатор своим кодом и получает те же кадры — все девять:

Команда (сокращённо) PGN Формат Кадр
-B -s can0:0x80 can0:,0x3ffff 0x3FFFF PDU2 1BFFFF80#0123456789ABCDEF
-B -s can0:0x80 can0:,0x12300 0x12300 PDU1, всем 1923FF80#0123456789ABCDEF
-s can0:0x80 can0:0x40,0x12300 0x12300 PDU1 → 0x40 19234080#0123456789ABCDEF
-s can0:0x80,0x12300 :0x40,0x32100 0x32100 PDU1 → 0x40 1B214080#0123456789ABCDEF
-B -s -p3 can0:0x80 :,0x0200 0x00200 PDU1, всем 0C02FF80#0123456789ABCDEF

Приоритет по умолчанию — 6 (старший байт 0x18), с -p3 — 0x0C. Обратно: кадр 18234140 — это отправитель 0x40, получатель 0x41, группа 0x02300.

Номера групп. Десятичный PGN — просто 18-битное число:

Группа PGN Десятично Формат Откуда
подтверждение (ACK) 0xE800 59392 PDU1 j1939_timedate_cmn.h
запрос (request) 0xEA00 59904 PDU1 j1939.h
транспорт, данные (TP.DT) 0xEB00 60160 PDU1 кадры 18EB… в kickstart
транспорт, управление (TP.CM) 0xEC00 60416 PDU1 кадры 18EC… в kickstart
заявка адреса 0xEE00 60928 PDU1 j1939.h
назначение адреса 0xFED8 65240 PDU2 j1939.h
дата и время (TD) 0xFEE6 65254 PDU2 j1939_timedate_cmn.h
координаты (VP1) 0xFEF3 65267 PDU2 j1939_vehicle_position_cmn.h
координаты (VP2) 0xFBF6 64502 PDU2 там же
двигатель: обороты (EEC1) 0xF004 61444 PDU2 не сверено
скорость машины (CCVS) 0xFEF1 65265 PDU2 не сверено
двигатель: температуры (ET1) 0xFEEE 65262 PDU2 не сверено
расход топлива (LFE) 0xFEF2 65266 PDU2 не сверено
DM1, DM2, DM3 0xFECA…0xFECC 65226…65228 PDU2 не сверено
номер машины (VIN) 0xFEEC 65260 PDU2 не сверено

Для всех строк десятичное число и формат посчитал стенд. «Не сверено» — только название группы: оно по памяти о J1939-71 и J1939-73.

Знакомые по разделу 19в идентификаторы 0x18DA10F1 и 0x18DB33F1 укладываются в ту же схему: PF 0xDA и 0xDB меньше 240, значит это адресные группы 0xDA00 (55808) и 0xDB00 (56064) с получателем в PS.


19и.3 NAME, заявка адреса и запрос

Адрес узла — один байт, а уникальное имя — 64-битный NAME. Поля по j1939.h:

Биты Поле
0–20 номер экземпляра (identity number)
21–31 код изготовителя
32–34 экземпляр блока
35–39 экземпляр функции
40–47 функция
48 резерв
49–55 система машины
56–59 экземпляр системы
60–62 отрасль (industry group)
63 умеет сменить адрес (arbitrary address capable)

Особые адреса там же: 0xFD — последний обычный, 0xFE — «адреса нет» (J1939_IDLE_ADDR), 0xFF — «всем».

Заявка. Узел шлёт группу 0xEE00 всем, в данных — свой NAME восемью байтами, младшим вперёд (j1939acd.c копирует name в кадр и переставляет байты только на машинах со старшим байтом вперёд). Если на тот же адрес заявился другой, побеждает меньший NAME: в j1939acd.c уступает тот, у кого s.name > saddr.can_addr.j1939.name. Проигравший ищет новый адрес в своём диапазоне — по умолчанию 0x80…0xFD, а при выходе заявляет 0xFE: «у меня адреса нет».

Запрос. Группа 0xEA00, в данных три байта — номер нужной группы младшим вперёд. j1939acd на старте спрашивает у всех заявки ({ 0, 0xee, 0 }), ждёт ответов 1250 мс и только потом выбирает адрес. Разбор номера там же: dat[0] + (dat[1] << 8) + ((dat[2] & 0x03) << 16).

Прогон. Три узла: двигатель (NAME 0x100000000F6003E9, адрес 0x00), прибор (0x80008100390007D2, бит 63 взведён) и регистратор (0x0000810039000BBB):

Время, мс Кадр Что
0 18EEFF00#E903600F00000010 двигатель заявил 0x00
5 18EAFFFE#00EE00 прибор без адреса спросил всех о заявках
6 18EEFF00#E903600F00000010 двигатель повторил заявку
1255 18EEFF80#D207003900810080 прибор заявил 0x80
1300 18EEFF80#BB0B003900810000 регистратор заявил тот же 0x80
1301 18EEFF81#D207003900810080 прибор уступил и заявил 0x81

Регистратор пришёл позже, но выиграл: его NAME меньше. Бит 63 — старший, поэтому узел, который умеет сменить адрес, при прочих равных уступает. Итог: двигатель 0x00, регистратор 0x80, прибор 0x81.


19и.4 SPN: масштаб, смещение, байты

Группа — это восемь байт (или больше), а параметр (SPN) — поле в них со своим шагом и смещением: значение = сырое × шаг + смещение. Два примера из can-utils с описанием прямо в заголовках:

Группа Поле Байты Шаг Смещение
VP1, 65267 широта, SPN 584 1–4 10⁻⁷ °/бит −210°
VP1, 65267 долгота, SPN 585 5–8 10⁻⁷ °/бит −210°
TD, 65254 секунды 1 0,25 с/бит 0
TD, 65254 минуты, часы, месяц 2, 3, 4 1 0
TD, 65254 день месяца 5 0,25 сут/бит 0
TD, 65254 год 6 1 год/бит 1985

Стенд разослал от двигателя 18FEF300#F01D679EC8669793: широта 2657558000 × 10⁻⁷ − 210 = 55,7558°, долгота 2476173000 × 10⁻⁷ − 210 = 37,6173°. Кадр времени 18FEE600#E022090974290003 читается как 2026 год, 9-й месяц, день 116 × 0,25 = 29,00, 09:34:56 UTC (0xE0 = 224 × 0,25 = 56 с) и смещение +3 ч. Сервер j1939-timedate-srv считает секунды так же: tm_sec / 0.25, и не пускает их выше 239 — 59,75 с.

Параметры двигателя — главное, ради чего J1939 читают, — в can-utils не описаны. По памяти о J1939-71 (не сверено):

Параметр Группа Байты Шаг Смещение
обороты, SPN 190 EEC1, 61444 4–5 0,125 об/мин на бит 0
температура ОЖ, SPN 110 ET1, 65262 1 1 °C на бит −40 °C
скорость по колёсам, SPN 84 CCVS, 65265 2–3 1/256 км/ч на бит 0
расход топлива, SPN 183 LFE, 65266 1–2 0,05 л/ч на бит 0

Стенд собрал по этим правилам кадры 0CF00400#FFFFFFE02EFFFFFF (0x2EE0 = 12000 × 0,125 = 1500 об/мин) и 18FEEE00#82FFFFFFFFFFFFFF (0x82 = 130 − 40 = 90 °C) — это проверка арифметики, не стандарта. Байты нумеруются с единицы, многобайтовые поля — младшим вперёд. Прежде чем верить таблице, сверьте её с J1939-71 или с DBC-файлом своей машины (раздел 19е).


19и.5 Транспорт: BAM и RTS/CTS

В кадр влезает 8 байт. Длиннее — две группы: 0xEC00 (управление) и 0xEB00 (данные). Кадр данных — номер пакета с единицы и 7 байт, хвост последнего добит 0xFF. Кадр управления — код и сведения о сообщении, последние три байта — номер передаваемой группы младшим вперёд.

BAM — всем, без подтверждений. Из can-j1939-kickstart.md, 20 байт группы 0x12300 от 0x80:

18ECFF80#20140003FF002301     20 — BAM, 0x0014 = 20 байт, 3 пакета, группа 0x012300
18EBFF80#010123456789ABCD     пакет 1
18EBFF80#02EF0123456789AB     пакет 2
18EBFF80#03CDEF01234567FF     пакет 3, хвост 0xFF

Там же сказано: при рассылке всем кадры идут через 50 мс. Кадр управления 0x20 = 32.

RTS/CTS — одному получателю, с управлением потоком. Те же 20 байт от 0x80 узлу 0x90:

18EC9080#1014000303002301     10 — RTS: 20 байт, 3 пакета, не больше 3 за раз
18EC8090#110301FFFF002301     11 — CTS: шли 3 пакета, начиная с 1-го
18EB9080#01… 02… 03…          три пакета данных
18EC8090#13140003FF002301     13 — всё принято: 20 байт, 3 пакета

Коды: RTS — 16, CTS — 17, подтверждение — 19. Получатель задаёт темп сам, поэтому, как сказано в kickstart, такая передача может идти гораздо быстрее 50 мс на кадр. Стек стенда выдал обе последовательности байт в байт — 4 и 6 кадров — и собрал сообщение на приёме.

Сколько можно передать. Число пакетов — один байт, нумерация с 1, значит максимум 255 × 7 = 1785 байт. В can-j1939.md написано «max 1780» — это на 5 байт меньше арифметики; по стандарту это здесь не сверено. Стенд: 8 байт транспорт не берёт (−1: это один кадр), 9 байт — 3 кадра (управление и 2 пакета), 1785 байт — 256 кадров, 1786 — отказ. Рассылка BAM на все 1785 байт при 50 мс между кадрами длится 255 × 50 = 12 750 мс.

Запрос VIN. Прибор со своим новым адресом 0x81 просит у двигателя 0x00 группу 0xFEEC: 18EA0081#ECFE00. VIN из раздела 19г, WP0ZZZ99ZTS390000, — 17 байт, три пакета, ответ идёт через RTS/CTS:

18EC8100#10110003FFECFE00     RTS: 0x11 = 17 байт, 3 пакета, за раз — сколько угодно (0xFF)
18EC0081#110301FFFFECFE00     CTS: 3 пакета с 1-го
18EB8100#015750305A5A5A39     "WP0ZZZ9"
18EB8100#02395A5453333930     "9ZTS390"
18EB8100#03303030FFFFFFFF     "000" и хвост
18EC0081#13110003FFECFE00     принято 17 байт

С запросом — 7 кадров. На сбой стенд тоже посмотрел: BAM без второго пакета не собирается, счётчик потерь 1 — переспросить нельзя, отправитель о потере не узнает.


19и.6 DM1 и приёмник на STM32F103

DM1 — активные неисправности (группа 65226, 0xFECA). Раскладка ниже — по памяти о J1939-73 (не сверено):

Байт Что
1 лампы, по 2 бита: MIL (7–6), красная (5–4), жёлтая (3–2), защитная (1–0)
2 мигание ламп
3–6 и далее по 4 код: SPN — 19 бит (8 + 8 + 3 старших в битах 7–5 третьего байта), FMI — 5 бит, CM — 1 бит, OC — 7 бит

CM (conversion method) — признак старой раскладки SPN в ранних редакциях стандарта. Один код — 6 байт, влезает в кадр; два и больше — транспорт. DM2 (65227) — ранее активные коды, DM3 (65228) — их стирание по запросу (не сверено).

Прогон: двигатель горит MIL и жёлтой, три кода. 14 байт уходят BAM:

18ECFF00#200E0002FFCAFE00     BAM: 14 байт, 2 пакета, группа 0xFECA
18EBFF00#0144FF6E00000164     через 50 мс
18EBFF00#02000103BE00027F     ещё через 50 мс

Данные: 44 FF 6E 00 00 01 64 00 01 03 BE 00 02 7F. Прибор собрал их за 100 мс и разобрал: MIL 1, жёлтая 1; SPN 110 FMI 0 OC 1, SPN 100 FMI 1 OC 3, SPN 190 FMI 2 OC 127. Без кодов DM1 — 8 байт 00 FF 00 00 00 00 FF FF. Края поля: SPN 524287 (2¹⁹ − 1), FMI 31, OC 126 кодируются и читаются обратно без потерь.

Узел на STM32F103 (f103.c, полный код — в 19и.7): bxCAN на PA11/PA12, APB1 36 МГц, заявка адреса 0x80, запрос VIN, сборка транспорта и разбор DM1 тем же j1939.c. Тайминг 250 кбит/с подобрала can-calc-bit-timing для bxcan:

МК, APB1 BRP Квант, нс TS1 TS2 Квантов Точка выборки CAN_BTR
F103, 36 МГц 9 250 6 + 7 2 16 87,5 % 0x001c0008
F407, 42 МГц 21 500 3 + 3 1 8 87,5 % 0x00050014

В разделе 19.4 для тех же 36 МГц взяты BRP 18 и 8 квантов — тоже 250 кбит/с и 87,5 %; программа предпочла квант мельче.

Фильтры — три 32-битных банка по маске на поле PF (EDP и DP обязаны быть 0):

filt(0, J_PF_ID(0xE8), J_PF_MASK(0xF8));   /* PF E8…EF: ACK, запрос, TP.DT, TP.CM, заявка адреса */
filt(1, J_PF_ID(0xF0), J_PF_MASK(0xFF));   /* PF F0: 0xF000…0xF0FF */
filt(2, J_PF_ID(0xFE), J_PF_MASK(0xFF));   /* PF FE: 0xFE00…0xFEFF */
Банк FR1 FR2
0 0x07400004 0x1FC00006
1 0x07800004 0x1FF80006
2 0x07F00004 0x1FF80006

Слово — идентификатор, сдвинутый на 3, плюс IDE (бит 2); в маске ещё и RTR (бит 1), чтобы проходили только расширенные кадры данных. Адрес получателя фильтр не смотрит: чужие адресные кадры отсеивает программа. Все 20 кадров сценария фильтры пропустили; из семи посторонних прошёл один 18FEF100, а 1BFFFF80 (DP = 1), 18FF0080 и диагностический 18DA00F1 из раздела 19в — нет: для UDS по 29-битным адресам нужен четвёртый банк.

Сборка arm-none-eabi-gcc 12.2, -Os, -Wall -Wextra -Werror:

Что Байт
прошивка F103, код (text) 1700
прошивка F103, ОЗУ (bss) 1812
из них буфер сборки транспорта 1804
j1939.o для Cortex-M3 1542
j1939.o для Cortex-M4 (F407) 1548

Почти всё ОЗУ — буфер на 1785 байт: принимать одно сообщение любой длины.


19и.7 Полный код стека и узла на F103

Стек — два своих файла, j1939.h и j1939.c, без malloc и без библиотеки C: один и тот же код идёт и в стенд на ПК, и в прошивку. Узел для F103 — f103.c, стартовый код start_f103.c и скрипт компоновщика f103.ld. Всё это ниже целиком. Уступать адрес прошивка не умеет — она только шлёт свою заявку; спор по NAME разбирает стенд j1939_host.c, и из него приведены две функции заявки. Кода can-utils здесь нет.

Сборка — та же команда, что в проверке. CMSIS_F1 — каталог Device/Include пака Keil STM32F1xx_DFP 2.4.1 (в нём stm32f10x.h), CMSIS_CORE — CMSIS/Core/Include из CMSIS 5.9.0 (github.com/ARM-software/CMSIS_5):

F="-mcpu=cortex-m3 -mthumb -Os -Wall -Wextra -Werror -ffreestanding -ffunction-sections -fdata-sections -DSTM32F10X_MD -I$CMSIS_F1 -I$CMSIS_CORE"
arm-none-eabi-gcc $F start_f103.c f103.c j1939.c -T f103.ld -nostdlib -Wl,--gc-sections -o f103.elf
arm-none-eabi-size f103.elf

Заголовок стека. Номера групп, коды кадров управления транспорта, структуры кадра, разобранного идентификатора, NAME, отправителя и приёмника транспорта, DM1; в конце — слова фильтра bxCAN из 19и.6:

/* Раздел 19и: маленький стек J1939 — идентификатор, PGN, транспорт BAM и RTS/CTS, DM1.
 * Один и тот же код собирается gcc на ПК (стенд j1939_host.c) и arm-none-eabi-gcc для STM32F103 (f103.c). */
#ifndef J1939_H
#define J1939_H
#include <stdint.h>

#define J_PGN_ACK        0xE800u  /* 59392, подтверждение */
#define J_PGN_REQUEST    0xEA00u  /* 59904, запрос PGN */
#define J_PGN_TP_DT      0xEB00u  /* 60160, кадр данных транспорта */
#define J_PGN_TP_CM      0xEC00u  /* 60416, кадр управления транспорта */
#define J_PGN_ADDR_CLAIM 0xEE00u  /* 60928, заявка адреса */
#define J_ADDR_NULL      0xFEu    /* адреса нет (J1939_IDLE_ADDR) */
#define J_ADDR_GLOBAL    0xFFu    /* всем (J1939_NO_ADDR) */
#define J_TP_MAX_LEN     (255u * 7u)

#define TP_RTS   16u
#define TP_CTS   17u
#define TP_EOMA  19u
#define TP_BAM   32u
#define TP_ABORT 255u

typedef struct {
    uint32_t id;      /* 29-битный идентификатор */
    uint8_t len;
    uint8_t d[8];
} j1939_frame_t;

typedef struct {
    uint8_t prio, edp, dp, pf, ps, sa;
    uint8_t da;       /* PDU1: = PS; PDU2: 0xFF */
    uint32_t pgn;     /* PDU1: PS обнулён */
} j1939_id_t;

int j1939_pgn_is_pdu1(uint32_t pgn);
uint32_t j1939_id_pack(uint8_t prio, uint32_t pgn, uint8_t da, uint8_t sa);
void j1939_id_unpack(uint32_t id, j1939_id_t *out);

/* NAME, 64 бита (раскладка — linux/can/j1939.h) */
typedef struct {
    uint32_t identity;      /* биты 0–20 */
    uint16_t manufacturer;  /* 21–31 */
    uint8_t ecu_inst;       /* 32–34 */
    uint8_t func_inst;      /* 35–39 */
    uint8_t function;       /* 40–47 */
    uint8_t vsys;           /* 49–55 */
    uint8_t vsys_inst;      /* 56–59 */
    uint8_t industry;       /* 60–62 */
    uint8_t aac;            /* 63 */
} j1939_name_t;
uint64_t j1939_name_pack(const j1939_name_t *n);
void j1939_u64_le(uint64_t v, uint8_t *out);
uint64_t j1939_le_u64(const uint8_t *in);

/* запрос: три байта PGN младшим вперёд */
void j1939_request_build(uint32_t pgn, uint8_t da, uint8_t sa, j1939_frame_t *f);
uint32_t j1939_request_pgn(const j1939_frame_t *f);

/* транспорт */
int j1939_tp_bam_build(uint32_t pgn, const uint8_t *data, uint16_t len, uint8_t prio, uint8_t sa,
                       j1939_frame_t *out, int max);

typedef struct {
    const uint8_t *data;
    uint16_t len;
    uint8_t packets, next, sa, da, prio, max_per_cts, done;
    uint32_t pgn;
} j1939_tp_tx_t;
void j1939_tp_rts_start(j1939_tp_tx_t *s, uint32_t pgn, const uint8_t *data, uint16_t len,
                        uint8_t prio, uint8_t sa, uint8_t da, uint8_t max_per_cts, j1939_frame_t *rts);
/* кадр на входе отправителя (CTS / EoMA / отказ); out — сколько DT выдать; возвращает их число */
int j1939_tp_tx_on_frame(j1939_tp_tx_t *s, const j1939_frame_t *in, j1939_frame_t *out, int max);

typedef struct {
    uint8_t buf[J_TP_MAX_LEN];
    uint16_t len;
    uint8_t packets, got, mode, own, peer, window;
    uint32_t pgn;
    uint8_t done;
    uint16_t lost;   /* кадров с неверным номером */
} j1939_tp_rx_t;
/* приём кадра TP.CM/TP.DT; own — наш адрес; reply — ответ (CTS/EoMA), если нужен.
 * Возвращает: 0 — ничего, 1 — есть reply, 2 — сообщение собрано (и, возможно, reply). */
int j1939_tp_rx_frame(j1939_tp_rx_t *r, uint8_t own, const j1939_frame_t *in, j1939_frame_t *reply, int *has_reply);

/* DM1 */
typedef struct {
    uint32_t spn;   /* 19 бит */
    uint8_t fmi;    /* 5 бит */
    uint8_t cm;     /* 1 бит */
    uint8_t oc;     /* 7 бит */
} j1939_dtc_t;
typedef struct {
    uint8_t mil, rsl, awl, pl;   /* по 2 бита */
    uint8_t flash;
    uint8_t n;
    j1939_dtc_t dtc[16];
} j1939_dm_t;
int j1939_dm1_encode(const j1939_dm_t *m, uint8_t *buf);
int j1939_dm1_decode(const uint8_t *buf, int len, j1939_dm_t *m);

/* bxCAN, 32-битный фильтр по маске: слово FR1 (идентификатор) и FR2 (маска) для 29-битного кадра.
 * Бит 2 — IDE (расширенный), бит 1 — RTR; маска требует IDE = 1 и RTR = 0. */
#define J_BX_FR1(id29)   ((((uint32_t)(id29)) << 3) | 4u)
#define J_BX_FR2(mask29) ((((uint32_t)(mask29)) << 3) | 6u)
/* фильтр по диапазону PF: всё, у чего EDP = DP = 0 и PF & pf_mask == pf */
#define J_PF_ID(pf)        ((uint32_t)(pf) << 16)
#define J_PF_MASK(pf_mask) ((3u << 24) | ((uint32_t)(pf_mask) << 16))

#endif

Идентификатор и PGN (19и.2). У PDU1 при сборке в PS встаёт адрес получателя, при разборе PS из номера группы убирается:

/* Раздел 19и: маленький стек J1939 (см. j1939.h). Без malloc и без libc. */
#include "j1939.h"

int j1939_pgn_is_pdu1(uint32_t pgn)
{
    return ((pgn >> 8) & 0xFFu) < 240u;               /* PF < 0xF0 — адресный формат */
}

uint32_t j1939_id_pack(uint8_t prio, uint32_t pgn, uint8_t da, uint8_t sa)
{
    uint32_t id = ((uint32_t)(prio & 7u) << 26) | ((pgn & 0x3FFFFu) << 8) | sa;
    if (j1939_pgn_is_pdu1(pgn))
        id = (id & ~0xFF00u) | ((uint32_t)da << 8);   /* PS = адрес получателя */
    return id;
}

void j1939_id_unpack(uint32_t id, j1939_id_t *o)
{
    o->prio = (id >> 26) & 7u;
    o->edp = (id >> 25) & 1u;
    o->dp = (id >> 24) & 1u;
    o->pf = (id >> 16) & 0xFFu;
    o->ps = (id >> 8) & 0xFFu;
    o->sa = id & 0xFFu;
    o->pgn = (id >> 8) & 0x3FFFFu;
    if (o->pf < 240u) {
        o->da = o->ps;
        o->pgn &= 0x3FF00u;
    } else {
        o->da = J_ADDR_GLOBAL;
    }
}

NAME и запрос (19и.3). NAME — 64 бита, на шину идёт младшим байтом вперёд; номер запрошенной группы — три байта так же:

uint64_t j1939_name_pack(const j1939_name_t *n)
{
    return ((uint64_t)(n->identity & 0x1FFFFFu))
         | ((uint64_t)(n->manufacturer & 0x7FFu) << 21)
         | ((uint64_t)(n->ecu_inst & 7u) << 32)
         | ((uint64_t)(n->func_inst & 0x1Fu) << 35)
         | ((uint64_t)n->function << 40)
         | ((uint64_t)(n->vsys & 0x7Fu) << 49)
         | ((uint64_t)(n->vsys_inst & 0xFu) << 56)
         | ((uint64_t)(n->industry & 7u) << 60)
         | ((uint64_t)(n->aac & 1u) << 63);
}

void j1939_u64_le(uint64_t v, uint8_t *out)
{
    for (int i = 0; i < 8; i++)
        out[i] = (uint8_t)(v >> (8 * i));
}

uint64_t j1939_le_u64(const uint8_t *in)
{
    uint64_t v = 0;
    for (int i = 7; i >= 0; i--)
        v = (v << 8) | in[i];
    return v;
}

void j1939_request_build(uint32_t pgn, uint8_t da, uint8_t sa, j1939_frame_t *f)
{
    f->id = j1939_id_pack(6, J_PGN_REQUEST, da, sa);
    f->len = 3;
    f->d[0] = (uint8_t)pgn;
    f->d[1] = (uint8_t)(pgn >> 8);
    f->d[2] = (uint8_t)(pgn >> 16);
}

uint32_t j1939_request_pgn(const j1939_frame_t *f)
{
    return f->d[0] | ((uint32_t)f->d[1] << 8) | ((uint32_t)(f->d[2] & 0x03u) << 16);
}

Транспорт: отправитель (19и.5). BAM готовит все кадры сразу, с шагом 50 мс их выдаёт уже вызывающий. RTS/CTS — конечный автомат: на каждый CTS отдаёт столько кадров данных, сколько попросили, с указанного номера; EoMA или отказ закрывают передачу:

static void cm_fill(j1939_frame_t *f, uint8_t ctrl, uint8_t b1, uint8_t b2, uint8_t b3, uint8_t b4, uint32_t pgn)
{
    f->len = 8;
    f->d[0] = ctrl;
    f->d[1] = b1;
    f->d[2] = b2;
    f->d[3] = b3;
    f->d[4] = b4;
    f->d[5] = (uint8_t)pgn;
    f->d[6] = (uint8_t)(pgn >> 8);
    f->d[7] = (uint8_t)(pgn >> 16);
}

static void dt_fill(j1939_frame_t *f, uint8_t seq, const uint8_t *data, uint16_t len)
{
    uint16_t off = (uint16_t)((seq - 1u) * 7u);
    f->len = 8;
    f->d[0] = seq;
    for (int i = 0; i < 7; i++)
        f->d[1 + i] = (off + i < len) ? data[off + i] : 0xFFu;   /* хвост — 0xFF */
}

int j1939_tp_bam_build(uint32_t pgn, const uint8_t *data, uint16_t len, uint8_t prio, uint8_t sa,
                       j1939_frame_t *out, int max)
{
    if (len < 9u || len > J_TP_MAX_LEN)
        return -1;
    int packets = (len + 6) / 7;
    if (packets + 1 > max)
        return -2;
    out[0].id = j1939_id_pack(prio, J_PGN_TP_CM, J_ADDR_GLOBAL, sa);
    cm_fill(&out[0], TP_BAM, (uint8_t)len, (uint8_t)(len >> 8), (uint8_t)packets, 0xFF, pgn);
    for (int k = 1; k <= packets; k++) {
        out[k].id = j1939_id_pack(prio, J_PGN_TP_DT, J_ADDR_GLOBAL, sa);
        dt_fill(&out[k], (uint8_t)k, data, len);
    }
    return packets + 1;
}

void j1939_tp_rts_start(j1939_tp_tx_t *s, uint32_t pgn, const uint8_t *data, uint16_t len,
                        uint8_t prio, uint8_t sa, uint8_t da, uint8_t max_per_cts, j1939_frame_t *rts)
{
    s->data = data;
    s->len = len;
    s->packets = (uint8_t)((len + 6u) / 7u);
    s->next = 1;
    s->sa = sa;
    s->da = da;
    s->prio = prio;
    s->pgn = pgn;
    s->max_per_cts = max_per_cts;
    s->done = 0;
    rts->id = j1939_id_pack(prio, J_PGN_TP_CM, da, sa);
    cm_fill(rts, TP_RTS, (uint8_t)len, (uint8_t)(len >> 8), s->packets, max_per_cts, pgn);
}

int j1939_tp_tx_on_frame(j1939_tp_tx_t *s, const j1939_frame_t *in, j1939_frame_t *out, int max)
{
    j1939_id_t id;
    j1939_id_unpack(in->id, &id);
    if (id.pgn != J_PGN_TP_CM || id.da != s->sa || id.sa != s->da)
        return 0;
    if (in->d[0] == TP_EOMA || in->d[0] == TP_ABORT) {
        s->done = in->d[0] == TP_EOMA ? 1 : 2;
        return 0;
    }
    if (in->d[0] != TP_CTS)
        return 0;
    int n = in->d[1];
    s->next = in->d[2];
    int k = 0;
    for (; k < n && k < max && s->next <= s->packets; k++, s->next++) {
        out[k].id = j1939_id_pack(s->prio, J_PGN_TP_DT, s->da, s->sa);
        dt_fill(&out[k], s->next, s->data, s->len);
    }
    return k;
}

Транспорт: приёмник. Один буфер на 1785 байт. На RTS отвечает CTS, по концу окна — новым CTS, после последнего пакета — EoMA; пакет не по порядку сбрасывает сборку и увеличивает счётчик потерь:

int j1939_tp_rx_frame(j1939_tp_rx_t *r, uint8_t own, const j1939_frame_t *in, j1939_frame_t *reply, int *has_reply)
{
    j1939_id_t id;
    j1939_id_unpack(in->id, &id);
    *has_reply = 0;
    if (id.da != own && id.da != J_ADDR_GLOBAL)
        return 0;
    if (id.pgn == J_PGN_TP_CM) {
        uint8_t c = in->d[0];
        if (c != TP_BAM && c != TP_RTS)
            return 0;
        r->mode = c;
        r->len = (uint16_t)(in->d[1] | (in->d[2] << 8));
        r->packets = in->d[3];
        r->pgn = in->d[5] | ((uint32_t)in->d[6] << 8) | ((uint32_t)(in->d[7] & 3u) << 16);
        r->got = 0;
        r->done = 0;
        r->own = own;
        r->peer = id.sa;
        if (r->len > J_TP_MAX_LEN || r->packets != (r->len + 6u) / 7u) {
            r->mode = 0;
            return 0;
        }
        if (c == TP_RTS) {   /* ответить CTS: сколько пакетов и с какого номера */
            r->window = in->d[4] < r->packets ? in->d[4] : r->packets;
            reply->id = j1939_id_pack(id.prio, J_PGN_TP_CM, id.sa, own);
            cm_fill(reply, TP_CTS, r->window, 1, 0xFF, 0xFF, r->pgn);
            *has_reply = 1;
            return 1;
        }
        return 0;
    }
    if (id.pgn == J_PGN_TP_DT && r->mode && id.sa == r->peer) {
        uint8_t seq = in->d[0];
        if (seq != r->got + 1u) {
            r->lost++;
            r->mode = 0;
            return 0;
        }
        for (int i = 0; i < 7; i++) {
            uint16_t off = (uint16_t)((seq - 1u) * 7u + i);
            if (off < r->len)
                r->buf[off] = in->d[1 + i];
        }
        r->got = seq;
        if (r->got == r->packets) {
            r->done = 1;
            if (r->mode == TP_RTS) {
                reply->id = j1939_id_pack(id.prio, J_PGN_TP_CM, id.sa, own);
                cm_fill(reply, TP_EOMA, (uint8_t)r->len, (uint8_t)(r->len >> 8), r->packets, 0xFF, r->pgn);
                *has_reply = 1;
            }
            r->mode = 0;
            return 2;
        }
        if (r->mode == TP_RTS && r->got % r->window == 0u) {   /* окно кончилось — новый CTS */
            uint8_t left = (uint8_t)(r->packets - r->got);
            uint8_t n = left < r->window ? left : r->window;
            reply->id = j1939_id_pack(id.prio, J_PGN_TP_CM, id.sa, own);
            cm_fill(reply, TP_CTS, n, (uint8_t)(r->got + 1u), 0xFF, 0xFF, r->pgn);
            *has_reply = 1;
            return 1;
        }
    }
    return 0;
}

DM1 (19и.6). Без кодов — восемь байт: лампы, мигание, четыре нуля и два 0xFF. Разбор останавливается на «пустом» коде:

int j1939_dm1_encode(const j1939_dm_t *m, uint8_t *b)
{
    b[0] = (uint8_t)(((m->mil & 3u) << 6) | ((m->rsl & 3u) << 4) | ((m->awl & 3u) << 2) | (m->pl & 3u));
    b[1] = m->flash;
    if (m->n == 0) {   /* кодов нет: нули и 0xFF до восьми байт */
        b[2] = b[3] = b[4] = b[5] = 0;
        b[6] = b[7] = 0xFF;
        return 8;
    }
    int k = 2;
    for (int i = 0; i < m->n; i++) {
        const j1939_dtc_t *d = &m->dtc[i];
        b[k++] = (uint8_t)d->spn;
        b[k++] = (uint8_t)(d->spn >> 8);
        b[k++] = (uint8_t)((((d->spn >> 16) & 7u) << 5) | (d->fmi & 0x1Fu));
        b[k++] = (uint8_t)(((d->cm & 1u) << 7) | (d->oc & 0x7Fu));
    }
    if (k < 8) {
        while (k < 8)
            b[k++] = 0xFF;
    }
    return k;
}

int j1939_dm1_decode(const uint8_t *b, int len, j1939_dm_t *m)
{
    if (len < 6)
        return -1;
    m->mil = (b[0] >> 6) & 3u;
    m->rsl = (b[0] >> 4) & 3u;
    m->awl = (b[0] >> 2) & 3u;
    m->pl = b[0] & 3u;
    m->flash = b[1];
    m->n = 0;
    for (int k = 2; k + 4 <= len && m->n < 16; k += 4) {
        uint32_t spn = b[k] | ((uint32_t)b[k + 1] << 8) | ((uint32_t)(b[k + 2] >> 5) << 16);
        if (spn == 0 && (b[k + 2] & 0x1Fu) == 0)
            break;   /* «кодов нет» */
        j1939_dtc_t *d = &m->dtc[m->n++];
        d->spn = spn;
        d->fmi = b[k + 2] & 0x1Fu;
        d->cm = b[k + 3] >> 7;
        d->oc = b[k + 3] & 0x7Fu;
    }
    return m->n;
}

Заявка адреса — из стенда j1939_host.c, логика по j1939acd.c. Меньший NAME побеждает, проигравший берёт следующий свободный адрес из своего диапазона, а если свободных нет — 0xFE:

/* ── узел, заявляющий адрес (логика — по j1939acd.c) ───────────────────────── */
typedef struct { const char *name_s; uint64_t name; uint8_t sa; uint8_t lo, hi; } node_t;
static void claim_frame(const node_t *n, j1939_frame_t *f)
{
    f->id = j1939_id_pack(6, J_PGN_ADDR_CLAIM, J_ADDR_GLOBAL, n->sa);
    f->len = 8;
    j1939_u64_le(n->name, f->d);
}
/* чужая заявка на наш адрес: меньший NAME побеждает, проигравший берёт следующий свободный */
static int on_claim(node_t *n, const j1939_frame_t *f, uint8_t *taken)
{
    j1939_id_t u;
    j1939_id_unpack(f->id, &u);
    uint64_t other = j1939_le_u64(f->d);
    taken[u.sa] = 1;
    if (u.sa != n->sa || other == n->name)
        return 0;
    if (n->name < other)
        return 1;                           /* мы сильнее — повторить свою заявку */
    for (unsigned a = n->lo; a <= n->hi; a++)
        if (!taken[a]) { n->sa = (uint8_t)a; return 2; }
    n->sa = J_ADDR_NULL;                /* адресов нет — «не могу занять» */
    return 3;
}

Узел на F103: часы, фильтры, bxCAN. 72 МГц от кварца 8 МГц, APB1 — 36 МГц; CAN_BTR — строка can-calc-bit-timing из 19и.6, фильтры — три банка по PF:

/* Раздел 19и: узел J1939 на STM32F103 (bxCAN, PA11/PA12, 250 кбит/с при APB1 = 36 МГц).
 * Заявляет адрес 0x80, пропускает фильтрами три диапазона PGN, собирает TP и разбирает DM1.
 * Проверяется только сборкой (CAN в QEMU нет). */
#include "stm32f10x.h"
#include "j1939.h"

#define OWN_SA 0x80u
static const uint64_t OWN_NAME = 0x80008100390007D2ull;   /* NAME прибора из стенда */

static j1939_tp_rx_t tp;
volatile uint8_t dm1_count, dm1_mil, vin_len;
volatile uint32_t rx_frames;

static void clock_72mhz(void)
{
    RCC->CR |= RCC_CR_HSEON;
    while (!(RCC->CR & RCC_CR_HSERDY)) {
    }
    FLASH->ACR = FLASH_ACR_PRFTBE | FLASH_ACR_LATENCY_2;
    RCC->CFGR = RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9 | RCC_CFGR_PPRE1_DIV2;   /* 8 × 9 = 72, APB1 = 36 МГц */
    RCC->CR |= RCC_CR_PLLON;
    while (!(RCC->CR & RCC_CR_PLLRDY)) {
    }
    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL) {
    }
}

static void filt(uint32_t bank, uint32_t id29, uint32_t mask29)
{
    CAN1->FA1R &= ~(1u << bank);
    CAN1->FS1R |= (1u << bank);            /* 32 бита */
    CAN1->FM1R &= ~(1u << bank);           /* по маске */
    CAN1->sFilterRegister[bank].FR1 = J_BX_FR1(id29);
    CAN1->sFilterRegister[bank].FR2 = J_BX_FR2(mask29);
    CAN1->FFA1R &= ~(1u << bank);          /* в очередь 0 */
    CAN1->FA1R |= (1u << bank);
}

static int can_init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_AFIOEN;
    RCC->APB1ENR |= RCC_APB1ENR_CAN1EN;
    GPIOA->CRH = (GPIOA->CRH & ~(0xFFu << 12)) | (0x8u << 12) | (0xBu << 16);   /* PA11 вход с подтяжкой, PA12 AF */
    GPIOA->ODR |= (1u << 11);

    CAN1->MCR &= ~CAN_MCR_SLEEP;
    CAN1->MCR |= CAN_MCR_INRQ;
    for (uint32_t t = 100000u; !(CAN1->MSR & CAN_MSR_INAK); t--)
        if (!t)
            return 1;
    /* can-calc-bit-timing, bxcan 36 МГц: BRP 9, TS1 = 6 + 7, TS2 = 2, SJW 1 -> CAN_BTR 0x001c0008 */
    CAN1->BTR = (9u - 1u) | ((13u - 1u) << 16) | ((2u - 1u) << 20) | ((1u - 1u) << 24);
    CAN1->MCR |= CAN_MCR_ABOM;             /* сам возвращается на шину после bus-off */
    CAN1->MCR &= ~CAN_MCR_INRQ;
    for (uint32_t t = 100000u; CAN1->MSR & CAN_MSR_INAK; t--)
        if (!t)
            return 2;

    CAN1->FMR |= CAN_FMR_FINIT;
    filt(0, J_PF_ID(0xE8), J_PF_MASK(0xF8));   /* PF E8…EF: ACK, запрос, TP.DT, TP.CM, заявка адреса */
    filt(1, J_PF_ID(0xF0), J_PF_MASK(0xFF));   /* PF F0: 0xF000…0xF0FF */
    filt(2, J_PF_ID(0xFE), J_PF_MASK(0xFF));   /* PF FE: 0xFE00…0xFEFF */
    CAN1->FMR &= ~CAN_FMR_FINIT;
    return 0;
}

Отправка и приём. Один почтовый ящик на отправку, очередь 0 на приём; 11-битные кадры отбрасываются:

static void can_send(const j1939_frame_t *f)
{
    while (!(CAN1->TSR & CAN_TSR_TME0)) {
    }
    CAN1->sTxMailBox[0].TDTR = f->len;
    CAN1->sTxMailBox[0].TDLR = f->d[0] | (f->d[1] << 8) | (f->d[2] << 16) | ((uint32_t)f->d[3] << 24);
    CAN1->sTxMailBox[0].TDHR = f->d[4] | (f->d[5] << 8) | (f->d[6] << 16) | ((uint32_t)f->d[7] << 24);
    CAN1->sTxMailBox[0].TIR = (f->id << 3) | CAN_TI0R_IDE | CAN_TI0R_TXRQ;
}

static int can_recv(j1939_frame_t *f)
{
    if (!(CAN1->RF0R & CAN_RF0R_FMP0))
        return 0;
    uint32_t rir = CAN1->sFIFOMailBox[0].RIR, lo = CAN1->sFIFOMailBox[0].RDLR, hi = CAN1->sFIFOMailBox[0].RDHR;
    f->id = rir >> 3;
    f->len = CAN1->sFIFOMailBox[0].RDTR & 0xFu;
    for (int i = 0; i < 4; i++) {
        f->d[i] = (uint8_t)(lo >> (8 * i));
        f->d[4 + i] = (uint8_t)(hi >> (8 * i));
    }
    CAN1->RF0R |= CAN_RF0R_RFOM0;
    return (rir & CAN_RI0R_IDE) ? 1 : 0;    /* 11-битные кадры J1939 не нужны */
}

Главный цикл. Заявка адреса 0x80, запрос VIN у двигателя, дальше — опрос очереди: кадры транспорта идут в сборку, остальные — нам или всем — сразу в on_message():

static void on_message(uint32_t pgn, const uint8_t *d, uint16_t len)
{
    j1939_dm_t dm;
    if (pgn == 0xFECAu && j1939_dm1_decode(d, len, &dm) >= 0) {
        dm1_count = dm.n;
        dm1_mil = dm.mil;
    } else if (pgn == 0xFEECu) {
        vin_len = (uint8_t)len;
    }
}

int main(void)
{
    j1939_frame_t f, rep;
    int hr;
    clock_72mhz();
    if (can_init())
        for (;;) {
        }
    f.id = j1939_id_pack(6, J_PGN_ADDR_CLAIM, J_ADDR_GLOBAL, OWN_SA);   /* заявка адреса */
    f.len = 8;
    j1939_u64_le(OWN_NAME, f.d);
    can_send(&f);
    j1939_request_build(0xFEEC, 0x00, OWN_SA, &f);                       /* запрос VIN у двигателя */
    can_send(&f);
    for (;;) {
        if (!can_recv(&f))
            continue;
        rx_frames++;
        j1939_id_t u;
        j1939_id_unpack(f.id, &u);
        if (u.pgn == J_PGN_TP_CM || u.pgn == J_PGN_TP_DT) {
            if (j1939_tp_rx_frame(&tp, OWN_SA, &f, &rep, &hr) == 2)
                on_message(tp.pgn, tp.buf, tp.len);
            if (hr)
                can_send(&rep);
        } else if (u.da == OWN_SA || u.da == J_ADDR_GLOBAL) {
            on_message(u.pgn, f.d, f.len);
        }
    }
}

Стартовый код — как в разделе 4в, прерываний нет:

/* Раздел 19и: стартовый код BluePill (как в разделе 4в).
 * Прерывания не используются: узел J1939 опрашивает очередь приёма в цикле. */
#include <stdint.h>

extern uint32_t _estack, _sidata, _sdata, _edata, _sbss, _ebss;
int main(void);

void Reset_Handler(void)
{
    uint32_t *src = &_sidata, *dst = &_sdata;
    while (dst < &_edata)
        *dst++ = *src++;
    for (dst = &_sbss; dst < &_ebss;)
        *dst++ = 0;
    main();
    for (;;) {
    }
}

void Default_Handler(void)
{
    for (;;) {
    }
}

typedef void (*vector_t)(void);

__attribute__((section(".isr_vector"), used))
const vector_t vectors[16] = {
    [0] = (vector_t)&_estack, [1] = Reset_Handler, [2] = Default_Handler, [3] = Default_Handler,
    [4] = Default_Handler, [5] = Default_Handler, [6] = Default_Handler, [15] = Default_Handler,
};

Скрипт компоновщика f103.ld — BluePill, 64 КБ флеш-памяти и 20 КБ ОЗУ:

/* Раздел 19и: BluePill, 64 КБ флеш-памяти, 20 КБ ОЗУ.
 * Каждая строка отвечает на один вопрос: где лежит, где работает, откуда берутся начальные значения. */
ENTRY(Reset_Handler)                     /* с чего начинается программа — для отладчика и ELF */

MEMORY
{
  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 64K
  RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}

_estack     = ORIGIN(RAM) + LENGTH(RAM); /* вершина стека: стек растёт вниз от конца ОЗУ */
_stack_size = 0x800;                     /* сколько ОЗУ оставить под стек — проверяется в ASSERT */

SECTIONS
{
  .isr_vector :
  {
    KEEP(*(.isr_vector))                 /* таблица векторов — первой, с адреса 0x08000000 */
  } > FLASH

  .text :
  {
    *(.text .text.*)                     /* код */
    *(.rodata .rodata.*)                 /* константы и строки */
    . = ALIGN(4);
    _etext = .;
  } > FLASH

  .data :
  {
    _sdata = .;                          /* где .data работает — адрес в ОЗУ (VMA) */
    *(.data .data.*)
    . = ALIGN(4);
    _edata = .;
  } > RAM AT > FLASH                     /* а хранится во флеш-памяти сразу за кодом (LMA) */
  _sidata = LOADADDR(.data);             /* откуда стартовый код копирует начальные значения */

  .bss (NOLOAD) :
  {
    _sbss = .;                           /* нули: во флеш-памяти не хранятся вовсе */
    *(.bss .bss.*)
    *(COMMON)
    . = ALIGN(4);
    _ebss = .;
  } > RAM

  ASSERT(_ebss + _stack_size <= _estack, "RAM too small for .data + .bss + stack")
}

19и.8 Грабли

Сокет J1939 не открывается. socket(j1939): Protocol not supported — в ядре нет модуля can-j1939. На WSL2 его нет, на обычном Linux — modprobe can-j1939 (так в kickstart). Без него остаётся сырой CAN и свой разбор, как в стенде.

Адресная группа не совпадает с идентификатором. У PDU1 в PS сидит адрес: кадр 18EA0080 — это группа 0xEA00, а не 0xEA00 + 0x80. Сравнивать надо номер после обнуления PS, иначе запросы разным адресатам будут выглядеть разными группами.

Две заявки на один адрес в один момент. Идентификаторы одинаковые (18EEFF80), арбитраж их не разведёт — разойдутся уже данные. Поэтому после заявки узел слушает ответ и уступает, если его NAME больше.

BAM теряет молча. Подтверждений нет: пропал пакет — сообщение не собрано, отправитель не знает. Счётчик потерь на приёме — единственный след.

Номера в справке и в коде расходятся. j1939-vehicle-position-srv в справке называет VP1 и VP2 группами 65265 и 65266, а в заголовке j1939_vehicle_position_cmn.h — 0x0FEF3 (65267) и 0x0FBF6 (64502); предел длины в can-j1939.md — 1780 при арифметике 1785; справка j1939acd обещает диапазон адресов «0-253», а в коде по умолчанию стоит 0x80-0xfd. Верить надо коду и расчёту, а не тексту рядом.

Таблица SPN «из головы». Шаги и смещения параметров двигателя в 19и.4 не сверены. Ошибка в шаге даёт правдоподобные, но неверные числа — берите J1939-71 или DBC своей машины.

Фильтр пропускает не то. Маска из раздела 19г для 11 бит здесь не годится: у 29-битного кадра идентификатор в регистре сдвинут на 3, а не на 21, и без бита IDE в маске пройдут и стандартные кадры.


19и.9 Итоги


Дальше: раздел 19к «K-line: диагностика старых машин по ISO 9141 и KWP2000 на UART» — до CAN блоки говорили по одному проводу, и сканеры до сих пор это умеют.

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