Chapter 128: DAC Output and Waveform Generation

Chapter Objectives

By the end of this chapter, you will be able to:

  • Understand the principles of Digital-to-Analog Conversion (DAC).
  • Identify which ESP32 variants include DAC hardware.
  • Configure and use the ESP32’s built-in DAC channels in one-shot mode to output specific DC voltages.
  • Utilize the DAC in continuous (DMA) mode to generate various waveforms like square, sawtooth, and sine waves.
  • Implement the built-in Cosine Wave Generator (CWG) for efficient sine wave production.
  • Understand the differences in DAC capabilities and pin assignments across ESP32, ESP32-S2, and ESP32-S3.
  • Troubleshoot common issues related to DAC operation.

Introduction

In the realm of embedded systems, microcontrollers predominantly operate in the digital domain, processing information as discrete binary values. However, the physical world is largely analog, characterized by continuous signals like sound, light intensity, and sensor readings that vary smoothly over time. To bridge this gap and allow microcontrollers to interact with or control analog devices, a Digital-to-Analog Converter (DAC) is essential.

A DAC takes a digital input value (typically a binary number) and converts it into a proportional analog voltage or current. This capability is crucial for a wide range of applications, including audio signal generation, motor control, sensor simulation, and creating arbitrary analog waveforms for testing or control purposes.

Many ESP32 variants are equipped with built-in DACs, providing an easy way to generate analog output signals directly from the chip. This chapter will guide you through the theory of DAC operation, the specifics of the ESP32’s DAC peripheral, and practical examples using the ESP-IDF v5.x framework to produce both static DC voltages and dynamic waveforms.

Theory

Digital-to-Analog Conversion Fundamentals

A Digital-to-Analog Converter (DAC) translates digital numerical values into corresponding analog voltage levels. Key parameters define a DAC’s performance:

  • Resolution: This is determined by the number of bits the DAC can process. An N-bit DAC can produce 2N distinct analog output levels. For example, an 8-bit DAC (common in ESP32) can produce 28 = 256 discrete voltage steps. A higher resolution means finer control over the output voltage and a smoother approximation of analog signals.
  • Output Voltage Range: This is the range between the minimum and maximum analog voltage the DAC can produce. Typically, for ESP32, this is from 0V to the analog supply voltage (VDD_A, usually 3.3V).
  • Conversion Speed (Settling Time): This is the time it takes for the DAC output to change from one level to another and stabilize within a specified accuracy after the digital input changes.
  • DAC Types: Various architectures exist (e.g., R-2R ladder, weighted resistor). ESP32 typically uses an R-2R ladder type DAC.
Parameter Description Significance
Resolution The number of bits (N) the DAC uses to represent the digital input. An N-bit DAC can produce 2N distinct analog output levels. Higher resolution means finer voltage steps and a smoother, more accurate analog output. ESP32 DACs are typically 8-bit (256 levels).
Output Voltage Range The range between the minimum and maximum analog voltage the DAC can produce. For ESP32, this is typically 0V to VDD_A (analog supply voltage, usually 3.3V). Defines the operational limits of the analog output.
Conversion Speed (Settling Time) The time it takes for the DAC output to change from its previous level to a new level and stabilize within a specified accuracy after the digital input code changes. Affects the maximum frequency of waveforms that can be accurately generated. Faster settling time allows for higher frequency signals.
DAC Types / Architectures The internal structure of the DAC (e.g., R-2R ladder, weighted resistor, string DAC). Affects performance characteristics like linearity, speed, and complexity. ESP32 typically uses an R-2R ladder type.
Reference Voltage (Vref) A stable voltage used by the DAC as a reference to scale its output. The accuracy and stability of Vref directly impact the accuracy of the DAC output. For ESP32 DACs, Vref is usually VDD_A.
Linearity (DNL/INL) Differential Non-Linearity (DNL) and Integral Non-Linearity (INL) measure the deviation of the DAC’s actual output steps from their ideal values. Indicates how accurately the DAC can reproduce a linear change in digital input as a linear change in analog output. Lower DNL/INL is better.

The output voltage (Vout) of an ideal N-bit DAC can often be calculated as:

Vout = (Digital_Input_Value / (2N – 1)) * Vref

Where Vref is the reference voltage, which for ESP32 DACs is typically the analog power supply (VDD_A). For an 8-bit DAC (0-255 values), this becomes:

Vout = (Digital_Input_Value / 255) * VDD_A

ESP32 DAC Hardware

Several ESP32 variants include built-in DAC peripherals.

  • ESP32: Features two 8-bit DAC channels.
    • DAC Channel 1: Connected to GPIO25.
    • DAC Channel 2: Connected to GPIO26.
  • ESP32-S2: Features two 8-bit DAC channels.
    • DAC Channel 1: Connected to GPIO17.
    • DAC Channel 2: Connected to GPIO18.
  • ESP32-S3: Features two 8-bit DAC channels.
    • DAC Channel 1: Connected to GPIO17.
    • DAC Channel 2: Connected to GPIO18.
ESP32 Variant DAC Hardware Number of Channels Resolution DAC Channel 1 (DAC_CHAN_0) DAC Channel 2 (DAC_CHAN_1)
ESP32 (Original) Yes 2 8-bit GPIO25 GPIO26
ESP32-S2 Yes 2 8-bit GPIO17 GPIO18
ESP32-S3 Yes 2 8-bit GPIO17 GPIO18
ESP32-C3 No 0 N/A N/A N/A
ESP32-C6 No 0 N/A N/A N/A
ESP32-H2 No 0 N/A N/A N/A

Warning: ESP32-C3, ESP32-C6, and ESP32-H2 series microcontrollers do not have built-in hardware DAC peripherals. If analog output is required on these chips, alternative methods like using Pulse Width Modulation (PWM) followed by a Low-Pass Filter (LPF) must be employed. This chapter’s direct DAC examples are not applicable to these variants.

