Skip to content

Commit

Permalink
Add a HAL implementation for SPI master
Browse files Browse the repository at this point in the history
Implement embedded-hal's FullDuplex SPI master trait for the dedicated
SPI peripheral built into a lot of AVR chips.
  • Loading branch information
jonahbron committed Apr 23, 2020
1 parent 2cb668d commit 726f7f4
Show file tree
Hide file tree
Showing 11 changed files with 448 additions and 3 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions avr-hal-generic/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
203 changes: 203 additions & 0 deletions avr-hal-generic/src/spi.rs
Original file line number Diff line number Diff line change
@@ -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<u8> 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<u8, Self::Error> {
self.flush()?;
Ok(self.peripheral.spdr.read().bits())
}
}
};
}
50 changes: 50 additions & 0 deletions boards/arduino-leonardo/examples/leonardo-spi-feedback.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
1 change: 1 addition & 0 deletions boards/arduino-leonardo/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<hal::clock::MHz16>;
Expand Down
14 changes: 13 additions & 1 deletion boards/arduino-leonardo/src/pins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
55 changes: 55 additions & 0 deletions boards/arduino-uno/examples/uno-spi-feedback.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
1 change: 1 addition & 0 deletions boards/arduino-uno/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<hal::clock::MHz16>;
Expand Down
Loading

0 comments on commit 726f7f4

Please sign in to comment.