ESP32 CAN Bus Arduino Library Gotcha Part II

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.

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.

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.

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.

STM32CubeIDE Enviornment printf Support

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.

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.

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.

Alchitry I2C Controller Example

I have worked on many embedded systems during my career. Some of those projects used a processor while others required custom approaches such as using a field programmable gate array (FPGA). I’ve been using FPGAs since the early 1990’s where initially they where configured using schematic entry. Later we moved to VHDL to create behavioral descriptions of the circuits. Today a more common FPGA hardware description language is Verilog.

For an upcoming proof of principle project I determined that a processor only solution wasn’t flexible enough to meet some of the strict timing requirements as well as not supporting potential system changes based on testing.

Some of the project requirements included the measurement of multiple input signal properties, controlling a digital to analog converter (DAC), and sending the measurement results to another computer via USB.

While searching for a simple FPGA development board and programming environment I discovered the Alchitry FPGA board set and development environment with the hardware being sold by SparkFun.

The current hardware offering has three different FPGA development boards. Two of the boards use the AMD (Xilinx) Artix 7 FPGA (Au and Au+) and the third board uses the Lattice iCE40 HX (Cu). Each board has different resources but all include a connector system for adding additional boards that add a USB 3.0 interface (Ft Element), multiple IO types (Io Element), and an IO breakout (Br Prototype Element). For my project the Au FPGA Development Board with the Br Prototype Element Board gave me the resources and connectivity that I needed. The picture below shows the SparkFun Alchitry Au FPGA kit that includes the Au, Io, and Br boards plus a female header set.

The Alchitry Labs development environment is an abstraction layer called Lucid to help simplify FPGA programming. The Alchitry Labs IDE actually converts Lucid structure into Verilog before interfacing with the FPGA vendor’s tools to create your FPGA image. The Alchitry Labs IDE has an extensive library of components (e.g., I2C Controller) as well has having an interface to the FPGA vendor’s tools to create specialized IP cores (e.g., clocking module).

One of the project’s features is to control a DAC. For that task I decided to use an Adafruit MCP4725 breakout board that has an I2C interface. The MCP4725 is a 12-bit DAC that supports a 3.4Mbps fast mode I2C interface. This requires the FPGA to operate as an I2C controller. The Alchitry Au has a QWIIC/STEMMA QT connector to support a 3V3 I2C connectivity and the IDE has an I2C controller component.

In this I2C controller example I am using Architry Labs Version 1.2.7. For my project there are basically four steps to control an I2C DAC:

  • Add the Alchitry I2C Controller component (Lucid source file)
  • Define I2C FPGA pinout (interface constraint file)
  • Create MCP4275 module to write new DAC value (voltage)
  • Create a test driver to verify operation

To add a predefine component in the IDE use Project > Add Components. A popup window appears where you select under Protocols the I2C Controller. This adds a file, i2c_controller.luc, under Components in the IDE left pane.

To define the pinout you need to know which pins are assigned to SCL and SDA. The Au environment as an IO abstraction that matches the Br pinout, which is show below (from the SparkFun website). You can either add the pinout to an existing Constraints file (alchitry.acf) or create a new one and add the lines below. I did not add a pullup resistor to each line since the MCP4725 breakout already has pullup resistors. I am using the Au QWIIC connector on Au board requiring the signals to be assigned as scl = A24 and sda = A23. The letter is the bank and the number is the pin within the bank (e.g., A23 is bank A, pin 23, lower right of bank A).

To create the MCP4725 module add a new file, File > New File. In the popup add the file name and select Lucid Source and then Create File.

The module interface is listed below. The signal vdata[12] is the 12-bit voltage value and write is a single clock signal (high = write) that starts the DAC write over I2C. The module has feedback to indicate if the module can accept a new value via the busy signal. Busy HIGH indicates the module is current writing a DAC value and cannot accept a new value.

This module has the I2C controller (qwiic) that uses a FSM (state) to send the data (voltage value) to the MCP4725 as well as a memory element to save the vdata input in case it changes during FSM execution (dff newvalue[12]).

