Skip to the content.

Embedded Systems Study Group

Basic of ADC and DAC:

ADC

DAC

R-2R Ladder DAC:

Application

ESP32 DAC

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <driver/dac.h>


void app_main(void)
{
    dac_output_enable(DAC_CHANNEL_1);
    while (1) {
        for(int i = 0; i <= 255; i++)
        {
           dac_output_voltage(DAC_CHANNEL_1, i); 
           printf("%d\n", i);
           vTaskDelay(10 / portTICK_PERIOD_MS);
        }
        for(int i = 255; i >= 0; i--)
        {
           dac_output_voltage(DAC_CHANNEL_1, i); 
           printf("%d\n", i);
           vTaskDelay(10 / portTICK_PERIOD_MS);
        }        
        
    }
}

ESP32 ADC

  1. These are minimal initialization required for adc, then raw readings can be read using followig function:

    int val = adc1_get_raw(ADC1_CHANNEL_0);

  2. For more accurate operation one should calibrate and characterize adc before reading adc raw values.
    For more information checkout this adc example code: adc1_example

ESP32 ADC code for reading input wave:

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <driver/dac.h>
#include <driver/adc.h>

void adc_task(void *arg)
{
    adc1_config_width(ADC_WIDTH_BIT_12);
    adc1_config_channel_atten(ADC1_CHANNEL_0,ADC_ATTEN_DB_11); 
    int val = adc1_get_raw(ADC1_CHANNEL_0);
	while(1)
	{	
        val = adc1_get_raw(ADC1_CHANNEL_0);
        printf("%d\n", val);
        vTaskDelay(10 / portTICK_PERIOD_MS);  
    }
	
}

void dac_task(void *arg)
{
    dac_output_enable(DAC_CHANNEL_1);
    while (1) {
        for(int i = 0; i <= 255; i++)
        {
           dac_output_voltage(DAC_CHANNEL_1, i); 
           //printf("%d\n", i);
           vTaskDelay(10 / portTICK_PERIOD_MS);
        }
        for(int i = 255; i >= 0; i--)
        {
           dac_output_voltage(DAC_CHANNEL_1, i); 
           //printf("%d\n", i);
           vTaskDelay(10 / portTICK_PERIOD_MS);
        }  
            
    }
}
       
void app_main(void)
{
    xTaskCreate(&adc_task,"adc_task",4096,NULL,1,NULL);
    xTaskCreate(&dac_task,"dac_task",4096,NULL,1,NULL);
}

Bit manipulations

and operator

a & b does bitwise and of the two values

#include <stdio.h>

int main()
{
    unsigned int val1 = 0x1;
    unsigned int val2 = 0x0;
    unsigned int result = val1 & val2;

    printf("decimal: %d hex: %x\n", result, result);
}

output: decimal: 0 hex: 0

or operator

a | b does bitwise or of the two values

#include <stdio.h>

int main()
{
    unsigned int val1 = 0x1;
    unsigned int val2 = 0x0;
    unsigned int result = val1 | val2;

    printf("decimal: %d hex: %x\n", result, result);
}

output: decimal: 1 hex: 1

« operator

a << b left shifts a by b times.

if a = 00001001 and b = 2, then a << b = 00100100

#include <stdio.h>

void print_bin(unsigned int integer)
{
    int i = 8 * sizeof(int); /* however many bits are in an integer */
    while(i--) {
        putchar('0' + ((integer >> i) & 1)); 
    }
    putchar('\n');
}

int main()
{
    unsigned int val = 0x1;

    for(int i = 0; i < 32; i++)
    {
        print_bin(val);
        val = val << 1;
    }
}

output:

