STM32 Blue Pill CAN Communication in Loopback Mode – A Register-Level Implementation

In my previous blog post, I covered the fundamental concepts of the CAN bus protocol. If you haven’t had the chance to read it yet, I suggest doing so as it will enhance your understanding of how the CAN bus operates within the STM32 Blue Pill and offer valuable tips for debugging the CAN bus during the development process. You can check it out here.

In this blog post, we will dive into a hands-on session with the CAN bus in loopback mode. For this, you only need a Blue Pill board—no external hardware is required. We will explore what loopback mode is, discuss the different modes supported by the STM32 Blue Pill, and more in the upcoming sections.

The STM32 Blue Pill’s CAN module, called bxCAN (Basic Extended CAN), supports both standard identifiers (11-bit) and extended identifiers (29-bit), compliant with the CAN 2.0 protocol. The CAN module features two FIFOs (FIFO0 and FIFO1) for receiving messages sent by other CAN nodes. Each FIFO has three stages, meaning it can hold up to three messages in its queue. For transmitting data, the module uses three mailboxes. Messages are sent from the mailboxes either based on their priority or in a FIFO order, depending on the configuration.

Additionally, the module provides 14 filter banks that allow you to select specific packets from the received data. I will explain how to configure these filter banks in the upcoming sections. Below is a simplified block diagram of the CAN peripheral to help you visualize its components.

The STM32 bxCAN module supports several test modes, which are useful for different debugging and testing scenarios:

  1. Loopback Mode: This mode allows you to configure and debug the CAN setup without requiring external hardware. (Explained in detail in this post.)
  2. Silent Mode: Ideal for observing CAN bus traffic without actively participating or interfering with the communication.
  3. Combined Loopback and Silent Mode: Enables offline testing without sending signals onto the CAN bus, combining the advantages of loopback and silent modes.

Filter bank configuration

The STM32 Blue Pill’s CAN peripheral includes 14 filter banks, each designed to selectively process CAN messages based on their Identifier (ID). Each filter bank consists of two 32-bit registers: FR1 and FR2. These registers allow you to configure filters in either 32-bit width or 16-bit width mode, with two main filtering options: List Mode and Mask Mode.

Filter Configuration Modes
ModeWidthConfiguration Details
List32-bitFR1 and FR2 are used to configure 2 IDs (1 in each register).
16-bitFR1 and FR2 are divided into two 16-bit sections, allowing configuration of 4 IDs (2 in each register).
Mask32-bitFR1 configures the ID, while FR2 configures the mask. Bits set to 1 in the mask must match the ID; bits set to 0 are “don’t care.”
16-bitThe first 16 bits of FR1 and FR2 configure the ID and mask for one filter. The remaining 16 bits configure the ID and mask for a second filter.

This information conveyed in the Figure 230 of RM0008.

Registers used

  1. CAN Control and Status Register
    • Clock configuration registers – RCC_APB1
    • CAN Master Control Register – CAN_MCR
    • CAN Master Status Register – CAN_MSR
    • CAN Transmit Status Register – CAN_TSR
    • CAN Bit Timing Register – CAN_BTR
    • CAN Interrupt Enable Register – CAN_IER
    • CAN Receive FIFO 0 Register – CAN_RF0R
  2. CAN Filter configuration Registers
    • CAN Filter Master Register – CAN_FMR
    • CAN Filter Mode Register – CAN_FM1R
    • CAN Filter Scale Register – CAN_FS1R
    • CAN Filter Activation Register – CAN_FA1R
    • CAN Filter FIFO Assignment Register – CAN_FFA1R
    • CAN Filter Bank Registers – CAN_FR1 & CAN_FR2
  3. CAN Mailbox Registers
    • CAN TX Identifier Register – CAN_TIxR
    • CAN Receive FIFO mailbox Identifier register – CAN_RIxR
    • CAN Mailbox Data Length Control and Time stamp Register – CAN_TDTxR
    • CAN Receive FIFO Data Length Control and Time stamp Register – CAN_RDTxR
    • CAN Mailbox data low/high register – CAN_TDLxR/CAN_TDHxR
    • CAN Receive FIFO data low/high register – CAN_RDLxR/CAN_RDHxR

