The problem
Commercial EVs such as buses and trucks often use SAE J1939 on CAN, which organises data into parameter groups with priority, source address and transport rules for messages longer than eight bytes. Learning its structure is valuable for EV jobs in heavy vehicles. This project builds the key mechanisms on a battery node using proprietary parameter groups, without claiming compliance with the standard.
Objectives
- Build and parse 29-bit identifiers with priority, PGN and source address.
- Send periodic battery parameter groups in the proprietary range.
- Respond to a request for a specific PGN.
- Transfer a message longer than 8 bytes with BAM transport.
- Decode everything on a PC tool and verify timing.
What you need
From the EV Embedded Development Kit
- STM32F103C8T6 microcontroller unit (Arm Cortex-M3)
- 1S battery with connector and T-sense input
- CAN bus interface with screw terminal
- 16×2 LCD with contrast control
- Analog potentiometer input
Software
- STM32CubeIDE
- Embedded C with STM32 HAL
- Python with python-can
Extra parts
- USB-CAN adapter for the PC — The board has one CAN node. The adapter provides the second node that acknowledges frames, sends requests and decodes the traffic with python-can.
This project uses the kit's lithium cell. Respect polarity, never short the terminals, and don't leave the cell charging unattended.
Key concepts
- 29-bit identifier structure
- J1939 splits the extended ID into a 3-bit priority, data page bits, an 8-bit PDU format (PF), an 8-bit PDU specific (PS) field and an 8-bit source address. PGN is formed from the data page, PF and PS fields.
- PDU1 and PDU2
- If PF is below 240, the message is PDU1 and PS is a destination address. If PF is 240 or above, it is PDU2, broadcast only, and PS is a group extension.
- Proprietary B PGNs
- PGNs 0xFF00 to 0xFFFF are reserved for manufacturer-defined broadcast data, so a project can use them without colliding with standard parameter groups.
- BAM transport
- To broadcast more than 8 bytes, a node sends a connection-management announce (PGN 0xEC00) giving size and packet count, then numbered data-transfer packets (PGN 0xEB00) of 7 data bytes each.
Step-by-step guide
- 1
Study the structure and define your groups
Write out how priority, data page, PF, PS and source address map onto the 29 bits, with examples. Define your parameter groups in the proprietary B range: for example 0xFF10 every 100 ms with cell voltage, temperature and SOC; 0xFF11 every 1 s with limits; and 0xFF12 on request only, a 20-byte block of identity and statistics that needs transport. Choose a source address for the battery node and the PC node.
Checkpoint: A parameter-group table with PGN, priority, period, length and signal layout.
- 2
Set up the bus
Connect the USB-CAN adapter to the kit's CAN screw terminal with twisted pair, check termination (about 60 Ω across CAN_H and CAN_L) and enable the CAN DIP switch. Configure bxCAN at 250 kbit/s, the rate used on many J1939 networks, by doubling the prescaler of your 500 kbit/s setting. Use python-can on the PC to receive and send frames. Check the error counters stay at zero while the adapter acknowledges frames.
Checkpoint: A test frame from the board appears on the PC and a frame from the PC is received by the board.
- 3
Write ID helpers
Write functions to build an ID from priority, PGN, destination and source address and to parse a received ID back into these fields, handling the PDU1/PDU2 rule. Unit-test them on the PC against hand-worked examples before using them on the board. Use IDE = CAN_ID_EXT and ExtId in the transmit header. Include the priority range 0 to 7, destination-specific PDU1 groups and the global address, and check that parsing a built ID returns exactly the fields you put in for every combination you test.
Checkpoint: Unit tests for at least six IDs covering PDU1 and PDU2 pass.
- 4
Send periodic groups
Read calibrated cell voltage, temperature and a simple SOC estimate. Pack them into 0xFF10 and 0xFF11 with documented scaling, and send them at their periods from a timer flag. Use the potentiometer as an extra emulated signal if you want more changing data. Show the last sent values on the LCD. Set unused bytes to 0xFF, which J1939-style practice uses for 'not available', and give each signal a defined 'error' and 'not available' raw value so receivers can tell missing data from real readings.
Checkpoint: The PC tool decodes 0xFF10 at 100 ms and 0xFF11 at 1 s with correct values.
- 5
Handle requests
Configure a filter for the request PGN 0xEA00 addressed to your node or to the global address 0xFF. Decode the 3-byte requested PGN from the data. If it is one of your groups, send it; if not, reply with a negative acknowledgement as your design defines. Send requests from the PC tool to test. Respond within a fixed time limit you set and measure the actual response time from the PC log.
Checkpoint: A request for 0xFF11 from the PC gets an immediate response; a request for an unknown PGN gets the defined negative response.
- 6
Implement BAM transport
When 0xFF12 is requested, it is longer than 8 bytes, so send a connection-management announce frame with control byte for BAM, total size, number of packets and the PGN, then send each data-transfer packet with a sequence number in byte 0 and 7 data bytes, padding the last with 0xFF. Space packets 50 to 200 ms apart as the BAM rules expect, using a timer, not a blocking delay.
Checkpoint: The PC receives the announce and all packets with correct sequence numbers and spacing.
- 7
Build the PC decoder
Extend the Python tool to parse IDs into priority, PGN and source address, decode your proprietary groups by their layout, and reassemble BAM transfers into the complete message, checking packet count and sequence. Print a readable trace and save a CSV. Add a filter to show one PGN or one source address at a time, and a summary view that lists every PGN seen with its count and average period. Make the decoder reject a BAM transfer if a packet is missing or the timing between packets is far outside the expected range.
Checkpoint: The decoder reassembles the 20-byte message and prints all its fields correctly.
- 8
Test timing and robustness
Log 10 minutes of traffic and check periods and jitter for each group. Send several requests close together and check the node handles them in order without corrupting a transfer in progress. Disconnect and reconnect the bus during a BAM transfer and check the node recovers and the PC discards the incomplete message. Record all results in a test table with expected and observed behaviour for each case.
Checkpoint: Measured periods within a few per cent, and correct behaviour under repeated requests and interruptions.
- 9
Document
Write the parameter-group definitions, ID helpers, request and transport logic, test results and a clear note that this is J1939-style for learning, using proprietary PGNs, not a certified implementation. Include the PC tool and a captured trace. Draw the 29-bit ID layout with an annotated example for each of your groups, and add a timing diagram of a BAM transfer. List what a real J1939 node would also need, such as address claiming and standard diagnostic messages, so the scope of the project is clear to examiners.
Checkpoint: A report and repository another team could use to add a new parameter group.
Testing & validation
- ID build and parse unit tests.
- Periodic group timing measured on the PC.
- Correct response to known and unknown requests.
- BAM transfer reassembled correctly with correct packet spacing.
- Recovery after bus interruption during transport.
Expected results
A J1939-style battery node with proprietary parameter groups, request handling and BAM transport, and a PC decoder that reassembles and displays the data.
Take it further
- Add address claiming (PGN 0xEE00) and handle address conflicts.
- Implement connection-mode transport (RTS/CTS) for destination-specific transfers.
- Add a diagnostic message in the style of J1939 active fault codes.
- Add a second kit as another node that consumes the battery data.
Viva and review questions
How is a PGN formed from a 29-bit ID?
From the data page bits, the PDU format byte and, for PDU2 messages, the PDU specific byte. For PDU1 the PS byte is a destination address and is not part of the PGN.
Why use proprietary B PGNs?
They are reserved for manufacturer-defined data, so the project's messages cannot clash with standard parameter groups on a real network.
How does BAM send more than 8 bytes?
An announce frame states size and number of packets, then numbered data packets each carry 7 bytes; receivers reassemble them using the sequence numbers.