Table of Contents

Hardware-specific extensions in programming

 General audience classification icon  General audience classification icon  General audience classification icon
Some generic programming techniques and patterns mentioned above require adaptation for different hardware platforms. It may occur whenever hardware-related aspects are in charge, e.g., accessing GPIOs, ADC conversion, timers, interrupts, multitasking (task scheduling and management), multicore management, power saving extensions and most of all, integrated communication capabilities (if any). It can be different for almost every single MCU or MCU family.
It is common for hardware vendors to provide rich examples, either in the form of documentation and downloadable samples (e.g. STM) or via Github (Espressif), presenting specific C/C++ code for microcontrollers.

Analog input

Some MCUs use specific setups. Analogue input may work out of the box. Still, low-level control usually brings better results and higher flexibility (e.g. instead of changing the input voltage to reflect the whole measurement range, you can regulate internal amplification and sensitivity.

A special note on analogue inputs in ESP32
Please note implementation varies even between the ESP32 chips family, and not all chips provide all of the functions, so it is essential to refer to the technical documentation [1].

ESP32 has 15 channels exposed (18 total) of the up to 12-bit resolution ADCs. Reading the raw data (12-bit resolution is the default, 8 samples per measure as default) using the analogRead() function is easy.
Technically, under the hood on the hardware level, there are two ADCs (ADC1 and ADC2). ADC 1 uses GPIOs 32 through 39. ADC2 GPIOs 0,2,4, 12-15 and 25-27. Note that ADC2 is used for WiFi, so you cannot use it when WiFi communication is enabled.
Just execute analogRead(GPIO).
Several useful functions are here (not limited to):

Do not execute consequent way analogRead(). As technically all channels use the same two registers (ADC1 and ADC2), you need to give it some time to sample (e.g. delay(100) between consecutive reads on different channels).

Analog output

PWM frequently controls analogue-style, efficient voltage on the GPIO pin. Instead of using a resistance driver, PWM uses pulses to change the adequate power delivered to the actuator. It applies to motors, LEDs, bulbs, heaters and indirectly to the servos (but that works another way).

A special note on ESP32 MCUs
The classical analogWrite method, known from Arduino (Uno, Mega) and ESP8266, does not work for ESP32.
ESP32 has up to sixteen (0 to 15) PWM channels (controllers) that can be freely bound to any of the regular GPIOs.
The exact number of PWM channels depends on the family member of the ESP chips, e.g. ESP32-S2 and S3 series have only 8 independent PWM channels while ESP32-C3 has only 6. In the Arduino software framework for ESP32, it is referred to as ledc. ESP32 can use various resolutions of the PWM, from 1 to 20 bits, while regular Arduino uses only 8-bit one. Note - there is a strict relation between resolution and frequency: e.g. with high PWM frequency, you cannot go with a resolution too high as the internal frequency of the ESP32 chip is limited.

To use PWM in ESP32, one must perform the following steps:

More information and detailed references can be found in the technical documentation for the ESP32 chips family [2].

Sample code controlling an LED on GPIO 26 with 5kHz frequency and 8-bit resolution is presented below:

#include "Arduino.h"
 
...
 
#define RGBLED_R 26
#define PWM1_Ch   5
#define PWM_Res   8
#define PWM_Freq  5000
 
...
 
ledcSetup(PWM1_Ch, PWM_Freq, PWM_Res); 
    //Instantiate timer-based PWM -> PWM channel
ledcAttachPin(RGBLED_R, PWM1_Ch); 
    //Bind a PWM channel to the GPIO
ledcWrite(PWM1_Ch,255); 
    //Full on: control via the PWM channel, not via the GPIO
...
You can bind one PWM channel to many GPIOs to control them synchronously.

This technique can be easily adapted to control, e.g. standard and digital servos. PWM signal specification to control servos is presented in the chapter hardware actuators.

Interrupts

Arduino boards used to have a limited set of GPIOs to trigger interrupts. In other MCUs, it is a rule of thumb that almost all GPIOs (but those used, e.g. for external SPI flash) can trigger an interrupt; thus, there is much higher flexibility in, e.g., the use of user interface devices such as buttons.

A special note on ESP8266 and ESP32
Suppose the interrupt routine (function handler) uses any variables or access flash memory. In that case, it is necessary to use some tagging of the ISR function because of the specific, low-level memory management. A use of IRAM_ATTR is necessary (part of the code present in Interrupts:

void IRAM_ATTR ButtonIRS() {  //IRS function
  button_toggle =!button_toggle;
}
If the ISR and some other process write to the memory (variable), providing exclusive access to the variable is important. This may be achieved with so-called Muxes, Semaphores and critical sections to ensure no deadlock will occur. However, it is unnecessary if ISR writes to the variable and some other process is reading it. The use of volatile for the variable should be enough.
Without an advanced configuration, using the float type (hardware accelerated floating point) will cause the application to hang, throwing a panic error and immediate restart of the MCU. It is due to the specific construction of the MCU and FPU. Do not use the float type in interrupt handling. If floating point operations are needed, use double as this one is calculated the software way.

Timers

The number of hardware timers, their features, and specific configuration is per MCU. Even single MCU families have different numbers of timers, e.g., in the case of the STM32 chips, the ESP32, and many others. Those differences, unfortunately, also affect Arduino Framework as there is no uniform HAL (Hardware Abstraction Layer) for all MCUs so far.

A special note on ESP32 MCUs
The number of hardware timers varies between family members. Most ESP32s have 4, but ESP32-C3 has only two [3]. A timer is usually running at some high speed. The most common is 80MHz and requires a prescaller to be useful. Timers periodically call an interrupt (a handler) written by the developer and bound to the timer during the configuration. Because interrupt routines can run asynchronously to the main code and, most of all, because ESP32s (most) are double core, it is necessary to take care of the deadlocks that can appear during the parallel access to the shared memory values, such as service flags, counters etc.
Special techniques using the critical section, muxes and semaphores are needed when more than one routine writes to the shared variable between processes (usually main code and an interrupt handler). However, It is unnecessary in the scenario where the interrupt handler writes to the variable and some other code (e.g. in the loop() section reads it without writing, as in the case of the example presented below.
In this example, the base clock for the timer in the ESP32 chip is 80MHz, and the timer (tHBT - short from Hear Beat Timer) runs at the 1MHz speed (PRESCALLER is 80) and counts up to 2 000 000. So, the interrupt handler is effectively called once every 2 seconds. This code runs separate from the loop() function, asynchronously calling the onHBT() interrupt handler.
onHBT() interrupt handler swaps the boolean value every two seconds. The value then is translated by the main loop() code to drive an LED on the ESP32 development board (here it is GPIO 0), switching it on and off. The onHBT() handler function could directly drive the GPIO to turn the LED on and off. Still, we present a more complex example with a volatile variable LEDOn just for education purposes.

#include "esp32-hal-timer.h"
 
#define LED_GPIO 0       //RED LED on GPIO 0 - vendor-specific
#define PRESCALLER 80    //80MHz->1MHz
#define COUNTER 2000000  //2 million us = 2s
 
volatile bool LEDOn = false;
hw_timer_t *tHBT = NULL; //Heart Beat Timer
 
void IRAM_ATTR onHBT(){  //Heart Beat Timer interrupt handler
  LEDOn = !LEDOn;        //Change true to false and opposite; 
                         //every call
}
 
void setup() {
  Serial.begin(9600);
  pinMode(LED_GPIO, OUTPUT);
 
  tHBT = timerBegin(0, PRESCALLER, true);  
    //Instantiate a timer 0 (first)
    //Most ESP32s (but ESP32-C3) have 4 timers (0-3), 
    //and ESP32-C3 has only two (0-1).
  if (tHBT==NULL) //Check timer is created OK, NULL otherwise
  {
    Serial.println("Timer creation error! Rebooting...");
    delay(1000);
    ESP.restart();
  }
  timerAttachInterrupt(tHBT, &onHBT, true); 
    //Attach interrupt to the timer
  timerAlarmWrite(tHBT, COUNTER, true);     
    //Configure to run every 2s (2000000us) and repeat forever
  timerAlarmEnable(tHBT);
 
}
//Loop function only reads LEDOn value and updates GPIO accordingly
void loop() {
  digitalWrite(LED_GPIO, LEDOn);
}

Timers can also be used to implement a Watchdog. Regarding the example above, it is usually a “one-time” triggered action instead of a periodic one. All one needs to do is to change the last parameter of the timerAlarmWrite function from true to false.