Skip to content

Commit

Permalink
Merge pull request #835 from 9names/spibus_example
Browse files Browse the repository at this point in the history
Add SPI example using embedded_hal_bus
  • Loading branch information
thejpster authored Sep 1, 2024
2 parents 960afaf + ecf3fb3 commit 512b102
Show file tree
Hide file tree
Showing 3 changed files with 312 additions and 1 deletion.
4 changes: 4 additions & 0 deletions rp2040-hal-examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ dht-sensor = "0.2.1"
embedded-alloc = "0.5.1"
embedded-hal = "1.0.0"
embedded-hal-async = "1.0.0"
embedded-hal-bus = {version = "0.2.0", features = ["defmt-03"]}
embedded-io = "0.6.1"
embedded_hal_0_2 = {package = "embedded-hal", version = "0.2.5", features = ["unproven"]}
fugit = "0.3.6"
Expand All @@ -33,5 +34,8 @@ panic-halt = "0.2.0"
panic-probe = {version = "0.3.1", features = ["print-defmt"]}
pio = "0.2.0"
pio-proc = "0.2.0"
# We aren't using this, but embedded-hal-bus 0.2 unconditionally requires atomics.
# Should be fixed in e-h-b 0.3 via https://github.com/rust-embedded/embedded-hal/pull/607
portable-atomic = {version = "1.7.0", features = ["critical-section"]}
rp2040-boot2 = "0.3.0"
rp2040-hal = {path = "../rp2040-hal", version = "0.10.0", features = ["binary-info", "critical-section-impl", "rt", "defmt"]}
2 changes: 1 addition & 1 deletion rp2040-hal-examples/src/bin/spi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ fn main() -> ! {
&mut pac.RESETS,
);

// These are implicitly used by the spi driver if they are in the correct mode
// 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>();
Expand Down
307 changes: 307 additions & 0 deletions rp2040-hal-examples/src/bin/spi_eh_bus.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
//! # 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`].
//!
//! [`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]

// Ensure we halt the program on panic (if we don't mention this crate it won't
// be linked)
use panic_halt as _;

// Alias for our HAL crate
use rp2040_hal as hal;

// 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;
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.
/// Note: This boot block is not necessary when using a rp-hal based BSP
/// as the BSPs already perform this step.
#[link_section = ".boot2"]
#[used]
pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_GENERIC_03H;

/// External high-speed crystal on the Raspberry Pi Pico board is 12 MHz. Adjust
/// if your board has a different frequency
const XTAL_FREQ_HZ: u32 = 12_000_000u32;

/// 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)]
pub enum MyError<E> {
/// We got an error from the *SPI Device*
Spi(E),
// Add other errors for your driver here.
}

/// 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
SD: SpiDevice,
{
/// 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 }
}

/// 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(())
}

/// 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(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 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 p = hal::pac::Peripherals::take().unwrap();

// Set up the watchdog driver - needed by the clock setup code
let mut watchdog = hal::Watchdog::new(p.WATCHDOG);

// Configure the clocks
let clocks = hal::clocks::init_clocks_and_plls(
XTAL_FREQ_HZ,
p.XOSC,
p.CLOCKS,
p.PLL_SYS,
p.PLL_USB,
&mut p.RESETS,
&mut watchdog,
)
.unwrap();

// The single-cycle I/O block controls our GPIO pins
let sio = hal::Sio::new(p.SIO);

// Set the pins to their default state
let pins = hal::gpio::Pins::new(p.IO_BANK0, p.PADS_BANK0, sio.gpio_bank0, &mut p.RESETS);

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

let timer = hal::Timer::new(p.TIMER, &mut p.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(),
);

// 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(p.UART0, uart_pins, &mut p.RESETS);

// 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();

// ========================================================================
// Set up our *SPI Bus* and one *SPI Device* on the bus, and then build our
// *Device Driver*:
// ========================================================================

// 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>();

// 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(p.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 p.RESETS,
clocks.peripheral_clock.freq(),
16.MHz(),
embedded_hal::spi::MODE_0,
);

// 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(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: {}", value);
}
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();
}
}

// End of file

0 comments on commit 512b102

Please sign in to comment.