Both sides previous revisionPrevious revisionNext revision | Previous revision |
en:iot-open:introductiontoembeddedprogramming2:cppfundamentals:hardwarespecific [2023/08/20 08:01] – pczekalski | en:iot-open:introductiontoembeddedprogramming2:cppfundamentals:hardwarespecific [2023/11/23 10:24] (current) – pczekalski |
---|
==== Hardware-specific extensions in programming ==== | ====== 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.\\ | {{:en:iot-open:czapka_b.png?50| General audience classification icon }}{{:en:iot-open:czapka_m.png?50| General audience classification icon }}{{:en:iot-open:czapka_e.png?50| General audience classification icon }}\\ |
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. | 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 === | ==== 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. | 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 == | ** 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 ((https://docs.espressif.com/projects/esp-idf/en/v4.2/esp32/api-reference/peripherals/adc.html)). | 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 ((https://docs.espressif.com/projects/esp-idf/en/v4.2/esp32/api-reference/peripherals/adc.html)). |
| |
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. \\ | 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)''.\\ | Just execute ''analogRead(GPIO)''.\\ |
A number of useful functions are here (not limited to): | 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. | * ''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. | * ''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. | * ''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: | * ''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_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_2_5db'' - 1.34 (1V on input = 2086 reading on ADC), so full scale is 0..1.5V, |
* ''ADC_11db'' - 3.6 (1V on input = 3959 reading on ADC), so full scale is 0..3.9V. | * ''ADC_11db'' - 3.6 (1V on input = 3959 reading on ADC), so full scale is 0..3.9V. |
| |
<note important>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).</note> | <note important>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).</note> |
| |
=== Analog output === | ==== Analog output ==== |
PWM frequently controls analogue-style, efficient voltage on the GPIO pin. Instead of using a resistance driver, | 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. | 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). | It applies to motors, LEDs, bulbs, heaters and indirectly to the servos (but that works another way). |
| |
== A special note on ESP32 MCUs == | ** A special note on ESP32 MCUs **\\ |
The classical ''analogWrite'' method, known from Arduino (Uno, Mega) and ESP8266, does not work for ESP32.\\ | 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.\\ Exact number of PWM channels depends on the family member of the ESP chips, i.e. 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: i.e. with high PWM frequency, you cannot go with a resolution too high as the internal frequency of the ESP32 chip is limited. | 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: | To use PWM in ESP32, one must perform the following steps: |
* initiate PWM controller by fixing PWM frequency and resolution, | * initiate PWM controller by fixing PWM frequency and resolution, |
* bind the controller to the GPIO pin, | * 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. | * 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 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 ((https://docs.espressif.com/projects/esp-idf/en/v4.2/esp32/api-reference/peripherals/dac.html)). | More information and detailed references can be found in the technical documentation for the ESP32 chips family ((https://docs.espressif.com/projects/esp-idf/en/v4.2/esp32/api-reference/peripherals/dac.html)). |
| |
Sample code controlling a LED with 5kHz frequency and 8-bit resolution is presented below: | Sample code controlling an LED on GPIO 26 with 5kHz frequency and 8-bit resolution is presented below: |
<code c> | <code c> |
#include "Arduino.h" | #include "Arduino.h" |
... | ... |
| |
ledcSetup(PWM1_Ch, PWM_Freq, PWM_Res); //Instantiate timer-based PWM -> PWM channel | ledcSetup(PWM1_Ch, PWM_Freq, PWM_Res); |
ledcAttachPin(RGBLED_R, PWM1_Ch); //Bind a PWM channel to the GPIO | //Instantiate timer-based PWM -> PWM channel |
ledcWrite(PWM1_Ch,255); //Full on: control via the PWM channel, not via the GPIO | 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 |
... | ... |
</code> | </code> |
| |
| <note tip>You can bind one PWM channel to many GPIOs to control them synchronously.</note> |
| |
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 [[en:iot-open:hardware2:actuators_motors|hardware actuators]]. | 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 [[en:iot-open:hardware2:actuators_motors|hardware actuators]]. |
| |
=== Interrupts === | ==== 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, i.e. for external SPI flash) can trigger an interrupt; thus, there is much higher flexibility in, i.e. use of user interface devices such as buttons. | 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 == | ** 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 [[en:iot-open:introductiontoembeddedprogramming2:cppfundamentals:interrupts|]]: | 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 [[en:iot-open:introductiontoembeddedprogramming2:cppfundamentals:interrupts|]]: |
<code c> | <code c> |
</code> | </code> |
| |
<note important>If the ISR and some other process both 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.</note> | <note important>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.</note> |
| |
| <note warning>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.</note> |
| |
=== Timers === | ==== 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. | 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 == | ** 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 ((https://espressif-docs.readthedocs-hosted.com/projects/arduino-esp32/en/latest/api/timer.html)). 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.\\ | The number of hardware timers varies between family members. Most ESP32s have 4, but ESP32-C3 has only two ((https://espressif-docs.readthedocs-hosted.com/projects/arduino-esp32/en/latest/api/timer.html)). 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 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.\\ | 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.\\ | 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. | ''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. |
<code c> | <code c> |
#include "esp32-hal-timer.h" | #include "esp32-hal-timer.h" |
| |
#define LED_GPIO 0 //RED LED on GPIO 0 - vendor specific | #define LED_GPIO 0 //RED LED on GPIO 0 - vendor-specific |
#define PRESCALLER 80 //80MHz->1MHz | #define PRESCALLER 80 //80MHz->1MHz |
#define COUNTER 2000000 //2 million us = 2s | #define COUNTER 2000000 //2 million us = 2s |
| |
volatile bool LEDOn = false; | volatile bool LEDOn = false; |
| |
void IRAM_ATTR onHBT(){ //Heart Beat Timer interrupt handler | void IRAM_ATTR onHBT(){ //Heart Beat Timer interrupt handler |
LEDOn = !LEDOn; //Change true to false and opposite; every call | LEDOn = !LEDOn; //Change true to false and opposite; |
| //every call |
} | } |
| |
pinMode(LED_GPIO, OUTPUT); | pinMode(LED_GPIO, OUTPUT); |
| |
tHBT = timerBegin(0, PRESCALLER, true); //Instantiate a timer 0 (first) | tHBT = timerBegin(0, PRESCALLER, true); |
// Most ESP32s (but ESP32-C3) have 4 timers (0-3), and ESP32-C3 has only two (0-1). | //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 | if (tHBT==NULL) //Check timer is created OK, NULL otherwise |
{ | { |
ESP.restart(); | ESP.restart(); |
} | } |
timerAttachInterrupt(tHBT, &onHBT, true); //Attach interrupt to the timer | timerAttachInterrupt(tHBT, &onHBT, true); |
timerAlarmWrite(tHBT, COUNTER, true); //Configure to run every 2s (2000000us) and repeat forever | //Attach interrupt to the timer |
| timerAlarmWrite(tHBT, COUNTER, true); |
| //Configure to run every 2s (2000000us) and repeat forever |
timerAlarmEnable(tHBT); | timerAlarmEnable(tHBT); |
| |
} | } |
</code> | </code> |
Timers can also be used to implement a Watchdog. Regarding the example above, it is usually a "one-time" triggered action instead of a periodical one. All one needs to do is to change the last parameter of the ''timerAlarmWrite'' function from ''true'' to ''false''. | 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''. |