pamoja

An SDK for IoT, robotics, and drones

pamojaRev 0.2.0MIT licensepamoja.molex.cloud

I2C, SPI, and GPIO#

Before a node reaches any network it has to talk to the chips wired to the same board. Three interfaces cover most of that hardware: a plain pin for the relays, buttons, and float switches that sit on one line, I2C for the dense breakout sensors, and SPI for displays, SD cards, and radios. Each carries a small piece of exact logic, and each is a classic field bug when it is wrong: which level energizes a relay, which address byte reaches the bus, and which clock mode a part expects.

pamoja carries that logic and none of the wiring. A relay is a Switch and a float switch is a Contact, in all four languages, over whatever line the board gives you: a scripted line in a test, a GpioLine on a Raspberry Pi or any Linux board, or a microcontroller's own pin. The code between those two lines never changes.

What the example does#

It runs a pump while a tank fills. The pump sits on a relay board that energizes when its input is pulled low, and the float switch closes to ground when the tank is full, so both are active low and the example says so once instead of inverting a level by hand at every call site. The float reads open, then closed; the pump stops. Then it looks up the two facts a datasheet asks for before a part on a shared bus will answer at all: the byte the address becomes on the wire, and the clock mode.

It proves:

  • Naming a relay active low is enough: running the pump drives its line low and stopping it drives the line high, with no inversion written at the call site.
  • The same polarity read the other way turns a level into an answer, so an open float reads as not full and a closed one reads as full.
  • A tank reaching full is a falling edge on that line, which a watch armed for the rising edge would sleep through.
  • A part at 0x76 is written to as 0xEC and read from as 0xED, which is why a datasheet and a bus capture rarely print the same number.
  • 0x78 is not a device address at all, because it opens a block the I2C specification keeps for itself.
  • SPI mode 3 is clock polarity 1 with phase 1, the pair a datasheet quotes as one number.

Run it#

Rust

cargo run -p pamoja-examples --example gpio

TypeScript

npm --prefix bindings/node run guides -- gpio

Python

python bindings/python/guides/gpio.py

C#

dotnet run --project bindings/dotnet/samples/Pamoja.Guides -- gpio

Each program runs with nothing plugged in. On a Raspberry Pi wired as On a board shows, set PAMOJA_GPIO_CHIP=/dev/gpiochip0 before the same command and it runs the real pump as well.

Rust#

pamoja-gpio carries the pin model and the two parts, Switch and Contact, which take any embedded-hal output or input line; pamoja-hal carries PinScript, the line a test gives them. Switch implements the core Actuator and Contact the core Sensor, which is why the example drives the pump with apply and reads the float with read: a profile or a rule drives the same pump without knowing it is a pin. The direct calls are set and is_asserted, used in On a board. A line that fails returns its own error through the Result; a scripted line never fails, so the example expects.

From examples/guides/gpio.rs:

Rust
use pamoja_core::{Actuator, Sensor};
use pamoja_gpio::i2c::{Address, Direction, RESERVED_FROM};
use pamoja_gpio::pin::{Edge, Level};
use pamoja_gpio::spi::Mode;
use pamoja_gpio::switch::{Contact, Switch};
use pamoja_hal::digital::PinState;
use pamoja_hal::script::PinScript;

// Most relay boards energize when their input is pulled low, and a float switch wired
// to ground closes the same way. Saying "active low" once, here, is what keeps the
// inversion out of every line below it.
let mut pump = Switch::active_low(PinScript::new([]));
let mut float = Contact::active_low(PinScript::new([PinState::High, PinState::Low]));
let runs_on = pump.polarity().level(true);
println!("a pump on an active-low relay runs when its line is {runs_on:?}");

// The pump runs while the tank fills. The scripted line answers open and then closed,
// so this is the real loop with nothing plugged in; on a board the same two lines take
// a pin from the board's GPIO library instead.
pump.apply(true).await.expect("the relay takes it");
let while_filling = float.read().await.expect("the line reads");
let once_filled = float.read().await.expect("the line reads");
let full = |closed: bool| if closed { "full" } else { "not full" };
println!(
    "the float reads {}, then {}",
    full(while_filling),
    full(once_filled)
);