00000000000000000000000000000001
00000000000000000000000000000010
00000000000000000000000000000100
00000000000000000000000000001000
00000000000000000000000000010000
00000000000000000000000000100000
00000000000000000000000001000000
00000000000000000000000010000000
00000000000000000000000100000000
00000000000000000000001000000000
00000000000000000000010000000000
00000000000000000000100000000000
00000000000000000001000000000000
00000000000000000010000000000000
00000000000000000100000000000000
00000000000000001000000000000000
00000000000000010000000000000000
00000000000000100000000000000000
00000000000001000000000000000000
00000000000010000000000000000000
00000000000100000000000000000000
00000000001000000000000000000000
00000000010000000000000000000000
00000000100000000000000000000000
00000001000000000000000000000000
00000010000000000000000000000000
00000100000000000000000000000000
00001000000000000000000000000000
00010000000000000000000000000000
00100000000000000000000000000000
01000000000000000000000000000000
10000000000000000000000000000000

» operator

a >> b right shifts a by b times.

if a = 00001001 and b = 2, then a << b = 00000010

#include <stdio.h>

void print_bin(unsigned int integer)
{
    int i = 8 * sizeof(int); /* however many bits are in an integer */
    while(i--) {
        putchar('0' + ((integer >> i) & 1)); 
    }
    putchar('\n');
}

int main()
{
    unsigned int val = 0x80000000;

    for(int i = 0; i < 32; i++)
    {
        print_bin(val);
        val = val << 1;
    }
}

output:

10000000000000000000000000000000
01000000000000000000000000000000
00100000000000000000000000000000
00010000000000000000000000000000
00001000000000000000000000000000
00000100000000000000000000000000
00000010000000000000000000000000
00000001000000000000000000000000
00000000100000000000000000000000
00000000010000000000000000000000
00000000001000000000000000000000
00000000000100000000000000000000
00000000000010000000000000000000
00000000000001000000000000000000
00000000000000100000000000000000
00000000000000010000000000000000
00000000000000001000000000000000
00000000000000000100000000000000
00000000000000000010000000000000
00000000000000000001000000000000
00000000000000000000100000000000
00000000000000000000010000000000
00000000000000000000001000000000
00000000000000000000000100000000
00000000000000000000000010000000
00000000000000000000000001000000
00000000000000000000000000100000
00000000000000000000000000010000
00000000000000000000000000001000
00000000000000000000000000000100
00000000000000000000000000000010
00000000000000000000000000000001

GPIO

A General Purpose Input/output (GPIO) is an interface available on most modernmicrocontrollers (MCU) to provide an ease of access to the device’s internal properties.

Generally there are multiple GPIO pins on a single MCU for the use of multiple interaction for simultaneous application. The pins can be programmed as input, where data from some external source is being fed into the system to be manipulated at a desired time and location. Output can also be performed on GPIOs.

GPIOs are used in a diverse variety of applications, limited only by the electrical and timing specifications of the GPIO interface and the ability of software to interact with GPIOs in a sufficiently timely manner.

Implementation

GPIO interfaces vary widely. In some cases, they are simple, group of pins that can switch as a group to either input or output. In others, each pin can be set up to accept or source different logic voltages, with configurable drive strengths and pull ups/downs.

A GPIO pin’s state may be exposed to the software developer through one of a number of different interfaces, such as a memory-mapped I/O peripheral, or through dedicated IO port instructions.

A GPIO port is a group of GPIO pins (usually 8 GPIO pins) arranged in a group and controlled as a group.

GPIO abilities may include:

GPIO’s on ESP32

Each gpio can be controlled using register inside the ESP32, but to make our life easy espressif provides a SDK, called ESP-IDF which handles all the talking/reading from the registers and provides nice C functions to use the GPIO, following is the C code to setup GPIO on the esp32, this seems a bit complex but it isn’t. gpio_conf_t is a struct defined in the source code, which holds all the gpio settings.

Config GPIOs

gpio_config_t io_conf;
// bit mask for the pins, each bit maps to a GPIO 
io_conf.pin_bit_mask = (1ULL<<21);

// set gpio mode to input
io_conf.mode = GPIO_MODE_INPUT;

// enable pull up resistors
io_conf.pull_up_en = 1;