The ESP32 DACs can operate in several modes:

  1. One-Shot Mode (Direct Mode): The CPU writes a digital value directly to the DAC register, and the DAC outputs the corresponding voltage. This is suitable for setting DC levels or generating very low-frequency waveforms with direct CPU control.
  2. Continuous Mode (DMA Mode): The DAC uses Direct Memory Access (DMA) to read digital values from a buffer in RAM and convert them sequentially without continuous CPU intervention. This is highly efficient for generating complex, higher-frequency waveforms like audio signals.
  3. Cosine Wave Generator (CWG) Mode: Some ESP32 DACs (ESP32, ESP32-S2, ESP32-S3) include a dedicated hardware module to generate cosine (sine) waves with configurable frequency, amplitude, and phase. This offloads the CPU from needing to calculate or store sine wave samples for basic tone generation.
Operating Mode Description Mechanism Primary Use Cases ESP-IDF Driver
One-Shot Mode (Direct Mode) Outputs a single, static analog voltage level based on a digital value written by the CPU. CPU writes an 8-bit value directly to the DAC’s data register. The DAC holds this voltage until a new value is written. Setting DC bias voltages, simple manual control of analog levels, very low-frequency waveform generation (CPU intensive). driver/dac_oneshot.h
Continuous Mode (DMA Mode) Generates continuous analog waveforms by streaming digital samples from RAM to the DAC using Direct Memory Access (DMA). DMA controller fetches samples from a pre-defined buffer in memory and feeds them to the DAC at a configured rate, without constant CPU intervention. Generating complex or higher-frequency waveforms (e.g., audio, arbitrary signals), reducing CPU load for waveform generation. driver/dac_continuous.h
Cosine Wave Generator (CWG) Mode Utilizes a dedicated hardware module to produce cosine (or sine) waves with configurable parameters. Hardware generates the waveform based on set frequency, amplitude, phase, and offset. CPU only needs to configure it. Efficient generation of basic sine/cosine tones, simple audio alerts, offloading CPU from sine calculation/storage. driver/dac_cosine.h

ESP-IDF DAC Driver (v5.x)

The ESP-IDF provides a comprehensive DAC driver (driver/dac_oneshot.hdriver/dac_continuous.hdriver/dac_cosine.h) to configure and control the DAC peripherals.

1. One-Shot DAC (dac_oneshot.h)

This mode is used for outputting a single, specific voltage level.

  • Handle: dac_oneshot_handle_t represents a configured DAC channel.
  • Configuration: dac_oneshot_config_t structure is used to specify the channel ID.
C
typedef struct {
    dac_channel_t chan_id; /*!< DAC channel id */
} dac_oneshot_config_t;
  • Key Functions:
    • dac_oneshot_new_channel(): Allocates and initializes a DAC one-shot channel.
    • dac_oneshot_output_voltage(): Outputs a voltage corresponding to the given 8-bit digital value (0-255).
    • dac_oneshot_del_channel(): Deallocates a DAC one-shot channel.

2. Continuous DAC / DMA Mode (dac_continuous.h)

This mode is for generating continuous analog waveforms by streaming data from memory to the DAC using DMA.

  • Handle: dac_continuous_handle_t represents a configured DAC continuous mode controller.
  • Configuration:dac_continuous_config_t structure is used.
C
typedef struct {
    dac_channel_mask_t chan_mask;   /*!< DAC channels that need to be enabled */
    uint32_t desc_num;              /*!< Number of DMA descriptors. Max value is 127. Each descriptor can link a buffer up to 4095 bytes */
    size_t buf_size;                /*!< Size of buffer that linked by each DMA descriptor */
    uint32_t freq_hz;               /*!< Frequency of DAC conversion in continuous mode, in Hz.
                                         The DAC digital controller (DigCtl) will work at this frequency.
                                         The clock source of DigCtl is APLL or ACLK, which is selectable in Kconfig.
                                         Range: 60 ~ 20_000_000 Hz for APLL, 150 ~ several MHz for ACLK */
    dac_continuous_data_format_t data_format; /*!< Data format of the buffer that loaded into DMA */
    // ... other fields for DMA configuration
} dac_continuous_config_t;
    • chan_mask: Specifies which DAC channels (1, 2, or both) will be used.
    • desc_num: Number of DMA descriptors. More descriptors allow for larger or more complex data streaming.
    • buf_size: Size of the buffer linked by each DMA descriptor.
    • freq_hz: The rate at which digital samples from the buffer are converted to analog voltages. This determines the output waveform’s frequency characteristics.
    • data_format: Specifies how data is arranged in the buffer (e.g., 8-bit right-aligned, 8-bit left-aligned, or 16-bit for stereo). For standard 8-bit DAC output, DAC_CONTINUOUS_DATA_FORMAT_RIGHT_ALIGNED is common.