// The moment the float closes is that line going low, which is a falling edge. A watch
// armed for the rising one would sleep through the tank filling.
let closing = Edge::Falling.triggered_by(Level::High, Level::Low);
let edge = if closing {
    "a falling edge"
} else {
    "not a falling edge"
};
println!("the float closing is {edge} on that line");

// Full, so the pump stops. Releasing the switch hands the line back, and the levels it
// was driven to are the whole conversation the board saw.
pump.apply(false).await.expect("the relay takes it");
let line = pump.release();
let (ran, stopped) = (line.driven()[0], line.driven()[1]);
println!("running drove the line {ran:?} and stopping drove it {stopped:?}");

// A part on a shared bus answers to an address, and the byte on the wire is not the
// address the datasheet prints: it shifts up one and the low bit says read or write.
let sensor = Address::seven_bit(0x76).expect("a 7-bit address");
let to_write = sensor.frame(Direction::Write).as_bytes()[0];
let to_read = sensor.frame(Direction::Read).as_bytes()[0];
println!("a part at 0x76 is written to as {to_write:#04X} and read from as {to_read:#04X}");

// Two ranges belong to the specification itself, so a part answering in either is a
// wiring mistake rather than a device.
let reserved = Address::seven_bit(RESERVED_FROM)
    .expect("in range")
    .is_reserved();
let owner = if reserved {
    "reserved by the specification"
} else {
    "free for a device"
};
println!("{RESERVED_FROM:#04X} is {owner}");

// And a datasheet quotes SPI's clock polarity and phase as one mode number.
let (idles_high, trailing_edge) = Mode::Mode3.cpol_cpha();
let idle = if idles_high { "high" } else { "low" };
let edge = if trailing_edge { "trailing" } else { "leading" };
println!("SPI mode 3 idles {idle} and samples on the {edge} edge");

TypeScript#

@pamoja/gpio has the same three types, Switch, Contact, and PinScript, and the same pure functions under pin, i2c, and spi. The calls are synchronous. A level is PinLevel.High or PinLevel.Low, and anything with a drive(level) method goes under a Switch and anything with read() under a Contact, so a GpioLine on Linux, a PinScript in a test, or a two-line adapter over another pin library all fit. A refusal throws an Error whose message names the cause.

From bindings/node/guides/gpio.ts:

TypeScript
import { Contact, GpioLine, PinEdge, PinLevel, PinScript, Switch, i2c, pin, spi } from '@pamoja/gpio'

// Most relay boards energize when their input is pulled low, and a float switch wired to
// ground closes the same way. Saying "active low" once, here, is what keeps the inversion
// out of every line below it.
const pump = Switch.activeLow(new PinScript())
const float = Contact.activeLow(new PinScript([PinLevel.High, PinLevel.Low]))
const runsOn = pin.levelFor(pump.polarity, true)
console.log(`a pump on an active-low relay runs when its line is ${runsOn}`)

// The pump runs while the tank fills. The scripted line answers open and then closed, so
// this is the real loop with nothing plugged in; on a board the same two lines take a pin
// from the board's GPIO library instead.
pump.set(true)
const whileFilling = float.isAsserted()
const onceFilled = float.isAsserted()
const full = (closed: boolean) => (closed ? 'full' : 'not full')
console.log(`the float reads ${full(whileFilling)}, then ${full(onceFilled)}`)

// The moment the float closes is that line going low, which is a falling edge. A watch
// armed for the rising one would sleep through the tank filling.
const closing = pin.triggers(PinEdge.Falling, PinLevel.High, PinLevel.Low)
console.log(`the float closing is ${closing ? 'a falling edge' : 'not a falling edge'} on that line`)

