Only two pins on the ESP32-S2 are capable of being DAC outputs:
GPIO23
isDAC_1
GPIO24
isDAC_2
Source: ESP32-S2 datasheet
Only two pins on the ESP32-S2 are capable of being DAC outputs:
GPIO23
is DAC_1
GPIO24
is DAC_2
Source: ESP32-S2 datasheet
You can use the LEDC timer (typically used for PWM) to output a 50% duty cycle clock with 3.3V P-P amplitude on any output-capable GPIO pin.
First,
#include <driver/ledc.h>
then setup the timer. You only need to do this once on startup, no code in your loop function is required.
/** * Setup 2.048MHz clock output on GPIO33 */ ledc_timer_config_t ledc_timer = { .speed_mode = LEDC_HIGH_SPEED_MODE, .bit_num = LEDC_TIMER_2_BIT, .timer_num = LEDC_TIMER_0, .freq_hz = 2048000 }; ledc_channel_config_t ledc_channel = { .gpio_num = GPIO_NUM_33, .speed_mode = LEDC_HIGH_SPEED_MODE, .channel = LEDC_CHANNEL_0, .timer_sel = LEDC_TIMER_0, .duty = 2 }; ledc_timer_config(&ledc_timer); ledc_channel_config(&ledc_channel);
The ESP32 DAC has a built-in cosine waveform generator. Even though it’s an 8-bit DAC, the waveform looks pretty clean.
For an example on how to generate this wavefrm in firmware, see How to use the ESP32 DAC sine/cosine waveform generator using Arduino / PlatformIO
The ESP32 and its derivatives such as the ESP32-S2 have a built-in sine/cosine waveform generator for the built-in 8-bit DAC.
Using it requires ESP-IDF v5.1+ (see the official example). Using it with Arduino is slightly harder, since the stable version of the arduino-esp32
framework at the time of writing this post is based on ESP-IDF v4.4 which does not provide the DAC cosine generator API.
Therefore, we have to explicitly specify the arduino-espressif32
version (git commit) in platformio.ini
:
[env:esp32dev] platform = espressif32 # Commit f9cddfde697b659b9e818ec514f1505d2bd4a8ae is branch esp-idf-v5.1-libs @2022-02-01 platform_packages = framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#f9cddfde697b659b9e818ec514f1505d2bd4a8ae board = esp32dev framework = arduino
The example main source code is pretty simple:
#include <Arduino.h> #include <driver/dac_cosine.h> void setup() { dac_cosine_handle_t chan0_handle; dac_cosine_config_t cos0_cfg = { .chan_id = DAC_CHAN_1, // GPIO26 .freq_hz = 1000, .clk_src = DAC_COSINE_CLK_SRC_DEFAULT, .atten = DAC_COSINE_ATTEN_DEFAULT, .phase = DAC_COSINE_PHASE_0, .offset = 0, //.flags.force_set_freq = false, }; ESP_ERROR_CHECK(dac_cosine_new_channel(&cos0_cfg, &chan0_handle)); ESP_ERROR_CHECK(dac_cosine_start(chan0_handle)); } void loop() { // put your main code here, to run repeatedly: delay(1000); }
If you want to see how the generated waveform looks on an oscilloscope, see How does the ESP32 DAC cosine generator waveform look on an Oscilloscope?
Based on our Minimal PlatformIO ESP8266 ArduinoOTA example, this is a minimal starting point for your ESP32 program running ArduinoOTA.
#include <Arduino.h> #include <WiFi.h> #include <ArduinoOTA.h> void setup() { Serial.begin(115200); /** * Connect to Wifi */ WiFi.begin("MyWifi", "abc123abc"); uint32_t notConnectedCounter = 0; while (WiFi.status() != WL_CONNECTED) { delay(100); Serial.println("Wifi connecting..."); notConnectedCounter++; if(notConnectedCounter > 150) { // Reset board if not connected after 15s Serial.println("Resetting due to Wifi not connecting..."); ESP.restart(); } } Serial.print("Wifi connected, IP address: "); Serial.println(WiFi.localIP()); /** * Enable OTA update */ ArduinoOTA.begin(); } void loop() { // Check for over the air update request and (if present) flash it ArduinoOTA.handle(); }
You can leave your platformio.ini at default values, the only aspect you need to change is to set monitor_speed = 115200
[env:esp32dev] board = esp32dev platform = espressif32 framework = arduino monitor_speed = 115200
It’s as simple as
Serial.println(WiFi.macAddress());
#include <Arduino.h> void setup() { Serial.begin(115200); Serial.println(WiFi.macAddress()); } void loop() { // ... }
uint8_t mac[6]; WiFi.macAddress(mac);
This approach works for Arduino / PlatformIO as well as ESP-IDF projects.
#include <esp_heap_caps.h> uint32_t freeHeapBytes = heap_caps_get_free_size(MALLOC_CAP_DEFAULT); uint32_t totalHeapBytes = heap_caps_get_total_size(MALLOC_CAP_DEFAULT); float percentageHeapFree = freeHeapBytes * 100.0f / (float)totalHeapBytes; // Print to serial Serial.printf("[Memory] %.1f%% free - %d of %d bytes free\n", percentageHeapFree, freeHeapBytes, totalHeapBytes);
Also see How to find number of free bytes in ESP32 memory / heap?
This approach works for Arduino / PlatformIO as well as ESP-IDF projects.
#include <esp_heap_caps.h> uint32_t freeHeapBytes = heap_caps_get_free_size(MALLOC_CAP_DEFAULT);
You see an error message like the following one while compiling your ESP32 project
src/main.cpp:128:3: error: 'vPortGetHeapStats' was not declared in this scope vPortGetHeapStats(&heapStats);
Although vPortGetHeapStats()
is typically defined in freertos/portable.h
, you can not use vPortGetHeapStats()
on the ESP32 since the frameworks do not use the FreeRTOS heap implementation.
In order to find informatio about heap usage, use the ESP heap API such as esp_get_free_heap_size()
.
Almost all of the ESP32 heap_caps_...()
functions take a uint32_t caps
argument.
In case you just want to have general information about the heap, use
MALLOC_CAP_DEFAULT
as an argument.
Most applications will rarely use any other value than MALLOC_CAP_DEFAULT
. Other values which are used semi-frequently include:
MALLOC_CAP_SPIRAM
MALLOC_CAP_INTERNAL
(memory must not be located in SPI RAM)On the ESP32, you can use heap_caps_print_heap_info()
to print information to the serial port about how much memory is free on the heap (plus other details such as the largest free block).
#include <esp_heap_caps.h> void setup() { } void loop() { heap_caps_print_heap_info(MALLOC_CAP_8BIT); }
Heap summary for capabilities 0x00000004: At 0x3ffb8000 len 6688 free 0 allocated 4404 min_free 0 largest_free_block 0 alloc_blocks 8 free_blocks 0 total_blocks 8 At 0x3ffb0000 len 25480 free 0 allocated 22204 min_free 0 largest_free_block 0 alloc_blocks 70 free_blocks 0 total_blocks 70 At 0x3ffae6e0 len 6192 free 8 allocated 3860 min_free 8 largest_free_block 0 alloc_blocks 10 free_blocks 1 total_blocks 11 At 0x3ffb6388 len 7288 free 0 allocated 4524 min_free 0 largest_free_block 0 alloc_blocks 38 free_blocks 0 total_blocks 38 At 0x3ffb9a20 len 16648 free 8 allocated 13964 min_free 0 largest_free_block 0 alloc_blocks 32 free_blocks 1 total_blocks 33 At 0x3ffcc5d0 len 80432 free 8 allocated 73140 min_free 8 largest_free_block 0 alloc_blocks 320 free_blocks 1 total_blocks 321 At 0x3ffe0440 len 15072 free 0 allocated 12260 min_free 0 largest_free_block 0 alloc_blocks 41 free_blocks 0 total_blocks 41 At 0x3ffe4350 len 113840 free 18440 allocated 90724 min_free 2560 largest_free_block 7796 alloc_blocks 157 free_blocks 12 total_blocks 169 Totals: free 18464 allocated 225080 min_free 2576 largest_free_block 7796
If you see an error message like the following one on your microcontroller (such as ESP32):
E (46462) esp-tls-mbedtls: mbedtls_ssl_handshake returned -0x0010
this means MBEDTLS_ERR_MPI_ALLOC_FAILED
. In other words, mbedtls can’t allocate enough memory for its operation.
In order to fix this, try to reduce the amount of memory other parts of your application consume.
If you see an error message like the following one on your microcontroller (such as ESP32):
E (137011) esp-tls-mbedtls: mbedtls_ssl_handshake returned -0x2700
this means MBEDTLS_ERR_X509_CERT_VERIFY_FAILED
.
Either you are using the wrong certificate on the server or you are using the wrong certificate on the mbed-tls side for verifying the certificate.
In order to check the server side, it is often helpful to check the server’s TLS certificate using OpenSSL:
openssl s_client -connect myhostname.com:443
When you see an error message such as
E (169535) esp-tls-mbedtls: mbedtls_ssl_handshake returned -0x3F80
on your microcontroller (e.g. ESP32), this means
MBEDTLS_ERR_PK_ALLOC_FAILED
In other words, there is not enough memory for mbed-tls to work – specifically, there is not enough memory to allocate the public key. Try to reduce the memory usage of your application.
If you see an error message like the following one on your microcontroller (such as ESP32):
E (41544) esp-tls-mbedtls: mbedtls_ssl_handshake returned -0x3B00
this means MBEDTLS_ERR_PK_INVALID_PUBKEY
.
As of the version of mbed TLS used in esp-idf v4.4.3, only RSA & (certain types of) Elliptic Curve keys are supported. In my tests, X25519/EC256
keys didn’t work and there were indications that P-384
keys also didn’t work. Generally, using RSA keys is a safe bet when working with mbed-tls.
When you see an error message such as
E (61175) esp-tls-mbedtls: mbedtls_ssl_setup returned -0x7F00
on your microcontroller (e.g. ESP32), this means
MBEDTLS_ERR_SSL_ALLOC_FAILED
In other words, there is not enough memory for mbed-tls to work. Try to reduce the memory usage of your application.
On FreeRTOS on the ESP32, you want to use a critical zone like this:
portENTER_CRITICAL(); // Your critical code goes here! portEXIT_CRITICAL();
but while compiling the procject, you see an error message like
src/main.cpp: In function 'void MyFunc(size_t, int16_t)': /home/uli/.platformio/packages/[email protected]d/tools/sdk/esp32/include/freertos/port/xtensa/include/freertos/portmacro.h:476:75: error: too few arguments to function 'void vPortEnterCritical(portMUX_TYPE*)' #define portENTER_CRITICAL(mux) vPortEnterCritical(mux)
You need to use portENTER_CRITICAL()
and portEXIT_CRITICAL()
with a spinlock, i.e.
portENTER_CRITICAL(&mySpinlock); // TODO Your critical code goes here! portEXIT_CRITICAL(&mySpinlock);
In order to see a full example on how to initialize a spinlock in FreeRTOS and use it for critical zones, see our previous post ESP32 critical zone example using FreeRTOS / PlatformIO
In order to enter a critical zone on the ESP32 using FreeRTOS, you have to do the following:
Globally declare a spinlock:
portMUX_TYPE mySpinlock;
In setup()
, initialize the spinlock:
spinlock_initialize(&mySpinlock);
Now, wherever you want to enter a critical zone, run:
portENTER_CRITICAL(&mySpinlock); // TODO Your critical code goes here! portEXIT_CRITICAL(&mySpinlock);
When using this in an interrupt handler, use this instead:
portENTER_CRITICAL_ISR(&mySpinlock); // TODO Your critical code goes here! portEXIT_CRITICAL_ISR(&mySpinlock);
FreeRTOS will ensure that no two threads using mySpinlock
are run at the same time.
Based on our previous post How to generate PWM output representing a sine wave on the ESP32 (Arduino/PlatformIO) this post uses two different IO pins to generate both a sine and a cosine wave dynamically.
#include <Arduino.h> #include <driver/ledc.h> void setup() { Serial.begin(115200); ledcSetup(LEDC_CHANNEL_0, 10000 /* Hz */, 12); ledcSetup(LEDC_CHANNEL_1, 10000 /* Hz */, 12); ledcAttachPin(GPIO_NUM_32, LEDC_CHANNEL_0); ledcAttachPin(GPIO_NUM_25, LEDC_CHANNEL_1); } /** * @brief Calculate the PWM duty cycle (assuming 12 bits resolution) of a sine wave of * given frequency. micros() is used as a timebase * * @param frequency The frequency in Hz * @return int the corresponding 12-bit PWM value */ int sinePWMValue(float frequency, int maxPWMValue, float (*sinCos)(float)) { unsigned long currentMicros = micros(); // get the current time in microseconds // calculate the sine wave value for the current time int halfMax = maxPWMValue/2; int sineValue = halfMax + (halfMax-10) * sinCos(2 * PI * currentMicros / (1000000 / frequency)); return sineValue; } void loop() { // Example of how to change the duty cycle to 25% ledcWrite(LEDC_CHANNEL_0, sinePWMValue(1.0, 4096, sinf)); ledcWrite(LEDC_CHANNEL_1, sinePWMValue(1.0, 4096, cosf)); }
The output, filtered by a 4th order Salley-Key filter each (using the LM324) looks like this: