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

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:

GPIO Pins

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

Note: The GPIO pins operate at 3.3V. Higher voltage may damage the microcontroller.

Communication Interfaces

The Raspberry Pi Pico supports multiple communication interfaces:

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

The Raspberry Pi Pico can be powered in several ways:

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

The Raspberry Pi Pico is versatile and can be used for a wide range of applications:

Configuring Arduino IDE for Raspberry Pi Pico

The Raspberry Pi Pico can be programmed using the Arduino IDE. Follow these steps to configure the Arduino IDE to support the Raspberry Pi Pico with the RP2040 chip:

Step-by-Step Configuration

Step 1: Install the Latest Arduino IDE

First, ensure that you have the latest version of the Arduino IDE installed. You can download the latest version from the official Arduino website: Arduino IDE Download.

Step 2: Add the Raspberry Pi Pico Core

The Raspberry Pi Pico is not natively supported by the Arduino IDE. You will need to install the necessary board core. Follow these steps to add the core for the Raspberry Pi Pico:

  1. Open the Arduino IDE.
  2. Go to File > Preferences.
  3. In the "Additional Boards Manager URLs" field, paste the following URL:
    https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json
  4. Click OK to save the changes.

Step 3: Install the Raspberry Pi Pico Core

Once you have added the URL, follow these steps to install the board support package for the Raspberry Pi Pico:

  1. Go to Tools > Board > Boards Manager....
  2. In the Boards Manager window, search for "Raspberry Pi Pico" or "RP2040".
  3. Select the "Raspberry Pi Pico/RP2040" package by Earle Philhower, and click Install.

Step 4: Select the Raspberry Pi Pico Board

After installing the core, you can now select the Raspberry Pi Pico board in the Arduino IDE:

  1. Go to Tools > Boardand scroll down to find the "Raspberry Pi Pico" in the list of available boards.
  2. Select Raspberry Pi Pico from the list.

Step 5: Choose the Correct Port

Connect your Raspberry Pi Pico to your computer via a micro-USB cable. To choose the correct port:

  1. Go to Tools > Portand select the port to which your Raspberry Pi Pico is connected.
  2. If your Pico does not appear, press the BOOTSEL button on the Pico, then connect the USB cable while holding the button down. This will allow the Pico to enter bootloader mode.

Step 6: Upload a Test Program

Now that you’ve set up the Raspberry Pi Pico with the Arduino IDE, you can upload a simple test program to verify that everything is working. Use the following example code to blink an onboard LED:

void setup() {
    pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
    digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on
    delay(1000);                       // wait for a second
    digitalWrite(LED_BUILTIN, LOW);    // turn the LED off
    delay(1000);                       // wait for a second
}

Click the Upload button in the Arduino IDE, and the code will be uploaded to your Raspberry Pi Pico. The onboard LED should start blinking.

Raspberry Pi Pico 2 & RP2350 Overview

Introduction

The Raspberry Pi Pico 2 is an enhanced version of the original Raspberry Pi Pico, featuring the RP2350 microcontroller. This new chip offers improved processing power, security, and memory compared to its predecessor, the RP2040.

RP2350 vs. RP2040: Key Differences

Feature RP2040 RP2350
CPU Dual-core ARM Cortex-M0+ (133MHz) Dual-core ARM Cortex-M33 (150MHz)
SRAM 264KB 520KB (multi-bank)
Flash Storage Supports up to 16MB external Supports up to 16MB external (some variants include internal flash)
GPIO 30 pins (4 for ADC) 30 pins (standard) / Up to 48 in larger packages
USB Support USB 1.1 (Host/Device) USB 1.1 (Improved performance)
Security Basic ARM TrustZone for enhanced security
Additional Features PWM, UART, SPI, I2C Enhanced PWM, improved peripheral support

Boards using the Raspberry Pi RP2040 & RP2350 devices

