ADC Input

This program was an introduction to ADC input in order to control the brightness of an LED connected to a GPIO pin. A 3-pin potentiometer was used to control the input to the ADC of the Pico. This was also an introduction to the input on a GPIO pin. The resources for the project include the C SDK User Guide, the RP2040 Datasheet and Prof. Hunter's website.

The RaspberryPi Pico has a 12-bit ADC. That means that the range of ADC input is 0 to 4095 for the given voltage range (0-3.3V here). Then, I used this input to control the brightness of the LED. Since the maximum of ADC input is 4095 and the maximum PWM interrupt cycles in a signal is 65536, I chose a multiplier of 16 in order to scale the ADC input to the duty cycle.


The complete code

/*
 * Parth Sarthi Sharma (pss242@cornell.edu)
 * Code based on examples from Raspberry Pi Foundation.
 * The code initializes a pin to be PWM output and a pin
 * to be ADC input. Based on the input from the ADC input,
 * the brightness of the LED is changed.
 */
#include <stdio.h> //The standard C library
#include "pico/stdlib.h" //Standard library for Pico
#include "pico/time.h" //The pico time library
#include "hardware/irq.h" //The hardware interrupt library
#include "hardware/pwm.h" //The hardware PWM library
#include "hardware/adc.h" //The hardware ADC library
#include "hardware/gpio.h" //The hardware GPIO library

#define LED 16 //The LED Pin

int brightness; //Brightness level

void wrapHandler(){ //The PWM wrap handler function
    pwm_clear_irq(pwm_gpio_to_slice_num(LED)); //Clear the interrupt flag
    pwm_set_gpio_level(LED, brightness * 16); //Set the PWM level for the slice and channel associated with a GPIO
}

int main() {
    stdio_init_all(); //Initialize all of the present standard stdio types that are linked into the binary
    gpio_set_function(LED, GPIO_FUNC_PWM); //Set the LED Pin to be PWM
    uint sliceNum = pwm_gpio_to_slice_num(LED); //Get PWM slice number

    adc_init(); //Initialise the ADC hardware
    adc_gpio_init(26); //Initialise the gpio for use as an ADC pin
    adc_select_input(0); //Select an ADC input

    pwm_clear_irq(sliceNum); //Clear the IRQ for the linked slice
    pwm_set_irq_enabled(sliceNum, true); //Enable the IRQ for the given slice
    irq_set_exclusive_handler(PWM_IRQ_WRAP, wrapHandler); //Set an exclusive interrupt handler for the interrupt
    irq_set_enabled(PWM_IRQ_WRAP, true); //Enable or disable a specific interrupt on the executing core

    pwm_config config = pwm_get_default_config(); //Get a set of default values for PWM configuration
    pwm_config_set_clkdiv(&config, 4.f); //Set clock divider in a PWM configuration
    pwm_init(sliceNum, &config, true); //Initialise a PWM with settings from a configuration object

    while (1) { //While eternity
        brightness = adc_read(); //Read the ADC input
        sleep_ms(10); //Sleep for 10 milliseconds
    }
}


Stepping through the code

Includes

The first lines of code in the C source file include some header files. One of these is standard C headers (stdio.h) and the others are headers which come from the C SDK for the Raspberry Pi Pico. The first of these, pico/stdlib.h is what the SDK calls a "High-Level API." These high-level API's "provide higher level functionality that isn’t hardware related or provides a richer set of functionality above the basic hardware interfaces." The architecture of this SDK is described at length in the SDK manual. All libraries within the SDK are INTERFACE libraries.

The next includes pull in hardware APIs which are not already brought in by pico/stdlib.h. These include hardware/irq.h, hardware/pwm.h, hardware/adc.h, hardware/gpio.h and pico/time.h. As the names suggest, these interface libraries give us access to the API's associated with the hardware PWM, hardware IRQ, hardware ADC, hardware GPIO and pico time on the RP2040.

Don't forget to link these in the CMakeLists.txt file!

#include <stdio.h>
#include "pico/stdlib.h"
#include "pico/time.h"
#include "hardware/irq.h"
#include "hardware/pwm.h"
#include "hardware/adc.h"
#include "hardware/gpio.h"


Global declarations and defines

The next section of the code is the #define's and the global variables which will be used throughout the code. These are the LED pin number (GPIO 16 in this case) and the brightness declaration.

#define LED 16
int brightness;


The PWM wrap handler

