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.
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):
analogReadResolution(res)
- where res
is a value between 9 and 12 (default 12). For 9-bit resolution, you get 0..511 values; for 12-bit resolution, it is 0..4095 respectively. analogSetCycles(ccl)
- where ccl
is number of cycles per ADC sample. The default is 8: the valid number is between 1 and 255.analogSetClockDiv(divider)
- sets base clock divider for the ADC. That has an impact on the speed of conversion.analogSetAttenuation(a)
and analogSetPinAttenuation(GPIO, a)
- sets input attenuation (for all channels or selected channels). The default is ADC_11db
. This parameter reflects the dynamic scaling of the input value:ADC_0db
- no attenuation (1V on input = 1088 reading on ADC), so full scale is 0..1.1V,ADC_2_5db
- 1.34 (1V on input = 2086 reading on ADC), so full scale is 0..1.5V,ADC_6db
- 1.5 (1V on input = 2975 reading on ADC), so full scale is 0..2.2V,ADC_11db
- 3.6 (1V on input = 3959 reading on ADC), so full scale is 0..3.9V.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).
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:
OUTPUT
,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 ...
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.
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; }
volatile
for the variable should be enough.
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.
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
.