Self-Aligning Dish in Rust: Blink

ian_ndeda

Ian Ndeda

Posted on November 21, 2024

Self-Aligning Dish in Rust: Blink

In the last post of this series, we looked at how to write a minimal Rust program for the Raspberry Pi Pico. In this post, we'll expand on that code to implement the indication LED.

Table of Contents

Flowchart

indication-led-flowchart

We'll need a timer with an interrupt that is set to go off every 1 second. When the alarm goes off, the LED will be toggled between ON and OFF states.

Requirements

  • Raspberry Pico board
  • USB Cable type 2.0

NB The Pico has an onboard LED connected to GP25 which we will be using.

Implementation

Import the rp2040-pac crate. This crate allows us to access the various peripherals of the Pico. It is generated from RP2040's SVD. Add it to the dependencies in the Cargo.toml file.

rp2040-pac = { version = "0.6.0", features = ["rt"] }
Enter fullscreen mode Exit fullscreen mode

Bring it into scope in our application by adding the line below under our imports in main.rs.

use rp2040_pac as pac;
Enter fullscreen mode Exit fullscreen mode

In our main code, we'll first have to expose all the device's peripherals by taking ownership of them.

let dp = unsafe { pac::Peripherals::steal() };// Take the device peripherals
let _cp = pac::CorePeripherals::take().unwrap();// Take the core peripherals
Enter fullscreen mode Exit fullscreen mode

Clock Configuration

Next we have to configure the clocks.

The RP2040 has an internal ring oscillator, the ROSC, which provides clocking on initial bootup before configuration of other clock sources is possible. It also has the option of being clocked from an external oscillator, the XOSC. This can be used to drive the PLLs, phased locked loops, to enable higher frequencies of operation (up to 133 MHz for the Pico board). The regular Pico board is shipped with a 12 MHz XOSC.

clocks-overview

Now we'll configure the XOSC. We first expose it's registers by taking ownership of it.

let xosc = dp.XOSC;
Enter fullscreen mode Exit fullscreen mode

We then configure the crystal oscillator by setting the frequency range of the XOSC and setting the start-up delay time.

Instructions for setting the startup delay can be found in the reference manual. The start-up delay value is calculated using the below formula.

start up delay=(fCrystal×tStable)÷256 start\ up\ delay = (fCrystal \times tStable) \div 256

With a 12MHz XOSC and a desired 1ms wait time, our calculated start-up delay time will be: (12×106)×(1×103)256=47{(12 \times 10^6) \times (1 \times 10^-3) \over 256} = 47

xosc.ctrl().modify(|_, w| w.freq_range()._1_15mhz());// Set freq. range
xosc.startup().write(|w| unsafe { w.delay().bits(47) });// Set the startup delay
Enter fullscreen mode Exit fullscreen mode

Enable the XOSC and wait for it to stabilize.

xosc.ctrl().modify(|_, w| w.enable().enable());// Enable the xosc
while xosc().status.read().stable().bit_is_clear() {}// Await stabilization
Enter fullscreen mode Exit fullscreen mode

With the XOSC running, we now need to configure the clocks and PLLs.

First create handles for the clocks and PLLS. We'll also need one for the resets.

let clocks = dp.CLOCKS;
let pll_sys = dp.PLL_SYS;
let resets = dp.RESETS;
Enter fullscreen mode Exit fullscreen mode

Before using a peripheral it must first be deasserted.

resets.reset().modify(|_, w| w
              .pll_sys().clear_bit()// deassert pll sys
              );
Enter fullscreen mode Exit fullscreen mode

PLL programming

The PLL runs from the reference clock, in this case a 12 MHz crystal oscillator, and multiplies it to attain higher frequencies.

pll

There are a number of parameters we need to set the PLL: feedback divider, reference divider, and two postdividers. Instructions on how to get these values are in the manual. The formula is as below:

Output frequency=Crystal FrequencyReference Divider×Feedback DividerPostdivider 1×Postdivider 2 Output\ frequency = {Crystal\ Frequency \over Reference\ Divider} \times {Feedback\ Divider \over Postdivider\ 1 \times Postdivider\ 2}

For an output frequency of 125 MHz we'll select the following values:

  • feed back divider = 125
  • reference divider = 1
  • postdivider1 = 6
  • postdivider1 = 1