graph TB
    subgraph "CPU / System Memory (RAM)"
        direction LR
        B1["Buffer 1 <br> (Waveform Data Segment)"]
        B2["Buffer 2 <br> (Waveform Data Segment)"]
        B3[...]
        BDESC1["DMA Descriptor 1 <br> (Points to Buffer 1, Length)"]
        BDESC2["DMA Descriptor 2 <br> (Points to Buffer 2, Length)"]
        BDESC3["..."]
        BDESC_LIST["DMA Descriptor List <br> (Circular)"]
        
        BDESC1 --> B1
        BDESC2 --> B2
        BDESC3 --> B3
        BDESC_LIST -.-> BDESC1
        BDESC_LIST -.-> BDESC2
        BDESC_LIST -.-> BDESC3
    end

    subgraph ESP32 SoC
        direction TB
        CPU_CTRL["CPU <br> (Initial Setup & Control <br> e.g., <code>dac_continuous_load_buf</code>)"]
        DMA_CTRL[DMA Controller]
        DAC_HW["DAC Hardware <br> (Channel 0/1)"]
    end

    RAM_DAC_DATA["Waveform Data <br> (Digital Samples)"]
    
    CPU_CTRL -- Configures & Starts --> DMA_CTRL
    DMA_CTRL -- Reads Descriptors --> BDESC_LIST
    DMA_CTRL -- Fetches Data based on Descriptor --> RAM_DAC_DATA
    RAM_DAC_DATA -- Streams to --> DAC_HW
    DAC_HW --> OUTPUT["Analog Output <br> (e.g., GPIO25)"]

    %% Styling
    classDef primary fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef memory fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;
    classDef hardware fill:#D1FAE5,stroke:#059669,stroke-width:1px,color:#065F46;
    classDef dataflow fill:#FFF,stroke:#374151,stroke-width:1.5px,color:#374151,stroke-dasharray:5 2;

    class CPU_CTRL,DMA_CTRL process;
    class B1,B2,B3,BDESC1,BDESC2,BDESC3,BDESC_LIST,RAM_DAC_DATA memory;
    class DAC_HW,OUTPUT hardware;
    
    linkStyle 0,1,2,3,4,5,6,7,8,9,10 stroke:#4B5563,stroke-width:1.5px;
    linkStyle 3 stroke-dasharray: 2 2;
    linkStyle 4 stroke-dasharray: 2 2;
    linkStyle 5 stroke-dasharray: 2 2;


    %% More explicit connections for clarity
    subgraph "DMA Operation Flow"
        DMA_CTRL -- "1- Reads next descriptor" --> BDESC_LIST
        BDESC_LIST -- "2- Descriptor points to data buffer" --> B1
        B1 -- "3- Data from buffer" --> RAM_DAC_DATA
        DMA_CTRL -- "4- Transfers data" --> RAM_DAC_DATA
        RAM_DAC_DATA -- "5- Digital samples" --> DAC_HW
    end
    
    style BDESC_LIST fill:#FFFBEB,stroke:#FBBF24
    style DMA_CTRL fill:#BFDBFE,stroke:#60A5FA
    style DAC_HW fill:#A7F3D0,stroke:#34D399
    
    %% Note: This diagram simplifies the circular buffer management which can involve multiple descriptors
    %% pointing to different parts of a larger buffer or a chain of buffers.
    %% The key is DMA reads data from RAM based on descriptors and feeds it to DAC.
  • Key Functions:
    • dac_continuous_new_channels(): Allocates and initializes DAC channel(s) for continuous mode.
    • dac_continuous_enable(): Enables the DAC continuous mode functionality (starts the DMA engine clock, etc.).
    • dac_continuous_load_buf(): Loads the data buffer into the DMA descriptor list. This data will be output continuously.
    • dac_continuous_start_async_writing(): Starts the asynchronous DMA conversion. The DAC will continuously output the loaded buffer.
    • dac_continuous_write(): Synchronously writes data to the DAC. Blocks until data is written.
    • dac_continuous_write_asynchronously(): Asynchronously writes data. Useful for updating the buffer while DMA is running, often used with DMA event callbacks.
    • dac_continuous_disable(): Disables the DAC continuous mode.
    • dac_continuous_del_channels(): Deallocates DAC continuous mode channel(s).

3. Cosine Wave Generator (CWG) (dac_cosine.h)

This hardware feature allows generating a cosine wave without needing a software lookup table or DMA for the waveform itself, only for configuration.

  • Handle: dac_cosine_handle_t represents a configured DAC cosine wave channel.
  • Configuration:dac_cosine_config_t structure.
C
typedef struct {
    dac_channel_t chan_id;      /*!< DAC channel id */
    uint32_t freq_hz;           /*!< Cosine wave frequency, in Hz */
    dac_cosine_clk_src_t clk_src; /*!< Clock source of cosine wave generator */
    int8_t offset;              /*!< DC offset of cosine wave, range is -128 ~ 127.
                                     The corresponding voltage is `offset * VDD / 256 + VDD / 2`.
                                     It's an optional configuration, can be zero by default */
    dac_cosine_phase_t phase;   /*!< Phase of cosine wave, range is 0 ~ 3.
                                     0: 0 degree
                                     1: 90 degree (Sine wave)
                                     2: 180 degree
                                     3: 270 degree
                                     It's an optional configuration, can be zero by default */
    int8_t attenuation;         /*!< Attenuation of cosine wave amplitude, range is 0 ~ 3.
                                     The corresponding peak-to-peak voltage is `VDD / (2^attenuation)`.
                                     It's an optional configuration, can be `DAC_COSINE_ATTEN_DEFAULT` by default */
    // ... other flags
} dac_cosine_config_t;
    • freq_hz: Desired output frequency of the cosine wave.
    • clk_src: Clock source for the CWG (e.g., RTC clock, PLL clock).
    • offset: DC offset of the wave.
    • phase: Phase shift (0 for cosine, 1 (90 degrees) for sine).
    • attenuation: Controls the amplitude of the wave. DAC_COSINE_ATTEN_DB_0 for full scale.
  • Key Functions:
    • dac_new_cosine_channel(): Allocates and configures a DAC cosine wave channel.
    • dac_cosine_start(): Starts the cosine wave generation.
    • dac_cosine_stop(): Stops the cosine wave generation.
    • dac_del_cosine_channel(): Deallocates a DAC cosine wave channel.
