The Raspberry Pi Pico is a full-featured Arm-core microcontroller for $4 ($7 with Wi-Fi and pre-soldered headers). There are C and MicroPython SDKs, and there is community support for Rust.

One unique feature of the RP2040 chip is the PIO state machine module—useful for driving interfaces with tight timing requirements (e.g., for LED strips) without tying up the CPU.

The Raspberry Pi Pico 2 (released 2024) is $5 and the RP2350 chip has extensive security feature upgrades. There was a GPIO input current leakage bug that has been fixed in the latest stepping.

There is also a Raspberry Pi Debug Probe for $12 (very useful).

See:

C SDK

These are my notes from getting up and running with the C SDK on Mac OS. I had to dig to find the documentation I needed; this is an attempt to collect it all in one place.

Warning: it’s more of a lab notebook than a howto. But it has all the info I’d send back to myself if I could.

See these first:

Precompiled binaries

Start by loading precompiled example programs…

https://www.raspberrypi.com/documentation/microcontrollers/c_sdk.html#your-first-binaries

Mac OS toolchain installation

To compile your own programs…

These didn’t have complete info for Mac OS so I also relied on this video:

I was using the Pico W (version with Wi-Fi).

Here are commands I ran:

brew install make cmake git gcc-arm-embedded libusb
git clone https://github.com/raspberrypi/pico-sdk.git
cd pico-sdk
git submodule update --init

cd ..
git clone https://github.com/raspberrypi/pico-examples.git
mkdir hello-pico
cd hello-pico

cp ../pico-examples/pico_w/wifi/blink/picow_blink.c blink.c
cp ../pico-examples/pico_sdk_import.cmake .

# CMakeLists.txt from the video
wget https://gist.githubusercontent.com/eldelto/0740e8f5259ab528702cef74fa96622e/raw/c8dc72471aaf22f79b677eaac2db8b542b01b1b0/CMakeLists.txt

# edit CMakeLists.txt to uncomment line for Pico W
brew install picotool
# or alternative: export PICOTOOL_FETCH_FROM_GIT_PATH=../../picotool
cd build

export PICO_BOARD=pico_w
export PICO_SDK_PATH=../../pico-sdk
cmake ..

# or alternative: cmake -DPICO_BOARD=pico_w -DPICO_SDK_PATH=../../pico-sdk ..

make -j8        # 8 cores

# hold down BOOTSEL and connect USB
cp blink.uf2 /Volumes/RPI-RP2

Troubleshooting

Make sure you’re in the build directory and running cmake ... Note the ...

If you get errors, you may need to do a clean rebuild by deleting the entire build directory and starting over at cmake ... E.g., I had to do this after running cmake in the wrong directory.

run cmake again or just make?

Usually just running ‘make’ will detect changes in ‘CMakeLists.txt’ and automatically run a ‘cmake ..’ However; there can be times when changes in ‘CMakeLists.txt’ are simply ignored and even a manual ‘cmake ..’ won’t resolve that. You then need to delete the entire ‘./build’ folder and re-build from the start.

Saving build envs

Save the build config env vars so you don’t have to keep retyping/autocompleting them.

Save to picoenvs.sh:

export PICO_BOARD=pico_w
export PICO_SDK_PATH=~/path/to/pico-sdk
export WIFI_SSID='Your SSID here'
export WIFI_PASSWORD='Your passphrase here'

then source it when opening a terminal session.

Add to ~/.profile

alias make="/usr/bin/make -j 8"

Now it’s just

cd build
cmake ..
make

# hold down BOOTSEL and connect USB
cp blink.uf2 /Volumes/RPI-RP2

Building example code

Raspberry Pi supplies a few dozen example programs for the Pico. See the readme.

https://github.com/raspberrypi/pico-examples

Note that some only work on the RP2350 chip.

(This assumes the pico-sdk and pico-examples repos were cloned during toolchain setup.)

source ~/path/to/picoenvs.sh
cd pico-examples

mkdir build
cd build
cmake ..

cd blink
make

# hold down BOOTSEL and connect USB
cp blink.uf2 /Volumes/RPI-RP2

