Table of Contents
Espressif’s new ESP32 Boards Manager for the Arduino IDE offers significant performance improvements but has also caused a lot of grief for those attempting to run older ESP32 code. Today, we’ll look at Boards Manager 3 and see how to convert our Boards Manager 2 code to work with it.
Introduction
It’s fair to say that Espressif’s ESP32 line of microcontrollers has become pretty popular with hobbyists. Combined with the easy-to-use Arduino IDE, they make a powerful combination for constructing a variety of wired and wireless microcontroller-based devices.
The integration of the ESP32 and the Arduino IDE is accomplished by a Boards Manager. This component acts as a bridge, providing the necessary tools and resources to compile, upload, and manage code for various ESP32 boards. This includes the core ESP32 libraries, compiler toolchain, and board definitions that enable seamless integration with the Arduino development environment.
Espressif has a long history of releasing Boards Managers, beginning in 2016. In November 2023, they released an Alpha of their latest version, ESP32 Boards Manager 3. This version has significant improvements in security, features, and performance over Boards Manager 2, which has been the default since 2021.
However, many of these improvements have been made at the expense of existing Version 2 APIs, resulting in an epidemic of sketches failing to compile. You may have encountered this yourself, where a sketch that used to compile perfectly is now fraught with error messages.
Today, we will examine Boards Manager 3’s new features and see how we can rewrite older Boards Manager 2 code to compile properly with the new version.
ESP32 Boards Manager 3
The ESP32 Boards Manager 3 introduces several exciting features, including improved Wi-Fi and Bluetooth performance, enhanced security, and better support for peripherals like I2S, SPI, and UART. It includes support for the Matter protocol, which is essential for IoT applications. Additionally, it supports various new boards, such as the ESP32-C6 and ESP32-H2.
This update coincided with the release of ESP-IDF (Espressif IoT Development Framework) version 5.3, the primary development framework for ESP32 devices. ESP-IDF 5.3 introduced new optimizations, security enhancements, and support for the latest ESP32 chips, including improved Wi-Fi and Bluetooth coexistence. Boards Manager 3 incorporates these updates, allowing Arduino IDE users to benefit from many of the same performance enhancements and expanded hardware support available in ESP-IDF 5.3
Keeping Version 2
Despite all of the advantages offered by the new Boards Manager 3, there are a few situations in which you might need to revert to version 2.
One of these situations is when you have a third-party library, such as for a sensor or display, that is not yet updated for Boards Manager 3. Some libraries were dependent upon other Boards Manager libraries, and if they haven’t been updated, they may fail when compiled using version 3.
This situation is becoming less common, as Boards Manager 3 has been the default since July 2024. Most manufacturers have already adapted their coder for it,
You might also have some code from a website (perhaps even this one) that was intended for Boards Manger 2, code that you just want to compile and run to finish a project. If the code is complex, you may not wish to spend the time converting it to compile with Boards Manager 3.
All are valid reasons for using Boards Manager 2. There are a few ways to do this.
1 – Downgrade Your Production Boards Manager
It’s not my personal favorite, but it’s undoubtedly the easiest method. You can search the Arduino IDEs Boards Manager pane for “ESP32”. Find the ESP32 by Espressif Systems entry and use the drop-down to change the version from the current one to 2.0.17, the last stable Boards Manager 2 release. Click the Install button and let all the files copy, which takes a few minutes.
After doing this, it’s probably a good idea to close and reopen the Arduino IDE, but it isn’t a requirement.
I’m not that fond of doing this because I don’t like the idea of downgrading my production environment and potentially messing up some of the libraries. I also work on multiple workstations, and I like consistency.
But, as I said, it is the quickest and easiest method of downgrading to version 2 so that you can compile old code. Once you are done, you can always revert to version 3.
2 – Install Arduino IDE 1.8
I’m assuming that you currently run version 2 of the Arduino IDE. If you just need to compile older Boards Manager 2 code and are willing to accept an older work environment, you can install the legacy Arduino IDE 1.8 in tandem with your current Arduino IDE.
I’d recommend going into Preferences” when you have finished the installation and setting the working directory to something other than the default “Arduino” folder. That way, you won’t risk mixing up libraries between versions.
This works best on Linux or macOS, as you can separate the environments. Due to some Arduino IDE registry entries, Windows users will have more difficulty separating environments.
3 – Virtual Machine
Another way to set up an independent Arduino IDE is to use a Virtual Machine. This allows you to emulate another computer within your current desktop. You can install the Arduino IDE here and keep it completely independent of your host workstation.
If you choose this option, check that your Virtual Machine allows the Arduino IDE to access your USB ports. You will need this to upload your sketches.
4 – Separate Workstation
The most elementary solution is also the best – just configure the Arduino IDE on another workstation with Boards Manager 2.
Of course, this depends on whether you have another workstation. It’s pretty easy to repurpose an older computer to do this; the Arduino IDE doesn’t require a lot of power from its host machine. Perhaps you have an older machine that couldn’t be updated past Windows 10 or could be reformatted with Linux.
You can also run the Arduino IDE on a Raspberry Pi, which can be used in “headless” mode to control it using your preferred workstation.
Once again, Windows users may encounter a “gotcha,” as if you are logged in, the Arduino folder gets shared in your OneDrive. This can be useful, of course, but in our case, it isn’t desirable. The solution here is to change your working directory to a different folder.
Whichever method you choose to establish a Boards Manager 2 environment, remember that it’s only to be used if nothing else works. You’re really advised to move up to Boards Manager 3 and update your code to work with it.
API Updates and Changes
The release of Boards Manager 3 has brought many API changes. Espressif has published a migration guide for updating older sketches. Some of this text has been repeated in the following section.
This is a summary of API changes in Boards Manager 3. These APIs have been affected:
- ADC – Analog to Digital Converter.
- BLE – Bluetooth Low Energy.
- Hall – Hall Sensor (now removed).
- I2S – Inter-IC Sound.
- LEDC – LED and PWM control.
- RMT – Remote Control Transceiver (A hardware module designed for precise control and generation of pulse patterns.)
- SigmaDelta – Sigma Delta modulation module (SigmaDelta is a digital-to-analog conversion tool that generates high-frequency pulse trains to approximate analog voltage levels.)
- Timer – Hardware Timer control.
- UART – Hardware Serial.
- Wi-Fi – WiFi control.
New APIs
Many new APIs have been included in Boards Manager 3.
LEDC – 5
- ledcAttach – Set up the LEDC pin (merged ledcSetup and ledcAttachPin functions).
- ledcOutputInvert – Set a pin as an inverted output.
- ledcFade – Set up and start a fade on a given LEDC pin.
- ledcFadeWithInterrupt – Set up and start a fade on a given LEDC pin with an interrupt.
- ledcFadeWithInterruptArg – Set up and start a fade on a given LEDC pin with an interrupt using arguments.
RMT – 4
- rmtSetEOT – Sets the End of Transmission (EOT) marker for an RMT transmission.
- rmtWriteAsync – Initiates an asynchronous write operation to transmit data via the RMT peripheral.
- rmtTransmitCompleted – Checks if the RMT transmission has been completed.
- rmtSetRxMinThreshold – Sets the minimum pulse width threshold for the RMT in receive mode.
SigmaDelta – 3
- sigmaDeltaAttach – Set up the SigmaDelta pin (channel is acquired automatically).
- timerGetFrequency – Get the actual frequency of the timer.
- timerAttachInterruptArg – Attach the interrupt to a timer using arguments.
Timer – 3
- timerAlarm – Set up Alarm for the timer and enable it automatically (merged timerAlarmWrite and timerAlarmEnable functions).
- timerGetFrequency – Get the actual frequency of the timer.
- timerAttachInterruptArg – Attach the interrupt to a timer using arguments.
Removed APIs
If your Boards Manager 2 code doesn’t compile using the new Boards Manager, the API you are calling may no longer exist.
The following is a list of removed APIs in Boartds Manger 3.
ADC – 3
- analogSetClockDiv – Used to set the clock divider for the ADC module to control the sampling rate. The ADC clock is now managed internally for consistency across the platform, making this function unnecessary.
- adcAttachPin – Used to configure a GPIO pin for ADC functionality. The ADC pin configuration is now handled automatically.
- analogSetVRefPin – Allowed users to set an external reference voltage pin for the ADC. External voltage references are no longer supported in the same way due to hardware and driver changes.
Hall Effect – 1
The Hall Sensor included in the original ESP32 is no longer supported in Boards Manager 3.
- hallRead – Provided a direct reading from the ESP32’s built-in hall effect sensor.
LEDC – 2
- ledcSetup – Configured the frequency and resolution of an LEDC channel for PWM signals. Replaced with more detailed configuration methods that include additional parameters like clock sources and error handling.
- ledcAttachPin – Assigned a GPIO pin to a specific LEDC channel. The pin assignment process has been integrated into the new LEDC initialization functions.
RMT – 6
- _rmtDumpStatus – A debugging function that printed the status of the RMT peripheral. Debugging utilities have been consolidated or removed in favor of external debugging tools.
- rmtSetTick – Set the clock divider for the RMT peripheral to adjust the tick duration. The clock configuration has been reworked to ensure accurate timing without manual adjustment.
- rmtWriteBlocking – Sent pulse data synchronously, blocking execution until the transmission was complete. Replaced by non-blocking APIs to support asynchronous operations and multitasking.
- rmtEnd – Finalized RMT transmission or reception operations. Resource deallocation and cleanup are now handled automatically by the updated RMT driver.
- rmtBeginReceive – Started the RMT in receive mode to capture pulse data. Replaced by more structured APIs for receive mode setup.
- rmtReadData – Retrieved received data from the RMT buffer. Replaced by DMA-based data handling for improved efficiency and accuracy.
SigmaDelta – 2
- sigmaDeltaSetup – Configured the Sigma-Delta modulation peripheral. Replaced with a more robust initialization process that integrates clock and duty configuration.
- sigmaDeltaRead – Read the current Sigma-Delta output configuration or value. Sigma-Delta parameters are now handled in updated APIs or internal registers.
Timer – 16
- timerGetConfig – Get the timer configuration.
- timerSetConfig – Set the timer configuration. Timer configuration is now encapsulated in a unified setup process.
-
- timerGetDivider – Get the clock divider for the timer to adjust its frequency.
- timerSetDivider – Set the clock divider for the timer to adjust its frequency. Higher-level functions now manage timer clock settings.
- timerGetCountUp – Get the timer’s counting direction (up or down).
- timerSetCountUp – Set the timer’s counting direction (up or down). Counting mode is now integrated into the timer’s initialization settings.
- timerGetAutoReload – Get automatic reloading status.
- timerSetAutoReload – Enable or disable automatic reloading of the timer counter. Auto-reload is now configured as part of the timer initialization process.
- timerAlarmEnable – Enable the alarm value for a timer.
- timerAlarmDisable – Disable the alarm value for a timer.
- timerAlarmWrite – Write the alarm value for a timer. A unified interrupt and event-handling system has replaced alarm functionality.
- timerAlarmEnabled – Check if the alarm is enabled.
- timerAlarmRead – Read alarm value. Alarm status is now managed within the new timer driver.
- timerAlarmReadMicros – Read the alarm value in microseconds.
- timerAlarmReadSeconds – Read the alarm value in seconds
- timerAttachInterruptFlag – Attach an interrupt handler with a specific flag to a timer. Timer interrupts are now managed by a more flexible and standardized interrupt-handling system.
Changed APIs
Some of the new Boards Manager’s APIs have changed how they operate.
BLE – 3
- Changed APIs return and parameter type from std::string to Arduino style String.
- Changed UUID data type from uint16_t to BLEUUID class.
- BLEScan::start and BLEScan::getResults methods return type changed from BLEScanResults to BLEScanResults*.
LEDC – 2
- ledcDetachPin was renamed to ledcDetach.
- In all functions, the input parameter channel has been changed to pin.
RMT – 11
- The input parameter rmt_obj_t* rmt has been changed to int pin in all functions.
- rmtInit return parameter changed to bool.
- rmtInit input parameter bool tx_not_rx has been changed to rmt_ch_dir_t channel_direction.
- rmtInit new input parameter uint32_t frequency_Hz to set the frequency of RMT channel (as function rmtSetTick was removed).
- rmtWrite is now sending data in blocking mode. It only returns after sending all data or through a timeout. For Async mode, use the new rmtWriteAsync function.
- rmtWrite new input parameter uint32_t timeout_ms.
- rmtLoop was renamed to rmtWriteLooping.
- rmtRead input parameters changed to int pin, rmt_data_t* data, size_t *num_rmt_symbols, uint32_t timeout_ms.
- rmtReadAsync input parameters changed to int pin, rmt_data_t* data, size_t *num_rmt_symbols.
- rmtSetRxThreshold was renamed to rmtSetRxMaxThreshold, and the input parameter uint32_t value has been changed to uint16_t idle_thres_ticks.
- rmtSetCarrier input parameters uint32_t low, uint32_t high have been changed to uint32_t frequency_Hz, float duty_percent.
SigmaDelta – 2
- sigmaDeltaDetachPin was renamed to sigmaDeltaDetach.
- sigmaDeltaWrite input parameter channel has been changed to pin.
Timer – 2
- timerBegin now has only one parameter (frequency). There is an automatic calculation of the divider using different clock sources to achieve the selected frequency.
- timerAttachInterrupt now has only two parameters. The edge parameter has been removed.
UART – 2
- setHwFlowCtrlMode input parameter uint8_t mode has been changed to SerialHwFlowCtrl mode.
- setMode input parameter uint8_t mode has been changed to SerialMode mode.
Functional Changes
There have been functional changes in the operation of the UART and Wi-Fi.
UART
- Default pins for some SoCs have been changed to avoid conflicts with other peripherals: * ESP32’s UART1 RX and TX pins are now GPIO26 and GPIO27, respectively; * ESP32’s UART2 RX and TX pins are now GPIO4 and GPIO25, respectively; * ESP32-S2’s UART1 RX and TX pins are now GPIO4 and GPIO5, respectively.
- It is now possible to detach UART0 pins by calling end() with no previous call of begin().
- It is now possible to call setPins() before begin() or in any order.
- setPins() will detach any previous pins that have been changed.
- begin(baud, rx, tx) will detach any previous attached pins.
- setPins() or begin(baud, rx, tx) when called at first, will detach console RX0/TX0, attached in boot.
- Any pin set as -1 in begin() or setPins() won’t be changed nor detached.
- begin(baud) will not change any pins that have been set before this call, through a previous begin(baud, rx, tx) or setPin().
- If the application only uses RX or TX, begin(baud, -1, tx) or begin(baud, rx) will change only the assigned pin and keep the other unchanged.
Wi-Fi
- In Arduino (and other frameworks) the method named flush() is intended to send out the transmit buffer content. WiFiClient and WiFiUDP method flush() won’t clear the receive buffer anymore. A new method called clear() is now used for that. Currently flush() does nothing in WiFiClient, WiFiClientSecure and WiFiUDP.
- WiFiServer has functions accept() and available() with the same functionality. In Arduino, available() should work differently, so it is now deprecated.
- WiFiServer had unimplemented write functions inherited from Print class. These are now removed. Also unimplemented method stopAll() is removed. The methods were unimplemented because WiFiServer doesn’t manage connected WiFiClient objects for print-to-all-clients functionality.
ESP32 Boards Manager 3 – I2S, ESP-NOW & LEDC
With the vast number of changes and additions in Boards Manager 3, it would be impossible (or at least impractical) to cover each in detail. So today, we will focus on three APIs, popular ones that have had changes due to the new boards manager:
I2S – The digital audio service supported by the ESP32.
ESP-NOW – The peer-to-peer networking service used with ESP32.
LEDC – The Pulse Width Modulation service for controlling LEDs, motors, and creating sound.
We will look at the changes for each of these APIs and review a code example.
Updated – I2S
I2S (Inter-IC Sound) is an electrical serial bus interface standard and protocol to transmit high-quality digital audio signals between different components, such as digital-to-analog converters (DACs), microcontrollers, and digital signal processors (DSPs). It was developed by Philips Semiconductors in the 1980s and has since become a widely adopted standard in audio equipment.
The ESP32 supports I2S and can act as either a controller or peripheral. Version 3 of the Boards Manager increases the options and parameters for using I2S. Changes in Boards Manager 3 include:
-
- Multichannel Audio – You can now configure I2S to handle more than just stereo (2-channel) audio, allowing for applications like surround sound or multi-microphone arrays.
- Improved Audio Buffer – The I2S driver now utilizes the ESP-IDF’s DMA buffer management system more effectively. This leads to better performance and reduced overhead when transferring audio data.
- PDM Support – Boards Manager 3.0 introduces robust support for Pulse Density Modulation (PDM) microphones and devices.
- Enhanced Timing – The new API includes improvements to timing accuracy and synchronization for I2S clocking and data transmission. This makes the ESP32 more suitable for professional-grade audio projects.
- Simplified Initialization – Designated initializers allow you to set up the structure more concisely and clearly, specifying each field’s value directly within curly braces. This makes the code more readable and easier to understand.
I2S Example – Sweep Tone Generator
To illustrate the use of I2S with ESP32 Boards Manager 3, we will build a simple audio sweep generator. We will use I2S to generate a tone played through an I2S amplifier module and speaker.
You can alter the sweep’s frequency, duration, and other parameters in the sketch.
Tone Generator Hookup
The hookup of the tone generator is fairly simple, but before you head to your parts drawer, you’ll need to choose a suitable ESP32 for the project.
The code will make use of the PSRAM (Pseudo Static RAM) feature that many, but not all, ESP32 modules have. It needs this extra memory for the audio buffer.
I used an ESP32 WROVER Dev Kit for the experiment, as it meets the PSRAM requirements. Here is how it is hooked up to an Adafruit MAX98357A I2S audio amplifier module.
You’ll also need a small 4-8 ohm speaker for the demonstration. I would suggest having a method of disconnecting the speaker, as the sweep tone can get rather annoying!
The ground connection to the GAIN pin is optional. It reduces the amplifier gain, so if you want it louder, you can remove it.
Tone Generator Code
Here is the sketch that we will be using to create our rather annoying audio sweep sound:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
/* I2S Audio Sweep Generator I2SSweepDemoBM3.ino Generate a Frequency Sweep using I2S Requires ESP32 with PSRAM enables (ESP32 WROOM used in demo) Requires AdafruitMAX9857A I2S Amplifier or equivilent DroneBot Workshop 2024 https://dronebotworkshop.com */ // Include drivers & libraries #include <driver/i2s_std.h> #include <cmath> // I2S configuration #define I2S_SAMPLE_RATE 44100 // Now that we have PSRAM, we can use a higher sample rate #define I2S_PORT I2S_NUM_0 #define I2S_BCK_IO GPIO_NUM_26 // BCK pin #define I2S_WS_IO GPIO_NUM_25 // WS pin #define I2S_DO_IO GPIO_NUM_22 // Data Out pin // Audio settings #define SWEEP_START_FREQ 400.0f #define SWEEP_END_FREQ 2000.0f #define SWEEP_DURATION 3.0f // Duration of the sweep in seconds // I2S driver handle i2s_chan_handle_t i2s_handle; // Function to Initialize I2S bool init_i2s() { // Configure I2S Channel 0 in Master Mode i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_PORT, I2S_ROLE_MASTER); // Create new I2S Channel Handle esp_err_t ret_val = i2s_new_channel(&chan_cfg, &i2s_handle, NULL); if (ret_val != ESP_OK) { Serial.printf("Error creating I2S channel: %s\n", esp_err_to_name(ret_val)); return false; } // Clock settings, slot configuration (16-bit mono), and GPIO pins for BCK, WS, and DOUT i2s_std_config_t std_cfg = { .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(I2S_SAMPLE_RATE), .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO), .gpio_cfg = { .mclk = I2S_GPIO_UNUSED, .bclk = I2S_BCK_IO, .ws = I2S_WS_IO, .dout = I2S_DO_IO, .din = I2S_GPIO_UNUSED, .invert_flags = { .mclk_inv = false, .bclk_inv = false, .ws_inv = false, }, }, }; // Initialize the channel in standard (Philips) mode. ret_val = i2s_channel_init_std_mode(i2s_handle, &std_cfg); if (ret_val != ESP_OK) { Serial.printf("Error initializing I2S standard mode: %s\n", esp_err_to_name(ret_val)); return false; } return true; } void setup() { // Start Serial Monitor Serial.begin(115200); // Initialize I2S if (init_i2s()) { Serial.println("I2S Initialized Successfully"); i2s_channel_enable(i2s_handle); } else { Serial.println("Failed to Initialize I2S"); } } void loop() { // Establish Frequency and Phase at beginning of sweep static float currentFreq = SWEEP_START_FREQ; static float phase = 0.0f; // Calculate the number of samples for the entire sweep uint32_t numSamples = (uint32_t)(SWEEP_DURATION * I2S_SAMPLE_RATE); // Allocate memory in PSRAM int16_t *samples = (int16_t *)heap_caps_malloc(numSamples * sizeof(int16_t), MALLOC_CAP_SPIRAM); if (samples == NULL) { Serial.println("PSRAM allocation failed!"); return; // Handle the error appropriately } for (uint32_t i = 0; i < numSamples; i++) { // Linear frequency sweep currentFreq = SWEEP_START_FREQ + (SWEEP_END_FREQ - SWEEP_START_FREQ) * (float)i / numSamples; // Generate a sine wave sample samples[i] = (int16_t)(0.5 * 32767.0f * sin(phase)); // Update the phase phase += 2.0f * PI * currentFreq / I2S_SAMPLE_RATE; if (phase > 2.0f * PI) { phase -= 2.0f * PI; } } // Write samples to I2S size_t bytesWritten; esp_err_t err = i2s_channel_write(i2s_handle, samples, numSamples * sizeof(int16_t), &bytesWritten, 1000); if (err != ESP_OK) { Serial.printf("I2S write error: %s\n", esp_err_to_name(err)); } if (bytesWritten != numSamples * sizeof(int16_t)) { Serial.println("I2S write timeout or incomplete write"); } free(samples); // Free the PSRAM } |
We start the sketch by including the I2S driver, make note of its new location under Boards Manager 3. We also will use the cmath library to calculate the sine wave values for our output waveform.
Next, we specify the I2S sample rate and the connections to the ESP32. The ESP32 has no dedicated I2S pins, so you need to define them in your code.
The next group of settings are for the audio waveform and can be altered to suit your own preferences. Note that these numbers are specified as floats.
We then define a function to initialize the I2S features. Here we:
- Configure an I2S channel. The ESP32 has two I2S channels.
- Create a handle to that channel.
- Set the clock and GPIO pins.
- Initialize the channel
If all works correctly, we return a TRUE; otherwise, we return FALSE.
In Setup, we start a Serial Monitor and call the I2S initialization function. Providing that everything initializes successfully, we move on to the Loop.
In the Loop, we establish the frequency and phase at the beginning of the sweep. After that, we calculate the number of samples required for the entire sweep.
Next, we set up a buffer named “samples” using PSRAM. We need this to store the segments of our waveform, as we are sampling at a rate of 44100 samples per second.
Finally, we generate the sweep. We are creating a linear sweep, so we divide the frequency range by the number of samples. At each point, we generate a sine wave by manipulating the phase of the output waveform and storing the result in the “samples” buffer.
When the wave is complete, we write it to the I2S channel using all the parameters required by Boards Manager 3. Once the waveform has been written, we clear the buffer and finish the Loop.
Upload the sketch to your circuit. Assuming your wiring is good, you should be greeted by a rather loud and annoying audio sweep sound! Note that this sound is generated at audiophile rates, so it is a clean-sounding irritating noise!
You can use the same technique to generate sounds with different waveforms and properties, in fact, you could create a useful tone generator using the I2S functions in ESP32 Boards Manager 3.
Updated – ESP-NOW
ESP-NOW uses ESP32 and ESP8266 boards in a peer-to-peer networking configuration. This arrangement allows ESP32 boards to communicate with each other.
While ESP-NOW utilizes a 2.4 GHz Wi-Fi channel, it does not require Wi-Fi. To maximize performance, the user may select an unused channel.
ESP-NOW Enhancements
Board Manager 3 introduces several enhancements and changes to the ESP-NOW functionality, improving performance, security, and ease of use. Some of the fundamental changes include:
- Enhanced Security – Improved encryption methods for ESP-NOW communication, ensuring data integrity and confidentiality.
- Increased Payload Size – The maximum payload size for ESP-NOW packets has increased, allowing more data to be transmitted in a single packet.
- Improved Pairing Mechanism – Simplified and more reliable pairing process for establishing connections between ESP32 devices.
- Expanded Peer Management – Boards Manager 3 also expands peer management functions, enabling you to dynamically add, modify, or remove peers.
- Better Error Handling – Enhanced error detection and handling mechanisms, such as automatic retries on failure and better acknowledgment handling. Also, error logs can be configured to display different levels of detail for easier debugging.
In addition, there are improvements in power consumption and range.
ESP-NOW Changes – Callback
The most notable change is the callback function used to receive ESP-NOW data. In Board Manager 2, the callback function had three parameters. Board Manager 3 introduces a new structure, as shown here:
BM2:
1 |
(const uint8_t * mac_addr, const uint8_t *incomingData, int len) |
BM3:
1 |
(const esp_now_recv_info_t * esp_now_info, const uint8_t *incomingData, int len) |
The change here is esp_now_recv_info_t, which provides more detailed information about the received data structure, including:
- mac_addr – MAC address of the sender.
- channel – The channel on which the data was received.
- rssi – Received Signal Strength Indicator (RSSI) of the received packet.
- state – State of the peer device.
To update your ESP-NOW code from Board Manager 2 to Board Manager 3, you’ll need to take the following steps:
- Modify the callback function – Change the function signature to include the esp_now_recv_info_t parameter.
- Access data from the structure – Update your code to retrieve the MAC address and other information from the esp_now_info structure instead of the old mac_addr parameter.
ESP-NOW Example – 2-Way Peer-to-Peer Communications
To see how easy it is to use ESP-NOW, we will build a pair of simple communicators, each consisting of an ESP32, an LED, and a push button. The two ESP32s will be set up as peers using ESP-NOW. Pressing the button on one of the peers will light the LED on the other one. The LED toggles, so pressing it again will extinguish it.
ESP-NOW Hookup
I used a couple of ESP32-C6 modules; these are among the modules now supported by ESP32 Boards Manager 3 that were not useable with the previous boards manager. But any ESP32 that is capable of Wi-Fi will work.
You will need to wire up two of these circuits; the ESP32 devices don’t need to be identical.
The push button is a standard momentary contact, normally-open device. You can use any color of LED you like, and the value of the dropping resistor is not critical.
Getting the MAC Address
Before you can write code for these boards you will need to know their MAC (Media Access Control) addresses, as this will be used to identify each peer. The simple sketch shown here will print the MAC address in the serial monitor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
/* MAC Address espMAC.ino Print ESP32 MAC Address DroneBot Workshop 2024 https://dronebotworkshop.com */ #include <WiFi.h> void setup() { Serial.begin(115200); delay(10); // Set the Wi-Fi mode to station mode (STA) WiFi.mode(WIFI_STA); Serial.println(); Serial.print("ESP32 MAC Address: "); Serial.println(WiFi.macAddress()); } void loop() { // Nothing to do here, the MAC address is printed in setup() } |
Make a label with the MAC address for each circuit; it will help avoid confusion when setting things up!
ESP-NOW Code
Now that you have the MAC addresses for both modules, you can move on to their sketches.
Each module will run an identical sketch, with only one difference – they each need the OTHER ESP32s MAC Address. In other words, they require the Peer MAC Address.
The sketch is shown here:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
/* ESP-NOW 2-Way Communication Demo with LED Toggle ESP-NOW2wayToggle.ino DroneBot Workshop 2024 https://dronebotworkshop.com */ // Include Required Libraries #include <esp_now.h> #include <WiFi.h> // REPLACE WITH YOUR RECEIVER ESP32 MAC ADDRESS uint8_t targetMACAddress[] = { 0x40, 0x4C, 0xCA, 0x43, 0x6A, 0x30 }; // Define GPIO Pins #define LED_PIN 2 #define BUTTON_PIN 3 // Structure to send data typedef struct struct_message { bool toggle; // A flag to indicate a toggle request } struct_message; struct_message myData; // Variable to store the LED state on the receiver bool ledState = false; // Callback when data is sent void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) { Serial.print("\r\nLast Packet Send Status:\t"); Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail"); } // Callback when data is received void OnDataRecv(const esp_now_recv_info_t *esp_now_info, const uint8_t *incomingData, int len) { memcpy(&myData, incomingData, sizeof(myData)); Serial.print("Bytes received: "); Serial.println(len); Serial.print("From MAC: "); // Format & print Incoming MAC Address for (int i = 0; i < 6; i++) { Serial.printf("%02X", esp_now_info->src_addr[i]); if (i < 5) Serial.print(":"); } Serial.println(); // Toggle the LED state based on the received flag if (myData.toggle) { ledState = !ledState; digitalWrite(LED_PIN, ledState ? HIGH : LOW); Serial.print("LED state toggled to: "); Serial.println(ledState ? "ON" : "OFF"); } } // Interrupt Service Routine for pushbutton void IRAM_ATTR handleButtonInterrupt() { static unsigned long last_interrupt_time = 0; unsigned long interrupt_time = millis(); // If interrupts come faster than 200ms, assume it's a bounce and ignore if (interrupt_time - last_interrupt_time > 200) { // Send the message - indicate a toggle request myData.toggle = true; esp_err_t result = esp_now_send(targetMACAddress, (uint8_t *)&myData, sizeof(myData)); // Verify data sent OK if (result == ESP_OK) { Serial.println("Sent toggle request from ISR"); } else { Serial.println("Error sending the data from ISR"); } } last_interrupt_time = interrupt_time; } void setup() { // Start Serial Monitor Serial.begin(115200); // WiFi in Station Mode WiFi.mode(WIFI_STA); // Initialize & check ESP-NOW if (esp_now_init() != ESP_OK) { Serial.println("Error initializing ESP-NOW"); return; } // Set Pin Modes for LED and Pushbutton pinMode(LED_PIN, OUTPUT); pinMode(BUTTON_PIN, INPUT_PULLUP); // Attach interrupt to the button pin attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), handleButtonInterrupt, FALLING); // Register ESP-NOW send and receive callbacks esp_now_register_send_cb(OnDataSent); esp_now_register_recv_cb(OnDataRecv); // Find and add peer esp_now_peer_info_t peerInfo = {}; memcpy(peerInfo.peer_addr, targetMACAddress, 6); peerInfo.channel = 0; peerInfo.encrypt = false; if (esp_now_add_peer(&peerInfo) != ESP_OK) { Serial.println("Failed to add peer"); return; } } void loop() { // Not used in this code - all is done within the ISR } |
We start with the required libraries, the ESP-NOW library, and the Wi-Fi library. These are required for all your ESP-NOW sketches.
Next, we have the target MAC address. You must edit this line for each microcontroller, inserting the other ESP32s MAC address in the format shown.
After that, we define the GPIO pins used by the push button and LED. You can move these to different pins if you wish.
All ESP-NOW messages are sent using structured data. In our sketch, this is a very simple structure consisting of only one element – a boolean labeled “toggle.” This indicates a toggle request, which we transmit between the peers. We define our structure in an object named “myData.”
We also define a variable to define the LED state.
Next, we define our two callbacks, which are called when data is sent or received. Pay particular attention to the structure of these callbacks, as it has changed in ESP32 Boards Manager 3.
The send callback simply prints a message with the package sent status. The receive callback is more complicated. It prints out some information, such as the number of bytes received and the MAC address of the peer. It then uses the toggle data to change the status of the LED.
The last function we define is an Interrupt Service Routine for the push button switch. Using an interrupt for the switch instead of polling its status in the Loop is a more reliable method of interfacing with the push button.
Within the ISR, we check the switch status and confirm that it is a genuine button press, not a bounce from the last press. If it is genuine, we send our data packet to the peer. Note that we verify a successful send before resetting the interrupt time and exiting the ISR.
In Setup, we start the Serial Monitor, initialize the Wi-Fi, and start the ESP-NOW. Note that we check the ESP-NOW initialization, mandatory in Boards Manager 3.
Next, we set the pin modes for the switch and LED.
Now, we deal with interrupts and callbacks, assigning each to their proper service routine.
Finally, we go out looking for our peer. We add the peer info and exit Setup.
There is no code in the Loop, as it’s all initiated with the interrupt generated by the push button.
Load the code to both ESP32 modules, making sure to edit each with the correct peer MAC address. Now try it out by observing both the LEDs and the serial monitor on each board (you can open two instances of the Arduino IDE to view both serial monitors).
You should be able to toggle the other boards’ LED and observe the data sent and receive information on the serial monitors.
Updated – LEDC
The LEDC API is not solely for use with LEDs. It is a precision PWM (Pulse Width Modulation) generation system that can also be used for other applications, such as controlling motors or generating tones.
LEDC has changed considerably in Boards Manager 3. Some of the more significant changes include:
- Increased PWM Resolution – Boards Manager 3 increases the PWM resolution from 16 to 20 bits.
- High-Speed Mode – A new high-speed mode has been introduced, enabling PWM frequencies up to 40MHz.
- Enhanced Timer Configuration – The new ledc_timer_config() function allows for more precise control over the PWM signal’s frequency and duty cycle resolution.
- Enhanced Fade Control – The LEDC fade functionality has been improved, offering smoother and more precise control over LED fading.
- Simplified API – Many aspects of the LEDC API have been streamlined in Boards Manager 3.0, making it more straightforward to configure.
LEDC Example – Fade with Interrupt
We can build a simple LED fader to illustrate using the new ledcFadeWithInterrupt function. In this demonstration, the LED simply fades from full to off, and then increases back to full brightness.
The lecdFadeWithInterrupt function is used with a timer to create a very precise fade. You can alter the fade parameters within the code.
LEDC Hookup
The experiment can be run using the built-in LED found in moist ESP32 modules. However, the effect is easier to see with an external LED.
The LED color is not critical. Hook the anode of the LED to GPIO pin 25 (you can use a different pin but will need to change the code to reflect this). Connect the cathode to a 100 ohm dropping resistor, and connect the other end of that resistor to an ESP32 Ground (GND) pin.
LEDC Code
The ledcFadeWithInterrupt function is part of the LEDC (LED Control) API in ESP32 Boards Manager 3. It simplifies creating smooth brightness changes (fading effects) on LEDs or other devices driven by PWM signals. It has the following parameters:
- ledc_channel -The LEDC channel to configure (e.g., LEDC_CHANNEL_0).
- target_duty – The final duty cycle you want to achieve after the fade.
- fade_time -The time, in milliseconds, over which the fade will occur.
- callback – A user-defined function that is called when the fade operation completes.
In our sketch we will use the interrupt generated by ledcFadeWithInterrupt to change the status of a flag. We will then look for that flag in the Loop and use it to set either a fade-on or fade-off.
Here is the complete sketch:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
/* LEDC LED Fader Example LEDCDemoBM3.ino Fades LED using ledcFadeWithInterrupt LED connected to GPIO25 Based upon LEDC Fade example from Espressif docs https://espressif-docs.readthedocs-hosted.com/projects/arduino-esp32/en/latest/api/ledc.html DroneBot Workshop 2024 https://dronebotworkshop.com */ // Use 12 bit precission for LEDC timer #define LEDC_TIMER_12_BIT 12 // Use 5000 Hz as a LEDC base frequency #define LEDC_BASE_FREQ 5000 // LED Pin (replace value with LED_BUILTIN constant for built-in LED) #define LED_PIN 25 // Define starting duty, target duty and maximum fade time #define LEDC_START_DUTY (0) #define LEDC_TARGET_DUTY (4095) #define LEDC_FADE_TIME (3000) // Set status of LED fade bool fade_ended = false; bool fade_on = true; // Interrupt Service Routine void ARDUINO_ISR_ATTR LED_FADE_ISR() { // Set Fade-Ended flag fade_ended = true; } void setup() { // Start Serial Port Serial.begin(115200); while (!Serial) delay(10); // Setup timer and attach timer to an LED ledcAttach(LED_PIN, LEDC_BASE_FREQ, LEDC_TIMER_12_BIT); // Setup and start Fade-On ledcFade(LED_PIN, LEDC_START_DUTY, LEDC_TARGET_DUTY, LEDC_FADE_TIME); Serial.println("LED Fade on started."); // Wait for fade to end delay(LEDC_FADE_TIME); // Setup and start Fade-Off and use Interrupt Service Routine (ISR) ledcFadeWithInterrupt(LED_PIN, LEDC_TARGET_DUTY, LEDC_START_DUTY, LEDC_FADE_TIME, LED_FADE_ISR); Serial.println("LED Fade off started."); } void loop() { // Check if fade_ended flag was set to true in ISR if (fade_ended) { Serial.println("LED fade ended"); // Reset fade_ended flag fade_ended = false; // Check if last fade was Fade-On if (fade_on) { ledcFadeWithInterrupt(LED_PIN, LEDC_START_DUTY, LEDC_TARGET_DUTY, LEDC_FADE_TIME, LED_FADE_ISR); Serial.println("LED Fade off started."); fade_on = false; } else { ledcFadeWithInterrupt(LED_PIN, LEDC_TARGET_DUTY, LEDC_START_DUTY, LEDC_FADE_TIME, LED_FADE_ISR); Serial.println("LED Fade on started."); fade_on = true; } } } |
We start by defining the timer precision, LED base frequency, and the GPIO pin we use. You can change the pin number to use a different GPIO pin if you wish.
The start and target duty cycles are defined. As we use a 12-bit timer, we have a duty cycle ranging from 0 to 4095, with zero being completely off and 4095 being full on. If you want to change the range of the fades, you can do it here. We also define the time, in milliseconds, for the fad to occur.
Two booleans that represent the LED fade status are defined and set.
Next is the interrupt service routine, which is called after each fade operation. It simply sets the fade_ended flag status to TRUE.
In Setup, we start the serial monitor. We then use three of the new ESP32 Boards Manager 3 API calls:
- ledcAttach – We attach the LED
- ledcFade – We do a simple fade
- ledcFadeWithInterrupt – We initiate our first fade with interrupt.
Now we go into the Loop, already having initiated a fade with interrupt. If the interrupt had been completed by now, the fade_ended flag status would have been set to TRUE; otherwise, the initial fade is still on and the flag is still set to FALSE.
If it is TRUE, then we reset the flag and check if the last fade was a fade-on or fade-off. Whatever it is, we perform the opposite fade. Note how the order of the LEDC_START_DUTY and LEDC_TARGET_DUTY is reversed between the two.
And that’s it. The interrupt will set the flag, letting the code in the Loop know it needs to initiate another fade.
Load it up and watch the LED. Experiment with the parameters if you wish. It’s a fun demonstration of a powerful API for PWM.
Conclusion
Espressifs ESP32 Boards Manager 3 introduces significant improvements over version 2, enhancing both functionality and developer experience. Key updates include revamped APIs for I2S, LEDC, Timer, and ESP-NOW, offering greater flexibility and precision in audio processing, PWM control, and wireless communication.
These enhancements give makers and developers a more robust and intuitive platform for creating innovative, high-performance applications. While there is the issue of converting older code to be compatible with the new Boards Manager, the effort is worth it for all of the improvements Boards Manager 3 brings.
Parts List
Here are some components you might need to complete the experiments in this article. Please note that some of these links may be affiliate links, and the DroneBot Workshop may receive a commission on your purchases. This does not increase the cost to you and is a method of supporting this ad-free website.
ESP32 WROVER DevKit | Digikey | Amazon |
ESP32-C6 DevKit | Digikey | Amazon |
MAX98357A I2S Amplifier | Adafruit | Amazon (Generic Version) |
Solderless Breadboard | Amazon |
Resources
Code Samples – All the code used in this article in one easy-to-use ZIP file!
Article PDF – A PDF version of this article in a ZIP file.
ESP32 Boards Manager 3 Migration Guide – A list of changes in Boards Manager 3 (from Espressif).
Espressif I2S Guide – Documentation for the I2S Library.
Espressif ESP-NOW Guide – Documentation for the ESP-NOW Library.
Espressif LEDC Guide – Documentation for the LEDC Library.
ESP32 Boards Manager 3 – Release on GitHub


