This post is a proof of concept. A test to show that the generic PID class I wrote with a TI MSPM0, is actually generic.
In, this post, I make it run on a Raspberry Pico2.
The design is a stabile DC voltage supply.
- Pico PWM generates a DC voltage. The level is set by changing the duty cycle between 0% (0 V) and 100% (3.3 V)
- a low-pass RC filter turns the PWM signal into DC.
- Pico ADC monitors that DC value (feedback).
- my PID class controls the PWM, based on desired output, and the ADC feedback.
For a full design explanation, see PID controller without floating point for ARM controllers using DSP C++ fixed point API . This post uses the exact same design. Only the peripherals are ported from the MSPM0 SDK to Pico SDK: GPIO, ADC, PWM and timer tick.

Differences compared to MSPM0 design:
- the Pico can run the PWM at higher frequency, with the same duty cycle resolution (0-4095): MSPM0: 488 Hz, Pico 36 kHz.
- It executes the PID loop faster. With compiler optimisation to max: MSPM0: 140 µs, Pico RP2350: 2.8 µs
Same as MSPM0 design:
- identical code, except the SDK calls to GPIO, PWM, ADC and timer
- same resolution of ADC and PWM duty cycle
- same GCC ARM compiler version and optimisation settings
Pico port of the PID example
source
#include "fixed_point.hpp"
#include "hardware/adc.h"
#include "hardware/gpio.h"
#include "hardware/pwm.h"
#include "pico/time.h"
import pid;
using namespace arm_cmsis_dsp;
using fp_t = Q15;
constexpr Q15 operator""_fp(long double val) {
return operator""_q15(val);
}
using pid_controller_t = pid::pid<fp_t>;
// PID loop timer
volatile bool perform = false;
struct repeating_timer timer;
bool repeating_timer_callback(struct repeating_timer *t) {
perform = true;
return true;
}
// ADC and PWM declarations
// Pico SDK types are used here:
const uint ADC_PIN = 26;
const uint PWM_PIN = 0;
const uint PWM_CHANNEL = 0;
// frequency of PWM will be 125 MHz / (PWM_MAX * CKDIV)
// i.e. about 30.5 kHz for PWM_MAX = 4095
const uint PWM_MAX = 4095;
const float CKDIV = 1; // set CKDIV to 1 for approx 30.5 kHz PWM frequency if PWM_MAX is 4095
uint slice_num;
// PID variables and objects
fp_t feedback;
constinit fp_t target_command = 0.66_fp; // Example target command in fixed point per-unit (0.0 to 1.0)
constinit fp_t output_pid = 0.0_fp; // Initialize output_pid to 0.0 in fixed point per-unit
pid_controller_t controller(0.1_fp, 0.01_fp, 0.05_fp); // Example PID gains for voltage control
// GPIO pin, to measure duration of a PID loop
const uint GPIO_PIN = 22; // happens to be the Pico Eurocard LED
void init_adc() {
adc_init();
adc_gpio_init(ADC_PIN);
adc_select_input(0);
}
void perform_adc() {
uint16_t adc = adc_read();
feedback.v = static_cast<int16_t>((adc) << 3); // Direct to 0.0 - 1.0 per-unit in Q15!
}
void init_pwm() {
gpio_set_function(PWM_PIN, GPIO_FUNC_PWM);
slice_num = pwm_gpio_to_slice_num(PWM_PIN);
pwm_set_clkdiv(slice_num, CKDIV);
pwm_set_wrap(slice_num, PWM_MAX-1);
pwm_set_chan_level(slice_num, PWM_CHANNEL, 0);
pwm_set_enabled(slice_num, true);
}
void perform_pwm() {
// Convert Q15 per-unit (0.0 to 1.0 excluded) to raw timer counts (0 to 65535)
// the level will be 0 - 4095, 12 bits
pwm_set_chan_level(slice_num, PWM_CHANNEL, static_cast<std::uint16_t>(output_pid.v >> 3)); // set PWM value
}
void init_gpio() {
gpio_init(GPIO_PIN);
gpio_put(GPIO_PIN, 0);
gpio_set_dir(GPIO_PIN, true);
}
void perform_pid() {
output_pid = controller.compute(target_command, feedback);
}
int main() {
init_adc();
init_pwm();
init_gpio();
// PWM is +- 30 (36.6) kHz, set PID loop to 1/10: 3.66 kHz
add_repeating_timer_us(-274, repeating_timer_callback, NULL, &timer); // call PID every 274 µs, Start-tot-Start timing
while (1) {
if (perform) {
perform = false;
gpio_xor_mask(1u << GPIO_PIN); // toggle before
perform_adc();
perform_pid();
perform_pwm();
gpio_xor_mask(1u << GPIO_PIN); // toggle after
}
}
}
CMake
cmake_minimum_required(VERSION 3.28)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 26)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fmodules-ts -fcommon -fno-rtti -fno-exceptions")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mthumb -mfpu=fpv5-sp-d16")
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# set(PICO_DEOPTIMIZED_DEBUG 1)
# This block automatically detects the active selection in VS Code
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
# 1. If you select "Debug" in VS Code:
set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g -gstrict-dwarf -gdwarf-3" CACHE STRING "Debug flags" FORCE)
# 2. If you select "Release" in VS Code:
set(CMAKE_CXX_FLAGS_RELEASE "-O3" CACHE STRING "Release flags" FORCE)
endif()
# path to PICO utilities (if needed)
if (NOT DEFINED ENV{PIOASM_DIR})
message("PIOASM: set PIOASM_DIR env variable if you use PIO")
else()
message("PIOASM: PIOASM_DIR env variable is set to '$ENV{PIOASM_DIR}'")
set(pioasm_DIR $ENV{PIOASM_DIR})
endif()
if (NOT DEFINED ENV{PICOTOOL_DIR})
message("PICOTOOL: set PICOTOOL_DIR env variable if you create uf2 output with pico_add_extra_outputs()")
else()
message("PICOTOOL: PICOTOOL_DIR env variable is set to '$ENV{PICOTOOL_DIR}'")
set(picotool_DIR $ENV{PICOTOOL_DIR})
endif()
# set(PICO_PLATFORM rp2350-riscv CACHE STRING "Pico Platform")
set(PICO_PLATFORM rp2350-arm-s CACHE STRING "Pico Platform")
set(PICO_BOARD pico2 CACHE STRING "Board type")
include(pico_sdk_import.cmake)
project(pico_pid_template C CXX ASM)
pico_sdk_init()
include(FetchContent)
# CMSIS DSP fixed point c++ library dsppp
FetchContent_Declare(dsp_fixed_point
GIT_REPOSITORY "https://github.com/ARM-software/CMSIS-DSP.git"
GIT_TAG HEAD
GIT_SHALLOW OFF # needed to find the commit HEAD
# prevent makefile execution (if any)
SOURCE_SUBDIR =
)
FetchContent_MakeAvailable(dsp_fixed_point)
# PID embedded C++ library
FetchContent_Declare(pid_embedded
GIT_REPOSITORY "https://github.com/jancumps/pid_embedded_cpp.git"
GIT_TAG origin/main
# prevent makefile execution (if any)
SOURCE_SUBDIR =
)
FetchContent_MakeAvailable(pid_embedded)
include_directories( # this project uses CMSIS DSP fixed point library, so include the path to the CMSIS DSP headers
${PICO_SDK_PATH}/src/rp2_common/cmsis/stub/CMSIS/Core/Include
${dsp_fixed_point_SOURCE_DIR}/dsppp/Include/dsppp
)
add_executable(${CMAKE_PROJECT_NAME})
# 4. Voeg ook de CMSIS definitie toe voor ARM-modus
target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE
ARM_MATH_CM33=1
)
target_sources(${CMAKE_PROJECT_NAME}
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/main.cpp
)
target_sources(${CMAKE_PROJECT_NAME}
PUBLIC
FILE_SET cxx_modules TYPE CXX_MODULES FILES
${pid_embedded_SOURCE_DIR}/source/pid_template.cpp
)
target_link_libraries( ${CMAKE_PROJECT_NAME}
pico_stdlib
hardware_adc
hardware_gpio
hardware_pwm
)
pico_add_extra_outputs(${CMAKE_PROJECT_NAME} )
references:
Development kit: Pi Pico Eurocard Development Board
MSPM0 blog: PID controller without floating point for ARM controllers using DSP C++ fixed point API
PID class GitHub repository
Thank you for reading







