Basic CAN configuration with STM32F407

Controller Area Network (CAN) is one of the most common protocols in the Embedded Systems world. Through this article my intention is to take you with me on my journey of learning CAN protocol using the STM32 eco system. I use STM32 here simply because I have an STM32F407-Disco board at my disposal. Please note that this article wouldn’t teach you CAN protocol, rather, it refers to materials that already exist in the Internet and builds upon that knowledge to implement basic communication scheme on actual hardware.

The main use case for CAN is in the automotive industry. It is one of the most common communication protocols used in modern automobiles. Most of the sensors transmit their data through the CAN bus that is connected to the control unit of the automobile. You can find hundreds of articles about these use cases in the Internet.

This article is the first in a series. I plan to expand on this foundation and implement a mock CAN network in the coming weeks.

Board bring up

As the first step of any embedded project, it’s a best practice to write a simple program to blink an LED. This is one of the most powerful programs, because it proves that your compiler, programmer and the board works properly. Therefore, naturally, this was my first step.

Learning CAN protocol

It’s very difficult to start learning how to use a protocol without understanding the basics of the protocol. One doesn’t need to understand all the nitty-gritties of the protocol, however, a basic understanding is a must to be ready for what comes next in datasheets and reference manuals.

I referred the Introduction to the Controller Area Network (CAN) document by Texas Instruments to get the basic understanding.

Once I could understand the basic concepts, I had a look at the bxCAN implementation in the STM32F407 reference manual. It explains the mechanics of the STM32 adaptation and realization of the CAN protocol.

CAN interface configuration

Afterwards, I enabled the CAN interface using the STM32CubeMX software. I selected CAN1 and it automatically configured the required GPIO pins. The only other configuration I changed was the operating mode under the CAN Advanced Parameters configuration. I set it to “Loopback combined with Silent”. This mode is designed to test CAN configuration of nodes without interrupting any network that they are connected to. The configuration is explained in detail in the section 32.5 of the reference manual. My idea is to make use of the loopback mode to test the implementation without depending on any external components.

Understanding the STM32Cube HAL API for CAN

One thing I really like about the STM32Cube HAL is their API documentation. All the relevant information is usually available at the begining of the source file. In this case, for CAN API, all the information is available in the stm32f4xx_hal_can.c file.

If I summarize the required APIs,

Define HAL_CAN_MspInit()

Auto generated in stm32f4xx_hal_msp.c Called by HAL_CAN_Init() GPIO configuration and clock configuration is autogenerated inside this function

Call HAL_CAL_Init()

This function is called within the autogenerated MX_CAN1_Init() in main.c

Configure RX filters using HAL_CAN_ConfigFilter()

This function call is NOT auto generated

Start the CAN peripheral by calling HAL_CAN_Start()

This function call is NOT auto generated

Use a function to transmit a CAN frame

This function call is NOT auto generated

Use a function to recieve a CAN frame

This function call is NOT auto generated

First two steps are already done by the STM32CubeMx software. Therefore, we only need to manually implement the last four steps in the sequence above.

Filter configuration

This is the most confusing configuration in this example. I’ll try to explain the simplest filtering scheme here. However, before we start talking about the filtering we need to briefly look at the format of a CAN frame.

Filter

In the above image, the CAN frame is the physical data that transmits through the network. Once the bxCAN peripheral receives it, the standard ID, extended ID, Identifier Extension bit, and Remote Transmission Request bit are extracted from the message. These form the identifier of the CAN frame as recognized by the bxCAN implementation.

The identifier register contains the value that the peripheral compares against this received packet.

However, there is another register called the mask register that is used as a bit mask to select what are the must match bits and what are the don’t care bits.

In the simplest filter configuration, we can set the mask register to be 0xFFFFFFFF such that all the bits in the extracted identifier gets compared with all the bits of the filter identifier register. In the diagram below, I attempted to summarize this whole operation using two logic AND operations. If value1 and value2 are equal, that is considered a match in the filter.

MaskRegister

Assume the STDID is 0x02 and all other IDs are 0. In that case the maching filter value should be 0x0040 0000. If the filter mask register is 0xFFFF FFFF, then our peripheral would recieve this message without any issue.

MatchingFilter