tried these examples:
blink
pico_w/wifi/blink
status_led

All three examples blink the LED on the board.

hello_world

hello_world/usb

Note: stdio is directed to USB. The Pico should be connected to the computer via its USB port. See serial debugging.

screen /dev/tty.usbmodem2302 115200    # or similar name

Ctrl+a then k to quit screen

wifi_scan

pico_w/wifi/wifi_scan

Note: stdio is directed to UART by default. You can either connect to the UART or edit the project to redirect stdio to USB instead. See serial debugging.

ntp_client, tls_client

network time protocol and transport layer security

pico_w/wifi/ntp_client
pico_w/wifi/tls_client

Note that WIFI_SSID and WIFI_PASSWORD env vars must be set or cmake will skip building these examples.

Skipping ntp_client example as WIFI_SSID is not defined

Note: stdio is directed to UART by default. You can either connect to the UART or edit the project to redirect stdio to USB instead. See serial debugging.

Programming and debugging

Overview

options for programming:

  1. program via USB as mass storage volume
  2. program via USB with picotool
  3. program via SWD with openocd (requires Debug Probe)

options for debugging:

  1. printf debugging via USB virtual serial port
  2. printf debugging via UART (requires usb-to-serial adapter or Debug Probe)
  3. gdb/lldb debugging via SWD with openocd (requires Debug Probe)

A Raspberry Pi Debug Probe ($12) is quick to set up and lets you program via SWD, use gdb/lldb debugger, and monitor the UART output—with no need to continually unplug and reconnect cables. Highly recommended for development.

Note that the Pico with pre-soldered headers has a JST connector for the SWD debug port, but the Pico without headers doesn’t. You’ll need to solder on header pins to use SWD.

serial debugging

Good old printf debugging works.

printf("Hello, world!\n");

UART

By default, stdio is directed to UART. You can connect with the Debug Probe or any other usb-to-serial TTL level adapter (FTDI, etc.).

RX/TX/GND connect to pins 1/2/3 on the Pico. See diagram.

brew install minicom
ls /dev/tty.usbmodem*
minicom -b 115200 -o -D /dev/tty.usbmodem2302    # or similar name
# or alternative: screen /dev/tty.usbmodem2302 115200

USB

You can also redirect stdio to USB. The Pico has built-in support to emulate a virtual serial port over its USB port. This way all you need is a USB cable.

The hello_world example shows how you would configure the CMakeLists.txt. It would look something like this:

# enable usb output, disable uart output
pico_enable_stdio_usb(hello_usb 1)
pico_enable_stdio_uart(hello_usb 0)

picotool

query and program Pico via USB

See first:

you must first reboot while holding the BOOTSEL button or

as of version 1.1 of picotool it is also possible to interact with Raspberry Pi microcontrollers that are not in BOOTSEL mode, but are using USB stdio support from the SDK by using the -f argument of picotool.

brew install picotool

useful commands to try

picotool info
picotool info -a
picotool load blink.uf2
picotool reboot

handy way to reboot into BOOTSEL mode
(Note: only works if the loaded program is configured for USB stdio. E.g., you could load pico-examples/hello-world/usb via USB mass storage method.)

picotool info -f

openocd

program Pico via SWD

connect Debug Probe JST-SH for SWD and RX/TX/GND pins for UART

See first:

Installing for RP2350

For RP2040, brew install openocd worked fine for me.

But for RP2350, I had to install Raspberry Pi Foundation’s fork of openocd. I followed instructions from this video. I didn’t run into the compile error he shows in batch.c:194 so it must be fixed.

git clone git@github.com:raspberrypi/openocd.git
cd openocd
git submodule update --init --recursive
brew install libtool automake libusb wget pkg-config gcc texinfo capstone libftdi hidapi
./bootstrap
./configure --disable-werror
make -j4
sudo make install

There is also a warning that appears when programming which can be worked around by adding -c "rp2350.dap.core1 cortex_m reset_config sysresetreq". For example:

openocd -f interface/cmsis-dap.cfg -f target/rp2350.cfg -c "adapter speed 5000" -c "rp2350.dap.core1 cortex_m reset_config sysresetreq" -c "program blink.elf verify reset exit"

