Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement the SpiBus trait from embedded-hal 1.0. #503

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions avr-hal-generic/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
#![cfg_attr(avr_hal_asm_macro, feature(asm_experimental_arch))]
#![cfg_attr(not(avr_hal_asm_macro), feature(llvm_asm))]

pub use embedded_hal_v0 as hal;
pub use embedded_hal as hal;
pub use embedded_hal_v0 as hal_v0;

#[doc(hidden)]
pub use avr_device;
Expand All @@ -24,7 +25,7 @@ pub mod wdt;

/// Prelude containing all HAL traits
pub mod prelude {
pub use crate::hal::prelude::*;
pub use crate::hal_v0::prelude::*;
pub use ufmt::uWrite as _ufmt_uWrite;
pub use unwrap_infallible::UnwrapInfallible as _unwrap_infallible_UnwrapInfallible;
}
Expand Down
139 changes: 136 additions & 3 deletions avr-hal-generic/src/spi.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
//! SPI Implementation
use crate::port;
use core::cmp::Ordering;
use core::marker::PhantomData;
use embedded_hal_v0::spi;
use embedded_hal::spi;

/// Oscillator Clock Frequency division options.
///
Expand Down Expand Up @@ -75,6 +76,10 @@ pub trait SpiOps<H, SCLK, MOSI, MISO, CS> {
fn raw_check_iflag(&self) -> bool;
fn raw_read(&self) -> u8;
fn raw_write(&mut self, byte: u8);

fn raw_read_buf(&self, buf: &mut [u8]);
fn raw_write_buf(&mut self, buf: &[u8]);
fn raw_read_write_buf(&mut self, buffer: &mut [u8], bytes: &[u8]);
}

/// Wrapper for the CS pin
Expand Down Expand Up @@ -106,19 +111,50 @@ impl<CSPIN: port::PinOps> embedded_hal_v0::digital::v2::StatefulOutputPin for Ch
}
}

