ADC Input UART

This program was an introduction to digital input in order to control the number of LEDs glowing while simultaneously controlling their brightness based on ADC input from a 3-pin potentiometer. This was also an introduction to the output using UART. 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 an array of pins to be PWM output and a pin
 * to be ADC input. Additionally, it also initializes a pushbutton pin.
 * Based on the input from the ADC input, the brightness of the LEDs is changed.
 * Lastly, the input from the pushbutton toggles the number of LEDs that are glowing.
 */
#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
#include "hardware/uart.h" //The hardware UART library

#define BUTTON 5 //The pushbutton
#define UARTTX 0 //UART GPIO Transmit Pin
#define UARTRX 1 //UART GPIO Receive Pin

int LEDs[5] = {6, 7, 2, 3, 4}; //Array og GPIOs for LED PWM
int slices[5]; //An array of slices for PWM output
int level = 0, brightness = 0; //The variables to store the number of glowing LEDs and the current brightness

void wrapHandler(){ //The PWM wrap handler function
    for(int i = 0; i < level; i++){ //For the number of glowing LEDs
        pwm_clear_irq(pwm_gpio_to_slice_num(LEDs[i])); //Clear the IRQ for the particular pin
        pwm_set_gpio_level(LEDs[i], brightness * 16); //Set the PWM level for the slice and channel associated with a GPIO
    }
    for(int i = level; i < 5; i++){
        pwm_clear_irq(pwm_gpio_to_slice_num(LEDs[i])); //Clear the IRQ for the particular pin
        pwm_set_gpio_level(LEDs[i], 0); //Set the PWM level as 0 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
    uart_init(uart0, 115200); //Initialise a UART with a given baudrate
    gpio_set_function(UARTTX, GPIO_FUNC_UART); //Set the transmit pin to be UART Transmit
    gpio_set_function(UARTRX, GPIO_FUNC_UART); //Set the transmit pin to be UART Receive

    for(int i = 0; i < 5; i++){ //For all the pins
        gpio_set_function(LEDs[i], GPIO_FUNC_PWM); //Set the LED Pins to be PWM
        slices[i] = pwm_gpio_to_slice_num(LEDs[i]); //Get PWM slice number

        pwm_clear_irq(slices[i]); //Clear the IRQ for the linked slice
        pwm_set_irq_enabled(slices[i], 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
    for(int i = 0; i < 5; i++){ //For all the pins
        pwm_init(slices[i], &config, true); //Initialise a PWM with settings from a configuration object
    }

    gpio_init(BUTTON); //Initialize the pushbutton pin
    gpio_set_dir(BUTTON, GPIO_IN); //Initialize the pushbutton pin to be input

    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. 0...3 are GPIOs 26...29 respectively

    while(1){ //While eternity
        brightness = adc_read(); //Read the ADC input

        if(gpio_get(BUTTON)){ //If the state of the pushbutton pin is high, i.e. it is pressed
            level = (level + 1) % 6; //Increase the level (the number of glowing LEDs). Reset if it reaches 6
            sleep_ms(200); //Sleep for 200ms (button debounce time)
        }
        uart_puts(uart0, "Hello world!\n"); //Print "Hello World!" on the serial output
        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, hardware/uart.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, hardware UART 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"
#include "hardware/uart.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. The #define's include the pushbutton, the UARTTX and the UARTRX GPIO pins. The global variables include a GPIO array, an array of PWM slices, a level indicator which keeps a track of the number of LEDs that are glowing and the brightness tracker. We chose GPIO 0 and GPIO 1 for UART because they are directly connected to the UART0 as seen from the pinout attached below.

PWM signal generated based on value from the ADC
#define BUTTON 5
#define UARTTX 0
#define UARTRX 1

int LEDs[5] = {6, 7, 2, 3, 4};
int slices[5];
int level = 0, brightness = 0;


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 of the required number of pins. For the rest of the pins, the duty cycle is set to 0.

void wrapHandler(){
    for(int i = 0; i < level; i++){
        pwm_clear_irq(pwm_gpio_to_slice_num(LEDs[i]));
        pwm_set_gpio_level(LEDs[i], brightness * 16);
    }
    for(int i = level; i < 5; i++){
        pwm_clear_irq(pwm_gpio_to_slice_num(LEDs[i]));
        pwm_set_gpio_level(LEDs[i], 0);
    }
}


The main function

Initializing communication

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();


Configuring UART

In order to initialize the UART, I used the uart_init() function. It puts the UART into a known state, and enables it. Next, in order to map the UART functionality to the GPIO pins, we used the gpio_set_function() for both UARTTX and UARTRX pins.

uart_init(uart0, 115200);
gpio_set_function(UARTTX, GPIO_FUNC_UART);
gpio_set_function(UARTRX, GPIO_FUNC_UART);


Initializing the PWM

I used the function gpio_set_function(); to set the LED pins as PWM pins. Then I used the pwm_gpio_to_slice_num() function to get the PWM slice numbers for the LED pins. Next, I used the pwm_clear_irq() to clear the interrupts on the given PWM slices. This allows me to enable the PWM interrupt on the given slices 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.

for(int i = 0; i < 5; i++){
    gpio_set_function(LEDs[i], GPIO_FUNC_PWM);
    slices[i] = pwm_gpio_to_slice_num(LEDs[i]);

    pwm_clear_irq(slices[i]);
    pwm_set_irq_enabled(slices[i], 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);
for(int i = 0; i < 5; i++){
    pwm_init(slices[i], &config, true);
}


Initializing the GPIO input

In the next 2 lines of the code, I initialized the button pin and configured it to be the input pin. The gpio_init() function is used to initialize the pin and the gpio_set_dir() function us used to set the pin direction which can be GPIO_OUT (output) or GPIO_IN (input).

gpio_init(BUTTON);
gpio_set_dir(BUTTON, GPIO_IN);


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 follows the following algorithm over and over again:

In order to read the input from the ADC, I used the adc_read() function. The gpio_get() function is used to get the digital state of the GPIO pin (0 for low, non-zero for high). I also used a sleep_ms(200) to act as the debounce time for the pushbutton. Finally, the uart_puts() function is used to write string to UART for transmission.

while(1){
    brightness = adc_read();

    if(gpio_get(BUTTON)){
        level = (level + 1) % 6;
        sleep_ms(200);
    }
    uart_puts(uart0, "Hello world!\n");
    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.69 Hz.

Moreover, when I change rotate the potentiometer, it also changes the duty cycle of the PWM signals on all the turned on LED pins and behaves as it should. Moreover, pressing the pushbutton also changes the number of LEDs that are glowing.

PWM signal generated based on value from the ADC

I checked the output of the UART on the Arduino serial monitor and it looks as follows.

Output on the Arduino serial monitor

CMakeLists.txt

cmake_minimum_required(VERSION 3.13)

include(pico_sdk_import.cmake)

project(ADCInputMultLEDPWMUART)

pico_sdk_init()

add_executable(ADCInputMultLEDPWMUART ADCInputMultLEDPWMUART.c)

pico_enable_stdio_usb(ADCInputMultLEDPWMUART 1)
pico_enable_stdio_uart(ADCInputMultLEDPWMUART 1)

pico_add_extra_outputs(ADCInputMultLEDPWMUART)

target_link_libraries(ADCInputMultLEDPWMUART pico_stdlib pico_time hardware_gpio hardware_irq hardware_pwm hardware_adc hardware_uart)