Usage

See also:

check Debug Probe firmware version

$ openocd -f interface/cmsis-dap.cfg
...
Info : CMSIS-DAP: FW Version = 2.0.0
...

programming

openocd -f interface/cmsis-dap.cfg -f target/rp2040.cfg -c "adapter speed 5000" -c "program hello_serial.elf verify reset exit"

reboot pico

openocd -f interface/cmsis-dap.cfg -f target/rp2040.cfg -c "adapter speed 5000" -c "init" -c "reset run" -c "shutdown"

halt pico

openocd -f interface/cmsis-dap.cfg -f target/rp2040.cfg -c "adapter speed 5000" -c "init" -c "reset halt" -c "shutdown"

gdb/lldb debugging

See this:

See also:

start openocd server

openocd -f interface/cmsis-dap.cfg -f target/rp2040.cfg -c "adapter speed 5000"

connect with lldb

lldb path/to/project.elf
(lldb) platform select remote-gdb-server
(lldb) process connect connect://localhost:3333

useful commands:

- breakpoint set --hardware --name function_name
  - br list / br delete / br enable / br disable
- continue / c
- step / s - step in
- next / n - step over
- finish - step out
- stepi / si // nexti / ni - by instruction
- print / p
  - parray, po, poarray
- list / l
- disassemble (?) / di
- backtrace / bt
- frame variable
- register read
- var / v
- x
- memory read --size 4 --format x --count 10 0x10000000
- repl

reboot target

(lldb) process plugin packet monitor reset run

create an ~/.lldbinit file:

command alias bh breakpoint set --hardware --name %1

TODO: arm-none-eabi-gdb

TODO: VS Code with Cortex-Debug

flashing from the makefile

(derived from this project on github)

To program with make flash, add this to CMakeLists.txt

set(PROJECT hello_serial)
add_custom_target(flash
    COMMAND echo "Flashing ${PROJECT} ..."
    COMMAND openocd -f interface/cmsis-dap.cfg -f target/rp2040.cfg -c "adapter speed 5000" -c "program ${PROJECT}.elf verify reset exit"
    DEPENDS "${PROJECT}"
    COMMENT "Flash target using openocd"
)

And for make reboot, make halt, and make term:

add_custom_target(reboot
    COMMAND echo "Rebooting ${PROJECT} ..."
    COMMAND openocd -f interface/cmsis-dap.cfg -f target/rp2040.cfg -c init -c reset -c reboot -c exit
    COMMENT "Reboot target using openocd"
)

add_custom_target(halt
    COMMAND echo "Halting ${PROJECT} ..."
    COMMAND openocd -f interface/cmsis-dap.cfg -f target/rp2040.cfg -c init -c reset -c halt -c exit
    COMMENT "Halt target using openocd"
)

add_custom_target(term
    COMMAND minicom -b 115200 -o -D /dev/tty.usbmodem*
)

MQTT

MQTT is a lightweight pub-sub message queue protocol commonly used with embedded devices.

The pico C SDK includes MQTT support as part of lwIP, its TCP/IP stack. The pico-examples repo has an MQTT example that I adapted to connect to adafruit.io using TLS encryption.

qqrs/pico-examples:mqtt-adafruit-io

Seeing is believing. To verify that the MQTT traffic was actually being encrypted, I inspected the packets with Wireshark.

To check that the server certificate was being verified, I turned on debug logging for mbedtls and lwIP and tested with a valid root certificate for the wrong signing authority.

MQTT basics

MQTT concepts:

  • connect to a broker
  • subscribe to a topic
  • publish a message to a topic
  • receive a message

You’ll need an MQTT broker to connect to for testing:

  • you can use the publicly available test server at test.mosquitto.org
  • you can install and run mosquitto locally (the pico-examples mqtt readme has instructions)
  • or, you can use Adafruit I/O (free for basic plan)

lwIP MQTT

The pico C SDK includes MQTT support as part of lwIP, its TCP/IP stack.

The key piece is linking pico_lwip_mqtt.

target_link_libraries(project_name_here
	...
	pico_lwip_mqtt
)