graph TD
    A["Start: Generate Cosine/Sine Wave with CWG"] --> B{"Define CWG Configuration <br> (<code>dac_cosine_config_t</code>)"};
    B -- Set Parameters --> C["- Channel ID (<code>chan_id</code>) <br>- Frequency (<code>freq_hz</code>) <br>- Clock Source (<code>clk_src</code>) <br>- Offset (<code>offset</code>) <br>- Phase (<code>phase</code>) <br>- Attenuation (<code>attenuation</code>)"];
    C --> D{"Allocate & Initialize CWG Channel <br> (<code>dac_new_cosine_channel</code>)"};
    D --> E{"Initialization Successful?"};
    E -- Yes --> F["CWG Handle Created <br> (<code>cwg_handle</code> is valid)"];
    E -- "No (Error)" --> G["Handle Error <br> (e.g., Log, Check Config)"];
    F --> H{"Start Cosine Wave Generation <br> (<code>dac_cosine_start</code>)"};
    H --> I{"Wave Generation Successful?"};
    I -- Yes --> J["Cosine/Sine Wave Output <br> on DAC Pin"];
    I -- "No (Error)" --> K["Handle Start Error"];
    J --> L{"Need to Stop or Change?"};
    L -- "Yes, Stop" --> M{"Stop Cosine Wave Generation <br> (<code>dac_cosine_stop</code>)"};
    M --> N{"Deallocate CWG Channel <br> (<code>dac_del_cosine_channel</code>)"};
    L -- "Yes, Change Parameters" --> O{"Stop CWG (<code>dac_cosine_stop</code>) <br> Optional: Deallocate (<code>dac_del_cosine_channel</code>) <br> Modify <code>dac_cosine_config_t</code> <br> Re-initialize (<code>dac_new_cosine_channel</code>) <br> Restart (<code>dac_cosine_start</code>) <br> <i>Note: Some params might be updatable with <br> <code>force_update_config</code> flag and re-calling <br> <code>dac_new_cosine_channel</code> on existing handle.</i>"};
    O --> J;
    N --> P["End"];
    G --> P;
    K --> P;
    L -- "No (Continue Output)" --> J;

    %% Styling
    classDef primary fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef success fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46;
    classDef decision fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;
    classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef error fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B;
    classDef config fill:#E0E7FF,stroke:#A5B4FC,stroke-width:1px,color:#3730A3;

    class A,P primary;
    class B,C config;
    class D,H,M,N,O process;
    class E,I,L decision;
    class F,J success;
    class G,K error;

Practical Examples

Before running these examples, ensure your ESP-IDF environment is set up correctly in VS Code and you have an appropriate ESP32, ESP32-S2, or ESP32-S3 development board. For observing the analog output, a multimeter is useful for DC voltages, and an oscilloscope is ideal for waveforms.

Project Setup:

For each example, you’ll create a new ESP-IDF project or add a new main file to an existing project.

The CMakeLists.txt in your main component directory should include:

Plaintext
idf_component_register(SRCS "main.c"
                    INCLUDE_DIRS ".")

No special Kconfig options are typically needed for DAC, as the drivers are part of the core ESP-IDF.

Example 1: Setting a DC Voltage (One-Shot Mode)

This example demonstrates how to set a specific DC voltage on DAC Channel 1.

main.c:

C
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/dac_oneshot.h"
#include "esp_log.h"

static const char *TAG = "DAC_ONESHOT_EXAMPLE";

// ESP32: DAC_CHAN_0 is GPIO25, DAC_CHAN_1 is GPIO26
// ESP32-S2/S3: DAC_CHAN_0 is GPIO17, DAC_CHAN_1 is GPIO18
#define EXAMPLE_DAC_CHAN DAC_CHAN_0 // Using DAC Channel 1 (GPIO25 on ESP32, GPIO17 on S2/S3)

void app_main(void)
{
    dac_oneshot_handle_t dac_handle;
    dac_oneshot_config_t dac_config = {
        .chan_id = EXAMPLE_DAC_CHAN,
    };

    ESP_LOGI(TAG, "Initializing DAC one-shot channel %d", EXAMPLE_DAC_CHAN);
    ESP_ERROR_CHECK(dac_oneshot_new_channel(&dac_config, &dac_handle));

    // Output a voltage corresponding to digital value 128 (approx. VDD_A / 2)
    // For VDD_A = 3.3V, 128/255 * 3.3V ~= 1.65V
    uint8_t dac_value_mid = 128;
    ESP_LOGI(TAG, "Outputting DAC value: %d", dac_value_mid);
    ESP_ERROR_CHECK(dac_oneshot_output_voltage(dac_handle, dac_value_mid));

    vTaskDelay(pdMS_TO_TICKS(5000)); // Keep voltage for 5 seconds

    // Output a voltage corresponding to digital value 200
    // For VDD_A = 3.3V, 200/255 * 3.3V ~= 2.59V
    uint8_t dac_value_high = 200;
    ESP_LOGI(TAG, "Outputting DAC value: %d", dac_value_high);
    ESP_ERROR_CHECK(dac_oneshot_output_voltage(dac_handle, dac_value_high));

    vTaskDelay(pdMS_TO_TICKS(5000)); // Keep voltage for 5 seconds

    // Output minimum voltage (0)
    ESP_LOGI(TAG, "Outputting DAC value: 0");
    ESP_ERROR_CHECK(dac_oneshot_output_voltage(dac_handle, 0));
    
    vTaskDelay(pdMS_TO_TICKS(5000));

    // Cleanup (optional here as app_main exits, but good practice)
    // ESP_LOGI(TAG, "Deleting DAC channel");
    // ESP_ERROR_CHECK(dac_oneshot_del_channel(dac_handle));
    // ESP_LOGI(TAG, "DAC channel deleted");
    
    ESP_LOGI(TAG, "Example finished. You can measure the voltage on the DAC pin.");
}

Build and Flash Instructions:

  1. Save the code as main.c in your project’s main directory.
  2. Open the ESP-IDF terminal in VS Code.
  3. Set your target ESP32 variant: idf.py set-target esp32 (or esp32s2esp32s3).
  4. Build the project: idf.py build.
  5. Flash the project: idf.py -p /dev/ttyUSB0 flash (replace /dev/ttyUSB0 with your ESP32’s serial port).
  6. Monitor the output: idf.py monitor.

Observe:

  • Connect a multimeter to the DAC output pin (GPIO25 for ESP32, GPIO17 for ESP32-S2/S3) and ground.
  • You should see the voltage change according to the values set (approximately 1.65V, then 2.59V, then 0V, assuming VDD_A is 3.3V).

Example 2: Generating a Square Wave (Toggling One-Shot Mode)

This example generates a simple square wave by rapidly toggling the DAC output between two values in a task.

main.c:

C
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/dac_oneshot.h"
#include "esp_log.h"

static const char *TAG = "DAC_SQUARE_WAVE";

