#i2c-bus #i2c #adafruit #micro-controller #seesaw #shared-bus

nightly no-std adafruit-seesaw

Platform-agnostic implementation for communicating with devices that run the Seesaw firmware

21 releases (11 breaking)

0.11.0 Feb 7, 2025
0.9.0 Jan 31, 2025
0.6.1 Jan 30, 2024
0.5.3 Nov 3, 2023
0.0.3 Jul 13, 2022

#78 in Embedded development

Download history 2/week @ 2024-10-30 1/week @ 2024-11-06 1/week @ 2024-11-13 1/week @ 2024-11-20 144/week @ 2025-01-15 266/week @ 2025-01-29 303/week @ 2025-02-05

713 downloads per month

MIT/Apache

280KB
1K SLoC

Adafruit Seesaw Logo

crates.io page docs.rs CI

Introduction

What is Seesaw? From Adafruit's guide: "Adafruit seesaw is a near-universal converter framework which allows you to add and extend hardware support to any I2C-capable microcontroller or microcomputer. Instead of getting separate I2C GPIO expanders, ADCs, PWM drivers, etc, seesaw can be configured to give a wide range of capabilities."

This crate aims to be a functionally-equivalent Rust driver for Adafruit's own C++ driver.

A note on terminology: Adafruit's Seesaw firmware refers to blocks of device functionality/capabilities as "modules". e.g. a Seesaw device that has controllable neopixels will have the GPIO module loaded and available in its firmware. Don't confuse them with Rust modules! This crate exports a module called modules that contains all the modules for the Seesaw devices.

If you want to learn more about modules, this page in the Seesaw guide explains it pretty well.

Usage

The library follows the patterns of the shared-bus library so that multiple devices can be connected and communicated with without owning the I2C bus.

Communicating with Seesaw devices requires a bus that implements both I2C traits and Delay from embedded-hal.

#![no_std] (single-threaded)

If you're communicating with devices within a single thread, use the SeesawRefCell typed struct, which uses the RefCellBus wrapper to enable sharing of the bus across multiple Seesaw devices.

// Setup on an STM32F405
let cp = cortex_m::Peripherals::take().unwrap();
let clocks = dp.RCC.constrain().cfgr.freeze();
let delay = cp.SYST.delay(&clocks);
let i2c = I2c::new(dp.I2C1, (scl, sda), 400.kHz(), &clocks);
let seesaw = SeesawRefCell::new(delay, i2c);
let mut neokeys = NeoKey1x4::new_with_default_addr(seesaw.acquire_driver())
    .init()
    .expect("Failed to start NeoKey1x4");

std (multi-threaded)

This requires turning on the std feature flag.

For multi-threaded purposes, use the SeesawStdMutex typed struct, which wraps the bus in a std Mutex.

Example usage of using multi-threaded Seesaw in a std context, running on an ESP32-S3:

use adafruit_seesaw::{devices::RotaryEncoder, prelude::*, SeesawStdMutex};
use esp_idf_hal::{
    self,
    delay::Delay,
    gpio::PinDriver,
    i2c::{I2cConfig, I2cDriver},
    peripherals::Peripherals,
    prelude::*,
};
use std::time::Duration;

fn main() -> Result<(), anyhow::Error> {
    esp_idf_hal::sys::link_patches();
    esp_idf_svc::log::EspLogger::initialize_default();

    // System
    let peripherals = Peripherals::take().unwrap();
    let mut i2c_power = PinDriver::output(peripherals.pins.gpio7).unwrap();
    i2c_power.set_low()?;
    std::thread::sleep(Duration::from_millis(333));

    // I2C
    let (sda, scl) = (peripherals.pins.gpio3, peripherals.pins.gpio4);
    let config = I2cConfig::new().baudrate(400.kHz().into());
    let i2c = I2cDriver::new(peripherals.i2c0, sda, scl, &config)?;
    i2c_power.set_high()?;
    std::thread::sleep(Duration::from_millis(333));

    let seesaw: &'static _ = {
        use once_cell::sync::OnceCell;
        static MANAGER: OnceCell<SeesawStdMutex<(Delay, I2cDriver<'_>)>> =
            OnceCell::new();

        match MANAGER.set(SeesawStdMutex::new(Delay::new_default(), i2c)) {
            Ok(_) => MANAGER.get(),
            Err(_) => None,
        }
    }
    .unwrap();

    let _encoder =
        RotaryEncoder::new_with_default_addr(seesaw.acquire_driver())
            .init()
            .expect("Failed to start rotary encoder.");

    loop {
        // Do stuff with rotary encoder
    }
}

