Skip to content

Commit

Permalink
Added even more comments to spi_eh_bus.
Browse files Browse the repository at this point in the history
  • Loading branch information
thejpster committed Aug 19, 2024
1 parent 3d89e33 commit da58df6
Showing 1 changed file with 180 additions and 70 deletions.
250 changes: 180 additions & 70 deletions rp2040-hal-examples/src/bin/spi_eh_bus.rs
Original file line number Diff line number Diff line change
@@ -1,38 +1,57 @@
//! # SPI Bus example
//!
//! This application demonstrates how to construct a simple Spi Driver,
//! and configure rp2040-hal's Spi peripheral to access it by utilising
//! `ExclusiveDevice`` from `embedded_hal_bus`
//! This application demonstrates how to construct a simple SPI Driver and
//! configure rp2040-hal's SPI peripheral to access it by utilising
//! [`ExclusiveDevice`] from [`embedded-hal-bus`].
//!
//! [`ExclusiveDevice`]:
//! https://docs.rs/embedded-hal-bus/latest/embedded_hal_bus/spi/struct.ExclusiveDevice.html
//! [`embedded-hal-bus`]: https://crates.io/crates/embedded-hal-bus
//!
//! It may need to be adapted to your particular board layout and/or pin
//! assignment.
//!
//! See the top-level `README.md` file for Copyright and license details.
//!
//! ## Glossary
//!
//! * *SPI Bus* - a shared bus consisting of three shared wires (Clock, COPI and
//! CIPO) and a unique 'Chip Select' wire for each device on the bus. An *SPI
//! Bus* typically only has one *SPI Controller* which is 'driving' the bus
//! (specifically it is driving the clock signal and the *COPI* data line).
//! * *COPI* - One of the data lines on an *SPI Bus*. Stands for *Controller
//! Out, Peripheral In*, but we use the term *SPI Device* instead of *SPI
//! Peripheral*.
//! * *CIPO* - One of the data lines on an *SPI Bus*. Stands for *Controller In,
//! Peripheral Out*, but we use the term *SPI Device* instead of *SPI
//! Peripheral*.
//! * *SPI Controller* - a block of silicon within the RP2040 designed for
//! driving an *SPI Bus*.
//! * *SPI Device* - a device on the *SPI Bus*; has its own unique chip select
//! signal.
//! * *Device Driver* - a Rust type (and/or a value of that type) which
//! represents a particular *SPI Device*, such as an Bosch BMA400
//! accelerometer chip, or an ST7789 LCD controller chip.

#![no_std]
#![no_main]

use embedded_hal::spi::Operation;
use embedded_hal::spi::SpiDevice;
use embedded_hal_bus::spi::ExclusiveDevice;
// Ensure we halt the program on panic (if we don't mention this crate it won't
// be linked)
use panic_halt as _;

use rp2040_hal::gpio::PinState;
use rp2040_hal::uart::{DataBits, StopBits, UartConfig};
// Alias for our HAL crate
use rp2040_hal as hal;

// Some traits we need
// Some types/traits we need
use core::fmt::Write;
use embedded_hal::spi::Operation;
use embedded_hal::spi::SpiDevice;
use embedded_hal_bus::spi::ExclusiveDevice;
use hal::clocks::Clock;
use hal::fugit::RateExtU32;

// A shorter alias for the Peripheral Access Crate, which provides low-level
// register access
use hal::pac;
use hal::gpio::PinState;
use hal::uart::{DataBits, StopBits, UartConfig};

/// The linker will place this boot block at the start of our program image. We
/// need this to help the ROM bootloader get our code up and running.
Expand All @@ -46,62 +65,89 @@ pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_GENERIC_03H;
/// if your board has a different frequency
const XTAL_FREQ_HZ: u32 = 12_000_000u32;

/// Our Spi device driver
/// We need to use a generic here (SPI), because we want our driver to work for any other
/// microcontroller that implements the embedded-hal Spi traits
struct MySpiDriver<SPI> {
spi: SPI,
}