For this example we are only performing a single write of a 12-bit value to the DAC over I2C. Future enhancements include creating a fully supported MCP4724 interface that includes writing and reading from the MCP4725 EEPROM. For now we are only interested in changing the DAC voltage value. To start we assign some of the default signal values in the always block. The module is busy when not in the IDLE state.

The core of this module is the FSM that executes the steps required to write the new voltage value to the DAC. The description on how to use the I2C Controller component is found in the Component source code. As a summary, to use the I2C Controller you set the start signal to high for 1 clock cycle and then write or read as many bytes as desired. Once finished, set the stop signal high for 1 clock cycle.

To write to the MCP4725 we are using the fast mode write method has shown in the datasheet in Figure 6-1. This method transmits 3 bytes – Byte 1: the 7-bit address with the R/W bit set to 0, Byte 2: the upper 4-bits of data with the command bits (C2, C1 = 0) and normal power (PD2, PD1 = 0), and Byte3: lower 8-bits of data.

The FSM Lucid code is shown below. The FSM is IDLE until the module input write bit is set to 1, which saves the input data to a 12-bit DFF and then moves to the next state START. If the controller isn’t busy, the I2C controller start bit is set to 1 and the next state is ADDRESS. When the controller isn’t busy, each of the three bytes is sent in order. Once all three bytes have been transmitted, the I2C controller stop is issued. There are two items to note. First, in each non IDLE state the controller is check for busy before writing the next byte or stopping the controller. Second, although available, the write ack is never checked. I2C has support an acknowledgement after each byte is written to the peripheral.

The final item is to test the design. At the top level I created a counter. Whenever the 12-bit counter value changes, the value is written to the DAC. This creates a sawtooth waveform since the counter wraps around from 0xFFF to 0. The code for the test driver is shown below.