With this information let’s have a look at the API.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
CAN_FilterTypeDef can_filter;
can_filter.FilterIdHigh = 0x0040;
can_filter.FilterIdLow = 0x0000;
can_filter.FilterMaskIdHigh = 0xFFFF;
can_filter.FilterMaskIdLow = 0xFFFF;
can_filter.FilterFIFOAssignment = CAN_FILTER_FIFO0;
can_filter.FilterBank = 0;
can_filter.FilterMode = CAN_FILTERMODE_IDMASK;
can_filter.FilterScale = CAN_FILTERSCALE_32BIT;
can_filter.FilterActivation = CAN_FILTER_ENABLE;
if (HAL_CAN_ConfigFilter(&hcan1, &can_filter) != HAL_OK)
{
    Error_Handler();
}

FilterIdHigh and FilterIdLow

Filter identifier to match

FilterMaskIdLow and FilterMaskIdHigh

Filter mask to match Since it is 0xFFFF all the bits must match

FilterFIFOAssignment

There are two recieve FIFOs Which FIFO to use In our example, I decided to use FIFO0

FilterBank

There are 28 filter banks Since we only have one identifier this is set to 0

FilterMode

This is set to mask mode because we compare identifier with the bit mask

FilterScale

The full 32 bit register is used for matching If we set this to 16, the identifier mapping is different and it is not covered in this example

FilterActivation

This simply activates the filter

That’s the full filter configuration and this is one of the most complex configurations in the bxCAN peripheral.

Creating the transmit message

Next step is transmitting a message over the wire. This is not that complex, however, let’s have a look at this line by line.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
CAN_TxHeaderTypeDef header;

header.DLC = 1;
header.ExtId = 0;
header.StdId = 2;
header.IDE = CAN_ID_STD;
header.RTR = CAN_RTR_DATA;
header.TransmitGlobalTime = DISABLE;

uint32_t tx_mailbox_used;
uint8_t tx_data[8];
tx_data[0] = 0x12;

HAL_CAN_AddTxMessage(&hcan1, &header, tx_data, &tx_mailbox_used);

DLC

Data Length Code is a field in both standard and extended CAN frames. Data Length Code specifies the byte count of data. The maximum value is 8

ExtId

Value of the extended identifier field in the CAN frame

StdId

Value of the standard identifier field in the CAN frame

IDE

Identifier Extensions bit specifies whether the frame is extended or standard Since we set the IDE bit to 0 in the filter, we have to use the CAN_ID_STD here. If we want extended CAN frames, then we have to change the filter as well

RTR

This bit is set when one node requests data from another node. In our case, since we send data, the bit needs to be set to DATA

TransmitGlobalTime

ENABLING this would send the timestamp information in DATA6 and DATA7 bits I don’t intend to use this feature in our example

Finally, let’s have look at the API.

tx_data

Actual user data payload Maximum length is 8, however, I’ve set DLC to 1 which means the length is just 1 byte

tx_mailbox_used

This is returned from the API and stores what mailbox is used to send the data out There are three mailboxes in bxCAN peripeheral The value can be used as an input to some other APIs to get the status of the mailbox, etc.

Waiting for the tx to complete

This is straight forward, I waited in a while loop until all transmit mailboxes are free.

while(HAL_CAN_GetTxMailboxesFreeLevel(&hcan1) != 3) {};

Since the peripheral contains 3 transmit mailboxes, if all three mailboxes are free, we are sure that TX has completed.

Receiving the frame

Since I used silent loopack mode for CAN peripheral, the transmitted frame should end up in the receiver. CAN peripheral automatically receives, filters and stores the incoming messages without any CPU intervention. Which is pretty awesome!

1
2
3
4
5
6
7
8
int msg_count = HAL_CAN_GetRxFifoFillLevel(&hcan1, CAN_RX_FIFO0);

if (msg_count > 0)
{  
    CAN_RxHeaderTypeDef rx_header;
    uint8_t rx_data[8];
    HAL_CAN_GetRxMessage(&hcan1, CAN_RX_FIFO0, &rx_header, rx_data);
}

This code is somewhat self explanatory, in that, it first checks receive fifo level. If it sees a message in the FIFO, it would then read it out. The CAN header and data are read into two separate data structures to make it easier to parse.

CAN module is stuck in INITIALIZATION

I added some additional code to blink an LED everytime we receive a packet. Otherwise, it will be annoying to keep on looking at breakpoints to ensure that everything works.