Hands-On

In this hands-on session, we will configure the CAN peripheral in loopback mode to send and receive messages. Follow these steps:

  1. Task: Send a message (e.g., 2 bytes) with ID 0x102 after configuring the filter with:
    • Filter ID: 0x102 and Mask: 0x7FF
    Expected Output:
    The received message will be printed from the interrupt handler to UART in the following Msg(id:102, len:2) data[0]:1, data[1]:2
  2. Test Case 1:
    • Keep the same filter configuration (ID 0x102, mask 0x7FF).
    • Send a message with ID 0x103.
    Expected Output:
    The message will not be received in the interrupt handler. This is because the mask 0x7FF enforces a strict match with the filter ID, and 0x103 does not match the configured filter.
  3. Test Case 2:
    • Configure the filter with:
      • Filter ID: 0x102
      • Mask: 0x7FE
    • Send messages with ID 0x102 and ID 0x103.
    Expected Output:
    Messages with both ID 0x102 and ID 0x103 will be received. This happens because the least significant bit (LSB) of the mask is 0, making the corresponding bit of the message ID a “don’t care” condition.

Steps to be followed

Initialize the CAN peripheral and enable the receive interrupt

Configure the clock control register and interrupts CAN receive interrupts

RCC->APB1ENR |= RCC_APB1ENR_CAN1EN;
prio_grp = NVIC_GetPriorityGrouping();
NVIC_SetPriority(USB_LP_CAN1_RX0_IRQn, NVIC_EncodePriority(prio_grp, 0, 0));
NVIC_EnableIRQ(USB_LP_CAN1_RX0_IRQn);
NVIC_SetPriority(CAN1_RX1_IRQn, NVIC_EncodePriority(prio_grp, 0, 0));
NVIC_EnableIRQ(CAN1_RX1_IRQn);

After powering on the peripheral, we have to set the hardware in to initialization mode by setting the INRQ in the CAN_MCR register and wait until the INAK bit in the MSR is set. This is required to configure the CAN peripheral.

CAN1->MCR |= CAN_MCR_INRQ;
while(0 == (CAN1->MSR & CAN_MSR_INAK));

After initialization, the CAN peripheral will be in sleep mode, exit from sleep mode by clearing SLEEP bit in the CAN_MCR register and wait until SLAK bit in the CAN_MSR is set.

CAN1->MCR &= ~CAN_MCR_SLEEP;
while(0 != (CAN1->MSR & CAN_MSR_SLAK));

Configure the mode as loopback and time quanta(TQ) for TS1 and TS2, I am taking 2TQ for TS1 and 1TQ for TS2 in the CAN_BTR register

CAN1->BTR |= (uint32_t) (CAN_BTR_LBKM | CAN_BTR_TS1_0);
Configure filter to receive data

We need to put the filter into initialization mode before configuring in the CAN_FMR register

CAN1->FMR |= CAN_FMR_FINIT;

To configure a filter bank, first we need to deactivate it by clearing the corresponding bit in the CAN_FA1R. Please note that this register can only be configured if FINIT bit is set in the CAN_FMR register. (say SEL_FILTER_BANK = 0)

CAN1->FA1R &= ~(1 << SEL_FILTER_BANK);

I am using the filter register in 32-bit width. Please not that we are using the standard identifier which require only 16-bit width. But for simplicity I am using not using 16-bit

CAN1->FS1R |= (1 << SEL_FILTER_BANK);

Configure the message ID and filter mask in FR1 and FR2 respectively in the selected filter bank( in our case we have selected filter bank 0).

CAN1->sFilterRegister[SEL_FILTER_BANK].FR1 = message_id;
CAN1->sFilterRegister[SEL_FILTER_BANK].FR2 = filter_mask;

Configure filter bank as mask mode (use with ID and mask) rather than list mode.

CAN1->FM1R &= ~(1 << SEL_FILTER_BANK);

Assign the filter to any one of the FIFO, I am assigning to FIFO0

CAN1->FFA1R &= ~(1 << SEL_FILTER_BANK);

Enable the filter bank and leave from the initialization mode