// Full, so the pump stops. Releasing the switch hands the line back, and the levels it was
// driven to are the whole conversation the board saw.
pump.set(false)
const [ran, stopped] = pump.release().driven
console.log(`running drove the line ${ran} and stopping drove it ${stopped}`)

// A part on a shared bus answers to an address, and the byte on the wire is not the
// address the datasheet prints: it shifts up one and the low bit says read or write.
const hex = (byte: number) => `0x${byte.toString(16).toUpperCase().padStart(2, '0')}`
const toWrite = i2c.addressFrame(0x76)[0]!
const toRead = i2c.addressFrame(0x76, { read: true })[0]!
console.log(`a part at 0x76 is written to as ${hex(toWrite)} and read from as ${hex(toRead)}`)

// Two ranges belong to the specification itself, so a part answering in either is a wiring
// mistake rather than a device.
const reserved = i2c.isReserved(i2c.RESERVED_FROM)
console.log(`${hex(i2c.RESERVED_FROM)} is ${reserved ? 'reserved by the specification' : 'free for a device'}`)

// And a datasheet quotes SPI's clock polarity and phase as one mode number.
const clock = spi.clockFor(3)
const idle = clock.cpol ? 'high' : 'low'
const edge = clock.cpha ? 'trailing' : 'leading'
console.log(`SPI mode 3 idles ${idle} and samples on the ${edge} edge`)

Python#

pamoja.gpio has the same types in Python's spelling: Switch.active_low, is_asserted, PinScript. A level is Level.HIGH or Level.LOW, and a line is any object with drive(level) or read(), which the OutputLine and InputLine protocols describe for a type checker. A refusal raises PamojaError, except a value that means nothing at all, such as SPI mode 4, which raises ValueError.

From bindings/python/guides/gpio.py:

Python
from pamoja.gpio import Contact, Edge, GpioLine, Level, PinScript, Switch, i2c, pin, spi

# Most relay boards energize when their input is pulled low, and a float switch wired to
# ground closes the same way. Saying "active low" once, here, is what keeps the inversion
# out of every line below it.
pump = Switch.active_low(PinScript())
float_switch = Contact.active_low(PinScript([Level.HIGH, Level.LOW]))
runs_on = pin.level_for(pump.polarity, True)
print(f"a pump on an active-low relay runs when its line is {runs_on.value}")

# The pump runs while the tank fills. The scripted line answers open and then closed, so
# this is the real loop with nothing plugged in; on a board the same two lines take a pin
# from the board's GPIO library instead.
pump.set(True)
while_filling = float_switch.is_asserted()
once_filled = float_switch.is_asserted()
full = {True: "full", False: "not full"}
print(f"the float reads {full[while_filling]}, then {full[once_filled]}")

# The moment the float closes is that line going low, which is a falling edge. A watch
# armed for the rising one would sleep through the tank filling.
closing = pin.triggers(Edge.FALLING, Level.HIGH, Level.LOW)
edge = "a falling edge" if closing else "not a falling edge"
print(f"the float closing is {edge} on that line")

# Full, so the pump stops. Releasing the switch hands the line back, and the levels it was
# driven to are the whole conversation the board saw.
pump.set(False)
ran, stopped = pump.release().driven
print(f"running drove the line {ran.value} and stopping drove it {stopped.value}")

# A part on a shared bus answers to an address, and the byte on the wire is not the
# address the datasheet prints: it shifts up one and the low bit says read or write.
to_write = i2c.address_frame(0x76)[0]
to_read = i2c.address_frame(0x76, read=True)[0]
print(f"a part at 0x76 is written to as 0x{to_write:02X} and read from as 0x{to_read:02X}")

# Two ranges belong to the specification itself, so a part answering in either is a wiring
# mistake rather than a device.
reserved = i2c.is_reserved(i2c.RESERVED_FROM)
owner = "reserved by the specification" if reserved else "free for a device"
print(f"0x{i2c.RESERVED_FROM:02X} is {owner}")