The results were as expected. The only issue I encountered was related to my original FSM structure using signals to interface to the I2C controller. Most of the FSM states were optimized out, thus not executing the desired sequence. That problem was solved by interfacing directly with the I2C controller component signals. The output waveform is shown below. The sawtooth period matches the expected rate (100MHz clock / 256 (counter #DIV(8)) / 2^12 (counter size) = 95.367Hz).

Improvements to the DAC module include making a fully featured MCP4725 interface as well as modifying (creating my own) I2C controller to support the 3.4Mbps fast mode I2C interface. Currently the I2C controller operates at a single frequency based on the user specified divider. To enter the fast mode I2C a command is sent at the “normal I2C rate” then scl is increased to 3.4MHz.

I found both the hardware and Lucid programming environment to be excellent, as well as Alchitry support. The Au development board provides plenty for hardware resources and IO. The SparkFun boards are well made and of high quality. The hardware costs are very reasonable even for a hobbyist. The Lucid programming environment does simplify FPGA behavioral programming and interfaces well with the vendor’s FPGA tools. The Alchitry cores support the majority of a user’s needs. I recommend this board set and programming environment for both hobbyist and professionals developers.

STM32 I2S Interface With A MEMS Microphone

I’ve started using STMicroelectronics STM32 microcontrollers in my client’s embedded applications. Recently I needed to interface an STM32G0x0 MCU with a Knowles SPH0645LM4H-1 MEMS microphone using I2S interface.

The STM32G0x0 MCU is an entry level, 32-bit Arm® Cortext®-M0+ MCU that runs up to 64MHz. This MCU series is cost effective and part of the ST STM32 value line. My client selected the STM32G030F6, which has 32Kbytes of Flash and 8Kbytes of RAM packaged in a 20-pin TSSOP. This MCU has I2C, USART, SPI/I2S communication interfaces, a 12-bit ADC, multiple GPIOs, timers, DMA, and RTC.

The Knowles SPH0645LM4H-1 is a miniature, low power, bottom ported MEMS microphone with a 24-bit I2S standard digital interface. The microphone supports 16kHz to 64kHz sampling. This microphone can be configured for either a right or left channel by an external signal. The key microphone feature is the output is already in PCM format so no additional CODEC hardware or PDM firmware conversion is needed in this application.

I2S (Inter-IC Sound) is a serial bus communication standard used for communicating with digital audio devices. Philip Semiconductor introduced the standard in 1986. The interface has three signals, serial clock (SCLK), word select (WS) and serial data (SD). SCLK is a continuous signal and is 32 or 64 times the audio sample rate based on word size. For example working with 32-bit data frames for a 16KHz audio, SCLK is 2 (audio words left and right) x 32-bits (size of each audio word) x 16kHz (sample rate) = 1.024MHz. WS is used to select left (low) and right (high) channel. SD is the audio data that is either 16 or 32 bit data word. Below is the I2S SCLK/WS/SD relationships from the Knowles datasheet. The Knowles data is 24-bit left shifted in a 32-bit word format.

STM32G030F6 I2S Setup

The STMicroelectronics development environment, STM32CubeIDE, has a configuration tool to setup the STM32 I/O as well as to set the clock configuration. Once you setup the hardware components this tool generates the support code and data structures.

For this application the Multimedia I2S1 interface to configuration for half-duplex master mode. This has the STM32 send the SCLK and WS signals and can either send or received data (half-duplex).

The I2S parameter setup configurations the transmission mode, communication standard, data and frame format, audio frequency, and the clock polarity. In our application the transmission mode is Mode Master Receive since the processor is the master and the microphone is the data source.

For the communication standard the selections include I2S Philips, MSB First, LSB First, and a couple of PCM framing selections. Originally I used the MSB first standard but discovered that the data was under shifted (MSB was bit 30 not 31 as expected). A look at the STM32 reference manual showed why.

The STMicroelectronics RM0454 Reference Manual (rev 5) shows waveforms for both the I2S Philips standard the MSB justified formats. Both the I2S Philips and the MSB justified framing sets WS on the falling clock edge. In the I2S Philips standard data changes on the next rising SCLK edge and read on the falling edge creating a null 1/2 bit time before the first data bit. With MSB justified format there is no gap. Data changes on the SCLK falling edge along with WS and read on the next rising edge. By using the MSB justified format the STM32 read the first bit before being transmitted by the microphone.

Below is the I2S interface measured with an oscilloscope (channel 1 = SCLK, channel 2 = WS, channel 3 = SD). It is clear that WS changes to the left channel on the falling edge of SCLK and the first data bit from the microphone appears on the next rising edge.

Next is to configure the Data and Frame Format. The SPH0645LM4H-1 data format is 24-bits in a 32-bit frame. Of the 24-bits only the most significant 18-bits are valid and the remaining bits (lowest 6-bits of the 24-bit word) are set to zero by the microphone. The remaining data byte (8-bits) are tri-stated and with a 100K pulldown resistors are read as zero by the STM32. Our choice must include a 32 Bits Frame since that is the output of the microphone, but we can select 16, 24, or 32 data bits.

The 24 and 32-bit choices requires two reads from the receive data register while using 16-bits of data in a 32-bit frame requires only 1 read and the second 16-bits are automatically set to zero. For this test I selected 16-bits in a 32-bit frame.

The code is very simple when using the provide blocking call to receive I2S data. The call is made to HAL_I2S_Receive, which takes the I2S handle pointer created by the auto code generation tools when configuring the processor, a pointer to a data buffer, the number of samples, and a timeout value. Since the I2S interface is designed for a left and right channel, 2 samples are selected.

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
	volatile HAL_StatusTypeDef result = HAL_I2S_Receive(&hi2s1, (uint16_t *)&dataIn[0], 2, 100);
	if(result != HAL_OK) {
		printf("Error Code %08lX\n", HAL_I2S_GetError(&hi2s1));
	}
  }
  /* USER CODE END 3 */

Below is a screen shot showing the Live Expressions window for the data buffer. Note that dataIn[0] is our left channel and dataIn[1] is our right channel and is always 0 since we don’t have a another sensor attached.

From the single blocking read, there isn’t much time to processed the received I2S data. With two channels at 16kHz there is only 15.625 microseconds (16kHz x 4 16-bit words = 64kHz) before the next 16-bit data word is ready at the input SPI/I2S register. That is just 1,000 CPU clock cycles at 64MHz maximum clock rate. Just trying to convert the read integer to a string caused data overflow errors (sprintf is a very, very slow function). With an overflow error you miss some real-time data and the data may shift within the data buffer (i.e., left channel moved to dataIn[1]).

A safer approach to processing incoming audio stream data is to use DMA where data is read from the microphone and placed directly into memory without the CPU. The STM32 programming environment provides support for a continuous circular buffer with interrupt callbacks for buffer half full and completely full. The image below shows the DMA data buffer. Note that all right channel data is zero.

For the I2S DMA test I counted each buffer related interrupt and performed a timing analysis to verify continuous data was being read from the MEMS microphone at the realized audio sampling rate (slightly different than the desired audio rate due to processor clock tree).

Overall the STMicroelectronics STM32CubeIDE tool make the developer’s job easier with the hardware configuration interface and auto hardware abstraction layer (HAL) code and data structures generation.

MEMS Accelerometer Free Fall Detection

Micro Electro Mechanical Systems (MEMS) accelerometers have been available since the 1990’s. MEMS creates a microscopic mechanical sensing structures that is coupled with microelectronic circuits to measure physical parameters such as acceleration. One of the first applications of the silicon based MEMS accelerometer was to protect hard drives (HDD) media when being dropped while rotating. When free fall was detected the HDD head would be moved to a safe zone to protect the media.

When a MEMS accelerometer is in free fall all acceleration values move towards zero. The figures below help to understand why this is true (Figures from Physical StackExchange). When in free fall the frame and mass are both affected by the same acceleration so the force acting on the spring nears zero. For a MEMS accelerometer the measured acceleration nears zero.

An inexpensive MEMS accelerometer is the LIS3DH. This unit is a 3-axis accelerometer with scalable ranges (±2g/±4g/±8g/±16g), supports multiple data rates, is low power, has three 10-bit ADCs, and interfaces over I2C or SPI. This accelerometer can also generate interrupts for tap, double-tap, orientation changes, and freefall detection. These accelerometers are available on breakout boards from multiple vendor such as Adafruit and Sparkfun.

To configure the LIS3DH for free fall detection a threshold level in g’s and duration are programmed in device registers. The design tip DT0100 shows an example of setting the level to ~350mg with a duration of 30ms. For the interrupt to occur a level of 350mg or less must be detected on all three axis accelerometers for a minimum of 30ms before the interrupt occurs as shown in the figure below from the ST design note.

For this example I used an Adafruit LIS3DH breakout board. Adafruit does a nice job of providing libraries for their products. The Adafruit LIS3DH library unfortunately does not provide a free fall interrupt method. Normally I would inherit the library object and create my own methods but the library made key communication variables private, so this wasn’t an option without modifying the library. My client’s application did not required any other processing so I created code to replicate the free fall interrupt function in the Arduino loop().

The code uses the Adafruit sensors_event_t type, which returns the accelerometer values in m/s2. To match the design tip the free fall threshold would be set to 3.432 m/s2 (1g = 9.08665 m/s2). With a free fall threshold of 350mg, I found it too easy to enter free fall so I reduced the value to about 50mg, which worked well. When all three axis meet the free fall detection threshold, the free fall duration is checked. If the amount of time exceeds the free fall duration then a LED is turned on (the client turned a servo motor) indicating the unit is in free fall. If the free fall threshold isn’t met, then the duration time is updated to the current time using millis().

Below is the sample code emulating the LIS3DH free fall detection.

// Free Fall Detection
// Adafruit LIS3DH Accelerometer with SPI
// Adapted from Adafruit library acceldemo.ino

#include <SPI.h>
#include <Adafruit_LIS3DH.h>
#include <Adafruit_Sensor.h>

// Hardware Pins
#define LIS3DH_CS 10  // SPI device select
#define LED_PIN   4   // LED output pin to indicate freefall active high

// Operating Parameters
#define FREEFALL_ACCEL_THRESHOLD  0.5 // freefall threshold in m/s^2, 1g = 9.80665 m/s^2
#define FREEFALL_TIME_THRESHOLD   30  // freefall time threshold in ms

// hardware SPI for accelerometer
Adafruit_LIS3DH lis = Adafruit_LIS3DH(LIS3DH_CS); // accel object
sensors_event_t event;                            // sensor data to evaluate for freefall

// timer for free fall
uint32_t freeFallTime;           // timer for consecutive freefall detections

void setup(void) {
  Serial.begin(115200);
  while (!Serial) delay(10);     // will pause Zero, Leonardo, etc until serial console opens

  // setup output control pin for LED
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);
  
  if (! lis.begin()) {
    Serial.println("Couldnt start");
    while (1) yield();
  }

  lis.setRange(LIS3DH_RANGE_2_G);           // set to 2G range
  lis.setDataRate(LIS3DH_DATARATE_400_HZ);  // sample at fastest rate (2.5ms)

  Serial.println("Free Fall Detection with LIS3DH");

  freeFallTime = millis();
}

