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.
/*
* 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
}
}
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"
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.
#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 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);
}
}
stdio_init_all();
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);
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);
}
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);
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);
For this code, the infinite while loop follows the following algorithm over and over again:
level
(number of glowing LEDs).level
variable hits 6, reset it to 0;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);
}
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.
I checked the output of the UART on the Arduino serial monitor and it looks as follows.
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)