Generally I have found library support for the Arduino environment to be excellent. I’ve used a lot of different available libraries to support everything from sensors to networking protocols. I am currently using the ESP32 built-in CAN Bus support (known as the Two-wire Automotive Interface(TWAI)) for a distributed hardware system being developed for virtual pipe organs. My last post found a small bug in the Arduino CAN Bus library by Sandeep Mistry when setting the bit rate. Different ESP32 revisions ran at different rates because a configuration register previous reserve bit was being set. Recently I have found another bug when attempting to configure the extended ID acceptance filter.
The acceptance filter is used to only pass wanted received CAN Bus messages. Only accepted messages are stored in the receive FIFO. Acceptance filters are based on an CAN Bus ID and a mask. Filtering is ideal for reducing processor load by only passing desired CAN Bus messages.
The acceptance filter has two, 32-bit registers, acceptance code value and acceptance mask value. The code value specifies the ID bit pattern to match and the mask value allows the user to select the bits they want to match. The ESP32 Technical Manual Version 5.2 acceptance filter algorithm is shown below.
In the algorithm when a message bit and acceptance code bit matches then the XNOR output is true (1) and that bit is accepted. For bits the user wants to ignore the acceptance mask bit should be true (1) and that bit is accepted. For an extended CAN Bus ID, if all 29-bits are accepted, then the message is passed to the receive FIFO.
Previously with this library I used acceptance filtering on the 11-bit ID, which worked well. For this VPO system I’ve decided to used the CAN Bus extended 29-bit ID instead. For the Arduino CAN Bus library the method to setup the acceptance filtering is filterExtended() where the 29-bit ID and mask values are supplied. The Sandeep Mistry library version 0.3.1 filterExtended method is given below.
int ESP32SJA1000Class::filterExtended(long id, long mask)
{
id &= 0x1FFFFFFF;
mask &= ~(mask & 0x1FFFFFFF);
modifyRegister(REG_MOD, 0x17, 0x01); // reset
writeRegister(REG_ACRn(0), id >> 21);
writeRegister(REG_ACRn(1), id >> 13);
writeRegister(REG_ACRn(2), id >> 5);
writeRegister(REG_ACRn(3), id << 3);
writeRegister(REG_AMRn(0), mask >> 21);
writeRegister(REG_AMRn(1), mask >> 13);
writeRegister(REG_AMRn(2), mask >> 5);
writeRegister(REG_AMRn(3), (mask << 3) | 0x1f);
modifyRegister(REG_MOD, 0x17, 0x00); // normal
return 1;
}
The first issue is the forming of the 29-bit mask value. The mask value is AND’d with the inverted mask value resulting in a value of 0. This requires that all ID bits must matched for the message to be accepted. The mask value result should have 1’s in the bits you are not interested in. Changing the ‘&=’ to just ‘=’ solves this problem.
The second problem is writing the acceptance mask register(3). The current library puts 1’s in the lowest 5 bits. These five bits are:
Bit 0 – unused
Bit 1 – unused
Bit 2 – RTR
Bit 3 – ID0
Bit 4 – ID1
By setting the lowest 5 bits to 1, the acceptance filter is now ignoring ID[1:0] and RTR. The code should only set RTR = 1, leaving ID[1:0] to the inverted mask bits values, and always use 0 for unused register bits. The updated code is shown below.
int ESP32SJA1000Class::filterExtended(long id, long mask)
{
id &= 0x1FFFFFFF;
mask = ~(mask & 0x1FFFFFFF);
modifyRegister(REG_MOD, 0x17, 0x01); // reset
writeRegister(REG_ACRn(0), id >> 21);
writeRegister(REG_ACRn(1), id >> 13);
writeRegister(REG_ACRn(2), id >> 5);
writeRegister(REG_ACRn(3), id << 3);
writeRegister(REG_AMRn(0), mask >> 21);
writeRegister(REG_AMRn(1), mask >> 13);
writeRegister(REG_AMRn(2), mask >> 5);
writeRegister(REG_AMRn(3), (mask << 3) | 0x04);
modifyRegister(REG_MOD, 0x17, 0x00); // normal
return 1;
}
Overall this library works well for my application. There have been just a few surprises along the way. With these changes I was successful in filtering extended ID CAN Bus messages.
I am building an ESP32 based system that uses CAN bus communication to transfer data between processors. The ESP32 firmware is being developed using the Arduino programming environment. My plan is to purchase ESP32 modules and plug them on interface boards that are interconnected using the CAN bus. The maximum system is about 16 processors.
Controller Area Network (CAN) bus was officially released in 1986 at the Society of Automotive Engineers conference. It was designed as an in-vehicle communications backbone between engine control unit processor, sensors, actuators, indicators, displays, and other vehicle components. CAN bus defines the OSI network model physical and data link layers.
The physical network is a multi-drop bus that uses a single twisted pair wire with approximately one twist per inch and 120 ohm terminations at the ends. The Digikey blog CAN bus image below shows four CAN bus nodes (controller and transceiver), cabling, and terminations.
The CAN bus 2.0 has two frame versions, standard 2.0A and extended 2.0B. The difference between these frame types is the arbitration field with the length of the identifier (11-bits vs. 29-bits) and the control field. The picture from Typhoon HIL Documentation shows the two frame formats.
CAN bus operates on a decentralized networking principle where all nodes are equal. Any node can transmit a data frame when the bus is free. Each frame contains identification information and supports variable data lengths from 0 up to 8 bytes. CAN bus supports a multitude of bits rates from 25kb/s to 1000kb/s and up to 5.0Mb/s for CAN FD. Because it is a decentralized network, there exists bus arbitration and robust error detection used to create a reliable network.
I selected the ESP32 because it has a built in CAN bus 2.0A/B controller (called two-wire automotive interface (TWAI)) that supports bit rates from 12.5kb/s to 1000kb/s and there are available Arduino libraries. The ESP32 does not support CAN FD protocol. The lowest rates 12.5kb/s to 20kb/s are only available on the ESP32 revision 2 or later. To use the ESP32 you must add a CAN bus physical layer transceiver interface.
I setup a ESP32 CAN bus test with two devices. This simple setup should have been straight forward. There are examples with the Sandeep Mistry Arduino CAN bus library (version 0.3.1) I am using and multiple examples on the Internet. I ran into two problems using this library. The first was solved quickly while the second took some digging.
The first problem was with an include file. I’m using the latest Arduino IDE (2.3.2) with ESP32 support. I got a complier error stating that “esp_intr.h” could not be found. A quick search showed a change was needed in the ESP32SJA1000.cpp file changing the reference to #include “esp_intr_alloc.h.” This allowed the code to compile.
// Copyright (c) Sandeep Mistry. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
#ifdef ARDUINO_ARCH_ESP32
//#include "esp_intr.h"
#include "soc/dport_reg.h"
#include "driver/gpio.h"
#include "esp_intr_alloc.h"
#include <rom/gpio.h>
#include "ESP32SJA1000.h"
In my test I had two ESP32 connected via CAN bus transceivers with the correct terminations. The two devices would not communicate even after testing several different bit rates. I used two different manufacturer’s modules, purchased at different times so I tried two devices from the same manufacturer’s and that worked! Then I tried the other manufacturer’s modules and that worked! But the two different manufacturers modules would not work together.
Finally I hooked up my oscilloscope and measured the bit times. I configured the system to run at 1Mb/s. On the newer ESP32 modules the bit rate was only 0.5Mb/s. The bit rate was correct on the older ESP32 modules.
Espressif Systems in newer ESP32 revisions support CAN bus bit rates less than 25kb/s. This update has caused an issue with the Sandeep Mistry Arduino CAN bus library (and I suspect other libraries as well). The library writes all bits to the REG_IER register that in newer ESP32 revisions has a bit that enables a divide by 2 CAN bit rate. The fix is to modify the REG_IER bit for ESP32 revisions 2 or greater. The modification is needed in the ESP32SJA1000.cpp begin() method after REG_IER write as shown below. After making these changes the CAN bus is now interoperable between different ESP revisions and manufacturer’s modules.
modifyRegister(REG_BTR1, 0x80, 0x80); // SAM = 1
writeRegister(REG_IER, 0xff); // enable all interrupts
if (ESP.getChipRevision() >= 2) {
modifyRegister(REG_IER, 0x10, 0); // From rev2 used as "divide BRP by 2"
}
Besides learning about the CAN bus library capabilities I wanted to stress test the CAN bus to determine how well it will work in my application. The stress test uses four senders and one receiver (I only had 5 CAN bus transceivers for the test) to pass messages.
The receiver tracks each individual sender identified by the unique 11-bit ID and verifies all messages are received by checking a message counter. When an error occurs a serial message is printed and the new message counter is used.
// CAN Receiver Stress Test
// Standard message (11 bit ID)
// Variable time between messages and message length
// Constant ID and message counter to check for lost messages
// Multiple senders
#include <CAN.h>
#define MAX_SENDERS 16
struct messageCheck {
int id11;
uint8_t messageNum;
} receivedData[MAX_SENDERS];
uint8_t seqNum;
int id11;
void setup() {
Serial.begin(115200);
while (!Serial);
Serial.println("CAN Stress Receiver");
CAN.setPins(22,21);
// start the CAN bus at 1000 kbps
if (!CAN.begin(1000E3)) {
Serial.println("Starting CAN failed!");
while (1);
}
// voltage translator OE
pinMode(12, OUTPUT);
digitalWrite(12, HIGH); // has 10K pull down
// initialize test structure
for(int i = 0; i < MAX_SENDERS; i++) {
receivedData[i].id11 = 0;
receivedData[i].messageNum = 0;
}
}
void loop() {
// try to parse packet
int packetSize = CAN.parsePacket();
if (packetSize) {
id11 = CAN.packetId();
// first packet id in list
int i = 0;
while(receivedData[i].id11 != 0) {
if(receivedData[i].id11 == id11) {
break;
}
i++;
}
if(i < MAX_SENDERS) {
if(receivedData[i].id11 == 0) { // new to list
receivedData[i].id11 = id11;
receivedData[i].messageNum = CAN.read();
}
else { // id found
receivedData[i].messageNum++;
seqNum = CAN.read();
if(receivedData[i].messageNum != seqNum) { // error
receivedData[i].messageNum = seqNum;
Serial.print("Error ID "); Serial.println(receivedData[i].id11);
}
}
}
else { // some other error occured
Serial.println("i >= MAX_SENDERS Error");
}
}
}
Each sender transmits data at random times between 1 and 50ms and each frame has random number of bytes between 1 (message counter) and 8. The message counter, which is always present, is used to track message delivery by the receiver.
// CAN Sender Stress Test
// Standard message (11 bit ID)
// Variable time between messages and message length
// Constant ID and message counter to check for lost messages
#include <CAN.h>
#include "esp_mac.h"
int id11 = 0; // 11 bit ID, use MAC address
uint8_t messageNum = 0; // first byte of message data, increments over time for testing
uint8_t bytesRandom; // random number of additional bytes to send 0 to 7
long waitRandom; // random wait time in MS before sending next message
void setup() {
Serial.begin(115200);
while (!Serial);
Serial.println("CAN Sender Stress Test");
CAN.setPins(22,21);
// start the CAN bus at 500 kbps
if (!CAN.begin(1000E3)) {
Serial.println("Starting CAN failed!");
while (1);
}
// voltage translator OE
pinMode(12, OUTPUT);
digitalWrite(12, HIGH); // has 10K pull down
uint8_t mac[8];
esp_efuse_mac_get_default(mac);
for(int i = 0; i < 8; i++) {
id11 += mac[i];
}
id11 &= 0x07ff;
randomSeed(analogRead(39));
}
void loop() {
// wait random ms
waitRandom = random(1,50);
delay(waitRandom);
// send packet: id is 11 bits, packet can contain up to 8 bytes of data
Serial.print("Sending packet ID ... "); Serial.print(id11, HEX);
bytesRandom = (uint8_t)random(7); // random number of additional bytes
CAN.beginPacket(id11); // ID
CAN.write(messageNum++); // message count used to measure reliability during stress test
for(int i = 0; i < bytesRandom; i++) { // add random number of bytes
CAN.write((uint8_t)random());
}
CAN.endPacket();
Serial.println(" done");
}
Overall I’m pleased with CAN bus performance and reliability. I connected an oscilloscope and observe random message timing and message collision events. I can disconnect/reconnect/reset senders and the receiver properly detects a node message error. I have run the test for hours without any dropped messages at 1Mb/s bit rate on a very short bus (<2feet). Future testing includes adding more nodes and increasing the bus wire length.
Sometimes when working with an embedded system sending information to a serial port is useful. It can be used to provide system telemetry data, user feedback, or to aid with debugging a program. With most embedded development environments there is support for the old C standard printf(). Generally to use printf() a user needs to provide the putchar() routine that directs character output to your serial port. The STM32CubeIDE is no different and has printf() support. DigiKey provides a nice tutorial on the subject including enabling the floating point support.
There are only two steps needed to provide basic printf() support. First create a UART project instance in STM32CubeIDE. Use the Connectivity Category and select the UART you want to use. This assigns the processor GPIO pins for the TX and RX signals and creates a UART instance and handle. Under Parameter Settings set the desired basic settings such as baud rate, word length, parity, and stop bits.
The next step is to redirect printf() output to use the UART instance. First include the standard IO (stdio.h) header file. Based on your complier, the second part creates the interface to output a single character to the UART instance using the STM32 hardware abstraction layer (HAL) function. With the STM32 environment, you need the UART handle from the created UART instance (e.g., hlpuart1 in the sample code). The necessary code segment is shown below and is placed in the Private Function Prototypes (PFP) code area.
/* USER CODE BEGIN PFP */
#ifdef __GNUC__ #define PUTCHAR_PROTOTYPE int __io_putchar(int ch) #else #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f) #endif
One issue with using printf() is that the CPU is involved with moving each formatted character to the UART. The printf() function actually performs two activities. First a character string is created based on the formatting string and arguments in the printf() function call. The second activity is moving that character string to the UART one byte at a time.
One way to improve the printf() CPU I/O efficiency is create the output formatted character string but use DMA to move the character string bytes to the UART. Using a DMA channel eliminates the need for the CPU to move the individual character string bytes to the UART.
In the STM32 IDE when configuring the UART you can assign a DMA channel to the UART transmitter. For an out going message the DMA transfer is setup as memory to peripheral (transmit) with a byte data width.
When you are ready to send a message first use sprintf() to create the formatted character string in a memory buffer. Once the string has been created start the transmit DMA using the HAL DMA api call (HAL_UART_Transmit_DMA). A small code segment is shown below using sprintf() and the HAL DMA api.
/* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
// check if ready to output // flag set by timer interrupt, use DMA to transmit data if(outputState == XMITNOW) { sprintf(currentOutBuffer, "%llu, %lu, %08lX, %llu, %llu, %llu, %llu\n", ++outSequence, outputADCSums->pdCount, outputADCSums->error, outputADCSums->violet, outputADCSums->blue, outputADCSums->red, outputADCSums->green);
HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)currentOutBuffer, strlen(currentOutBuffer)); outputState = OUTPUT_IDLE; } } /* USER CODE END 3 */
The STM32 IDE programming environment and HAL api greatly simplifies using a DMA channel to transmit formatted character strings without involving the CPU. Hopefully this example gets you started using DMA for your data transfers.