Raspberry Pi Pico - Detailed Description

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.

Key Specifications
  • Microcontroller: RP2040 (Dual-core ARM Cortex-M0+ @ 133 MHz)
  • RAM: 264KB SRAM
  • Flash Memory: 2MB onboard QSPI flash
  • GPIO Pins: 26 (3.3V logic) including I2C, SPI, UART, PWM, ADC
  • ADC: 12-bit, 3-channel ADC (with internal temperature sensor)
  • USB: USB 1.1 Host/Device support (Micro-USB)
  • Clock Speed: Up to 133 MHz
  • Power Supply: 1.8V to 5.5V via USB or external source
  • Programming: MicroPython, C/C++ via Raspberry Pi Pico SDK
  • Operating Temperature: -20°C to +85°C
RP2040 Microcontroller

The Raspberry Pi Pico is powered by the RP2040 microcontroller. It is a custom-designed chip by the Raspberry Pi Foundation that features:

  • Dual-core ARM Cortex-M0+: Low-power, 32-bit processors optimized for simple tasks.
  • 264KB of SRAM: Sufficient for a wide range of embedded applications.
  • DMA Controllers: For fast, efficient memory transfer without involving the CPU.
  • Flexible Clocking: Allows for a clock speed up to 133 MHz for performance, or lower for power-saving.
GPIO Pins

The Raspberry Pi Pico features 26 multi-function GPIO pins. These pins can be configured for:

  • Digital I/O: All GPIO pins can act as digital input/output (3.3V logic).
  • PWM: Multiple pins support Pulse Width Modulation for applications like motor control and dimming LEDs.
  • ADC: 3 ADC channels (12-bit resolution) can be used for reading analog sensors (e.g., temperature, light, etc.).
  • UART, I2C, SPI: Hardware support for common serial communication protocols.
Note: The GPIO pins operate at 3.3V. Higher voltage may damage the microcontroller.
Communication Interfaces
  • UART: 2 hardware UARTs for serial communication with other devices.
  • I2C: 2 I2C controllers for connecting to sensors, displays, and other peripherals.
  • SPI: 2 SPI controllers for high-speed communication with peripherals like sensors, SD cards, etc.
USB Support

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.

Power Supply Options
  • Micro-USB: The most common way to power the Pico, providing 5V which is regulated down to 3.3V for internal use.
  • VSYS Pin: A dedicated pin allows for an external power supply ranging from 1.8V to 5.5V.
Programming the Raspberry Pi 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.

Applications
  • Robotics and motor control (PWM output).
  • Sensor monitoring (using ADC and I2C/SPI interfaces).
  • Embedded systems, home automation, and IoT projects.
  • Educational tools for learning embedded systems and programming.
Raspberry Pi Pico ADC Detailed Description
Overview

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.

ADC Channels

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 Sensor

Each external channel can measure analog signals between 0V and 3.3V, and the internal temperature sensor allows for onboard temperature measurements.

ADC Resolution

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.

Input Voltage Range

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.

Sampling Rate

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.

Internal Temperature Sensor

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
Using ADC in Arduino IDE

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.

Additional Features

The Raspberry Pi Pico ADC also supports additional features such as:

  • Continuous Sampling Mode: The ADC can continuously sample at a set frequency using DMA (Direct Memory Access) to store the samples directly in memory without CPU intervention.
  • Voltage Reference: The ADC uses the 3.3V as its reference voltage. It's important to ensure that the Pico is powered correctly and that the 3.3V rail is stable to ensure accurate readings.
  • Low-Power Mode: The ADC can be used in low-power applications by reducing the sampling rate, which reduces power consumption when reading sensors infrequently.
Conclusion

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.

Raspberry Pi Pico - I2C Master & Slave Example

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.

Master-Slave Communication Overview

In this example:

  • Master: The Raspberry Pi Pico acting as the master sends data to the slave and requests data from it.
  • Slave: Another Raspberry Pi Pico acting as the slave listens for data from the master and responds accordingly.
Wiring

To set up I2C communication, connect the Raspberry Pi Pico I2C pins as follows:

  • SDA (Data Line): Connect GPIO4 on the master to GPIO4 on the slave.
  • SCL (Clock Line): Connect GPIO5 on the master to GPIO5 on the slave.
  • Connect a common ground between both boards.
  • Pull-up resistors (4.7kΩ) are recommended between the SDA and SCL lines to 3.3V to ensure reliable communication.
I2C Master Code

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
}
        
I2C Slave Code

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
}
        
How the Code Works

Master Code:

  • The master initializes the I2C bus with Wire.begin() and sets up Serial communication for debugging.
  • In each loop, the master sends a string message to the slave using Wire.write() and Wire.endTransmission().
  • After sending data, the master requests 13 bytes of data from the slave using Wire.requestFrom() and prints the received data to the Serial Monitor.

Slave Code:

  • The slave initializes the I2C bus with the address 8 using Wire.begin(8).
  • The Wire.onReceive() function is used to process any data the slave receives from the master. The received data is printed to the Serial Monitor.
  • The 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!".
Conclusion

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.

Raspberry Pi Pico - SPI Master & Slave Example

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.

Master-Slave Communication Overview
  • Master: The Raspberry Pi Pico acting as the master sends data to the slave and reads data from it.
  • Slave: Another Raspberry Pi Pico acting as the slave receives data from the master and sends a response back.
Wiring
  • MOSI: GPIO19 on master to GPIO19 on slave
  • MISO: GPIO16 on master to GPIO16 on slave
  • SCK: GPIO18 on master to GPIO18 on slave
  • SS: GPIO17 on master to GPIO17 on slave
  • Connect a common ground between both boards.
SPI Master Code

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
}
        
SPI Slave Code

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
    }
}
        
How the Code Works

Master Code:

  • The master initializes the SPI bus with SPI.begin() and sets the SS pin as an output to control when the slave is active.
  • In each loop, the master sends a string message to the slave one byte at a time using SPI.transfer().
  • After sending data, the master sends dummy bytes (0x00) to receive the response from the slave and prints it to the Serial Monitor.
Slave Code:
  • The slave initializes the SPI bus with SPI.begin() and configures its MISO pin as an output for sending data back to the master.
  • An interrupt service routine (ISR) handles incoming data from the master using SPI_STC_vect, which is triggered each time data is received.
  • The ISR reads the received data, stores it in a buffer, and sends a response back to the master using the SPI data register SPDR.

Conclusion

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.

Understanding Timers in Raspberry Pi Pico (RP2040)

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.

General Purpose Timers in RP2040

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.

  • Timer 0: First general-purpose timer used for standard timekeeping tasks, often used to generate periodic interrupts.
  • Timer 1: Second general-purpose timer, similar to Timer 0, but can be used for separate tasks or events.
  • Timer 2: Third general-purpose timer that can be configured for tasks requiring high timing precision.
  • Timer 3: Fourth general-purpose timer, often used in multi-tasking applications where various time-critical events need to be tracked.

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.

More information on MCU timers

Key Registers for RP2040 Timers

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:

  • TIMER_BASE (0x40054000): Base address for the timer peripheral, allowing access to the timer-specific registers.
  • TIMEHW (0x40054000): This is the high 32-bit word of the 64-bit timer counter. It increments continuously and provides the higher-order bits of the time.
  • TIMEHR (0x40054004): This is the low 32-bit word of the 64-bit timer counter, incrementing at a rate of 1 MHz. Together with TIMEHW, it provides the current value of the system-wide time, measured in microseconds.
  • TIMER_ALARMn (0x40054010 + n*4): Four individual alarm registers (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_INTR (0x40054040): This register holds the interrupt status for the four alarms. Each bit in this register indicates whether a specific alarm has triggered an interrupt.
  • TIMER_ARMED (0x40054044): A register indicating whether any of the alarms are currently armed. This is useful for debugging or checking if a timer is still set to trigger in the future.
  • TIMER_LOAD (0x40054048): Writing to this register allows you to load a specific value into the timer. This can be used to reset the timer to a known value or to simulate a timer event by setting a future time directly.
  • TIMER_DBGPAUSE (0x40054054): This register allows you to pause the timer during debugging, preventing the timer from continuing to count while stepping through code in a debugging session.
  • TIMER_PAUSE (0x40054058): The 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.
Timer Functionality

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:

  • Periodic Interrupts: You can set the alarm to trigger an interrupt at a specific interval, such as every 1 second or every millisecond, depending on the application.
  • Single-Shot Interrupts: The alarm can be set to trigger once and then be disabled, which is useful for generating a time delay.
Steps to Configure a Timer
  1. Set the current time in the TIMEHW and TIMEHR registers if necessary.
  2. Write the desired alarm value into the corresponding TIMER_ALARM register.
  3. Enable the interrupt for the timer by setting the correct bit in the interrupt enable register.
  4. Wait for the timer to reach the set value and handle the interrupt in the ISR (Interrupt Service Routine).
Watchdog Timer (WDT)

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.

PWM and Input Capture

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.

External Triggers

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.

Conclusion

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.

Raspberry Pi Pico - Timer Examples
1. Generate a Square Wave Frequency

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
}
        
2. PWM at 25 kHz

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);
    }
}
        
3. PWM with Hardware Reset

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:

  • This PWM setup uses the ESP32's **MCPWM (Motor Control PWM) hardware module**, which is significantly more precise and flexible than Arduino's standard analogWrite.
  • The **hardware sync input** allows the timer to be reset by an external signal (RESET_GPIO) without CPU intervention. This is ideal for high-speed or real-time control where CPU latency or software delays are unacceptable.
  • The **CPU can be entirely idle** for this operation, freeing processing power for other tasks or allowing deeper sleep modes while PWM continues accurately.
  • This approach is outside the standard Arduino framework. It directly accesses ESP32 hardware registers and features via the ESP-IDF-based API available in Arduino, giving low-level control over timers.
  • Because it bypasses software loops, there is **no use of `delay()`**, ensuring consistent and deterministic timing.

Use Cases: Motor control, high-speed LED dimming, synchronized multi-channel PWM, and applications requiring deterministic timing where software loops cannot guarantee precision.

4. Frequency Counter

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
}
        
5. Time Measurement

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
    }
}
        
6. Timer Interrupt

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
}
        
Timers as Triggers
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
Timer Triggers
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
Raspberry Pi Pico – Serial Communication (Arduino C)

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.

Simple Send and Receive (Text)

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);
  }
}
        
Send and Receive a Struct with CRC

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.

Raspberry Pi Pico Interrupt Examples

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.

Interrupt Overview

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:

  • External Interrupts: Triggered by changes on GPIO pins, e.g., button presses.
  • Timer Interrupts: Triggered by timers to execute periodic tasks.
  • Serial Interrupts: Triggered by incoming UART/USB serial data.
  • SPI Interrupts: Triggered by SPI data transfers.
  • I2C Interrupts: Triggered by I2C bus activity.
  • ADC Interrupts: Triggered when an analog-to-digital conversion completes.
External Interrupt

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
}
        
SPI Interrupt

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
}
        
I2C Interrupt

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
}
        
Timer Interrupt

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
}
        
Serial Interrupt

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
}
        
ADC Interrupt

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
}
        
Raspberry Pi Pico - USB Examples
1. USB Serial Communication

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
    }
}
        
2. USB HID Keyboard Example

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
}
        
3. USB MIDI Device Example

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
}
        
4. USB Mass Storage Example

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
}
        
5. USB CDC (Communication Device Class) Example

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
    }
}
        
Understanding DMA (Direct Memory Access)

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.

How DMA Works

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.

Basic DMA Operation

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:

  1. Initialization: The CPU configures the DMA controller, specifying the source and destination addresses, the size of the data to be transferred, and the transfer mode.
  2. Trigger: The DMA transfer is triggered by a peripheral request or a software trigger initiated by the CPU.
  3. Transfer: Once triggered, the DMA controller reads the data from the source and writes it to the destination without CPU intervention.
  4. Completion: After the transfer is complete, the DMA controller can generate an interrupt to notify the CPU for post-transfer processing.
DMA Transfer Modes

DMA supports several modes depending on the needs of the application:

  • Normal Mode: Transfers a specified number of data items and then stops.
  • Circular Mode: Continuously transfers data in a circular buffer, ideal for real-time streams.
  • Peripheral-to-Memory Mode: Transfers data from a peripheral to memory.
  • Memory-to-Peripheral Mode: Transfers data from memory to a peripheral.
  • Memory-to-Memory Mode: Transfers data between two memory regions for fast copying or rearrangement.
DMA Trigger Sources
  • Peripheral Requests: Peripherals can request DMA when data is ready to send or receive (UART, SPI, ADC).
  • Timer Events: Timers trigger DMA transfers at specific intervals.
  • Software Triggers: CPU can initiate DMA transfers directly.
Benefits of Using DMA
  • Reduced CPU Load: CPU is free while DMA handles data transfers.
  • Higher Data Throughput: Enables high-speed, uninterrupted data transfers.
  • Low Latency: Transfers occur with minimal delay for real-time applications.
DMA Limitations
  • Peripheral Compatibility: Not all peripherals support DMA.
  • Memory Access Conflicts: DMA competes with the CPU for memory bus access.
Conclusion

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.

