From 726f7f49793a3ac1168593b35f9eb7b69f54b9a4 Mon Sep 17 00:00:00 2001 From: Jonah Dahlquist Date: Thu, 23 Apr 2020 06:01:23 -0700 Subject: [PATCH] Add a HAL implementation for SPI master Implement embedded-hal's FullDuplex SPI master trait for the dedicated SPI peripheral built into a lot of AVR chips. --- README.md | 7 +- avr-hal-generic/src/lib.rs | 1 + avr-hal-generic/src/spi.rs | 203 ++++++++++++++++++ .../examples/leonardo-spi-feedback.rs | 50 +++++ boards/arduino-leonardo/src/lib.rs | 1 + boards/arduino-leonardo/src/pins.rs | 14 +- .../arduino-uno/examples/uno-spi-feedback.rs | 55 +++++ boards/arduino-uno/src/lib.rs | 1 + chips/atmega328p-hal/src/lib.rs | 40 ++++ chips/atmega328p-hal/src/spi.rs | 40 ++++ chips/atmega32u4-hal/src/lib.rs | 39 ++++ 11 files changed, 448 insertions(+), 3 deletions(-) create mode 100644 avr-hal-generic/src/spi.rs create mode 100644 boards/arduino-leonardo/examples/leonardo-spi-feedback.rs create mode 100644 boards/arduino-uno/examples/uno-spi-feedback.rs create mode 100644 chips/atmega328p-hal/src/spi.rs diff --git a/README.md b/README.md index 569c772147..855ca3dcbc 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ The following peripherals are supported in `avr-hal-generic`: - [x] A spinning delay implementation - [x] `PORTx` peripherals as digital IO (v2) - [x] A TWI based I2C implementation +- [X] SPI primary-mode implementation ### HAL Status The chip-HAL crates currently support the following peripherals: @@ -75,11 +76,13 @@ The chip-HAL crates currently support the following peripherals: - [x] `PORTB`, `PORTC`, `PORTD` as digital IO (v2) - [x] `USART0` for serial communication - [x] I2C using `TWI` + - [x] SPI * [`atmega32u4-hal`](./chips/atmega32u4-hal) - [x] Spinning Delay - [x] `PORTB`, `PORTC`, `PORTD`, `PORTE`, `PORTF` as digital IO (v2) - [x] `USART1` for serial communication - [x] I2C using `TWI` + - [x] SPI * [`attiny85-hal`](./chips/attiny85-hal) - [x] Spinning Delay - [x] `PORTB` as digital IO (v2) @@ -89,10 +92,10 @@ In `boards/` there are crates for the following hardware. Please note that this * [Arduino Leonardo](./boards/arduino-leonardo) - [Website](https://www.arduino.cc/en/Main/Arduino_BoardLeonardo) - - Support for basic digital IO + - Support for basic digital IO and SPI * [Arduino Uno](./boards/arduino-uno) - [Website](https://store.arduino.cc/usa/arduino-uno-rev3) - - Support for basic digital IO + - Support for basic digital IO and SPI * [Adafruit Trinket (3V3 or 5V)](./boards/trinket) (**not** PRO!) - [Website](https://learn.adafruit.com/introducing-trinket) - Support for basic digital IO diff --git a/avr-hal-generic/src/lib.rs b/avr-hal-generic/src/lib.rs index 80078a0c78..2514fbe112 100644 --- a/avr-hal-generic/src/lib.rs +++ b/avr-hal-generic/src/lib.rs @@ -15,6 +15,7 @@ pub mod delay; pub mod port; pub mod serial; pub mod i2c; +pub mod spi; /// Prelude containing all HAL traits pub mod prelude { diff --git a/avr-hal-generic/src/spi.rs b/avr-hal-generic/src/spi.rs new file mode 100644 index 0000000000..4666e7935f --- /dev/null +++ b/avr-hal-generic/src/spi.rs @@ -0,0 +1,203 @@ +//! SPI Implementation + +pub use embedded_hal::spi; + +/// Oscillator Clock Frequency division options. Controls both SPR and SPI2X register bits. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SerialClockRate { + OscfOver2, + OscfOver4, + OscfOver8, + OscfOver16, + OscfOver32, + OscfOver64, + OscfOver128, +} + +/// Order of data transmission, either MSB first or LSB first +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum DataOrder { + MostSignificantFirst, + LeastSignificantFirst, +} + +/// Settings to pass to Spi. +/// +/// Easiest way to initialize is with +/// `Settings::default()`. Otherwise can be instantiated with alternate +/// settings directly. +#[derive(Clone, PartialEq, Eq)] +pub struct Settings { + pub data_order: DataOrder, + pub clock: SerialClockRate, + pub mode: spi::Mode, +} + +impl Default for Settings { + fn default() -> Self { + Settings { + data_order: DataOrder::MostSignificantFirst, + clock: SerialClockRate::OscfOver4, + mode: spi::Mode { + polarity: spi::Polarity::IdleLow, + phase: spi::Phase::CaptureOnSecondTransition, + }, + } + } +} + +/// Implement traits for a SPI interface +#[macro_export] +macro_rules! impl_spi { + ( + $(#[$spi_attr:meta])* + pub struct $Spi:ident { + peripheral: $SPI:ty, + pins: { + sclk: $sclkmod:ident::$SCLK:ident, + mosi: $mosimod:ident::$MOSI:ident, + miso: $misomod:ident::$MISO:ident, + } + } + ) => { + + use $crate::void::Void; + use $crate::hal::spi; + pub use avr_hal::spi::*; + + type SCLK = $sclkmod::$SCLK<$crate::port::mode::Output>; + type MOSI = $mosimod::$MOSI<$crate::port::mode::Output>; + type MISO = $misomod::$MISO<$crate::port::mode::Input<$crate::port::mode::PullUp>>; + + /// Behavior for a SPI interface. + /// + /// Stores the SPI peripheral for register access. In addition, it takes + /// ownership of the MOSI and MISO pins to ensure they are in the correct mode. + /// Instantiate with the `new` method. + $(#[$spi_attr])* + pub struct $Spi { + peripheral: $SPI, + sclk: SCLK, + mosi: MOSI, + miso: MISO, + settings: Settings, + is_write_in_progress: bool, + } + + /// Implementation-specific behavior of the struct, including setup/tear-down + impl $Spi { + /// Instantiate an SPI with the registers, SCLK/MOSI/MISO pins, and settings + /// + /// The pins are not actually used directly, but they are moved into the struct in + /// order to enforce that they are in the correct mode, and cannot be used by anyone + /// else while SPI is active. + pub fn new(peripheral: $SPI, sclk: SCLK, mosi: MOSI, miso: MISO, settings: Settings) -> $Spi { + let spi = Spi { + peripheral, + sclk, + mosi, + miso, + settings, + is_write_in_progress: false, + }; + spi.setup(); + spi + } + + /// Disable the SPI device and release ownership of the peripheral + /// and pins. Instance can no-longer be used after this is + /// invoked. + pub fn release(self) -> ($SPI, SCLK, MOSI, MISO) { + self.peripheral.spcr.write(|w| { + w.spe().clear_bit() + }); + (self.peripheral, self.sclk, self.mosi, self.miso) + } + + /// Write a byte to the data register, which begins transmission + /// automatically. + fn write(&mut self, byte: u8) { + self.is_write_in_progress = true; + self.peripheral.spdr.write(|w| unsafe { w.bits(byte) }); + } + + /// Check if write flag is set, and return a WouldBlock error if it is not. + fn flush(&mut self) -> $crate::nb::Result<(), $crate::void::Void> { + if self.is_write_in_progress { + if self.peripheral.spsr.read().spif().bit_is_set() { + self.is_write_in_progress = false; + } else { + return Err($crate::nb::Error::WouldBlock); + } + } + Ok(()) + } + + /// Sets up the control/status registers with the right settings for this secondary device + fn setup(&self) { + // set up control register + self.peripheral.spcr.write(|w| { + // enable SPI + w.spe().set_bit(); + // Set to primary mode + w.mstr().set_bit(); + // set up data order control bit + match self.settings.data_order { + DataOrder::MostSignificantFirst => w.dord().clear_bit(), + DataOrder::LeastSignificantFirst => w.dord().set_bit(), + }; + // set up polarity control bit + match self.settings.mode.polarity { + spi::Polarity::IdleHigh => w.cpol().set_bit(), + spi::Polarity::IdleLow => w.cpol().clear_bit(), + }; + // set up phase control bit + match self.settings.mode.phase { + spi::Phase::CaptureOnFirstTransition => w.cpha().clear_bit(), + spi::Phase::CaptureOnSecondTransition => w.cpha().set_bit(), + }; + // set up clock rate control bit + match self.settings.clock { + SerialClockRate::OscfOver2 => w.spr().val_0x00(), + SerialClockRate::OscfOver4 => w.spr().val_0x00(), + SerialClockRate::OscfOver8 => w.spr().val_0x01(), + SerialClockRate::OscfOver16 => w.spr().val_0x01(), + SerialClockRate::OscfOver32 => w.spr().val_0x02(), + SerialClockRate::OscfOver64 => w.spr().val_0x02(), + SerialClockRate::OscfOver128 => w.spr().val_0x03(), + } + }); + // set up 2x clock rate status bit + self.peripheral.spsr.write(|w| match self.settings.clock { + SerialClockRate::OscfOver2 => w.spi2x().set_bit(), + SerialClockRate::OscfOver4 => w.spi2x().clear_bit(), + SerialClockRate::OscfOver8 => w.spi2x().set_bit(), + SerialClockRate::OscfOver16 => w.spi2x().clear_bit(), + SerialClockRate::OscfOver32 => w.spi2x().set_bit(), + SerialClockRate::OscfOver64 => w.spi2x().clear_bit(), + SerialClockRate::OscfOver128 => w.spi2x().set_bit(), + }); + } + } + + /// FullDuplex trait implementation, allowing this struct to be provided to + /// drivers that require it for operation. Only 8-bit word size is supported + /// for now. + impl $crate::hal::spi::FullDuplex for $Spi { + type Error = $crate::void::Void; + + /// Sets up the device for transmission and sends the data + fn send(&mut self, byte: u8) -> $crate::nb::Result<(), Self::Error> { + self.flush()?; + self.write(byte); + Ok(()) + } + + /// Reads and returns the response in the data register + fn read(&mut self) -> $crate::nb::Result { + self.flush()?; + Ok(self.peripheral.spdr.read().bits()) + } + } + }; +} diff --git a/boards/arduino-leonardo/examples/leonardo-spi-feedback.rs b/boards/arduino-leonardo/examples/leonardo-spi-feedback.rs new file mode 100644 index 0000000000..f548d043dd --- /dev/null +++ b/boards/arduino-leonardo/examples/leonardo-spi-feedback.rs @@ -0,0 +1,50 @@ +//! This example demonstrates how to set up a SPI interface and communicate +//! over it. The physical hardware configuation consists of connecting a +//! jumper directly from ICSP pin 10 to ICSP pin 11. +//! +//! Once this program is written to the board, you can use the board's serial +//! connection to see the output. You should see it output the line +//! `data: 15` repeatedly (aka 0b00001111). If the output you see is +//! `data: 255`, you may need to check your jumper. + +#![no_std] +#![no_main] +#![feature(proc_macro_hygiene)] +extern crate panic_halt; +use arduino_leonardo::prelude::*; +use arduino_leonardo::spi::{Settings, Spi}; +use nb::block; +#[no_mangle] +pub extern "C" fn main() -> ! { + let dp = arduino_leonardo::Peripherals::take().unwrap(); + let mut delay = arduino_leonardo::Delay::new(); + let mut pins = arduino_leonardo::Pins::new(dp.PORTB, dp.PORTC, dp.PORTD, dp.PORTE); + + let mut serial = arduino_leonardo::Serial::new( + dp.USART1, + pins.d0, + pins.d1.into_output(&mut pins.ddr), + 57600, + ); + + pins.led_rx.into_output(&mut pins.ddr); // SS must be set to output mode. + + // Create SPI interface. + let mut spi = Spi::new( + dp.SPI, + pins.sck.into_output(&mut pins.ddr), + pins.mosi.into_output(&mut pins.ddr), + pins.miso.into_pull_up_input(&mut pins.ddr), + Settings::default(), + ); + + loop { + // Send a byte + block!(spi.send(0b00001111)).unwrap(); + // Because MISO is connected to MOSI, the read data should be the same + let data = block!(spi.read()).unwrap(); + + ufmt::uwriteln!(&mut serial, "data: {}\r", data).unwrap(); + delay.delay_ms(1000); + } +} diff --git a/boards/arduino-leonardo/src/lib.rs b/boards/arduino-leonardo/src/lib.rs index 8b9d6e84ef..d150822191 100644 --- a/boards/arduino-leonardo/src/lib.rs +++ b/boards/arduino-leonardo/src/lib.rs @@ -7,6 +7,7 @@ mod pins; pub use atmega32u4_hal::atmega32u4; pub use crate::atmega32u4::Peripherals; pub use atmega32u4_hal::prelude; +pub use atmega32u4_hal::spi; pub use crate::pins::*; pub type Delay = hal::delay::Delay; diff --git a/boards/arduino-leonardo/src/pins.rs b/boards/arduino-leonardo/src/pins.rs index 543cabf530..b6b0a4b766 100644 --- a/boards/arduino-leonardo/src/pins.rs +++ b/boards/arduino-leonardo/src/pins.rs @@ -87,11 +87,23 @@ avr_hal_generic::impl_board_pins! { pub d13: portc::pc7::PC7, /// `RX` /// - /// Led for indicating inbound data + /// Led for indicating inbound data. Also the CS pin. pub led_rx: portb::pb0::PB0, /// `TX` /// /// Led for indicating outbound data pub led_tx: portd::pd5::PD5, + /// `SCLK` + /// + /// ICSP SCLK pin + pub sck: portb::pb1::PB1, + /// `MOSI` + /// + /// ICSP MOSI pin + pub mosi: portb::pb2::PB2, + /// `MISO` + /// + /// ICSP MISO pin + pub miso: portb::pb3::PB3, } } diff --git a/boards/arduino-uno/examples/uno-spi-feedback.rs b/boards/arduino-uno/examples/uno-spi-feedback.rs new file mode 100644 index 0000000000..02193b8603 --- /dev/null +++ b/boards/arduino-uno/examples/uno-spi-feedback.rs @@ -0,0 +1,55 @@ +//! This example demonstrates how to set up a SPI interface and communicate +//! over it. The physical hardware configuation consists of connecting a +//! jumper directly from pin `~11` to pin `~12`. +//! +//! Once this program is written to the board, the serial output can be +//! accessed with +//! +//! ``` +//! sudo screen /dev/ttyACM0 57600 +//! ``` +//! +//! You should see it output the line `data: 15` repeatedly (aka 0b00001111). +//! If the output you see is `data: 255`, you may need to check your jumper. + +#![no_std] +#![no_main] +#![feature(proc_macro_hygiene)] +extern crate panic_halt; +use arduino_uno::prelude::*; +use arduino_uno::spi::{Settings, Spi}; +use nb::block; +#[no_mangle] +pub extern "C" fn main() -> ! { + let dp = arduino_uno::Peripherals::take().unwrap(); + let mut delay = arduino_uno::Delay::new(); + let mut pins = arduino_uno::Pins::new(dp.PORTB, dp.PORTC, dp.PORTD); + // set up serial interface for text output + let mut serial = arduino_uno::Serial::new( + dp.USART0, + pins.d0, + pins.d1.into_output(&mut pins.ddr), + 57600, + ); + + pins.d10.into_output(&mut pins.ddr); // SS must be set to output mode. + + // Create SPI interface. + let mut spi = Spi::new( + dp.SPI, + pins.d13.into_output(&mut pins.ddr), + pins.d11.into_output(&mut pins.ddr), + pins.d12.into_pull_up_input(&mut pins.ddr), + Settings::default(), + ); + + loop { + // Send a byte + block!(spi.send(0b00001111)).unwrap(); + // Because MISO is connected to MOSI, the read data should be the same + let data = block!(spi.read()).unwrap(); + + ufmt::uwriteln!(&mut serial, "data: {}\r", data).unwrap(); + delay.delay_ms(1000); + } +} diff --git a/boards/arduino-uno/src/lib.rs b/boards/arduino-uno/src/lib.rs index 2fb05e2a0e..e5b9b6b219 100644 --- a/boards/arduino-uno/src/lib.rs +++ b/boards/arduino-uno/src/lib.rs @@ -7,6 +7,7 @@ mod pins; pub use atmega328p_hal::atmega328p; pub use crate::atmega328p::Peripherals; pub use atmega328p_hal::prelude; +pub use atmega328p_hal::spi; pub use crate::pins::*; pub type Delay = hal::delay::Delay; diff --git a/chips/atmega328p-hal/src/lib.rs b/chips/atmega328p-hal/src/lib.rs index 4f2375f338..85a681cf93 100644 --- a/chips/atmega328p-hal/src/lib.rs +++ b/chips/atmega328p-hal/src/lib.rs @@ -46,6 +46,46 @@ pub mod i2c { } } +pub mod spi { + //! Implementation of the Rust Embedded-HAL SPI FullDuplex trait for AVR. + //! + //! The interface can be instantiated with the `new` method, and used directly + //! or passed into a driver. Example usage: + //! + //! ``` + //! pins.d10.into_output(&mut pins.ddr);// SS must be set to output mode + //! // create SPI interface + //! let mut spi = Spi::new( + //! dp.SPI,// SPI peripheral + //! pins.d13.into_output(&mut pins.ddr),// SCLK + //! pins.d11.into_output(&mut pins.ddr),// MOSI output pin + //! pins.d12.into_pull_up_input(&mut pins.ddr),// MISO input pin + //! Settings::default(), + //! ); + //! + //! // Send a byte + //! let sent = 0b10101010; + //! spi.send(sent).unwrap(); + //! let response = spi.read().unwrap(); + //! ``` + //! In the example above, all of the settings are left at the default. You can + //! also instantiate a Settings object with the other options available. + + use crate::port::portb; + pub use avr_hal::spi::*; + + avr_hal::impl_spi! { + pub struct Spi { + peripheral: crate::atmega328p::SPI, + pins: { + sclk: portb::PB5, + mosi: portb::PB3, + miso: portb::PB4, + } + } + } +} + /// Serial interface using USART pub mod usart { use crate::port::portd; diff --git a/chips/atmega328p-hal/src/spi.rs b/chips/atmega328p-hal/src/spi.rs new file mode 100644 index 0000000000..194915ba06 --- /dev/null +++ b/chips/atmega328p-hal/src/spi.rs @@ -0,0 +1,40 @@ +//! Implementation of the Rust Embedded-HAL SPI FullDuplex trait for AVR. +//! +//! The interface can be instantiated with the `new` method, and used directly +//! or passed into a driver. Example usage: +//! +//! ``` +//! pins.d10.into_output(&mut pins.ddr);// SS must be set to output mode +//! // create SPI interface +//! let mut spi = Spi::new( +//! dp.SPI,// SPI peripheral +//! pins.d13.into_output(&mut pins.ddr),// SCLK +//! pins.d11.into_output(&mut pins.ddr),// MOSI output pin +//! pins.d12.into_pull_up_input(&mut pins.ddr),// MISO input pin +//! Settings::default(), +//! ); +//! +//! // Send a byte +//! let sent = 0b10101010; +//! spi.send(sent).unwrap(); +//! let response = spi.read().unwrap(); +//! ``` +//! In the example above, all of the settings are left at the default. You can +//! also instantiate a Settings object with the other options available. + +extern crate avr_hal_generic as avr_hal; + +pub use avr_hal::spi::*; +use crate::port::{portb::PB3,portb::PB4,portb::PB5,mode}; + + +avr_hal::impl_spi! { + pub struct Spi { + peripheral: crate::atmega328p::SPI, + pins: { + sclk: PB5, + posi: PB3, + piso: PB4, + } + } +} diff --git a/chips/atmega32u4-hal/src/lib.rs b/chips/atmega32u4-hal/src/lib.rs index 16192a1c8f..46710eddff 100644 --- a/chips/atmega32u4-hal/src/lib.rs +++ b/chips/atmega32u4-hal/src/lib.rs @@ -46,6 +46,45 @@ pub mod i2c { } } +pub mod spi { + //! Implementation of the Rust Embedded-HAL SPI FullDuplex trait for AVR. + //! + //! The interface can be instantiated with the `new` method, and used directly + //! or passed into a driver. Example usage: + //! + //! ``` + //! pins.d10.into_output(&mut pins.ddr);// SS must be set to output mode + //! // create SPI interface + //! let mut spi = Spi::new( + //! dp.SPI,// SPI peripheral + //! pins.d11.into_output(&mut pins.ddr),// MOSI output pin + //! pins.d12.into_pull_up_input(&mut pins.ddr),// MISO input pin + //! Settings::default(), + //! ); + //! + //! // Send a byte + //! let sent = 0b10101010; + //! spi.send(sent).unwrap(); + //! let response = spi.read().unwrap(); + //! ``` + //! In the example above, all of the settings are left at the default. You can + //! also instantiate a Settings object with the other options available. + + pub use avr_hal::spi::*; + use crate::port::portb; + + avr_hal::impl_spi! { + pub struct Spi { + peripheral: crate::atmega32u4::SPI, + pins: { + sclk: portb::PB1, + mosi: portb::PB2, + miso: portb::PB3, + } + } + } +} + /// Serial interface using USART pub mod usart { use crate::port::portd;