The Raspberry Pi Pico is a low-cost, high-performance microcontroller board designed by the Raspberry Pi Foundation. It is based on the custom-built RP2040 microcontroller chip and features dual ARM Cortex-M0+ cores running at 133 MHz. The Pico offers versatile I/O options and is ideal for embedded projects, robotics, sensors, and more. Below is a detailed breakdown of its features and capabilities.
The Raspberry Pi Pico is powered by the RP2040 microcontroller. It is a custom-designed chip by the Raspberry Pi Foundation that features:
The Raspberry Pi Pico features 26 multi-function GPIO pins. These pins can be configured for:
3.3V. Higher voltage may damage the microcontroller.
The Pico has a Micro-USB connector that supports USB 1.1 Host/Device mode. It can be used to program the Pico or interact with peripherals like a USB keyboard, mouse, or external storage devices. The USB can also supply power to the Pico.
The Pico can be programmed using MicroPython or C/C++. The Raspberry Pi Pico SDK provides a C/C++ environment for developing applications. Programming can be done via the USB interface, and the Pico can appear as a mass storage device, simplifying the process of uploading new code.
The Raspberry Pi Pico is powered by the RP2040 microcontroller, which includes a versatile 12-bit Analog-to-Digital Converter (ADC). This ADC allows the microcontroller to read analog signals, converting them into digital values that can be processed by software. The Pico's ADC can be used for reading analog sensors such as temperature sensors, potentiometers, or any device that outputs an analog signal.
The RP2040 microcontroller on the Raspberry Pi Pico features three external ADC channels and one internal temperature sensor channel, for a total of four ADC inputs:
More information on Successive Approximation Conversion
ADC0- Pin 31 (GP26)ADC1- Pin 32 (GP27)ADC2- Pin 34 (GP28)ADC3- Internal Temperature SensorEach external channel can measure analog signals between 0V and 3.3V, and the internal temperature sensor allows for onboard temperature measurements.
The ADC on the RP2040 is a 12-bit ADC, which means it can represent analog values with 4096 (212) distinct levels. The voltage range of the ADC is 0 to 3.3V, so the resolution per step is:
Resolution = 3.3V / 4096 ≈ 0.00080586V (805.86µV per step)
This means that each increase in the ADC reading represents an increase in voltage of approximately 0.805mV.
The maximum input voltage that can be applied to the ADC pins is 3.3V. Applying a voltage higher than 3.3V could damage the ADC or the RP2040 chip. To read higher voltage signals, a voltage divider should be used to scale the input down to the 0-3.3V range.
The default sampling rate of the Pico’s ADC is approximately 500ksps (kilosamples per second). However, this can be modified using the Raspberry Pi Pico SDK to achieve a lower sampling rate depending on the application's needs.
The internal temperature sensor is connected to ADC3. It is used to measure the chip's temperature and outputs a voltage that is proportional to the temperature. This voltage can be converted into a temperature value through the formula provided in the RP2040 documentation.
Temperature (°C) = 27 - (ADC_Voltage - 0.706) / 0.001721
To use the ADC with the Arduino IDE, you can easily read the ADC values using the analogRead() function. Here’s a simple example to read the voltage from ADC0 (Pin GP26):
float voltage;
int adcValue;
void setup() {
Serial.begin(115200);
}
void loop() {
adcValue = analogRead(26); // Read the value from ADC0 (GP26)
voltage = adcValue * (3.3 / 4095.0); // Convert ADC value to voltage
Serial.print("ADC Value: ");
Serial.print(adcValue);
Serial.print(" | Voltage: ");
Serial.println(voltage);
delay(1000); // Wait for 1 second
}
In this code, analogRead() reads the analog value from GP26, and the value is then converted into a corresponding voltage between 0V and 3.3V.
The Raspberry Pi Pico ADC also supports additional features such as:
The Raspberry Pi Pico's 12-bit ADC is a highly flexible and accurate component for measuring analog signals. Whether you are reading sensor data, monitoring temperatures, or performing other analog measurements, the ADC provides reliable performance in a wide variety of applications.
This example demonstrates how to configure the Raspberry Pi Pico as both an I2C master and an I2C slave using the Arduino framework. I2C is a popular communication protocol used to connect microcontrollers and peripheral devices. It operates on two wires: SDA (data) and SCL (clock). One device acts as the master, initiating communication, while the other device acts as the slave, responding to the master's requests.
In this example:
To set up I2C communication, connect the Raspberry Pi Pico I2C pins as follows:
The master sends a message ("Hello, Slave!") to the slave device and then requests a response. It uses the Wire.beginTransmission() function to initiate communication with the slave and Wire.endTransmission() to complete it.
#include <Wire.h>
void setup() {
Wire.begin(); // Initialize as I2C master
Serial.begin(115200); // Initialize Serial for debugging
}
void loop() {
Wire.beginTransmission(8); // Address of the slave device (e.g., 8)
Wire.write("Hello, Slave!"); // Send message to slave
Wire.endTransmission(); // Complete transmission
delay(500); // Wait before sending the next message
Wire.requestFrom(8, 13); // Request 13 bytes from slave
while (Wire.available()) {
char c = Wire.read(); // Read byte from slave
Serial.print(c); // Print received byte to Serial Monitor
}
Serial.println(); // Newline for readability
delay(1000); // Delay before next loop
}
The slave listens for data from the master and responds when requested. The slave is initialized with Wire.begin(8), where 8 is the address of the slave. It uses the Wire.onReceive() and Wire.onRequest() functions to handle incoming data and send responses.
#include <Wire.h>
void setup() {
Wire.begin(8); // Initialize as I2C slave with address 8
Wire.onReceive(receiveEvent); // Register function to handle data received from master
Wire.onRequest(requestEvent); // Register function to handle requests from master
Serial.begin(115200); // Initialize Serial for debugging
}
void loop() {
delay(100); // Main loop can stay empty as everything is interrupt-driven
}
// Function to handle incoming data from master
void receiveEvent(int numBytes) {
while (Wire.available()) {
char c = Wire.read(); // Read incoming byte
Serial.print(c); // Print incoming byte to Serial Monitor
}
Serial.println(); // Newline for readability
}
// Function to handle request from master
void requestEvent() {
Wire.write("Hello, Master!"); // Send response to master
}
Master Code:
Wire.begin() and sets up Serial communication for debugging.Wire.write() and Wire.endTransmission().Wire.requestFrom() and prints the received data to the Serial Monitor.Slave Code:
Wire.begin(8).Wire.onReceive() function is used to process any data the slave receives from the master. The received data is printed to the Serial Monitor.Wire.onRequest() function is used to send a response back to the master when requested. In this case, the slave responds with the string "Hello, Master!".This example demonstrates how to set up basic I2C communication between two Raspberry Pi Pico boards using the Arduino framework. The master sends data to the slave and receives a response, allowing bi-directional communication. I2C is ideal for applications where you need to connect multiple sensors or devices to a single microcontroller, as it uses only two wires for communication.
This example demonstrates how to configure the Raspberry Pi Pico as both an SPI master and an SPI slave using the Arduino framework. SPI (Serial Peripheral Interface) is a communication protocol used to send data between devices using four main lines: MOSI, MISO, SCK, and SS. SPI is commonly used for short-distance, high-speed communication with peripherals such as sensors, SD cards, and displays.
The master sends a message ("Hello, Slave!") to the slave device and then requests a response. It uses the SPI.transfer() function to send and receive data simultaneously over the SPI bus.
#include <SPI.h>
void setup() {
SPI.begin(); // Initialize as SPI master
pinMode(SS, OUTPUT); // Set SS (Slave Select) pin as output
Serial.begin(115200); // Initialize Serial for debugging
}
void loop() {
digitalWrite(SS, LOW); // Enable slave device
// Send message to slave
for (const char* msg = "Hello, Slave!"; *msg; ++msg) {
SPI.transfer(*msg); // Send one byte at a time
}
digitalWrite(SS, HIGH); // Disable slave device
delay(500); // Wait before requesting data from slave
digitalWrite(SS, LOW); // Enable slave device
Serial.print("Slave response: ");
for (int i = 0; i < 13; ++i) {
char received = SPI.transfer(0x00); // Send dummy byte and read response
Serial.print(received); // Print received byte to Serial Monitor
}
Serial.println();
digitalWrite(SS, HIGH); // Disable slave device
delay(1000); // Delay before next loop
}
The slave listens for data from the master and responds with a message when requested. It uses the SPI.begin() function to initialize the SPI bus and SPI.setSlave() to configure the slave mode.
#include <SPI.h>
volatile bool dataReady = false;
char receivedData[13]; // Buffer to store received data
char response[] = "Hello, Master!"; // Response message
void setup() {
SPI.begin(); // Initialize as SPI slave
pinMode(MISO, OUTPUT); // Set MISO as output
SPI.setBitOrder(MSBFIRST); // Set bit order (most significant bit first)
SPI.setDataMode(SPI_MODE0); // Set SPI mode
SPI.attachInterrupt(); // Attach interrupt handler for SPI communication
Serial.begin(115200); // Initialize Serial for debugging
}
ISR(SPI_STC_vect) {
static int index = 0;
char received = SPDR; // Read received data
if (index < 13) {
receivedData[index++] = received;
} else {
SPDR = response[index - 13]; // Send response to master
}
if (index == 26) {
index = 0; // Reset index after full message is received and sent
dataReady = true;
}
}
void loop() {
if (dataReady) {
Serial.print("Master sent: ");
Serial.println(receivedData); // Print received data
dataReady = false; // Reset flag
}
}
Master Code:
SPI.begin() and sets the SS pin as an output to control when the slave is active.SPI.transfer().SPI.begin() and configures its MISO pin as an output for sending data back to the master.SPI_STC_vect, which is triggered each time data is received.SPDR.This example demonstrates how to set up basic SPI communication between two Raspberry Pi Pico boards using the Arduino framework. The master sends data to the slave and receives a response, allowing for fast, full-duplex communication. SPI is ideal for applications that require high-speed data transfer, such as reading data from sensors or controlling displays.
The Raspberry Pi Pico, based on the RP2040 microcontroller, offers a flexible system of timers that allow precise time control for various applications such as event scheduling, task delays, input capture, and PWM signal generation. This detailed guide will cover the timers available in RP2040, their functionality, and the key registers used to configure them.
The RP2040 features four general-purpose 64-bit timers which can be used independently for timing tasks. Each timer has its own alarm, which can trigger interrupts at specific times. These general-purpose timers run continuously based on the system clock and provide high-precision timekeeping.
These timers share a common 64-bit counter, and each timer can trigger alarms, making them ideal for tasks requiring precise delays or periodic actions. The counters are derived from a 1 MHz clock, which provides microsecond precision, and the timer continues running even in low-power modes.
The timers are configured and controlled using several registers. Here’s a breakdown of the key registers involved in managing the timers on the RP2040:
TIMEHW, it provides the current value of the system-wide time, measured in microseconds.TIMER_ALARM0through TIMER_ALARM3) allow you to set a future time at which an interrupt or event will be triggered. These alarms correspond to each general-purpose timer (0-3).TIMER_PAUSEregister halts the timer when specific conditions are met, such as entering low-power or sleep modes, providing energy-saving benefits in battery-operated devices.Each of the four general-purpose timers can be set to trigger an interrupt via their associated alarm register. When the current timer value matches the alarm register value, an interrupt is triggered. This allows for precise timing operations such as:
In addition to the general-purpose timers, the RP2040 includes a watchdog timer (WDT) that can be used to reset the system in the event of a software crash or unexpected behavior. This ensures the device doesn’t lock up indefinitely if an error occurs. The watchdog timer can be configured to reset the system if a pre-defined timeout period elapses without the watchdog being reset.
RP2040 timers are often used in combination with the Pulse Width Modulation (PWM) and Input Capture peripherals. For PWM, the timer counts to a specified value, and the result is used to generate a PWM signal with a specific duty cycle. In input capture mode, the timer captures the time at which an external event (such as a signal edge) occurs, which is useful for measuring time intervals or frequency.
The RP2040 timers can be triggered by external signals or peripherals, allowing for flexible configuration in multi-tasking applications. This is particularly useful when synchronizing timers with external events like signals from sensors or other microcontrollers.
The RP2040’s timers provide essential functionality for any application that requires precise timing, whether for time delays, periodic task scheduling, or event-driven programming. Understanding how to configure and use these timers, along with their associated registers, is crucial for building efficient, real-time systems using the Raspberry Pi Pico.
This example generates a square wave frequency on a specified pin using a simple toggle method.
Good for slow pulsing signals.
// --------------------------
// Example 1: Toggle Pin with Timer Interrupt
// --------------------------
const int frequencyPin = 15;
hw_timer_t *timer1 = NULL;
volatile bool pinState = false;
void IRAM_ATTR togglePin() {
pinState = !pinState;
digitalWrite(frequencyPin, pinState);
}
void setup() {
pinMode(frequencyPin, OUTPUT);
timer1 = timerBegin(0, 80, true); // Timer 0, prescaler 80 -> 1MHz base
timerAttachInterrupt(timer1, &togglePin, true);
timerAlarmWrite(timer1, 500000, true); // 500000us = 0.5s
timerAlarmEnable(timer1);
}
void loop() {
// Nothing needed here; hardware timer handles toggling
}
This example demonstrates how to set a PWM signal at a frequency of 25 kHz on a specific pin.
const int pwmPin = 16; // PWM output pin
void setup() {
pinMode(pwmPin, OUTPUT);
ledcSetup(0, 25000, 8); // Channel 0, 25 kHz, 8-bit resolution
ledcAttachPin(pwmPin, 0);
}
void loop() {
for (int dutyCycle = 0; dutyCycle <= 255; dutyCycle++) {
ledcWrite(0, dutyCycle); // Increase duty cycle
delay(10);
}
for (int dutyCycle = 255; dutyCycle >= 0; dutyCycle--) {
ledcWrite(0, dutyCycle); // Decrease duty cycle
delay(10);
}
}
This example demonstrates a PWM output that is automatically reset to zero using a hardware signal on a GPIO pin. Unlike CPU-monitored resets or Arduino analogWrite style PWM, the timer resets completely in hardware without any CPU intervention. This approach ensures precise timing and extremely low latency, making it suitable for high-speed or real-time applications where software delays are unacceptable.
#include "driver/mcpwm.h"
#include "soc/mcpwm_reg.h"
#include "soc/mcpwm_struct.h"
#define PWM_GPIO 16
#define RESET_GPIO 17
void setup() {
Serial.begin(115200);
// Initialize MCPWM unit 0, timer 0 for PWM output
mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM0A, PWM_GPIO);
mcpwm_config_t pwm_config;
pwm_config.frequency = 25000; // 25 kHz PWM frequency
pwm_config.cmpr_a = 50; // 50% duty cycle
pwm_config.cmpr_b = 0;
pwm_config.counter_mode = MCPWM_UP_COUNTER;
pwm_config.duty_mode = MCPWM_DUTY_MODE_0;
mcpwm_init(MCPWM_UNIT_0, MCPWM_TIMER_0, &pwm_config);
// Configure GPIO as hardware reset input
pinMode(RESET_GPIO, INPUT_PULLUP);
// Configure MCPWM hardware sync to reset timer on external signal
mcpwm_sync_config_t sync_config;
sync_config.sync_sig = MCPWM_SELECT_GPIO; // Use GPIO input for sync
sync_config.sync_pin_num = RESET_GPIO; // GPIO pin for external trigger
sync_config.count_direction = MCPWM_TIMER_DIRECTION_UP;
sync_config.prescale = 1;
sync_config.reload_on_sync = true; // Reset timer on pulse
mcpwm_sync_configure(MCPWM_UNIT_0, MCPWM_TIMER_0, &sync_config);
}
void loop() {
// No CPU intervention needed; PWM is reset automatically on RESET_GPIO pulse
}
Key Points and Explanation:
analogWrite.RESET_GPIO) without CPU intervention. This is ideal for high-speed or real-time control where CPU latency or software delays are unacceptable.Use Cases: Motor control, high-speed LED dimming, synchronized multi-channel PWM, and applications requiring deterministic timing where software loops cannot guarantee precision.
This example counts the frequency of a signal on a specified pin using interrupts.
const int signalPin = 15; // Signal input pin
volatile unsigned long count = 0;
void countFrequency() {
count++; // Increment count on each interrupt
}
void setup() {
pinMode(signalPin, INPUT);
attachInterrupt(digitalPinToInterrupt(signalPin), countFrequency, RISING);
Serial.begin(115200);
}
void loop() {
unsigned long tempCount = count;
delay(1000); // Measure frequency every second
Serial.print("Frequency: ");
Serial.println(tempCount);
count = 0; // Reset count
}
This example measures the elapsed time between two events using the micros() function.
unsigned long startTime;
void setup() {
Serial.begin(115200);
startTime = micros(); // Start time measurement
}
void loop() {
if (digitalRead(15) == HIGH) { // Assuming a signal on pin 15
unsigned long elapsedTime = micros() - startTime;
Serial.print("Elapsed Time: ");
Serial.println(elapsedTime);
startTime = micros(); // Reset start time
}
}
This example demonstrates how to use a timer interrupt to toggle an LED at regular intervals.
const int ledPin = 25; // LED pin
volatile bool ledState = false;
void setup() {
pinMode(ledPin, OUTPUT);
timer1_isr_init(); // Initialize timer interrupt
timer1_attachInterrupt(toggleLED); // Attach interrupt handler
timer1_enable(TIMER_1); // Enable timer
timer1_set_period(TIMER_1, 1000000); // Set period to 1 second
}
void toggleLED() {
ledState = !ledState; // Toggle LED state
digitalWrite(ledPin, ledState); // Set LED state
}
void loop() {
// Main loop does nothing, LED is controlled by interrupt
}
| Timer Channel | Triggered Peripheral / DMA Channel |
|---|---|
| Timer 0 Channel 0 | DMA Channel 0 |
| Timer 0 Channel 1 | DMA Channel 1 |
| Timer 1 Channel 0 | DMA Channel 2 |
| Timer 1 Channel 1 | DMA Channel 3 |
| Timer 2 Channel 0 | DMA Channel 4 |
| Timer 2 Channel 1 | DMA Channel 5 |
| Timer 3 Channel 0 | DMA Channel 6 |
| Timer 3 Channel 1 | DMA Channel 7 |
| Trigger Source | Timers Triggered |
|---|---|
| External Signal 1 (GPIO) | Timer 0, Timer 1 |
| External Signal 2 (GPIO) | Timer 2, Timer 3 |
| Timer 0 | Timer 1 |
| Timer 1 | Timer 2 |
| Timer 2 | Timer 3 |
| PWM | All Timers |
| ADC | All Timers |
The Raspberry Pi Pico supports multiple UARTs. In Arduino, the USB serial appears as Serial and UART0 (GPIO0/1) as Serial1. This example demonstrates simple communication and structured data transfer.
void setup() {
Serial.begin(115200); // USB Serial
while (!Serial) delay(10); // Wait for Serial ready
Serial.println("Pico Arduino Serial Ready");
}
void loop() {
Serial.println("Sending Hello");
delay(1000);
if (Serial.available()) {
char c = Serial.read();
Serial.print("Received: ");
Serial.println(c);
}
}
This struct contains multiple data types and ends with a CRC-8 checksum for integrity checking.
#include <Arduino.h>
typedef struct {
bool active;
byte id;
int16_t temperature;
int32_t pressure;
float voltage;
char label[16];
byte crc;
} __attribute__((packed)) DataPacket;
byte computeCRC8(const byte* data, size_t len) {
byte crc = 0x00;
for (size_t i = 0; i < len; i++) {
crc ^= data[i];
for (byte j = 0; j < 8; j++)
crc = (crc & 0x80) ? (crc << 1) ^ 0x07 : (crc << 1);
}
return crc;
}
void sendPacket(Stream& port, DataPacket& pkt) {
pkt.crc = computeCRC8((byte*)&pkt, sizeof(pkt) - 1);
port.write((byte*)&pkt, sizeof(pkt));
}
bool receivePacket(Stream& port, DataPacket& pkt) {
if (port.available() >= sizeof(pkt)) {
port.readBytes((byte*)&pkt, sizeof(pkt));
byte crc = computeCRC8((byte*)&pkt, sizeof(pkt) - 1);
return (crc == pkt.crc);
}
return false;
}
DataPacket packet = {
true, 1, 250, 101325, 3.3, "Pico Arduino", 0
};
void setup() {
Serial.begin(115200);
while (!Serial) delay(10);
}
void loop() {
sendPacket(Serial, packet);
delay(1000);
DataPacket rx;
if (receivePacket(Serial, rx)) {
Serial.print("Received label: ");
Serial.println(rx.label);
}
}
Note: The Pico board must be selected as "Raspberry Pi Pico" in Arduino IDE with the arduino-pico core installed from
https://github.com/earlephilhower/arduino-pico.
Interrupts allow the Raspberry Pi Pico to respond immediately to certain events by pausing the main program and executing an Interrupt Service Routine (ISR). This page provides examples of different types of interrupts supported by the Pico, including GPIO, timers, serial, SPI, I2C, and ADC interrupts.
Interrupts are signals that temporarily halt the main program to execute a special function, known as an ISR. The Raspberry Pi Pico supports various types of interrupts:
Sets up an external interrupt on a GPIO pin. When triggered (e.g., button press), an ISR toggles an LED.
#include "pico/stdlib.h"
const int buttonPin = 15; // Button pin
const int ledPin = 25; // LED pin
volatile bool ledState = false;
void isr() {
ledState = !ledState; // Toggle LED state
}
void setup() {
gpio_init(ledPin);
gpio_set_dir(ledPin, GPIO_OUT);
gpio_init(buttonPin);
gpio_set_dir(buttonPin, GPIO_IN);
gpio_pull_up(buttonPin);
gpio_set_irq_enabled_with_callback(buttonPin, GPIO_IRQ_EDGE_FALL, true, &isr);
}
void loop() {
gpio_put(ledPin, ledState); // Update LED state
}
Handles SPI communication using interrupts. When data is received, an ISR processes it.
#include "pico/stdlib.h"
#include "hardware/spi.h"
const int spiCS = 17; // Chip select pin
volatile uint8_t receivedData = 0;
void spi_isr() {
receivedData = spi_get_hw(spi0)->dr; // Read received data
// Process received data here
}
void setup() {
spi_init(spi0, 500 * 1000); // Initialize SPI at 500 kHz
gpio_init(spiCS);
gpio_set_dir(spiCS, GPIO_OUT);
// Enable SPI interrupt
spi_set_irq_enabled(spi0, true);
irq_set_exclusive_handler(SPI0_IRQ, spi_isr);
irq_set_enabled(SPI0_IRQ, true);
}
void loop() {
// Main code can run here
}
Handles I2C communication via interrupts. ISR is triggered when data is available on the I2C bus.
#include "pico/stdlib.h"
#include "hardware/i2c.h"
const int i2cAddress = 0x28; // Example I2C address
volatile uint8_t receivedData = 0;
void i2c_isr() {
if (i2c_get_event_status(i2c0) == I2C_EVENT_DATA_RECEIVED) {
receivedData = i2c_read_byte(i2c0); // Read byte
// Process received data here
}
}
void setup() {
i2c_init(i2c0, 400 * 1000); // Initialize I2C at 400 kHz
gpio_set_function(4, GPIO_FUNC_I2C); // SDA
gpio_set_function(5, GPIO_FUNC_I2C); // SCL
gpio_pull_up(4); // Pull-up for SDA
gpio_pull_up(5); // Pull-up for SCL
// Enable I2C interrupt
i2c_set_irq_enabled(i2c0, true);
irq_set_exclusive_handler(I2C0_IRQ, i2c_isr);
irq_set_enabled(I2C0_IRQ, true);
}
void loop() {
// Main code can run here
}
Sets up a timer interrupt that triggers at regular intervals. The ISR increments a counter each second.
#include "pico/stdlib.h"
volatile uint32_t seconds = 0;
void timer_isr() {
seconds++; // Increment seconds counter
}
void setup() {
// Initialize timer
add_repeating_timer_ms(1000, timer_isr, NULL, NULL);
}
void loop() {
// Main code can run here
}
Handles serial communication using interrupts. ISR reads incoming data when available.
#include "pico/stdlib.h"
volatile char receivedChar;
void serial_isr() {
if (serial_available()) {
receivedChar = serial_read(); // Read character
// Process received character here
}
}
void setup() {
Serial1.begin(115200); // Initialize serial at 115200 baud
Serial1.attachInterrupt(serial_isr); // Attach ISR
}
void loop() {
// Main code can run here
}
Configures an ADC channel to trigger an interrupt when a conversion completes. ISR reads and processes the ADC value.
#include "pico/stdlib.h"
#include "hardware/adc.h"
volatile uint16_t adcValue = 0;
void adc_isr() {
adcValue = adc_read(); // Read ADC value
// Process ADC value here
}
void setup() {
adc_init(); // Initialize ADC
adc_gpio_init(26); // Initialize ADC pin (GPIO 26)
// Set ADC interrupt
adc_set_irq_enabled(true);
irq_set_exclusive_handler(ADC_IRQ, adc_isr);
irq_set_enabled(ADC_IRQ, true);
}
void loop() {
// Main code can run here
}
This example demonstrates how to set up USB serial communication on the Raspberry Pi Pico. You can send and receive data over the USB connection using the Serial object.
#include "pico/stdlib.h"
void setup() {
usb_init(); // Initialize USB
stdio_init_all(); // Initialize USB Serial
}
void loop() {
if (usb_serial_available()) {
char c = usb_serial_read(); // Read character from USB
usb_serial_write(c); // Echo the character back
}
}
This example shows how to set up the Raspberry Pi Pico as a USB HID keyboard. The Pico sends keystrokes to the connected computer.
#include "pico/stdlib.h"
#include "bsp/board.h"
#include "usb_hid.h"
void setup() {
usb_init(); // Initialize USB
usb_hid_init(); // Initialize HID
}
void loop() {
usb_hid_keyboard_report(0, KEY_A); // Send 'A' key press
sleep_ms(1000); // Wait for 1 second
usb_hid_keyboard_report(0, 0); // Release the key
sleep_ms(1000); // Wait for 1 second
}
This example demonstrates how to create a USB MIDI device using the Raspberry Pi Pico. The Pico sends MIDI messages to a connected MIDI host.
#include "pico/stdlib.h"
#include "usb_midi.h"
void setup() {
usb_init(); // Initialize USB
usb_midi_init(); // Initialize MIDI
}
void loop() {
usb_midi_send_note_on(0, 60, 127); // Send Note On for middle C
sleep_ms(1000); // Hold the note for 1 second
usb_midi_send_note_off(0, 60, 0); // Send Note Off for middle C
sleep_ms(1000); // Wait before sending the next message
}
This example sets up the Raspberry Pi Pico as a USB mass storage device. It simulates a USB flash drive, allowing file transfer over USB.
#include "pico/stdlib.h"
#include "usb_mass_storage.h"
void setup() {
usb_init(); // Initialize USB
usb_mass_storage_init(); // Initialize Mass Storage
}
void loop() {
// Main loop can handle read/write requests from the host
usb_mass_storage_update(); // Update mass storage status
}
This example shows how to use the USB CDC for serial communication. This is similar to the serial communication example but uses the USB CDC protocol.
#include "pico/stdlib.h"
#include "usb_cdc.h"
void setup() {
usb_init(); // Initialize USB
usb_cdc_init(); // Initialize CDC
}
void loop() {
if (usb_cdc_available()) {
char c = usb_cdc_read(); // Read character from USB CDC
usb_cdc_write(c); // Echo the character back
}
}
Direct Memory Access (DMA) is a feature used in microcontrollers and other computing systems to transfer data directly between peripherals and memory, bypassing the CPU. By offloading these data transfers to the DMA controller, the CPU can perform other tasks, significantly improving system performance, especially in real-time applications.
In a typical embedded system without DMA, the CPU is responsible for reading data from a peripheral (e.g., an ADC, UART, or SPI) and writing it to memory, or vice versa. This process consumes significant CPU cycles, especially for high-frequency or large-volume data transfers. DMA automates these transfers by allowing peripherals to communicate directly with memory, reducing the CPU's involvement.
The DMA controller operates as a separate entity within the microcontroller, managing the data transfer between memory and peripherals. The following sequence outlines the basic operation of a DMA transfer:
DMA supports several modes depending on the needs of the application:
DMA improves data transfer efficiency, reduces CPU load, and enhances real-time performance. Offloading transfers from the CPU allows more complex and responsive applications. Understanding DMA configuration is key in embedded system development.
| Peripheral | DMA Channels |
|---|---|
| UART0 | DMA Channel 0 (RX), DMA Channel 1 (TX) |
| UART1 | DMA Channel 2 (RX), DMA Channel 3 (TX) |
| SPI0 | DMA Channel 4 (RX), DMA Channel 5 (TX) |
| SPI1 | DMA Channel 6 (RX), DMA Channel 7 (TX) |
| I2C0 | DMA Channel 8 (RX), DMA Channel 9 (TX) |
| I2C1 | DMA Channel 10 (RX), DMA Channel 11 (TX) |
| ADC | Any DMA channel can be used for ADC input |
| PIO0 | Any DMA channel can be used (DMA 0-11) |
| PIO1 | Any DMA channel can be used (DMA 0-11) |
| PWM (any channel) | Any DMA channel can be used (DMA 0-11) |
| DMA Channel | Valid Peripherals |
|---|---|
| DMA Channel 0 | UART0 (RX), SPI0 (RX), I2C0 (RX), ADC, PIO0, PWM |
| DMA Channel 1 | UART0 (TX), SPI0 (TX), I2C0 (TX), PIO0, PWM |
| DMA Channel 2 | UART1 (RX), SPI1 (RX), I2C1 (RX), PIO0, PWM |
| DMA Channel 3 | UART1 (TX), SPI1 (TX), I2C1 (TX), PIO0, PWM |
| DMA Channel 4 | SPI0 (RX), I2C0 (RX), PIO0, PWM |
| DMA Channel 5 | SPI0 (TX), I2C0 (TX), PIO0, PWM |
| DMA Channel 6 | SPI1 (RX), I2C1 (RX), PIO0, PWM |
| DMA Channel 7 | SPI1 (TX), I2C1 (TX), PIO0, PWM |
| DMA Channel 8 | I2C0 (RX), PIO0, PWM |
| DMA Channel 9 | I2C0 (TX), PIO0, PWM |
| DMA Channel 10 | I2C1 (RX), PIO0, PWM |
| DMA Channel 11 | I2C1 (TX), PIO0, PWM |
The Raspberry Pi Pico W and Pico 2 W offer built-in Wi-Fi capabilities, making them suitable for IoT projects, remote monitoring, and connected systems. The Pico W is equipped with the Infineon CYW43439 Wi-Fi chip, while the Pico 2 W uses the Cypress CYW43455 Wi-Fi chip, which offers enhanced connectivity features.
The Raspberry Pi Pico W utilizes the Infineon CYW43439 chip for Wi-Fi connectivity. This chip supports IEEE 802.11b/g/n Wi-Fi standards and is ideal for low-power wireless applications. The Wi-Fi capability on the Pico W can be accessed and controlled via the Arduino C SDK.
#include <WiFi.h>
const char* ssid = "yourSSID";
const char* password = "yourPassword";
void setup() {
Serial.begin(115200);
delay(10);
Serial.println("Connecting to Wi-Fi...");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("Connected to Wi-Fi");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
}
void loop() {
// Your code here
}
The Raspberry Pi Pico 2 W features the Cypress CYW43455 Wi-Fi chip, which provides higher throughput, enhanced range, and additional support for Bluetooth connectivity. This chip supports dual-band Wi-Fi (2.4GHz and 5GHz) and integrates advanced features for improved wireless communication.
#include <WiFi.h>
const char* ssid = "yourSSID";
const char* password = "yourPassword";
void setup() {
Serial.begin(115200);
delay(10);
Serial.println("Connecting to Wi-Fi...");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("Connected to Wi-Fi");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
}
void loop() {
// Your code here
}
| Feature | Raspberry Pi Pico W | Raspberry Pi Pico 2 W |
|---|---|---|
| Wi-Fi Chip | Infineon CYW43439 | Cypress CYW43455 |
| Wi-Fi Standards | 802.11 b/g/n | 802.11 a/b/g/n/ac (Dual-Band) |
| Bluetooth Support | No | Yes (Bluetooth 5.0) |
| Throughput | Up to 150 Mbps | Up to 400 Mbps |
Why dual core matters in embedded computing
The RP2040 microcontroller on the Raspberry Pi Pico features two cores, enabling parallel execution of tasks:
This example demonstrates using FreeRTOS to run two independent tasks on separate cores: blinking an LED on Core 0 and printing messages on Core 1.
FreeRTOS handles these tasks concurrently, allowing the Pico to perform multiple operations in parallel.
#include
#include
#include
#define LED_PIN 25
// Task running on Core 1
void core1Task(void *pvParameters) {
for (;;) {
Serial.println("Task on Core 1 is running");
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void setup() {
Serial.begin(115200);
while (!Serial);
pinMode(LED_PIN, OUTPUT);
Serial.print("Setup running on core ");
Serial.println(xPortGetCoreID());
multicore_launch_core1([]() {
core1Task(NULL);
});
xTaskCreate([](void *pvParameters) {
for (;;) {
digitalWrite(LED_PIN, HIGH);
vTaskDelay(500 / portTICK_PERIOD_MS);
digitalWrite(LED_PIN, LOW);
vTaskDelay(500 / portTICK_PERIOD_MS);
}
}, "Core0Task", 1024, NULL, 1, NULL);
}
void loop() {
// FreeRTOS handles tasks
}
Serial Monitor shows:
Setup running on core 0
Task on Core 1 is running
Task on Core 1 is running
...
The onboard LED blinks every 500ms on Core 0.
FreeRTOS manages multitasking on the Pico, letting multiple tasks run concurrently. Core 1 uses multicore_launch_core1, and Core 0 uses xTaskCreate.
The Raspberry Pi Pico, based on the RP2040 microcontroller, features programmable I/O state machines (SMs) for advanced I/O operations. These allow tasks such as protocol communication, signal generation, and time-sensitive operations to run independently of the main CPU.
The RP2040 includes 4 independent I/O state machines controlling up to 30 GPIO pins. Each runs its own program stored in instruction memory, allowing efficient handling of peripherals, signal generation, and communication protocols like SPI, I2C, and UART.
Each state machine is a simple finite state machine that reacts to inputs and produces outputs, enabling complex I/O tasks with minimal CPU load.
Set up the Raspberry Pi Pico SDK, which includes tools, libraries, and documentation for programming state machines.
Use Pico Assembly Language to define the behavior. Example: toggle a GPIO pin.
.program 0 ; Load program into State Machine 0
set(pindirs, 1<<0) ; Set GPIO 0 as output
loop:
set(pins, 1<<0) ; Set GPIO 0 high
wait(1000) ; Wait for 1000 cycles
set(pins, 0) ; Set GPIO 0 low
wait(1000) ; Wait for 1000 cycles
jmp(loop) ; Repeat loop
Use the Pico SDK to load and start the state machine:
#include "pico/stdlib.h"
#include "pico/multicore.h"
#include "pico/state_machine.h"
// Define the state machine program
const uint8_t sm_program[] = {
// Your program instructions (binary or hex)
};
// Initialize the state machine
state_machine_t sm;
int main() {
stdio_init_all(); // Initialize standard I/O
sm = state_machine_init(0); // Initialize State Machine 0
state_machine_load(sm, sm_program, sizeof(sm_program)); // Load program
state_machine_start(sm); // Start the state machine
while (1) {
tight_loop_contents(); // Main loop can perform other tasks
}
}
Programmatically start, stop, reset, or monitor GPIO pins and events triggered by the state machine.
I/O state machines on the Raspberry Pi Pico enable precise, independent control over GPIO and timing, minimizing CPU load. Mastering these state machines expands the Pico's capabilities for high-speed, complex I/O operations in embedded systems.
Raspberry Pi® and the RP2040 microcontroller are trademarks of Raspberry Pi Foundation. Visit Raspberry Pi website. Download RP2040 datasheet (PDF).
© 2025 Raspberry Pi Foundation. All rights reserved.
Official Pico, Pico‑W/H boards and accessories — ideal for 32‑bit low-cost MCU development.