Embedded HandbookApps
All sections › III. Peripherals
STM32F103, STM32F407 · level advanced · 35 min read
Open in the interactive handbook

19и. J1939: the CAN of trucks and special machinery — PGN, SPN and DM1 diagnostics

Sections 19г–19з were about a passenger car: a scanner asks, a unit answers, identifiers are 11-bit. Trucks, buses, tractors and special machinery have their own layer on top of CAN — J1939. Here the units themselves broadcast data on a schedule, take addresses by claiming them, and messages longer than eight bytes are cut into pieces by their own transport.

In this section: how the 29-bit identifier is built and what a PGN is, how a node gets an address, how to read parameters (SPN), how long messages travel and what the DM1 fault list looks like. The bench is a small stack in C: it reproduces byte for byte the frames from the can-utils documentation, runs an "engine — instrument" exchange and builds for STM32F103.

Prerequisites: can-utils — github.com/linux-can/can-utils (commit cbbad5ee): the kernel header include/linux/can/j1939.h, the descriptions can-j1939.md and can-j1939-kickstart.md, the programs testj1939.c, j1939acd.c, the library libj1939.c, the examples j1939_vehicle_position and j1939_timedate, calc-bit-timing. License: testj1939.c, j1939acd.c, libj1939.c and can-calc-bit-timing.c — GPL-2.0-only, j1939.h — GPL-2.0-only WITH Linux-syscall-note, the example headers — LGPL-2.0-only. The can-utils code in this section is retold, quotes are a line each. The SAE J1939 standards themselves are paid and were not used here. Everything taken not from can-utils but from memory of the standard is marked with the words "not verified".

Terms used in the documentation

Term Meaning
PGN (parameter group number) the number of a parameter group
SPN (suspect parameter number) the number of a parameter
PDU1 / PDU2 the addressed / broadcast format
PF, PS (PDU format, PDU specific) the format, the specific byte
SA, DA (source / destination address) the sender / receiver address
NAME the 64-bit name of a node
address claim claiming an address
request PGN a request for a group
transport protocol, TP.CM / TP.DT transport: the connection management frame / the data frame
BAM (broadcast announce message) a broadcast to all without acknowledgements
RTS / CTS / EoMA "ready to send" / "ready to receive" / "received everything"
DM1, DTC active faults, a fault code
FMI (failure mode identifier), OC the kind of failure, the occurrence counter

19и.1 How J1939 differs from OBD-II

According to can-j1939.md from can-utils, J1939 is:

The speed in python-obd is written right in the name of ELM327 protocol "A": SAE J1939 (CAN 29/250) — 29 bits, 250 kbit/s. The SAE_J1939 class in protocol_can.py is an ordinary 29-bit CAN protocol, nothing of its own: python-obd cannot parse PGNs and DM1, you will have to do it yourself.

In Linux J1939 is a separate kind of socket: socket(AF_CAN, SOCK_DGRAM, CAN_J1939). The kernel itself cuts and assembles long messages, the program sees only the PGN, the address and the data; an address is claimed by a separate program, j1939acd. In the WSL2 kernel (6.6.87.2-microsoft-standard-WSL2), where the run was done, the module is absent: the built testj1939 fails with socket(j1939): Protocol not supported. So the bench does not rely on sockets — it checks its own stack against the frames written down in can-j1939-kickstart.md.


19и.2 The identifier, PDU1/PDU2 and PGN

The 29 bits are divided like this (can-j1939.md, the PGN layout — j1939.h):

Bits 28…26 25 24 23…16 15…8 7…0
Field priority R (EDP) DP PF PS SA
0x18FECA00 6 0 0 0xFE 0xCA 0x00

The PGN is 18 bits: PS (bits 0–7), PF (8–15), DP (16) and R (17). In the kernel header bit 17 is called R — reserved, in Zephyr's isotp.h from section 19в — EDP.

The meaning of PS depends on PF:

On one DP page this gives 240 addressed groups and 16 × 256 = 4096 broadcast ones. An example from the bench — 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 = адрес получателя */