// disable pull down resistors
io_conf.pull_down_en = 0;

// disable gpio interrupts
io_conf.intr_type = GPIO_INTR_DISABLE;

// detailed description can be found at https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/gpio.html#_CPPv413gpio_config_t

// configure gpio's according to the setting specified in the gpio struct
esp_err_t err = gpio_config(&io_conf);

This the definition of gpio struct

typedef struct {
    uint64_t pin_bit_mask;          /*!< GPIO pin: set with bit mask, each bit maps to a GPIO */
    gpio_mode_t mode;               /*!< GPIO mode: set input/output mode                     */
    gpio_pullup_t pull_up_en;       /*!< GPIO pull-up                                         */
    gpio_pulldown_t pull_down_en;   /*!< GPIO pull-down                                       */
    gpio_int_type_t intr_type;      /*!< GPIO interrupt type                                  */
} gpio_config_t;

This can be found in esp/esp-idf/components/soc/include/hal/gpio_types.h

Set GPIOs

To set value of a gpio, we use the following function gpio_set_level(), so if we want to set HIGH to gpio 21, we’ll do this: gpio_set_level((gpio_num_t)21, 1)

Internal Implementation of this function is as follows

static inline void gpio_ll_set_level(gpio_dev_t *hw, gpio_num_t gpio_num, uint32_t level)
{
    if (level) {
        if (gpio_num < 32) {
            hw->out_w1ts = (1 << gpio_num);
        } else {
            hw->out1_w1ts.data = (1 << (gpio_num - 32));
        }
    } else {
        if (gpio_num < 32) {
            hw->out_w1tc = (1 << gpio_num);
        } else {
            hw->out1_w1tc.data = (1 << (gpio_num - 32));
        }
    }
}

As you can see, the variable out_w1ts is set to 1 << gpio_num, as this registers represents the register used to manipulate gpio pins, so we are setting bits of this register to accordingly set it’s value in the pin.

This can be found in esp/esp-idf/components/soc/include/hal/gpio_ll.h

Get GPIOs

To get value of a gpio, we use the following function gpio_get_level(), so if we want to get value of gpio 21, we’ll do this: gpio_get_level((gpio_num_t)21)

static inline int gpio_ll_get_level(gpio_dev_t *hw, gpio_num_t gpio_num)
{
    if (gpio_num < 32) {
        return (hw->in >> gpio_num) & 0x1;
    } else {
        return (hw->in1.data >> (gpio_num - 32)) & 0x1;
    }
}

This can be found in esp/esp-idf/components/soc/include/hal/gpio_ll.h

Similarly here we are reading from the input register, which stores the input value of the register and if it is HIGH, the bit is set to 1, or else 0. These registers are mapped to a single gpio pin, but using a mux which is setup while config, we can point this pin to either in/out registers.

We use bit manipulation to find the value stored at that specific bit, so here we right shift value stored in variable in and thus the required bit is the leftmost bit, then we bitwise and it with 0x1. So, if value of pin is 1, then 1 & 1 = 1 and if it is 0, then 0 & 1 = 0

How are GPIO’s handled internally

As we read earlier, these gpio’s are controlled using registers. There registers have been memory mapped, so consider them as a variable pointer which can then be manipulated to set their values.

Typically when we access registers in C based on memory-mapped IO we use a pointer notation to ‘trick’ the compiler into generating the correct load/store operations at the absolute address needed.

Below is the struct used to control the gpio’s, inshort this struct is used to represent the registers in our code as variables. Since a struct is stored as contagious memory location, so if we set the starting address of the this struct to that of the peripheral registers, we can use the struct to manipulate those registers.

Dealing with a memory mapped register is really no different than dealing with any other kind of pointer to memory. The memory mapped file is just a block of data that you can read and write to from any process using the same name.

