The CANBus Bible

this was ported automatically from our wiki and it did not turn out well

Everything anyone will ever need to know about the CAN communication interface, especially on STM32 devices.

CAN Hardware

CAN (Controller Area Network) is a wired network interface for connecting the various components of the car (ECU, sensors, driver display, and telemetry) to each other. Devices communicate with each other by broadcasting messages onto the shared bus and listening for messages from other devices. Since all of the devices communicate on the same bus at the same time, there is a fairly complex bus arbitration system that allows devices to have different priorities to maximize reliability when the bus utilization is near its full capacity.

Protocol

At a hardware level, the CAN bus uses a differential pair of signals, labeled CANH (CAN High) and CANL (CAN Low), to transfer data.

By EE JRW - Own work, CC BY-SA 4.0, https://commons.wikimedia.org/w/index.php?curid=55237229

In the idle (recessive) state, both lines are weakly pulled toward the midpoint between the supply and ground. This state represents a “1” bit on the bus. To transmit a “0” bit, a device has to pull the CANH signal high and the CANL signal low, resulting in a “dominant” state on the bus. Note that the bus is effectively a “wired-AND” configuration, where any device can pull the bus to 0, and the “1” state is only present when none of the devices are transmitting a 0. This allows devices to arbitrate bus access when attempting to send data at the same time.

Electrical Configuration

The CAN bus is a linear bus, terminated by 120 Ω resistors at each end. These termination resistors keep the differential pair signals at the same voltage in the recessive state and prevent the signal from being reflected when it reaches the end of the bus. Each device connects to the bus using a CAN transceiver, a dedicated chip that adapts the raw bus signals to a simple TX/RX signal pair that can be safely connected to a microcontroller. This transceiver does not perform any decoding or translation of the data on the bus; the microcontroller needs a CAN controller to perform the link-layer communication. Since the CAN protocol is fairly complex, the CAN controller is typically implemented in hardware, either as a peripheral inside the microcontroller or as a dedicated IC. It is possible to implement the CAN controller in software, but these implementations will require a signifiant amount of CPU time, especially if the bus is congested.

This is our “standard” CAN bus interface, consisting of an isolated transceiver, common-mode choke, TVS protection diodes, and an optional termination resistor.

This reference schematic can be found on the Git server: https://git.supermileage.ece.illinois.edu/Supermileage/KICAD_CANInterface.

CAN Implementation (Basics)

Note: this document is old! CAN Implementation (Advanced) covers everything here in more detail, but this is still a good reference for the basics if you’re not working on the CAN implementation.

Introduction

CAN Bus is a communication protocol that is used to connect multiple devices to a single bus. It uses a differential pair to transmit data in electrically noisy environment. It handles different communication priority and is asynchronous. In this article we will introduce the CAN bus both in software and hardware, going over the theory of operation, and discuss why CAN is the bus of choice for automotive applications.

Hardware Setup

The hardware of CAN bus requires two resistors for a bus line, and one transceiver for each device. They will be connected as shown. This setup is necessary as it is the way CAN bus enforces priority. When sending messages over the bus, each CAN Device sends its priority code in binary. The lower the number, the higher priority it is. As a Node communicates its priority, if the device pulls the line high with a one, but the voltage does not go up, it indicates that a lower number priority on the bus exists, and the node halts communication until the other device completes transmission. The hardware standard specifies 120 ohms, but other resistors may be used depending on transceiver specifications.

CAN Protocol Basics

From a programming perspective, the CAN protocol is fairly simple. Each device on the bus has its own unique 11-bit “identifier” value to identify itself. Devices can send data packets (or “frames”) to each other, with each packet containing up to 8 bytes of data. Any device can send a packet at any time, and all of the other devices (including the sending device) will see this packet. Since a typical CAN bus might contain dozens of devices with various purposes, it can quickly become difficult for a given device to run an interrupt for every single message sent on the bus. Fortunately, the data packets include the sending device’s identifier so that other devices can choose which packets to listen to or ignore.

There is some additional complexity in the actual data being sent on the bus. Most importantly, the device identifier determines its priority: the device with address 0 has highest priority and address 0x7FF (or 2047, the highest possible identifier) has the lowest priority. Packets sent with higher-priority identifiers will “block” any other lower-priority messages, forcing them to wait until the next available frame. This usually doesn’t affect much, but it can allow critical messages to be sent even if the bus is almost saturated.

There are many possible implementations of how we can use this identifier value to transmit packets more effectively. More details about our implementation are in the CAN Standardization document.

Software Setup

To begin the implementation of CAN Bus, the STM32 IOC file is used to generate the boilerplate code necessary to use the CAN Bus to its fullest.

Firstly we must setup the CanBus in the ioc file. You must enable it in the connectivity tab in the configuration ioc file. Then you must enable interrupts by clicking yes on the settings in the NVIC tab of the canbus configuration tabs. Then you must set the following magic numbers:

Nominal Prescaler: 1 Nominal Sync Jump: 16 Nominal Time Segment1: 63 Nominal Time Seg2: 24 (If not working try 16) Data Prescaler: 1 Data Sync Jump: 4 Data Time Seg1: 5 Data Time Seg2: 4 Std Filters Nbr 1 (or more if needed) Ext Filters Nbr 0

These numbers are not necessarily correct for your device this along with not initializing code, should be the very first things you question if your code breaks.

These numbers represent:

(Insert formulas here)

UPDATE: Magic numbers decoded!!

These numbers are simply the timings for the bit, i.e. they define when the bit is sampled, and you want this number to be close to 87.5, i.e. the industry standard.

To get this number, https://electronics.stackexchange.com/questions/478864/atsamc21-can-configuration-nominal-bit-timing-vs-data-bit-timing-time-qua plz read, more to come on the explanation and testing.

