Skip to main content

pamoja_hal/
linux.rs

1//! Linux backends: the kernel's I2C, SPI, and GPIO character devices as the bus traits.
2//!
3//! On a Raspberry Pi or any Linux single-board computer the buses are files:
4//! `/dev/i2c-1`, `/dev/spidev0.0`, `/dev/gpiochip0`. These functions open them as the
5//! traits every driver is written against, through `linux-embedded-hal`, so a driver
6//! moves from a scripted test to a gateway by being handed a different bus and
7//! nothing else. The kernel drivers have to be enabled first: on a Raspberry Pi that
8//! is `raspi-config`, or `dtparam=i2c_arm=on` and `dtparam=spi=on` in `config.txt`.
9//!
10//! A DS18B20 is the one part that should not be bit-banged from a Linux process,
11//! because user space cannot hold the microsecond slot timing the bus needs. Enable
12//! the kernel's own `w1-gpio` driver instead (`dtoverlay=w1-gpio`) and read the
13//! thermometer through the sysfs file it exposes, which the DS18B20 driver in
14//! `pamoja-sensors` does.
15//!
16//! # Examples
17//!
18//! ```no_run
19//! use pamoja_hal::i2c::I2c;
20//! use pamoja_hal::linux;
21//!
22//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
23//! // The BME280 on the Raspberry Pi's user I2C bus answers its chip id at 0xD0.
24//! const BME280: u8 = 0x76;
25//! let mut bus = linux::i2c("/dev/i2c-1")?;
26//! let mut id = [0u8; 1];
27//! bus.write_read(BME280, &[0xD0], &mut id)?;
28//! println!("chip id 0x{:02X}", id[0]);
29//! # Ok(())
30//! # }
31//! ```
32
33use std::fmt;
34use std::path::Path;
35
36use embedded_hal::digital::PinState;
37use linux_embedded_hal::gpio_cdev::{Chip, LineRequestFlags};
38use linux_embedded_hal::spidev::{SpiModeFlags, SpidevOptions};
39
40pub use linux_embedded_hal::{gpio_cdev, i2cdev, spidev};
41pub use linux_embedded_hal::{
42    CdevPin, CdevPinError, Delay, I2CError, I2cdev, SPIError, SpidevBus, SpidevDevice,
43};
44
45/// Why a bus or a line could not be opened.
46#[derive(Debug)]
47pub enum OpenError {
48    /// The I2C adapter could not be opened.
49    I2c(i2cdev::linux::LinuxI2CError),
50    /// The SPI device could not be opened or configured.
51    Spi(SPIError),
52    /// The GPIO chip or line could not be opened or requested.
53    Gpio(gpio_cdev::errors::Error),
54    /// The SPI mode was not 0, 1, 2, or 3.
55    SpiMode(u8),
56}
57
58impl fmt::Display for OpenError {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        match self {
61            OpenError::I2c(error) => write!(f, "opening the i2c adapter: {error}"),
62            OpenError::Spi(error) => write!(f, "opening the spi device: {error}"),
63            OpenError::Gpio(error) => write!(f, "opening the gpio line: {error}"),
64            OpenError::SpiMode(mode) => write!(f, "spi mode {mode} is not 0, 1, 2, or 3"),
65        }
66    }
67}
68
69impl std::error::Error for OpenError {
70    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
71        match self {
72            OpenError::I2c(error) => Some(error),
73            OpenError::Spi(error) => Some(error),
74            OpenError::Gpio(error) => Some(error),
75            OpenError::SpiMode(_) => None,
76        }
77    }
78}
79
80/// Opens an I2C adapter, such as `/dev/i2c-1`.
81///
82/// # Arguments
83///
84/// * `path` - the adapter's device file.
85///
86/// # Returns
87///
88/// The adapter, implementing the 7-bit and 10-bit [`I2c`](embedded_hal::i2c::I2c)
89/// traits.
90///
91/// # Errors
92///
93/// Returns [`OpenError::I2c`] if the file cannot be opened, which usually means the
94/// kernel driver is not enabled or the process lacks permission.
95pub fn i2c(path: impl AsRef<Path>) -> Result<I2cdev, OpenError> {
96    I2cdev::new(path).map_err(OpenError::I2c)
97}
98
99/// Opens an SPI device, such as `/dev/spidev0.0`, at a clock mode and speed.
100///
101/// # Arguments
102///
103/// * `path` - the device file, one per chip-select line.
104/// * `mode` - the clock mode, 0 to 3, as the part's datasheet quotes it.
105/// * `max_speed_hz` - the fastest clock the part accepts.
106///
107/// # Returns
108///
109/// The device, implementing [`SpiDevice`](embedded_hal::spi::SpiDevice) with 8-bit
110/// words.
111///
112/// # Errors
113///
114/// Returns [`OpenError::SpiMode`] for a mode above 3, or [`OpenError::Spi`] if the
115/// device cannot be opened or configured.
116pub fn spi(path: impl AsRef<Path>, mode: u8, max_speed_hz: u32) -> Result<SpidevDevice, OpenError> {
117    let flags = match mode {
118        0 => SpiModeFlags::SPI_MODE_0,
119        1 => SpiModeFlags::SPI_MODE_1,
120        2 => SpiModeFlags::SPI_MODE_2,
121        3 => SpiModeFlags::SPI_MODE_3,
122        other => return Err(OpenError::SpiMode(other)),
123    };
124    let mut device = SpidevDevice::open(path).map_err(OpenError::Spi)?;
125    let options = SpidevOptions::new()
126        .bits_per_word(8)
127        .max_speed_hz(max_speed_hz)
128        .mode(flags)
129        .build();
130    device
131        .0
132        .configure(&options)
133        .map_err(|error| OpenError::Spi(error.into()))?;
134    Ok(device)
135}
136
137/// Requests a GPIO line as an output.
138///
139/// # Arguments
140///
141/// * `chip` - the GPIO chip's device file, `/dev/gpiochip0` on a Raspberry Pi.
142/// * `line` - the line offset on that chip, the BCM number on a Raspberry Pi.
143/// * `consumer` - the name the kernel shows as holding the line.
144/// * `initial` - the level to drive as soon as the line is taken.
145///
146/// # Returns
147///
148/// The line, implementing [`OutputPin`](embedded_hal::digital::OutputPin).
149///
150/// # Errors
151///
152/// Returns [`OpenError::Gpio`] if the chip or line cannot be opened, or the line is
153/// already held by another process or a kernel driver.
154pub fn output(
155    chip: impl AsRef<Path>,
156    line: u32,
157    consumer: &str,
158    initial: PinState,
159) -> Result<CdevPin, OpenError> {
160    let handle = Chip::new(chip)
161        .and_then(|mut chip| chip.get_line(line))
162        .and_then(|line| {
163            line.request(
164                LineRequestFlags::OUTPUT,
165                u8::from(initial == PinState::High),
166                consumer,
167            )
168        })
169        .map_err(OpenError::Gpio)?;
170    CdevPin::new(handle).map_err(OpenError::Gpio)
171}
172
173/// Requests a GPIO line as an input.
174///
175/// # Arguments
176///
177/// * `chip` - the GPIO chip's device file.
178/// * `line` - the line offset on that chip.
179/// * `consumer` - the name the kernel shows as holding the line.
180///
181/// # Returns
182///
183/// The line, implementing [`InputPin`](embedded_hal::digital::InputPin).
184///
185/// # Errors
186///
187/// Returns [`OpenError::Gpio`] if the chip or line cannot be opened or requested.
188pub fn input(chip: impl AsRef<Path>, line: u32, consumer: &str) -> Result<CdevPin, OpenError> {
189    let handle = Chip::new(chip)
190        .and_then(|mut chip| chip.get_line(line))
191        .and_then(|line| line.request(LineRequestFlags::INPUT, 0, consumer))
192        .map_err(OpenError::Gpio)?;
193    CdevPin::new(handle).map_err(OpenError::Gpio)
194}
195
196/// Returns the delay that sleeps the process, for pacing a driver on a gateway.
197///
198/// # Returns
199///
200/// A [`DelayNs`](embedded_hal::delay::DelayNs) over `std::thread::sleep`.
201pub fn delay() -> Delay {
202    Delay
203}