CAN1->FA1R |= (1 << SEL_FILTER_BANK);
CAN1->FMR &= ~CAN_FMR_FINIT;
Handle the receive interrupt

The number of messages in the FIFO will be stored in FMP0 bits of CAN_RF0R register. The type of ID whether it is basic or extended will be set in IDE bit of CAN_RIR of FIFO 0.

if(CAN1->RF0R & CAN_RF0R_FMP0) {
    if(0 == (CAN1->sFIFOMailBox[0].RIR & CAN_RI0R_IDE)) {
        on_std_msg_received();
    }
} 

We can parse the ID of received message from CAN_RIR register and the length of data (DLC – Data Length Code) from CAN_RDTR register of FIFO 0.

uint16_t id = (CAN_RI0R_STID & CAN1->sFIFOMailBox[0].RIR) >> CAN_TI0R_STID_Pos;
uint8_t dlc = ((CAN1->sFIFOMailBox[0].RDTR & CAN_RDT0R_DLC) >> CAN_RDT0R_DLC_Pos);

Parse the actual data from CAN_RDLxR and CAN_RDHxR based on the DLC obtained in the previous step.

data[0] = (uint8_t)((CAN_RDL0R_DATA0 & CAN1->sFIFOMailBox[0].RDLR) >> CAN_RDL0R_DATA0_Pos);
data[1] = (uint8_t)((CAN_RDL0R_DATA1 & CAN1->sFIFOMailBox[0].RDLR) >> CAN_RDL0R_DATA1_Pos);
data[2] = (uint8_t)((CAN_RDL0R_DATA2 & CAN1->sFIFOMailBox[0].RDLR) >> CAN_RDL0R_DATA2_Pos);
data[3] = (uint8_t)((CAN_RDL0R_DATA3 & CAN1->sFIFOMailBox[0].RDLR) >> CAN_RDL0R_DATA3_Pos);
data[4] = (uint8_t)((CAN_RDH0R_DATA4 & CAN1->sFIFOMailBox[0].RDHR) >> CAN_RDH0R_DATA4_Pos);
data[5] = (uint8_t)((CAN_RDH0R_DATA5 & CAN1->sFIFOMailBox[0].RDHR) >> CAN_RDH0R_DATA5_Pos);
data[6] = (uint8_t)((CAN_RDH0R_DATA6 & CAN1->sFIFOMailBox[0].RDHR) >> CAN_RDH0R_DATA6_Pos);
data[7] = (uint8_t)((CAN_RDH0R_DATA7 & CAN1->sFIFOMailBox[0].RDHR) >> CAN_RDH0R_DATA7_Pos);

Finally release the FIFO0, this will decrement the number of pending messages in the FMP0

CAN1->RF0R |= CAN_RF0R_RFOM0;
Start CAN peripheral

We have configured everything, now just start the CAN peripheral and exiting from the initialization mode and wait until the hardware acknowledge it.

CAN1->MCR &= ~CAN_MCR_INRQ;
while(0 != (CAN1->MSR & CAN_MSR_INAK));

Enable the interrupt

CAN1->IER |= CAN_IER_FMPIE0;
Send the data

CAN send message is straight forward, check if any mailboxes are available from CAN_TSR register and get it from the same register

if(0 != (CAN1->TSR & (CAN_TSR_TME0 | CAN_TSR_TME1 | CAN_TSR_TME2))) {
    tx_mailbox = (CAN1->TSR & CAN_TSR_CODE) >> CAN_TSR_CODE_Pos;

Set standard ID in CAN_TIxR register, length in CAN_TDTR register and actual data in CAN_TDLxR or TDHxR

CAN1->sTxMailBox[tx_mailbox].TIR = (std_id << CAN_TI0R_STID_Pos);
CAN1->sTxMailBox[tx_mailbox].TDTR = len;
CAN1->sTxMailBox[tx_mailbox].TDLR = ((data[1] << CAN_TDL0R_DATA1_Pos) | (data[0] << CAN_TDL0R_DATA0_Pos));

Request the hardware to transmit the data

CAN1->sTxMailBox[tx_mailbox].TIR |= CAN_TI0R_TXRQ;

Solution

The complete source code of the solution is available in my git repository, feel free to comment below if you need any additional support.

Leave a comment