Raspberry Pi Pico C/C++ SDK Setup and First Blink Program with CMake

MicroPython is excellent for learning and prototyping, but some projects need more: hard real-time loops, fast signal processing, the full set of hardware APIs, or code that must fit a small flash budget. For these, Raspberry Pi provides the official Pico C/C++ SDK (pico-sdk). It gives direct, well-documented access to every peripheral, compiles with GCC for ARM, and uses CMake to manage builds. This tutorial sets up the toolchain, explains the project structure, and builds a blink program with USB serial output that works on Pico, Pico W, Pico 2 and Pico 2 W.

What the Toolchain Contains

ComponentRole
pico-sdkHeaders, libraries and CMake scripts for the RP2040 and RP2350
Arm GNU toolchain (arm-none-eabi-gcc)Cross-compiler that produces ARM Cortex-M machine code
CMake and Ninja or MakeGenerate and run the build
picotoolConverts ELF to UF2, inspects binaries and can load firmware over USB
VS Code with the Raspberry Pi Pico extensionOptional IDE that installs and wires up all of the above

Installing the SDK

Option A: VS Code Extension (Windows, macOS, Linux)

  1. Install Visual Studio Code, then install the official Raspberry Pi Pico extension from the Extensions panel.
  2. Click the Pico icon in the sidebar and choose New C/C++ Project.
  3. Enter a project name such as blink, select your board type (Pico, Pico W, Pico 2 or Pico 2 W) and the SDK version, and tick the option for USB console output.
  4. Click Create. On the first run the extension downloads the SDK, compiler, CMake, Ninja and picotool into your user folder; this takes a while and needs a good connection.
  5. Use the Compile button in the status bar to build, and Run to flash a board connected in BOOTSEL mode.

Option B: Command Line on Ubuntu or Raspberry Pi OS

sudo apt update
sudo apt install cmake gcc-arm-none-eabi libnewlib-arm-none-eabi \
                 libstdc++-arm-none-eabi-newlib build-essential git python3

cd ~
git clone https://github.com/raspberrypi/pico-sdk.git
cd pico-sdk
git submodule update --init      # fetches TinyUSB, the CYW43 driver and more

echo 'export PICO_SDK_PATH=$HOME/pico-sdk' >> ~/.bashrc
source ~/.bashrc

The submodule step matters: without it, USB serial and Pico W Wi-Fi support are missing and the build fails with confusing errors about TinyUSB or cyw43.

Project Files

A minimal project folder contains three files. Copy pico_sdk_import.cmake from pico-sdk/external/ into the folder; it locates the SDK using PICO_SDK_PATH. The VS Code extension creates all of this for you.

blink/
  CMakeLists.txt
  pico_sdk_import.cmake
  blink.c
CMakeLists.txt - builds blink for any Pico board, with USB stdio and the CYW43 driver when needed
cmake_minimum_required(VERSION 3.13)

# Must come before project(): pulls in the SDK and the cross-compiler setup
include(pico_sdk_import.cmake)

project(blink C CXX ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

# Initialise the SDK (board-specific settings come from PICO_BOARD)
pico_sdk_init()

add_executable(blink blink.c)

# pico_stdlib = GPIO, time, stdio and other common functions
target_link_libraries(blink pico_stdlib)

# On W boards the LED is on the wireless chip, so link its driver
if (PICO_CYW43_SUPPORTED)
    target_link_libraries(blink pico_cyw43_arch_none)
endif()

# printf goes to USB CDC serial, not UART
pico_enable_stdio_usb(blink 1)
pico_enable_stdio_uart(blink 0)

# Produce .uf2, .bin, .hex and .dis files in addition to .elf
pico_add_extra_outputs(blink)
blink.c - portable LED blink with USB printf for Pico, Pico W, Pico 2 and Pico 2 W
// blink.c - blink the onboard LED and print a counter over USB serial
#include <stdio.h>
#include "pico/stdlib.h"

#ifdef CYW43_WL_GPIO_LED_PIN
#include "pico/cyw43_arch.h"   // Pico W / Pico 2 W: LED is on the CYW43439
#endif

#define BLINK_MS 500

// Prepare the LED; returns PICO_OK (0) on success
static int led_init(void) {
#if defined(PICO_DEFAULT_LED_PIN)
    gpio_init(PICO_DEFAULT_LED_PIN);              // GP25 on Pico and Pico 2
    gpio_set_dir(PICO_DEFAULT_LED_PIN, GPIO_OUT);
    return PICO_OK;
#elif defined(CYW43_WL_GPIO_LED_PIN)
    return cyw43_arch_init();                     // start the wireless chip driver
#else
    return PICO_ERROR_GENERIC;                    // board has no known LED
#endif
}

static void led_set(bool on) {
#if defined(PICO_DEFAULT_LED_PIN)
    gpio_put(PICO_DEFAULT_LED_PIN, on);
#elif defined(CYW43_WL_GPIO_LED_PIN)
    cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, on);
#endif
}