The lwIP documentation is a bit sparse, but everything needed is there if you poke around a little:

There’s also a short lwIP MQTT example with a pretty good tutorial:

The tutorial walks through the important stuff. It also mentions this critical caveat:

You need to increase MEMP_NUM_SYS_TIMEOUT by one if you use MQTT!

MEMP_NUM_SYS_TIMEOUT is the number of simultaneously active timeouts.

What it means is to add a line to lwipopts.h:

#define MEMP_NUM_SYS_TIMEOUT        (LWIP_NUM_SYS_TIMEOUT_INTERNAL+1)

See also: pico-sdk issue #1106 and issue #1725

When I first began working on this, pico-examples didn’t have an MQTT example, so I found a project on Github to use as a starting point.

I had to fiddle with the CMakeLists.txt to get it to pick up the Wi-Fi config. I also did some refactoring to make the code manageable (happy path to the left margin, etc.) and triangulated with the lwIP tutorial to get it working.

MQTT example

pico_w/wifi/mqtt

The pico-examples repo now has a full-fledged MQTT example:

  • it logs sensor data to /temperature when the temperature changes
  • it controls the status LED with /led on and /led off
  • it echoes /print <message> to UART
  • it responds to /ping with /uptime <seconds>
  • /exit causes it to disconnect

It also supports TLS-encrypted MQTT connections.

This is an excellent starting point. It demos the functionality anyone is likely to need. It was pretty easy to get it running by following the README instructions.

MQTT example on Mac OS

The one sticking point is that the examples assume Linux on the host computer. There are some subtle differences with Mac OS.

Here’s what worked for me:

install and start mosquitto mqtt server

brew install mosquitto
brew services start mosquitto
without TLS

vim /opt/homebrew/etc/mosquitto/mosquitto.conf

allow_anonymous true
listener 1883 0.0.0.0
brew services restart mosquitto

set hostname (or IP address) of computer hosting the MQTT server

export MQTT_SERVER=10.100.9.86

(then build the example and flash the device)

mosquitto_pub -t '/led' -m on
mosquitto_pub -t '/led' -m off

(led turns on and off)

with TLS
cd /path/to/pico-examples/pico_w/wifi/mqtt/certs
bash makecerts.sh

(Note: sh makecerts.sh caused problems with the echo -n)

cd /opt/homebrew/etc/mosquitto/
mkdir ca_certificates certs
cp /path/to/pico-examples/pico_w/wifi/mqtt/certs/\$MQTT_SERVER/ca.crt ca_certificates/
cp /path/to/pico-examples/pico_w/wifi/mqtt/certs/\$MQTT_SERVER/server.{crt,key} certs/

vim mosquitto.conf

listener 8883 0.0.0.0
allow_anonymous true
cafile /opt/homebrew/etc/mosquitto/ca_certificates/ca.crt
certfile /opt/homebrew/etc/mosquitto/certs/server.crt
keyfile /opt/homebrew/etc/mosquitto/certs/server.key
require_certificate true
brew services restart mosquitto

(rebuild the example - maybe do a full rm -rf build to be sure - and flash the device)

cd /path/to/pico-examples/pico_w/wifi/mqtt/certs/
./pub.sh /led on
./sub.sh /uptime
./pub.sh /ping

MQTT example with adafruit.io

Adafruit I/O is a hosted MQTT broker service. The basic plan is free and allows 10 topics, 30 messages per minute, and 30 days retention. It’s a nice option if you don’t want to self-host.

Getting connected is pretty easy.

Adafruit IO MQTT API Connection Details

Host: io.adafruit.com
Port: 1883 or 8883 (for SSL encrypted connection)
Username: your Adafruit account username
Password: your Adafruit IO key (click the “AIO Key” button on the dashboard)

MQTT topic names follow this format:

(username)/feeds/(feed name or key)
(username)/f/(feed name or key)

The recommended MQTT protocol version is v3.1.1.

Getting the example running was fairly straightforward. I set the MQTT_SERVER, MQTT_USERNAME, and MQTT_PASSWORD env vars. I also needed to make a few code changes to accommodate the topic naming scheme.