#define EXAMPLE_DAC_CHAN DAC_CHAN_0 // GPIO25 on ESP32, GPIO17 on S2/S3
#define SQUARE_WAVE_FREQ_HZ 100 // Target frequency for the square wave
#define TASK_DELAY_MS (1000 / (2 * SQUARE_WAVE_FREQ_HZ)) // Delay for half period

void app_main(void)
{
    dac_oneshot_handle_t dac_handle;
    dac_oneshot_config_t dac_config = {
        .chan_id = EXAMPLE_DAC_CHAN,
    };

    ESP_LOGI(TAG, "Initializing DAC one-shot channel %d for square wave", EXAMPLE_DAC_CHAN);
    ESP_ERROR_CHECK(dac_oneshot_new_channel(&dac_config, &dac_handle));

    ESP_LOGI(TAG, "Generating square wave at approx %d Hz. Delay per half cycle: %d ms", 
             SQUARE_WAVE_FREQ_HZ, TASK_DELAY_MS);

    uint8_t dac_val_low = 50;  // Approx 0.65V
    uint8_t dac_val_high = 200; // Approx 2.59V

    while (1) {
        ESP_ERROR_CHECK(dac_oneshot_output_voltage(dac_handle, dac_val_high));
        vTaskDelay(pdMS_TO_TICKS(TASK_DELAY_MS));

        ESP_ERROR_CHECK(dac_oneshot_output_voltage(dac_handle, dac_val_low));
        vTaskDelay(pdMS_TO_TICKS(TASK_DELAY_MS));
    }
    // Note: For higher frequencies or more precise timing, DMA mode is preferred.
    // Cleanup is omitted as this task runs indefinitely.
}

Build, Flash, and Observe:

  • Follow the same build/flash steps as Example 1.
  • Observe the output on an oscilloscope. You should see a square wave alternating between approximately 0.65V and 2.59V at around 100 Hz.
  • You can also connect an LED (with an appropriate current-limiting resistor, e.g., 220 Ohm) between the DAC output and GND. You should see it blink.

Tip: For generating precise or high-frequency waveforms, using the DAC in continuous (DMA) mode is much more efficient and accurate than manually toggling values in a task, as vTaskDelay has limited resolution and can be affected by other tasks.

Example 3: Generating a Sawtooth Wave (Continuous/DMA Mode)

This example uses the DAC continuous mode with DMA to generate a sawtooth wave.

main.c:

C
#include <stdio.h>
#include <string.h> // For memset
#include <stdlib.h> // For malloc, free
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/dac_continuous.h"
#include "esp_log.h"
#include "soc/soc_caps.h" // For SOC_DAC_PERIPH_NUM

static const char *TAG = "DAC_SAWTOOTH_DMA";

#define SAWTOOTH_POINTS         256  // Number of points in one sawtooth period (0-255)
#define EXAMPLE_DAC_CHAN_MASK   DAC_CHANNEL_MASK_CH0 // Use DAC Channel 0 (GPIO25 on ESP32, GPIO17 on S2/S3)
// The DAC conversion frequency, determines how fast samples are output.
// If SAWTOOTH_POINTS = 256, and DAC_CONV_FREQ_HZ = 10000,
// then sawtooth wave frequency = 10000 / 256 = ~39 Hz.
#define DAC_CONV_FREQ_HZ        (10 * 1000) // 10 kHz conversion frequency
#define DMA_DESC_NUM            4
#define DMA_BUF_SIZE            1024 // Must be a multiple of SAWTOOTH_POINTS if data_format is 8-bit

static uint8_t *dac_sawtooth_buf = NULL;

void app_main(void)
{
    ESP_LOGI(TAG, "Initializing DAC continuous mode for sawtooth wave");

    dac_continuous_handle_t dac_cont_handle;
    dac_continuous_config_t cont_cfg = {
        .chan_mask = EXAMPLE_DAC_CHAN_MASK,
        .desc_num = DMA_DESC_NUM,
        .buf_size = DMA_BUF_SIZE,
        .freq_hz = DAC_CONV_FREQ_HZ,
        .offset = 0, // No DC offset
        .clk_src = DAC_CONTINUOUS_CLK_SRC_DEFAULT, // Use APLL clock by default
        .data_format = DAC_CONTINUOUS_DATA_FORMAT_RIGHT_ALIGNED, // 8-bit data
    };
    ESP_ERROR_CHECK(dac_continuous_new_channels(&cont_cfg, &dac_cont_handle));

    // Prepare sawtooth waveform data
    dac_sawtooth_buf = (uint8_t *)malloc(SAWTOOTH_POINTS);
    if (!dac_sawtooth_buf) {
        ESP_LOGE(TAG, "Failed to allocate memory for sawtooth buffer");
        return;
    }
    for (int i = 0; i < SAWTOOTH_POINTS; i++) {
        dac_sawtooth_buf[i] = i; // Simple ramp from 0 to 255
    }

    ESP_LOGI(TAG, "Enabling DAC continuous mode");
    ESP_ERROR_CHECK(dac_continuous_enable(dac_cont_handle));

    // Load the buffer to be output continuously by DMA
    // The total buffer length should be a multiple of `SAWTOOTH_POINTS` for cyclic output.
    // Here, we load SAWTOOTH_POINTS, and DMA will repeat this buffer.
    ESP_LOGI(TAG, "Loading sawtooth data to DMA buffer, length: %d bytes", SAWTOOTH_POINTS);
    ESP_ERROR_CHECK(dac_continuous_load_buf(dac_cont_handle, dac_sawtooth_buf, SAWTOOTH_POINTS));
    
    ESP_LOGI(TAG, "Starting asynchronous DMA writing");
    ESP_ERROR_CHECK(dac_continuous_start_async_writing(dac_cont_handle));

    ESP_LOGI(TAG, "Sawtooth wave generation started on DAC Channel 0.");
    ESP_LOGI(TAG, "Wave frequency: %lu Hz", DAC_CONV_FREQ_HZ / SAWTOOTH_POINTS);

    // Keep the task running, DMA operates in the background
    while(1) {
        vTaskDelay(pdMS_TO_TICKS(1000));
    }

    // Cleanup (not reached in this example)
    // dac_continuous_stop_async_writing(dac_cont_handle);
    // dac_continuous_disable(dac_cont_handle);
    // dac_continuous_del_channels(dac_cont_handle);
    // free(dac_sawtooth_buf);
}

