2025-10-18
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:
CAN protocol fundamentals
Hardware Abstraction Layer (HAL) for CAN communication
Raw CAN frame transmission
STM32 microcontroller CAN implementation
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
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.
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:
| 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:
| 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.
The CAN bus is physically implemented using two wires, CAN high and CAN low, which carry the signals.
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.
CAN-H: High signal line (around the voltage of VCC)
CAN-L: Low signal line (around the voltage of GND)
Twisted Pair: The lines are twisted together to improve electromagnetic performance.
Differential Signaling: Use the voltage difference between CAN-H and CAN-L, not CAN-H and GND.
Recessive: A low voltage bit. This represents a logical 1.
Dominant: A high voltage bit. This represents a logical 0.
Exercise: Why might we want to use high voltage as a logical 0? (There are many benefits.)
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.
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:
| 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 ID: 11-bit identifier (0x000
to 0x7FF),
used in CAN 2.0A (standard) and CAN 2.0B (extended).
Extended ID: 29-bit identifier (0x00000000
to0x1FFFFFFF),
used in CAN 2.0B.
Same arbitration mechanism!
We will not be using the extended bits.
Data Frame: Contains actual data payload (RTR bit is set to 0).
Remote Frame: Often used to request data from another node (RTR bit is set to 1, no data field).
Let’s examine the implementation file 📄can\_raw\_transmit.cc
to understand how raw CAN frames are transmitted.
/****************************************************************************
* *
* 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:
: Board support package for printing/debugging.
: CMSIS Real-Time Operating System API.
: Board \#defines
and basic functionality is in there. We simply write i.e. the main loop
in this file.
: CAN.
/**
* @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?
// 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:
CAN data length cannot exceed 8 bytes (per CAN 2.0 specification)
Standard CAN ID must fit in 11 bits (0x000 to 0x7FF)
This validation prevents protocol violations and potential communication failures.
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.
(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 timestampHere we configure the CAN transmission header structure with the following parameters:
StdId:
The 11-bit standard CAN identifier (0x000
to 0x7FF).
ExtId:
Extended identifier (ignored as we are using standard frames).
IDE:
Identifier extension bit. Set to CAN\_ID\_STD
(alias for 0x00000000U
- remember what this means?) for standard format (CAN 2.0A).
RTR:
Remote transmission request. Set to CAN\_RTR\_DATA
(also 0x00000000U)
for data frames.
DLC:
Data length code. This is the number of data bytes to send
(0-8).
TransmitGlobalTime:
Whether to include global time stamp in transmission (used for
synchronization, disabled).
(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:
can\_handle:
The CAN device handle.
tx\_header:
Reference to the configured transmission header.
data:
The data buffer to send.
tx\_mailbox:
Output parameter that receives which mailbox is to be used.
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?
// 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:
HAL\_CAN\_IsTxMessagePending():
Returns whether transmission is still in progress.
osDelay(1):
A small delay (measured in “ticks”).
This ensures the message is fully transmitted before the function returns.
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.
/**
* @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");
}The initialization process involves two steps:
MX\_CAN1\_Init:
Generated by STM32CubeMX to configure the CAN1 peripheral
settings.
HAL\_CAN\_Start:
Starts the CAN hardware and enables communication
If the start operation fails, an error is printed and the error handler is called to manage the fault condition.
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.
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);
}
}The main task implements an infinite loop (the “main loop”) that:
Sends a CAN frame with ID 0x200
containing motor control data.
Waits between transmissions.
Prints a counter to track completed cycles.
We went over the following:
CAN frame structure and transmission
Bus termination and wiring requirements
Message arbitration based on CAN ID
Acknowledgement
Standard (2.0A) vs extended (2.0B) frame formats
CAN hardware abstraction layer usage
CAN peripheral initialization
Transaction header configuration
Mailbox-based message transmission
RTOS functions
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.