/// When things go wrong, we could return Error(()) but that wouldn't help anyone troubleshoot
/// so we'll create an error type with additional info to return.
/// The ways our device driver might fail.
///
/// When things go wrong, we could return `Err(())`` but that wouldn't help
/// anyone troubleshoot so we'll create an error type with additional info to
/// return.
#[derive(Copy, Clone, Debug)]
enum MyError<SPI> {
Spi(SPI),
pub enum MyError<E> {
/// We got an error from the *SPI Device*
Spi(E),
// Add other errors for your driver here.
}

/// Implementation of the business logic for the remote SPI IC
impl<SPI> MySpiDriver<SPI>
/// Our *Device Driver* type.
///
/// This is an example of a device driver that manages some kind of *SPI
/// Device*.
///
/// It is not a driver for any real device - it's here as an example of how to
/// write such a driver. You would typically get real drivers from a library
/// crate but we've pasted it into the example so you can see it.
///
/// We need to use a generic here (`SD`), because we want our example
/// driver to work for any other microcontroller that implements the
/// embedded-hal SPI traits - specifically the `embedded_hal::spi::SpiDevice`
/// trait that represents access to a unique device on the bus.
pub struct MySpiDeviceDriver<SD> {
spi_dev: SD,
}

impl<SD> MySpiDeviceDriver<SD>
where
SPI: SpiDevice,
SD: SpiDevice,
{
/// Construct a new instance of SPI device driver
pub fn new(spi: SPI) -> Self {
Self { spi }
/// Construct a new instance of our *Device Driver*.
///
/// Takes ownership of a value representing a unique device on the *SPI
/// Bus*.
pub fn new(spi_dev: SD) -> Self {
Self { spi_dev }
}

/// Our hypothetical SPI device has a register at 0x20, that accepts a u8 value
pub fn set_value(&mut self, value: u8) -> Result<(), MyError<SPI::Error>> {
self.spi
/// Write to our hypothetical device.
///
/// We imagine our device has a register at `0x20`, that accepts a `u8`
/// value.
pub fn set_value(&mut self, value: u8) -> Result<(), MyError<SD::Error>> {
self.spi_dev
.transaction(&mut [Operation::Write(&[0x20, value])])
.map_err(MyError::Spi)?;

Ok(())
}

/// Our hypothetical Spi device has a register at 0x90, that we can read a 2 byte value from
pub fn get_value(&mut self) -> Result<[u8; 2], MyError<SPI::Error>> {
let mut buf = [0; 2];
self.spi
/// Read from our hypothetical device.
///
/// We imagine our device has a register at `0x90`, that we can read a 2
/// byte integer value from.
pub fn get_value(&mut self) -> Result<u16, MyError<SD::Error>> {
let mut buf = [0u8; 2];
self.spi_dev
.transaction(&mut [Operation::Write(&[0x90]), Operation::Read(&mut buf)])
.map_err(MyError::Spi)?;

Ok(buf)
Ok(u16::from_le_bytes(buf))
}
}

/// Entry point to our bare-metal application.
///
/// The `#[rp2040_hal::entry]` macro ensures the Cortex-M start-up code calls this function
/// as soon as all global variables and the spinlock are initialised.
/// The `#[rp2040_hal::entry]` macro ensures the Cortex-M start-up code calls
/// this function as soon as all global variables and the spinlock are
/// initialised.
///
/// The function configures the RP2040 peripherals, then performs some example
/// SPI transactions, then goes to sleep.
#[rp2040_hal::entry]
fn main() -> ! {
// ========================================================================
// Some things that pretty much every rp2040-hal program will do
// ========================================================================

// Grab our singleton objects
let mut pac = pac::Peripherals::take().unwrap();
let mut pac = hal::pac::Peripherals::take().unwrap();

// Set up the watchdog driver - needed by the clock setup code
let mut watchdog = hal::Watchdog::new(pac.WATCHDOG);
Expand Down Expand Up @@ -129,71 +175,135 @@ fn main() -> ! {
&mut pac.RESETS,
);

// ========================================================================
// Construct a timer object, which we will need later.
// ========================================================================

let timer = rp2040_hal::Timer::new(pac.TIMER, &mut pac.RESETS, &clocks);

// ========================================================================
// Set up a UART so we can print out the values we read using our *Device
// Driver*:
// ========================================================================

// How we want our UART to be configured
let uart_config = UartConfig::new(115200.Hz(), DataBits::Eight, None, StopBits::One);

// The pins we want to use for our UART.
let uart_pins = (
// UART TX (characters sent from RP2040) on pin 1 (GPIO0)
pins.gpio0.into_function(),
// UART RX (characters received by RP2040) on pin 2 (GPIO1)
pins.gpio1.into_function(),
);

// Set up a uart so we can print out the values from our SPI peripheral
let mut uart = hal::uart::UartPeripheral::new(pac.UART0, uart_pins, &mut pac.RESETS)
.enable(
UartConfig::new(115200.Hz(), DataBits::Eight, None, StopBits::One),
clocks.peripheral_clock.freq(),
)
.unwrap();
// Create new, uninitialized UART driver with one of the two UART objects
// from our PAC, and our UART pins. We need temporary access to the RESETS
// object to reset the UART.
let uart = hal::uart::UartPeripheral::new(pac.UART0, uart_pins, &mut pac.RESETS);

// Set up our SPI pins so they can be used by the SPI driver
let spi_mosi = pins.gpio7.into_function::<hal::gpio::FunctionSpi>();
let spi_miso = pins.gpio4.into_function::<hal::gpio::FunctionSpi>();
let spi_sclk = pins.gpio6.into_function::<hal::gpio::FunctionSpi>();
// Swap the uninitialised UART driver for an initialised one, passing in the
// desired configuration. We need to know the internal UART clock frequency
// to set up the baud rate dividers correctly.
let mut uart = uart
.enable(uart_config, clocks.peripheral_clock.freq())
.unwrap();

// Chip select is handled by SpiDevice, not SpiBus. It logically belongs with the specific SPI device we're talking to
let spi_cs = pins.gpio8.into_push_pull_output_in_state(PinState::High);
// ========================================================================
// Set up our *SPI Bus* and one *SPI Device* on the bus, and then build our
// *Device Driver*:
// ========================================================================

// Create new, uninitialized SPI bus with one of the 2 SPI peripherals from our PAC, and our SPI data/clock pins
let spi = hal::spi::Spi::<_, _, _, 8>::new(pac.SPI0, (spi_mosi, spi_miso, spi_sclk));
// Set up our SPI pins so they can be used by the *SPI Bus* driver.
let spi_copi = pins.gpio7.into_function::<hal::gpio::FunctionSpi>();
let spi_cipo = pins.gpio4.into_function::<hal::gpio::FunctionSpi>();
let spi_sclk = pins.gpio6.into_function::<hal::gpio::FunctionSpi>();

// Exchange the uninitialised Spi bus for an initialised one, passing in the extra bus parameters required
let spi = spi.init(
// Create new, uninitialized *SPI Bus* driver with one of the two *SPI*
// objects from our PAC, plus our data/clock pins for the *SPI Bus*.
//
// We've stubbed out most of the type parameters because the compiler can
// work them out for us. The only one we need to specify is the size of a
// word sent over the *SPI Bus* to our device - and we picked 8 bits (a
// byte).
let spi_bus = hal::spi::Spi::<_, _, _, 8>::new(pac.SPI0, (spi_copi, spi_cipo, spi_sclk));

// Exchange the uninitialised *SPI Bus* driver for an initialised one, by
// passing in the extra bus parameters required.
//
// We need temporary access to the RESETS object to reset the SPI
// peripheral, and we need to know the internal clock speed in order to set
// up the clock dividers correctly.
let spi_bus = spi_bus.init(
&mut pac.RESETS,
clocks.peripheral_clock.freq(),
16.MHz(),
embedded_hal::spi::MODE_0,
);

// We are the only task talking to this SPI peripheral, so we can use ExclusiveDevice here.
// If we had multiple tasks accessing this, you would need to use a different interface to
// ensure that we have exclusive access to this bus (via AtomicDevice or CriticalSectionDevice, for example)
//
// We can safely unwrap here, because the only possible failure is CS assertion failure, but our CS pin is infallible
let excl_dev = ExclusiveDevice::new(spi, spi_cs, timer).unwrap();

// Now that we've constructed a SpiDevice for our driver to use, we can finally construct our Spi driver
let mut driver = MySpiDriver::new(excl_dev);
// The Chip Select pin. Every *SPI Device* has a unique chip select signal,
// and when this pin goes low, it uniquely selects our desired
// (hypothetical) *SPI Device* on the *SPI Bus*.
let spi_cs = pins.gpio8.into_push_pull_output_in_state(PinState::High);

// Create an object which represents our (hypothetical) *SPI Device* on the
// *SPI Bus*.
//
// We are the only task talking to this *SPI Bus*, so we can use
// `ExclusiveDevice` here. If we had multiple tasks accessing the same bus
// (e.g. you had both an accelerometer and a pressure sensor on the same
// bus), we would need to use a different interface to ensure that we have
// exclusive access to this bus (via `AtomicDevice` or
// `CriticalSectionDevice`, for example).
//
// See https://docs.rs/embedded-hal-bus/0.2.0/embedded_hal_bus/spi for a
// list of options.
//
// We can safely unwrap here, because the only possible failure is CS
// assertion failure and our CS pin is infallible.
//
// Note that it also takes ownership of our timer object so that it has a
// way of measuring elapsed time. This is required so it can handle `Delay`
// transactions that can put small pauses inbetween say, a `Write`
// transaction and a `Read` transaction (which some SPI devices require
// because they're a bit sluggish and take time to process things).
let excl_spi_dev = ExclusiveDevice::new(spi_bus, spi_cs, timer).unwrap();

// Now that we've constructed a value of type `ExclusiveDevice` (which
// implements the embedded-hal `SpiDevice` traits) we can finally construct
// our device driver.
let mut driver = MySpiDeviceDriver::new(excl_spi_dev);

// ========================================================================
// Now let's use our device driver.
// ========================================================================

// Let's write to the device
match driver.set_value(10) {
Ok(_) => {
// Do something on success
_ = writeln!(uart, "Wrote to device OK!");
}
Err(_) => {
Err(e) => {
// Do something on failure
_ = writeln!(uart, "Device write error: {:?}", e);
}
}

// Let's read from the device
match driver.get_value() {
Ok(value) => {
// Do something on success
writeln!(uart, "Read value was {} {}\n", value[0], value[1]).unwrap();
_ = writeln!(uart, "Read value: {}", value);
}
Err(_) => {
Err(e) => {
// Do something on failure
_ = writeln!(uart, "Device write error: {:?}", e);
}
}

// We're done, so just sleep in an infinite loop. Your program should
// probably do something more useful.
loop {
cortex_m::asm::wfi();
}
Expand Down

0 comments on commit da58df6

Please sign in to comment.