Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
.env.*
!.env.example
.vscode
.aider*
548 changes: 0 additions & 548 deletions content/blog/2025-03-18_func-async.md

This file was deleted.

103 changes: 103 additions & 0 deletions content/blog/bare-metal/2025-06-03_pico-debug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
+++
title = "Pico 2 as debugprobe"
description = "How to use the Raspberry Pi Pico 2 as a hardware debugger."
draft = false
weight = 7
[taxonomies]
tags = [ "Raspberry Pi", "Pico", "SWD", "declarative", "Rust", "probe-rs", "cargo-embed" ]
+++

On most popular microcontrollers used for educational purposes, there is already some hardware debugging support (also called a **hardware debug probe**) on the board itself, such as on the [Micro:bit](https://microbit.org/) or the [ESP32](https://www.espressif.com/en/products/socs/esp32).

Having this debug probe allows you to debug the code running on the target Pico using GDB or other debugging tools.

A debug probe comes in the form of a small secondary chip that can be used to debug the main microcontroller on the board.

The Pico family of microcontrollers does not have this feature built-in. You have two options for debugging a Pico:

* It is possible to turn a spare Raspberry Pico into a hardware debugging probe for another Pico.
* You buy (or borrow) an official Rasberry Pi hardware debug probe.
* You manually force the target into BOOTSELF mode and flash with `picotool`. In that case, you will not be able to debug as easily (you will need a serial monitor).

In this workshop, we will pursue the first option. If you get stuck, feel free to ask for a pre-made hardware debugger.

### Turning a Pico 2 into a debugger Pico

The Raspberry Pi Foundation provides images for Picos that can be flashed to turn a Pico into a hardware debugger.

1. Download the latest `debugprobe_on_pico.uf2` flash image from the official [`debugprobe`](https://github.com/raspberrypi/debugprobe/releases)releases.
2. Attach the Pico to your laptop while holding the white BOOTSEL button. A mass storage device will appear in your file manager. It will be called something like `RP2350`.
3. Drop the downloaded `.uf2` file onto the mass storage drive emulated by the Pico. Wait for a fraction of a second while the Pico unmounts and reboots as a fresh `debugprobe`.

Now you have successfully made a cheap hardware debugging probe.

### Wire target to debugger

Let's make some aliases:

* Assume **D** is the homemade debug probe (a Pico).
* Assume **T** is the target Pico.

Right now, there is no cabling between the debug probe and the target Pico. The cables should be connected such that **D** can detect **T** over the SWD debugging protocol.

***Important**: For this step, you need to have a JST-SH cable. You can find them on [Kiwi](https://www.kiwi-electronics.com/en/jst-sh-1mm-pitch-3-pin-to-male-headers-cable-100mm-long-19930), but they can be hard to find.*

1. Plug the white connector of the JST cable into the SWD socket of **D**.

2. Place **T** and **D** in parallel with their USB ports facing upwards (to prevent confusion).

3. Connect the male jumper pins. The three male header pins from **T**'s JST cable should be connected to **D** as follows:

* **T** left (yellow) \<-\> **D** pin 5
* **T** middle (black) \<-\> **D** pin 3
* **T** right (orange) \<-\> **D** pin 4

Instead of pin numbers, you can also use the pin names:

* **T** SWCLK \<-\> **D** GP3
* **T** SWDIO \<-\> **D** GP2
* **T** GND \<-\> **D** GND

4. Provide power to **T** using a single USB cable by forwarding power from **D**:

* **T** GND pin 38 \<-\> **D** pin 38 (Connect ground)
* **T** VSYS pin 39 \<-\> **D** pin 39 (Connect power supply)

*Remark: You can also connect **T** to **D** for UART communication. However, I have not needed it so far.*

### Configure flashing from laptop

There is still one step remaining: we have to configure our laptop's development environment to enable flashing (this applies to any microcontroller with an onboard or external debugger).

1. Install `cargo-embed`, which is included in the `probe-rs` tool suite.

```bash
cargo install probe-rs-tools
```

2. Verify that `cargo-embed` is available in your shell's `PATH` (`cargo-[CMD]` can be called with `cargo [CMD]`):

```bash
cargo embed --version
```

3. Add `udev` rules for `probe-rs` as described in the [probe-rs documentation](https://probe.rs/docs/getting-started/probe-setup/).

```bash
sudo curl --output /etc/udev/rules.d/69-probe-rs.rules "https://probe.rs/files/69-probe-rs.rules"
sudo udevadm control --reload-rules
sudo udevadm trigger
```

Now you can flash changes in the source code directly to the target Pico (without re-plugging it or holding the BOOTSEL button). The debug probe Pico will function as an intermediary between your laptop and the target Pico.

```bash
cargo run
```

You should see two progress bars running to completion in your terminal. As soon as the flash process is finished:

* **T** will start running the new code.
* A debug server will be started on **D** so that you can step through your code while it runs on **T**.

While the Pico has a generous amount of flash memory, Embassy-produced binaries can sometimes be large. For microcontrollers with less memory, the [Min-sized Rust](https://github.com/johnthagen/min-sized-rust) guide provides tips for reducing binary size.
94 changes: 94 additions & 0 deletions content/blog/bare-metal/2025-06-04_embassy-example.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
+++
title = "Introduction to Embassy"
description = "Overview of the Embassy framework."
draft = false
weight = 8
[taxonomies]
tags = [ "Rust", "Embassy" ]
+++

## Minimal Embassy example

It can be useful to start with a minimal Embassy program. The following does nothing but can serve as a template for future programs.

```rust
#![no_std]
#![no_main]

use defmt_rtt as _;
use embassy_executor::{Spawner, main};
use embassy_rp::config::Config;
use panic_probe as _;
use embassy_rp::bind_interrupts;

bind_interrupts!(struct Irqs {
PIO0_IRQ_0 => InterruptHandler<PIO0>;
});

#[main]
async fn main(_spawner: Spawner) -> ! {
let p = embassy_rp::init(Config::default());
loop {
embassy_futures::yield_now().await;
}
}
```

As you can see, there are two notable attributes at the top of the file.

* `#![no_std]` means that the program does not use the standard library. Embedded systems are too small for the standard library. Instead of using `std::String`, you would a statically allocated `heapless::String`. Most `std` heap allocated container types have an analogue in `heapless`.
* `#![no_main]` means that the program does not have a typical `main` function (with arguments or an exit code) as on a typical operating system. Instead, calling and creating the `main` function is completely handled by the Embassy framework.

Then there are two `use x as _;` lines. These crates don't expose functions or public modules to be used, but they contain setup code that should be included at least once in your embedded program.

* The `panic_probe` crate provides a panic handler that is compatible with Embassy. Panics are **fatal errors**. Every embedded program needs a panic handler because traditional panics would unwind or abort and yield control back to the operating system. This operating system is absent, so we have to tell the compiler how to handle panics. Usually, this means going into an infinite loop.
* The `defmt_rtt` is not useful for the moment, but once you have configured a hardware debugger, it will allow you to log messages to the debugger console. This is useful for debugging your program.

There is a macro call `embassy_rp::bind_interrupts!` that binds hardware interrupts with the Embassy framework. This is necessary to be able to use hardware interrupts in your program. Hardware interrupts can stop the current ongoing computation and jump execution to some handler code elsewhere. Examples of hardware interrupt bindings available on the Pico 2 are:

* `PIO0_IRQ_0` is an interrupt coming from the PIO peripheral.
* `USBCTRL_IRQ` for USB interrupts (relevant in USB serial communication).
* `ADC_IRQ_FIFO` for ADC interrupts (relevant for reading data from the analog-to-digital converter in the moisture sensor).

The `spawner` argument allows users to spawn asynchronous tasks. Keep in mind, however, that each task should be non-generic and completely specified at compile time. This is because the Embassy framework does not support dynamic task creation at runtime.

The last line `loop { yield_now().await }` may seem unnecessary. The reason I have to write it is because the return type of `main` is "never" (written as `!`). The `never` return type is the type for a function that never returns.

Because of the signature of `main`, we cannot simply escape the `main` function. Running this program is the only thing that happens on the microcontroller. So we have to keep looping, even if we have already finished our work.

## Levels of Abstraction in Embedded Rust

This section provides an overview of the different levels of abstraction that can be used when programming microcontrollers in Rust.

### Low Level

The lowest level of software abstraction provides direct access to the microcontroller's hardware registers.

* **Core Support Crate**: Enables access to the core processor's features, like interrupts and system timers. See [Cortex-M](https://crates.io/crates/cortex-m).
* **Peripheral Access Crate (PAC)**: Built on top of the core support crate, the PAC contains auto-generated code for accessing hardware peripherals (like GPIO, ADC, etc.) based on SVD files from the chip manufacturer. See [RP235X-PAC](https://crates.io/crates/rp235x-pac).

The Embassy framework builds on top of the PAC and HAL to provide a more intuitive and convenient API for accessing the hardware.

### Medium Level

If the Embassy framework doesn't suit your needs, you can fall back to a more conventional level of abstraction without `async/await`.

The **Hardware Abstraction Layer (HAL)** is a more convenient way to access the hardware. It provides a higher level of abstraction than the PAC but still allows direct hardware access.

The Pico 2 has [rp235x-hal](https://crates.io/crates/rp235x-hal) as its HAL crate. You can view the [examples](https://github.com/rp-rs/rp-hal/tree/main/rp235x-hal-examples), which were used as a reference for this workshop.

*Remark: If you need to preempt tasks (i.e., interrupt a lower-priority task to run a higher-priority one), you should consider using [RTIC](https://github.com/rtic-rs/rtic). RTIC provides a different concurrency model based on preemption and priorities, which may be required for real-time applications.*

### High Level

For commonly used microcontrollers, there is often at least one good **Board Support Package (BSP)**. These are crates that provide a convenient, board-specific API, though they are sometimes less customizable than a HAL. For example, in the case of the Micro:bit controller, the BSP is called [microbit](https://crates.io/crates/microbit) and it allows you to draw shapes on the on-board LED array.

For the Raspberry Pi Pico 2 W, `embassy` (and its `embassy-rp` plugin) come the closest to a full-featured BSP.

## More Reading Material

Interesting books about embedded Rust:

* There is a book for beginners in embedded Rust: [The Discovery Book](https://docs.rust-embedded.org/discovery-mb2/). It assumes you have a Micro:bit v2 (\~€20).
* There is also a book about embedded Rust using an STM32 chip: [The Embedded Rust Book](https://docs.rust-embedded.org/book/).
* Another book about Rust and the Raspberry Pi Pico 2 is [Pico, In-Depth](https://pico.implrust.com/).
79 changes: 79 additions & 0 deletions content/blog/bare-metal/2025-06-05_defmt-logging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
+++
title = "Logging with defmt over RTT"
description = "How to use the defmt crate to log messages from a Raspberry Pi Pico 2 W over RTT."
draft = false
weight = 9
[taxonomies]
tags = [ "defmt", "RTT", "SWD", "Rust", "GDB" ]
+++


## Simple Logging

RTT (Real-Time Transfer) is a logging protocol that can be used on top of an SWD connection. It does not require specifying the baud rate, etc.

The `defmt` crate is the most popular crate for logging from embedded Rust programs. It exports macros like `info!` and `debug!`, similar to the macros in the standard `log` or `tracing` crates in Rust.

For the debug probe to actually show the log output from the target, you need to enable a "transport". In the case of `defmt`, it is usually the `RTT` transport using the `defmt-rtt` crate. The `defmt-rtt` crate could be compared to `tracing-subscriber` or other mainstream log consumers.

1. Add `defmt` and `defmt-rtt` as a dependency to your `Cargo.toml` file. Also, enable the `defmt` features for all existing dependencies that have it.

2. Import the `defmt-rtt` module in your binary or library:

```rust
use defmt_rtt as _;
```

This may seem useless, but it enables the setup of data necessary to link the binary against the `defmt-rtt` crate.

3. Add a compiler flag under the current target in the `.cargo/config.toml` file: `-C link-arg=-Tdefmt.x`.

```toml
[target.thumbv8m.main-none-eabihf]
rustflags = [
"-C",
"link-arg=--nmagic",
"-C",
"link-arg=-Tlink.x",
"-C",
"link-arg=-Tdefmt.x",
"-C",
"target-cpu=cortex-m33",
]
```

4. Specify the log level for `defmt` in the `.cargo/config.toml` file:

```toml
[env]
DEFMT_LOG = "debug"
```

5. Enable `rtt` in the `Embed.toml` file:

```toml
[default.rtt]
enabled = true
```

6. Add invocations of the `defmt` macros throughout your library or binary code (as necessary). For example, you could write:

```rust
use defmt::info;

async fn main(_spawner: Spawner) -> ! {
loop {
info!("A new iteration of the loop has started.");
}
}
```

There is nothing stopping you from adding such statements to library code.

7. Compile, flash, and run your binary on the target Pico 2 W:

```bash
cargo ru
```

This should open an RTT console that shows the log messages emitted by the `defmt` statements in your code.
104 changes: 104 additions & 0 deletions content/blog/bare-metal/2025-06-06_gdb.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
+++
title = "Usage of GDB debugger"
description = "How to use the GDB debugger with a Raspberry Pi Pico 2 W."
draft = false
weight = 10
[taxonomies]
tags = [ "SWD", "Pico", "Rust", "cargo-embed" ]
+++




## Debugging with GDB

Once you start creating slightly more complicated embedded programs, you might want to:

* introspect the values of local variables
* follow the execution flow

For this, you need a piece of software called a debugger. The most commonly used debugger for Rust and C is [GDB](https://en.wikipedia.org/wiki/GNU_Debugger).

*Remark: In VS Code, you can install the `probe-rs-debug` extension to use the `probe-rs` toolkit for debugging. It uses a different protocol than `gdb`. See [instructions](https://probe.rs/docs/tools/debugger/).*

### Setup of `cargo-embed`

Adjust the `Embed.toml` file in the root of this repository if necessary. This file configures the behavior of the `cargo embed` command when run on your laptop.

For example, if the configuration contains the following, a GDB debug server session will be started, and the loaded program will be reset to the first instruction.

```toml
[default.gdb]
enabled = true

[default.reset]
halt_afterwards = true
```

Prevent lines from being merged or reordered during the build step of your program. These kinds of changes can make it harder for the debugger to stop at the right breakpoints. Add the following to `Cargo.toml`:

```toml
[profile.dev]
debug = 2
opt-level = 0
```

To be sure the new configuration is used, you can clear the `target` build cache with `cargo clean` and then build again:

```bash
cargo clean
cargo build --example [BINARY_EXAMPLE_NAME]
```

### Starting a GDB Client

While searching for an appropriate GDB package, look for one that supports the architecture of your target chip. In the case of a Pico 2, `gdb` needs `ARM` support built in.

Install the multi-architecture version of `gdb`:

```bash
sudo apt-get install gdb-multiarch
```

Then run the following command to create and connect a `gdb` debugging client:

```bash
gdb-multiarch target/thumbv8m.main-none-eabi/debug/[BINARY_EXAMPLE_NAME]
```

*Note: The command may also be `gdb`.*

Within the `gdb` client on your laptop, you have to connect to the running `GDB` server on the debug Pico probe:

```txt
target remote :1337
monitor reset halt # optionally resets to the first instruction
tui enable
```

Alternatively, you can tell `gdb` to execute these commands automatically by writing them in a `.gdbinit` file in the root of this repository.

### Common GDB Commands

Breakpoints can be set in the `gdb` client by using the `break` command followed by a line number or function name:

```txt
break [FUNCTION_NAME] # Set a breakpoint at a specific function
break [LINE_NUMBER] # Set a breakpoint at a specific line number
break [FILE_NAME]:[LINE_NUMBER] # Set a breakpoint at a specific line in a file
```

You can also write hardware breakpoints directly in your code with `cortex_m::asm::bkpt()`.

To progress through the execution of your debugged program, you can use:

```txt
continue # Continue execution until the next breakpoint is hit
next # Step to the next line of code
```

For introspection of variables:

```txt
print [VAR_NAME]
```
9 changes: 9 additions & 0 deletions content/blog/bare-metal/_index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
+++
title = "Embedded"
description = "Low-level programming with microcontrollers"
insert_anchor_links = "heading"
sort_by ="weight"
template = "section.html"
+++

Some notes from a [workshop](https://github.com/sysghent/plant-pot) on bare metal programming with Rust and the Raspberry Pi Pico 2 W.
Loading