Program the PLL as below.

pll_sys.pwr().reset();// Turn off PLL in case it is already running
pll_sys.fbdiv_int().reset();
pll_sys.cs().modify(|_, w| unsafe { w.refdiv().bits(1) });// Set refdiv as 1
pll_sys.fbdiv_int().write(|w| unsafe { w.bits(125) });// Set fbdiv_int as 125
pll_sys.pwr().modify(|_, w| w
                   .pd().clear_bit()// Turn on PLL
                   .vcopd().clear_bit()// Turn on VCO
                   );
while pll_sys.cs().read().lock().bit_is_clear() {}// Await locking of pll
pll_sys.prim().modify(|_, w| unsafe {
    w.postdiv1().bits(6)// Set up postdivider 1
        .postdiv2().bits(2)// Set up postdivider 2
});
pll_sys.pwr().modify(|_, w| w.postdivpd().clear_bit());// Turn on postdividers
Enter fullscreen mode Exit fullscreen mode

To finish clock setup, we need to select our reference clock as XOSC divided by 1, i.e., 12 MHz, set the PLL as our clock source, and then set our system clock as the peripheral clock.

// Select ref clock source as XOSC divided by 1
clocks.clk_ref_ctrl().modify(|_, w| w.src().xosc_clksrc());
clocks.clk_ref_div().modify(|_, w| unsafe { w.int().bits(1) });

// Set pll sys as clk sys 
clocks.clk_sys_ctrl().modify(|_, w| w.src().clksrc_clk_sys_aux());
clocks.clk_sys_div().modify(|_, w| unsafe { w.int().bits(1) });

// Set clk sys as peripheral clock
// Used as reference clock for Peripherals
clocks.clk_peri_ctrl().modify(|_, w| w
                            .auxsrc().clk_sys()
                            .enable().set_bit()
                            );
Enter fullscreen mode Exit fullscreen mode

Next we have to configure and enable the clock. Before that the Watchdog has to be configured; the timer depends on the clk_tick reference from the watchdog.

// Watchdog: to provide the clk_tick required by the timer
let watchdog = dp.WATCHDOG;
watchdog.tick().modify(|_, w| unsafe{ w
    .cycles().bits(12)// For an effective frequency of 1MHz
    .enable().set_bit()
});
Enter fullscreen mode Exit fullscreen mode

Note that the Watchdog runs from clk_ref clock which is in turn drawn from the crystal oscillator which in our case runs at 12 MHz.

Timer

We can now proceed to the timer.

// Timer set up
let timer = dp.TIMER;
resets.reset().modify(|_, w| w.timer().clear_bit());// Deassert timer
Enter fullscreen mode Exit fullscreen mode

We have to enable the interrupt of alarm0 of the timer and unmask it. NVIC has to be brought into scope first by importing cortex_m::peripheral::NVIC.

 use cortex_m::peripheral::NVIC;
Enter fullscreen mode Exit fullscreen mode

The cortex-m crate has to be added to the project in the Cargo.toml file.

cortex-m = "0.7.7"
Enter fullscreen mode Exit fullscreen mode

Unmasking the interrupt...

timer.inte().modify(|_, w| w.alarm_0().set_bit());// set alarm0 interrupt
unsafe {
    NVIC::unmask(interrupt::TIMER_IRQ_0);// Unmask interrupt
}
Enter fullscreen mode Exit fullscreen mode

To arm the alarm, we need to set the time after which the alarm, and therefore the interrupt, should go off. We do this by adding the desired time to the current reading in the TIMERAWL register of the timer. This automatically sets the alarm.

timer.alarm0().modify(|_, w| unsafe{ w.bits(timer.timerawl().read().bits() + 1_000_000) });// set 1 sec after now alarm
Enter fullscreen mode Exit fullscreen mode

alarm0 is now armed and will go off after every 1 second. We'll handle it further in the interrupt service routine just after it goes off.

LED Pin

Having configured the clock, the GPIO block, to which the onboard LED is connected, has to be enabled. To control a GPIO pin, we need three peripherals: the SIO, Single-Cycle Input/Output, the IO_BANK, and the PADS_BANK. The latter are used to set the function of the pin and electrical properties of the pin, respectively.