RP2040 DMA Peripheral to Channel Matrix
Peripheral DMA Channels
UART0DMA Channel 0 (RX), DMA Channel 1 (TX)
UART1DMA Channel 2 (RX), DMA Channel 3 (TX)
SPI0DMA Channel 4 (RX), DMA Channel 5 (TX)
SPI1DMA Channel 6 (RX), DMA Channel 7 (TX)
I2C0DMA Channel 8 (RX), DMA Channel 9 (TX)
I2C1DMA Channel 10 (RX), DMA Channel 11 (TX)
ADCAny DMA channel can be used for ADC input
PIO0Any DMA channel can be used (DMA 0-11)
PIO1Any DMA channel can be used (DMA 0-11)
PWM (any channel)Any DMA channel can be used (DMA 0-11)
RP2040 DMA Channel to Peripheral Matrix
DMA Channel Valid Peripherals
DMA Channel 0UART0 (RX), SPI0 (RX), I2C0 (RX), ADC, PIO0, PWM
DMA Channel 1UART0 (TX), SPI0 (TX), I2C0 (TX), PIO0, PWM
DMA Channel 2UART1 (RX), SPI1 (RX), I2C1 (RX), PIO0, PWM
DMA Channel 3UART1 (TX), SPI1 (TX), I2C1 (TX), PIO0, PWM
DMA Channel 4SPI0 (RX), I2C0 (RX), PIO0, PWM
DMA Channel 5SPI0 (TX), I2C0 (TX), PIO0, PWM
DMA Channel 6SPI1 (RX), I2C1 (RX), PIO0, PWM
DMA Channel 7SPI1 (TX), I2C1 (TX), PIO0, PWM
DMA Channel 8I2C0 (RX), PIO0, PWM
DMA Channel 9I2C0 (TX), PIO0, PWM
DMA Channel 10I2C1 (RX), PIO0, PWM
DMA Channel 11I2C1 (TX), PIO0, PWM
Wi-Fi on Raspberry Pi Pico W and Pico 2 W

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.

Raspberry Pi Pico W (CYW43439)

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.

Example: Connecting to Wi-Fi on Raspberry Pi Pico W

#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
}
        
Raspberry Pi Pico 2 W (CYW43455)

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.

Example: Connecting to Wi-Fi on Raspberry Pi Pico 2 W

#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
}
        
Key Differences Between Pico W and Pico 2 W
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
Raspberry Pi Pico Dual Core Example (Arduino C)

Why dual core matters in embedded computing

The RP2040 microcontroller on the Raspberry Pi Pico features two cores, enabling parallel execution of tasks:

  • Core 0: Main core, usually for default tasks and interrupts, but available for user-defined tasks.
  • Core 1: Secondary core, fully available for custom tasks, allowing simultaneous operations.

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.

Code Description
  • Task1: Runs on Core 0, toggles an LED on GPIO 25 every 500ms.
  • Task2: Runs on Core 1, prints a message to the Serial Monitor every second.

FreeRTOS handles these tasks concurrently, allowing the Pico to perform multiple operations in parallel.

Key Functions Used
  • multicore_launch_core1: Launches code on Core 1.
  • vTaskDelay: FreeRTOS non-blocking delay for multitasking.
  • Serial.print: Standard Arduino function to print to Serial Monitor.
Example Code

#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
}
        
Steps to Run the Code
  • Copy the code into Arduino IDE.
  • Install the Raspberry Pi Pico board package from the board manager.
  • Select Raspberry Pi Pico and upload the code.
  • Open Serial Monitor (115200 baud) to observe Core 1 output.
Expected Output

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 Tasks Explanation

FreeRTOS manages multitasking on the Pico, letting multiple tasks run concurrently. Core 1 uses multicore_launch_core1, and Core 0 uses xTaskCreate.

Understanding I/O State Machines in Raspberry Pi Pico (RP2040)

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.

Overview of I/O State Machines

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.

Key Features of the I/O State Machines
  • Independent Operation: Run concurrently with the CPU without overhead.
  • Simple Assembly Language: Lightweight assembly-like language for easy programming.
  • Programmable I/O: Control GPIO pins as input, output, or alternate functions.
  • Flexible Timing: Precise timing suitable for high-speed communication and signal generation.
Architecture of I/O State Machines
  • Instruction Memory: Up to 32 instructions per state machine.
  • Program Counter: Tracks the current instruction.
  • Control Signals: Manage GPIO pin states (set, clear, read).
  • Input Signals: React dynamically to input changes.
Programming the I/O State Machines

1. Install the Pico SDK

Set up the Raspberry Pi Pico SDK, which includes tools, libraries, and documentation for programming state machines.

2. Write the State Machine Code

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
        

3. Load the Program

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
    }
}
        

4. Control the State Machine

Programmatically start, stop, reset, or monitor GPIO pins and events triggered by the state machine.

Applications Using I/O State Machines
  • Driving LED Displays: Generate multiplexing timing for LED matrices.
  • Communicating with Peripherals: Implement SPI, I2C, and UART protocols efficiently.
  • Signal Generation: Generate PWM signals or analog waveforms.
Conclusion

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®, Pico / RP2040 – Legal & Trademark Notice

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.

Raspberry Pi Pico / RP2040 Boards

Official Pico, Pico‑W/H boards and accessories — ideal for 32‑bit low-cost MCU development.