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 — это:
- только 29-битные кадры данных, без удалённых запросов (RTR);
- адреса поверх CAN: у каждого кадра есть отправитель, у части — получатель;
- сообщения любой длины: от 9 байт включается транспортный протокол, и та же группа идёт уже под другими идентификаторами;
- адреса можно получать на ходу по заявке (J1939-81), без ведущего узла.
Скорость в 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:
- PF < 240 (0xF0) — PDU1, адресный формат. PS — адрес получателя, 0xFF — «всем». В номере группы PS обнуляется: запрос — 0xEA00, чей бы адрес ни стоял в PS.
- PF ≥ 240 — PDU2, только всем. PS — часть номера: 0xFECA и 0xFECB — разные группы.
На одной странице 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 = адрес получателя */
Сверка с 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 Итоги
- J1939 — 29-битные кадры данных без RTR на 250 кбит/с; PGN — 18 бит: R, DP, PF, PS; PF < 240 — адресный формат (PS = получатель), от 240 — широковещательный.
- Стек стенда повторил байт в байт 9 кадров команд
testj1939, BAM из 4 кадров и RTS/CTS из 6 кадров изcan-j1939-kickstart.md. - Группы: запрос 59904, TP.DT 60160, TP.CM 60416, заявка адреса 60928, VP1 65267, TD 65254; на странице — 240 адресных и 4096 широковещательных.
- Заявка адреса: NAME 64 бита младшим вперёд, меньший NAME побеждает;
j1939acdждёт 1250 мс после запроса, диапазон 0x80…0xFD. - Транспорт: до 255 пакетов по 7 байт — 1785 байт; BAM с шагом 50 мс, 1785 байт — 12 750 мс; VIN 17 байт по RTS/CTS — 7 кадров с запросом.
- DM1 с тремя кодами — 14 байт, BAM из 3 кадров за 100 мс; раскладка DM1 и шаги параметров двигателя не сверены.
- F103: 250 кбит/с при 36 МГц — BRP 9, 16 квантов,
CAN_BTR0x001c0008; три фильтра по PF; прошивка 1700 байт кода и 1812 байт ОЗУ.
Дальше: раздел 19к «K-line: диагностика старых машин по ISO 9141 и KWP2000 на UART» — до CAN блоки говорили по одному проводу, и сканеры до сих пор это умеют.