Arduino Uno + STM32 Bluepill + STM32 Blackpill CAN Communication

by alaminashik in Circuits > Microcontrollers

29 Views, 0 Favorites, 0 Comments

Arduino Uno + STM32 Bluepill + STM32 Blackpill CAN Communication

Cover.jpg

CAN (Controller Area Network) communication is a reliable and efficient method of data exchange between electronic devices and microcontrollers. It has become very popular because of its ease of use, less number of wires. robustness and many more.

In this article, I will describe in details the use of this communication protocol using three different development boards, where one board will act as a master and the other two are slaves.

The master will periodically ask for data from the slaves, and they will respond with data to the master. Interrupts are used to ensure stability of the communication.

Note: Using MCP2515 CAN module with STM microcontrollers requires re-routing of wires on the PCB, this must be done carefully! (Step 4 and Step 5)

Supplies

List of things I used:

  1. 1 x Arduino Uno R3 dev board LINK
  2. 1 x STM32F103 Bluepill dev board LINK
  3. 1 x STM32F411 Blackpill dev board LINK
  4. 3 x MCP2515 CAN controller & Transceiver Module LINK
  5. 16 x Female-to-Female Jumper Wires LINK
  6. 7 x Male-to-Female Jumper Wires LINK
  7. 4 x Male-to-Male Jumper Wires LINK
  8. USB cables to connect dev boards to computer
  9. Soldering Iron (LINK), lead (LINK), and expertise!

Theory

can-voltage-levels.jpg
CAN-dataframe.jpg

This is one of the method for communicating between MCU's in a circuit. This is commonly used in vehicles and acts as the nervous system that connects the ECU (electronic control unit) in the vehicle.

Core concept:

  1. The communication of this protocol uses two wires: CANH and CANL.
  2. The standard voltage level of this CAN protocol is strictly 5V.
  3. It uses differential signalling:
  4. Recessive bit (logic 1) → both lines ≈ 2.5 V (no difference).
  5. Dominant bit (logic 0) → CAN_H ≈ 3.5 V, CAN_L ≈ 1.5 V (difference ≈ 2 V).
  6. When no data is being transferred using the bus wires, they both remain at 2.5V as shown on the diagram.
  7. When an MCU enquires data from the bus, both CANH and CANL becomes dominant bit, this triggers the start of frame.
  8. The MCP2515 is a CAN controller IC which communicates via SPI with the microcontrollers.

Data:

  1. There are varieties of CAN communications, but for our MCP2515 it can only send up to 8 bytes of data at ones.
  2. The data frame also contains, the unique address, Data length and acknowledge field.
  3. For sending higher bytes of data, several packets can be made and sent out sequentially.


To learn more about CAN protocol, check out these links:

https://www.picotech.com/library/knowledge-bases/oscilloscopes/can-bus-serial-protocol-decoding

https://medium.com/@sjindhirapooja/can-standard-data-frame-format-846b8f9fc749

Circuit Diagram

Circuit diagram CAN communication_bb.png

This is the complete circuit diagram of the setup.

All the CAN modules are connected via the SPI connection of the microcontrollers. Check if the correct wires are connected, as the pin configuration differs for all the development boards.

Make sure the two-pin jumper caps at the two end modules (Arduino and Black Pill) are connected, which shorts the communication lines parallel to an on-board 120 ohm resistor. This is to prevent signal reflections by matching the cable's impedance, which ensures stable and reliable communication.

An individual close-up of the circuit is written in the following steps.

Note: The interrupt (INT) pin is essential for proper data communication. It basically alerts the sender/receiver that someone wants to send/receive data.

Power supply can be provided from the same USB hub or computer; it will not interfere with the signal transmission.

Part 1: Arduino Uno (Master) & Code

Circuit diagram CAN communication_1.jpg

Arduino Uno contains one default SPI, which can be directly connected to the module.

The connections are:

//MCP2515 //Arduino UNO
VCC 5V
GND GND
CS 10
MISO 12
MOSI 11
SCK 13
INT 2


Coding