void loop() {

  lis.getEvent(&event);   // get latest data

  // detect free fall
  if(abs(event.acceleration.x) <= FREEFALL_ACCEL_THRESHOLD &&
     abs(event.acceleration.y) <= FREEFALL_ACCEL_THRESHOLD &&
     abs(event.acceleration.z) <= FREEFALL_ACCEL_THRESHOLD) {

    if((millis() - freeFallTime) >= FREEFALL_TIME_THRESHOLD) {
      digitalWrite(LED_PIN, HIGH);
    }
  }
  else { // not in freefall reset timer
    freeFallTime = millis();
  }
}

Update

The freefall detection code and hardware was used successfully as part of the Rogers Park Space Program high altitude balloon project when the “HELEN II” paper plane was released at 34,800′ above sea level. You can watch a cool video on YouTube of the entire flight that reached over 112,000′ on 5/28/2023.

ESP32 deep Sleep (RTC) GPIO Output

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 NumRTC GPIO NumAnalog
1
Analog
2
Analog
3
RTC
1
RTC
2
360ADC_HADC1_CH0RTC_GPIO0
371ADC_HADC1_CH1RTC_GPIO1
382ADC_HADC1_CH2RTC_GPIO2
393ADC_HADC1_CH3RTC_GPIO3
344ADC1_CH6RTC_GPIO4I2C_SCL
355ADC1_CH7RTC_GPIO5I2C_SDA
256DAC_1ADC2_CH8RTC_GPIO6I2C_SCL
267DAC_2ADC2_CH9RTC_GPIO7I2C_SDA
338XTAL_32K_NADC1_CH5TOUCH8RTC_GPIO8
329XTAL_32K_PADC1_CH4TOUCH9RTC_GPIO9
410ADC2_CH0TOUCH0RTC_GPIO10
011ADC2_CH1TOUCH1RTC_GPIO11
212ADC2_CH2TOUCH2RTC_GPIO12
1513ADC2_CH3TOUCH3RTC_GPIO13
1314ADC2_CH4TOUCH4RTC_GPIO14
1215ADC2_CH5TOUCH5RTC_GPIO15
1416ADC2_CH6TOUCH6RTC_GPIO16
2717ADC2_CH7TOUCH7RTC_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.

