Self-Aligning Dish in Rust: Setup

ian_ndeda

Ian Ndeda

Posted on November 21, 2024

Self-Aligning Dish in Rust: Setup

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
Enter fullscreen mode Exit fullscreen mode

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 {}
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Copy the following into the file.

pub mod critical_section_rp2040;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Results

We can now build our minimal RP2040 program.

cargo build
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
ian_ndeda
Ian Ndeda

Posted on November 21, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related