Skip to main content

pamoja_ffi/
gpio.rs

1//! The C ABI for on-board bus addressing and pin logic.
2//!
3//! These functions wrap [`pamoja_gpio`] for callers that reach the SDK through
4//! the flat C boundary: I2C addressing per NXP UM10204, the four SPI clock modes,
5//! and the pin model that maps a logical "asserted" onto a physical level.
6//!
7//! Nothing here allocates or holds state, so nothing here is a handle. An I2C
8//! address is two scalars and crosses by value as [`PamojaI2cAddress`]; the rest
9//! are enumerations and small pure functions over them.
10
11use pamoja_gpio::i2c::{Address, Direction};
12use pamoja_gpio::pin::{Edge, Level, Polarity};
13use pamoja_gpio::spi::Mode;
14use pamoja_gpio::GpioError;
15
16use crate::{set_last_error, PamojaStatus};
17
18/// The largest I2C address frame, in bytes: the two a 10-bit address needs.
19pub const PAMOJA_I2C_FRAME_MAX: usize = 2;
20
21/// The lowest 7-bit address the I2C specification keeps for itself.
22pub const PAMOJA_I2C_RESERVED_FROM: u8 = 0x78;
23
24/// The first 7-bit address above the reserved block at the bottom of the range.
25pub const PAMOJA_I2C_RESERVED_BELOW: u8 = 0x08;
26
27// The header generator does not read the crates this one depends on, so these
28// carry their value rather than the name of the constant that defines it.
29const _: () = assert!(PAMOJA_I2C_RESERVED_FROM == pamoja_gpio::i2c::RESERVED_FROM);
30const _: () = assert!(PAMOJA_I2C_RESERVED_BELOW == pamoja_gpio::i2c::RESERVED_BELOW);
31
32/// A validated I2C device address.
33///
34/// Build one with [`pamoja_i2c_address_seven_bit`] or
35/// [`pamoja_i2c_address_ten_bit`], which reject a value outside the width's
36/// range. Both fields are scalars, so this crosses the boundary by value.
37#[repr(C)]
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub struct PamojaI2cAddress {
40    /// The address itself, without the read/write bit.
41    pub value: u16,
42    /// `1` for a 10-bit address, `0` for a 7-bit one.
43    pub ten_bit: u8,
44}
45
46/// Which direction an I2C transfer runs, as the read/write bit encodes it.
47#[repr(C)]
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub enum PamojaI2cDirection {
50    /// The controller writes to the device. Read/write bit `0`.
51    Write = 0,
52    /// The controller reads from the device. Read/write bit `1`.
53    Read = 1,
54}
55
56/// The physical voltage level on a pin.
57#[repr(C)]
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub enum PamojaPinLevel {
60    /// A low level, near ground.
61    Low = 0,
62    /// A high level, near the supply voltage.
63    High = 1,
64}
65
66/// The signal transition that triggers a pin interrupt.
67#[repr(C)]
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69pub enum PamojaPinEdge {
70    /// A low-to-high transition.
71    Rising = 0,
72    /// A high-to-low transition.
73    Falling = 1,
74    /// Either transition.
75    Both = 2,
76}
77
78/// Whether a signal is asserted by a high or a low physical level.
79#[repr(C)]
80#[derive(Clone, Copy, Debug, PartialEq, Eq)]
81pub enum PamojaPinPolarity {
82    /// A high level means asserted.
83    ActiveHigh = 0,
84    /// A low level means asserted, the wiring of most buttons and relay boards.
85    ActiveLow = 1,
86}
87
88/// Validates a 7-bit I2C address.
89///
90/// The whole `0x00..=0x7F` range is accepted, reserved addresses included, since
91/// those are still legal on the wire. Test for them with
92/// [`pamoja_i2c_address_is_reserved`].
93///
94/// # Returns
95///
96/// [`PamojaStatus::Ok`] on success, with `*out_address` set, or
97/// [`PamojaStatus::InvalidArgument`] if `address` is above `0x7F`.
98///
99/// # Safety
100///
101/// `out_address` must point to a writable `PamojaI2cAddress`.
102#[no_mangle]
103pub unsafe extern "C" fn pamoja_i2c_address_seven_bit(
104    address: u8,
105    out_address: *mut PamojaI2cAddress,
106) -> PamojaStatus {
107    emit(Address::seven_bit(address), out_address)
108}
109
110/// Validates a 10-bit I2C address.
111///
112/// # Returns
113///
114/// [`PamojaStatus::Ok`] on success, with `*out_address` set, or
115/// [`PamojaStatus::InvalidArgument`] if `address` is above `0x3FF`.
116///
117/// # Safety
118///
119/// `out_address` must point to a writable `PamojaI2cAddress`.
120#[no_mangle]
121pub unsafe extern "C" fn pamoja_i2c_address_ten_bit(
122    address: u16,
123    out_address: *mut PamojaI2cAddress,
124) -> PamojaStatus {
125    emit(Address::ten_bit(address), out_address)
126}
127
128/// Returns how many bytes an address frame occupies.
129///
130/// # Returns
131///
132/// `1` for a 7-bit address, `2` for a 10-bit one.
133#[no_mangle]
134pub extern "C" fn pamoja_i2c_address_frame_len(address: PamojaI2cAddress) -> usize {
135    borrow(address).map_or(0, Address::frame_len)
136}
137
138/// Writes the address bytes a controller puts on the bus for a transfer.
139///
140/// # Returns
141///
142/// [`PamojaStatus::Ok`] on success, with `*out_len` set to how many bytes of
143/// `out_frame` were written, or [`PamojaStatus::InvalidArgument`] if the address
144/// is not one this SDK produced or `out_frame_cap` is smaller than
145/// [`pamoja_i2c_address_frame_len`].
146///
147/// # Safety
148///
149/// `out_frame` must point to at least `out_frame_cap` writable bytes, and
150/// `out_len` must point to a writable `size_t`.
151#[no_mangle]
152pub unsafe extern "C" fn pamoja_i2c_address_frame(
153    address: PamojaI2cAddress,
154    direction: PamojaI2cDirection,
155    out_frame: *mut u8,
156    out_frame_cap: usize,
157    out_len: *mut usize,
158) -> PamojaStatus {
159    if out_frame.is_null() || out_len.is_null() {
160        set_last_error("out_frame and out_len must not be null".to_owned());
161        return PamojaStatus::InvalidArgument;
162    }
163    let Some(address) = borrow(address) else {
164        set_last_error("address is not a valid I2C address".to_owned());
165        return PamojaStatus::InvalidArgument;
166    };
167    let out = std::slice::from_raw_parts_mut(out_frame, out_frame_cap);
168    match address.write_frame(direction.into(), out) {
169        Ok(written) => {
170            *out_len = written;
171            PamojaStatus::Ok
172        }
173        Err(error) => failed(error),
174    }
175}
176
177/// Reports whether a 7-bit address falls in a range the I2C specification reserves.
178///
179/// # Returns
180///
181/// `true` for a 7-bit address in `0x00..=0x07` or `0x78..=0x7F`, which leaves
182/// `0x08..=0x77` for ordinary devices. A 10-bit address is never reserved in this
183/// sense.
184#[no_mangle]
185pub extern "C" fn pamoja_i2c_address_is_reserved(address: PamojaI2cAddress) -> bool {
186    borrow(address).is_some_and(Address::is_reserved)
187}
188
189/// Reports whether an address is the general call address `0x00`.
190///
191/// # Returns
192///
193/// `true` for the broadcast every device on the bus listens to.
194#[no_mangle]
195pub extern "C" fn pamoja_i2c_address_is_general_call(address: PamojaI2cAddress) -> bool {
196    borrow(address).is_some_and(Address::is_general_call)
197}
198
199/// Returns the `(CPOL, CPHA)` pair an SPI mode number names.
200///
201/// # Returns
202///
203/// [`PamojaStatus::Ok`] on success, with `*out_cpol` set to whether the clock
204/// idles high and `*out_cpha` to whether data is sampled on the trailing edge, or
205/// [`PamojaStatus::InvalidArgument`] if `mode` is above 3.
206///
207/// # Safety
208///
209/// `out_cpol` and `out_cpha` must each point to a writable `bool`.
210#[no_mangle]
211pub unsafe extern "C" fn pamoja_spi_mode_cpol_cpha(
212    mode: u8,
213    out_cpol: *mut bool,
214    out_cpha: *mut bool,
215) -> PamojaStatus {
216    if out_cpol.is_null() || out_cpha.is_null() {
217        set_last_error("out_cpol and out_cpha must not be null".to_owned());
218        return PamojaStatus::InvalidArgument;
219    }
220    let Some(mode) = Mode::from_number(mode) else {
221        set_last_error("SPI mode must be 0, 1, 2, or 3".to_owned());
222        return PamojaStatus::InvalidArgument;
223    };
224    let (cpol, cpha) = mode.cpol_cpha();
225    *out_cpol = cpol;
226    *out_cpha = cpha;
227    PamojaStatus::Ok
228}
229
230/// Returns the SPI mode number a `(CPOL, CPHA)` pair names.
231///
232/// # Returns
233///
234/// The mode number `0..=3`. Every pair names a mode, so this never fails.
235#[no_mangle]
236pub extern "C" fn pamoja_spi_mode_from_cpol_cpha(cpol: bool, cpha: bool) -> u8 {
237    Mode::from_cpol_cpha(cpol, cpha).number()
238}
239
240/// Returns the level a boolean names.
241///
242/// # Returns
243///
244/// [`PamojaPinLevel::High`] for `true`, [`PamojaPinLevel::Low`] for `false`.
245#[no_mangle]
246pub extern "C" fn pamoja_pin_level_from_bool(high: bool) -> PamojaPinLevel {
247    Level::from_bool(high).into()
248}
249
250/// Returns the opposite level.
251///
252/// # Returns
253///
254/// The inverted level.
255#[no_mangle]
256pub extern "C" fn pamoja_pin_level_inverted(level: PamojaPinLevel) -> PamojaPinLevel {
257    Level::from(level).inverted().into()
258}
259
260/// Reports whether a change from one level to another fires an interrupt trigger.
261///
262/// # Returns
263///
264/// `true` if the transition matches `edge`; `false` for the other direction or
265/// for no change at all.
266#[no_mangle]
267pub extern "C" fn pamoja_pin_edge_triggered_by(
268    edge: PamojaPinEdge,
269    from: PamojaPinLevel,
270    to: PamojaPinLevel,
271) -> bool {
272    Edge::from(edge).triggered_by(from.into(), to.into())
273}
274
275/// Returns the physical level that represents a logical state under a polarity.
276///
277/// # Returns
278///
279/// The level to drive, which for active-low wiring is the inverse of `asserted`.
280#[no_mangle]
281pub extern "C" fn pamoja_pin_polarity_level(
282    polarity: PamojaPinPolarity,
283    asserted: bool,
284) -> PamojaPinLevel {
285    Polarity::from(polarity).level(asserted).into()
286}
287
288/// Reports whether a physical level means the signal is asserted.
289///
290/// # Returns
291///
292/// `true` if `level` asserts the signal under `polarity`.
293#[no_mangle]
294pub extern "C" fn pamoja_pin_polarity_is_asserted(
295    polarity: PamojaPinPolarity,
296    level: PamojaPinLevel,
297) -> bool {
298    Polarity::from(polarity).is_asserted(level.into())
299}
300
301impl From<PamojaI2cDirection> for Direction {
302    fn from(value: PamojaI2cDirection) -> Self {
303        match value {
304            PamojaI2cDirection::Write => Direction::Write,
305            PamojaI2cDirection::Read => Direction::Read,
306        }
307    }
308}
309
310impl From<PamojaPinLevel> for Level {
311    fn from(value: PamojaPinLevel) -> Self {
312        match value {
313            PamojaPinLevel::Low => Level::Low,
314            PamojaPinLevel::High => Level::High,
315        }
316    }
317}
318
319impl From<Level> for PamojaPinLevel {
320    fn from(value: Level) -> Self {
321        match value {
322            Level::Low => PamojaPinLevel::Low,
323            Level::High => PamojaPinLevel::High,
324        }
325    }
326}
327
328impl From<PamojaPinEdge> for Edge {
329    fn from(value: PamojaPinEdge) -> Self {
330        match value {
331            PamojaPinEdge::Rising => Edge::Rising,
332            PamojaPinEdge::Falling => Edge::Falling,
333            PamojaPinEdge::Both => Edge::Both,
334        }
335    }
336}
337
338impl From<PamojaPinPolarity> for Polarity {
339    fn from(value: PamojaPinPolarity) -> Self {
340        match value {
341            PamojaPinPolarity::ActiveHigh => Polarity::ActiveHigh,
342            PamojaPinPolarity::ActiveLow => Polarity::ActiveLow,
343        }
344    }
345}
346
347/// Rebuilds the validated address a [`PamojaI2cAddress`] describes.
348///
349/// The value crossed the boundary as plain scalars, so a caller could have
350/// changed it since; this puts it back through the same validation that produced
351/// it rather than trusting the fields.
352fn borrow(address: PamojaI2cAddress) -> Option<Address> {
353    if address.ten_bit == 0 {
354        u8::try_from(address.value)
355            .ok()
356            .and_then(|value| Address::seven_bit(value).ok())
357    } else {
358        Address::ten_bit(address.value).ok()
359    }
360}
361
362/// Writes a validated address to the caller's slot, or reports why it is invalid.
363///
364/// # Safety
365///
366/// `out_address` must point to a writable `PamojaI2cAddress`.
367unsafe fn emit(
368    address: Result<Address, GpioError>,
369    out_address: *mut PamojaI2cAddress,
370) -> PamojaStatus {
371    if out_address.is_null() {
372        set_last_error("out_address must not be null".to_owned());
373        return PamojaStatus::InvalidArgument;
374    }
375    match address {
376        Ok(address) => {
377            *out_address = PamojaI2cAddress {
378                value: address.value(),
379                ten_bit: u8::from(address.is_ten_bit()),
380            };
381            PamojaStatus::Ok
382        }
383        Err(error) => failed(error),
384    }
385}
386
387/// Records an addressing error and maps it onto its status.
388fn failed(error: GpioError) -> PamojaStatus {
389    set_last_error(error.to_string());
390    PamojaStatus::InvalidArgument
391}
392
393#[cfg(test)]
394mod tests {
395    use super::*;
396
397    /// Builds a 7-bit address, which the tests use as their ordinary case.
398    fn seven_bit(value: u8) -> PamojaI2cAddress {
399        let mut address = PamojaI2cAddress {
400            value: 0,
401            ten_bit: 0,
402        };
403        // Safety: the out-pointer is writable.
404        let status = unsafe { pamoja_i2c_address_seven_bit(value, &mut address) };
405        assert_eq!(status, PamojaStatus::Ok);
406        address
407    }
408
409    #[test]
410    fn a_seven_bit_address_frames_as_one_shifted_byte() {
411        let bme = seven_bit(0x76);
412        let mut frame = [0u8; PAMOJA_I2C_FRAME_MAX];
413        let mut len = 0usize;
414
415        // Safety: the buffer and the length slot are writable.
416        unsafe {
417            assert_eq!(
418                pamoja_i2c_address_frame(
419                    bme,
420                    PamojaI2cDirection::Write,
421                    frame.as_mut_ptr(),
422                    frame.len(),
423                    &mut len
424                ),
425                PamojaStatus::Ok
426            );
427            assert_eq!((len, frame[0]), (1, 0xEC), "(0x76 << 1) | 0");
428            assert_eq!(
429                pamoja_i2c_address_frame(
430                    bme,
431                    PamojaI2cDirection::Read,
432                    frame.as_mut_ptr(),
433                    frame.len(),
434                    &mut len
435                ),
436                PamojaStatus::Ok
437            );
438            assert_eq!((len, frame[0]), (1, 0xED), "(0x76 << 1) | 1");
439        }
440    }
441
442    #[test]
443    fn a_ten_bit_address_frames_as_the_reserved_prefix_and_the_low_byte() {
444        let mut address = PamojaI2cAddress {
445            value: 0,
446            ten_bit: 0,
447        };
448        let mut frame = [0u8; PAMOJA_I2C_FRAME_MAX];
449        let mut len = 0usize;
450
451        // Safety: the out-pointers and the buffer are writable.
452        unsafe {
453            assert_eq!(
454                pamoja_i2c_address_ten_bit(0x2A5, &mut address),
455                PamojaStatus::Ok
456            );
457            assert_eq!(pamoja_i2c_address_frame_len(address), 2);
458            assert_eq!(
459                pamoja_i2c_address_frame(
460                    address,
461                    PamojaI2cDirection::Write,
462                    frame.as_mut_ptr(),
463                    frame.len(),
464                    &mut len
465                ),
466                PamojaStatus::Ok
467            );
468            assert_eq!(len, 2);
469            assert_eq!(frame, [0xF4, 0xA5], "11110 then the top two bits, then r/w");
470        }
471    }
472
473    #[test]
474    fn an_out_of_range_address_is_refused() {
475        let mut address = PamojaI2cAddress {
476            value: 0,
477            ten_bit: 0,
478        };
479        // Safety: the out-pointer is writable.
480        let status = unsafe { pamoja_i2c_address_ten_bit(0x400, &mut address) };
481        assert_eq!(status, PamojaStatus::InvalidArgument);
482    }
483
484    #[test]
485    fn a_buffer_too_small_for_the_frame_is_refused() {
486        let mut address = PamojaI2cAddress {
487            value: 0,
488            ten_bit: 0,
489        };
490        let mut frame = [0u8; 1];
491        let mut len = 0usize;
492
493        // Safety: the out-pointers and the buffer are writable.
494        unsafe {
495            assert_eq!(
496                pamoja_i2c_address_ten_bit(0x2A5, &mut address),
497                PamojaStatus::Ok
498            );
499            assert_eq!(
500                pamoja_i2c_address_frame(
501                    address,
502                    PamojaI2cDirection::Write,
503                    frame.as_mut_ptr(),
504                    frame.len(),
505                    &mut len
506                ),
507                PamojaStatus::InvalidArgument
508            );
509        }
510    }
511
512    #[test]
513    fn the_reserved_ranges_are_recognised() {
514        assert!(pamoja_i2c_address_is_reserved(seven_bit(0x00)));
515        assert!(pamoja_i2c_address_is_general_call(seven_bit(0x00)));
516        assert!(pamoja_i2c_address_is_reserved(seven_bit(0x07)));
517        assert!(!pamoja_i2c_address_is_reserved(seven_bit(0x08)));
518        assert!(!pamoja_i2c_address_is_reserved(seven_bit(0x77)));
519        assert!(pamoja_i2c_address_is_reserved(seven_bit(0x78)));
520    }
521
522    #[test]
523    fn the_spi_modes_match_the_pairs_datasheets_quote() {
524        let mut cpol = false;
525        let mut cpha = false;
526        for (number, expected) in [
527            (0u8, (false, false)),
528            (1, (false, true)),
529            (2, (true, false)),
530            (3, (true, true)),
531        ] {
532            // Safety: both out-pointers are writable.
533            unsafe {
534                assert_eq!(
535                    pamoja_spi_mode_cpol_cpha(number, &mut cpol, &mut cpha),
536                    PamojaStatus::Ok
537                );
538            }
539            assert_eq!((cpol, cpha), expected, "mode {number}");
540            assert_eq!(pamoja_spi_mode_from_cpol_cpha(cpol, cpha), number);
541        }
542    }
543
544    #[test]
545    fn a_mode_number_above_three_is_refused() {
546        let mut cpol = false;
547        let mut cpha = false;
548        // Safety: both out-pointers are writable.
549        let status = unsafe { pamoja_spi_mode_cpol_cpha(4, &mut cpol, &mut cpha) };
550        assert_eq!(status, PamojaStatus::InvalidArgument);
551    }
552
553    #[test]
554    fn an_active_low_relay_is_energised_by_a_low_level() {
555        assert_eq!(
556            pamoja_pin_polarity_level(PamojaPinPolarity::ActiveLow, true),
557            PamojaPinLevel::Low
558        );
559        assert_eq!(
560            pamoja_pin_polarity_level(PamojaPinPolarity::ActiveHigh, true),
561            PamojaPinLevel::High
562        );
563        assert!(pamoja_pin_polarity_is_asserted(
564            PamojaPinPolarity::ActiveLow,
565            PamojaPinLevel::Low
566        ));
567    }
568
569    #[test]
570    fn an_edge_fires_only_on_its_own_transition() {
571        assert!(pamoja_pin_edge_triggered_by(
572            PamojaPinEdge::Rising,
573            PamojaPinLevel::Low,
574            PamojaPinLevel::High
575        ));
576        assert!(!pamoja_pin_edge_triggered_by(
577            PamojaPinEdge::Rising,
578            PamojaPinLevel::High,
579            PamojaPinLevel::Low
580        ));
581        assert!(pamoja_pin_edge_triggered_by(
582            PamojaPinEdge::Both,
583            PamojaPinLevel::High,
584            PamojaPinLevel::Low
585        ));
586        assert!(!pamoja_pin_edge_triggered_by(
587            PamojaPinEdge::Both,
588            PamojaPinLevel::High,
589            PamojaPinLevel::High
590        ));
591    }
592
593    #[test]
594    fn a_level_inverts() {
595        assert_eq!(
596            pamoja_pin_level_inverted(pamoja_pin_level_from_bool(true)),
597            PamojaPinLevel::Low
598        );
599    }
600}