This is an old revision of the document!


Hardware-specific extensions in programming

Some of the generic programming techniques and patterns mentioned above require adaptation for different hardware platforms. It may occur whenever hardware-related aspects are in charge, i.e., 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 (i.e. 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 (i.e. instead of changing the input voltage to reflect the full 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).
A number of 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 for 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.
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 (i.e. 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 effective 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 sixteen (0 to 15) PWM channels (controllers) that can be freely bound to any of the regular GPIOs.
ESP32 can use various “resolutions” of the PWM: from 8-bit (0..255) to 15-bit (0..32767), while regular Arduino uses only 8-bit one.

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

  • configure GPIO pin as OUTPUT,
  • initiate PWM controller by fixing PWM frequency and resolution,
  • bind the controller to the GPIO pin,
  • write to the controller (not to the PIN!) providing a duty cycle related to the resolution selected above - every call persistently sets the PWM duty cycle until there is the next call to the function setting duty cycle.

More information and detailed references, one can find in the technical documentation for the ESP32 chips family [2].

Timers

The number of hardware timers, their features, and specific configuration is per MCU. Even single MCU families have different numbers of timers, i.e., 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) that is 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 with the use of the critical section, muxes and semaphores are needed when more than one routine writes to the shared variable between processes (here 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 (i.e. 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 a LED on the board of the ESP32 development board (here it is GPIO 0), switching it on and off. Obviously, 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;
}
 
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);
  }
en/iot-open/introductiontoembeddedprogramming2/cppfundamentals/hardwarespecific.1689196891.txt.gz · Last modified: 2023/07/12 18:21 (external edit)
CC Attribution-Share Alike 4.0 International
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0