Self-Aligning Dish in Rust: Blink
Ian Ndeda
Posted on November 21, 2024
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
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"] }
Bring it into scope in our application by adding the line below under our imports in main.rs
.
use rp2040_pac as pac;
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
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.
Now we'll configure the XOSC. We first expose it's registers by taking ownership of it.
let xosc = dp.XOSC;
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.
With a 12MHz XOSC and a desired 1ms wait time, our calculated start-up delay time will be:
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
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
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;
Before using a peripheral it must first be deasserted.
resets.reset().modify(|_, w| w
.pll_sys().clear_bit()// deassert pll sys
);
PLL programming
The PLL runs from the reference clock, in this case a 12 MHz crystal oscillator, and multiplies it to attain higher frequencies.
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:
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
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()
);
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()
});
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
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;
The cortex-m
crate has to be added to the project in the Cargo.toml
file.
cortex-m = "0.7.7"
Unmasking the interrupt...
timer.inte().modify(|_, w| w.alarm_0().set_bit());// set alarm0 interrupt
unsafe {
NVIC::unmask(interrupt::TIMER_IRQ_0);// Unmask interrupt
}
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
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());
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
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));
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};
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));
});
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
}
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;
Each interrupt has a given name in the rp2040-pac
under interrupts
. TIMER_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
});
}
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
The onboard LED should blink every one second.
Next we'll configure the Pico's UART
to enable us receive commands remotely via Bluetooth.
Posted on November 21, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.