#include "driver/rtc_io.h"
gpio_num_t RST_PIN = GPIO_NUM_14;

void setup() {
  rtc_gpio_init(RST_PIN);
  rtc_gpio_set_direction(RST_PIN,RTC_GPIO_MODE_OUTPUT_ONLY);
  // toggle reset pin
  rtc_gpio_set_level(RST_PIN,1); //GPIO high
  rtc_gpio_set_level(RST_PIN,0); //GPIO LOW
  rtc_gpio_set_level(RST_PIN,1); //GPIO high
  delay(5000);  // wait
  rtc_gpio_set_level(RST_PIN,0); //GPIO hold low
  esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
  esp_sleep_enable_timer_wakeup(60*1000*1000);  // 60 seconds
  esp_deep_sleep_start();
}

void loop() {
  // none
}

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.

MIFARE CLASSIC® 1K SECTOR TRAILER ACCESS BITS

Radio Frequency Identification (RFID) has become an effective, low cost, contactless method of transferring data between a tag and reader. A reader is a device that has one or more antennas that emits radio waves. These radio waves energize a close proximity passive tag allowing for the exchange of data between the tag and reader. Some common RFID applications include inventory management, asset tracking, loyalty cards, fare cards, employee cards, and access control. The radio wave frequency affects the distance between the tag and reader.

  • Low-Frequency 30kHz to 500kHz (typical 125kHz) range few inches to less than 6 feet.
  • High-Frequency 3MHz to 30MHz (typical 13.56Mhz) range few inches to several feet.
  • Ultra High-Frequency 300MHz to 960MHz (typical 433MHz) up to 25+ feet.
  • Microwave 2.45GHz up to 30+ feet.