Board CPU Speed RAM Flash Memory Peripherals Operating Voltage Other Specifications
Pico (RP2040) 133 MHz (Dual-core ARM Cortex-M0+) 264 KB SRAM 2 MB 26 GPIO, 2 UART, 2 SPI, 2 I2C, ADC, PWM, USB, PIO 3.3V Affordable and versatile, suitable for general-purpose projects
Pimoroni Tiny 2040 133 MHz (Dual-core ARM Cortex-M0+) 264 KB SRAM 8 MB 8 GPIO, 2 UART, SPI, I2C, ADC, PWM, USB 3.3V Ultra-compact form factor, ideal for space-constrained designs
Adafruit Feather RP2040 133 MHz (Dual-core ARM Cortex-M0+) 264 KB SRAM 8 MB 20 GPIO, UART, SPI, I2C, ADC, PWM, USB, PIO 3.3V Feather form factor, LiPo battery support, ideal for portable projects
Arduino Nano RP2040 Connect 133 MHz (Dual-core ARM Cortex-M0+) 264 KB SRAM 16 MB 22 GPIO, UART, SPI, I2C, ADC, PWM, Wi-Fi, Bluetooth 3.3V Includes Wi-Fi and Bluetooth, compatible with Arduino IDE
Seeed Xiao RP2040 133 MHz (Dual-core ARM Cortex-M0+) 264 KB SRAM 2 MB 11 GPIO, UART, SPI, I2C, ADC, PWM, USB 3.3V Tiny form factor, ideal for wearable and compact IoT projects
Raspberry Pi Pico W 133 MHz (Dual-core ARM Cortex-M0+) 264 KB SRAM 2 MB 26 GPIO, 2 UART, 2 SPI, 2 I2C, ADC, PWM, USB, Wi-Fi 3.3V Same as Pico, but with integrated Wi-Fi for IoT applications
Pico (RP2350) 204 MHz (Dual-core ARM Cortex-M33) 512 KB SRAM 16 MB 30 GPIO, UART, SPI, I2C, ADC, PWM, USB, PIO 3.3V More powerful than RP2040, ideal for computationally intensive tasks
Adafruit Feather RP2350 204 MHz (Dual-core ARM Cortex-M33) 512 KB SRAM 16 MB 25 GPIO, UART, SPI, I2C, ADC, PWM, USB 3.3V Feather form factor with Cortex-M33 performance
Seeed Xiao RP2350 204 MHz (Dual-core ARM Cortex-M33) 512 KB SRAM 8 MB 12 GPIO, UART, SPI, I2C, ADC, PWM, USB 3.3V Compact form factor, powerful processing for small devices

Why Choose the RP2350?

Ideal Applications

The RP2350 in the Pico 2 is ideal for:

Raspberry Pi Pico GPIO Pinout (RP2040)

Actual GPIO pinout diagram of Raspberry Pi Pico (functions: SPI, I2C, UART, ADC)

Image: Vishnu Mohanan / Circuitstate Electronics, licensed **CC BY‑SA 4.0**.

Raspberry Pi Pico is a trademark of the Raspberry Pi Foundation. Pinout diagram under CC BY‑SA 4.0 per Circuitstate Electronics. This website is not officially affiliated with the Raspberry Pi Foundation.

Conclusion

The Raspberry Pi Pico can easily be programmed using the Arduino IDE once properly configured. By following the steps outlined above, you can leverage the powerful RP2040 microcontroller for your embedded projects, making use of familiar Arduino libraries and features.

The Raspberry Pi Pico 2 with the RP2350 chip is a powerful upgrade, making it a great choice for more advanced embedded systems while maintaining the simplicity and flexibility of the original Pico.

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:

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.

The formula to convert the ADC reading from the internal temperature sensor is as follows:

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;

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:

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:

Wiring

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

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 8is 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:

Slave Code:

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 (Master Out Slave In), MISO (Master In Slave Out), SCK (Serial Clock), and SS (Slave Select). SPI is commonly used for short-distance, high-speed communication with peripherals such as sensors, SD cards, and displays.

Master-Slave Communication Overview

In this example:

Wiring

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

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:

Slave Code:

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.

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.

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

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.


const int frequencyPin = 15; // Pin for frequency output
const unsigned long interval = 500; // 500ms toggle interval

void setup() {
    pinMode(frequencyPin, OUTPUT);
}

void loop() {
    digitalWrite(frequencyPin, HIGH);
    delay(interval);
    digitalWrite(frequencyPin, LOW);
    delay(interval);
}
        

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 External Reset