The wrapHandler() is the function that is called every time the PWM timer throws an interrupt. I cleared the interrupt as soon as the interrupt handler is called. Since I wanted to change the brightness according to the input from the ADC, I used a multiplier of 16 in order to scale the ADC input to the duty cycle. I used the pwm_set_gpio_level() function to change the duty cycle. Since the PWM in the RP2040 is 16-bit wide, it can take in values from 0 to 65536. If the input value is 0 then the duty cycle is 0% while if the input is 65536, the duty cycle is 100% (only if the PWM wrapping is set to be 0xFFFF). Every time the handler function is called, the new brightness level is sent into the pwm_set_gpio_level() function to change the duty cycle.

void wrapHandler(){
    pwm_clear_irq(pwm_gpio_to_slice_num(LED));
    pwm_set_gpio_level(LED, brightness * 16);
}


The main function

Initializing UART

The first line in main() is a call to stdio_init_all(). This function initializes stdio to communicate through either UART or USB, depending on the configurations in the CMakeLists.txt file.

stdio_init_all();


Initializing the PWM

I used the function gpio_set_function(LED, GPIO_FUNC_PWM); to set the LED as a PWM pin. Then I used the pwm_gpio_to_slice_num() function to get the PWM slice number for the LEDPin. Next, I used the pwm_clear_irq() to clear the interrupt on the given PWM slice. This allows me to enable the PWM interrupt on the given slice using the function pwm_set_irq_enabled().

After setting up the PWM interrupt for the given pin, I had to configure the interrupt handler function. I used the irq_set_exclusive_handler() function to do so. Now, whenever the PWM interrupt flag is set, it calls the interrupt handler function. All I needed to do next was to call the irq_set_enabled() function to enable the interrupt.

Now that the interrupt was setup, it was time to configure the PWM. In order to do so, I used the pwm_get_default_config() function to get the default configurations for the PWM. According to the SDK documentation "PWM config is free running at system clock speed, no phase correction, wrapping at 0xffff, with standard polarities for channels A and B." Right now, the PWM interrup will be thrown every single clock cycle. In order to avoid that, I used the pwm_config_set_clkdiv() function to set the clock divider to 4 so that the PWM interrupt is thrown every 4 clock cycles. Lastly, I initialized the PWM with the set configurations using the pwm_init() function.

gpio_set_function(LED, GPIO_FUNC_PWM);
uint sliceNum = pwm_gpio_to_slice_num(LED);

pwm_clear_irq(sliceNum);
pwm_set_irq_enabled(sliceNum, true);
irq_set_exclusive_handler(PWM_IRQ_WRAP, wrapHandler);
irq_set_enabled(PWM_IRQ_WRAP, true);

pwm_config config = pwm_get_default_config();
pwm_config_set_clkdiv(&config, 4.f);
pwm_init(sliceNum, &config, true);


Initializing the ADC

In order to use the ADC, I first initialised the ADC using the adc_init() function. I then initialized the GPIO 26 using the adc_gpio_init() function. From the datasheet, I know that the ADC inputs 0 to 3 are connected to GPIOs 26 to 29 respectively. In order to select the input, I used the adc_select_input() function.

adc_init();
adc_gpio_init(26);
adc_select_input(0);


The infinite while loop

For this code, the infinite while loop has only one function: read the input from the ADC and sleep for 10 milliseconds. In order to read the input from the ADC, I used the adc_read() function. The sleep_ms() function is important because ADC takes some time to read and store the output. If the sleep function is removed, the adc_read() function is called repeatedly and the CPU doesn't have enough time to store the result.

while(1){
    brightness = adc_read();
    sleep_ms(10);
}


Expected vs actual output

I used the clock divider to be 4. Therefore, the PWM interrupt was being called at $\frac{\text{sys_clk}}{4} = \frac{125}{4}\text{MHz} = 31.25$ MHz. Moreover, 1 PWM cycle consists of 65536 interrupt cycles. Therefore, the PWM frequency that should be generated is $\frac{\text{interrupt frequency}}{65536} = \frac{31.25}{65536}\text{MHz} = 476.83$ Hz.

The scope trace below shows the PWM output from the LED pin. The text in the top left corner of the screen confirms that the frequency of the generated PWM wave is infact 476.9 Hz.

Moreover, when I change rotate the potentiometer, it also changes the duty cycle of the PWM signal and behaves as it should.

PWM signal generated based on value from the ADC

CMakeLists.txt

cmake_minimum_required(VERSION 3.13)

include(pico_sdk_import.cmake)

project(ADCInput)

pico_sdk_init()

add_executable(ADCInput ADCInput.c)

pico_enable_stdio_usb(ADCInput 1)
pico_enable_stdio_uart(ADCInput 0)

pico_add_extra_outputs(ADCInput)

target_link_libraries(ADCInput pico_stdlib pico_time hardware_gpio hardware_irq hardware_pwm hardware_adc)