After you are done singing the magic incantations of the numbers, we can now proceed to boring boiler plate code. Aidan has written a library (available on git) to abstract some of this code away, however, it may be better to understand what all the lines do before using the library.

Starting the CAN Bus

It is highly important to start the CAN controller so that it is ready to send and receive data:

HAL_FDCAN_Start(&hfdcan1);

Important: Any configuration (for setting up read filters etc.) must be done before this function is called!

Sending Bytes

If you want to send bytes, this will first involved setting the TxHeader. This will include the identifier, the data size, the protocol used, etc. An example TxHeader is shown below:

FDCAN_TxHeaderTypeDef TxHeader;

TxHeader.Identifier = 0x321; TxHeader.IdType = FDCAN_STANDARD_ID; TxHeader.TxFrameType = FDCAN_DATA_FRAME; TxHeader.DataLength = FDCAN_DLC_BYTES_8; TxHeader.ErrorStateIndicator = FDCAN_ESI_ACTIVE; TxHeader.BitRateSwitch = FDCAN_BRS_OFF; TxHeader.FDFormat = FDCAN_CLASSIC_CAN; TxHeader.TxEventFifoControl = FDCAN_NO_TX_EVENTS;

We can then start the FDCAN bus, note that starting the Fdcan can come before the txheader, as long as it comes after the init function.

The message is then sent into the FIFO buffer through the function call HAL_FDCAN_AddMessageToTxFifoQ(&hfdcan1, &TxHeader, TxData0). This puts the message into a FIFO queue which at first notice the STM32 sends off into the universe, assuming that there is not a higher priority transmission being sent.

When using the CANLib library, use CANLib_SendMsg(&hfdcan1, datalength, data) instead to write data to the bus.

Receiving Bytes

Did you remember to set the interrupts? You better have… Otherwise you will suffer (Please set the interrupts to enable in the NVIC) Also double check the magic numbers, are the magic numbers correct? Are they different from ours? Please check what sample rate you want, and worst comes to worst, start guessing numbers. Those numbers were not as important for the sending, but are absolutely critical for receiving.

There are some additional setup functions that must be called to add filters and enable the interrupt notification. This must be done BEFORE calling Start()!

Setting the global filter

HAL_FDCAN_ConfigGlobalFilter(&hfdcan1, , , , );

The four parameters configure where non-matching and remote packets should be sorted, or if they should be ignored. Since we do not use remote frames or extended addressing and it is very unlikely that the system needs to store non-matching packets, these settings should usually be set to the following:

HAL_FDCAN_ConfigGlobalFilter(&hfdcan1, FDCAN_REJECT, FDCAN_REJECT, FDCAN_REJECT_REMOTE, FDCAN_REJECT_REMOTE);

If you need to change this, consult the header file (stm32**xx_hal_fdcan.h, look for FDCAN_Non_Matching_Frames) to see the available configuration options.

Adding filters

Before adding filters, it is highly important to set the CAN controller’s filter count in CubeMX. There are two separate numbers for standard (Std) and extended (Ext) addressing types; since we are only using standard addressing, the Ext filter count should be set to 0. Once enough filters have been allocated, it can be set up using the following template:

FDCAN_FilterTypeDef filter; filter.FilterConfig = ; // usually FDCAN_FILTER_TO_RXFIFO0 filter.FilterType = ; filter.FilterIndex = ; filter.IdType = FDCAN_STANDARD_ID; filter.FilterID1 = ; filter.FilterID2 = ;

<index> determines which filter to initialize or update. ID 1 and ID 2 are used to determine which addresses are matched depending on the mode.

<mode> can be one of the following:

FDCAN_FILTER_DISABLE Disable this filter
FDCAN_FILTER_TO_RXFIFO0 Push matching packets into RX FIFO 0
FDCAN_FILTER_TO_RXFIFO1 Push matching packets into RX FIFO 1
FDCAN_FILTER_REJECT Reject matching packets
FDCAN_FILTER_HP Mark matching packets as high-priority (this is queue priority, not bus priority)
FDCAN_FILTER_TO_RXFIFO0_HP Mark matching packets high-priority and push into RX FIFO 0
FDCAN_FILTER_TO_RXFIFO1_HP Mark matching packets high-priority and push into RX FIFO 1

<type> can be one of the following:

FDCAN_FILTER_RANGE Match packets with IDs between ID 1 and ID 2. It is not clear whether this excludes ID2 or not.
FDCAN_FILTER_DUAL Match packets if the ID equals ID 1 or ID 2.
FDCAN_FILTER_MASK Match packets if the bitwise AND of the ID with ID2 (the mask) equals ID1. For example, ID2 = 0x7FF implies that the ID must exactly equal ID1, and ID2 = 0x000 will match any ID.
FDCAN_FILTER_RANGE_NO_EIDM Same as FDCAN_FILTER_RANGE, but without an “EIDM” mask. Not sure what this means.

Examples

Receiving Data

Once filters are set up and the FDCAN controller is started, received messages will be stored into the receive FIFOs. This data can be read directly using HAL_FDCAN_GetRxMessage()

FDCAN_RxHeaderTypeDef header; // CAN message metadata uint8_t data[8]; // CAN message data (should always allocate 8 bytes for this)