Top — the 29-bit identifier 0x18FECA00, a DM1 frame from the engine, split into six fields: a 3-bit priority equal to 6, the EDP and DP bits equal to 0, PF 0xFE, PS 0xCA and the sender address 0x00; underlined that the 18-bit PGN is made up of EDP, DP, PF and PS; on the right an explanation: with PF less than 240 it is PDU1 and PS is the receiver address, zeroed in the group number, as in 0xEA00, 0xEC00 and 0xEE00, and with PF from 240 it is PDU2 for all and PS is part of the number, 0xFECA equals 65226, 0xFEF3 equals 65267. Below — a bench of three nodes on a 250 kbit/s bus: the engine with address 0x00, an instrument and a logger with their own NAMEs. The first exchange is the address claim PGN 60928: the instrument claims 0x80, the logger with a smaller NAME claims the same address, the instrument gives way and takes 0x81. The second is a DM1 with three codes, 14 bytes, as a BAM broadcast: the management frame 18ECFF00 20 0E 00 02 FF CA FE 00 and two data frames 50 ms apart, lamps MIL 1 and AWL 1, codes SPN 110, 100 and 190. The third is a VIN request by the instrument with address 0x81: 18EA0081 EC FE 00, the answer by RTS and CTS for 17 bytes in three packets and the acknowledgement 13

Comparison with can-utils. In can-j1939-kickstart.md nine testj1939 commands are written down together with the frame they put on the bus. The bench parses the same addresses with the real function libj1939_parse_canaddr() from libj1939.c, packs the identifier with its own code and gets the same frames — all nine:

Command (abbreviated) PGN Format Frame
-B -s can0:0x80 can0:,0x3ffff 0x3FFFF PDU2 1BFFFF80#0123456789ABCDEF
-B -s can0:0x80 can0:,0x12300 0x12300 PDU1, to all 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, to all 0C02FF80#0123456789ABCDEF

The default priority is 6 (the high byte 0x18), with -p3 — 0x0C. Back the other way: the frame 18234140 is sender 0x40, receiver 0x41, group 0x02300.

Group numbers. A decimal PGN is simply an 18-bit number:

Group PGN Decimal Format Source
acknowledgement (ACK) 0xE800 59392 PDU1 j1939_timedate_cmn.h
request 0xEA00 59904 PDU1 j1939.h
transport, data (TP.DT) 0xEB00 60160 PDU1 the 18EB… frames in kickstart
transport, management (TP.CM) 0xEC00 60416 PDU1 the 18EC… frames in kickstart
address claim 0xEE00 60928 PDU1 j1939.h
commanded address 0xFED8 65240 PDU2 j1939.h
date and time (TD) 0xFEE6 65254 PDU2 j1939_timedate_cmn.h
position (VP1) 0xFEF3 65267 PDU2 j1939_vehicle_position_cmn.h
position (VP2) 0xFBF6 64502 PDU2 the same
engine: speed (EEC1) 0xF004 61444 PDU2 not verified
vehicle speed (CCVS) 0xFEF1 65265 PDU2 not verified
engine: temperatures (ET1) 0xFEEE 65262 PDU2 not verified
fuel economy (LFE) 0xFEF2 65266 PDU2 not verified
DM1, DM2, DM3 0xFECA…0xFECC 65226…65228 PDU2 not verified
vehicle identification (VIN) 0xFEEC 65260 PDU2 not verified

For all rows the decimal number and the format were calculated by the bench. "Not verified" applies only to the name of the group: it comes from memory of J1939-71 and J1939-73.

The identifiers 0x18DA10F1 and 0x18DB33F1 familiar from section 19в fit the same scheme: PF 0xDA and 0xDB are less than 240, so these are the addressed groups 0xDA00 (55808) and 0xDB00 (56064) with the receiver in PS.


19и.3 NAME, address claim and request

A node's address is one byte, while the unique name is the 64-bit NAME. The fields according to j1939.h:

Bits Field
0–20 identity number
21–31 manufacturer code
32–34 ECU instance
35–39 function instance
40–47 function
48 reserved
49–55 vehicle system
56–59 vehicle system instance
60–62 industry group
63 arbitrary address capable

The special addresses are there too: 0xFD is the last ordinary one, 0xFE means "no address" (J1939_IDLE_ADDR), 0xFF means "all".

The claim. A node sends group 0xEE00 to all, with its NAME in the data as eight bytes, least significant first (j1939acd.c copies name into the frame and swaps the bytes only on big-endian machines). If another node has claimed the same address, the smaller NAME wins: in j1939acd.c the one that gives way is the one with s.name > saddr.can_addr.j1939.name. The loser looks for a new address in its range — by default 0x80…0xFD, and on exit it claims 0xFE: "I have no address".