adafruit.io with TLS encryption

The MQTT example already supported TLS encryption, but it required a few changes to work with adafruit.io.
qqrs/pico-examples:mqtt-adafruit-io

bash make_ca_cert.sh will generate the mqtt_client.inc file

The example was designed to use self-generated certificates, so I modified it to use a certificate authority for verifying the server. It was also configured to ask the server to verify the client, but this wasn’t needed.

The problem

MQTT sends everything, including the username and password, as plain text in TCP packets. For an example of this, see verifying TLS with Wireshark.

This means it could be possible for someone to sniff packets to steal sensitive data or to spoof messages to clients. TLS encryption is designed to prevent this.

Adafruit says:

We strongly recommend using SSL/TLS if your MQTT client allows it.

The main changes to the standard ESP8266 example are that WiFiClientSecure is used in place of WiFiClient, and port 8883 is used instead of MQTT port 1883. The sketch also checks the fingerprint for the io.adafruit.com certificate using the verifyFingerprint function.

// io.adafruit.com SHA1 fingerprint
const char* fingerprint = "26 96 1C 2A 51 07 FD 15 80 96 93 AE F7 32 CE B9 0D 01 55 C4";
Verifying the server

There are two ways to verify that a server is who it says it is:

  • certificate pinning
  • certificate authority (CA)

Certificate pinning is used by the Adafruit example above. The fingerprint of the server’s certificate is saved and checked while connecting.

The problem is that certificates only last a couple years, and this will soon be reduced to 47 days. Every time the certificate is rotated, you have to update the fingerprint on your device.

The alternative is to verify the server’s certificate using the root certificate of the certificate authority (CA). Root certificates are often valid for 15+ years.

To get the certificate chain, I ran this command:

$ openssl s_client -connect io.adafruit.com:8883 -servername io.adafruit.com -showcerts
...
depth=2 C=US, O=DigiCert Inc, OU=www.digicert.com, CN=DigiCert Global Root G2
verify return:1
depth=1 C=US, O=DigiCert Inc, OU=www.digicert.com, CN=GeoTrust TLS RSA CA G1
verify return:1
depth=0 C=US, ST=New York, L=New York, O=Adafruit Industries LLC, CN=*.adafruit.com
verify return:1
...

Then, to fetch the root certificate, I ran this (Mac OS):

security find-certificate \
  -c "DigiCert Global Root G2" -a -p \
  /System/Library/Keychains/SystemRootCertificates.keychain \
> digicert_global_root_g2.pem
Verifying the client

The example was configured to ask the server to verify the client’s certificate. This is called mutual TLS (mTLS). This might make sense in an internal enterprise setting, but wasn’t needed or supported by adafruit.io.

I created a separate path in the code that uses one-way TLS if no client certificate/key is defined.

Verifying TLS with Wireshark

I sniffed the Wi-Fi packets sent/received by the pico, decrypted them, and looked for the MQTT packets.

This part probably would have been easier if I had a Wi-Fi adapter that could be put into monitor mode and a router that was more configurable. I was just doing it with my Mac laptop.

Capturing traffic

Mac OS includes a sniffer that can be accessed from Wireless Diagnostics.
Go to Window Sniffer to open it.
.pcap files are saved to /var/tmp

I had to guess the channel the pico was using to connect to my Wi-Fi router. It was the third one I tried, out of the most common channels used for that band.

Decrypting 802.11 packets

After I had a capture as a .pcap file, I opened it in Wireshark.

Here are the steps I used to decrypt the wireless packets using my SSID and Wi-Fi password.

get ip address of pico

  • printed by MQTT example after connecting

get mac address of pico

sudo nmap -sn 192.168.1.2

filter by mac address

wlan.addr == 00:aa:bb:cc:dd:ee

filter to eapol packets

eapol && wlan.addr == 00:aa:bb:cc:dd:ee
  • Need to capture EAPOL Message 1, 2, 3, and 4. If you don’t get all of these, you need to repeat the capture. It took a few tries but I got them.

  • There may be caching mechanisms at play if reconnecting. Some routers allow you to turn this off. Power cycling or simply waiting and trying again may work.