The MIFARE Classic® 1K is a common low cost RFID tag that operates at 13.56MHz. These tags come in key card and keychain styles and costs less that $1 each in low quantities. As the 1K indicates, this tag stores 1K bytes of data. For the reader you can use a standard product like the ACR122U that has a USB connection to your computer. If you are making your own embedded system then the RC522 module is a simple, low cost reader module that interfaces to a processor using SPI.

The MIFARE Classic® 1K tag is organized into 16 sectors (0 – 15) with each sector containing 3 data blocks (blocks 0-2) and 1 sector trailer (block 3). Each data block/sector trailer has 16 bytes (16 sectors * 4 blocks/sector * 16 bytes/block = 1024 bytes). Not all bytes are available for user data. Sector 0, data block 0 is reserved for manufacturing data and each sector trailer can contain up to 7 user data bytes if key B isn’t being used. If only data blocks are used then 752 bytes are available for user data.

MIFARE Classic® 1K Data Organization

Manufacturer block (sector 0, block 0, address 0, 16 bytes) is programmed and write protected during product test. Data blocks are configurable as either read/write or value blocks as defined by the access bits in the sector trailer. Value blocks have special commands such as increment and decrement for direct control of stored values, which are helpful in electronic purse applications.

The sector trailer is the last block of the sector. Each sector trailer contains:

  • secret keys A (bytes 0-5) and B (bytes 10-15), which when protected from reading returns ‘0’
  • access conditions for all four sector blocks (bytes 6-8)

All keys are set to 0xFFFFFFFFFFFF at chip delivery and key A is never readable.

Sector Trailer Byte Definitions

Access bits control the access conditions for every data block and sector trailer in the given sector. Access control is defined by 3 bits for each block, which are stored in the sector trailer bytes 6-8 (3 bits/block x 4 blocks/sector = 12 bits/sector * 2 for true and inverted values = 24bits/sector = 3 bytes/sector). Access control limits the ability to read or write sector areas including the sector trailer as well as defining data block’s functions (read/write vs. value block). Formatting of bytes 6-8 is somewhat complex. These three bytes are formed by combining the access bits values of the four sector blocks as shown in the next figure (e.g., bit C22 is the C2 bit value for block 2 within the sector).

Access Bits Byte Definition

Conditions for sector trailer access are defined in the table below. Access control includes the ability to write key A (reading key A is never allowed), read and write key B, and the ability to read and write access bits. For example if you never want to write either key A or key B, but you still want to change the access bits in the future, then C1C2C3 = 101 (5) is a good selection. With this access setting you can never read or write either key, but you can still read the access bits using either key A or key B and write the access bits using key B. The factory default value is C1C2C3 = 001 (1), where key A is used for read and write access to all sector trailer values.

Sector Trailer Access Conditions

A similar table is used to configure the sector data blocks access bits. Access conditions include read, write, increment, decrement/transfer/restore. If you want to only read a data block using only key B, then C1C2C3 = 101 (5) could be used. Note that if key B is readable (set by the sector trailer access bits), it can’t be used for access control (see note [1]).

