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
| Component | Role |
|---|---|
| pico-sdk | Headers, 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 Make | Generate and run the build |
| picotool | Converts ELF to UF2, inspects binaries and can load firmware over USB |
| VS Code with the Raspberry Pi Pico extension | Optional IDE that installs and wires up all of the above |
Installing the SDK
Option A: VS Code Extension (Windows, macOS, Linux)
- Install Visual Studio Code, then install the official Raspberry Pi Pico extension from the Extensions panel.
- Click the Pico icon in the sidebar and choose New C/C++ Project.
- 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. - 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.
- 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 ~/.bashrcThe 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
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 - 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.hbrings in the GPIO, time and stdio APIs.gpio_init,gpio_set_dirandgpio_putare the C equivalents of MicroPython'sPin.PICO_DEFAULT_LED_PINandCYW43_WL_GPIO_LED_PINare defined by the board header chosen throughPICO_BOARD, so the preprocessor picks the right LED code at compile time.pico_cyw43_arch_nonelinks the wireless driver without a network stack, enough to control the LED. Wi-Fi projects usepico_cyw43_arch_lwip_pollorpico_cyw43_arch_lwip_threadsafe_backgroundinstead.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_PATHnot found: export the variable or set it in CMake before the include line.- Errors mentioning TinyUSB or cyw43: run
git submodule update --initinside the SDK folder. - LED does not blink on a Pico W: the build used
PICO_BOARD=pico. Delete the build folder and reconfigure withpico_w; CMake caches the board setting. - No serial port appears: check that
pico_enable_stdio_usb(blink 1)is present and thatstdio_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.