I compiled the code and programmed the MCU. Since I configured the CAN peripheral in the silent-loopback-mode I thought it would just loopback the message internally and work like a charm. However, soon I found my self in stackoverflow. The problem was, the CAN peripheral is stuck in the INITIALIZATION mode. I had to add breakpoints and look at the return codes of these APIs to find this out.

According to the reference manual, the CAN module requires a total of 11 consecutive recessive bits on the CANRX to move into NORMAL mode. For some reason, it didn’t receive the 11 consecutive recessive bits on the CAN_RX pin. Without trying to figure this out myself I referred this stackoverflow question.

It seems that for some unknown reason we need to configure an internal PULL_UP on the RX pin. This surpirsed me because based on the reference manual, the CAN peripheral must be detached from the physical pin.

After enabling the internal pull up on CAN_RX line, the peripheral successfully changed its state to NORMAL.

Bit timing issue

Once the initialization is sorted, I came across another issue where the receiver couldn’t see my data on the bus. I figured that a few things can go wrong here,

  • I might have incorrectly configured the filter
  • There is some problem in the default peripheral configuration

I was fairly confident with my understanding of the filter configuration, therefore, this time I started doubting the default peripheral configuration. In order to make sure that my CAN init parameters are correct, I referred one of the CAN examples shipped with the STM HAL library. This example can be found in STM32Cube_FW_F4_V1.26.2\Projects\STM324x9I_EVAL\Examples\CAN\CAN_LoopBack\Src folder.

I noticed that along with the filter configuration it updates the bit timing configuration as well. Till this point, I didn’t look at bit timing at all and it became apparant that I have to look at bit timing configuration now. I had a quick read on the bit timing explanation in the STM reference manual and then here

Bit timing calculation

CAN1 is connected to APB1 clock domain. Default clock for APB1 is 6.25 MHz. Let’s start our caculation there.

Bit timing for default configuration in CubeMX

Bit segment 1 = 1 Bit segment 2 = 1 Prescalar = 16

Let’s calculate the bit rate:

Pre-scalar = 16
Clock used for CAN1 = 6.25e6 / 16

Duration of time quanta for CAN1 = 16 / 6.25e6 = 2560 ns
Time quantas used for a nominal bit = 1 (Synch) + 1 (Bit segment 1) + 1 (Bit segment 2) = 3

I had a look at a calculator I found here which claims that minimum number of time quantas required for CAN is 8. It also claims that 16 is the best value to use, however, I didn’t refer other documents to understand this claim. This could be true, because when I looked at the STM CAN example that I mentioned before, I can see that they use 9 time quantas per nominal bit. Eventhough it is not 16, it is still more than 8. In the default configuration the amount of time quantas is just 3.

CANOpen is one of the most populer CAN communication protocol over CAN. It claims that bit sampling point shall be set to 87.5% of the nominal bit. The sampling point is the boundary between the Bit segment 1 and the bit segment 2. In the default configuration, the sampling point is at the 66.7% of the nominal bit.

Therefore, I decided to change the configuration such that,

  • We have 16 time quantas per nominal bit
  • The sampling point is at 87.5% of the nominal bit
| SYNC | ...Bit Segment 1... | .....Bit segment 2..... |
                             ^ (Sampling Point)

Updated calculation

Total time quantas = 16
Time quantas till the optimum sampling time = 16 * 87.5% = 14
Time quantas for bit segment 1 = 14 - 1 (synch) = 13
Time quantas for bit segment 2 = 16 - 14 = 2

Resulting nominal bit length = 16 * 2560 ns = 40960 ns
Resulting bit rate = 24.414 kbps

We can reduce CAN1 pre-scalar or increase APB1 clock rate to increase the bit rate. However, for out example I don’t concentrate too much on the bit rate. We can set these parameters in the STM32CubeMX, under CAN configuration.

Success

After all these changes, I could finally receive the CAN frame through my firmware. At this stage, it doesn’t do anything. However, this proves that the CAN peripheral configuration is valid and my understanding of the protocol is correct.

In the next article I’ll expand on this example and introduce interrupts. In this example, I added all this logic in the super loop.

The example is available in the following repository.

Repository: https://github.com/pradeepa-s/pradeepas_content.git

Tag: can_example_1

File path: <repo_root>/006_can_articles/can_example

Thank you!

-Pradeepa


comments powered by Disqus