Data Block Access Conditions

Unless you are using a tag reader with support software, the calculation of the access control bytes is a little tedious. I have created an Excel spreadsheet where the user selects each blocks access bits integer decimal values (unshaded table area) using the appropriate tables (C1C2C3) and the access bits byte values are automatically calculated (B6 B7 B8).

Excel Worksheet to Simplify Access Bits Byte Calculation

This has been a quick discussion on setting the MIFARE Classic® 1K tag access bits. If you want to learn more about this tag, Sanga Chidam YouTube channel has excellent video tutorials on the MIFARE Classic® 1K tags. All figures and tables in this post are from the NXP MF1S503X datasheet.

Arduino Tilt & Laser Distance Finder

A middle school teacher reached out looking for a firmware developer to create a tilt and laser range finder for classroom use to encourage STEM education. This looked like a fun project and appeared straight forward. The teacher had already selected, purchased, and assembled the hardware into a demonstration system for his classroom.

The hardware included:

  • Nano 33 BLE
  • Nextion NX4024T032 basic touch display (400×240, 3.2″ display)
  • Laser range module
  • MB102 power supply
  • 12V AA battery pack (AA cell used for on/off switch)
Laser Distance Finder and Tilt Measuring Device

The Nano 33 BLE has an Inertial Measurement Unit (IMU) to support tilt measurement and the laser range module provided distance information via a serial interface.

The laser module interface documentation was minimal and some test code was created to understand the control and response formats. Second I had planned on using ITEADLIB library to interface to the Nextion display, but the library doesn’t support Nano 33 BLE. So I had to generate code to interface to the Nextion display.

The client provided graphic for the tilt gauge and battery status and conceptual functions and screens. The final system had three screens:

  • Control and measurement used to start/stop measurements, display current measurement data (distance and tilt), and show battery status. It also had buttons to see a previous measurement list and go to the settings page.
  • Measurement list that shows the last 10 measurements in current units.
  • Settings page was used to change tilt and distance calibration offsets, change screen brightness, and toggle units between feet-inches, inches, and millimeters.

On the control and measurement screen there were three primary firmware calculations that were needed, distance measurement, tilt angle, and battery voltage percentage. The only control function was to turn the distance/tilt measurement on and off that used a multi-state Nextion button object.

Distance Calculation

The laser distance module is a self contained system where control and data used an asynchronous start/stop protocol at 115200 baud, 8-N-1. The laser module had both single and multiple (continuous) measurement modes. I used the multiple measurement mode where the module once enabled, continued to send measurements so the system could be pointed and adjusted as needed before stopping the measurement and storing the results.

While in this measurement mode, the distance was updated whenever a new measurement was received through the serial interface. The message was decoded and converted to the display string using the user selected units (ft-inches, inches, or mm). All data was stored in the firmware in inches and converted when needed.

Tilt Calculation

Tilt was updated whenever the Inertial Measurement Unit (IMU) had a new measurement. The interface to the LSM9DS1 IMU used an Arduino library. The IMU x-axis is inline with the laser module. I used all three axis in the calculation and applied the classical method of rectangular to spherical conversion to find the tilt. The key assumption was the only acceleration is due to gravity.

g_tilt = (180.0*acos(x/sqrt(x*x+y*y+z*z))/M_PI) - 90;  // +/- 90 degrees

Battery Percentage Calculation

A low current voltage divider was used to estimate the remaining battery level. Based on the voltage a percentage was calculated. On the Nextion display a progress bar was used to show the amount of remaining battery and the color was changed based on the level between green (50% or greater), yellow, and red (15% or less). The battery level was updated based on a programmable timer value.

Nextion Display