The following code is uploaded to Arduino UNO, and it works as follows:

  1. Initializing the libraries, variables, chip select (CS), and interrupt (INT) pins.
  2. The code under void setup() starts communication with the computer, with the module, and sets the interrupt pin as input (this can also be used as an interrupt, which is done for the slaves)
  3. The code under void askSlave() basically sends a single byte with the slave address to trigger a particular slave to send its data.
  4. The code under responseFromSlave() basically reads the data sent on the CAN bus


/*********************************************************************************************************
START FILE
*********************************************************************************************************/

#include <mcp_can.h> //add the can library
#include <SPI.h> //add SPI communication

long unsigned int rxId; //varibale to store the slave address when responded
unsigned char len = 0; //varibale to store the length of data sent by slave
unsigned char rxBuf[7]; //varibale to store the data bytes send by the slaves

char msgString[128]; // Array to store serial string
int receivedIndex = 0; // Current write index


#define CAN0_INT 2 // Set INT to pin D2
MCP_CAN CAN0(10); // Set CS to pin D10


void askSlave(uint16_t Addresss){
byte sndStat1 = CAN0.sendMsgBuf(Addresss, 0, 1, (byte*)1); //request with address through the bus //len should be equal to data

if (sndStat1 == CAN_OK) {
Serial.println("Message Sent!"); //message sent without any error (line is OK)
} else {
Serial.println("Error Sending Frames..."); //if there is something wrong with can bus
}
}

void responseFromSlave(){
//when any particular slave responds
CAN0.readMsgBuf(&rxId, &len, rxBuf); // Read data: len = data length, buf = data byte(s)

sprintf(msgString, "Standard ID: 0x%.3lX length: %d ", rxId, len);
Serial.print(msgString); //print ID and length of data
if((rxId & 0x40000000) == 0x40000000){ // Determine if message is a remote request frame.
sprintf(msgString, " REMOTE REQUEST FRAME");
Serial.print(msgString);
}
else if(rxId == 0x100){ // Check if the message is from the slave with address 0x100
for(int i = 0; i<len; i++){
msgString[receivedIndex++] = (byte)rxBuf[i]; // Store received data in msgString
}
msgString[receivedIndex] = '\0'; // Null-terminate the string
Serial.print("Data: "); // Print the received data string

for (int i = 0; i < len; i++) {
Serial.print((char)rxBuf[i]); // Print each byte of data by converting it to char
}
Serial.println(); // Print a newline after the data
}
else{
for(byte i = 0; i<len; i++){
sprintf(msgString, " 0x%.2X", rxBuf[i]);
Serial.print(msgString); //print the data in bytes
}
}
}


void setup()
{
Serial.begin(115200);
// Initialize MCP2515 running at 16MHz with a baudrate of 500kb/s and the masks and filters disabled.
if(CAN0.begin(MCP_ANY, CAN_500KBPS, MCP_16MHZ) == CAN_OK)
Serial.println("MCP2515 Initialized Successfully!");
else
Serial.println("Error Initializing MCP2515...");
CAN0.setMode(MCP_NORMAL); // Set operation mode to normal so the MCP2515 sends acks to received data.

pinMode(CAN0_INT, INPUT); // Configuring pin for /INT input //this is low when the line is active (someone is seeking data)
}

void loop()
{
Serial.println("Asking First Slave.");
askSlave(0x100); //asking from first slave with address 0x100
delay(10); //important otherwise data will propagate by one step.
if(!digitalRead(CAN0_INT)) // If CAN0_INT pin is low, read receive buffer
{
Serial.println("Slave responded");
responseFromSlave();
}
else Serial.println("Slave did not respond.");
delay(2000); // wait for 2 seconds before asking again
Serial.println();


Serial.println("Asking second Slave.");
askSlave(0x200); //asking from second slave with address 0x200
delay(10);
if(!digitalRead(CAN0_INT)) // If CAN0_INT pin is low, read receive buffer
{
Serial.println("Slave responded");
responseFromSlave();
}
else Serial.println("Slave did not respond.");
delay(2000); // wait for 2 seconds before asking again
Serial.println();



Serial.println("Asking Third Slave.");
askSlave(0x300); //asking from third slave with address 0x300
delay(10);
if(!digitalRead(CAN0_INT)) // If CAN0_INT pin is low, read receive buffer
{
Serial.println("Slave responded");
responseFromSlave();
}
else Serial.println("Slave did not respond.");
delay(2000);
Serial.println();
//Repeat
}