The request. Group 0xEA00, with three bytes of data — the number of the required group, least significant first. At start-up j1939acd asks all for their claims ({ 0, 0xee, 0 }), waits for answers for 1250 ms and only then chooses an address. The number is parsed there too: dat[0] + (dat[1] << 8) + ((dat[2] & 0x03) << 16).

A run. Three nodes: the engine (NAME 0x100000000F6003E9, address 0x00), the instrument (0x80008100390007D2, bit 63 set) and the logger (0x0000810039000BBB):

Time, ms Frame What
0 18EEFF00#E903600F00000010 the engine claimed 0x00
5 18EAFFFE#00EE00 the instrument without an address asked all about claims
6 18EEFF00#E903600F00000010 the engine repeated its claim
1255 18EEFF80#D207003900810080 the instrument claimed 0x80
1300 18EEFF80#BB0B003900810000 the logger claimed the same 0x80
1301 18EEFF81#D207003900810080 the instrument gave way and claimed 0x81

The logger came later but won: its NAME is smaller. Bit 63 is the most significant, so a node that can change its address gives way, other things being equal. The result: engine 0x00, logger 0x80, instrument 0x81.


19и.4 SPN: scale, offset, bytes

A group is eight bytes (or more), and a parameter (SPN) is a field in them with its own step and offset: значение = сырое × шаг + смещение (value = raw × step + offset). Two examples from can-utils with the description right in the headers:

Group Field Bytes Step Offset
VP1, 65267 latitude, SPN 584 1–4 10⁻⁷ °/bit −210°
VP1, 65267 longitude, SPN 585 5–8 10⁻⁷ °/bit −210°
TD, 65254 seconds 1 0.25 s/bit 0
TD, 65254 minutes, hours, month 2, 3, 4 1 0
TD, 65254 day of month 5 0.25 day/bit 0
TD, 65254 year 6 1 year/bit 1985

The bench broadcast 18FEF300#F01D679EC8669793 from the engine: latitude 2657558000 × 10⁻⁷ − 210 = 55.7558°, longitude 2476173000 × 10⁻⁷ − 210 = 37.6173°. The time frame 18FEE600#E022090974290003 reads as year 2026, month 9, day 116 × 0.25 = 29.00, 09:34:56 UTC (0xE0 = 224 × 0.25 = 56 s) and an offset of +3 h. The server j1939-timedate-srv calculates seconds the same way: tm_sec / 0.25, and does not let them go above 239 — 59.75 s.

Engine parameters — the main thing J1939 is read for — are not described in can-utils. From memory of J1939-71 (not verified):

Parameter Group Bytes Step Offset
engine speed, SPN 190 EEC1, 61444 4–5 0.125 rpm per bit 0
coolant temperature, SPN 110 ET1, 65262 1 1 °C per bit −40 °C
wheel-based speed, SPN 84 CCVS, 65265 2–3 1/256 km/h per bit 0
fuel rate, SPN 183 LFE, 65266 1–2 0.05 l/h per bit 0

The bench built by these rules the frames 0CF00400#FFFFFFE02EFFFFFF (0x2EE0 = 12000 × 0.125 = 1500 rpm) and 18FEEE00#82FFFFFFFFFFFFFF (0x82 = 130 − 40 = 90 °C) — this is a check of the arithmetic, not of the standard. Bytes are numbered from one, multi-byte fields are least significant first. Before trusting the table, check it against J1939-71 or the DBC file of your vehicle (section 19е).


19и.5 Transport: BAM and RTS/CTS

A frame holds 8 bytes. Longer ones use two groups: 0xEC00 (management) and 0xEB00 (data). A data frame is a packet number from one and 7 bytes; the tail of the last one is padded with 0xFF. A management frame is a code and information about the message; the last three bytes are the number of the group being transferred, least significant first.

BAM — to all, without acknowledgements. From can-j1939-kickstart.md, 20 bytes of group 0x12300 from 0x80:

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

It also says there: in a broadcast to all the frames go 50 ms apart. The management frame code 0x20 = 32.

RTS/CTS — to one receiver, with flow control. The same 20 bytes from 0x80 to node 0x90:

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

The codes: RTS — 16, CTS — 17, acknowledgement — 19. The receiver sets the pace itself, so, as kickstart says, such a transfer can go much faster than 50 ms per frame. The bench's stack produced both sequences byte for byte — 4 and 6 frames — and assembled the message on reception.