let sio = dp.SIO;
let io_bank0 = dp.IO_BANK0;
let pads_bank0 = dp.PADS_BANK0;
// reset the peripherals
resets.reset().modify(|_, w| w
                    .io_bank0().clear_bit()
                    .pads_bank0().clear_bit());
Enter fullscreen mode Exit fullscreen mode

We set the pin as a pushpull pin with function as a general IO pin (and will hence be controlled by the SIO peripheral).

pads_bank0.gpio(25).modify(|_, w| w
                           .pue().set_bit()// pullup enable
                           .pde().set_bit()// pulldown enable
                           .od().clear_bit()// output enable
                           .ie().set_bit()// input enable
                           );

io_bank0.gpio(25).gpio_ctrl().modify(|_, w| w.funcsel().sio());// set function as sio

sio.gpio_oe().modify(|r, w| unsafe { w.bits(r.gpio_oe().bits() | 1 << 25)});// Output enable for pin 25
Enter fullscreen mode Exit fullscreen mode

Interrupt

The SIO and TIMER peripherals have to be taken into global scope since we'll be using them in the interrupt routine, which is apart from the main program. To do this, we'll have to introduce global variables that will hold them. Just below our import, we'll add the following lines.

// Global variable for peripherals
static TIMER: Mutex<RefCell<Option<TIMER>>> = Mutex::new(RefCell::new(None));
static SIO: Mutex<RefCell<Option<SIO>>> = Mutex::new(RefCell::new(None));
Enter fullscreen mode Exit fullscreen mode

RefCell, Mutex, TIMER and SIO have to be imported into our application.

use cortex_m::interrupt::Mutex;
use core::cell::RefCell;
use rp2040_pac::{SIO, TIMER};
Enter fullscreen mode Exit fullscreen mode

In our main program, after creating handles for the peripherals, we move them into global scope as below.

// Move peripherals into global scope
cortex_m::interrupt::free(|cs| {
    SIO.borrow(cs).replace(Some(sio));
    TIMER.borrow(cs).replace(Some(timer));
});
Enter fullscreen mode Exit fullscreen mode

Since this program will be largely interrupt driven the super loop will have no task but wait for the interrupt.

loop {
    cortex_m::asm::wfi();// wait for interrupt
}
Enter fullscreen mode Exit fullscreen mode

We finally have to write the interrupt handler for the timer: the code that will run every time the interrupt fires. 

We introduce it with the #[interrupt] attribute, which we first have to import.

use rp2040_pac::interrupt;
Enter fullscreen mode Exit fullscreen mode

Each interrupt has a given name in the rp2040-pac under interruptsTIMER_IRQ_0 in this case.

We'll enter a critical section while in the interrupt routine to prevent data races.

#[interrupt]
fn TIMER_IRQ_0() {
    // Enter critical section
    cortex_m::interrupt::free(|cs| {
    // Borrow the peripherals under critical section. 
        let mut timer = TIMER.borrow(cs).borrow_mut();
        let mut sio = SIO.borrow(cs).borrow_mut();

        timer.as_mut().unwrap().intr().modify(|_, w| w.alarm_0().clear_bit_by_one());// first clear interrupt
        sio.as_mut().unwrap().gpio_out().modify(|r, w| unsafe { w.bits(r.gpio_out().bits() ^ 1 << 25)});// toggle bit 25    
        // set period for next alarm
        let (timer_alarm0, _overflow) = (timer.as_mut().unwrap().timerawl().read().bits())
            .overflowing_add(1_000_000); 

        timer.as_mut().unwrap().alarm0().write(|w|  unsafe { w
            .bits(timer_alarm0) });// set sec's after now alarm
    });
}
Enter fullscreen mode Exit fullscreen mode

Results

Our new application code after the foregoing additions can be found on this github page.

All the changes from the minimum program from the last part can be viewed in this github commit.

We can now flash our application code into the Pico by simply running the code.

cargo run
Enter fullscreen mode Exit fullscreen mode

The onboard LED should blink every one second.

blink

Next we'll configure the Pico's UART to enable us receive commands remotely via Bluetooth.

💖 💪 🙅 🚩
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