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.
In the course of developing an ESP32 battery based application I was stumped along with others, on how to maintain a GPIO output in a known state while in deep sleep. My plan was to hold an external peripheral in a reset state to minimize it’s power. The reset signal was an active low signal and the external peripheral had a 10K pullup resistor on the signal. I program the ESP32 in the Arduino environment so I had to find a solution that supported this IDE. I performed a bunch of searches and finally found a working example by Xabi Alaez.
The ESP32 has several “sleep” modes that are listed below. A great article about ESP32 sleep modes is found on the Last Minute Engineers web page.
Active keeps everything running including WiFi, Bluetooth, and processing core at all times. This mode consumes more than 240mA with some power spikes as high as 790mA.
Modem Sleep is Active minus the WiFi, Bluetooth, and radio. Dependent on the clock speed, which is programmable, consumes between 3mA (low clock speed) to 20mA (high clock speed).
Light Sleep the CPU(s) clock is gated while the RTC and ULP co-processor are active. This sleep mode has full RAM retention. Once entering Light Sleep a wake-up source is needed to resume processing. This power consumption is less than 1mA.
Deep Sleep the CPU(s), most RAM, and all digital peripherals are powered off while the ULP co-processor and RTC components remain operational. The power consumption is about 0.15mA with the ULP co-processor running.
Hibernation only has one RTC timer running and a few RTC GPIOs needed to wake up the chip from hibernation mode. Power consumption is about 2.5uA.
I was most interested in Deep Sleep and Hibernation modes. It turns out you control the power domains before entering Deep Sleep to create Hibernation mode (no API call for hibernation sleep). This also means you can be selective in which power domains are enabled to create a “custom” sleep mode that has less current consumption than Deep Sleep, but not as good as Hibernation.
The ESP power domains are controlled by calling esp_deep_sleep_pd_config() with the power domain and options that include ESP_PD_OPTION_OFF, ESP_PD_OPTION_ON, and ESP_PD_OPTION_AUTO. The auto option powers down the power domain unless it is needed by one of the wakeup options. The different ESP power domains are:
ESP_PD_DOMAIN_RTC_PERIPH for RTC IO, sensors and ULP co-processor.
ESP_PD_DOMAIN_RTC_SLOW_MEM for RTC slow memory.
ESP_PD_DOMAIN_RTC_FAST_MEM for RTC fast memory.
ESP_PD_DOMAIN_XTAL for XTAL oscillator.
ESP_PD_DOMAIN_RTC8M for internal 8M oscillator.
ESP_PD_DOMAIN_VDDSDIO for VDD_SDIO.
During Deep Sleep the only GPIO pins that are available are the RTC IO (RTC_GPIO), which are part of the ESP_PD_DOMAIN_RTC_PERIPH power domain. The ULP co-processor is also active in this power domain. The table below shows the mapping of GPIO pins to analog and RTC functions available through the RTC Mux. The table information is from the Espressif Systems ESP32 Technical Reference Manual Version 4.2, section 5.11, table 21.
GPIO Num
RTC GPIO Num
Analog 1
Analog 2
Analog 3
RTC 1
RTC 2
36
0
ADC_H
ADC1_CH0
–
RTC_GPIO0
–
37
1
ADC_H
ADC1_CH1
–
RTC_GPIO1
–
38
2
ADC_H
ADC1_CH2
–
RTC_GPIO2
–
39
3
ADC_H
ADC1_CH3
–
RTC_GPIO3
–
34
4
–
ADC1_CH6
–
RTC_GPIO4
I2C_SCL
35
5
–
ADC1_CH7
–
RTC_GPIO5
I2C_SDA
25
6
DAC_1
ADC2_CH8
–
RTC_GPIO6
I2C_SCL
26
7
DAC_2
ADC2_CH9
–
RTC_GPIO7
I2C_SDA
33
8
XTAL_32K_N
ADC1_CH5
TOUCH8
RTC_GPIO8
–
32
9
XTAL_32K_P
ADC1_CH4
TOUCH9
RTC_GPIO9
–
4
10
–
ADC2_CH0
TOUCH0
RTC_GPIO10
–
0
11
–
ADC2_CH1
TOUCH1
RTC_GPIO11
–
2
12
–
ADC2_CH2
TOUCH2
RTC_GPIO12
–
15
13
–
ADC2_CH3
TOUCH3
RTC_GPIO13
–
13
14
–
ADC2_CH4
TOUCH4
RTC_GPIO14
–
12
15
–
ADC2_CH5
TOUCH5
RTC_GPIO15
–
14
16
–
ADC2_CH6
TOUCH6
RTC_GPIO16
–
27
17
–
ADC2_CH7
TOUCH7
RTC_GPIO17
–
GPIO Pin Function via RTC Mux Selection
To setup the RTC Mux and control the RTC GPIOs in the Arduino environment, calls are made to rtc_gpio_* functions. These functions allow you to initialize the pin as RTC GPIO and set it’s direction and level. My test code adapted from Xabi Alaez’s is shown below.
The first step calls rtc_gpio_init(gpio_num_t gpio_num) with the GPIO pin number to the initialize the pad for an analog/RTC function. Next, rtc_gpio_set_direction(gpio_num_t gpio_num, rtc_gpio_mode_t mode) is used to set the pin as output. Finally rtc_gpio_set_level(gpio_num_t gpio_num, uint32_t level level) is used to set the pin output as either HIGH (1) or LOW (0).
After the pin setup and toggled, a delay of 5 seconds is introduced before setting the pin to the value needed during Deep Sleep. Note that the ESP_PD_DOMAIN_RTC_PERIPH is enabled prior to setting the wake up timer and entering Deep Sleep. After waking from Deep Sleep, setup() is repeated again.
Although this approach worked well, I was trying to save 5uA on an external peripheral. By enabling the RTC peripheral that includes the ULP co-processor, I added about 100uA+ to the ESP power. So in the end I didn’t use this approach.
The key to setting an ESP32 output to a known state during Deep Sleep is to select an RTC_GPIO, initialize and operate the pin using RTC_GPIO_* calls, and enabling the RTC peripheral power domain prior to entering Deep Sleep.