How much can be transferred. The number of packets is one byte, numbered from 1, so the maximum is 255 × 7 = 1785 bytes. can-j1939.md says "max 1780" — that is 5 bytes less than the arithmetic; against the standard this is not verified here. The bench: 8 bytes are not taken by the transport (−1: it is a single frame), 9 bytes — 3 frames (management and 2 packets), 1785 bytes — 256 frames, 1786 — refused. A BAM broadcast of all 1785 bytes with 50 ms between frames lasts 255 × 50 = 12 750 ms.

A VIN request. The instrument with its new address 0x81 asks the engine 0x00 for group 0xFEEC: 18EA0081#ECFE00. The VIN from section 19г, WP0ZZZ99ZTS390000, is 17 bytes, three packets, the answer goes through 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 байт

With the request — 7 frames. The bench also looked at a failure: a BAM without the second packet is not assembled, the loss counter is 1 — it cannot be re-requested, the sender will not learn about the loss.


19и.6 DM1 and a receiver on STM32F103

DM1 — active faults (group 65226, 0xFECA). The layout below is from memory of J1939-73 (not verified):

Byte What
1 lamps, 2 bits each: MIL (7–6), red (5–4), amber (3–2), protect (1–0)
2 lamp flashing
3–6 and further by 4 a code: SPN — 19 bits (8 + 8 + 3 upper in bits 7–5 of the third byte), FMI — 5 bits, CM — 1 bit, OC — 7 bits

CM (conversion method) is a flag of the old SPN layout in early editions of the standard. One code is 6 bytes and fits into a frame; two or more need transport. DM2 (65227) is the previously active codes, DM3 (65228) their clearing on request (not verified).

The run: the engine has MIL and amber lit, three codes. 14 bytes go out as a BAM:

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

The data: 44 FF 6E 00 00 01 64 00 01 03 BE 00 02 7F. The instrument assembled it in 100 ms and parsed it: MIL 1, amber 1; SPN 110 FMI 0 OC 1, SPN 100 FMI 1 OC 3, SPN 190 FMI 2 OC 127. Without codes DM1 is 8 bytes 00 FF 00 00 00 00 FF FF. The edges of the field: SPN 524287 (2¹⁹ − 1), FMI 31, OC 126 are encoded and read back without loss.

A node on STM32F103 (f103.c, the full code is in 19и.7): bxCAN on PA11/PA12, APB1 36 MHz, address claim 0x80, a VIN request, transport assembly and DM1 parsing with the same j1939.c. The 250 kbit/s timing was chosen by can-calc-bit-timing for bxcan:

MCU, APB1 BRP Quantum, ns TS1 TS2 Quanta Sample point CAN_BTR
F103, 36 MHz 9 250 6 + 7 2 16 87.5 % 0x001c0008
F407, 42 MHz 21 500 3 + 3 1 8 87.5 % 0x00050014

In section 19.4, for the same 36 MHz, BRP 18 and 8 quanta were taken — also 250 kbit/s and 87.5 %; the program preferred a smaller quantum.