# And a datasheet quotes SPI's clock polarity and phase as one mode number.
clock = spi.clock_for(3)
idle = "high" if clock.cpol else "low"
edge = "trailing" if clock.cpha else "leading"
print(f"SPI mode 3 idles {idle} and samples on the {edge} edge")

C##

Pamoja.Gpio has the same types. Switch.ActiveLow(line) and Contact.ActiveLow(line) infer the line's type, and a line is anything that implements IOutputLine or IInputLine: a GpioLine, a PinScript, or a class of your own over another library. A refusal throws PamojaException; opening a GpioLine anywhere but Linux throws PlatformNotSupportedException. A GpioLine is IDisposable, and disposing it hands the line back to the kernel.

From bindings/dotnet/samples/Pamoja.Guides/GpioGuide.cs:

C#
// Most relay boards energize when their input is pulled low, and a float switch
// wired to ground closes the same way. Saying "active low" once, here, is what
// keeps the inversion out of every line below it.
var pump = Switch.ActiveLow(new PinScript());
var floatSwitch = Contact.ActiveLow(new PinScript(PinLevel.High, PinLevel.Low));
PinLevel runsOn = Pin.LevelFor(pump.Polarity, true);
Console.WriteLine($"a pump on an active-low relay runs when its line is {runsOn}");

// The pump runs while the tank fills. The scripted line answers open and then
// closed, so this is the real loop with nothing plugged in; on a board the same two
// lines take a pin from the board's GPIO library instead.
pump.Set(true);
bool whileFilling = floatSwitch.IsAsserted();
bool onceFilled = floatSwitch.IsAsserted();
static string Full(bool closed) => closed ? "full" : "not full";
Console.WriteLine($"the float reads {Full(whileFilling)}, then {Full(onceFilled)}");

// The moment the float closes is that line going low, which is a falling edge. A
// watch armed for the rising one would sleep through the tank filling.
bool closing = Pin.Triggers(PinEdge.Falling, PinLevel.High, PinLevel.Low);
string edge = closing ? "a falling edge" : "not a falling edge";
Console.WriteLine($"the float closing is {edge} on that line");

// Full, so the pump stops. Releasing the switch hands the line back, and the levels
// it was driven to are the whole conversation the board saw.
pump.Set(false);
IReadOnlyList<PinLevel> driven = pump.Release().Driven;
(PinLevel ran, PinLevel stopped) = (driven[0], driven[1]);
Console.WriteLine($"running drove the line {ran} and stopping drove it {stopped}");

// A part on a shared bus answers to an address, and the byte on the wire is not
// the address the datasheet prints: it shifts up one and the low bit says read or
// write.
byte toWrite = I2c.AddressFrame(0x76)[0];
byte toRead = I2c.AddressFrame(0x76, read: true)[0];
Console.WriteLine(
    $"a part at 0x76 is written to as 0x{toWrite:X2} and read from as 0x{toRead:X2}");

// Two ranges belong to the specification itself, so a part answering in either is
// a wiring mistake rather than a device.
bool reserved = I2c.IsReserved(I2c.ReservedFrom);
string owner = reserved ? "reserved by the specification" : "free for a device";
Console.WriteLine($"0x{I2c.ReservedFrom:X2} is {owner}");

// And a datasheet quotes SPI's clock polarity and phase as one mode number.
SpiClock clock = Spi.ClockFor(3);
string idle = clock.Cpol ? "high" : "low";
string sampling = clock.Cpha ? "trailing" : "leading";
Console.WriteLine($"SPI mode 3 idles {idle} and samples on the {sampling} edge");

On a board#

On a Raspberry Pi, or any Linux board, a pin is a line on a GPIO chip, the device file /dev/gpiochip0 on most boards, and a line's number is the GPIO number a pinout gives, not the physical pin number. The program is the one above with its two scripted lines swapped for real ones:

PartLineHeader pinWiringPolarity
Relay board inputGPIO1711the board's input to the pin, its ground to a ground pinactive low
Float switchGPIO2713between the pin and ground, with a pull-up to 3.3 Vactive low