typedef volatile struct gpio_dev_s {
    uint32_t bt_select;                             /*NA*/
    uint32_t out;                                   /*GPIO0~31 output value*/
    uint32_t out_w1ts;                              /*GPIO0~31 output value write 1 to set*/
    uint32_t out_w1tc;                              /*GPIO0~31 output value write 1 to clear*/

// ....
// ....
// there's more stuff here, but it is not relevant for our lecture
// ....
// ....

uint32_t in;                                    /*GPIO0~31 input value*/
union {
    struct {
        uint32_t data:       8;                 /*GPIO32~39 input value*/
        uint32_t reserved8: 24;
    };
    uint32_t val;
} in1;

// ....
// ....
// there's more stuff here, but it is not relevant for our lecture
// ....
// ....

} gpio_dev_t;
extern gpio_dev_t GPIO;

This can be found in esp/esp-idf/components/soc/soc/esp32/include/soc/gpio_struct.h

How do we set this struct to specific memory location, here’s how we do that !

struct volatile gpio_dev_t *__gpio_reg = (void*)0x3FF44000;

Peripheral register map in ESP32

As we can see the address of GPIO peripheral is 0x3FF4_4000, thus if we point our struct to that address we can write to the register and control the gpio’s

GPIO register map in ESP32

This is the GPIO Register map for GPIO registers in ESP32, each of these register is a 32bit register and has a separate address. So we can individually manipulate each register using it’s address.

The registers that are important for us are the following GPIO_OUT_REG, GPIO_OUT_W1TS_REG, GPIO_OUT_W1TC_REG and GPIO_IN_REG. We can use these registers to manipulate GPIO’s, i.e. read and write from these registers to actually set output HIGH/LOW or read the input on gpios.

This is how the register actually look, since esp32 is a little endian system low address is for LSB and higher address is for MSB.

Using GPIO registers to read/write from GPIOs

Download esp-idf app from here. Unzip it and open the directory. Run idf.py flash.

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"

void app_main(void)
{
    gpio_config_t io_conf;
    // bit mask for the pins, each bit maps to a GPIO 
    io_conf.pin_bit_mask = (1ULL<<2);

    // set gpio mode to input
    io_conf.mode = GPIO_MODE_OUTPUT;

    // enable pull up resistors
    io_conf.pull_up_en = 1;

    // disable pull down resistors
    io_conf.pull_down_en = 0;

    // disable gpio interrupts
    io_conf.intr_type = GPIO_INTR_DISABLE;

    // detailed description can be found at https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/gpio.html#_CPPv413gpio_config_t

    // configure gpio's according to the setting specified in the gpio struct
    esp_err_t err = gpio_config(&io_conf);

    static volatile gpio_dev_t *__gpio_reg = (void*)0x3FF44000;
    
    while(1)
    {
        __gpio_reg->out_w1ts = (1 << 2);
        vTaskDelay(1000/portTICK_PERIOD_MS);
 
        __gpio_reg->out_w1tc = (1 << 2);
        vTaskDelay(1000/portTICK_PERIOD_MS);
    }
}

So, we set struct to address 0x3FF44000 and made it volatile variable as it’s value can change anytime due to external factors. Since we want to Blink led connected on Pin 2 onboard LED, we will set bit-2 as 1, so 1 << 2, and do this to w1ts to set HIGH to gpio and do this to w1tc to set LOW to gpio

Button input using struct to access registers

Download esp-idf app here. Unzip it and open the directory. Run idf.py flash.

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

void app_main(void)
{
    gpio_config_t io_conf;
    // bit mask for the pins, each bit maps to a GPIO 
    io_conf.pin_bit_mask = (1ULL<<0);

    // set gpio mode to input
    io_conf.mode = GPIO_MODE_INPUT;

    // enable pull up resistors
    io_conf.pull_up_en = 1;

    // disable pull down resistors
    io_conf.pull_down_en = 0;

    // disable gpio interrupts
    io_conf.intr_type = GPIO_INTR_DISABLE;

    // detailed description can be found at https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/gpio.html#_CPPv413gpio_config_t

    // configure gpio's according to the setting specified in the gpio struct
    esp_err_t err = gpio_config(&io_conf);

    static volatile gpio_dev_t *__gpio_reg = (void*)0x3FF44000;
    
    while(1)
    {
        int bit_value = __gpio_reg->in >> 0 & 0x1;
        
        ESP_LOGI("gpio","value: %d", bit_value);
        vTaskDelay(10/portTICK_PERIOD_MS);
    }
}