Preferences Protocols/IEEE 802.11 Decryption keys:
Add Key type=wpa-pwd, Key=<wi-fi password>:<wi-fi ssid>

  • If it works, you’ll see ARP, DHCP, and TCP/IP packets.

filter by ip address

ip.addr == 192.168.1.2

filter to mqtt packets

mqtt && ip.addr == 192.168.1.2
  • You should see MQTT packets

Inspecting the packets

The decoded version of the first MQTT packet, the connect command, looked like this:

MQ Telemetry Transport Protocol, Connect Command
    Header Flags: 0x10, Message Type: Connect Command
    Protocol Name: MQTT
    Version: MQTT v3.1.1 (4)
    Connect Flags: 0xee, User Name Flag, Password Flag, Will Retain, QoS Level: At least once delivery (Acknowledged deliver), Will Flag, Clean Session Flag
    Keep Alive: 60
    Client ID: pico
    Will Topic: /online
    Will Message: 30
    User Name: <my username>
    Password: <my password!>
    ...

There they were, MQTT username and password as plain text.

After switching on TLS encryption, I captured more packets. Instead of showing MQTT packets, Wireshark showed TLSv1.2 packets. They specified port 8883, which indicates MQTT, but there was no way to inspect the contents.

MQTT debug logging

You can turn on debug logging by building in DEBUG mode:

cd build
cmake .. -DCMAKE_BUILD_TYPE=DEBUG
cd pico_w/wifi/mqtt
make

For debugging TLS, this will produce output like this if the server’s certificate can’t be verified:

mbedtls_ssl_handshake failed: -9984

-9984 is -0x2700 which corresponds to:

#define MBEDTLS_ERR_X509_CERT_VERIFY_FAILED               -0x2700

For debugging the MQTT protocol, you can add a flag to lwipopts.h:

#define MQTT_DEBUG LWIP_DBG_ON

which produces output like this:

mqtt_publish: Publish with payload length 5 to topic "temperature"
mqtt_output_send: tcp_sndbuf: 11651 bytes, ringbuf_linear_available: 19, get 237, put 10
mqtt_parse_incoming: Remaining length after fixed header: 2
mqtt_parse_incoming: msg_idx: 4, cpy_len: 2, remaining 0
mqtt_message_received: PUBACK response with id 18

Time via HTTPS

I needed real-world time and saw that the pico-examples repo includes an ntp example. It uses the lwIP implementation of SNTP.

I hoped this stood for Secure NTP; unfortunately, it stands for Simple NTP, a stateless version of NTP for resource-limited devices. NTP and SNTP have little protection against attacks; there are secure extensions, but they aren’t implemented in lwIP.

As an alternative, I got the time via HTTPS.

http_client example

The pico-examples http_client example shows how to verify the TLS certificate of the server, send a GET request, and receive the response body.

pico_w/wifi/http_client

It didn’t take long to get it working; however, it also turned out to be a little half-baked. Resources weren’t being properly freed after each request.

The lack of robust examples seems to be a common complaint about lwIP. The book Programming the Raspberry Pi Pico In C says:

  • The driver and the LwIP library provide a huge range of poorly documented possibilities.

The online sample of that book also provides an example of an HTTPS GET request, but it has the same bug!

resource leak

I ran into a problem when running multiple requests in succession. It was fine for the first couple dozen requests; then it stopped working.

I found that it always happened after the 23rd request, and it didn’t matter if the interval between requests was 1 second, 10 seconds, or 10 minutes.

After digging into the lwIP source code (quite a bit more of it than I ever intended to!), I found the cause. lwIP packet buffers (pbuf’s) are a limited resource (PBUF_POOL_SIZE defaults to 24), and are supposed to be freed by our tcp_recv_fn callback.

This wasn’t being done by the example code. The fix is to free the pbuf as required.

lwIP docs: altcp_recv(), tcp_recv(), tcp_recv_fn():

Sets the callback function that will be called when new data arrives. […] If the callback function returns ERR_OK or ERR_ABRT it must have freed the pbuf, otherwise it must not have freed it.

tcp recv window