Filters — three 32-bit banks with a mask on the PF field (EDP and DP must be 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 */
Bank FR1 FR2
0 0x07400004 0x1FC00006
1 0x07800004 0x1FF80006
2 0x07F00004 0x1FF80006

The word is the identifier shifted by 3 plus IDE (bit 2); in the mask also RTR (bit 1), so that only extended data frames pass. The filter does not look at the receiver address: other nodes' addressed frames are filtered out by the program. All 20 frames of the scenario passed the filters; of the seven foreign ones only 18FEF100 passed, while 1BFFFF80 (DP = 1), 18FF0080 and the diagnostic 18DA00F1 from section 19в did not: UDS over 29-bit addresses needs a fourth bank.

Build with arm-none-eabi-gcc 12.2, -Os, -Wall -Wextra -Werror:

What Bytes
F103 firmware, code (text) 1700
F103 firmware, RAM (bss) 1812
of which the transport assembly buffer 1804
j1939.o for Cortex-M3 1542
j1939.o for Cortex-M4 (F407) 1548

Almost all RAM is the 1785-byte buffer: to receive one message of any length.


19и.7 The full code of the stack and the node on F103

The stack is two files of our own, j1939.h and j1939.c, without malloc and without the C library: the same code goes both into the bench on a PC and into the firmware. The node for F103 is f103.c, the start-up code start_f103.c and the linker script f103.ld. All of this is given below in full. The firmware cannot give way on an address — it only sends its own claim; the NAME contest is handled by the bench j1939_host.c, and two claim functions are quoted from it. There is no can-utils code here.

The build is the same command as in the check. CMSIS_F1 is the Device/Include directory of the Keil pack STM32F1xx_DFP 2.4.1 (with stm32f10x.h in it), CMSIS_CORE is CMSIS/Core/Include from 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

The stack header. Group numbers, transport management frame codes, the structures of a frame, a parsed identifier, NAME, the transport sender and receiver, DM1; at the end — the bxCAN filter words from 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

The identifier and PGN (19и.2). For PDU1, when packing, the receiver address goes into PS, and when parsing, PS is removed from the group number:

/* Раздел 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 and the request (19и.3). NAME is 64 bits, it goes onto the bus least significant byte first; the number of the requested group — three bytes, in the same way:

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);
}

Transport: the sender (19и.5). BAM prepares all frames at once; the caller then outputs them with a step of 50 ms. RTS/CTS is a state machine: on each CTS it sends as many data frames as were asked for, from the specified number; EoMA or an abort closes the transfer:

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;
}

Transport: the receiver. One buffer of 1785 bytes. To RTS it answers with CTS, at the end of a window with a new CTS, after the last packet with EoMA; a packet out of order resets the assembly and increments the loss counter:

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). Without codes — eight bytes: lamps, flashing, four zeros and two 0xFF. Parsing stops at an "empty" code:

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;
}

Address claim — from the bench j1939_host.c, the logic follows j1939acd.c. The smaller NAME wins, the loser takes the next free address from its range, and if there are no free ones — 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;
}

The node on F103: clocks, filters, bxCAN. 72 MHz from an 8 MHz crystal, APB1 — 36 MHz; CAN_BTR is the can-calc-bit-timing line from 19и.6, the filters are three banks by 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;
}

Sending and receiving. One mailbox for sending, FIFO 0 for receiving; 11-bit frames are discarded:

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 не нужны */
}

The main loop. Address claim 0x80, a VIN request to the engine, then polling the FIFO: transport frames go to assembly, the rest — to us or to all — straight into 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);
        }
    }
}

The start-up code — as in section 4в, no interrupts:

/* Раздел 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,
};

The linker script f103.ld — BluePill, 64 KB of Flash memory and 20 KB of RAM:

/* Раздел 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 Pitfalls

The J1939 socket does not open. socket(j1939): Protocol not supported — the kernel has no can-j1939 module. WSL2 does not have it; on ordinary Linux — modprobe can-j1939 (as in kickstart). Without it what remains is raw CAN and your own parsing, as in the bench.

An addressed group does not match the identifier. In PDU1 PS holds the address: the frame 18EA0080 is group 0xEA00, not 0xEA00 + 0x80. You must compare the number after zeroing PS, otherwise requests to different addressees will look like different groups.

Two claims for one address at the same moment. The identifiers are identical (18EEFF80), arbitration will not separate them — the data will differ. That is why after the claim a node listens for an answer and gives way if its NAME is larger.

BAM loses silently. There are no acknowledgements: a packet is lost — the message is not assembled, the sender does not know. The loss counter on reception is the only trace.

The numbers in the help and in the code differ. j1939-vehicle-position-srv in its help calls VP1 and VP2 groups 65265 and 65266, while the header j1939_vehicle_position_cmn.h says 0x0FEF3 (65267) and 0x0FBF6 (64502); the length limit in can-j1939.md is 1780 while the arithmetic gives 1785; the help of j1939acd promises an address range "0-253", while the default in the code is 0x80-0xfd. Trust the code and the calculation, not the text next to it.

An SPN table "from memory". The steps and offsets of the engine parameters in 19и.4 are not verified. An error in the step gives plausible but wrong numbers — take J1939-71 or the DBC of your vehicle.

The filter lets the wrong things through. The mask from section 19г for 11 bits does not fit here: in a 29-bit frame the identifier in the register is shifted by 3, not by 21, and without the IDE bit in the mask standard frames will also pass.


19и.9 Summary


Next: section 19к "K-line: diagnostics of older cars via ISO 9141 and KWP2000 over UART" — before CAN, units talked over a single wire, and scanners can still do it.

Open in the interactive handbook