Self-Aligning Dish in Rust: Setup
Ian Ndeda
Posted on November 21, 2024
In this section, we will lay the software setup for the project.
Table of Contents
Requirements
- Raspberry Pi Pico board
- USB Cable type 2.0
Implementation
We begin by creating a new project in our machines.
cargo new self-aligning-sat-dish
Minimum Program
Navigate to src/main.rs
and replace the contents of the main.rs
file with the following:
#![no_std]
#![no_main]
use panic_halt as _;
use cortex_m_rt::entry;
#[link_section = ".boot2"]
#[used]
pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_W25Q080;
#[entry]
fn main() -> ! {
loop {}
}
The above code is the minimum embedded program for the RP2040 microcontroller.
The #![no_std]
part is an attribute that tells the compiler that we'll not be using the bloated Rust std
library since embedded devices are usually resource constrained. The core
library will be used instead.
#![no_main]
says the entry point into the program will be directly after the #[entry]
attribute and not the std
library's main function.
We'll need to import a number of crates to build this code: panic_halt
defines actions to be performed in case of a panic (in this case the program should halt), cortex_m_rt
provides the #[entry]
attribute, and rp2040_boot2
is a second-stage bootloader for the RP2040 microcontroller.
These crates are added as dependencies to our project via the Cargo.toml
file, as shown below:
panic-halt = "0.2.0"
cortex-m-rt = "0.7.3"
rp2040-boot2 = "0.3.0"
Linker Script and Configuration
Next we'll add the linker script which gives the compiler the memory layout of the microcontroller we'll be using. This information is important for the cross-compilation of our code.
Create a memory.x
file in our project (at the same level as src
) with the following content:
MEMORY {
BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100
FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100
RAM : ORIGIN = 0x20000000, LENGTH = 256K
}
EXTERN(BOOT2_FIRMWARE)
SECTIONS {
/* ### Boot loader */
.boot2 ORIGIN(BOOT2) :
{
KEEP(*(.boot2));
} > BOOT2
} INSERT BEFORE .text
Create a file .cargo
and in it add the toml
file config.toml
. This provides further configuration for our particular target device.
[target.thumbv6m-none-eabi]
runner = "elf2uf2-rs -d"
rustflags = [
"-C", "link-arg=-Tlink.x",
]
[build]
target = "thumbv6m-none-eabi"
The target will change depending on the processor architecture of the device one's application is designed to run on. The RP2040's architecture is thumbv6m-none-eabi
.
NB You will need to download and set up the
elf2uf2-rs
on your local machine.
cargo install elf2uf2-rs --locked
Now that elf2uf2-rs
has been set as the runner, every time we run our program with cargo run
, elf2uf2-rs -d
will be executed.
Critical Section
Another thing we'll need to add is the critical section implementation for the RP2040. All microcontrollers need to implement the critical-section
crate.
Most HALs, such as the rp2040-hal
, have it already baked in. In our case, however, we'll need to manually implement it since the rp2040-pac
crate has no critical section implementation which is crucial for ensuring safe access to application data without data races.
In the /src
folder of your project, make another directory with the name drivers
.
mkdir drivers
Copy the below code in a file named critical_section_rp2040.rs
use rp2040_pac as pac;
use core::sync::atomic::{AtomicU8, Ordering};
struct RpSpinlockCs;
critical_section::custom_impl!(RpSpinlockCs);
/// Marker value to indicate no-one has the lock.
///
/// Initialising `LOCK_OWNER` to 0 means cheaper static initialisation so it's the best choice
const LOCK_UNOWNED: u8 = 0;
/// Indicates which core owns the lock so that we can call critical_section recursively.
///
/// 0 = no one has the lock, 1 = core0 has the lock, 2 = core1 has the lock
static LOCK_OWNER: AtomicU8 = AtomicU8::new(LOCK_UNOWNED);
/// Marker value to indicate that we already owned the lock when we started the `critical_section`.
///
/// Since we can't take the spinlock when we already have it, we need some other way to keep track of `critical_section` ownership.
/// `critical_section` provides a token for communicating between `acquire` and `release` so we use that.
/// If we're the outermost call to `critical_section` we use the values 0 and 1 to indicate we should release the spinlock and set the interrupts back to disabled and enabled, respectively.
/// The value 2 indicates that we aren't the outermost call, and should not release the spinlock or re-enable interrupts in `release`
const LOCK_ALREADY_OWNED: u8 = 2;
unsafe impl critical_section::Impl for RpSpinlockCs {
unsafe fn acquire() -> u8 {
RpSpinlockCs::acquire()
}
unsafe fn release(token: u8) {
RpSpinlockCs::release(token);
}
}
impl RpSpinlockCs {
unsafe fn acquire() -> u8 {
// Store the initial interrupt state and current core id in stack variables
let interrupts_active = cortex_m::register::primask::read().is_active();
// We reserved 0 as our `LOCK_UNOWNED` value, so add 1 to core_id so we get 1 for core0, 2 for core1.
// let core = crate::Sio::core() as u8 + 1_u8;
let core = (*pac::SIO::ptr()).cpuid.read().bits() as u8 + 1_u8;
// Do we already own the spinlock?
if LOCK_OWNER.load(Ordering::Acquire) == core {
// We already own the lock, so we must have called acquire within a critical_section.
// Return the magic inner-loop value so that we know not to re-enable interrupts in release()
LOCK_ALREADY_OWNED
} else {
// Spin until we get the lock
loop {
// Need to disable interrupts to ensure that we will not deadlock
// if an interrupt enters critical_section::Impl after we acquire the lock
cortex_m::interrupt::disable();
// Ensure the compiler doesn't re-order accesses and violate safety here
core::sync::atomic::compiler_fence(Ordering::SeqCst);
// Read the spinlock reserved for `critical_section`
let spinlock31 = &(*pac::SIO::ptr()).spinlock[31];
let lock = spinlock31.read().bits();
if lock > 0 {
// We just acquired the lock.
// 1. Forget it, so we don't immediately unlock
#[allow(forgetting_copy_types)]
//core::mem::forget(lock);
// 2. Store which core we are so we can tell if we're called recursively
LOCK_OWNER.store(core, Ordering::Relaxed);
break;
}
// We didn't get the lock, enable interrupts if they were enabled before we started
if interrupts_active {
cortex_m::interrupt::enable();
}
}
// If we broke out of the loop we have just acquired the lock
// As the outermost loop, we want to return the interrupt status to restore later
interrupts_active as _
}
}
unsafe fn release(token: u8) {
// Did we already own the lock at the start of the `critical_section`?
if token != LOCK_ALREADY_OWNED {
// No, it wasn't owned at the start of this `critical_section`, so this core no longer owns it.
// Set `LOCK_OWNER` back to `LOCK_UNOWNED` to ensure the next critical section tries to obtain the spinlock instead
LOCK_OWNER.store(LOCK_UNOWNED, Ordering::Relaxed);
// Ensure the compiler doesn't re-order accesses and violate safety here
core::sync::atomic::compiler_fence(Ordering::SeqCst);
// Release the spinlock to allow others to enter critical_section again
let spinlock31 = &(*pac::SIO::ptr()).spinlock[31];
spinlock31.write_with_zero(|b| b.bits(1));
// Re-enable interrupts if they were enabled when we first called acquire()
// We only do this on the outermost `critical_section` to ensure interrupts stay disabled
// for the whole time that we have the lock
if token != 0 {
cortex_m::interrupt::enable();
}
}
}
}
The code above is a slight edit of the critical implementation from the rp2040-hal
crate.
Create another rust file mod.rs
inside drivers
that will bring critical_section_rp2040
into scope.
vi mod.rs
Copy the following into the file.
pub mod critical_section_rp2040;
Our program tree up to this point should be as shown below.
├── Cargo.toml
├── memory.x
└── src
├── drivers
│ ├── critical_section_rp2040.rs
│ └── mod.rs
└── main.rs
Results
We can now build our minimal RP2040 program.
cargo build
The program should compile and build without issue.
Now flash the program by running the code while the Raspberry Pi Pico is plugged in via USB to the computer.
cargo run
NB ❗ The Pico will need to be in flash mode. This is achieved by plugging the USB into the computer while the button on the Pico is pressed. Otherwise it will simply run the last program flashed into it.
Our minimal program should flash into the Pico successfully.
In the next part of the, series we'll blink the indication LED on the Pico board.
Posted on November 21, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.