So, we set struct to address 0x3FF44000 and made it volatile variable as it’s value can change anytime due to external factors. Since we want to read the value of gpio 0, which has the boot button connected to it, we won’t need to left shift it, as bit 0 represents state of pin 0. We and it with 0x1, such that we extract the value of the 0th bit, and display it.

Download esp-idf app here. Unzip it and open the directory. Run idf.py flash.

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

void app_main(void)
{
    gpio_config_t io_conf;
    // bit mask for the pins, each bit maps to a GPIO 
    io_conf.pin_bit_mask = (1ULL<<2);

    // set gpio mode to input
    io_conf.mode = GPIO_MODE_OUTPUT;

    // enable pull up resistors
    io_conf.pull_up_en = 1;

    // disable pull down resistors
    io_conf.pull_down_en = 0;

    // disable gpio interrupts
    io_conf.intr_type = GPIO_INTR_DISABLE;

    // detailed description can be found at https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/gpio.html#_CPPv413gpio_config_t

    // configure gpio's according to the setting specified in the gpio struct
    esp_err_t err = gpio_config(&io_conf);

    static volatile uint32_t *gpio_out_set_var = (void*)0x3FF44008;
    static volatile uint32_t *gpio_out_clear_var = (void*)0x3FF4400c;
    
    while(1)
    {
        *gpio_out_set_var = (1 << 2);
        vTaskDelay(1000/portTICK_PERIOD_MS);
 
        *gpio_out_clear_var = (1 << 2);
        vTaskDelay(1000/portTICK_PERIOD_MS);
    }
}

In this case we directly access the two 32-bit registers, by assigning their address to pointers, i.e. 0x3FF44008 for w1ts and 0x3FF4400c for w1tc register.We made it volatile variable as it’s value can change anytime due to external factors. Since we want to Blink led connected on Pin 2 onboard LED, we will set bit-2 as 1, so 1 << 2, and do this to w1ts to set HIGH to gpio and do this to w1tc to set LOW to gpio

Button input using direct memory access

Download esp-idf app here. Unzip it and open the directory. Run idf.py flash.

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

void app_main(void)
{
    gpio_config_t io_conf;
    // bit mask for the pins, each bit maps to a GPIO 
    io_conf.pin_bit_mask = (1ULL<<0);

    // set gpio mode to input
    io_conf.mode = GPIO_MODE_INPUT;

    // enable pull up resistors
    io_conf.pull_up_en = 1;

    // disable pull down resistors
    io_conf.pull_down_en = 0;

    // disable gpio interrupts
    io_conf.intr_type = GPIO_INTR_DISABLE;

    // detailed description can be found at https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/gpio.html#_CPPv413gpio_config_t

    // configure gpio's according to the setting specified in the gpio struct
    esp_err_t err = gpio_config(&io_conf);

    static volatile uint32_t *gpio_in_arr = (void*)0x3FF4403C;
    
    while(1)
    {
        int bit_value = *gpio_in_arr & 0x1;
        
        ESP_LOGI("gpio","value: %d", bit_value);
        vTaskDelay(10/portTICK_PERIOD_MS);
    }
}

In this case we directly access the 32-bit registers, by assigning their address to pointers, i.e. 0x3FF4403C for input register. We made it volatile variable as it’s value can change anytime due to external factors. Since we want to read the value of gpio 0, which has the boot button connected to it, we won’t need to left shift it, as bit 0 represents state of pin 0. We and it with 0x1, such that we extract the value of the 0th bit, and display it.

Assignment