Introduction to CAN Transmission with STM32 HAL

Austin Yang Illini RoboMaster
[[email protected], https://www.illinirobomaster.com]

2025-10-18

Read the pdf version here.

Introduction

Plan

Last time, we went through the object layer implementation for CAN on a Linux board. This time, we will go through the transfer layer specification and a toy implementation. This tutorial will go through 📄can\_raw\_transmit.cc line-by-line as an introduction to CAN (Controller Area Network) communication at a low level. We will cover concepts in:

You can find the files covered/mentioned at https://github.com/illini-robomaster/iRM_Embedded_2026/blob/embedded_tutorial/ examples/motor/can_raw_transmit.cc https://github.com/illini-robomaster/iRM_Embedded_2026/blob/embedded_tutorial/shar- ed/bsp/bsp_can.h https://github.com/illini-robomaster/iRM_Embedded_2026/blob/embedded_tutorial/shar- ed/bsp/bsp_can.cc

Basic Knowledge

CAN Protocol Basics

CAN is a communication protocol (“bus”) that allows devices to talk to each other in a distributed manner. It was first used on cars, but now is a standard protocol in a variety of fields, including robotics. It is a mailbox-based system, which means that messages are broadcast to all devices connected to the network. Each device has an ID, and only care about the messages that are marked with an ID they care about (typically, their own). While we can only specify a single ID in our CAN frame (we will see this very explicitly in ), we can use some tricks to talk to multiple devices with a single packet. We will see an example of this later in our own motor communication code.

CAN Frame Structure

The following information can all be found on Wikipedia: https://en.wikipedia.org/wiki/CAN_bus. I have reworded it and added some comments. I have also changed the color coding scheme. I have colored the entries that we care about, mostly. This is the structure of the standard CAN packet:

Explanation of each field in a standard CAN 2.0A frame
Field Bits Purpose
SOF 1 Start of Frame”. You can guess what this does.

Identifier

11 Contains a standard 11-bit CAN ID.

RTR

1 Remote Transmission Request”. This specifies the type of frame the message uses.
IDE 1 Identifier Extension”. This specifies whether we use a standard CAN frame (0) or extended CAN frame (1).
r0 1 Reserved.
DLC 4 Data Length Code”. Specifies how many bytes are in the data.

Data Field

0-64 Contains 0 to 8 bytes of the actual data.

CRC

15 Cyclic Redundancy Check”. This is used for detection and correction of errors. This will be explained in the future.
CRC Delimiter 1 Marks end of CRC.

ACK Slot

1 Acknowledgement slot”. Receivers set this bit to tell the transmitter they received a message.
ACK Delimiter 1 Marks end of ACK.
EOF 7 End of Frame”. You can guess what this does.
IFS 3 Inter-frame spacing”. This isn’t a part of the frame. This is the minimum distance between two CAN frames.

This is the structure of the extended CAN packet:

Explanation of each field in an extended CAN 2.0B frame
Field Bits Purpose
SOF 1 Start of Frame”. You can guess what this does.

Identifier A

11 The first 11 bits of the CAN ID.
SRR 1 Substitute Remote Request”. This is set to recessive. I will explain why in .
IDE 1 Identifier Extension Bit”. This declares we are using an extended frame. This must be set to recessive (1).

Identifier B

18 Contains the 18-bit ID extension.

RTR

1 Remote Transmission Request”. This specifies the type of frame the message uses.
r1 1 Reserved.
r0 1 Reserved.
DLC 4 Data Length Code”. Specifies how many bytes are in the data.

Data Field

0-64 Contains 0 to 8 bytes of the actual data.

CRC

15 Cyclic Redundancy Check”. This is used for detection and correction of errors. This will be explained in the future.
CRC Delimiter 1 Marks end of CRC.

ACK Slot

1 Acknowledgement slot”. Receivers set this bit to tell the transmitter they received a message.
ACK Delimiter 1 Marks end of ACK.
EOF 7 End of Frame”. You can guess what this does.
IFS 3 Inter-frame spacing”. This isn’t a part of the frame. This is the minimum distance between two CAN frames.

Here is the CAN\_TxHeaderTypeDef structure from 📄stm32f4xx\_hal\_can.h. We can see some of the fields we are allowed to set in the packet header.

/**
  * @brief  CAN Tx message header structure definition
  */
typedef struct
{
/*!< Specifies the standard identifier.
  This parameter must be a number between Min_Data = 0 and Max_Data = 0x7FF. */
  uint32_t StdId;

/*!< Specifies the extended identifier.
  This parameter must be a number between Min_Data = 0 and Max_Data = 0x1FFFFFFF. */
  uint32_t ExtId;

/*!< Specifies the type of identifier for the message that will be transmitted.
  This parameter can be a value of @ref CAN_identifier_type */
  uint32_t IDE;

 /*!< Specifies the type of frame for the message that will be transmitted.
  This parameter can be a value of @ref CAN_remote_transmission_request */
  uint32_t RTR;

/*!< Specifies the length of the frame that will be transmitted.
  This parameter must be a number between Min_Data = 0 and Max_Data = 8. */
  uint32_t DLC;

/*!< Specifies whether the timestamp counter value captured on start
  of frame transmission, is sent in DATA6 and DATA7 replacing pData[6] and pData[7].
  @note: Time Triggered Communication Mode must be enabled.
  @note: DLC must be programmed as 8 bytes, in order these 2 bytes are sent.
  This parameter can be set to ENABLE or DISABLE. */
  FunctionalState TransmitGlobalTime;

} CAN_TxHeaderTypeDef;

Note I changed the spacing to make it fit on the page. Only the first line number is correct.

Understanding Raw CAN Hardware Setup

CAN Bus Hardware

The CAN bus is physically implemented using two wires, CAN high and CAN low, which carry the signals.

CAN Bus Termination

The CAN bus must be terminated with 120Ω resistors at both ends to prevent reflections that can cause data corruption. You will learn this in ECE 329 if you choose to take it. Without this the bus will be unreliable.

Terminology

Exercise: Why might we want to use high voltage as a logical 0? (There are many benefits.)

Baud Rate

The baud rate is the frequency at which the protocol operates. It is the number of bits transmitted per second. We use a baud rate of 115200.

CAN Bus Terminals

CAN Message Arbitration and Acknowledgement

We need to determine the priority of transmission when multiple devices are in a network. We take advantage of the fact that every message begins with an ID. Every device can see the current state of the bus (high or low voltage). They are also synchronized. Note that when a dominant bit and recessive bit are sent at the same time, the dominant bit (high voltage, logical 0) wins out. Therefore, when transmitting, all devices that send attempt transmitting the packet, starting with the ID. They also all observe the bus. When they try to send a recessive bit (low voltage, logical 1) but notice a dominant bit (high voltage, logical 1) is on the bus, they stop and wait before retransmitting. The effect is that lower indexed IDs have priority. It also turns out that standard CAN frames naturally have priority over extended CAN frames. Exercise: try to reason this out by yourself. Here is a handy diagram:

Bit(s) 1 2-12 13 14
Standard SOF ID RTR IDE EOF
Extended SOF ID A SRR IDE EOF

Hint: In a standard frame, RTR can be recessive or dominant. The same bit in the extended frame is always . The 14th bit in each frame is both the IDE, so the frame always wins out by sending a bit, causing the other one to stop and wait.

Read https://en.wikipedia.org/wiki/CAN_bus#Data_transmission_and_arbitration for additional information. Notice that there is an ACK bit set aside for acknowledgement. This is used by the receiver to tell the transmitter it has successfully gotten an uncorrupted message. Notice its location in the packet:

Simplified CAN Frame Diagram
SOF ID RTR Data CRC ACK EOF

It is after receiving both the data and CRC. When the receiver verifies the data received is correct using the CRC, it sends a dominant signal into the wire for a length of time corresponding to one bit. This tells the transmitter the receiver has seen the correct message.

Standard vs Extended IDs

We will not be using the extended bits.

Data vs Remote Frames

Understanding the CAN Tutorial Program

Let’s examine the implementation file 📄can\_raw\_transmit.cc to understand how raw CAN frames are transmitted.

Include Statements and Copyright

/****************************************************************************
 *                                                                          *
 *  Copyright (C) 2025 RoboMaster.                                          *
 *  Illini RoboMaster @ University of Illinois at Urbana-Champaign          *
 *                                                                          *
 *  This program is free software: you can redistribute it and/or modify    *
 *  it under the terms of the GNU General Public License as published by    *
 *  the Free Software Foundation, either version 3 of the License, or       *
 *  (at your option) any later version.                                     *
 *                                                                          *
 *  This program is distributed in the hope that it will be useful,         *
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of          *
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the           *
 *  GNU General Public License for more details.                            *
 *                                                                          *
 *  You should have received a copy of the GNU General Public License       *
 *  along with this program. If not, see <http://www.gnu.org/licenses/>.    *
 *                                                                          *
 ****************************************************************************/

#include "bsp_print.h"
#include "cmsis_os.h"
#include "main.h"
#include "can.h"

The file starts with a copyright notice. You should be familiar with this from the previous tutorial. The includes bring in the necessary libraries for CAN communication on STM32:

Raw CAN Frame Transmission Function

/**
 * @brief Send a raw CAN frame using HAL directly
 * @param can_handle Pointer to CAN handle (e.g., &hcan1)
 * @param id CAN ID (11-bit standard ID)
 * @param data Data buffer to send (up to 8 bytes)
 * @param length Data length (0-8 bytes)
 * @return HAL status (HAL_OK if successful)
 */
HAL_StatusTypeDef SendRawCANFrame(CAN_HandleTypeDef* can_handle,
                                  uint32_t id, uint8_t* data, uint8_t length) {

This function definition and the docstring above shows the signature for sending a raw CAN frame using the STM32 HAL (Hardware Abstraction Layer). (Note I split the last lines into two lines in order to fit it.) After reading the previous tutorial, try to identify what each part means by yourself. Exercise: We never included . Why can we still use uint32\_t and uint8\_t?

Parameter Validation

  // Validate parameters
  if (length > 8) {
    print("Error: CAN data length exceeds 8 bytes\r\n");
    return HAL_ERROR;
  }

  if (id > 0x7FF) {
    print("Error: Standard CAN ID exceeds 11-bit range (0x7FF)\r\n");
    return HAL_ERROR;
  }

The function first validates the input parameters to ensure they comply with CAN protocol specifications:

This validation prevents protocol violations and potential communication failures.

Printing?

Have you noticed a problem? We just used print. But we are on an STM32, which is an embedded system usually with no screens attached! What are we even printing to? Where does print even come from?

Notice when we introduced , we mentioned it is for printing and debugging. We are actually outputting bytes into another serial port using a standard protocol called UART, so that we can plug in our computer at the other end to debug. Indeed, let us look at the actual definition of print in 📄shared/bsp/bsp\_print.cc:

int32_t print(const char* format, ...) {
#ifdef NDEBUG
  UNUSED(format);
  UNUSED(print_buffer);
  return 0;
#else   // == #ifdef DEBUG
  va_list args;
  int length;

  va_start(args, format);
  length = vsnprintf(print_buffer, MAX_PRINT_LEN, format, args);
  va_end(args);

  if (print_uart)
    return print_uart->Write((uint8_t*)print_buffer, length);
  else if (print_usb)
    return print_usb->Write((uint8_t*)print_buffer, length);
  else
    return 0;
#endif  // #ifdef NDEBUG
}

It is writing to UART! We will cover UART in a future tutorial.

CAN Transmission Header Configuration

(I de-indented the comment on line 49 up so you can see the whole thing.)

  // Configure CAN transmission header
  CAN_TxHeaderTypeDef tx_header;
  tx_header.StdId = id;                    // Standard ID (11-bit)
  tx_header.ExtId = 0x0;          // Extended ID (not used for standard frames)
  tx_header.IDE = CAN_ID_STD;              // Use standard ID format
  tx_header.RTR = CAN_RTR_DATA;            // Data frame (not remote frame)
  tx_header.DLC = length;                  // Data length code
  tx_header.TransmitGlobalTime = DISABLE;  // Don't include timestamp

Here we configure the CAN transmission header structure with the following parameters:

Sending the CAN Message

(Note I had to split line 56 to fit it.)

  uint32_t tx_mailbox;
  HAL_StatusTypeDef status = HAL_CAN_AddTxMessage(can_handle, &tx_header, data,
                                                  &tx_mailbox);

  if (status != HAL_OK) {
    print("Error: Failed to add CAN message to TX mailbox, status: %d\r\n", status);
    return status;
  }

The HAL\_CAN\_AddTxMessage function adds the message to one of the CAN hardware mailboxes for transmission. Let’s look at its implementation in (the header doesn’t contain the docstring I want to show you) (PPS I split line 1250 to fit it):

/**
  * @brief  Add a message to the first free Tx mailbox and activate the
  *         corresponding transmission request.
  * @param  hcan pointer to a CAN_HandleTypeDef structure that contains
  *         the configuration information for the specified CAN.
  * @param  pHeader pointer to a CAN_TxHeaderTypeDef structure.
  * @param  aData array containing the payload of the Tx frame.
  * @param  pTxMailbox pointer to a variable where the function will return
  *         the TxMailbox used to store the Tx message.
  *         This parameter can be a value of @arg CAN_Tx_Mailboxes.
  * @retval HAL status
  */
HAL_StatusTypeDef HAL_CAN_AddTxMessage(CAN_HandleTypeDef *hcan,
  CAN_TxHeaderTypeDef *pHeader, uint8_t aData[], uint32_t *pTxMailbox)

Then we know:

This function returns a status indicating success or failure, which we check and respond to appropriately.

Exercise: What played the role of tx\_mailbox in our previous tutorial about Linux CAN? Hint: was it ftx or frx?

Waiting for Transmission Completion

  // Wait for transmission to complete
  while (HAL_CAN_IsTxMessagePending(can_handle, tx_mailbox)) {
    osDelay(1); // Small delay to prevent busy waiting
  }

After queuing the message for transmission, we check if it’s still pending in the mailbox:

This ensures the message is fully transmitted before the function returns.

Transmission Confirmation

  print("CAN Frame sent successfully - ID: 0x%03X, Length: %d, Mailbox: %d\r\n",
        (unsigned int)id, length, (unsigned int)tx_mailbox);

  return HAL_OK;

Finally, we print a confirmation message showing the transmitted ID, data length, and mailbox used, then return HAL\_OK to indicate success.

CAN Initialization Function

/**
 * @brief Initialize CAN and start the peripheral
 */
void InitializeRawCAN() {
  // Initialize CAN1
  MX_CAN1_Init();

  // Start CAN peripheral
  if (HAL_CAN_Start(&hcan1) != HAL_OK) {
    print("Error: Failed to start CAN1\r\n");
    Error_Handler();
  }

  print("CAN1 initialized and started successfully\r\n");
  print("Ready to send raw CAN frames on hcan1\r\n");
}

Hardware Initialization

The initialization process involves two steps:

If the start operation fails, an error is printed and the error handler is called to manage the fault condition.

RTOS Init Function

void RM_RTOS_Init() {
  print_use_uart(&huart1);
  InitializeRawCAN();
}

This is the init function defined in . It is run before the main loop and is used for setup and initialization. Notice the functions we are calling inside of it.

Default Task Implementation

void RM_RTOS_Default_Task(const void* args) {
  UNUSED(args);

  print("Raw CAN Transmission Example\r\n");
  print("Sending various CAN frames every 2 seconds...\r\n");

  uint32_t counter = 0;

  while (true) {
    // Example 1: Send motor command (similar to DJI motor protocol)
    uint8_t motor_data[8] = {0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
    SendRawCANFrame(&hcan1, 0x200, motor_data, 8);


    osDelay(2);


    print("=== Cycle %lu completed ===\r\n", counter);
  }
}

Main Task Loop

The main task implements an infinite loop (the “main loop”) that:

Concepts Covered

We went over the following:

CAN Protocol

STM32 HAL CAN

Embedded Programming

Conclusion

This tutorial introduced raw CAN transmission on an STM32 embedded board. We covered the basics of CAN protocol, hardware setup, and a very simple usage of it to send a CAN packet (if you were here in person, we made a motor spin using this!). Next time I will go over the basics of motors and maybe CRC. Same as last time: if there are any concepts I have failed to cover, or you do not completely understand, search it up and ask an LLM.