The relay line is opened high, so the pump stays off from the moment the program takes the line until it asks for it. The float needs a pull-up because a switch to ground only ever pulls the line down: a 10 kΩ resistor from the pin to 3.3 V, or gpio=27=ip,pu in config.txt, which sets the pull at boot. The pump stops whatever happens, including a float that never closes, since a pump left running is the fault the program exists to prevent.

Rust#

From examples/guides/gpio.rs:

Rust
/// The same pump and float on a Raspberry Pi: the relay board's input on GPIO17 and the
/// float switch between GPIO27 and ground. Only the two lines change.
fn on_a_board(chip: &str) -> std::result::Result<(), Box<dyn Error>> {
    use std::time::{Duration, Instant};

    use pamoja_gpio::linux;
    use pamoja_gpio::pin::Level;
    use pamoja_gpio::switch::{Contact, Switch};

    // The relay energizes on a low input, so its line is taken high and the pump stays off
    // until it is asked to run. The float closes to ground against a pull-up.
    let mut pump = Switch::active_low(linux::output(chip, 17, Level::High)?);
    let mut float = Contact::active_low(linux::input(chip, 27)?);

    // Run the pump until the float closes, and stop it whatever happens: a pump left
    // running on a failed float is the fault this whole program exists to prevent.
    pump.set(true)?;
    let deadline = Instant::now() + Duration::from_secs(600);
    let filled = (|| -> std::result::Result<(), Box<dyn Error>> {
        while !float.is_asserted()? {
            if Instant::now() >= deadline {
                return Err(
                    "the tank did not fill in ten minutes; check the float and the supply".into(),
                );
            }
            std::thread::sleep(Duration::from_millis(100));
        }
        Ok(())
    })();
    pump.set(false)?;
    filled?;
    println!("the tank is full and the pump is off");
    Ok(())
}

TypeScript#

From bindings/node/guides/gpio.ts:

TypeScript
// The same pump and float on a Raspberry Pi: the relay board's input on GPIO17 and the
// float switch between GPIO27 and ground. Only the two lines change.
async function onABoard(chip: string): Promise<void> {
  // The relay energizes on a low input, so its line is taken high and the pump stays off
  // until it is asked to run. The float closes to ground against a pull-up.
  const relay = GpioLine.openOutput(chip, 17, PinLevel.High)
  const floatLine = GpioLine.openInput(chip, 27)
  const pump = Switch.activeLow(relay)
  const float = Contact.activeLow(floatLine)

  // Run the pump until the float closes, and stop it whatever happens: a pump left running
  // on a failed float is the fault this whole program exists to prevent.
  const deadline = Date.now() + 10 * 60_000
  pump.set(true)
  try {
    while (!float.isAsserted()) {
      if (Date.now() >= deadline) {
        throw new Error('the tank did not fill in ten minutes; check the float and the supply')
      }
      await new Promise((resolve) => setTimeout(resolve, 100))
    }
  } finally {
    pump.set(false)
    relay.close()
    floatLine.close()
  }
  console.log('the tank is full and the pump is off')
}

Python#

From bindings/python/guides/gpio.py:

Python
def on_a_board(chip: str) -> None:
    """The same pump and float on a Raspberry Pi: the relay board's input on GPIO17 and
    the float switch between GPIO27 and ground. Only the two lines change."""
    # The relay energizes on a low input, so its line is taken high and the pump stays
    # off until it is asked to run. The float closes to ground against a pull-up.
    with (
        GpioLine.open_output(chip, 17, Level.HIGH) as relay,
        GpioLine.open_input(chip, 27) as float_line,
    ):
        pump = Switch.active_low(relay)
        float_switch = Contact.active_low(float_line)

        # Run the pump until the float closes, and stop it whatever happens: a pump left
        # running on a failed float is the fault this whole program exists to prevent.
        deadline = time.monotonic() + 10 * 60
        pump.set(True)
        try:
            while not float_switch.is_asserted():
                if time.monotonic() >= deadline:
                    raise TimeoutError(
                        "the tank did not fill in ten minutes; check the float and the supply"
                    )
                time.sleep(0.1)
        finally:
            pump.set(False)
    print("the tank is full and the pump is off")