I tried the ESP NOW sketch above this morning. I used the example sketch in the Arduino 2.x IDE to determine the MAC Addresses of each of my boards. Several MAC addresses appeared for each board, the latter ones were one digit larger than the previous one. When I tried using Bill’s MAC address sketch above it returned a MAC address of 00:00:00:00:00:00 for each board. This puzzles me. I decided to try the first MAC address given for each board from the example sketch. I pasted the MAC address for Board B in the sketch for Board A. I… Read more »
Found my main problem. I was powering one board with 3.3 Vin. I changed it to 5V in and the built in LED became brighter. The boards now communicate as shown in Bill’s video. I tried Bill’s simple MAC address sketch again and always get all zeroes for the MAC address of each board. I tried the Random House Nerds MAC address sketch and the example on the IDE and both gave the same MAC addresses for my boards which are correct. The other two sketches are more complex but do work with my boards. I have some other ideas… Read more »
I modified the ESP NOW sketch in the tutorial above and have been mostly successful. My Master ESP32 board has an onboard relay on it besides a momentary switch. My Responder ESP32 boards just have a momentary switch on them with no LED. The idea is that pushing the momentary on the Master board will toggle the status of its onboard relay. Pushing the momentary on any Responder board will also toggle the status of the onboard relay on the Master. The relay will control a contactor for my shop air compressor. This means I can toggle the status from… Read more »
Thanks for the great article on the changes to ESPNow. I updated an existing project with the new ESPNow with no problems thanks to your article. I have a question about the esp_now_recv_info_t, You mentioned that is now contains the senders MAC Address, Channel, RSSI and Status. However, your sample sketch only shows how to obtain the MAC Address. Can you provide the code to extract the other information. Thanks.