This example demonstrates how to reset the PWM output to zero using an external signal.


const int pwmPin = 16; // PWM output pin
const int resetPin = 17; // External reset pin

void setup() {
    pinMode(pwmPin, OUTPUT);
    pinMode(resetPin, INPUT_PULLUP);
    ledcSetup(0, 25000, 8);
    ledcAttachPin(pwmPin, 0);
}

void loop() {
    if (digitalRead(resetPin) == LOW) {
        ledcWrite(0, 0); // Reset PWM to zero
    } else {
        for (int dutyCycle = 0; dutyCycle <= 255; dutyCycle++) {
            ledcWrite(0, dutyCycle);
            delay(10);
        }
    }
}
        

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 Serialand 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-picocore installed from https://github.com/earlephilhower/arduino-pico.

Raspberry Pi Pico - Interrupt Examples

1. External Interrupt

This example sets up an external interrupt on a GPIO pin. When the pin is triggered (for example, by a button press), an interrupt service routine (ISR) is executed, toggling 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
 }
         

2. SPI Interrupt

This example demonstrates how to use SPI communication with interrupts. The Pico receives data via SPI, and when data is received, an ISR is triggered to handle the incoming data.


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

3. I2C Interrupt

This example illustrates how to handle I2C communication using interrupts. When data is available to be read from the I2C bus, an ISR is triggered to process the incoming data.


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

4. Timer Interrupt

This example sets up a timer interrupt that triggers at regular intervals. In the ISR, a counter variable is incremented every second, showcasing the ability to use timer interrupts for periodic tasks.


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

5. Serial Interrupt

This example demonstrates how to handle serial communication using interrupts. When new data is available on the serial port, an ISR is triggered to read the incoming data.


 #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 9600 baud
     Serial1.attachInterrupt(serial_isr); // Attach ISR
 }
 
 void loop() {
     // Main code can run here
 }
         

6. ADC Interrupt

This example configures an ADC channel to trigger an interrupt when a new conversion is complete. The ISR reads the ADC value and processes it accordingly.


 #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 an event, such as a peripheral request (e.g., a timer, UART, or ADC) or a software trigger initiated by the CPU.
  3. Transfer: Once triggered, the DMA controller reads the data from the source (peripheral or memory) and writes it to the destination (memory or peripheral), without CPU intervention.
  4. Completion: After the transfer is complete, the DMA controller can generate an interrupt to notify the CPU, allowing the CPU to handle post-transfer processing if necessary.

DMA Transfer Modes

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

DMA Trigger Sources

DMA transfers can be triggered by a variety of sources, including:

Benefits of Using DMA

DMA provides several advantages over CPU-driven data transfers:

DMA Limitations

Despite its benefits, DMA has some limitations:

Conclusion

DMA is an essential feature in embedded systems, offering improved data transfer efficiency, reduced CPU load, and enhanced real-time performance. By offloading data transfer tasks from the CPU to the DMA controller, system resources are optimized, allowing for more complex and responsive applications. Understanding how to configure and utilize DMA effectively is a key skill in embedded systems development.

DMA matrix

RP2040 DMA Peripheral to Channel Matrix

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 (typically mapped to available channels depending on the configuration)
PIO0 Any DMA channel can be used (DMA Channel 0-11 based on your application)
PIO1 Any DMA channel can be used (DMA Channel 0-11 based on your application)
PWM (any channel) Any DMA channel can be used (DMA Channel 0-11 based on your application)

RP2040 DMA Channel to Peripheral Matrix

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

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 in Arduino C

This example demonstrates how to utilize the dual-core functionality of the Raspberry Pi Pico’s RP2040 microcontroller using Arduino C. The RP2040 features two cores:

Code Description

We define two tasks in this code:

We use FreeRTOS, which is supported by the RP2040, to manage these tasks and assign them to specific cores. The tasks run independently, allowing the Raspberry Pi Pico to perform multiple operations simultaneously on separate cores.

Key Functions Used:

Example Code


#include <Arduino.h>
#include <FreeRTOS.h>
#include <task.h>

// Pin for onboard LED
#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);  // Delay for 1 second
    }
}

