rppal_pfd/lib.rs
1#![deny(missing_docs)]
2#![doc = include_str!("../README.md")]
3//!
4//! ## Extended example
5//!
6//! This example is available in `${CARGO_MANIFEST_DIR}/examples/blink-fast-slow.rs`.
7//!
8//! ``` rust no_run
9#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/examples/blink-fast-slow.rs"))]
10//! ```
11
12use std::{
13 cell::RefCell,
14 fmt::{self, Write},
15 rc::Rc,
16 result,
17 time::Duration,
18};
19
20#[cfg(not(any(test, feature = "mockspi")))]
21use log::warn;
22use log::{Level::Debug, debug, error, info, log_enabled};
23#[cfg(not(any(test, feature = "mockspi")))]
24use rppal::gpio::{self, Event as GpioEvent, Gpio, Trigger};
25#[cfg(not(feature = "mockspi"))]
26use rppal_mcp23s17::{IOCON, Mcp23s17, RegisterAddress};
27#[cfg(feature = "mockspi")]
28pub use rppal_mcp23s17::{IOCON, Mcp23s17, RegisterAddress};
29
30use thiserror::Error;
31
32/// Re-export of `rppal_mcp23s17` crate APIs which we use on this crate's APIs.
33pub use rppal_mcp23s17::{ChipSelect, InterruptMode, Level, OutputPin, SpiBus, SpiMode};
34
35//--------------------------------------------------------------------------------------
36/// The hardware address of the device - two bits.
37///
38/// The MCP23S17 supports three hardware address bits but the PiFace Digital only exposes
39/// `A0` and `A1` on `JP1` and `JP2` respectively.
40#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
41pub struct HardwareAddress(u8);
42
43impl HardwareAddress {
44 /// Hardware address space is two bits wide so 0-3 are valid.
45 pub const MAX_HARDWARE_ADDRESS: u8 = 3;
46
47 /// Create a HardwareAddress bounds-checking that it is valid.
48 pub fn new(address: u8) -> Result<Self> {
49 if address <= Self::MAX_HARDWARE_ADDRESS {
50 Ok(Self(address))
51 } else {
52 Err(PiFaceDigitalError::HardwareAddressBoundsError(address))
53 }
54 }
55}
56
57impl TryFrom<u8> for HardwareAddress {
58 type Error = PiFaceDigitalError;
59
60 fn try_from(value: u8) -> Result<Self> {
61 HardwareAddress::new(value)
62 }
63}
64
65impl From<HardwareAddress> for rppal_mcp23s17::HardwareAddress {
66 fn from(addr: HardwareAddress) -> Self {
67 // A PiFace Digital address is smaller than an MCP23S17 address.
68 rppal_mcp23s17::HardwareAddress::new(addr.0).unwrap()
69 }
70}
71
72impl From<HardwareAddress> for u8 {
73 fn from(addr: HardwareAddress) -> Self {
74 addr.0
75 }
76}
77
78impl TryFrom<rppal_mcp23s17::HardwareAddress> for HardwareAddress {
79 type Error = PiFaceDigitalError;
80
81 fn try_from(value: rppal_mcp23s17::HardwareAddress) -> Result<Self> {
82 Self::try_from(u8::from(value))
83 }
84}
85
86impl fmt::Display for HardwareAddress {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 fmt::Display::fmt(&format!("{}", self.0), f)
89 }
90}
91
92//--------------------------------------------------------------------------------------
93
94/// Errors that operation of the PiFace Digital can raise.
95#[derive(Error, Debug)]
96pub enum PiFaceDigitalError {
97 /// Errors from the `rppal_mcp23s17::Mcp23s17`.
98 #[error("MCP23S17 error")]
99 Mcp23s17Error {
100 /// Underlying error source.
101 #[from]
102 source: rppal_mcp23s17::Mcp23s17Error,
103 },
104
105 /// Attempt to access a PiFace Digital beyond the hardware address range
106 /// (0 - [`HardwareAddress::MAX_HARDWARE_ADDRESS`]).
107 #[error("Hardware address out of range")]
108 HardwareAddressBoundsError(u8),
109
110 /// Failed to detect the presence of any physical PiFace Digital device connected to
111 /// the SPI bus.
112 #[error("No hardware connected to {spi_bus} at hardware address={hardware_address})")]
113 NoHardwareDetected {
114 /// SPI bus we tried to access the device over.
115 spi_bus: SpiBus,
116 /// Hardware address of the device we tried to access.
117 hardware_address: HardwareAddress,
118 },
119
120 /// Errors accessing the GPIO for the interrupt input.
121 #[error("GPIO error")]
122 GpioError {
123 /// Underlying error source.
124 #[from]
125 source: rppal::gpio::Error,
126 },
127}
128
129/// Convenient alias for [`Result<_>`] types can have [`PiFaceDigitalError`]s.
130pub type Result<T> = result::Result<T, PiFaceDigitalError>;
131
132/// An input pin.
133///
134/// The [`InputPin`] exposes the capabilities of the underlying `rppal_mcp23s17::InputPin`
135/// with the addition of interrupt handling.
136///
137/// # Example usage
138///
139/// ```no_run
140/// # use rppal_pfd::{ChipSelect, HardwareAddress, Level, PiFaceDigital, SpiBus, SpiMode};
141/// #
142/// # // Create an instance of the driver for the device with the hardware address
143/// # // (A1, A0) of 0b00 on SPI bus 0 clocked at 100kHz. The address bits are set using
144/// # // `JP1` and `JP2` on the PiFace Digital board.
145/// # let pfd = PiFaceDigital::new(
146/// # HardwareAddress::new(0).expect("Invalid hardware address"),
147/// # SpiBus::Spi0,
148/// # ChipSelect::Cs0,
149/// # 100_000,
150/// # SpiMode::Mode0,
151/// # )
152/// # .expect("Failed to create PiFace Digital");
153/// #
154/// // Given an instance of a PiFaceDigital, take ownership of the output pin on bit 4
155/// // of the device.
156/// let pin = pfd
157/// .get_output_pin(4)
158/// .expect("Failed to get Pin");
159///
160/// // Set the pin to logic-level low.
161/// pin.write(Level::Low).expect("Bad pin write");
162/// ```
163#[derive(Debug)]
164pub struct InputPin {
165 pin: rppal_mcp23s17::InputPin,
166 interrupts_enabled: bool,
167 #[allow(dead_code)] // in test config no functionality accesses pfd_state.
168 pfd_state: Rc<RefCell<PiFaceDigitalState>>,
169}
170
171/// Internal state of the PiFace Digital card.
172#[derive(Debug)]
173pub struct PiFaceDigitalState {
174 mcp23s17: Mcp23s17,
175 #[cfg(not(any(test, feature = "mockspi")))]
176 _gpio: Gpio,
177 #[cfg(not(any(test, feature = "mockspi")))]
178 interrupt_pin: gpio::InputPin,
179}
180
181/// Represents an instance of the PiFace Digital I/O expander for the Raspberry Pi.
182///
183/// This is the key entrypoint into the driver. This driver is a thin wrapper around
184/// the `rppal_mcp23s17` driver that is responsible for ensuring that the I/O
185/// expander chip is configured in a manner compatible with the capabilities of the
186/// PiFace Digital hardware.
187///
188/// The PiFace Digital has two GPIO ports:
189///
190/// - `GPIOA` configured as outputs to:
191/// - two relays (bits 0 and 1)
192/// - surface-mount LEDs on each output
193/// - an 8-way terminal block
194///
195/// - `GPIOB` configured as inputs connected to
196/// - four on-board push switches on bits 0-3
197/// - an 8-way terminal block
198///
199/// The user should instantiate a [`PiFaceDigital`] and then use
200/// [`PiFaceDigital::get_input_pin()`] and [`PiFaceDigital::get_output_pin()`] to acquire
201/// an [`InputPin`] or [`OutputPin`].
202///
203/// ```no_run
204/// use rppal_pfd::{ChipSelect, HardwareAddress, PiFaceDigital, SpiBus, SpiMode};
205///
206/// // Create an instance of the driver for the device with the hardware address
207/// // (A1, A0) of 0b00 on SPI bus 0 clocked at 100kHz. The address bits are set using
208/// // JP1 and JP2 on the PiFace Digital board.
209/// let pfd = PiFaceDigital::new(
210/// HardwareAddress::new(0).expect("Invalid hardware address"),
211/// SpiBus::Spi0,
212/// ChipSelect::Cs0,
213/// 100_000,
214/// SpiMode::Mode0,
215/// )
216/// .expect("Failed to create PiFace Digital");
217///
218/// // Take ownership of the output pin on bit 4 of the device.
219/// let pin = pfd
220/// .get_output_pin(4)
221/// .expect("Failed to get OutputPin");
222/// ```
223#[derive(Debug)]
224pub struct PiFaceDigital {
225 pfd_state: Rc<RefCell<PiFaceDigitalState>>,
226}
227
228impl PiFaceDigital {
229 /// Create a PiFace Digital instance.
230 pub fn new(
231 address: HardwareAddress,
232 spi_bus: SpiBus,
233 chip_select: ChipSelect,
234 spi_clock: u32,
235 spi_mode: SpiMode,
236 ) -> Result<Self> {
237 let mcp23s17 = Mcp23s17::new(address.into(), spi_bus, chip_select, spi_clock, spi_mode)?;
238 #[cfg(any(test, feature = "mockspi"))]
239 let pfd_state = PiFaceDigitalState { mcp23s17 };
240 #[cfg(not(any(test, feature = "mockspi")))]
241 let pfd_state = {
242 let gpio = Gpio::new()?;
243 let interrupt_pin = gpio.get(25)?.into_input();
244 PiFaceDigitalState {
245 mcp23s17,
246 _gpio: gpio,
247 interrupt_pin,
248 }
249 };
250 Ok(PiFaceDigital {
251 pfd_state: Rc::new(RefCell::new(pfd_state)),
252 })
253 }
254
255 /// Initialise the PiFace Digital I/O board.
256 ///
257 /// Ensures that the registers in the MCP23S17 are configured appropriately for the
258 /// hardware setup. In normal use, this function should always be called immediately
259 /// after construction of the [`PiFaceDigital`] instance. Only makes sense _not_ to
260 /// initialise the device in situations where there are multiple instances and you
261 /// can guarantee another instance has initialised the device and you don't want
262 /// your instance to overwrite what may now be non-default config (_e.g._ because
263 /// pins have been constructed.)
264 ///
265 /// ## Default settings
266 ///
267 /// | Register | Purpose | GPIO-A side<br>(Output) | GPIO-B side<br>(Input) |
268 /// |-----------|----------------------|:-----------:|:-----------:|
269 /// | IODIR | I/O direction | 0x00 | 0xFF |
270 /// | IPOL | Input polarity | 0x00 | 0x00 |
271 /// | GPINTEN | GPIO interrupt enable| 0x00 | 0x00 |
272 /// | DEFVAL | Default value | 0x00 | 0x00 |
273 /// | INTCON | Interrupt control | 0x00 | 0x00 |
274 /// | IOCON | I/O control | 0x28 | (Note 1) |
275 /// | GPPU | GPIO pull-up | 0x00 | 0xFF<br>(Note 3) |
276 /// | INTF | Interrupt Flag | | |
277 /// | INTCAP | Interrupt capture | | |
278 /// | GPIO | Input/Output | 0x00 | |
279 /// | OLAT | Output latch | (Note 2) |(Note 2) |
280 ///
281 /// **Notes:**
282 ///
283 /// 1) There is only one IOCON register (though it appears at two addresses).
284 /// The value written into IOCON represents:
285 ///
286 /// - `BANK` off
287 /// - `MIRROR` off
288 /// - `SEQOP` off
289 /// - `DISSLW` enabled (this is an I<sup>2</sup>C function so effectively "don't care")
290 /// - `HAEN` on
291 /// - `ODR` off
292 /// - `INTPOL` active low
293 ///
294 /// The duplicate IOCON register is not written by this function.
295 ///
296 /// 2) OLATA is not explicitly written by this function but the value written to
297 /// GPIOA is also written through to OLATA by the device itself.
298 ///
299 /// 3) May mean that active digital inputs see an inappropriate pull-up load on
300 /// initialisation, but avoids having floating inputs picking up noise (and,
301 /// anyway, this is what the four push-switches on `GPIOB-0` to `GPIOB-3` are
302 /// expecting!)
303 ///
304 /// Once the MCP23S17 is in the desired state, the interrupt line on the Raspberry
305 /// Pi's GPIO gets enabled.
306 pub fn init(&mut self) -> Result<()> {
307 info!("Initialise PiFaceDigital registers to default values");
308
309 // First ensure IOCON is correct so that register addressing is set appropriately.
310 // It can't be done in the table below because the bits() function isn't const.
311 let iocon = (IOCON::BANK_OFF
312 | IOCON::MIRROR_OFF
313 | IOCON::SEQOP_OFF
314 | IOCON::DISSLW_SLEW_RATE_CONTROLLED
315 | IOCON::HAEN_ON
316 | IOCON::ODR_OFF
317 | IOCON::INTPOL_LOW)
318 .bits();
319 self.pfd_state
320 .borrow()
321 .mcp23s17
322 .write(RegisterAddress::IOCON, iocon)?;
323
324 // There are no acknowledgements in the SPI protocol so read-back the value to
325 // assess whether there's actually anything connected.
326 if self
327 .pfd_state
328 .borrow()
329 .mcp23s17
330 .read(RegisterAddress::IOCON)?
331 != iocon
332 {
333 return Err(PiFaceDigitalError::NoHardwareDetected {
334 spi_bus: self.pfd_state.borrow().mcp23s17.get_spi_bus(),
335 hardware_address: self
336 .pfd_state
337 .borrow()
338 .mcp23s17
339 .get_hardware_address()
340 .try_into()
341 .expect("MCP23S17 hardware address limited to PiFace Digital range"),
342 });
343 }
344
345 // Log debug info about the current register state.
346 debug!("Uninitialised MCP23S17 state");
347 self.debug_current_state("Uninitialised MCP23S17 state:")?;
348
349 const RESET_REGISTER_STATES: [(RegisterAddress, Option<u8>); RegisterAddress::LENGTH] = [
350 (RegisterAddress::IODIRA, Some(0x00)),
351 (RegisterAddress::IODIRB, Some(0xFF)),
352 (RegisterAddress::IPOLA, Some(0x00)),
353 (RegisterAddress::IPOLB, Some(0x00)),
354 (RegisterAddress::GPINTENA, Some(0x00)),
355 (RegisterAddress::GPINTENB, Some(0x00)),
356 (RegisterAddress::DEFVALA, Some(0x00)),
357 (RegisterAddress::DEFVALB, Some(0x00)),
358 (RegisterAddress::INTCONA, Some(0x00)),
359 (RegisterAddress::INTCONB, Some(0x00)),
360 (RegisterAddress::IOCON, None),
361 (RegisterAddress::IOCON2, None),
362 (RegisterAddress::GPPUA, Some(0x00)),
363 (RegisterAddress::GPPUB, Some(0xFF)),
364 (RegisterAddress::INTFA, None),
365 (RegisterAddress::INTFB, None),
366 (RegisterAddress::INTCAPA, None),
367 (RegisterAddress::INTCAPB, None),
368 (RegisterAddress::GPIOA, Some(0x00)),
369 (RegisterAddress::GPIOB, None),
370 (RegisterAddress::OLATA, None),
371 (RegisterAddress::OLATB, None),
372 ];
373
374 for (register_address, default_value) in RESET_REGISTER_STATES {
375 if let Some(data) = default_value {
376 self.pfd_state
377 .borrow()
378 .mcp23s17
379 .write(register_address, data)?;
380 debug!("New {register_address:?} register state: 0x{data:02x}");
381 }
382 }
383
384 // Log debug info about the updated register state.
385 debug!("Initialised MCP23S17 state");
386 self.debug_current_state("Initialised MCP23S17 state:")?;
387
388 // Enable the GPIO interrupts. The MCP23S17 should be in a state where all
389 // interrupts are disabled so there shouldn't be an immediate trigger.
390 #[cfg(not(any(test, feature = "mockspi")))]
391 self.pfd_state
392 .borrow_mut()
393 .interrupt_pin
394 .set_interrupt(Trigger::FallingEdge, None)?;
395
396 Ok(())
397 }
398
399 /// Returns an [`InputPin`] for the specified pin number configured as a
400 /// high-impedance input.
401 ///
402 /// If the pin is already in use, or the pin number `pin` is greater than 7 then
403 /// `get_input_pin()` returns `Err(`[`PiFaceDigitalError::Mcp23s17Error`]`)`
404 /// with the source error type of `rppal_mcp23s17::Mcp23s17Error::PinNotAvailable`.
405 ///
406 /// After the [`InputPin`] goes out of scope, it can be retrieved again through
407 /// another `get_input_pin()` call.
408 ///
409 /// When constructed, the pin has interrupts disabled.
410 pub fn get_input_pin(&self, pin: u8) -> Result<InputPin> {
411 // Get the unconfigured Pin (assuming it's available) and then convert it to
412 // an InputPin (i.e. high-impedance input).
413 Ok(InputPin {
414 pin: self
415 .pfd_state
416 .borrow()
417 .mcp23s17
418 .get(rppal_mcp23s17::Port::GpioB, pin)?
419 .into_input_pin()?,
420 interrupts_enabled: false,
421 pfd_state: self.pfd_state.clone(),
422 })
423 }
424
425 /// Returns an [`InputPin`] for the specified pin number configured with a pull-up
426 /// resistor.
427 ///
428 /// If the pin is already in use, or the pin number `pin` is greater than 7 then
429 /// `get_input_pin()` returns `Err(`[`PiFaceDigitalError::Mcp23s17Error`]`)`
430 /// with the source error type of `rppal_mcp23s17::Mcp23s17Error::PinNotAvailable`.
431 ///
432 /// After the [`InputPin`] goes out of scope, it can be retrieved again through
433 /// another `get_input_pin()` call.
434 ///
435 /// When constructed, the pin has interrupts disabled.
436 pub fn get_pull_up_input_pin(&self, pin: u8) -> Result<InputPin> {
437 // Get the unconfigured Pin (assuming it's available) and then convert it to
438 // an InputPin (i.e. high-impedance input).
439 Ok(InputPin {
440 pin: self
441 .pfd_state
442 .borrow()
443 .mcp23s17
444 .get(rppal_mcp23s17::Port::GpioB, pin)?
445 .into_pullup_input_pin()?,
446 interrupts_enabled: false,
447 pfd_state: self.pfd_state.clone(),
448 })
449 }
450
451 /// Returns an [`OutputPin`] for the specified pin number.
452 ///
453 /// If the pin is already in use, or the pin number `pin` is greater than 7 then
454 /// `get_input_pin()` returns `Err(`[`PiFaceDigitalError::Mcp23s17Error`]`)`
455 /// with the source error type of `rppal_mcp23s17::Mcp23s17Error::PinNotAvailable`.
456 ///
457 /// After the [`OutputPin`] goes out of scope, it can be retrieved again through
458 /// another `get_output_pin()` call.
459 pub fn get_output_pin(&self, pin: u8) -> Result<OutputPin> {
460 // Get the unconfigured Pin (assuming it's available) and then convert it to
461 // an InputPin (i.e. high-impedance input).
462 Ok(self
463 .pfd_state
464 .borrow()
465 .mcp23s17
466 .get(rppal_mcp23s17::Port::GpioA, pin)?
467 .into_output_pin()?)
468 }
469
470 /// Returns an [`OutputPin`] for the specified pin number already set high.
471 ///
472 /// If the pin is already in use, or the pin number `pin` is greater than 7 then
473 /// `get_input_pin()` returns `Err(`[`PiFaceDigitalError::Mcp23s17Error`]`)`
474 /// with the source error type of `rppal_mcp23s17::Mcp23s17Error::PinNotAvailable`.
475 ///
476 /// After the [`OutputPin`] goes out of scope, it can be retrieved again through
477 /// another `get_output_pin()` call.
478 pub fn get_output_pin_high(&self, pin: u8) -> Result<OutputPin> {
479 // Get the unconfigured Pin (assuming it's available) and then convert it to
480 // an InputPin (i.e. high-impedance input).
481 Ok(self
482 .pfd_state
483 .borrow()
484 .mcp23s17
485 .get(rppal_mcp23s17::Port::GpioA, pin)?
486 .into_output_pin_high()?)
487 }
488
489 /// Returns an [`OutputPin`] for the specified pin number already set low.
490 ///
491 /// If the pin is already in use, or the pin number `pin` is greater than 7 then
492 /// `get_input_pin()` returns `Err(`[`PiFaceDigitalError::Mcp23s17Error`]`)`
493 /// with the source error type of `rppal_mcp23s17::Mcp23s17Error::PinNotAvailable`.
494 ///
495 /// After the [`OutputPin`] goes out of scope, it can be retrieved again through
496 /// another `get_output_pin()` call.
497 pub fn get_output_pin_low(&self, pin: u8) -> Result<OutputPin> {
498 // Get the unconfigured Pin (assuming it's available) and then convert it to
499 // an InputPin (i.e. high-impedance input).
500 Ok(self
501 .pfd_state
502 .borrow()
503 .mcp23s17
504 .get(rppal_mcp23s17::Port::GpioA, pin)?
505 .into_output_pin_high()?)
506 }
507
508 // Waits for interrupts across a set of InputPins (actual docs are included because
509 // two flavours of this function exist)
510 #[doc = include_str!("sync-interrupts.md")]
511 #[cfg(not(any(test, feature = "mockspi")))]
512 pub fn poll_interrupts<'a>(
513 &self,
514 pins: &[&'a InputPin],
515 reset: bool,
516 timeout: Option<Duration>,
517 ) -> Result<Option<Vec<(&'a InputPin, Level)>>> {
518 // Including a pin that can't raise interrupts is considered a coding error.
519 for pin in pins {
520 assert!(
521 pin.interrupts_enabled(),
522 "InputPin({}) included in poll() does not have interrupts enabled!",
523 pin.get_pin_number()
524 );
525 }
526
527 let mut pfd_state = self.pfd_state.borrow_mut();
528
529 match pfd_state.interrupt_pin.poll_interrupt(reset, timeout)? {
530 Some(_level) => {
531 // There was an interrupt so work out what pin/pins registered it and
532 // get the input capture so we can report the levels on the pins.
533 let interrupt_flags = pfd_state.mcp23s17.read(RegisterAddress::INTFB)?;
534 let input_port = pfd_state.mcp23s17.read(RegisterAddress::INTCAPB)?;
535 let mut interrupting_pins = Vec::new();
536
537 for pin in pins {
538 let pin_no = pin.get_pin_number();
539 if (interrupt_flags & (0x01 << pin_no)) != 0 {
540 let level: Level = (input_port & (0x01 << pin_no)).into();
541 debug!("Active interrupt on pin {pin_no} level {level}");
542 interrupting_pins.push((*pin, level));
543 }
544 }
545
546 // Finding no active interrupts may be intentional but most likely
547 // indicates a misconfiguration, so log a warning.
548 if interrupting_pins.is_empty() {
549 warn!(
550 "No interrupts on any of pins {pins:?} - will poll again but interrupt will have been lost!"
551 );
552 }
553 Ok(Some(interrupting_pins))
554 }
555
556 // Poll timed out.
557 None => Ok(None),
558 }
559 }
560
561 // Dummy ("mockspi") version of synchronous interrupt poll (actual docs are included
562 // because two flavours of this function exist).
563 #[doc = include_str!("sync-interrupts.md")]
564 #[cfg(any(test, feature = "mockspi"))]
565 pub fn poll_interrupts<'a>(
566 &self,
567 pins: &[&'a InputPin],
568 _reset: bool,
569 _timeout: Option<Duration>,
570 ) -> Result<Option<Vec<(&'a InputPin, Level)>>> {
571 for pin in pins {
572 assert!(
573 pin.interrupts_enabled(),
574 "InputPin({}) included in poll() does not have interrupts enabled!",
575 pin.get_pin_number()
576 );
577 }
578 let mut _pfd_state = self.pfd_state.borrow_mut();
579
580 Ok(None)
581 }
582
583 // Asynchronous interrupt poll (actual docs are included because two flavours of this
584 // function exist).
585 #[doc = include_str!("async-interrupts.md")]
586 #[cfg(not(any(test, feature = "mockspi")))]
587 pub fn subscribe_async_interrupts<C: FnMut(GpioEvent) + Send + 'static>(
588 &mut self,
589 callback: C,
590 ) -> Result<()> {
591 self.pfd_state
592 .borrow_mut()
593 .interrupt_pin
594 .set_async_interrupt(Trigger::FallingEdge, None, callback)?;
595 Ok(())
596 }
597
598 // Dummy ("mockspi") version of asynchronous interrupt poll (actual docs are included
599 // because two flavours of this function exist).
600 #[doc = include_str!("async-interrupts.md")]
601 #[cfg(any(test, feature = "mockspi"))]
602 pub fn subscribe_async_interrupts<C: FnMut(bool) + Send + 'static>(
603 &mut self,
604 _callback: C,
605 ) -> Result<()> {
606 let mut _pfd_state = self.pfd_state.borrow_mut();
607 Ok(())
608 }
609
610 /// Clear the registered callback for interrupt notifications.
611 #[cfg(not(any(test, feature = "mockspi")))]
612 pub fn clear_async_interrupts(&mut self) -> Result<()> {
613 self.pfd_state
614 .borrow_mut()
615 .interrupt_pin
616 .clear_async_interrupt()?;
617 Ok(())
618 }
619
620 /// Register a callback for interrupt notifications.
621 #[cfg(any(test, feature = "mockspi"))]
622 pub fn clear_async_interrupts(&mut self) -> Result<()> {
623 let _pfd_state = self.pfd_state.borrow_mut();
624 Ok(())
625 }
626
627 /// Access the Interrupt Capture register for the input port.
628 pub fn get_interrupt_capture(&self) -> Result<u8> {
629 let data = self
630 .pfd_state
631 .borrow()
632 .mcp23s17
633 .read(RegisterAddress::INTCAPB)?;
634 Ok(data)
635 }
636
637 /// Access the Interrupt Capture register for the input port.
638 pub fn get_interrupt_flags(&self) -> Result<u8> {
639 let data = self
640 .pfd_state
641 .borrow()
642 .mcp23s17
643 .read(RegisterAddress::INTFB)?;
644 Ok(data)
645 }
646 /// Generate a debug log containing the state of the MCP23S17.
647 ///
648 /// If logging at `Debug` level, log the values currently in the MCP23S17's
649 /// registers, otherwise does nothing.
650 pub fn debug_current_state(&self, context: &str) -> Result<()> {
651 if log_enabled!(Debug) {
652 let mut state = String::new();
653 for register in 0..RegisterAddress::LENGTH {
654 let register_address = RegisterAddress::try_from(register).unwrap();
655 let data = self.pfd_state.borrow().mcp23s17.read(register_address)?;
656 writeln!(state, "{:10} : 0x{:02x}", register_address, data).unwrap();
657 }
658 debug!("{context}\n{state}");
659 }
660 Ok(())
661 }
662
663 /// In testing environments provide an API to get access to the mock SPI that
664 /// allows unit tests to be run without a real Raspberry Pi.
665 ///
666 /// Returns a tuple containing:
667 ///
668 /// - The current data in the mock SPI register `register`.
669 /// - The number of read accesses made to the register.
670 /// - The number of write accesses made to the register.
671 ///
672 /// ```
673 /// use rppal_pfd::{ChipSelect, HardwareAddress, Level, PiFaceDigital, RegisterAddress, SpiBus, SpiMode};
674 ///
675 /// let mut pfd = PiFaceDigital::new(
676 /// HardwareAddress::new(0).unwrap(),
677 /// SpiBus::Spi0,
678 /// ChipSelect::Cs0,
679 /// 100_000,
680 /// SpiMode::Mode0,
681 /// ).expect("Failed to construct!");
682 /// pfd.init().expect("Failed to initialise!");
683 ///
684 /// // The IOCON register gets set once and then read back by the initialise to
685 /// // test that there's actually some hardware connected. The 0x28 represents the
686 /// // default configuration.
687 /// assert_eq!(pfd.get_mock_data(RegisterAddress::IOCON),
688 /// (0x28, 1, 1));
689 /// ```
690
691 #[cfg(any(test, feature = "mockspi"))]
692 pub fn get_mock_data(&self, register: RegisterAddress) -> (u8, usize, usize) {
693 self.pfd_state.borrow().mcp23s17.get_mock_data(register)
694 }
695
696 /// In testing environments provide an API to get access to the mock SPI that
697 /// allows unit tests to be run without a real Raspberry Pi.
698 ///
699 /// ```
700 /// use rppal_pfd::{ChipSelect, HardwareAddress, Level, PiFaceDigital, RegisterAddress, SpiBus, SpiMode};
701 ///
702 /// let mut pfd = PiFaceDigital::new(
703 /// HardwareAddress::new(0).unwrap(),
704 /// SpiBus::Spi0,
705 /// ChipSelect::Cs0,
706 /// 100_000,
707 /// SpiMode::Mode0,
708 /// ).expect("Failed to construct!");
709 ///
710 /// pfd.set_mock_data(RegisterAddress::IOCON, 0x55);
711 /// assert_eq!(pfd.get_mock_data(RegisterAddress::IOCON),
712 /// (0x55, 0, 0));
713 ///
714 /// pfd.init().expect("Failed to initialise!");
715 ///
716 /// // The IOCON register gets set once and then read back by the initialise to
717 /// // test that there's actually some hardware connected. The 0x28 represents the
718 /// // default configuration.
719 /// assert_eq!(pfd.get_mock_data(RegisterAddress::IOCON),
720 /// (0x28, 1, 1));
721 /// ```
722 #[cfg(any(test, feature = "mockspi"))]
723 pub fn set_mock_data(&self, register: RegisterAddress, data: u8) {
724 self.pfd_state
725 .borrow()
726 .mcp23s17
727 .set_mock_data(register, data)
728 }
729}
730
731impl Default for PiFaceDigital {
732 /// Creates a default PiFaceDigital that:
733 ///
734 /// - Is at hardware address `0`
735 /// - On SPI bus `Spi0`
736 /// - Uses chip-select `Cs0`
737 /// - Is clocked at 100kHz
738 /// - Uses SPI mode 0
739 fn default() -> Self {
740 PiFaceDigital::new(
741 HardwareAddress::new(0).unwrap(),
742 SpiBus::Spi0,
743 ChipSelect::Cs0,
744 100_000,
745 SpiMode::Mode0,
746 )
747 .expect("Failed to create default PiFaceDigital")
748 }
749}
750
751impl InputPin {
752 /// Reads the pin's logic level.
753 #[inline]
754 pub fn read(&self) -> Result<Level> {
755 Ok(self.pin.read()?)
756 }
757
758 /// Reads the pin's logic level, and returns [`true`] if it is set to
759 /// [`Level::Low`].
760 #[inline]
761 pub fn is_low(&self) -> Result<bool> {
762 Ok(self.pin.read()? == Level::Low)
763 }
764
765 /// Reads the pin's logic level, and returns [`true`] if it is set to
766 /// [`Level::High`].
767 #[inline]
768 pub fn is_high(&self) -> Result<bool> {
769 Ok(self.pin.read()? == Level::High)
770 }
771
772 /// Enable synchronous interrupts.
773 ///
774 /// Synchronous interrupts can be polled once enabled by either:
775 ///
776 /// - Calling [`InputPin::poll_interrupt()`] in the case where just one `InputPin`
777 /// is configured to raise interrupts.
778 /// - Calling [`PiFaceDigital::poll_interrupts()`] in the case where more than one
779 /// InputPin is configured to raise interrupts.
780 ///
781 /// Interrupts can be disabled by a call to [`InputPin::clear_interrupt()`] and
782 /// will also be automatically disabled when the `InputPin` is dropped.
783 pub fn set_interrupt(&mut self, mode: InterruptMode) -> Result<()> {
784 self.interrupts_enabled = true;
785 self.pin.set_interrupt_mode(mode).map_err(|e| e.into())
786 }
787
788 /// Disable synchronous interrupts on the pin.
789 ///
790 /// Note that:
791 ///
792 /// - Multiple calls to `clear_interrupt()` are permitted (though don't do anything
793 /// useful).
794 /// - If not explicitly disabled, the interrupts will be disabled when the pin
795 /// is dropped.
796 pub fn clear_interrupt(&mut self) -> Result<()> {
797 self.interrupts_enabled = false;
798 self.pin
799 .set_interrupt_mode(InterruptMode::None)
800 .map_err(|e| e.into())
801 }
802
803 /// Wait for an interrupt (or timeout) on this pin.
804 ///
805 /// Must only be called if interrupts have been enabled - calling with interrupts
806 /// disabled is considered a coding error and will panic.
807 ///
808 /// If `reset` is `true` it will cause the GPIO interrupts to be flushed before
809 /// starting the poll.
810 ///
811 /// If no interrupts have happened after `timeout`, the function will exit returning
812 /// `Ok(None))`.
813 ///
814 /// Note that interrupts will have been re-enabled by the time that the poll returns
815 /// so there may be repeated interrupts.
816 ///
817 /// ## Example usage
818 ///
819 /// ```no_run
820 /// use rppal_pfd::{ChipSelect, HardwareAddress, InterruptMode, PiFaceDigital, SpiBus, SpiMode};
821 /// use std::time::Duration;
822 ///
823 /// let mut pfd = PiFaceDigital::new(
824 /// HardwareAddress::new(0).unwrap(),
825 /// SpiBus::Spi0,
826 /// ChipSelect::Cs0,
827 /// 100_000,
828 /// SpiMode::Mode0,
829 /// )
830 /// .expect("Failed to create PFD");
831 /// pfd.init().expect("Failed to initialise PFD");
832 ///
833 /// let mut pin = pfd.get_input_pin(0).expect("Failed to get pin");
834 /// pin.set_interrupt(InterruptMode::BothEdges)
835 /// .expect("Failed to enable interrupts");
836 ///
837 /// match pin.poll_interrupt(false, Some(Duration::from_secs(60))) {
838 /// Ok(Some(level)) => {
839 /// println!("Button pressed!");
840 /// let pin_no = pin.get_pin_number();
841 /// println!("Interrupt: pin({pin_no}) is {level}");
842 /// }
843 ///
844 /// Ok(None) => {
845 /// println!("Poll timed out");
846 /// }
847 ///
848 /// Err(e) => {
849 /// eprintln!("Poll failed with {e}");
850 /// }
851 /// }
852 ///
853 #[cfg(not(any(test, feature = "mockspi")))]
854 pub fn poll_interrupt(
855 &mut self,
856 reset: bool,
857 timeout: Option<Duration>,
858 ) -> Result<Option<Level>> {
859 use std::time::Instant;
860
861 assert!(
862 self.interrupts_enabled,
863 "InputPin({}): No interrupts enabled before trying to poll()",
864 self.get_pin_number()
865 );
866
867 let wait_until = timeout.map(|delay| Instant::now() + delay);
868
869 // The interrupt line may be asserted by any pin on this device or, potentially,
870 // other PiFace Digital devices on the same SPI bus. Check the relevant INTFB
871 // bit to see if this pin caused the interrupt.
872 loop {
873 let timeout = wait_until.map(|end_time| end_time - Instant::now());
874 let mut pfd_state = self.pfd_state.borrow_mut();
875 match pfd_state.interrupt_pin.poll_interrupt(reset, timeout)? {
876 Some(_level) => {
877 if pfd_state
878 .mcp23s17
879 .get_bit(RegisterAddress::INTFB, self.pin.get_pin_number())?
880 .into()
881 {
882 // We did raise the interrupt condition.
883 info!("Received interrupt on pin {}", self.pin.get_pin_number());
884 return Ok(Some(self.read()?));
885 } else {
886 // Wasn't this pin. We have to read the port to clear the
887 // interrupt but this probably wasn't what was intended so raise
888 // a warning.
889 warn!(
890 "Interrupt was not on pin {} - will poll again but interrupt will have been lost!",
891 self.pin.get_pin_number()
892 );
893 let _ = self.read()?;
894 }
895 }
896 None => return Ok(None),
897 }
898 }
899 }
900
901 /// Dummy version of interrupt poll routine for use in testing environments.
902 ///
903 /// Immediately returns as if a timeout occurred.
904 #[cfg(any(test, feature = "mockspi"))]
905 pub fn poll_interrupt(
906 &mut self,
907 _reset: bool,
908 _timeout: Option<Duration>,
909 ) -> Result<Option<Level>> {
910 assert!(
911 self.interrupts_enabled,
912 "InputPin({}): No interrupts enabled before trying to poll()",
913 self.get_pin_number()
914 );
915 Ok(None)
916 }
917
918 /// Get the pin number (0-7) that this pin is connected to.
919 pub fn get_pin_number(&self) -> u8 {
920 self.pin.get_pin_number()
921 }
922
923 /// Get the interrupt state.
924 pub fn interrupts_enabled(&self) -> bool {
925 self.interrupts_enabled
926 }
927}
928
929impl Drop for InputPin {
930 fn drop(&mut self) {
931 if self.interrupts_enabled {
932 self.clear_interrupt()
933 .expect("InputPin failed to clear interrupts on Drop");
934 }
935 }
936}
937
938#[cfg(test)]
939mod test {
940 use super::*;
941
942 #[test]
943 fn pfd_input_pin_poll_interrupt() {
944 let mut pfd = PiFaceDigital::new(
945 HardwareAddress::new(0).unwrap(),
946 SpiBus::Spi0,
947 ChipSelect::Cs0,
948 100_000,
949 SpiMode::Mode0,
950 )
951 .expect("Failed to create PFD");
952 pfd.init().expect("Failed to initialise PFD");
953
954 let mut pin = pfd.get_input_pin(0).expect("Failed to get pin");
955 pin.set_interrupt(InterruptMode::BothEdges)
956 .expect("Failed to enable interrupts");
957
958 assert_eq!(pin.poll_interrupt(false, None).expect("Bad poll"), None);
959 }
960
961 #[test]
962 #[should_panic]
963 fn pfd_input_pin_poll_interrupt_bad_config() {
964 let mut pfd = PiFaceDigital::new(
965 HardwareAddress::new(0).unwrap(),
966 SpiBus::Spi0,
967 ChipSelect::Cs0,
968 100_000,
969 SpiMode::Mode0,
970 )
971 .expect("Failed to create PFD");
972 pfd.init().expect("Failed to initialise PFD");
973
974 let mut pin = pfd.get_input_pin(0).expect("Failed to get pin");
975
976 let _ = pin.poll_interrupt(false, None).expect("Bad poll");
977 }
978
979 #[test]
980 fn pfd_input_pins_poll_interrupts() {
981 let mut pfd = PiFaceDigital::new(
982 HardwareAddress::new(0).unwrap(),
983 SpiBus::Spi0,
984 ChipSelect::Cs0,
985 100_000,
986 SpiMode::Mode0,
987 )
988 .expect("Failed to create PFD");
989 pfd.init().expect("Failed to initialise PFD");
990
991 let mut pin1 = pfd.get_input_pin(0).expect("Failed to get pin");
992 pin1.set_interrupt(InterruptMode::BothEdges)
993 .expect("Failed to enable interrupts");
994 let mut pin2 = pfd.get_input_pin(1).expect("Failed to get pin");
995 pin2.set_interrupt(InterruptMode::BothEdges)
996 .expect("Failed to enable interrupts");
997
998 let interrupt_pins = [&pin1, &pin2];
999 if let Some(interrupting_pins) = pfd
1000 .poll_interrupts(&interrupt_pins, false, None)
1001 .expect("Bad poll")
1002 {
1003 panic!("Not expecting any interrupts! Got: {interrupting_pins:?}")
1004 }
1005 }
1006
1007 #[test]
1008 #[should_panic]
1009 fn pfd_input_pins_poll_interrupts_bad_config() {
1010 let mut pfd = PiFaceDigital::new(
1011 HardwareAddress::new(0).unwrap(),
1012 SpiBus::Spi0,
1013 ChipSelect::Cs0,
1014 100_000,
1015 SpiMode::Mode0,
1016 )
1017 .expect("Failed to create PFD");
1018 pfd.init().expect("Failed to initialise PFD");
1019
1020 let mut pin1 = pfd.get_input_pin(0).expect("Failed to get pin");
1021 pin1.set_interrupt(InterruptMode::BothEdges)
1022 .expect("Failed to enable interrupts");
1023 let pin2 = pfd.get_input_pin(1).expect("Failed to get pin");
1024
1025 let interrupt_pins = [&pin1, &pin2];
1026 let _ = pfd.poll_interrupts(&interrupt_pins, false, None);
1027 }
1028
1029 #[test]
1030 fn pfd_input_pin_enable_interrupts() {
1031 let mut pfd = PiFaceDigital::new(
1032 HardwareAddress::new(0).unwrap(),
1033 SpiBus::Spi0,
1034 ChipSelect::Cs0,
1035 100_000,
1036 SpiMode::Mode0,
1037 )
1038 .expect("Failed to create PFD");
1039 pfd.init().expect("Failed to initialise PFD");
1040 assert_eq!(
1041 pfd.get_mock_data(RegisterAddress::GPINTENB),
1042 (0b0000_0000, 0, 1)
1043 );
1044
1045 {
1046 let mut pin = pfd.get_input_pin(0).expect("Failed to get pin");
1047 pin.set_interrupt(InterruptMode::BothEdges)
1048 .expect("Failed to enable interrupts");
1049 assert_eq!(
1050 pfd.get_mock_data(RegisterAddress::GPINTENB),
1051 (0b0000_0001, 1, 2)
1052 );
1053 }
1054 assert_eq!(
1055 pfd.get_mock_data(RegisterAddress::GPINTENB),
1056 (0b0000_0000, 2, 3)
1057 );
1058 }
1059
1060 #[test]
1061 fn pfd_input_pin_read_levels() {
1062 let mut pfd = PiFaceDigital::new(
1063 HardwareAddress::new(0).unwrap(),
1064 SpiBus::Spi0,
1065 ChipSelect::Cs0,
1066 100_000,
1067 SpiMode::Mode0,
1068 )
1069 .expect("Failed to create PFD");
1070 pfd.init().expect("Failed to initialise PFD");
1071
1072 let pin = pfd.get_input_pin(0).expect("Failed to get pin");
1073 assert!(pin.is_low().expect("Bad pin access"));
1074
1075 pfd.set_mock_data(RegisterAddress::GPIOB, 0b0000_0001);
1076 assert!(pin.is_high().expect("Bad pin access"));
1077 }
1078
1079 #[test]
1080 fn pfd_init() {
1081 let mut pfd = PiFaceDigital::new(
1082 HardwareAddress::new(0).unwrap(),
1083 SpiBus::Spi0,
1084 ChipSelect::Cs0,
1085 100_000,
1086 SpiMode::Mode0,
1087 )
1088 .expect("Failed to create PFD");
1089 pfd.init().expect("Failed to initialise PFD");
1090
1091 // Sample a few of the registers for correct values.
1092 assert_eq!(
1093 pfd.get_mock_data(RegisterAddress::IODIRA),
1094 (0x00, 0, 1),
1095 "Bad IODIRA"
1096 );
1097 assert_eq!(
1098 pfd.get_mock_data(RegisterAddress::IODIRB),
1099 (0xFF, 0, 1),
1100 "Bad IODIRB"
1101 );
1102 assert_eq!(
1103 pfd.get_mock_data(RegisterAddress::IOCON),
1104 (0x28, 1, 1),
1105 "Bad IOCON"
1106 );
1107 assert_eq!(
1108 pfd.get_mock_data(RegisterAddress::GPPUB),
1109 (0xFF, 0, 1),
1110 "Bad GPPUB"
1111 );
1112 }
1113
1114 #[test]
1115 fn pfd_init_no_hardware() {
1116 let mut pfd = PiFaceDigital::new(
1117 HardwareAddress::new(0).unwrap(),
1118 SpiBus::Spi6, // Magic value that makes mock simulate no hardware.
1119 ChipSelect::Cs0,
1120 100_000,
1121 SpiMode::Mode0,
1122 )
1123 .expect("Failed to create PFD");
1124 let init_result = pfd.init();
1125
1126 // Check we get the expected error.
1127 println!("{init_result:?}");
1128 match init_result {
1129 Err(PiFaceDigitalError::NoHardwareDetected {
1130 spi_bus: bus,
1131 hardware_address: address,
1132 }) => {
1133 assert_eq!(bus, SpiBus::Spi6);
1134 assert_eq!(address, 0.try_into().unwrap())
1135 }
1136 _ => panic!("Unexpected return result: {init_result:?}"),
1137 }
1138 }
1139
1140 #[test]
1141 fn pfd_address_to_mcp23s17_addr() {
1142 let pfd_addr = HardwareAddress::new(3).expect("valid HardwareAddress");
1143 let mcp_addr: rppal_mcp23s17::HardwareAddress = pfd_addr.into();
1144 assert_eq!(
1145 mcp_addr,
1146 rppal_mcp23s17::HardwareAddress::new(3).expect("valid HardwareAddress")
1147 );
1148 }
1149 #[test]
1150 fn good_hardware_address() {
1151 let addr = HardwareAddress::new(2).expect("Bad address");
1152 assert_eq!(2u8, addr.into(), "Unexpected address value");
1153 }
1154
1155 #[test]
1156 fn bad_hardware_address() {
1157 let addr = HardwareAddress::new(4);
1158 match addr {
1159 Err(PiFaceDigitalError::HardwareAddressBoundsError(4)) => (),
1160 _ => panic!("Unexpected return value: {addr:?}"),
1161 }
1162 }
1163
1164 #[test]
1165 fn try_into_good_hardware_address() {
1166 let addr: HardwareAddress = 3u8.try_into().expect("Bad address");
1167 assert_eq!(3u8, addr.into(), "Unexpected address value");
1168 }
1169
1170 #[test]
1171 fn try_into_bad_hardware_address() {
1172 let addr: Result<HardwareAddress> = 8u8.try_into();
1173 match addr {
1174 Err(PiFaceDigitalError::HardwareAddressBoundsError(8)) => (),
1175 _ => panic!("Unexpected return value: {addr:?}"),
1176 }
1177 }
1178}