diff --git a/rp2040-hal-examples/Cargo.toml b/rp2040-hal-examples/Cargo.toml index 852526368..7529eb2df 100644 --- a/rp2040-hal-examples/Cargo.toml +++ b/rp2040-hal-examples/Cargo.toml @@ -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" @@ -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"]} diff --git a/rp2040-hal-examples/src/bin/spi.rs b/rp2040-hal-examples/src/bin/spi.rs index 85b0b7445..a7a76ff4b 100644 --- a/rp2040-hal-examples/src/bin/spi.rs +++ b/rp2040-hal-examples/src/bin/spi.rs @@ -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::(); let spi_miso = pins.gpio4.into_function::(); let spi_sclk = pins.gpio6.into_function::(); diff --git a/rp2040-hal-examples/src/bin/spi_eh_bus.rs b/rp2040-hal-examples/src/bin/spi_eh_bus.rs new file mode 100644 index 000000000..456de1764 --- /dev/null +++ b/rp2040-hal-examples/src/bin/spi_eh_bus.rs @@ -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 { + /// 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 { + spi_dev: SD, +} + +impl MySpiDeviceDriver +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> { + 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> { + 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::(); + let spi_cipo = pins.gpio4.into_function::(); + let spi_sclk = pins.gpio6.into_function::(); + + // 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