int main(void) {
    stdio_init_all();             // enable USB serial for printf

    if (led_init() != PICO_OK) {
        while (true) {
            printf("LED init failed\n");
            sleep_ms(1000);
        }
    }

    uint32_t count = 0;
    while (true) {
        led_set(true);
        sleep_ms(BLINK_MS);
        led_set(false);
        sleep_ms(BLINK_MS);
        printf("blink %lu, uptime %llu ms\n",
               (unsigned long)count++,
               (unsigned long long)(time_us_64() / 1000));
    }
}

Building and Flashing

Set PICO_BOARD to match your hardware. The SDK uses it to select the LED pin, flash size and wireless support. Valid values include pico, pico_w, pico2 and pico2_w.

cd blink
cmake -B build -DPICO_BOARD=pico_w
cmake --build build -j4

ls build/*.uf2
build/blink.uf2

Besides the UF2, the build folder holds blink.elf (the file a debugger such as a Raspberry Pi Debug Probe uses, since it contains symbols), blink.bin and blink.hex (raw images for other programmers) and blink.dis, a disassembly listing that is useful when you want to see what the compiler generated for a time-critical loop. Rebuilding after a code change only recompiles what changed, so later builds take seconds.

Hold BOOTSEL, plug in the board and copy build/blink.uf2 onto the RPI-RP2 or RP2350 drive. The LED starts blinking. Open the new USB serial port in any terminal program (PuTTY, the VS Code Serial Monitor, or minicom -D /dev/ttyACM0) to see the printf output. If picotool is installed you can also flash with picotool load -x build/blink.uf2 while the board is in BOOTSEL mode.

How the Code Works

  • pico/stdlib.h brings in the GPIO, time and stdio APIs. gpio_init, gpio_set_dir and gpio_put are the C equivalents of MicroPython's Pin.
  • PICO_DEFAULT_LED_PIN and CYW43_WL_GPIO_LED_PIN are defined by the board header chosen through PICO_BOARD, so the preprocessor picks the right LED code at compile time.
  • pico_cyw43_arch_none links the wireless driver without a network stack, enough to control the LED. Wi-Fi projects use pico_cyw43_arch_lwip_poll or pico_cyw43_arch_lwip_threadsafe_background instead.
  • time_us_64() reads the 64-bit microsecond timer, which does not wrap in any practical lifetime.
  • Printing after the first blink gives the USB host time to enumerate, so early messages are not lost.

Troubleshooting

  • PICO_SDK_PATH not found: export the variable or set it in CMake before the include line.
  • Errors mentioning TinyUSB or cyw43: run git submodule update --init inside the SDK folder.
  • LED does not blink on a Pico W: the build used PICO_BOARD=pico. Delete the build folder and reconfigure with pico_w; CMake caches the board setting.
  • No serial port appears: check that pico_enable_stdio_usb(blink 1) is present and that stdio_init_all() is called.

Summary

You installed the Pico C/C++ SDK through the VS Code extension or manually, created a CMake project, wrote a blink program that adapts to all four Pico boards at compile time, printed over USB serial and flashed the resulting UF2. This project is the template for every C program in the series, including the dual-core example in the next tutorial.