We can get a hint at what else is needed by looking at lwip/src/apps/http/http_client.c.

    if (req->recv_fn != NULL) {
      return req->recv_fn(req->callback_arg, pcb, p, r);
    } else {
      altcp_recved(pcb, p->tot_len);
      pbuf_free(p);
    }

So our recv_fn should probably call altcp_recved(), too.

(I confirmed this is correct by turning on tcp recv window logging.)

lwIP docs: altcp_recved(), tcp_recved():

This function should be called by the application when it has processed the data. The purpose is to advertise a larger window when the data has been processed.

Stack Overflow

I came across pico-examples PR 385 which led me to issue 318 and a Stack Overflow reply about the problem:

However, that example is incomplete since it has a memory leak. You will need to free the memory taken by the struct pbuf *hdr in the headers function and struct pbuf *p in the body function with respectively pbuf_free(hdr); and pbuf_free(p);

Without those modifications, it will stop working after about 20 calls (probably depends on the size of the response).

(Strikeout mine.)

Another reply says:

This worked for me but could only add pbuf_free(p) to the body function, not the headers function, or else it would throw an error about a double free: “pbuf_free: pref > 0”.

This also matched my experience. It looks like lwIP frees the header pbuf itself, so we don’t need to do it.

wiki

The Stack Overflow post also led to an lwIP wiki, which provides a little clarifying information, but still no complete examples.

tls_client example

pico-examples issue 318 pointed me to the tls_client example, which also shows how to verify the TLS certificate of the server, send a GET request, and receive the response body.

pico_w/wifi/tls_client/tls_verify.c

It gets the pbuf_free() and altcp_recved() calls correct.

However, it uses the lwIP TCP and TLS APIs directly, rather than the higher-level http_client “app”. There is a lot more initialization and deinitialization to manage, not to mention needing to parse the response yourself.

http_client handles all of that for you. There just needs to be a giant warning telling you that you still need to free the pbuf’s! It’s a counterintuitive pattern to expect you to free resources you didn’t explicitly allocate.

lwIP debug logging

pbuf leak

To debug the problem and confirm my suspicion of a resource leak, I turned on the lwIP stats.

Compile with cmake -DCMAKE_BUILD_TYPE=Debug .. to enable LWIP_STATS and LWIP_STATS_DISPLAY.

Add to lwipopts.h

#define MEMP_STATS 1
#define PBUF_STATS 1

then call

extern void stats_display();
...
stats_display();

to print lwIP stats, including pbuf stats.

Lots of info is printed and it’s unwieldy. To just print the pbuf stats:

#include "lwip/stats.h"
#include "lwip/memp.h"

static void pbuf_stats(void) {
    const struct stats_mem *s = lwip_stats.memp[MEMP_PBUF_POOL];
    printf("MEM PBUF_POOL avail=%u  used=%u  max=%u  err=%u\n",
		(unsigned)s->avail, (unsigned)s->used,
		(unsigned)s->max, (unsigned)s->err);
}

If a debug probe is attached, it should be possible to watch lwip_stats to see when resources are allocated and released.

pbuf pool size

I also reduced the pbuf pool size for testing:

lwipopts.h

#define PBUF_POOL_SIZE 6
#define LWIP_DISABLE_TCP_SANITY_CHECKS 1

tcp recv window logging

While investigating when to call altcp_recved(), I turned on debugging for the TCP window.

Compile with cmake -DCMAKE_BUILD_TYPE=Debug .. to enable LWIP_DEBUG.

Add to lwipopts.h

#define TCP_DEBUG LWIP_DBG_ON

The output looks like this. (I added the labels along the right margin.)

tcp_recved: received 1380 bytes, wnd 16384 (0).         <- TLS
tcp_recved: received 1283 bytes, wnd 16384 (0).         <- TLS
tcp_recved: received 51 bytes, wnd 16384 (0).           <- TLS
tcp_recved: received 29 bytes, wnd 15643 (741).         <- TLS
tcp_recved: received 731 bytes, wnd 16374 (10).         <- HTTP headers
-- my recv callback runs here --
tcp_recved: received 10 bytes, wnd 16384 (0).           <- HTTP content