Build, Flash, and Observe:

  • Follow the build/flash steps.
  • Observe the output on an oscilloscope connected to the DAC pin. You should see a sawtooth waveform.
  • The frequency of the sawtooth wave will be DAC_CONV_FREQ_HZ / SAWTOOTH_POINTS. With the values above, it’s 10000 / 256 = ~39.06 Hz.

Example 4: Generating a Sine Wave (Continuous/DMA Mode with Lookup Table)

This example generates a sine wave using a pre-calculated lookup table and DMA.

main.c:

C
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h> // For sin()
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/dac_continuous.h"
#include "esp_log.h"

static const char *TAG = "DAC_SINE_DMA";

#define SINE_WAVE_POINTS        256  // Number of points in one sine wave period
#define EXAMPLE_DAC_CHAN_MASK   DAC_CHANNEL_MASK_CH0 // Use DAC Channel 0
// If SINE_WAVE_POINTS = 256, and DAC_CONV_FREQ_HZ = 25600,
// then sine wave frequency = 25600 / 256 = 100 Hz.
#define DAC_CONV_FREQ_HZ        (256 * 100) // Target 100 Hz sine wave
#define DMA_DESC_NUM            4
#define DMA_BUF_SIZE            1024 

static uint8_t *dac_sine_buf = NULL;

void generate_sine_lookup_table(uint8_t *buffer, int points) {
    for (int i = 0; i < points; i++) {
        // sin() returns -1 to 1. We need to scale and offset to 0-255.
        // Value = (sin(angle) + 1) * 127.5
        double val = (sin(2.0 * M_PI * i / points) + 1.0) * 127.5;
        buffer[i] = (uint8_t)round(val);
        if (buffer[i] > 255) buffer[i] = 255; // Clamp to max
    }
}