/*********************************************************************************************************
END FILE
*********************************************************************************************************/

Part 2: STM32 Bluepill (Slave) & Code

Circuit diagram CAN communication_2.jpg
20250727_173444.jpg
20250727_173452.jpg

The STM32F103C8T6 Blue Pill contains two SPIs, and one of which is connected to the CAN module.

The standard CAN communication works at 5V, but the pins of the blue pill are 3.3V-tolerant, so we need to make some adjustments (given in step 8) to power the MCP2515 by 3.3V and the TJA1050 by 5V.

The connections are:

//MCP2515 //Blue-pill
*PWR 5V
VCC 3.3V
GND GND
CS PA4
MISO PA6
MOSI PA7
SCK PA5
INT PA2



Coding

If you don't know how to upload code to an STM board, check out this link from pcb-hero to learn more.


The following code is uploaded to the bluepill board, and it works as follows:

  1. Initializing the libraries, variables, chip select (CS), and interrupt (INT) pins.
  2. The code under void setup() starts the CAN communication, and attaches the interrupt pin to call the canInterrupt function every time this pin is physically triggered(by master).
  3. The code under canInterrupt basically reads the data and address sent by master. It then compares its address and if its a match, it will sent its data.
/*********************************************************************************************************
START FILE
*********************************************************************************************************/

#include <mcp_can.h>
#include <SPI.h>

uint16_t myAddress = 0x100;

long unsigned int rxId;
unsigned char len = 0;
unsigned char rxBuf[7];

MCP_CAN CAN0(PA4); // Set CS to pin PA4
#define CAN0_INT PA2 // Set INT to pin PA8

const char myString[] = "Hi"; // 2 bytes of data

// run when interrupt is triggered
void canInterrupt()
{
Serial.println("CAN Interrupt Triggered!");
if (!digitalRead(CAN0_INT)) // If CAN0_INT pin is low, read receive buffer
{
Serial.print("Master is asking data ");
CAN0.readMsgBuf(&rxId, &len, rxBuf); // Read data: len = data length, buf = data byte(s)
if (rxId == myAddress)
{
Serial.println("from Me!!");

// send only button state (0 or 1)///
byte sndStat1 = CAN0.sendMsgBuf(myAddress, 0, 2, (byte*)&myString); //address, data length, data pointer
Serial.println("I sent data with my address 0x100");
if (sndStat1 == CAN_OK) {
Serial.println("Message Sent properly!");
} else {
Serial.println("Error Sending Frames...");
}
}

else
Serial.println("from someone else!");
}
}

void setup()
{
Serial.begin(115200);

// Initialize MCP2515 running at 16MHz with a baudrate of 500kb/s and the masks and filters disabled.
if (CAN0.begin(MCP_ANY, CAN_500KBPS, MCP_16MHZ) == CAN_OK)
Serial.println("MCP2515 Initialized Successfully!");
else
Serial.println("Error Initializing MCP2515...");

CAN0.setMode(MCP_NORMAL); // Change to normal mode to allow messages to be transmitted
attachInterrupt(digitalPinToInterrupt(CAN0_INT), canInterrupt, FALLING); // Attach interrupt to CAN0_INT pin
// pinMode(CAN0_INT, INPUT); // Configuring pin for /INT input

}

void loop()
{
delay(10);
}

/*********************************************************************************************************
END FILE
*********************************************************************************************************/

Part 3: STM32 Blackpill (Slave) & Code

Circuit diagram CAN communication_3.jpg
20250727_173444.jpg
20250727_173452.jpg

The STM32F401 Black Pill has three SPIs, and we will use the first one only.

The standard CAN communication works at 5V, but the pins of the black pill are 3.3V-tolerant, so we need to make some adjustments (given in step 8) to power the MCP2515 by 3.3V and the TJA1050 by 5V.