HAL_FDCAN_GetRxMessage( &hfdcan1, // pointer to the FDCAN controller instance FDCAN_RX_FIFO0, // which FIFO to read from (can also be FDCAN_RX_FIFO1) &header, // pointer to write header metadata into data // pointer to write message data into ); // returns HAL_OK on success and an error condition if the FIFO is empty or another error occurred

// Note: there is a function to directly check the FIFO fill level, but I cannot remember what it is – please update this page if you know

Interrupts And Callbacks

The FDCAN controller can also generate interrupts when new messages are received; this is the preferred method of processing CAN messages as it reduces the likelihood of lost messages due to a full FIFO buffer. However, there are some extra steps to set this up:

Once all this is set up, received CAN messages will trigger system interrupts, which will call the following callback functions:

// add these functions into your code somewhere void HAL_FDCAN_RxFifo0Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo0ITs) { }

// no need to add this function, though, if you’re not using the second receive FIFO void HAL_FDCAN_RxFifo1Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo1ITs) { }

Since these callback functions are declared inside the HAL with “weak linkage”, they can be declared anywhere inside your code. The linker will automatically override the default (empty) callbacks with your custom function; there is no need to manually add the callbacks anywhere. The callback function receives a pointer to the CAN controller instance that created the interrupt and a bit field that defines which event(s) caused the interrupt to be triggered. Even though we only activated the notification for NEW_MESSAGE events, it is good practice to check this bit field before assuming that a new message is available:

if (RxFifo0ITs & FDCAN_IT_RX_FIFO0_NEW_MESSAGE) { // process new message }

Once this is verified, the message can be read using the HAL_FDCAN_GetRxMessage() function as usual.

Using the CANLib library

The CANLib library provides a layer of abstraction over the HAL, making it somewhat easier to use (especially when using the receive callbacks).

To use, download the zip file from the Software Setup section (or get the latest version from the git server https://git.supermileage.ece.illinois.edu/Supermileage/canlib). Then follow the provided README to install the library into your project.

Initialization is simple; simply create an FDCAN using the above setup instructions, create a TxHeader, and add the following code to the initialization section of your main() function:

void canlib_rx0_callback(FDCAN_HandleTypeDef *hfdcan, FDCAN_RxHeaderTypeDef *header, uint8_t *data) { // process message data here }

struct CANLib can; if (CANLib_Init(&can, &hfdcan1, &txHeader) != 0) Error_Handler(); // use &hfdcan2 if you’re using the second FDCAN peripheral // setup incoming non-matching/remote message filtering if (CANLib_SetFilterMode(&can, CANLIB_NONMATCH_REJECT, 0) != 0) Error_Handler();

// optionally add receive filtering and callbacks here if (CANLib_AddFilter(&can, CANLIB_FILTER_1_MASK_2, CANLIB_FILTER_TO_FIFO0, 0x123, 0x7FF)) Error_Handler();

CANLib_SetReceiveCallback(0, canlib_rx0_callback);

// start the CAN controller if (CANLib_Start(&can) != 0) Error_Handler();

// send messages uint8_t data[8] = … if (CANLib_SendMsg(&can, CANLIB_LENGTH_8B, data) != 0) Error_Handler();

CAN Implementation (Advanced)

To understand how we configure CAN timings, we really need to dig deep into how the protocol works. For the purposes of this guide, think of CAN as a communication interface with many people (devices) on a bus. For our use-case, the main ECU is the bus driver.

CAN Packet Structure

First, CAN has four types of messages, but we really only care about three: the Data Frame, the Remote Frame, and the Error Frame.

CAN Protocol Tutorial img 1

The Data Frame is a type of message that constitutes most communication. “Hey everyone, here’s some information.” It’s used by devices to transmit data regularly to the ECU or to other devices. This is the main one we will be using, since we can also use data frames to ask for specific information.

screen-shot-2020-08-03-at-54743-pm

The Remote Frame is a type of message that can only request information. This is usually an announcement by the ECU or bus driver: “can someone check if the tires just popped?” Remote Frames are structured the same way as Data Frames, except the RTR bit is recessive (1) rather than 0, and the data length field refers to the expected length of the data frame that a device must respond with. Furthermore, there’s no data field.

screen-shot-2020-08-03-at-54753-pm

The Error Frame is a type of message that violates CAN transmission rules in order to report an error. Basically, this will force all CAN devices to detect an error. Don’t worry about the specifics for this one, since CAN controllers all implement error handling in hardware, and will attempt to retransfer any data after a period of time. The default CAN operational mode has error frames enabled, and we remain in this mode by transmitting with FDCAN_ESI_ACTIVE.

Now that we understand the types of messages, we need to look at the specifics of each type of field.

Above is an example of a complete CAN Data Frame with one byte of data, bit by bit.

When the frame starts to transmit, the device transmitting starts by setting the CAN bus to dominant (0) for one bit. This is to indicate to other devices that it is transmitting, so no other devices start a transmission after this one starts. This is implemented in the HAL, and we don’t need to worry about it.

Next, the device transmitting sends the Arbitration Field. This is 13 bits of information, including 11 bits which is the identifier and the RTR bit. Again, don’t worry about the stuff bits, those are implemented in the HAL.

The identifier is a general ID for CAN bus priority. It ranges from 0x000 to 0x7FF. The lower the number, the higher priority the device has on the bus. The ECU, or bus driver, typically has the highest priority, with the ID 0x000. In the case that two CAN transmissions start on the same bit, the controllers are always listening to what they’re setting versus what’s actually on the bus. Since 0 is dominant, if a controller detects that it’s trying to send 1 during arbitration and it reads a 0, it stops writing and allows the other devices to finish the transmission before trying again.

A common practice when we don’t have hundreds of devices on our CAN network is that we can split the identifier into two separate ideas: a priority level and an address. For our purposes, it makes sense to use a 3-bit priority level (8 levels) and 8-bit address (256 devices). Then, we can mask out the first three bits on our CAN filter, and receive for specific devices. When we do this, each device can now send messages of various priority, so only the most important data is sent first. It’s very important in implementation that each device can only transmit with a unique range of addresses, so that two devices cannot be transmitting past the Arbitration Field.

The RTR bit is just a bit to mark the type of transmission. 0 (dominant) means a device is sending data, while 1 (recessive) means a device wants to receive this specific piece of information.

Then, we have the Control Field, which is 7 bits. By this point, we can assume only 1 device is transmitting, so we can be more flexible with the format. It starts with the IDE bit, which should always be 0 in our case. This confirms we’re using the standard CAN 11-bit identifier. Next, we have the r0 bit, which is reserved. This bit doesn’t matter, but we set it to 0 just in case.

Next, we have the Data Length Code. This is a 4-bit identifier that ranges from 0000 (0) to 1000 (8). This just prefaces the number of bytes of data that we’re about to send.

After that, we transmit the Data Field. This field contains up to 8 bytes, or 64 bits of data. This is what we actually program the CAN bus to transmit in software, everything else is metadata for how it’s transmitted. We can stuff any binary representation of a structure up to 8 bytes. See the implementation for each device to see how this data field is used.

Finally, we transmit the CRC, a redundancy check that iterates over all transmitted bits and ensures that they are correct. This is 15 bits long. Next, the CRC delimiter bit, which should always be recessive (1). Then, we can acknowledge the response using the ACK bit. The transmitter always sends this recessive (1), but any receiver that accepts the message will pull it down to dominant (0). In many cases, including the STM32 FDCAN, messages transmitted will be resent if not acknowledged. Next, the ACK delimiter bit, which should always be recessive (1). Finally, we have a 7-bit end of frame, which is all 1s, and an inter-frame spacing, which is the minimum time that devices will wait before transmitting again. This actually varies between platforms, but 3 bits seems to be standard. We don’t have to worry about any of these, either, since the HAL implements all of this for us.

If any of these fields are invalid, the CAN controller will ensure that the ACK bit is not pulled dominant (0), and that the message was not received.

Implementation

Transmitting

Let’s look at how CAN is structured in STM32’s FDCAN HAL API. First, for transmission, we call

HAL_FDCAN_AddMessageToTxFifoQ(FDCAN_HandleTypeDef *hfdcan, ``const FDCAN_TxHeaderTypeDef *pTxHeader, ``const uint8_t *pTxData);

Our first parameter is the CAN bus that we’ve enabled, which is &hfdcanX, where X is the bus ID (1 or 2 usually)

Our second parameter is a pointer to our transmission header. This header specifies everything in the Arbitration Field.

/**

* @brief  FDCAN Tx header structure definition

*/

typedef struct

{

uint32_t Identifier;          ``// the 11-bit identifier

uint32_t IdType;              ``// FDCAN_STANDARD_ID, since we aren't using FDCAN_EXTENDED_ID

uint32_t TxFrameType;         ``// FDCAN_DATA_FRAME or FDCAN_REMOTE_FRAME

uint32_t DataLength;          ``// FDCAN_DLC_BYTES_X where X is the number of bytes (0-8)

uint32_t ErrorStateIndicator; ``// FDCAN_ESI_ACTIVE or FDCAN_ESI_PASSIVE if we disable active errors

uint32_t BitRateSwitch;       ``// for standard implementations, FDCAN_BRS_OFF

uint32_t FDFormat;            ``// FDCAN_CLASSIC_CAN, we do not transmit with flexible data rates

uint32_t TxEventFifoControl;  ``// storing TX events takes a lot of memory, so FDCAN_NO_TX_EVENTS

uint32_t MessageMarker;       ``// leave this as 0, it only matters if we use TxEventFifoControl

} FDCAN_TxHeaderTypeDef;

Our last parameter is a pointer to our data structure storing our data bytes, transmitted in forward order.

For all intensive purposes, transmitting is as simple as that. Configure a TxHeader, then add the data along with its header to a FIFO queue, which is then sent in order whenever the device is able to. For devices that transmit a stream of data without remote frames, consider this code snippet to ensure the transmission queue is never full: (source)

if (HAL_FDCAN_GetTxFifoFreeLevel(&hfdcan1) > 0)

{

if (HAL_FDCAN_AddMessageToTxFifoQ(&hfdcan1, &FDCAN1_TxHeader, FDCAN1_TxData) != HAL_OK) {

Error_Handler();

}

}

else

{

uint32_t txFifoRequest = HAL_FDCAN_GetLatestTxFifoQRequestBuffer(&hfdcan1);

if (HAL_FDCAN_IsTxBufferMessagePending(&hfdcan1, txFifoRequest)) {

HAL_FDCAN_AbortTxRequest(&hfdcan1, txFifoRequest);

}

}

Receiving

a

(The arrow from each filter to a FIFO is configurable, in this case filter 1 and 2 go to FIFO 0 and filter 3 goes to FIFO 1)

When a CAN Packet is received, there’s quite a few layers it has to go through to reach a callback function.

First, it must pass through filters that all correspond with a global filter configuration.

HAL_FDCAN_ConfigGlobalFilter(&hfdcan1, <nonMatchingStd>, <nonMatchingExt>, <rejectRemoteStd>, <rejectRemoteExt>);

Since we are not using the extended CAN frame, we reject both non-matching and remote frames of that kind. The point of a filter is to reject non-matching frames, so we also reject that. Matching standard remote frames should be rejected or accepted depending on implementation.

HAL_FDCAN_ConfigGlobalFilter(&hfdcan1, FDCAN_REJECT, FDCAN_REJECT, FDCAN_ACCEPT_REMOTE, FDCAN_REJECT_REMOTE);

Next, we have to configure filters that lead certain types of messages into certain queues. Here’s that part copied from the CAN Protocol document:

FDCAN_FilterTypeDef filter;

filter.FilterConfig = <mode>;

filter.FilterType   = <type>;

filter.FilterIndex  = <index>;

filter.IdType       = FDCAN_STANDARD_ID;

filter.FilterID1    = <ID ``1``>;

filter.FilterID2    = <ID ``2``>;

<index> determines which filter to initialize or update. ID 1 and ID 2 are used to determine which addresses are matched depending on the mode.

<mode> can be one of the following:

FDCAN_FILTER_DISABLE Disable this filter
FDCAN_FILTER_TO_RXFIFO0 Push matching packets into RX FIFO 0
FDCAN_FILTER_TO_RXFIFO1 Push matching packets into RX FIFO 1
FDCAN_FILTER_REJECT Reject matching packets
FDCAN_FILTER_HP Mark matching packets as high-priority (this is queue priority, not bus priority)
FDCAN_FILTER_TO_RXFIFO0_HP Mark matching packets high-priority and push into RX FIFO 0
FDCAN_FILTER_TO_RXFIFO1_HP Mark matching packets high-priority and push into RX FIFO 1

<type> can be one of the following:

FDCAN_FILTER_RANGE Match packets with IDs between ID 1 and ID 2. It is not clear whether this excludes ID2 or not.
FDCAN_FILTER_DUAL Match packets if the ID equals ID 1 or ID 2.
FDCAN_FILTER_MASK Match packets if the bitwise AND of the ID with ID2 (the mask) equals ID1. For example, ID2 = 0x7FF implies that the ID must exactly equal ID1, and ID2 = 0x000 will match any ID.
FDCAN_FILTER_RANGE_NO_EIDM Same as FDCAN_FILTER_RANGE, but without an “EIDM” mask. Not sure what this means.

Examples

Remember to add the number of filters that we have to the configuration in STM32CubeMX, in the parameter settings.

a

Then, to receive messages via callback, we first have to set up our NVIC interrupts. Note that I did this on an STM32G0B1, and it might be different on other chips:

a

The interrupt IT0 corresponds with FIFO0 on the FDCAN, and IT1 corresponds with FIFO1. Remember, the FIFOs correspond with each FDCAN peripheral, so you have two FIFOs to process per CAN interface.

Finally, we can enable notifications to the callbacks when we receive a message in the FIFOs:

HAL_FDCAN_ActivateNotification(

&hfdcan1, FDCAN_IT_RX_FIFO0_NEW_MESSAGE | FDCAN_IT_RX_FIFO1_NEW_MESSAGE, 0

);

// note: the 0 is ignored, since we are not using the interrupt for TX success.

Then, we use the callback functions and do whatever we need with them.

FDCAN_RxHeaderTypeDef header;

uint8_t data[8];

void HAL_FDCAN_RxFifo0Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo0ITs)

{

if (hfdcan == &hfdcan1 && (RxFifo0ITs & FDCAN_IT_RX_FIFO0_NEW_MESSAGE))

{

HAL_FDCAN_GetRxMessage(&hfdcan1, FDCAN_RX_FIFO0, &header, data);

}

}

void HAL_FDCAN_RxFifo1Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo1ITs)

{

if (hfdcan == &hfdcan1 && (RxFifo1ITs & FDCAN_IT_RX_FIFO1_NEW_MESSAGE))

{

HAL_FDCAN_GetRxMessage(&hfdcan1, FDCAN_RX_FIFO1, &header, data);

}

}

And here’s the documentation for FDCAN_RxHeaderTypeDef:

/**

* @brief  FDCAN Rx header structure definition

*/

typedef struct

{

uint32_t Identifier;            ``/*!< Specifies the identifier.

This parameter must be a number between:

- 0 and 0x7FF, if IdType is FDCAN_STANDARD_ID

- 0 and 0x1FFFFFFF, if IdType is FDCAN_EXTENDED_ID               */

uint32_t IdType;                ``/*!< Specifies the identifier type of the received message.

This parameter can be a value of @ref FDCAN_id_type               */

uint32_t RxFrameType;           ``/*!< Specifies the the received message frame type.

This parameter can be a value of @ref FDCAN_frame_type            */

uint32_t DataLength;            ``/*!< Specifies the received frame length.

This parameter can be a value of @ref FDCAN_data_length_code     */

uint32_t ErrorStateIndicator;   ``/*!< Specifies the error state indicator.

This parameter can be a value of @ref FDCAN_error_state_indicator */

uint32_t BitRateSwitch;         ``/*!< Specifies whether the Rx frame is received with or without bit

rate switching.

This parameter can be a value of @ref FDCAN_bit_rate_switching    */

uint32_t FDFormat;              ``/*!< Specifies whether the Rx frame is received in classic or FD

format.

This parameter can be a value of @ref FDCAN_format                */

uint32_t RxTimestamp;           ``/*!< Specifies the timestamp counter value captured on start of frame

reception.

This parameter must be a number between 0 and 0xFFFF              */

uint32_t FilterIndex;           ``/*!< Specifies the index of matching Rx acceptance filter element.

This parameter must be a number between:

- 0 and 127, if IdType is FDCAN_STANDARD_ID

- 0 and 63, if IdType is FDCAN_EXTENDED_ID

When the frame is a Non-Filter matching frame, this parameter

is unused.                                                        */

uint32_t IsFilterMatchingFrame; ``/*!< Specifies whether the accepted frame did not match any Rx filter.

Acceptance of non-matching frames may be enabled via

HAL_FDCAN_ConfigGlobalFilter().

This parameter takes 0 if the frame matched an Rx filter or

1 if it did not match any Rx filter                               */

} FDCAN_RxHeaderTypeDef;

CAN Timings

When configuring the FDCAN peripheral, there’s a lot of “magic numbers” in the configuration. This part aims to explain exactly how all of these work.

a

Bit Timings

a

Now, instead of looking at how each byte is written, let’s look at the timing for every single bit. First, notice how the bit timings are split up into time quanta, or the small sections on the bottom of the diagram. Those are determined by the period of the FDCAN clock.

a

The clock generally either comes from your system clock (PCLK/PLLR) or a secondary PLL clock (PLLQ). We can configure these clocks under clock configuration in STM32CubeMX. Then, it passes through a clock divider, which can be configured in the CAN parameters, then into two prescalers. The reason two prescalers exist here is because FDCAN supports CAN with flexible data rates, meaning that data can be transferred faster than arbitration since only one device is transmitting. However, for our purposes, our bit timings for our arbitration and data transfer are the same. Then, we can calculate the length of each time quantum using this formula:

tTime Quantum=FDCAN_CLKClock Divisor⋅Prescaler�Time Quantum=FDCAN_CLKClock Divisor⋅Prescaler

Now, we know how much a time quantum is, so let’s move onto bit segments. There are four bit segments, SYNC_SEGPROP_SEGPHASE_SEG_1, and PHASE_SEG_2. They are all measured in time quanta.

SYNC_SEG is by default 1 time quantum long. We never want to measure the level during the sync segment because this is when devices across the bus synchronize, and the signal is unpredictable. STM32’s CAN controller also gives you the option to extend the synchronization segment, by as much as the Sync Jump Width. When we do this, it delays everything else after it, shortening PHASE_SEG_2 (since the bit time must remain constant). Thus, the sync jump width must be less than or equal to the length of PHASE_SEG_2, since otherwise the chip will never read the bit on the bus. A larger sync jump width proportion is better, since it allows the controller to correct for sync for a larger portion of the bit time, improving reliability.

PROP_SEG and PHASE_SEG_1 are both considered part of the Seg1 Time on STM32. The PROP_SEG and PHASE_SEG_1 are automatically balanced depending on the state of the signals. PROP_SEG is used to compensate for signal delay errors, and PHASE_SEG_1 is used to compensate for edge phase and edge detection errors. The sum of these two cannot be shortened.

Next, we have the Sample Point. The STM32 reads the bit on the CAN bus after the end of PHASE_SEG_1. We measure the Sample Point as a percentage of the nominal bit time, or the total time for a bit to be transferred.

PHASE_SEG_2 has its length equal to Seg2 Time. This segment must be no shorter than 2 cycles on PCLK, so the STM32 can process and load the bit.

We can calculate the Nominal Bit Time as Seg1 Time + Seg2 Time + 1. Take the inverse of this and we get the effective bitrate, which must be kept the same between devices for them to communicate properly. CubeMX actually does this for you, by the way.

a

In order for our CAN controllers to work properly, we have to configure the timing so that the Sample Point is somewhere between 75% to 87.5%, with higher being better. Luckily, we don’t have to trial and error these numbers, we have a tool to do just that:

http://www.bittiming.can-wiki.info/

a

(note: the SJW option does not work for STM32)

This should give you the prescaler value and the length of the two segments. Then, to find SJW, use this formula:

SJW=⌊Seg 2−2Prescaler⌋SJW=⌊Seg 2−2Prescaler⌋

For our implementation (CAN with fixed data rates), the segment times and prescalers should be the same for nominal and data.

Exercise

Try calculating the timings for a 125kbps bitrate on a 48MHz clock with a 87.5% sample time.

You should get these numbers:

Prescaler Seg 1 Seg 2 SJW Sample Point
24 13 2 1 87.5%

With a large (>16) and divisible prescaler, one thing we should do to increase reliability is increase the proportion of the SJW. We can divide the prescaler and multiply the segment by a factor of the prescaler. Then, we can recalculate the SJW. Here, we do this by a factor of 2:

Prescaler Seg 1 Seg 2 SJW Sample Point
12 26 4 3 87.5%

We cannot go further, since the data time for Seg 1 and 2 must be less than or equal to 32 quanta.

Additional Settings

There are a few additional settings in CubeMX to be careful about:

Frame Format - choose Classic Mode, since it’s the mode that uses standard 11-bit identifiers.

Mode - choose Normal Mode for read/write, Restricted operation for read and acknowledge, Bus Monitoring for read only.

Auto Retransmission - will automatically resend failed transfers. Should be on for devices that give specific, one-time instructions or data transfers.

Transmit Pause – add an additional pause during transmission. This pause supposedly helps give other transmitters access to the bus if their priority is low, though we need to see if this affects anything. Keep it disabled for now.

Protocol Exception – if CAN-FD’s protocol fails, it will raise an exception. Keep this disabled, since we use classic CAN.

Resources

https://www.kvaser.com/can-protocol-tutorial/

https://en.wikipedia.org/wiki/CAN_bus

https://github.com/STMicroelectronics/STM32CubeG4/blob/master/Drivers/STM32G4xx_HAL_Driver/Inc/stm32g4xx_hal_fdcan.h

http://www.oertel-halle.de/files/cia99paper.pdf

https://prog.world/configuring-fdcan-in-cubemx/

http://www.bittiming.can-wiki.info/

https://www.st.com/resource/en/application_note/an5348-introduction-to-fdcan-peripherals-for-stm32-product-classes-stmicroelectronics.pdf

CAN Standardization

CAN Architecture

a

On one end, we have our ECU, which acts as one terminus of the bus. All other input/output nodes are connected to the bus, wired to minimize the distance between each node. The other terminus is any node, arbitrarily chosen with another 120 ohm termination resistor between the data high and low lines.

Any monitoring devices such as the USB to CAN debug board should not be allowed to acknowledge packets. They must be configured in monitoring mode by default. If messages need to be written, they should be written with the mode updated to normal mode, then an interrupt should be activated once the packet is written setting the bus back to monitoring mode.

CAN Speed

For flexibility, we assume the worst case scenario.

Specification Expectation Justification
Maximum trunk length 10m Circumference of the vehicle, rounded up
Maximum drop length 2.5m Length of the vehicle
Message capacity (2-byte) 4500/s 4 devices sending 1 packet at 1kHz + some more
Message capacity (8-byte) 2400/s 4 devices sending 1 packet at 1kHz + some more

According to official CAN recommendations, here are what speeds make sense to us:

Nominal speed 1Mbps 500kbps 125kbps 50kbps 10kbps
Maximum trunk length 25m 100m 500m 1km 5km
Maximum drop length (cumulative) 1.5m (7.5m) 5.5m (27.5m) 22m (110m) 55m (275m) 275m (1375m)
Message capacity (2-byte) 10000/s 5000/s 1250/s 500/s 100/s
Message capacity (8-byte) 5500/s 2750/s 680/s 275/s 55/s

For all standard CAN communications, we will use a nominal data rate of 500kbps.

CAN Standard

All communications on the main CAN bus will operate under “Classic CAN mode”, using CAN-FS peripherals.

This entails:

CAN Timings

Microcontrollers must have their CAN peripheral clocks set to a multiple of 16MHz. Then, we follow this formula for the timings, using kernel clock division to reduce the prescaler below 16:

FDCAN Peripheral Clock Clock Divisor Prescaler # of Time Quanta Seg 1 Seg 2 SJW Time Quantum Length Sample Point Nominal Data Rate
16N MHz Divide kernel clock by 1 N 32 27 4 3 62.5ns 87.5% 500000bps
32MHz Divide kernel clock by 1 2 32 27 4 3 62.5ns 87.5% 500000bps
48MHz Divide kernel clock by 1 3 32 27 4 3 62.5ns 87.5% 500000bps
64MHz Divide kernel clock by 1 4 32 27 4 3 62.5ns 87.5% 500000bps
80MHz Divide kernel clock by 1 5 32 27 4 3 62.5ns 87.5% 500000bps
96MHz Divide kernel clock by 1 6 32 27 4 3 62.5ns 87.5% 500000bps
112MHz Divide kernel clock by 1 7 32 27 4 3 62.5ns 87.5% 500000bps
128MHz Divide kernel clock by 1 8 32 27 4 3 62.5ns 87.5% 500000bps
144MHz Divide kernel clock by 1 9 32 27 4 3 62.5ns 87.5% 500000bps
160MHz Divide kernel clock by 1 10 32 27 4 3 62.5ns 87.5% 500000bps
192MHz Divide kernel clock by 1 12 32 27 4 3 62.5ns 87.5% 500000bps
240MHz Divide kernel clock by 1 15 32 27 4 3 62.5ns 87.5% 500000bps
320MHz Divide kernel clock by 2 10 32 27 4 3 62.5ns 87.5% 500000bps
480MHz Divide kernel clock by 2 15 32 27 4 3 62.5ns 87.5% 500000bps

11-bit Identifier

We lay out the 11-bit identifier as 2 groups, a 3-bit priority and 8-bit address.

10 (MSB) 9 8 7 6 5 4 3 2 1 0 (LSB)
ID[2] ID[1] ID[0] ADDR[7] ADDR[6] ADDR[5] ADDR[4] ADDR[3] ADDR[2] ADDR[1] ADDR[0]

Priority Levels

Priority Level Binary Explanation
0 000 Highest priority. Reserved for system-wide control instructions, such as a full halt.
1 001 Reserved for ECU control instructions
2 010 General control instructions
3 011 Low-priority control instructions
4 100 High-priority data transfers
5 101 Medium-priority data transfers
6 110 Low-priority/auxiliary data transfers
7 111 Lowest-priority data transfers, testing/debugging payloads

Address Groups

Address Group Number Explanation
0x00-0x0F 1 Reserved for main ECU board.
0x10-0x1F 16 Reserved for communications devices communicating with another interface (telemetry, U(S)ART bridge, etc)
0x20-0xFF 224 All other nodes

Specific Addresses (edit as more devices are implemented)

0 1 2 3 4 5 6 7 8 9 A B C D E F
0x0X ECU ECU ECU ECU ECU ECU ECU ECU ECU ECU ECU ECU ECU ECU ECU ECU
0x1X Tele. Tele. Tele. Tele.
0x2X
0x3X DDU DDU DDU DDU
0x4X MAF MAF O2 O2 Temp. Temp.
0x5X
0x6X
0x7X
0x8X
0x9X
0xAX
0xBX
0xCX
0xDX
0xEX
0xFX

canlib2 Implementation

Introduction

canlib2 is a high-level abstraction library that abstracts over the STM32 FDCAN peripheral. It integrates Eco Illini standardization, making it easy to implement without thinking too much about the specific hardware aspects. This document walks through a few standard implementations.

STM32CubeMX Configuration

Configure a FDCAN peripheral as such:

For prescaler values, see the table in CAN Standardization.

If using TX/RX interrupts, enable them in NVIC configuration.

Adding canlib2 to your project

copy canlib2.h into Project/Inc and canlib2.c into Project/Src.

Initialization & Configuration

Note that almost every single function returns CANLIB2_OK on success and CANLIB2_ERROR on error.

Peripheral Operation:

// create a canlib2_fdcan_t*
canlib2_fdcan_t* can = canlib2_configure(&hfdcan1);

// start the fdcan peripheral
canlib2_start(can);

// stop the fdcan peripheral
canlib2_stop(can);

// deconfigure and deallocate an fdcan peripheral
canlib2_deconfigure(can);

Global Configuration:

// change CAN operation mode to read and write (normal)
canlib2_change_mode(can, CANLIB2_MODE_READ_WRITE);

// change configuration for filtering non-matching identifiers and remote frames.
// we usually want to reject these
canlib2_change_global_filter_config(can, CANLIB2_NM_REJECT, CANLIB2_REJECT_REMOTE);

// enable an RX interrupt when FIFO #0 receives a new message
canlib2_enable_rx_interrupt(can, CANLIB2_RX_FIFO0_NEW_MESSAGE);
canlib2_disable_rx_interrupt(can, CANLIB2_RX_FIFO0_NEW_MESSAGE);

// TX interrupts are also supported
canlib2_enable_tx_interrupt(can, CANLIB2_TX_COMPLETE);
canlib2_disable_tx_interrupt(can, CANLIB2_TX_COMPLETE);

// set the interrupt functions that canlib2 calls
// the function typedefs are as follows:
// typedef void (*canlib2_rx_callback)(FDCAN_HandleTypeDef* fdcan, canlib2_rx_return_t ret);
// typedef void (*canlib2_tx_callback)(FDCAN_HandleTypeDef* fdcan, canlib2_tx_return_t ret);
canlib2_set_rx_callback(can, function);
canlib2_set_tx_callback(can, function);
Transmitting Packets
uint8_t data[6]; // pretend this had 6 bytes of data in it

// send all 6 bytes of data with priority 0 and address 0x3F
canlib2_send_data(can, 0x3F, 6, &data);
 
// send all 6 bytes of data with priority 4 and address 0x3F
canlib2_send_data(can, 4, 0x3F, 6, &data);

// send all 6 bytes of data with full identifier 0x43F
canlib2_send_data(can, 0x43F, 6, &data);

/** TX headers: use internally and if you want to implement sending manually **/
// create a TX header for this CAN with priority 0 and address 0x3F
FDCAN_TxHeaderTypeDef tx_header = canlib2_get_tx_header(can, 0x3F);

// create a TX header for this CAN with priority 4 and address 0x3F
FDCAN_TxHeaderTypeDef tx_header = canlib2_get_tx_header_p(can, 4, 0x3F);

// create a TX header for this CAN with full identifier 0x43F
FDCAN_TxHeaderTypeDef tx_header = canlib2_get_tx_header_id(can, 0x43F);
Receiving Packets
// create a filter for a specific address 0x3F and send packets to FIFO #0
canlib2_add_rx_filter_by_address(can, CANLIB2_FILTER_TO_FIFO0, 0x3F);

// create a filter for a specific address 0x3F and priority 4 and send packets to FIFO #0
canlib2_add_rx_filter_by_address_p(can, CANLIB2_FILTER_TO_FIFO0, 4, 0x3F);

// create a filter for a partial address 0x3X and send packets to FIFO #0
canlib2_add_rx_filter_by_partial_address(can, CANLIB2_FILTER_TO_FIFO0, 4, 0x30);

// create a filter for all packets with priority 0 and send packets to FIFO #0
canlib2_add_rx_filter_by_priority(can, CANLIB2_FILTER_TO_FIFO0, 0);

// there's quite a few more ways to filter, see canlib2.h

Note: RX/TX callbacks are called regardless of event! You still need to filter which canlib2_rx_event or canlib2_tx_event you received! Refer to the below for the struct passed to callback functions:

typedef struct CANLib2_RX_Callback_ReturnType {
    /// @brief pointer to HAL FDCAN peripheral
    FDCAN_HandleTypeDef* fdcan;

    /// @brief event received
    /// @ref canlib2_rx_event
    canlib2_rx_event event;

    /// @brief identifier received
    /// @ref canlib2_identifier
    canlib2_identifier_t identifier;

    /// @brief type of received frame
    /// @ref canlib2_frame_type
    canlib2_frame_type frame_type;

    /// @brief data field length (0-8 bytes)
    uint8_t length;

    /// @brief pointer to data field
    uint8_t* data;
} canlib2_rx_return_t;

typedef struct CANLib2_TX_Callback_ReturnType {
    /// @brief pointer to HAL FDCAN peripheral
    FDCAN_HandleTypeDef* fdcan;

    /// @brief event received
    /// @ref canlib2_rx_event
    canlib2_tx_event event;
} canlib2_tx_return_t;

Compatibility

Compatible Not Compatible
STM32G0B1xx STM32F0XXxx
STM32G4XXxx STM32F1XXxx
STM32H7XXxx STM32F3XXxx
STM32L5XXxx STM32F4XXxx

Example

  // configuration
  can = canlib2_configure(&hfdcan1);
  if (canlib2_enable_rx_interrupt(can, CANLIB2_RX_FIFO0_NEW_MESSAGE)) Error_Handler();
  if (canlib2_change_global_filter_config(can, CANLIB2_NM_REJECT, CANLIB2_REJECT_REMOTE)) Error_Handler();
  if (canlib2_add_rx_filter_by_address(can, CANLIB2_FILTER_TO_FIFO0, 0x20)) Error_Handler();
  if (canlib2_start(can) == CANLIB2_ERROR) Error_Handler();
  canlib2_set_rx_callback(can, CANLIB2_FIFO0, rx_callback);

  // in loop, to send data
  canlib2_send_data_p(can, 0x3, 0x20, 0x06, data);

  // callback function
  void rx_callback(FDCAN_HandleTypeDef* fdcan, canlib2_rx_return_t ret) {
      if (ret.event == CANLIB2_RX_FIFO0_NEW_MESSAGE) {
          // ......
      }
  }