void app_main(void)
{
    ESP_LOGI(TAG, "Initializing DAC continuous mode for sine wave");

    dac_continuous_handle_t dac_cont_handle;
    dac_continuous_config_t cont_cfg = {
        .chan_mask = EXAMPLE_DAC_CHAN_MASK,
        .desc_num = DMA_DESC_NUM,
        .buf_size = DMA_BUF_SIZE,
        .freq_hz = DAC_CONV_FREQ_HZ,
        .offset = 0,
        .clk_src = DAC_CONTINUOUS_CLK_SRC_DEFAULT,
        .data_format = DAC_CONTINUOUS_DATA_FORMAT_RIGHT_ALIGNED,
    };
    ESP_ERROR_CHECK(dac_continuous_new_channels(&cont_cfg, &dac_cont_handle));

    dac_sine_buf = (uint8_t *)malloc(SINE_WAVE_POINTS);
    if (!dac_sine_buf) {
        ESP_LOGE(TAG, "Failed to allocate memory for sine buffer");
        return;
    }
    generate_sine_lookup_table(dac_sine_buf, SINE_WAVE_POINTS);
    ESP_LOGI(TAG, "Sine lookup table generated.");

    ESP_ERROR_CHECK(dac_continuous_enable(dac_cont_handle));
    ESP_LOGI(TAG, "Loading sine data to DMA buffer, length: %d bytes", SINE_WAVE_POINTS);
    ESP_ERROR_CHECK(dac_continuous_load_buf(dac_cont_handle, dac_sine_buf, SINE_WAVE_POINTS));
    
    ESP_LOGI(TAG, "Starting asynchronous DMA writing");
    ESP_ERROR_CHECK(dac_continuous_start_async_writing(dac_cont_handle));

    ESP_LOGI(TAG, "Sine wave generation started on DAC Channel 0.");
    ESP_LOGI(TAG, "Wave frequency: %lu Hz", DAC_CONV_FREQ_HZ / SINE_WAVE_POINTS);

    while(1) {
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

Build, Flash, and Observe:

  • Follow the build/flash steps.
  • Observe the output on an oscilloscope. You should see a sine wave.
  • The frequency will be DAC_CONV_FREQ_HZ / SINE_WAVE_POINTS. For the example values, (256 * 100) / 256 = 100 Hz.

Example 5: Using the Cosine Wave Generator (CWG)

This example demonstrates using the built-in hardware Cosine Wave Generator. This is often the simplest way to produce a sine or cosine tone.

main.c:

C
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/dac_cosine.h"
#include "esp_log.h"

static const char *TAG = "DAC_COSINE_WAVE";

// ESP32: DAC_CHAN_0 is GPIO25, DAC_CHAN_1 is GPIO26
// ESP32-S2/S3: DAC_CHAN_0 is GPIO17, DAC_CHAN_1 is GPIO18
#define EXAMPLE_CWG_CHAN DAC_CHAN_0 
#define CWG_FREQ_HZ      1000 // 1 kHz

void app_main(void)
{
    ESP_LOGI(TAG, "Initializing DAC Cosine Wave Generator on channel %d", EXAMPLE_CWG_CHAN);

    dac_cosine_handle_t cwg_handle;
    dac_cosine_config_t cwg_cfg = {
        .chan_id = EXAMPLE_CWG_CHAN,
        .freq_hz = CWG_FREQ_HZ,
        .clk_src = DAC_COSINE_CLK_SRC_DEFAULT, // Use RTC Fast clock by default for ESP32, PLL_D2_CLK for S2/S3
        .offset = 0,                           // No DC offset, wave centered at VDD_A / 2
        .phase = DAC_COSINE_PHASE_0,           // 0 degrees (cosine wave)
                                               // Use DAC_COSINE_PHASE_90 for a sine wave
        .atten = DAC_COSINE_ATTEN_DEFAULT,     // Default attenuation (full scale: VDD_A peak-to-peak)
                                               // DAC_COSINE_ATTEN_DB_0 for full scale
                                               // DAC_COSINE_ATTEN_DB_6 for VDD_A / 2 peak-to-peak
                                               // DAC_COSINE_ATTEN_DB_12 for VDD_A / 4 peak-to-peak
                                               // DAC_COSINE_ATTEN_DB_18 for VDD_A / 8 peak-to-peak
        .flags.force_update_config = true,
    };
    ESP_ERROR_CHECK(dac_new_cosine_channel(&cwg_cfg, &cwg_handle));

    ESP_LOGI(TAG, "Starting Cosine Wave Generator at %d Hz", CWG_FREQ_HZ);
    ESP_ERROR_CHECK(dac_cosine_start(cwg_handle));

    ESP_LOGI(TAG, "Cosine wave generation started. Outputting for 10 seconds.");
    vTaskDelay(pdMS_TO_TICKS(10000));

    ESP_LOGI(TAG, "Stopping Cosine Wave Generator.");
    ESP_ERROR_CHECK(dac_cosine_stop(cwg_handle));

    ESP_LOGI(TAG, "Deleting Cosine Wave Generator channel.");
    ESP_ERROR_CHECK(dac_del_cosine_channel(cwg_handle));

    ESP_LOGI(TAG, "Example finished.");
}

Build, Flash, and Observe:

  • Follow the build/flash steps.
  • Observe the output on an oscilloscope. You should see a 1 kHz cosine wave.
  • Try changing phase to DAC_COSINE_PHASE_90 to get a sine wave.
  • Experiment with different atten values to change the amplitude.

Variant Notes

  • DAC Availability:
    • ESP32, ESP32-S2, ESP32-S3: These variants have two 8-bit DAC channels and support one-shot, continuous (DMA), and Cosine Wave Generator (CWG) modes.
      • ESP32 Pins: DAC1 (Channel 0) is on GPIO25, DAC2 (Channel 1) is on GPIO26.
      • ESP32-S2 Pins: DAC1 (Channel 0) is on GPIO17, DAC2 (Channel 1) is on GPIO18.
      • ESP32-S3 Pins: DAC1 (Channel 0) is on GPIO17, DAC2 (Channel 1) is on GPIO18.
    • ESP32-C3, ESP32-C6, ESP32-H2: These variants do not have built-in hardware DACs. The examples in this chapter that use the dac_ driver family will not work on these chips. To generate analog-like outputs on these devices, you would typically use a PWM signal passed through an external Low-Pass Filter (LPF).
  • Cosine Wave Generator (CWG) Clock Source:
    • The default clock source for the CWG (DAC_COSINE_CLK_SRC_DEFAULT) can differ:
      • ESP32: Typically uses RTC_FAST_CLK.
      • ESP32-S2/S3: Typically uses PLL_D2_CLK or a similar high-frequency PLL clock, allowing for a wider range of CWG output frequencies. Refer to the ESP-IDF documentation and soc_caps.h for the specific default for your chip and IDF version.
    • The available frequency range for the CWG depends on the selected clock source and internal dividers.
  • DMA Functionality:
    • While the API is largely consistent, underlying DMA controller details might vary slightly between ESP32, ESP32-S2, and ESP32-S3. However, the dac_continuous driver abstracts these differences.
  • Power Consumption:
    • Using the DAC, especially in continuous mode at high frequencies, will increase power consumption. Consider this for battery-powered applications.
Feature / Aspect ESP32 (Original) ESP32-S2 ESP32-S3 ESP32-C3 / C6 / H2
Hardware DAC Availability Yes (2 channels, 8-bit) Yes (2 channels, 8-bit) Yes (2 channels, 8-bit) No Hardware DAC
DAC Channel 0 Pin GPIO25 GPIO17 GPIO17 N/A
DAC Channel 1 Pin GPIO26 GPIO18 GPIO18 N/A
Supported DAC Modes One-Shot, Continuous (DMA), CWG One-Shot, Continuous (DMA), CWG One-Shot, Continuous (DMA), CWG N/A (Use PWM + LPF for analog-like output)
CWG Default Clock Source (Typical) RTC_FAST_CLK PLL_D2_CLK (or similar high-frequency PLL) PLL_D2_CLK (or similar high-frequency PLL) N/A
CWG Frequency Range More limited by RTC clock speed Wider range due to faster PLL clock source Wider range due to faster PLL clock source N/A
DMA Functionality Supported by dac_continuous driver Supported by dac_continuous driver Supported by dac_continuous driver N/A for DAC
Power Consumption Note DAC usage, especially continuous mode at high frequencies, increases power consumption. N/A for DAC; PWM has its own power implications.

Common Mistakes & Troubleshooting Tips

Mistake / Issue Symptom(s) Troubleshooting / Solution
Incorrect GPIO Pin for DAC Output – No analog output, or output on an unintended pin.
– DAC functions might return errors if pin check is internal (less common for DAC fixed pins).
Verify DAC GPIO pins:
– ESP32: GPIO25 (DAC1/CH0), GPIO26 (DAC2/CH1).
– ESP32-S2/S3: GPIO17 (DAC1/CH0), GPIO18 (DAC2/CH1).
– Use enum DAC_CHAN_0 or DAC_CHAN_1 which maps to correct pins.
Using DAC on Unsupported Chips – Compilation errors (missing symbols for DAC drivers).
– Runtime errors like ESP_ERR_NOT_SUPPORTED when calling dac_..._new_channel().
Check chip compatibility:
– ESP32-C3, ESP32-C6, ESP32-H2 do not have hardware DACs.
– For these, use PWM output with an external Low-Pass Filter (LPF) for analog-like signals.
DMA Buffer Issues (Continuous Mode) – No waveform, incorrect waveform, or crashes.
– DMA errors logged.
– Waveform plays once and stops, or plays garbled data.
Check DMA buffer setup:
– Buffer must be in DMA-capable memory (heap malloc, static, or global). Avoid stack buffers for DMA.
buf_size in dac_continuous_config_t should be adequate.
– Data length in dac_continuous_load_buf() should match intended waveform segment.
– For cyclic output, ensure DMA is configured to loop or buffer is reloaded.
– Data format should be DAC_CONTINUOUS_DATA_FORMAT_RIGHT_ALIGNED for standard 8-bit samples.
Continuous/DMA Mode Not Starting or Incorrect Frequency – No output, or waveform frequency is wrong.
– DAC output remains static.
Verify function call sequence and parameters:
1. dac_continuous_new_channels()
2. dac_continuous_enable()
3. dac_continuous_load_buf() (with valid data and length)
4. dac_continuous_start_async_writing()
freq_hz in config is the sample rate. Waveform freq = freq_hz / points_per_cycle.
CWG Output Issues (No wave, wrong frequency/amplitude) – No output from CWG, or output is static.
– Frequency or amplitude is not as configured.
Check CWG configuration and start sequence:
– Ensure dac_new_cosine_channel() is successful.
– Call dac_cosine_start() after initialization.
– Verify freq_hz, attenuation, phase, and clk_src in dac_cosine_config_t.
– For ESP32, CWG freq range can be limited by RTC_FAST_CLK. ESP32-S2/S3 have wider range with PLL clocks.
Output Voltage Not as Expected (Incorrect Level or Distortion) – Measured DC voltage is off from (value/255) * VDD_A.
– Waveforms are clipped or distorted.
Check power and loading:
– Ensure VDD_A (analog supply, usually 3.3V) is stable and correct.
– DAC output has limited current drive. Heavy loads can pull voltage down or cause distortion. Buffer with an op-amp for higher current needs.
– Minor inaccuracies are normal due to DAC DNL/INL (see datasheet).
– For audio, ensure a DC-blocking capacitor is used if connecting to an AC-coupled input.
Forgetting ESP-IDF Error Checks – Program behaves unexpectedly, crashes without clear cause. Initialization or operation silently fails. Wrap all ESP-IDF DAC function calls with ESP_ERROR_CHECK() or manually check return codes.
– Log errors using ESP_LOGE() to understand failures.

Exercises

  1. Triangle Wave Generation:
    • Modify Example 3 (Sawtooth Wave) to generate a triangle wave using DMA. You’ll need to create a buffer that ramps up from 0 to 255 and then ramps down from 255 to 0.
    • Calculate the expected frequency based on your DAC_CONV_FREQ_HZ and the number of points in your triangle wave buffer. Verify with an oscilloscope.
  2. Adjustable CWG Frequency and Amplitude:
    • Modify Example 5 (Cosine Wave Generator).
    • Connect a potentiometer to an ADC channel (refer to Chapter 126/127 on ADC).
    • Read the ADC value and use it to control:
      • The frequency (freq_hz) of the cosine wave.
      • The attenuation (atten) of the cosine wave.
    • You will need to stop, deinitialize, reconfigure, and restart the CWG channel to apply new settings. (Alternatively, some parameters might be dynamically updatable; check dac_cosine_update_frequency or similar if available, or if force_update_config in dac_cosine_config_t along with re-calling dac_new_cosine_channel or a dedicated update function works). The flags.force_update_config = true when calling dac_new_cosine_channel with an existing handle might allow re-configuration. Otherwise, dac_del_cosine_channel and dac_new_cosine_channel is the safe path.
  3. Dual-Channel Waveform Output (Lissajous Figures):
    • If you have an oscilloscope with X-Y mode:
    • Configure both DAC channels (e.g., DAC_CHAN_0 and DAC_CHAN_1) in continuous (DMA) mode or CWG mode.
    • Output a sine wave on DAC_CHAN_0.
    • Output another sine wave (or cosine wave) on DAC_CHAN_1 with a slightly different frequency or a specific phase relationship (e.g., 90 degrees out of phase).
    • Connect DAC_CHAN_0 to the X-input and DAC_CHAN_1 to the Y-input of the oscilloscope to observe Lissajous figures.
  4. Simple Audio Tone Player:
    • Using the continuous (DMA) mode, generate a buffer representing a simple audio tone (e.g., a 440 Hz sine wave – A4 note).
    • Set the DAC_CONV_FREQ_HZ to a common audio sample rate like 8000 Hz, 16000 Hz, or 22050 Hz.
    • Calculate the number of points needed in your buffer for one cycle of the 440 Hz tone at your chosen sample rate.
    • Connect the DAC output to a small speaker or headphones through a suitable amplifier (an LM386 is a common choice for simple audio amplification) and a DC-blocking capacitor.
    • Warning: Directly connecting a speaker to the DAC output might damage the ESP32 or the speaker due to DC offset and insufficient current. Always use a DC-blocking capacitor and preferably an amplifier.

Summary

  • DACs convert digital values to analog voltages, enabling microcontrollers to interact with the analog world.
  • ESP32, ESP32-S2, and ESP32-S3 have two 8-bit DAC channels. ESP32-C3/C6/H2 do not have hardware DACs.
  • DAC pins are fixed: GPIO25/26 for ESP32, GPIO17/18 for ESP32-S2/S3.
  • One-Shot Mode (dac_oneshot_... API) is for setting static DC voltage levels.
    • Use dac_oneshot_new_channel() and dac_oneshot_output_voltage().
  • Continuous (DMA) Mode (dac_continuous_... API) is for generating continuous waveforms efficiently.
    • Requires configuring DMA descriptors, buffer sizes, and conversion frequency (dac_continuous_config_t).
    • Use dac_continuous_new_channels()dac_continuous_enable()dac_continuous_load_buf(), and dac_continuous_start_async_writing().
  • Cosine Wave Generator (CWG) (dac_cosine_... API) is a hardware feature on supported chips for easy sine/cosine wave generation with configurable frequency, phase, and amplitude.
    • Use dac_new_cosine_channel() and dac_cosine_start().
  • The output voltage is typically 0V to VDD_A (approx 3.3V), mapped from digital values 0-255.
  • For chips without DACs, PWM combined with an external Low-Pass Filter (LPF) can approximate analog output.

Further Reading

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top