The Nextion Display was developed using the Nextion’s HMI software. Buttons used images to reflect on, off, settings, save, and back. The Nextion display interface is serial and the protocol is defined in the user documentation. Below is a sequence that sets the length of the progress bar, sets the color, and writes the text value of the battery percentage.

  nextionSerial.print("j0.val=");              // progress bar
  nextionSerial.print(String(100-intPerCent));
  nextionSerial.print("\xFF\xFF\xFF");

  if(percentage < 15) {
    nextionSerial.print("j0.bco=63488");       // progress bar red
    nextionSerial.print("\xFF\xFF\xFF");    
  }
  else if (percentage >= 50) {
    nextionSerial.print("j0.bco=1024");       // progress bar green
    nextionSerial.print("\xFF\xFF\xFF");        
  }
  else {
    nextionSerial.print("j0.bco=65504");       // progress bar yellow
    nextionSerial.print("\xFF\xFF\xFF");    
  }
  
  nextionSerial.print("t3.txt=");       // text value
  nextionSerial.print("\"" + String(intPerCent) + "%" + "\"");
  nextionSerial.print("\xFF\xFF\xFF");

Saving Operating Parameters

The Nano 33 BLE does not have EEPROM. The only non-volatile memory is flash. For this project the user wanted to store the units and offsets between power cycles. I looked at several libraries and found one designed to store parameters in flash by Dirk Frehiling on Github. The library works with a data structure defined by the programmer (typedef struct) and uses writePrefs and readPrefs object methods.

// cal flash data structure
typedef struct flashStruct {
  float calDistance;        // inches (always)
  float calTilt;            // degrees
  enum outputType outUnits; // units
} calibrationData;

The project has some other nice features like going into a power down mode after inactivity and displayed fractions of an inch as a fraction for measurements in FT-IN, and INCHES. One future improvement is to increase the accuracy of the tilt measurement using more advance calibration techniques.

NORDIC nRF52840 GPIO High Drive

I recently had a project using the Adafruit Feather nRF52840 Express to control a 2N2222 transistor being used as a high power switch. The nRF52840 SoC is a higher-end Bluetooth 5.2 SoC supporting Bluetooth Low Energy, Bluetooth mesh, NFC, Thread, and Zigbee. It has a 64MHz Arm Cortex-M4 with FPU, 1MB Flash, 256KB RAM and all the standard microcontroller features including UART, SPI, TWI, PDM, I2S, QSPI, PWM, and 12-bit ADCs. Adafruit has created a board support package for the Arduino development environment.

After building the code and making a test circuit with a 620 ohm base resistor, I measured the nRF52840 VOH and the voltage was much lower than expected. Reviewing the nRF52840 datasheet the IOH for standard drive is a minimum of 1mA and a maximum of 4mA. I was exceeding the GPIO pin guaranteed current drive capability.

The nRF52840 is an interesting microcontroller where you can control the GPIO drive between standard, high, and disconnect (open). You can specify both IOH and IOL and they can be different. The RW DRIVE parameter is set in the PIN_CNF[n] register as specified in the nRF52840 Product Specification v1.2. The high drive for a 3.3V VDD is a minimum of 6mA for both IOH and IOL.

First I needed to find the correct GPIO pin [n] value to set the drive parameters. Most of the parameters are set when creating the PWM object, but there is no method to set the RW DRIVE parameter. To find the correct pin number I needed to review variant.h and variant.cpp files in the directory containing the board support package …\packages\adafruit\hardware\nrf52\0.22.0\variants\feather_nrf52840_express.

I was using the Feather A0 pin, which is connected to the nRF52840 P0.04 pin. In variant.h file A0 is mapped to pin 14 (D14). From variant.cpp D14 is mapped to pin 4. This is the [n].

To set the RW DRIVE requires either writing directly to the register (NRF_GPIO->PIN_CNF[4] = 0x203) or use a call to nrf_gpio_cnf with the desired parameters. See the file nrf_gpio.h for enum types and support functions. The following call was made to set VOH to high drive and VOL to standard drive (S0H1). Once this change was made the VOH was well within specification when turning the transistor switch on.

nrf_gpio_cfg(4, 
             NRF_GPIO_PIN_DIR_OUTPUT, 
             NRF_GPIO_PIN_INPUT_DISCONNECT, 
             NRF_GPIO_PIN_NOPULL, 
             NRF_GPIO_PIN_S0H1, 
             NRF_GPIO_PIN_NOSENSE);