The connections are:

//MCP2515 //Black-pill
*PWR 5V
VCC 3.3V
GND GND
CS PA4
MISO PA6
MOSI PA7
SCK PA5
INT PA2


Coding

If you don't know how to upload code to an STM board, check out this link from pcb-hero to learn more.


The following code is uploaded to the blackpill board, and it works as follows:

  1. Initializing the libraries, variables, chip select (CS), and interrupt (INT) pins.
  2. The code under void setup() starts the CAN communication, and attaches the interrupt pin to call the canInterrupt function every time this pin is physically triggered(by the master).
  3. The code under canInterrupt basically reads the data and address sent by the master. It then compares its address, and if it's a match, it will send its data.
/*********************************************************************************************************
START FILE
*********************************************************************************************************/

#include <mcp_can.h>
#include <SPI.h>

uint16_t myAddress = 0x100;

long unsigned int rxId;
unsigned char len = 0;
unsigned char rxBuf[7];

MCP_CAN CAN0(PA4); // Set CS to pin PA4
#define CAN0_INT PA2 // Set INT to pin PA2

const char myString[] = "Hi"; // 2 bytes of data

// run when interrupt is triggered
void canInterrupt()
{
Serial.println("CAN Interrupt Triggered!");
if (!digitalRead(CAN0_INT)) // If CAN0_INT pin is low, read receive buffer
{
Serial.print("Master is asking data ");
CAN0.readMsgBuf(&rxId, &len, rxBuf); // Read data: len = data length, buf = data byte(s)
if (rxId == myAddress)
{
Serial.println("from Me!!");

// send only button state (0 or 1)///
byte sndStat1 = CAN0.sendMsgBuf(myAddress, 0, 2, (byte*)&myString); //address, data length, data pointer
Serial.println("I sent data with my address 0x100");
if (sndStat1 == CAN_OK) {
Serial.println("Message Sent properly!");
} else {
Serial.println("Error Sending Frames...");
}
}

else
Serial.println("from someone else!");
}
}

void setup()
{
Serial.begin(115200);

// Initialize MCP2515 running at 16MHz with a baudrate of 500kb/s and the masks and filters disabled.
if (CAN0.begin(MCP_ANY, CAN_500KBPS, MCP_16MHZ) == CAN_OK)
Serial.println("MCP2515 Initialized Successfully!");
else
Serial.println("Error Initializing MCP2515...");

CAN0.setMode(MCP_NORMAL); // Change to normal mode to allow messages to be transmitted
attachInterrupt(digitalPinToInterrupt(CAN0_INT), canInterrupt, FALLING); // Attach interrupt to CAN0_INT pin
// pinMode(CAN0_INT, INPUT); // Configuring pin for /INT input

}

void loop()
{
delay(10);
}

/*********************************************************************************************************
END FILE
*********************************************************************************************************/

Output From Master

master_serialprint_data.png

The master keeps asking the slaves in sequence for data. This output shows the serial monitor only one slave (black pill) connected, and so only one slave is responding.

This can be done for multiple slaves.

Refer to the code above and match the output.

Output From Slaves

slave_serialprint_data.png

The master is asking 3 different slaves in sequence. This shows the output serial monitor of one of the slaves (Blue Pill). Whenever the asking address matches the slave's address, it will respond with a data "hi" and address. Otherwise, it ignores by saying "Master is asking data from someone else".

This is the same for the black pill as well. This ensures successful communication between the devices.

Troubleshot & Power Management

20250727_173444.jpg
20250727_173452.jpg
  1. Ensure both ends of the communication bus are connected to 120 ohm resistors. These resistors are already present on the CAN module, you just have to short the 2 male header pins.
  2. One important thing is that the standard CAN communication uses 5V to operate, and each bus holds 2.5V while not communicating. Thus, we need to modify the MCS2515 CAN board for the STM microcontrollers.
  3. As shown in the picture above, cut the wire that powers the TJA1050 IC with 3.3V, and connect it to the 5V of the microcontroller.
  4. As shown in the second picture, the red silicon wire is powering the TJA1050 by 5V.

and Done! cheers!