ESP32 CAN Bus Arduino Library Gotcha

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.

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.

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.

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.

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.

RTC DS3231 with Ardunio

Most embedded systems require some kind of real time clock for different timing events. Recently a client wanted a chicken coop controller. Central to the implementation is an Adafruit DS3231 Precision RTC Breakout. This board uses Maxim’s DS3231 Integrated Real Time Clock.

The DS3231 is an extremely accurate real-time clock with an integrated temperature compensated crystal oscillator and crystal. This device supports battery backup to maintain timekeeping when main power is interrupted. This device maintains seconds, minutes, hours, day, date, month, and year information that includes leap year corrections. The DS3231 also has two programmable time of day alarms that trigger an interrupt output when the specified time occurs. The DS3231 interface is I2C.

The chicken coop controller included an Ardunio UNO, Adafruit DS3231 precision real-time clock (RTC), Pololu 5V 1A regulator, 2-channel relay module, AC light dimmer (forward phase) module, 12V linear actuator, and a 12V battery. The Ardunio sketch was to:

  • At sunrise retract the linear actuator for 3 minutes.
  • 1-hour after sunrise turn on lights at 100% brightness
  • Leave lights at 100% brightness for 12.5 hours, then gradually dim over a 1-hour period.
  • At sunset extent the linear actuator for 3 minutes.

Looking at the requirements, all events except the dimming occur on the minute boundary (sunrise, sunset, actuator time, lights on full time). The DS3231 has two alarms. One alarm works down to the second and the other alarm works down to the minute. The need for an alarm to occur on seconds boundary is driven by the light dimming over a 1-hour period. The light dimming uses the 8-bit PWM control. Using all of the PWM control states (256) over 1-hour period has the PWM value updating every 14 seconds (255 down to 0). Since light dimming needs an alarm to occur every 14 seconds, alarm 1 was selected for light control and alarm 2 for actuator control.

The next alarm time is determined by adding the seconds, minutes, hours, and days to a time. For example if the next alarm is to occur in 3 minutes from now, 3 minutes is added to the current time. An add time function was developed to support this sketch. The time is added to the appropriate parameter and then rollover values are tested and updated. When adjusting months the code continues to loop through rollovers until all adjustments are made.

Function to Add time

// function to add time to input time
void addTime(ts *t, int secs, int mins, int hours, int days) {
  boolean adjustMonths = true;
  
  // add time then adjust for rollovers
  t->sec += secs;
  t->min += mins;
  t->hour += hours;
  t->mday += days; 

  // adjust date/time based on rollovers
  if (t->sec >= 60) {
    t->min += t->sec / 60;
    t->sec %= 60;
  }
  if (t->min >= 60) {
    t->hour += t->min / 60;
    t->min %= 60;
  }
  if(t->hour >= 24) {
    t->mday = t->hour / 24;
    t->hour %= 24;
  }

  // adjust months
  while(adjustMonths) {
    switch(t->mon) {
      case 1:
      case 3:
      case 5:
      case 7:
      case 8:
      case 10:
      case 12:
        if (t->mday > 31) {
          t->mday -= 31;
          t->mon += 1;
          if (t->mon > 12) {
            t->mon = 1;
            t->year += 1;
          }
        }
        else
          adjustMonths = false;
        break;
      case 4:
      case 6:
      case 9:
      case 11:
        if (t->mday > 30) {
          t->mday -= 30;
          t->mon += 1;
        }
        else
          adjustMonths = false;
      case 2:
        if (t->year % 4 == 0) {   // leap year, don't worry about % 400
          if (t->mday > 29) {
            t->mday -= 29;
            t->mon += 1;
          }
          else
            adjustMonths = false;
        }
        else
          if (t->mday > 28) {
            t->mday -= 28;
            t->mon += 1;
          }
          else
            adjustMonths = false;
        break;
    }
  }
}

In setting the alarm the developer must enable all values (seconds, minutes, hours, date) up to the desired match (i.e. if alarm is on a hour value, then seconds, and minutes must also be enabled).

When the time matches the enabled alarm parameters then the DS3231 INT/ pin goes active low. Since the I2C Adruino library uses interrupts to communicate, this sketch polls this DS3231 pin instead of using an interrupt. During the polling each alarm is checked and the proper flag(s) is(are) set.

One final item with the DS3231 is the control register used to enable/disable alarms and set other parameters. This register is not readable. If the alarms or other controls are enabled at different times in the code, then a “shadow” register is needed to maintain the current control register state. For example is you are enabling alarm 2 you don’t know the current state of alarm 1 unless you maintain an internal variable in your code.

Finally the library select to support the DS3231 was ds3231FS by Petre Rodan. This library provided an interface to setting the alarms.