If altcp_recved() is called in the recv callback, the TCP window returns to its max value (wnd 16384) by the last line. If it isn’t called, the window remains at 16374.

This isn’t a showstopper like the pbuf leak, though; when a new connection is made, it starts over with wnd 16384. It would only be an issue if reusing TCP connections with Keep-Alive.

mbedTLS logging

I also turned on mbedTLS debug logging so I could be sure the first four tcp_recved lines in the tcp recv window logging were from TLS, not something unexplained.

Compile with cmake -DCMAKE_BUILD_TYPE=Debug .. to enable LWIP_DEBUG.

mbedtls_config.h

#define MBEDTLS_DEBUG_C

lwipopts.h

#define ALTCP_MBEDTLS_LIB_DEBUG LWIP_DBG_ON

main.c

#include "mbedtls/debug.h"

int main() {
...
    mbedtls_debug_set_threshold(2);
}

The output is extremely verbose even at level 2! By scrolling back I was able to see when the TLS handshaking happened.

C SDK bonus libraries

Hardware hashing

The RP2350 chip has hardware support for SHA-256.

C SDK pdf

To use this in mbedtls you need to define MBEDTLS_SHA256_ALT in your mbedtls_config.h. Use LIB_PICO_SHA256 to check if hardware SHA256 is supported and fallback to defining MBEDTLS_SHA256_C for the software SHA256 calculation.

mbedtls_sha256/mbedtls_config.h example:

#if LIB_PICO_SHA256
// Enable hardware acceleration
#define MBEDTLS_SHA256_ALT
#else
#define MBEDTLS_SHA256_C
#endif

Benchmarking with pico-examples/sha/mbedtls_sha256, it’s ~14X faster to hash 1MB in hardware on RP2350.

RP2040 software~950 ms
RP2350 software~430 ms
RP2350 hardware~30 ms

base64

base64 is a scheme for encoding binary data using only 64 printable text characters: A-Z, a-z, 0-9, +, and /.

There is pico support for base64 encoding and decoding via the mbedtls libraries. To use it, link the pico_mbedtls library and include mbedtls/base64.h.

mbed-tls docs:

int mbedtls_base64_decode(unsigned char *dst, size_t dlen, size_t *olen, const unsigned char *src, size_t slen)

Unit testing

RPIPicoCPPUTest

I found a template project that integrates the CppUTest framework.

YouTube: C/C++ Unit Testing on Raspberry PI Pico

Github: RPIPicoCPPUTest

  • Template Project for CPPUTest on Raspberry PI Pico

I copied RPIPicoCPPUTest/baremetal into a new project directory and was able to use it as a base project already set up for unit tests.

To build the test target, I ran:

mkdir build
cd build
cmake ..
make test

I was compiling for RP2350 and got errors around these lines in two CMakeLists.txt files:

    PICO_DEFAULT_UART_RX_PIN=16
    PICO_DEFAULT_UART_TX_PIN=17

Removing them resolved the errors. I flashed the binary to the device, connected via UART terminal, and was able to see test runner output:

RUNNING TESTS
...
Errors (1 failures, 3 tests, 2 ran, 4 checks, 0 ignored, 1 filtered out, 0 ms)

While poking through the CMakeLists.txt files, I made a few changes that seemed like a good idea: pinned the CPPUTest git hash; changed the destination dir for compiled targets to bin/, instead of src/ and test/; and picked more descriptive names than NAME and TEST_NAME for the CMake variables referring to the targets.

I also added custom targets:

  • make flash, make flash-tests to flash the device using OpenOCD
  • make reboot and make halt to reboot/halt the device
  • make term to open a minicom terminal to the UART

CppUTest

CppUTest is a C/C++ based framework for unit testing and for test-driving your code. It is written in C++ but is used in C and C++ projects and frequently used in embedded systems.

CppUTest’s core design principles

  • Simple to use and small
  • Portable to old and new platforms
  • Build with Test-driven Development in mind

LGTM!

The CppUTest manual is only about 9 pages long. They aren’t kidding about “simple to use and small.”

This repository shows example usage, including how to link function mocks.