C##

From bindings/dotnet/samples/Pamoja.Guides/GpioGuide.cs:

C#
/// <summary>
/// The same pump and float on a Raspberry Pi: the relay board's input on GPIO17 and the
/// float switch between GPIO27 and ground. Only the two lines change.
/// </summary>
/// <param name="chip">The GPIO chip's device file.</param>
public static void OnABoard(string chip)
{
    // The relay energizes on a low input, so its line is taken high and the pump stays
    // off until it is asked to run. The float closes to ground against a pull-up.
    using GpioLine relay = GpioLine.OpenOutput(chip, 17, PinLevel.High);
    using GpioLine floatLine = GpioLine.OpenInput(chip, 27);
    var pump = Switch.ActiveLow(relay);
    var floatSwitch = Contact.ActiveLow(floatLine);

    // Run the pump until the float closes, and stop it whatever happens: a pump left
    // running on a failed float is the fault this whole program exists to prevent.
    DateTime deadline = DateTime.UtcNow.AddMinutes(10);
    pump.Set(true);
    try
    {
        while (!floatSwitch.IsAsserted())
        {
            if (DateTime.UtcNow >= deadline)
            {
                throw new TimeoutException(
                    "the tank did not fill in ten minutes; check the float and the supply");
            }

            Thread.Sleep(100);
        }
    }
    finally
    {
        pump.Set(false);
    }

    Console.WriteLine("the tank is full and the pump is off");
}

A line is held by one program at a time, and the kernel records which, so gpioinfo shows every line with its holder; one opened here shows pamoja. On Raspberry Pi OS a user in the gpio group opens lines without root.

Values at a glance#

Polarity is the one fact that keeps an inversion out of every call site. Name it once, where the part is made:

PolarityAsserted byTurning it on drivesA switch is wiredTypical parts
Active higha high levelhighto the supply, with a pull-downan LED from the pin to ground
Active lowa low levellowto ground, with a pull-upmost relay boards, a button to ground

Edges name which change a watch fires on:

EdgeFires onFits
Risinglow to higha button to the supply being pressed
Fallinghigh to lowa switch to ground closing, such as the float filling
Botheither changea door that reports opening and closing

SPI modes are the clock polarity (CPOL, the level the clock idles at) and the clock phase (CPHA, which edge data is sampled on), quoted by a datasheet as one number. The LoRa radios on the hardware page take mode 0.

ModeCPOLCPHAClock idlesData sampled on
000lowthe rising edge, the first
101lowthe falling edge, the second
210highthe falling edge, the first
311highthe rising edge, the second

I2C addresses are seven bits, and the I2C-bus specification (NXP UM10204, Rev. 7.0, Table 4) keeps sixteen of them. is_reserved answers for the whole of both blocks, and is_general_call for 0x00:

7-bit addressWhat the specification uses it for
0x00the general call when writing, the START byte when reading
0x01the CBUS address
0x02reserved for a different bus format
0x03reserved for future purposes
0x04 to 0x07the Hs-mode controller code
0x08 to 0x77devices
0x78 to 0x7Bthe first byte of a 10-bit address
0x7C to 0x7Fthe device ID

On the wire a 7-bit address becomes one byte, the address shifted up one with the read bit at the bottom. A 10-bit address, 0x000 to 0x3FF, becomes two: 11110, the top two address bits and the read bit, then the low eight bits. The specification allows a reserved address to be given to a device on a bus that will never use it for its purpose, which is why the functions report reserved addresses rather than refusing them.

The same calls in each language:

ToRustTypeScriptPythonC#
drive a part on a lineSwitch::active_low(line), set(on)Switch.activeLow(line), set(on)Switch.active_low(line), set(on)Switch.ActiveLow(line), Set(on)
read a part on a lineContact::active_low(line), is_asserted()Contact.activeLow(line), isAsserted()Contact.active_low(line), is_asserted()Contact.ActiveLow(line), IsAsserted()
give a test a linePinScript::new(reads)new PinScript(reads)PinScript(reads)new PinScript(reads)
open a line on Linuxlinux::output(chip, n, level), linux::input(chip, n)GpioLine.openOutput(chip, n, level), GpioLine.openInput(chip, n)GpioLine.open_output(chip, n, level), GpioLine.open_input(chip, n)GpioLine.OpenOutput(chip, n, level), GpioLine.OpenInput(chip, n)
find the level for a statepolarity.level(on)pin.levelFor(polarity, on)pin.level_for(polarity, on)Pin.LevelFor(polarity, on)
find the state from a levelpolarity.is_asserted(level)pin.isAsserted(polarity, level)pin.is_asserted(polarity, level)Pin.IsAsserted(polarity, level)
ask whether an edge firesedge.triggered_by(from, to)pin.triggers(edge, from, to)pin.triggers(edge, from, to)Pin.Triggers(edge, from, to)
frame an I2C addressAddress::seven_bit(a)?.frame(direction)i2c.addressFrame(a, { read })i2c.address_frame(a, read=read)I2c.AddressFrame(a, read)
check an I2C addressaddress.is_reserved()i2c.isReserved(a)i2c.is_reserved(a)I2c.IsReserved(a)
read an SPI modeMode::Mode3.cpol_cpha()spi.clockFor(3)spi.clock_for(3)Spi.ClockFor(3)
name an SPI modeMode::from_cpol_cpha(cpol, cpha)spi.modeFor(cpol, cpha)spi.mode_for(cpol, cpha)Spi.ModeFor(cpol, cpha)

When it goes wrong#

What each call refuses, and how each language says so:

CallRefused whenRustTypeScriptPythonC#
an I2C addressa 7-bit address above 0x7F, a 10-bit one above 0x3FFErr(GpioError::AddressOutOfRange)throws Errorraises PamojaErrorthrows PamojaException
an SPI mode numberabove 3Mode::from_number returns Nonethrows Errorraises ValueErrorthrows PamojaException
opening a linethe platform is not LinuxErr(OpenError::Unsupported)throws Errorraises PamojaErrorthrows PlatformNotSupportedException
opening a linethe chip or the line cannot be openedErr(OpenError::Gpio { .. })throws Errorraises PamojaErrorthrows PamojaException
driving or reading a linethe kernel refuses, as it does a drive on an inputErr(LineError)throws Errorraises PamojaErrorthrows PamojaException

Every message from opening, driving, or reading a line starts with the chip and the line, as /dev/gpiochip0 line 17: , then says what the kernel reported.

The mistakes that cost an afternoon:

  • The relay clicks on when the program starts. It is active low and its line started low. Open it high, as the examples do, and to hold it off while the board boots, before any program runs, add gpio=17=op,dh to config.txt.
  • A switch reads at random, or never changes. The line has no pull, so an open switch floats. A switch to ground needs a pull-up: a resistor to 3.3 V, or gpio=27=ip,pu in config.txt.
  • Opening a line says permission denied. The user is not in the gpio group. Add it with sudo usermod -aG gpio $USER, then log out and back in.
  • Opening a line says it is busy. Another program or a kernel driver holds it; gpioinfo names the holder.
  • Opening says the chip does not exist, or the line is out of range. gpiodetect lists the chips, and gpioinfo names each line: the chip whose lines are named GPIO17 and GPIO27 is the header.
  • An I2C part does not answer at the address its datasheet prints. Some datasheets print the address already shifted, the way it appears on the wire: 0xEC and 0xED are the part at 0x76. i2cdetect -y 1 lists the 7-bit addresses that answer on a Raspberry Pi's user bus.
  • An SPI part answers 0x00, 0xFF, or noise. Check the mode against the datasheet's CPOL and CPHA, then the chip select.

Where next#

Reference#

Edit this page on GitHub