Communicating with a Device

At a minimum, a device implements the SeesawDevice trait which specifies a common constructor function, along with lots of other device-specific information specified as const values:

Product value Const method on all SeesawDevices Notes
Default Address Device::default_addr()
Hardware ID Device::hardware_id() This value depends on the host MCU of the device
Product ID Device::product_id() You can use this value to go to the product page at adafruit.com/product/$product_id

Let's talk to a NeoKey1x4 using the seesaw manager we created above.

Using the default address

let neokeys = NeoKey1x4::new_with_default_addr(seesaw.acquire_driver());

Using a custom address

let neokeys = NeoKey1x4::new(0x00, seesaw.acquire_driver());

Initializing

Devices that implement SeesawDevice also implmement SeesawDeviceInit, which defines a device-specific init function for setting up a device's hardware functionality. The intention is to run a set of sensible defaults so you don't have to remember to do it yourself.

let neokeys = NeoKey1x4::new_with_default_addr(seesaw.acquire_driver())
    .init()
    .expect("Failed to initialize NeoKey1x4");

For instance, the init function for our Neokey1x4 does the following:

  • Resets the device
  • Reads & verifies the device hardware ID
  • Enables the on-device neopixels
  • Enables the on-device buttons

Calling init is of course optional, but without it you'll have to handle initialization yourself.

Predefined Devices

The crate comes with a few predefined devices that you can use. Their documentation is available here.

Device Product ID MCU Notes
ArcadeButton1x4 5296 ATTiny8x7
NeoKey1x4 4980 SAMD09
NeoSlider 5295 ATTiny8x7
NeoTrellis 3954 SAMD09 Example demo video neotrellis_ripples.rs
NeoRotary4 5752 ATTiny8x7
RotaryEncoder 4991 SAMD09

Creating Your Own Devices

So far, this library only implements a few Seesaw devices (i.e., the ones that I currently own). You can define your own device using the seesaw_device! macro and then configuring its modules using their respective traits.

Let's assume you have some future Adafruit Neokey-esque device that has 6 buttons and 6 neopixels.

You call the seesaw_device! macro with information about the device:

seesaw_device! {
    name: Neokey2x3,
    hardware_id: HardwareId::_,
    product_id: _,
    default_addr: _
}

Then implement the module traits for its various capabilities:

impl<D: Driver> GpioModule<D> for Neokey2x3<D> {}
impl<D: Driver> NeopixelModule<D> for Neokey2x3<D> {
    type Color = Neokey2x3Color;

    const N_LEDS: usize = 6;
    const PIN: u8 = _;
}

The last thing you might want to do is implmeent the SeesawDeviceInit trait to handle the device intialization:

impl<D: Driver> SeesawDeviceInit<D> for Neokey2x3<D> {
    fn init(mut self) -> Result<Self, Self::Error> {
        self.reset_and_verify_seesaw()
            .and_then(|_| self.enable_neopixel())
            .and_then(|_| self.enable_button_pins())
            .map(|_| self)
    }
}

Now you can use the new device as you would any other:

let neokeys = NeoKey2x3::new_with_default_addr(seesaw.acquire_driver())
    .init()
    .expect("Failed to initialize NeoKey2x3");

Implementation Progress

Seesaw Module Implemented
ADC
EEPROM ⬜️
Encoder
GPIO
Keypad
Neopixel
Sercom0 ⬜️
Spectrum ⬜️
Status
Timer
Touch ⬜️

Known Issues

These issues are based solely on my own experience testing this crate on my own devices. Any confirmation of similar or contrary experience is much appreciated.

.version() returns an incorrect product ID for some devices

The .version() function--a function of the Status module--returns incorrect dates and product IDs for some devices. I'm not too concerned about the date, but product ID matters in the case of identifying a device.

Works for Returns
NeoKey1x4 SeesawVersion { id: 4980, year: 2036, month: 5, day: 5 }
NeoRotary4 SeesawVersion { id: 5752, year: 2023, month: 6, day: 27 }
NeoSlider SeesawVersion { id: 5295, year: 2021, month: 11, day: 16 }
RotaryEncoder SeesawVersion { id: 4991, year: 2035, month: 5, day: 5 }
Does not work for Returns Should be
NeoTrellis SeesawVersion { id: 0, year: 2050, month: 10, day: 4 } 3954
Neopixel Driver SeesawVersion { id: 5742, year: 2023, month: 5, day: 20 } 5766

License

adafruit-seesaw is licensed under either of

at your option.


Not affiliated with, nor officially supported by Adafruit.

Dependencies