impl<CSPIN: port::PinOps> embedded_hal_v0::digital::v2::ToggleableOutputPin for ChipSelectPin<CSPIN> {
impl<CSPIN: port::PinOps> embedded_hal_v0::digital::v2::ToggleableOutputPin
for ChipSelectPin<CSPIN>
{
type Error = core::convert::Infallible;
fn toggle(&mut self) -> Result<(), Self::Error> {
self.0.toggle();
Ok(())
}
}

impl<CSPIN: port::PinOps> embedded_hal::digital::ErrorType for ChipSelectPin<CSPIN> {
type Error = core::convert::Infallible;
}

impl<CSPIN: port::PinOps> embedded_hal::digital::OutputPin for ChipSelectPin<CSPIN> {
fn set_high(&mut self) -> Result<(), Self::Error> {
self.0.set_high();
Ok(())
}

fn set_low(&mut self) -> Result<(), Self::Error> {
self.0.set_low();
Ok(())
}
}

impl<CSPIN: port::PinOps> embedded_hal::digital::StatefulOutputPin for ChipSelectPin<CSPIN> {
fn is_set_high(&mut self) -> Result<bool, Self::Error> {
Ok(self.0.is_set_high())
}

fn is_set_low(&mut self) -> Result<bool, Self::Error> {
Ok(self.0.is_set_low())
}
}

/// 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.
///
/// This can be used both with the embedded-hal 0.2 spi::FullDuplex trait, and
/// with the embedded-hal 1.0 spi::SpiBus trait.
Comment on lines +156 to +157
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can link the traits like this:

Suggested change
/// This can be used both with the embedded-hal 0.2 spi::FullDuplex trait, and
/// with the embedded-hal 1.0 spi::SpiBus trait.
/// This can be used both with the embedded-hal 0.2 [`spi::FullDuplex`] trait, and
/// with the embedded-hal 1.0 [`spi::SpiBus`] trait.
///
/// [`spi::FullDuplex`]: `embedded_hal_v0::spi::FullDuplex`
/// [`spi::SpiBus`]: `embedded_hal::spi::SpiBus`

pub struct Spi<H, SPI, SCLKPIN, MOSIPIN, MISOPIN, CSPIN> {
p: SPI,
sclk: port::Pin<port::mode::Output, SCLKPIN>,
Expand Down Expand Up @@ -241,7 +277,7 @@ where
/// 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<H, SPI, SCLKPIN, MOSIPIN, MISOPIN, CSPIN> spi::FullDuplex<u8>
impl<H, SPI, SCLKPIN, MOSIPIN, MISOPIN, CSPIN> embedded_hal_v0::spi::FullDuplex<u8>
for Spi<H, SPI, SCLKPIN, MOSIPIN, MISOPIN, CSPIN>
where
SPI: SpiOps<H, SCLKPIN, MOSIPIN, MISOPIN, CSPIN>,
Expand Down Expand Up @@ -290,6 +326,70 @@ where
{
}

impl<H, SPI, SCLKPIN, MOSIPIN, MISOPIN, CSPIN> embedded_hal::spi::ErrorType
for Spi<H, SPI, SCLKPIN, MOSIPIN, MISOPIN, CSPIN>
where
SPI: SpiOps<H, SCLKPIN, MOSIPIN, MISOPIN, CSPIN>,
SCLKPIN: port::PinOps,
MOSIPIN: port::PinOps,
MISOPIN: port::PinOps,
CSPIN: port::PinOps,
{
type Error = core::convert::Infallible;
}

impl<H, SPI, SCLKPIN, MOSIPIN, MISOPIN, CSPIN> embedded_hal::spi::SpiBus
for Spi<H, SPI, SCLKPIN, MOSIPIN, MISOPIN, CSPIN>
where
SPI: SpiOps<H, SCLKPIN, MOSIPIN, MISOPIN, CSPIN>,
SCLKPIN: port::PinOps,
MOSIPIN: port::PinOps,
MISOPIN: port::PinOps,
CSPIN: port::PinOps,
{
fn read(&mut self, read: &mut [u8]) -> Result<(), Self::Error> {
Ok(self.p.raw_read_buf(read))
}

fn write(&mut self, write: &[u8]) -> Result<(), Self::Error> {
Ok(self.p.raw_write_buf(write))
}

fn transfer(&mut self, read: &mut [u8], write: &[u8]) -> Result<(), Self::Error> {
match read.len().cmp(&write.len()) {
Ordering::Less => {
let (write1, write2) = write.split_at(read.len());
self.p.raw_read_write_buf(read, write1);
self.p.raw_write_buf(write2);
}
Ordering::Equal => self.p.raw_read_write_buf(read, write),
Ordering::Greater => {
let (read1, read2) = read.split_at_mut(write.len());
self.p.raw_read_write_buf(read1, write);
self.p.raw_read_buf(read2);
}
}
Comment on lines +359 to +371
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a thought: The following implementation would be equivalent. But I am not sure which one is more elegant. I think yours might still be more explicit about the intent and thus easier to understand. But I'll let you decide which one to use.

let common_length = read.len().min(write.len());
let (write1, write2) = write.split_at(common_length);
let (read1, read2) = read.split_at(common_length);
self.p.raw_read_write_buf(read1, write1);

debug_assert!(write2.len() == 0 || read2.len() == 0);
self.p.raw_write_buf(write2);
self.p.raw_read_buf(read2);

Ok(())
}

fn transfer_in_place(&mut self, buffer: &mut [u8]) -> Result<(), Self::Error> {
let write = unsafe {
let p = buffer.as_ptr();
core::slice::from_raw_parts(p, buffer.len())
};
self.p.raw_read_write_buf(buffer, write);
Comment on lines +376 to +380
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To my knowledge, this is illegal: You are constructing an immutable reference to the same data that is referenced mutably by buffer. This violates aliasing rules.

I think we have to provide a separate low-level raw_read_write_in_place() here - there is no way to reuse raw_read_write_buf().

Ok(())
}

fn flush(&mut self) -> Result<(), Self::Error> {
if self.write_in_progress {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to make sure I understand this correctly:

You are only making use of the write_in_progress flag here to ensure correct behavior when both e-h0.2::FullDuplex and e-h1.0::SpiBus are used at the same time, right?

All the other methods of e-h1.0::SpiBus synchronize with the end of the transfer internally and thus they don't need to set write_in_progress. Is that correct?

And related: What if write_in_progress is set when calling e.g. SpiBus::write()? Shouldn't SpiBus::write() call <Self as SpiBus>::flush() before operating on the bus to ensure any FullDuplex transfer from before has actually finished?

while !self.p.raw_check_iflag() {}
self.write_in_progress = false;
}
Ok(())
}
}

/// Implement traits for a SPI interface
#[macro_export]
macro_rules! impl_spi {
Expand Down Expand Up @@ -370,6 +470,39 @@ macro_rules! impl_spi {
fn raw_write(&mut self, byte: u8) {
self.spdr.write(|w| unsafe { w.bits(byte) });
}

/// Read n bytes from the data register, n being to the size of
/// the given buffer.
fn raw_read_buf(&self, buf: &mut [u8]) {
buf.iter_mut().for_each(|b| *b = self.spdr.read().bits())
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look right to me: You can't just read from a SPI bus without somehow triggering a transfer. You'll also have to wait for the transfer to have completed somewhere.

The documentation from embedded-hal says this:

The word value sent on MOSI during reading is implementation-defined, typically 0x00, 0xFF, or configurable.

So something like this is needed:

            fn raw_read_buf(&self, buf: &mut [u8]) {
                for b in buf.iter_mut() {
                    // We send 0x00 on MOSI during "pure" reading
                    self.spdr.write(|w| unsafe { w.bits(0x00) });
                    while self.spsr.read().spif().bit_is_clear() {}
                    *b = self.spdr.read().bits();
                }
            }

}

/// Write n bytes to the data register, n being the size of the
/// given buffer.
fn raw_write_buf(&mut self, buf: &[u8]) {
buf.iter().for_each(|b| {
self.spdr.write(|w| unsafe { w.bits(*b) });
while self.spsr.read().spif().bit_is_clear() {}
})
Comment on lines +483 to +486
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer a plain for loop over .for_each() when the .for_each isn't strictly required for some reason:

Suggested change
buf.iter().for_each(|b| {
self.spdr.write(|w| unsafe { w.bits(*b) });
while self.spsr.read().spif().bit_is_clear() {}
})
for b in buf.iter() {
self.spdr.write(|w| unsafe { w.bits(*b) });
while self.spsr.read().spif().bit_is_clear() {}
}

}

/// Read and write n bytes at the same time, n being the size of
/// the given buffers.
///
/// <div class="warning">The input and output buffers must have
/// the same size</div>
fn raw_read_write_buf(&mut self, buffer: &mut [u8], bytes: &[u8]) {
assert_eq!(buffer.len(), bytes.len());

let mut buffer_it = buffer.iter_mut();
let mut bytes_it = bytes.iter();
while let Some(byte) = bytes_it.next() {
self.raw_write(*byte);
while self.spsr.read().spif().bit_is_clear() {}
let mut data = buffer_it.next().unwrap();
*data = self.raw_read();
}
Comment on lines +497 to +504
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option would be to zip the two iterators:

for (byte, data) in bytes.iter().zip(buffer.iter_mut()) {
    self.raw_write(*byte);
    while self.spsr.read().spif().bit_is_clear() {}
    *data = self.raw_read();
}

}
}
};
}
1 change: 1 addition & 0 deletions examples/arduino-uno/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ embedded-hal = "1.0"
pwm-pca9685 = "0.3.1"
infrared = "0.14.1"
embedded-storage = "0.2"
embedded-hal-bus = "0.1.0"

[dependencies.embedded-hal-v0]
version = "0.2.3"
Expand Down
66 changes: 66 additions & 0 deletions examples/arduino-uno/src/bin/uno-spi-feedback-embedded-hal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*!
* Example of the SPI bus, by looping back output to input, but with the
* feedback function not being aware of the SPI part of `avr-hal` and using
* only the `embedded-hal` and `embedded-hal-bus` traits.
*
* 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 `D11` to pin `D12`.
*
* If done correctly, 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.
*
* Connections:
* - `D11` connected directly to `D12` (loop MOSI to MISO)
*/
#![no_std]
#![no_main]

use arduino_hal::hal::port::{PD0, PD1};
use arduino_hal::pac::USART0;
use arduino_hal::port::{
mode::{Input, Output},
Pin,
};
use arduino_hal::prelude::*;
use arduino_hal::spi;
use arduino_hal::Usart;
use panic_halt as _;

#[arduino_hal::entry]
fn main() -> ! {
let dp = arduino_hal::Peripherals::take().unwrap();
let pins = arduino_hal::pins!(dp);

// Set up the serial interface for text output
let mut serial = arduino_hal::default_serial!(dp, pins, 57600);

// Create SPI interface.
let (spi_bus, cs_pin) = arduino_hal::Spi::new(
dp.SPI,
pins.d13.into_output(),
pins.d11.into_output(),
pins.d12.into_pull_up_input(),
pins.d10.into_output(),
spi::Settings::default(),
);
let mut spi = embedded_hal_bus::spi::ExclusiveDevice::new_no_delay(spi_bus, cs_pin);

feedback(&mut spi, &mut serial);
}

fn feedback(
spi: &mut impl embedded_hal::spi::SpiDevice,
serial: &mut Usart<USART0, Pin<Input, PD0>, Pin<Output, PD1>>,
) -> ! {
loop {
// Send a byte and read data, which should be the same because MISO is
// connected to MOSI
let write = [15u8];
let mut read = [255u8; 1];
spi.transfer(&mut read, &write).unwrap();

ufmt::uwriteln!(serial, "data: {}\r", read[0]).unwrap_infallible();
arduino_hal::delay_ms(1000);
}
}
38 changes: 24 additions & 14 deletions mcu/atmega-hal/src/spi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
use crate::port;
pub use avr_hal_generic::spi::*;

#[cfg(any(feature = "atmega128a", feature = "atmega1280", feature = "atmega2560", feature = "atmega32u4"))]
#[cfg(any(
feature = "atmega128a",
feature = "atmega1280",
feature = "atmega2560",
feature = "atmega32u4"
))]
pub type Spi = avr_hal_generic::spi::Spi<
crate::Atmega,
crate::pac::SPI,
Expand All @@ -11,7 +16,12 @@ pub type Spi = avr_hal_generic::spi::Spi<
port::PB3,
port::PB0,
>;
#[cfg(any(feature = "atmega128a", feature = "atmega1280", feature = "atmega2560", feature = "atmega32u4"))]
#[cfg(any(
feature = "atmega128a",
feature = "atmega1280",
feature = "atmega2560",
feature = "atmega32u4"
))]
avr_hal_generic::impl_spi! {
hal: crate::Atmega,
peripheral: crate::pac::SPI,
Expand All @@ -21,7 +31,12 @@ avr_hal_generic::impl_spi! {
cs: port::PB0,
}

#[cfg(any(feature = "atmega168", feature = "atmega328p", feature = "atmega48p",))]
#[cfg(any(
feature = "atmega168",
feature = "atmega328p",
feature = "atmega48p",
feature = "atmega8"
))]
pub type Spi = avr_hal_generic::spi::Spi<
crate::Atmega,
crate::pac::SPI,
Expand All @@ -30,7 +45,12 @@ pub type Spi = avr_hal_generic::spi::Spi<
port::PB4,
port::PB2,
>;
#[cfg(any(feature = "atmega168", feature = "atmega328p", feature = "atmega48p",))]
#[cfg(any(
feature = "atmega168",
feature = "atmega328p",
feature = "atmega48p",
feature = "atmega8"
))]
avr_hal_generic::impl_spi! {
hal: crate::Atmega,
peripheral: crate::pac::SPI,
Expand Down Expand Up @@ -95,13 +115,3 @@ avr_hal_generic::impl_spi! {
miso: port::PB6,
cs: port::PB4,
}

#[cfg(any(feature = "atmega8"))]
avr_hal_generic::impl_spi! {
hal: crate::Atmega,
peripheral: crate::pac::SPI,
sclk: port::PB5,
mosi: port::PB3,
miso: port::PB4,
cs: port::PB2,
}
Comment on lines -98 to -107
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch! I took the liberty to merge this change already in commit b2be3f9 ("atmega-hal: Drop redundant atmega8 macro invocation"). Please rebase your PR on latest main when you send the next iteration :)