void setup() {
    // Initialize Serial Monitor
    Serial.begin(115200);
    while (!Serial);

    // Configure LED pin as output
    pinMode(LED_PIN, OUTPUT);

    // Print which core the setup is running on (Core 0)
    Serial.print("Setup running on core ");
    Serial.println(xPortGetCoreID());

    // Start task on Core 1
    multicore_launch_core1([]() {
        core1Task(NULL);
    });

    // Create a task to toggle the LED on Core 0
    xTaskCreate([](void *pvParameters) {
        for (;;) {
            digitalWrite(LED_PIN, HIGH);
            vTaskDelay(500 / portTICK_PERIOD_MS);  // LED ON for 500ms
            digitalWrite(LED_PIN, LOW);
            vTaskDelay(500 / portTICK_PERIOD_MS);  // LED OFF for 500ms
        }
    }, "Core0Task", 1024, NULL, 1, NULL);
}

void loop() {
    // No code here, as tasks are handled by FreeRTOS
}
    

Steps to Run the Code:

  1. Copy the code into the Arduino IDE.
  2. Install the Raspberry Pi Pico board package from the board manager if you haven’t already.
  3. Select the correct board (Raspberry Pi Pico) and upload the code.
  4. Open the Serial Monitor (baud rate: 115200) to observe the output from Core 1.

Expected Output

In the Serial Monitor, you should see the following messages:

Setup running on core 0
Task on Core 1 is running
Task on Core 1 is running
...

Additionally, the onboard LED (GPIO pin 25) will blink every 500 milliseconds, running on Core 0.

Explanation of FreeRTOS Tasks

This example uses FreeRTOS to handle multitasking on the Raspberry Pi Pico. FreeRTOS allows you to run multiple tasks concurrently without the need for writing complex threading code. Each task is assigned to a specific core using the multicore_launch_core1function for Core 1 and xTaskCreatefor Core 0.

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

The Raspberry Pi Pico, based on the RP2040 microcontroller, features a unique I/O architecture that includes programmable state machines (SMs). These state machines allow for advanced I/O operations and can be programmed to handle various tasks such as protocol communication, signal generation, and other time-sensitive operations without burdening the main CPU. This guide provides a detailed overview of the I/O state machines and how to program them effectively.

Overview of I/O State Machines

The RP2040 microcontroller includes a total of 4 I/O state machines that can control up to 30 GPIO pins. Each state machine operates independently, executing its program, which is stored in the state machine's instruction memory. This allows for efficient and flexible I/O management, suitable for applications such as driving peripherals, generating signals, or implementing communication protocols like SPI, I2C, or UART.

Each state machine is essentially a simple finite state machine, capable of executing a sequence of instructions. It can be programmed to react to various inputs and produce outputs accordingly, making it highly versatile for handling complex I/O tasks.

Key Features of the I/O State Machines

Architecture of I/O State Machines

Each state machine consists of the following key components:

Programming the I/O State Machines

Programming the I/O state machines typically involves writing a sequence of instructions that dictate how the state machine will behave. The RP2040 provides a dedicated assembler (part of the Pico SDK) to facilitate this process. Below are the steps to program an I/O state machine:

1. Install the Pico SDK

Before you can program the I/O state machines, ensure that you have set up the Raspberry Pi Pico SDK on your development environment. The SDK includes tools, libraries, and documentation to aid in programming the Pico.

2. Write the State Machine Code

The state machine code is typically written in a simplified assembly language called "Pico Assembly Language." Below is an example of a simple program that toggles 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 into the State Machine

After writing your state machine code, you can load it into the desired state machine using the `state_machine` class in the Pico SDK. Here’s an example of how to load and start the program:


#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

You can control the state machine programmatically through various commands, such as starting, stopping, or resetting the state machine. You can also read the state of GPIO pins or monitor events triggered by the state machine.

Examples of Applications Using I/O State Machines

Conclusion

The I/O state machines in the Raspberry Pi Pico provide a powerful mechanism for handling I/O operations with minimal CPU intervention. By allowing precise control over GPIO pins and enabling complex timing operations, state machines enhance the functionality of the Pico for a wide range of applications. Understanding how to program and utilize these state machines opens up numerous possibilities for embedded systems development.