diff --git a/README.md b/README.md
index 4207036bf9..4d16704806 100644
--- a/README.md
+++ b/README.md
@@ -1009,53 +1009,59 @@ your specific needs.
LSM303A |
LSM6DS33 |
LSM6DSO |
+LTC2497 |
+LTC2499 |
LTC2984 |
+
+| MAX14661 |
MAX31855 |
MAX31865 |
-
| MAX6966 |
MAX7219 |
+MCP23X08 |
+
| MCP23x17 |
MCP2515 |
MCP3008 |
MCP7941x |
-
| MCP990X |
MMC5603 |
+
| MS5611 |
MS5837 |
NOKIA5110 |
NRF24 |
-
| TFT-DISPLAY |
PAT9125EL |
+
| PCA8574 |
PCA9535 |
PCA9548A |
PCA9685 |
-
+| PCAL6524 |
QMC5883L |
+
| SH1106 |
SIEMENS-S65 |
SIEMENS-S75 |
SK6812 |
SK9822 |
-
| SSD1306 |
+
| ST7586S |
ST7789 |
STTS22H |
STUSB4500 |
SX1276 |
-
| SX128X |
+
| TCS3414 |
TCS3472 |
TLC594x |
TMP102 |
TMP12x |
-
| TMP175 |
+
| TOUCH2046 |
VL53L0 |
VL6180 |
diff --git a/src/modm/driver/adc/ltc2497.hpp b/src/modm/driver/adc/ltc2497.hpp
new file mode 100644
index 0000000000..66b5cb80b7
--- /dev/null
+++ b/src/modm/driver/adc/ltc2497.hpp
@@ -0,0 +1,206 @@
+// coding: utf-8
+/*
+ * Copyright (c) 2026, Niklas Hauser
+ *
+ * This file is part of the modm project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+// ----------------------------------------------------------------------------
+
+#pragma once
+
+#include
+#include
+#include
+#include
+
+namespace modm
+{
+
+/// @ingroup modm_driver_ltc2497
+struct ltc2497
+{
+ /// Three-state wiring option of one address pin (CA0, CA1 or CA2).
+ enum class
+ AddressPin : uint8_t
+ {
+ Low = 0,
+ High = 1,
+ Float = 2,
+ };
+
+ /// The 27 pin-selectable 7-bit addresses.
+ static constexpr uint8_t
+ address(AddressPin ca2 = AddressPin::Low, AddressPin ca1 = AddressPin::Low, AddressPin ca0 = AddressPin::Low)
+ {
+ constexpr uint8_t table[3][3][3]
+ {
+ // ca1 = Low ca1 = High ca1 = Float
+ { {0x14, 0x16, 0x15}, {0x26, 0x34, 0x27}, {0x17, 0x25, 0x24} }, // ca2 = Low
+ { {0x56, 0x64, 0x57}, {0x74, 0x76, 0x75}, {0x65, 0x67, 0x66} }, // ca2 = High
+ { {0x35, 0x37, 0x36}, {0x47, 0x55, 0x54}, {0x44, 0x46, 0x45} }, // ca2 = Float
+ };
+ return table[uint8_t(ca2)][uint8_t(ca1)][uint8_t(ca0)];
+ }
+
+ /// Reserved general call address to synchronize the conversion start of
+ /// multiple LTC24xx delta-sigma devices on the same bus (write-only).
+ static constexpr uint8_t GlobalAddress = 0x77;
+
+ /// Worst-case (max) conversion time: simultaneous 50Hz/60Hz rejection with
+ /// auto-calibration.
+ static constexpr std::chrono::milliseconds ConversionTime{150};
+
+ enum class
+ InputChannel : uint8_t
+ {
+ // Differential inputs
+ Ch0Ch1 = 0xA0,
+ Ch1Ch0 = 0xA8,
+ Ch2Ch3 = 0xA1,
+ Ch3Ch2 = 0xA9,
+ Ch4Ch5 = 0xA2,
+ Ch5Ch4 = 0xAA,
+ Ch6Ch7 = 0xA3,
+ Ch7Ch6 = 0xAB,
+ Ch8Ch9 = 0xA4,
+ Ch9Ch8 = 0xAC,
+ Ch10Ch11 = 0xA5,
+ Ch11Ch10 = 0xAD,
+ Ch12Ch13 = 0xA6,
+ Ch13Ch12 = 0xAE,
+ Ch14Ch15 = 0xA7,
+ Ch15Ch14 = 0xAF,
+
+ // Single-ended inputs
+ Ch0 = 0xB0,
+ Ch1 = 0xB8,
+ Ch2 = 0xB1,
+ Ch3 = 0xB9,
+ Ch4 = 0xB2,
+ Ch5 = 0xBA,
+ Ch6 = 0xB3,
+ Ch7 = 0xBB,
+ Ch8 = 0xB4,
+ Ch9 = 0xBC,
+ Ch10 = 0xB5,
+ Ch11 = 0xBD,
+ Ch12 = 0xB6,
+ Ch13 = 0xBE,
+ Ch14 = 0xB7,
+ Ch15 = 0xBF,
+ };
+
+ static constexpr InputChannel
+ channel(uint8_t channel)
+ {
+ return InputChannel(0xB0 | ((channel & 1) << 3) | ((channel & 0xF) >> 1));
+ }
+
+ // Data output is 24 bits: SIG, MSB, a 16-bit two's complement result and
+ // 6 bits that are always 0 (the LTC2499 outputs 32 bits instead: SIG,
+ // MSB, a 24-bit result and 6 meaningful sub-LSBs). SIG+MSB act as two
+ // extra range bits on top of the 16-bit result, so the mid-scale offset
+ // is 1<<17, see Table 1 of the datasheet.
+ struct modm_packed
+ Data
+ {
+ template
+ friend class Ltc2497;
+
+ constexpr uint32_t
+ getRawValue() const
+ {
+ return (uint32_t(data[0]) << 16) | (uint32_t(data[1]) << 8) | data[2];
+ }
+
+ constexpr int32_t
+ getValue() const
+ {
+ return static_cast(getRawValue() >> 6) - (1 << 17);
+ }
+
+ constexpr float
+ getVoltage(float vref) const
+ {
+ if (getRawValue() == 0xC00000) return std::numeric_limits::infinity();
+ if (getRawValue() == 0x3FFFC0) return -std::numeric_limits::infinity();
+ return static_cast(getValue()) * vref / static_cast(1 << 17);
+ }
+
+ protected:
+ uint8_t data[3];
+ };
+};
+
+/**
+ * @tparam I2cMaster I2cMaster interface
+ *
+ * The LTC2497 continuously converts. A write selects the mux for the next
+ * conversion, while a read returns the previous result and starts a new one.
+ * Since a read also starts a new conversion, every method below waits for
+ * `tmr` (armed by whichever call started the conversion currently in
+ * progress) before issuing its own I2C transaction, and rearms it for
+ * `ConversionTime` again afterwards.
+ */
+template
+class Ltc2497 : public ltc2497, public modm::I2cDevice
+{
+public:
+ inline Ltc2497(Data &data, uint8_t address = ltc2497::address()) :
+ modm::I2cDevice(address), data(data) {}
+
+ /// Selects the mux for the next conversion and starts it.
+ bool inline
+ startMeasurement(InputChannel channel)
+ {
+ tmr.wait();
+ const uint8_t command = static_cast(channel);
+ const bool success = this->write(&command, 1);
+ if (success) tmr.restart(ConversionTime);
+ return success;
+ }
+
+ /// Reads the latest conversion result and restarts conversion on the current mux.
+ bool inline
+ readConversionResult()
+ {
+ tmr.wait();
+ const bool success = I2cDevice::read(data.data, 3);
+ if (success) tmr.restart(ConversionTime);
+ return success;
+ }
+
+ /// Reads the latest conversion result and selects the next mux in one transfer.
+ bool inline
+ readConversionResult(InputChannel nextInput)
+ {
+ tmr.wait();
+ const uint8_t command = static_cast(nextInput);
+ const bool success = this->writeRead(&command, 1, data.data, 3);
+ if (success) tmr.restart(ConversionTime);
+ return success;
+ }
+
+ bool inline
+ read(InputChannel channel)
+ {
+ if (not startMeasurement(channel)) return false;
+ return readConversionResult();
+ }
+
+ inline Data &
+ getData()
+ {
+ return data;
+ }
+
+private:
+ Data &data;
+ modm::ShortTimeout tmr;
+};
+
+} // modm namespace
diff --git a/src/modm/driver/adc/ltc2497.lb b/src/modm/driver/adc/ltc2497.lb
new file mode 100644
index 0000000000..ad2d7c91b1
--- /dev/null
+++ b/src/modm/driver/adc/ltc2497.lb
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2026, Niklas Hauser
+#
+# This file is part of the modm project.
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# -----------------------------------------------------------------------------
+
+
+def init(module):
+ module.name = ":driver:ltc2497"
+ module.description = """
+# LTC2497 ADC
+
+The LTC2497 is a 16-channel (eight differential), 16-bit delta-sigma ADC with
+Easy Drive technology and a 2-wire, I2C interface.
+
+Note that the sampling time is very long (175ms per channel), so this ADC is
+not suitable for fast sampling. You should change the channel while reading the
+previous channel to not waste additional time.
+"""
+
+def prepare(module, options):
+ module.depends(
+ ":architecture:i2c.device",
+ ":processing:timer")
+ return True
+
+def build(env):
+ env.outbasepath = "modm/src/modm/driver/adc"
+ env.copy("ltc2497.hpp")
diff --git a/src/modm/driver/adc/ltc2499.hpp b/src/modm/driver/adc/ltc2499.hpp
new file mode 100644
index 0000000000..e755c7da0e
--- /dev/null
+++ b/src/modm/driver/adc/ltc2499.hpp
@@ -0,0 +1,326 @@
+// coding: utf-8
+/*
+ * Copyright (c) 2026, Niklas Hauser
+ *
+ * This file is part of the modm project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+// ----------------------------------------------------------------------------
+
+#pragma once
+
+#include
+#include
+#include
+#include
+
+namespace modm
+{
+
+/// @ingroup modm_driver_ltc2499
+struct ltc2499
+{
+ /// Three-state wiring option of one address pin (CA0, CA1 or CA2).
+ enum class
+ AddressPin : uint8_t
+ {
+ Low = 0,
+ High = 1,
+ Float = 2,
+ };
+
+ /// The 27 pin-selectable 7bit addresses.
+ static constexpr uint8_t
+ address(AddressPin ca2 = AddressPin::Low, AddressPin ca1 = AddressPin::Low, AddressPin ca0 = AddressPin::Low)
+ {
+ constexpr uint8_t table[3][3][3]
+ {
+ // ca1 = Low ca1 = High ca1 = Float
+ { {0x14, 0x16, 0x15}, {0x26, 0x34, 0x27}, {0x17, 0x25, 0x24} }, // ca2 = Low
+ { {0x56, 0x64, 0x57}, {0x74, 0x76, 0x75}, {0x65, 0x67, 0x66} }, // ca2 = High
+ { {0x35, 0x37, 0x36}, {0x47, 0x55, 0x54}, {0x44, 0x46, 0x45} }, // ca2 = Float
+ };
+ return table[uint8_t(ca2)][uint8_t(ca1)][uint8_t(ca0)];
+ }
+
+ /// Reserved general call address to synchronize the conversion start of
+ /// multiple LTC24xx delta-sigma devices on the same bus (write-only).
+ static constexpr uint8_t GlobalAddress = 0x77;
+
+ /// Line frequency rejection mode (FA, FB config bits). Simultaneous
+ /// rejects both 50Hz and 60Hz by at least 87dB; the single-frequency
+ /// modes reject their target frequency by at least 110dB but are not
+ /// as robust against the other line frequency.
+ enum class
+ RejectionMode : uint8_t
+ {
+ Simultaneous50Hz60Hz = 0,
+ Hz50 = 1,
+ Hz60 = 2,
+ };
+
+ /// Output data rate (SPD config bit). 2x speed disables the
+ /// auto-calibration that removes offset and drift every conversion,
+ /// in exchange for roughly double the output rate. This is ignored
+ /// (always 1x) for temperature measurements.
+ enum class
+ SpeedMode : uint8_t
+ {
+ Speed1x = 0,
+ Speed2x = 1,
+ };
+
+ /// Worst-case (max) conversion time for a given rejection/speed
+ /// configuration, see tCONV_1/tCONV_2 in the datasheet.
+ static constexpr std::chrono::milliseconds
+ conversionTime(RejectionMode rejection, SpeedMode speed)
+ {
+ // indexed [speed][Simultaneous50Hz60Hz, Hz50, Hz60]
+ constexpr uint32_t ms[2][3]
+ {
+ {150, 164, 137}, // 1x
+ { 76, 82, 67}, // 2x
+ };
+ return std::chrono::milliseconds{ms[speed == SpeedMode::Speed2x ? 1 : 0][uint8_t(rejection)]};
+ }
+
+ enum class
+ InputChannel : uint8_t
+ {
+ // Differential inputs
+ Ch0Ch1 = 0xA0,
+ Ch1Ch0 = 0xA8,
+ Ch2Ch3 = 0xA1,
+ Ch3Ch2 = 0xA9,
+ Ch4Ch5 = 0xA2,
+ Ch5Ch4 = 0xAA,
+ Ch6Ch7 = 0xA3,
+ Ch7Ch6 = 0xAB,
+ Ch8Ch9 = 0xA4,
+ Ch9Ch8 = 0xAC,
+ Ch10Ch11 = 0xA5,
+ Ch11Ch10 = 0xAD,
+ Ch12Ch13 = 0xA6,
+ Ch13Ch12 = 0xAE,
+ Ch14Ch15 = 0xA7,
+ Ch15Ch14 = 0xAF,
+
+ // Single-ended inputs
+ Ch0 = 0xB0,
+ Ch1 = 0xB8,
+ Ch2 = 0xB1,
+ Ch3 = 0xB9,
+ Ch4 = 0xB2,
+ Ch5 = 0xBA,
+ Ch6 = 0xB3,
+ Ch7 = 0xBB,
+ Ch8 = 0xB4,
+ Ch9 = 0xBC,
+ Ch10 = 0xB5,
+ Ch11 = 0xBD,
+ Ch12 = 0xB6,
+ Ch13 = 0xBE,
+ Ch14 = 0xB7,
+ Ch15 = 0xBF,
+ };
+
+ static constexpr InputChannel
+ channel(uint8_t channel)
+ {
+ return InputChannel(0xB0 | ((channel & 1) << 3) | ((channel & 0xF) >> 1));
+ }
+
+ // Data output is 32 bits: SIG, MSB, a 24-bit two's complement result and
+ // 6 sub-LSBs. SIG+MSB act as two extra range bits on top of the 24-bit
+ // result, so the mid-scale offset is 1<<25.
+ struct modm_packed
+ Data
+ {
+ template
+ friend class Ltc2499;
+
+ constexpr uint32_t
+ getRawValue() const
+ {
+ return (uint32_t(data[0]) << 24) | (uint32_t(data[1]) << 16) |
+ (uint32_t(data[2]) << 8) | data[3];
+ }
+
+ constexpr int32_t
+ getValue() const
+ {
+ return static_cast(getRawValue() >> 6) - (1 << 25);
+ }
+
+ constexpr float
+ getVoltage(float vref) const
+ {
+ if (getRawValue() == 0xC0000000) return std::numeric_limits::infinity();
+ if (getRawValue() == 0x3FFFFFC0) return -std::numeric_limits::infinity();
+ return static_cast(getValue()) * vref / static_cast(1 << 25);
+ }
+
+ /// The raw 24-bit result field (DATAOUT24 in the datasheet), used by
+ /// the temperature formulas below. Unlike getValue(), this is not
+ /// converted from offset-binary to a signed value, since the PTAT
+ /// signal is always a small positive fraction of VREF.
+ constexpr uint32_t
+ getRawResult24() const
+ { return (getRawValue() >> 6) & 0xFFFFFF; }
+
+ /// Converts a temperature-sensor reading (see `Ltc2499::readTemperature()`)
+ /// to Kelvin, given the reference voltage.
+ constexpr float
+ getTemperatureKelvin(float vref) const
+ { return static_cast(getRawResult24()) * vref / 1570.0f; }
+
+ /// Converts a temperature-sensor reading (see `Ltc2499::readTemperature()`)
+ /// to degrees Celsius, given the reference voltage.
+ constexpr float
+ getTemperatureCelsius(float vref) const
+ { return getTemperatureKelvin(vref) - 273.0f; }
+
+ protected:
+ uint8_t data[4];
+ };
+
+protected:
+ /// @cond
+ /// Builds the second (configuration) input byte: EN2, IM, FA, FB, SPD.
+ static constexpr uint8_t
+ configByte(RejectionMode rejection, SpeedMode speed, bool measureTemperature = false)
+ {
+ constexpr uint8_t rejectionBits[3] { 0x00, 0x10, 0x20 }; // Simultaneous50Hz60Hz, Hz50 (FB), Hz60 (FA)
+ return 0x80 // EN2
+ | (measureTemperature ? 0x40 : 0x00) // IM
+ | rejectionBits[uint8_t(rejection)]
+ | (speed == SpeedMode::Speed2x ? 0x08 : 0x00); // SPD
+ }
+
+ /// First input byte that keeps the previously selected channel instead
+ /// of selecting a new one (preamble "10", EN = 0), used when only the
+ /// configuration (e.g. for a temperature reading) needs to be updated.
+ static constexpr uint8_t KeepChannel = 0x80;
+ /// @endcond
+};
+
+/**
+ * @tparam I2cMaster I2cMaster interface
+ *
+ * The LTC2499 continuously converts. A write selects the mux for the next
+ * conversion, while a read returns the previous result and starts a new one.
+ *
+ * This driver exposes a rejection mode (50Hz, 60Hz or simultaneous), a 1x/2x
+ * speed mode (2x roughly doubles the output rate by disabling the offset
+ * auto-calibration) and the integrated temperature sensor.
+ * Every `startMeasurement()`/`read()` call always sends both configuration
+ * bytes, so the device configuration never depends on hidden prior state.
+ *
+ * Since a read also starts a new conversion, every method below waits for
+ * `tmr` (armed by whichever call started the conversion currently in
+ * progress, for however long that specific rejection/speed mode takes) before
+ * issuing its own I2C transaction, and rearms it afterwards.
+ */
+template
+class Ltc2499 : public ltc2499, public modm::I2cDevice
+{
+public:
+ inline Ltc2499(Data &data, uint8_t address = ltc2499::address()) :
+ modm::I2cDevice(address), data(data) {}
+
+ /// Selects the mux and the rejection/speed mode for the next conversion, and starts it.
+ bool inline
+ startMeasurement(InputChannel channel, RejectionMode rejection = RejectionMode::Simultaneous50Hz60Hz,
+ SpeedMode speed = SpeedMode::Speed1x)
+ {
+ tmr.wait();
+ const uint8_t command[2] { uint8_t(channel), configByte(rejection, speed) };
+ const bool success = this->write(command, 2);
+ if (success) rearm(rejection, speed);
+ return success;
+ }
+
+ /// Selects the internal temperature sensor and rejection mode for the
+ /// next conversion, and starts it. Always uses 1x speed, as required
+ /// by the datasheet.
+ bool inline
+ startTemperatureMeasurement(RejectionMode rejection = RejectionMode::Simultaneous50Hz60Hz)
+ {
+ tmr.wait();
+ const uint8_t command[2] { KeepChannel, configByte(rejection, SpeedMode::Speed1x, /*measureTemperature=*/true) };
+ const bool success = this->write(command, 2);
+ if (success) rearm(rejection, SpeedMode::Speed1x);
+ return success;
+ }
+
+ /// Reads the latest conversion result and restarts conversion with the current configuration.
+ bool inline
+ readConversionResult()
+ {
+ tmr.wait();
+ const bool success = I2cDevice::read(data.data, 4);
+ if (success) tmr.restart(currentConversionTime);
+ return success;
+ }
+
+ /// Reads the latest conversion result and selects the next mux and
+ /// rejection/speed mode in one transfer.
+ bool inline
+ readConversionResult(InputChannel nextInput, RejectionMode rejection = RejectionMode::Simultaneous50Hz60Hz,
+ SpeedMode speed = SpeedMode::Speed1x)
+ {
+ tmr.wait();
+ const uint8_t command[2] { uint8_t(nextInput), configByte(rejection, speed) };
+ const bool success = this->writeRead(command, 2, data.data, 4);
+ if (success) rearm(rejection, speed);
+ return success;
+ }
+
+ /// Selects the mux/rejection/speed mode, waits for the conversion to
+ /// finish (blocking) and reads the result.
+ bool inline
+ read(InputChannel channel, RejectionMode rejection = RejectionMode::Simultaneous50Hz60Hz,
+ SpeedMode speed = SpeedMode::Speed1x)
+ {
+ if (not startMeasurement(channel, rejection, speed)) return false;
+ return readConversionResult();
+ }
+
+ /// Selects the internal temperature sensor, waits for the conversion to
+ /// finish (blocking) and reads the result. Use `Data::getTemperatureKelvin()`
+ /// / `Data::getTemperatureCelsius()` to interpret the result.
+ bool inline
+ readTemperature(RejectionMode rejection = RejectionMode::Simultaneous50Hz60Hz)
+ {
+ if (not startTemperatureMeasurement(rejection)) return false;
+ return readConversionResult();
+ }
+
+ inline Data &
+ getData()
+ {
+ return data;
+ }
+
+private:
+ void inline
+ rearm(RejectionMode rejection, SpeedMode speed)
+ {
+ currentConversionTime = conversionTime(rejection, speed);
+ tmr.restart(currentConversionTime);
+ }
+
+ Data &data;
+ modm::ShortTimeout tmr;
+ // the conversion time of whichever rejection/speed mode was configured
+ // by the last successful startMeasurement()/startTemperatureMeasurement(),
+ // used to rearm `tmr` after a plain readConversionResult() that doesn't
+ // itself know the current configuration
+ std::chrono::milliseconds currentConversionTime{
+ conversionTime(RejectionMode::Simultaneous50Hz60Hz, SpeedMode::Speed1x)};
+};
+
+} // modm namespace
diff --git a/src/modm/driver/adc/ltc2499.lb b/src/modm/driver/adc/ltc2499.lb
new file mode 100644
index 0000000000..19c02e54d2
--- /dev/null
+++ b/src/modm/driver/adc/ltc2499.lb
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2026, Niklas Hauser
+#
+# This file is part of the modm project.
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# -----------------------------------------------------------------------------
+
+
+def init(module):
+ module.name = ":driver:ltc2499"
+ module.description = """
+# LTC2499 ADC
+
+The LTC2499 is a 16-channel (eight differential), 24-bit delta-sigma ADC with
+Easy Drive technology, an integrated temperature sensor, and a 2-wire, I2C
+interface. It is the 24-bit, pin-compatible sibling of the LTC2497.
+"""
+
+def prepare(module, options):
+ module.depends(
+ ":architecture:i2c.device",
+ ":processing:timer")
+ return True
+
+def build(env):
+ env.outbasepath = "modm/src/modm/driver/adc"
+ env.copy("ltc2499.hpp")
diff --git a/src/modm/driver/gpio/mcp23s08.hpp b/src/modm/driver/gpio/mcp23s08.hpp
deleted file mode 100644
index e455be2549..0000000000
--- a/src/modm/driver/gpio/mcp23s08.hpp
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright (c) 2009, Thorsten Lajewski
- * Copyright (c) 2009-2012, Fabian Greif
- * Copyright (c) 2012-2014, Niklas Hauser
- *
- * This file is part of the modm project.
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/.
- */
-// ----------------------------------------------------------------------------
-
-#ifndef MODM_MCP23S08_HPP
-#define MODM_MCP23S08_HPP
-
-#include
-#include
-#include
-
-namespace modm
-{
- /**
- * \brief 8-Bit I/O Expander with Serial Interface
- *
- * A1 and A0 need to be tided low.
- *
- * \author Fabian Greif
- * \ingroup modm_driver_mcp23s08
- */
- template
- class Mcp23s08
- {
- public:
- static void
- initialize();
-
- /**
- * @brief Configure pins
- *
- * @param inputMask 1=Input, 0=Output
- * @param pullupMask 1=Pullup, 0=Pullup-Disabled
- */
- static void
- configure(uint8_t inputMask, uint8_t pullupMask);
-
- //void
- //configureInterrupt();
-
- static uint8_t
- read();
-
- static void
- write(uint8_t output);
-
- protected:
- enum RegisterAddress
- {
- MCP_IODIR = 0x00, ///< Port direction (1=input, 0=output)
- MCP_IPOL = 0x01, ///< Invert polarity
- MCP_GPINTEN = 0x02, ///< Enable interrupt
- MCP_DEFVAL = 0x03, ///< Compare register for interrupt
- MCP_INTCON = 0x04,
- MCP_IOCON = 0x05, ///< Configuration
- MCP_GPPU = 0x06, ///< Enable pullups
- MCP_INTF = 0x07, ///< Interrupt flag register
- MCP_INTCAP = 0x08, ///< Interrupt capture register
- MCP_GPIO = 0x09, ///< Port values
- MCP_OLAT = 0x0a ///< Output latch register
- };
-
- enum RW
- {
- WRITE = 0,
- READ = 1
- };
-
- static const uint8_t deviceAddress = 0x40;
-
- static Spi spi;
- static Cs cs;
- static Int interrupt;
- };
-}
-
-#include "mcp23s08_impl.hpp"
-
-#endif // MODM_MCP23S08_HPP
diff --git a/src/modm/driver/gpio/mcp23s08_impl.hpp b/src/modm/driver/gpio/mcp23s08_impl.hpp
deleted file mode 100644
index b07319bd3d..0000000000
--- a/src/modm/driver/gpio/mcp23s08_impl.hpp
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright (c) 2009, Martin Rosekeit
- * Copyright (c) 2009-2011, Fabian Greif
- * Copyright (c) 2012, Niklas Hauser
- * Copyright (c) 2014, Sascha Schade
- *
- * This file is part of the modm project.
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/.
- */
-// ----------------------------------------------------------------------------
-
-#ifndef MODM_MCP23S08_HPP
- #error "Don't include this file directly, use 'mcp23s08.hpp' instead!"
-#endif
-
-// ----------------------------------------------------------------------------
-template
-Spi modm::Mcp23s08::spi;
-
-template
-Cs modm::Mcp23s08::cs;
-
-template
-Int modm::Mcp23s08::interrupt;
-
-// ----------------------------------------------------------------------------
-template
-void
-modm::Mcp23s08::initialize()
-{
- spi.initialize();
- cs.set();
- cs.setOutput();
- interrupt.setInput();
-
- modm::this_fiber::sleep_for(1us);
-
- // disable address pins (as they are by default) and enable the
- // open-drain output from the interrupt line
- cs.reset();
- spi.write(deviceAddress | WRITE);
- spi.write(MCP_IOCON);
- spi.write(1 << 2);
- cs.set();
-
- modm::this_fiber::sleep_for(1us);
-}
-
-template
-void
-modm::Mcp23s08::configure(uint8_t inputMask, uint8_t pullupMask)
-{
- cs.reset();
- spi.write(deviceAddress | WRITE);
- spi.write(MCP_IODIR);
- spi.write(inputMask);
- cs.set();
-
- modm::this_fiber::sleep_for(1us);
-
- cs.reset();
- spi.write(deviceAddress | WRITE);
- spi.write(MCP_GPPU);
- spi.write(pullupMask);
- cs.set();
-
- modm::this_fiber::sleep_for(1us);
-}
-
-//void
-//configureInterrupt();
-
-// ----------------------------------------------------------------------------
-template
-uint8_t
-modm::Mcp23s08::read()
-{
- cs.reset();
- spi.write(deviceAddress | READ);
- spi.write(MCP_GPIO);
-
- uint8_t value = spi.write(0x00);
- cs.set();
-
- modm::this_fiber::sleep_for(1us);
-
- return value;
-}
-
-template
-void
-modm::Mcp23s08::write(uint8_t output)
-{
- cs.reset();
- spi.write(deviceAddress | WRITE);
- spi.write(MCP_GPIO);
- spi.write(output);
- cs.set();
-
- modm::this_fiber::sleep_for(1us);
-}
diff --git a/src/modm/driver/gpio/mcp23x08.hpp b/src/modm/driver/gpio/mcp23x08.hpp
new file mode 100644
index 0000000000..7f4bdd0edf
--- /dev/null
+++ b/src/modm/driver/gpio/mcp23x08.hpp
@@ -0,0 +1,261 @@
+/*
+ * Copyright (c) 2026, Niklas Hauser
+ *
+ * This file is part of the modm project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+// ----------------------------------------------------------------------------
+
+#ifndef MODM_MCP23X08_HPP
+#define MODM_MCP23X08_HPP
+
+#include
+#include
+#include "mcp23_transport.hpp"
+
+namespace modm
+{
+
+/// @ingroup modm_driver_mcp23x08
+struct mcp23x08
+{
+protected:
+ /// @cond
+ /// The addresses of the Configuration and Data Registers
+ enum class
+ Register : uint8_t
+ {
+ IODIR = 0x00, ///< Port direction (1=input, 0=output)
+ IPOL = 0x01, ///< Invert polarity
+ GPINTEN = 0x02, ///< Enable interrupt
+ DEFVAL = 0x03, ///< Compare register for interrupt
+ INTCON = 0x04,
+ IOCON = 0x05, ///< Configuration
+ GPPU = 0x06, ///< Enable pullups
+ INTF = 0x07, ///< Interrupt flag register
+ INTCAP = 0x08, ///< Interrupt capture register
+ GPIO = 0x09, ///< Port values
+ OLAT = 0x0A ///< Output latch register
+ };
+
+ enum class
+ IoCon : uint8_t
+ {
+ SeqOp = Bit5, ///< Sequential Operation mode bit
+ DisSlw = Bit4, ///< Slew Rate control bit for SDA output
+ HaEn = Bit3, ///< Hardware Address Enable bit
+ Odr = Bit2, ///< This bit configures the INT pin as an open-drain output
+ IntPol = Bit1 ///< This bit sets the polarity of the INT output pin
+ };
+ MODM_FLAGS8(IoCon);
+
+ static constexpr uint8_t
+ i(Register reg) { return uint8_t(reg); }
+ /// @endcond
+
+public:
+ enum class
+ Pin : uint8_t
+ {
+ P0 = Bit0,
+ P1 = Bit1,
+ P2 = Bit2,
+ P3 = Bit3,
+ P4 = Bit4,
+ P5 = Bit5,
+ P6 = Bit6,
+ P7 = Bit7,
+ };
+ typedef modm::Flags8 Pins;
+ MODM_INT_TYPE_FLAGS(Pins);
+}; // struct mcp23x08
+
+/**
+ * @see Mcp23TransportI2c
+ * @see Mcp23TransportSpi
+ *
+ * @tparam Transport Either the I2C or SPI Transport Layer.
+ *
+ * @author Niklas Hauser
+ *
+ * @ingroup modm_driver_mcp23x08
+ */
+template
+class Mcp23x08 : public mcp23x08, public Transport, public modm::GpioExpander
+{
+public:
+ static constexpr uint8_t width = 8;
+
+ using PortType = uint8_t;
+
+ static constexpr uint8_t
+ indexFromPin(Pin pin)
+ {
+ return modm::leftmostBit(PortType(pin));
+ }
+
+public:
+ /// Constructor, sets address to default of 0x20 (range 0x20 - 0x27)
+ Mcp23x08(uint8_t address=0x20);
+
+public:
+ bool
+ initialize();
+
+ bool
+ setOutput(Pins pins);
+
+ bool
+ set(Pins pins);
+
+ bool
+ reset(Pins pins);
+
+ bool
+ toggle(Pins pins);
+
+ bool
+ set(Pins pins, bool value);
+
+ bool
+ isSet(Pin pin)
+ {
+ // high is 1, low is 0
+ return memory.outputLatch.any(pin);
+ }
+
+ modm::Gpio::Direction
+ getDirection(Pin pin)
+ {
+ // output is 0, input is 1
+ return memory.direction.any(pin) ?
+ modm::Gpio::Direction::In :
+ modm::Gpio::Direction::Out;
+ }
+
+public:
+ bool
+ setInput(Pins pins);
+
+ bool
+ setPullUp(Pins pins);
+
+ bool
+ resetPullUp(Pins pins);
+
+ bool
+ setInvertInput(Pins pins);
+
+ bool
+ resetInvertInput(Pins pins);
+
+ bool
+ read(Pin pin)
+ {
+ // high is 1, low is 0
+ return memory.gpio.any(pin);
+ }
+
+ bool inline
+ readInput()
+ { return Transport::read(i(Register::GPIO), buffer + 9, 2); }
+
+ bool inline
+ readAllInput()
+ { return Transport::read(i(Register::INTF), buffer + 7, 8); }
+
+public:
+ bool
+ writePort(PortType data);
+
+ bool
+ readPort(PortType &data);
+
+public:
+ Pins inline
+ getDirections()
+ { return ~memory.direction; }
+
+ Pins inline
+ getOutputs()
+ { return memory.outputLatch; }
+
+ Pins inline
+ getInputs()
+ { return memory.gpio; }
+
+ Pins inline
+ getPolarities()
+ { return memory.polarity; }
+
+public:
+ /// Alias-templates for simpler use of the Pin
+ /// @{
+ template < Mcp23x08 &object >
+ using P0 = GpioExpanderPin< Mcp23x08, object, Pin::P0 >;
+ template < Mcp23x08 &object >
+ using P1 = GpioExpanderPin< Mcp23x08, object, Pin::P1 >;
+ template < Mcp23x08 &object >
+ using P2 = GpioExpanderPin< Mcp23x08, object, Pin::P2 >;
+ template < Mcp23x08 &object >
+ using P3 = GpioExpanderPin< Mcp23x08, object, Pin::P3 >;
+ template < Mcp23x08 &object >
+ using P4 = GpioExpanderPin< Mcp23x08, object, Pin::P4 >;
+ template < Mcp23x08 &object >
+ using P5 = GpioExpanderPin< Mcp23x08, object, Pin::P5 >;
+ template < Mcp23x08 &object >
+ using P6 = GpioExpanderPin< Mcp23x08, object, Pin::P6 >;
+ template < Mcp23x08 &object >
+ using P7 = GpioExpanderPin< Mcp23x08, object, Pin::P7 >;
+ /// @}
+
+ /// Alias-templates for simpler use of the Port
+ template < Mcp23x08 &object, Pin StartPin, uint8_t Width, GpioPort::DataOrder DataOrder = GpioPort::DataOrder::Normal >
+ using Port = GpioExpanderPort< Mcp23x08, object, StartPin, Width, DataOrder >;
+
+private:
+ struct modm_packed
+ Memory
+ {
+ Memory() :
+ direction(0xff),
+ polarity(0),
+ interruptEnable(0),
+ interruptDefault(0),
+ interruptControl(0),
+ control(0),
+ pullup(0),
+ interruptFlag(0),
+ interruptCapture(0),
+ gpio(0),
+ outputLatch(0)
+ {}
+
+ Pins direction; // IODIR
+ Pins polarity; // IPOL
+ Pins interruptEnable; // GPINTEN
+ Pins interruptDefault; // DEFVAL
+ Pins interruptControl; // INTCON
+ IoCon_t control; // IOCON
+ Pins pullup; // GPPU
+ Pins interruptFlag; // INTF
+ Pins interruptCapture; // INTCAP
+ Pins gpio; // GPIO
+ Pins outputLatch; // OLAT
+ };
+
+ union
+ {
+ Memory memory;
+ uint8_t buffer[sizeof(Memory)];
+ };
+};
+
+} // namespace modm
+
+#include "mcp23x08_impl.hpp"
+
+#endif // MODM_MCP23X08_HPP
diff --git a/src/modm/driver/gpio/mcp23x08.lb b/src/modm/driver/gpio/mcp23x08.lb
new file mode 100644
index 0000000000..802300d791
--- /dev/null
+++ b/src/modm/driver/gpio/mcp23x08.lb
@@ -0,0 +1,47 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2026, Niklas Hauser
+#
+# This file is part of the modm project.
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# -----------------------------------------------------------------------------
+
+
+def init(module):
+ module.name = ":driver:mcp23x08"
+ module.description = """
+# MCP23x08 8-Bit I/O Expander
+
+The lower three address bits can be configured: `0100abc`.
+
+Notice that you can specify multiple pins at the same time for functions
+with argument type `Pins`, either by ORing the according pins, or
+converting a 8bit value using the `Pins(uint8_t)` converting constructor.
+
+Other functions with argument type `Pin` can only take one pin.
+If you want to operate on all 8bit, use the `get(Inputs|Outputs|Directions|Polarities)()`
+getters.
+"""
+
+def prepare(module, options):
+ module.depends(
+ ":architecture:gpio.expander",
+ ":architecture:i2c.device",
+ ":architecture:register",
+ ":architecture:spi.device",
+ ":architecture:fiber")
+ return True
+
+def build(env):
+ env.outbasepath = "modm/src/modm/driver/gpio"
+ env.copy("mcp23x08.hpp")
+ env.copy("mcp23x08_impl.hpp")
+ if not env.has_module(":driver:mcp23x17"):
+ env.copy("mcp23_transport.hpp")
+ env.copy("mcp23_transport_impl.hpp")
+
+
diff --git a/src/modm/driver/gpio/mcp23x08_impl.hpp b/src/modm/driver/gpio/mcp23x08_impl.hpp
new file mode 100644
index 0000000000..b4eb14cdb2
--- /dev/null
+++ b/src/modm/driver/gpio/mcp23x08_impl.hpp
@@ -0,0 +1,175 @@
+/*
+ * Copyright (c) 2026, Niklas Hauser
+ *
+ * This file is part of the modm project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+// ----------------------------------------------------------------------------
+
+#ifndef MODM_MCP23X08_HPP
+# error "Don't include this file directly, use 'mcp23x08.hpp' instead!"
+#endif
+
+// ----------------------------------------------------------------------------
+template < class Transport >
+modm::Mcp23x08::Mcp23x08(uint8_t address) :
+ Transport(address), memory()
+{
+}
+
+template < class Transport >
+bool
+modm::Mcp23x08::initialize()
+{
+ memory = Memory(); // reset local register cache
+ this->write(i(Register::IOCON), memory.control.value);
+
+ this->write(i(Register::IODIR), memory.direction.value);
+ this->write(i(Register::IPOL), memory.polarity.value);
+ this->write(i(Register::GPINTEN), memory.interruptEnable.value);
+ this->write(i(Register::DEFVAL), memory.interruptDefault.value);
+ this->write(i(Register::INTCON), memory.interruptControl.value);
+ this->write(i(Register::GPPU), memory.pullup.value);
+ this->write(i(Register::GPIO), memory.gpio.value);
+ this->write(i(Register::OLAT), memory.outputLatch.value);
+ Transport::read(i(Register::INTF), buffer + 7, 4);
+
+ return Transport::read(i(Register::IODIR), buffer, sizeof(buffer));
+}
+
+// MARK: - Tasks
+template < class Transport >
+bool
+modm::Mcp23x08::setOutput(Pins pins)
+{
+ // output is 0, input is 1
+ memory.direction.reset(pins);
+
+ return this->write(i(Register::IODIR), memory.direction.value);
+}
+
+template < class Transport >
+bool
+modm::Mcp23x08::set(Pins pins)
+{
+ // high is 1, low is 0
+ // set output latches locally, but only those that are output
+ memory.outputLatch.set(pins & ~memory.direction);
+
+ return this->write(i(Register::GPIO), memory.outputLatch.value);
+}
+
+template < class Transport >
+bool
+modm::Mcp23x08::reset(Pins pins)
+{
+ // high is 1, low is 0
+ // reset output latches locally, but only those that are output
+ memory.outputLatch.reset(pins & ~memory.direction);
+
+ return this->write(i(Register::GPIO), memory.outputLatch.value);
+}
+
+template < class Transport >
+bool
+modm::Mcp23x08::toggle(Pins pins)
+{
+ // high is 1, low is 0
+ // toggle output latches locally, but only those that are output
+ memory.outputLatch.toggle(pins & ~memory.direction);
+
+ return this->write(i(Register::GPIO), memory.outputLatch.value);
+}
+
+template < class Transport >
+bool
+modm::Mcp23x08::set(Pins pins, bool value)
+{
+ // high is 1, low is 0
+ // update output latches locally, but only those that are output
+ memory.outputLatch.update(pins & ~memory.direction, value);
+
+ return this->write(i(Register::GPIO), memory.outputLatch.value);
+}
+
+template < class Transport >
+bool
+modm::Mcp23x08::setInput(Pins pins)
+{
+ // output is 0, input is 1
+ memory.direction.set(pins);
+ memory.outputLatch.reset(pins);
+
+ return this->write(i(Register::IODIR), memory.direction.value);
+}
+
+template < class Transport >
+bool
+modm::Mcp23x08::setPullUp(Pins pins)
+{
+ // inverted is 1, normal is 0
+ memory.pullup.set(pins);
+
+ return this->write(i(Register::GPPU), memory.pullup.value);
+}
+
+template < class Transport >
+bool
+modm::Mcp23x08::resetPullUp(Pins pins)
+{
+ // inverted is 1, normal is 0
+ memory.pullup.reset(pins);
+
+ return this->write(i(Register::GPPU), memory.pullup.value);
+}
+
+template < class Transport >
+bool
+modm::Mcp23x08::setInvertInput(Pins pins)
+{
+ // inverted is 1, normal is 0
+ memory.polarity.set(pins);
+
+ return this->write(i(Register::IPOL), memory.polarity.value);
+}
+
+template < class Transport >
+bool
+modm::Mcp23x08::resetInvertInput(Pins pins)
+{
+ // inverted is 1, normal is 0
+ memory.polarity.reset(pins);
+
+ return this->write(i(Register::IPOL), memory.polarity.value);
+}
+
+template < class Transport >
+bool
+modm::Mcp23x08::writePort(PortType data)
+{
+ // high is 1, low is 0
+ // output is 0, input is 1
+ // set output latches locally, but only those that are output
+ // clear all outputs
+ memory.outputLatch.clear(~memory.direction);
+ // set masked output values
+ memory.outputLatch.set(Pins(data) & ~memory.direction);
+
+ return this->write(i(Register::GPIO), memory.outputLatch.value);
+}
+
+template < class Transport >
+bool
+modm::Mcp23x08::readPort(PortType &data)
+{
+ if (readInput())
+ {
+ data = memory.gpio.value;
+ return true;
+ }
+
+ return false;
+}
diff --git a/src/modm/driver/gpio/pcal6524.hpp b/src/modm/driver/gpio/pcal6524.hpp
new file mode 100644
index 0000000000..45a7ebc9c2
--- /dev/null
+++ b/src/modm/driver/gpio/pcal6524.hpp
@@ -0,0 +1,428 @@
+/*
+ * Copyright (c) 2026, Niklas Hauser
+ *
+ * This file is part of the modm project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+// ----------------------------------------------------------------------------
+
+#pragma once
+
+#include
+#include
+#include
+
+namespace modm
+{
+
+/// @ingroup modm_driver_pcal6524
+struct pcal6524
+{
+ /// Wiring options of the single ADDR pin, see Table 4 of the datasheet.
+ enum class
+ AddressPin : uint8_t
+ {
+ Scl = 0x20,
+ Sda = 0x21,
+ Vss = 0x22,
+ Vdd = 0x23,
+ };
+
+ /// The 24 port pins P0_0 .. P0_7, P1_0 .. P1_7, P2_0 .. P2_7.
+ enum class
+ Pin : uint32_t
+ {
+ P0_0 = Bit0, P0_1 = Bit1, P0_2 = Bit2, P0_3 = Bit3,
+ P0_4 = Bit4, P0_5 = Bit5, P0_6 = Bit6, P0_7 = Bit7,
+
+ P1_0 = Bit8, P1_1 = Bit9, P1_2 = Bit10, P1_3 = Bit11,
+ P1_4 = Bit12, P1_5 = Bit13, P1_6 = Bit14, P1_7 = Bit15,
+
+ P2_0 = Bit16, P2_1 = Bit17, P2_2 = Bit18, P2_3 = Bit19,
+ P2_4 = Bit20, P2_5 = Bit21, P2_6 = Bit22, P2_7 = Bit23,
+ };
+ typedef modm::Flags32 Pins;
+ MODM_INT_TYPE_FLAGS(Pins);
+
+protected:
+ /// @cond
+ /// Start address of each 3-register group (port 0, 1, 2), see Table 6.
+ /// A 3-byte transaction starting at one of these addresses accesses all
+ /// three ports in one go, since the pointer register auto-increments
+ /// through the group by default (AI = 0).
+ enum class
+ Register : uint8_t
+ {
+ InputPort0 = 0x00,
+ OutputPort0 = 0x04,
+ PolarityInversion0 = 0x08,
+ Configuration0 = 0x0C,
+ PullUpDownEnable0 = 0x4C,
+ PullUpDownSelection0 = 0x50,
+ InterruptMask0 = 0x54,
+ InterruptStatus0 = 0x58,
+ };
+
+ static constexpr uint8_t
+ i(Register reg) { return uint8_t(reg); }
+ /// @endcond
+}; // struct pcal6524
+
+/**
+ * PCAL6524 Ultra Low-Voltage Translating 24-bit I2C I/O Expander.
+ *
+ * A 24-bit general-purpose I/O expander with three 8-bit ports (P0, P1, P2)
+ * providing remote I/O expansion via a Fm+ I2C-bus interface up to 1MHz.
+ * Unlike simpler expanders, it has independently selectable pull-up *or*
+ * pull-down resistors per pin and a maskable, edge- or level-sensitive
+ * interrupt output.
+ *
+ * This driver implements the `modm::GpioExpander` interface plus the most
+ * commonly used Agile I/O features (polarity inversion, pull resistors,
+ * interrupt mask/status). Output drive strength, input latching, switch
+ * debounce, individual open-drain configuration, software reset and the
+ * device ID registers are not implemented.
+ *
+ * @code
+ * modm::Pcal6524 expander;
+ * expander.initialize();
+ * expander.setOutput(pcal6524::Pin::P0_0);
+ * expander.set(pcal6524::Pin::P0_0);
+ * @endcode
+ *
+ * @ingroup modm_driver_pcal6524
+ * @author Niklas Hauser
+ */
+template < class I2cMaster >
+class Pcal6524 : public pcal6524, public modm::I2cDevice, public modm::GpioExpander
+{
+public:
+ static constexpr uint8_t width = 24;
+ using PortType = uint32_t;
+
+ static constexpr uint8_t
+ indexFromPin(Pin pin)
+ { return modm::leftmostBit(PortType(pin)); }
+
+public:
+ /// Constructor.
+ /// @param address see `pcal6524::AddressPin`, default is ADDR tied to VSS.
+ Pcal6524(uint8_t address = uint8_t(AddressPin::Vss)) :
+ modm::I2cDevice(address), memory()
+ {}
+
+public:
+ /// Resets the local register cache to the power-up defaults and writes
+ /// them out to the device.
+ bool
+ initialize()
+ {
+ memory = Memory();
+ bool success = write24(Register::Configuration0, memory.direction.value);
+ success &= write24(Register::OutputPort0, memory.outputLatch.value);
+ success &= write24(Register::PolarityInversion0, memory.polarity.value);
+ success &= write24(Register::PullUpDownSelection0, memory.pullSelect.value);
+ success &= write24(Register::PullUpDownEnable0, memory.pullEnable.value);
+ success &= write24(Register::InterruptMask0, memory.interruptMask.value);
+ return success and readInput();
+ }
+
+ bool
+ setOutput(Pins pins)
+ {
+ // output is 0, input is 1
+ memory.direction.reset(pins);
+ return write24(Register::Configuration0, memory.direction.value);
+ }
+
+ bool
+ set(Pins pins)
+ {
+ // set output latches locally, but only those that are output
+ memory.outputLatch.set(pins & ~memory.direction);
+ return write24(Register::OutputPort0, memory.outputLatch.value);
+ }
+
+ bool
+ reset(Pins pins)
+ {
+ memory.outputLatch.reset(pins & ~memory.direction);
+ return write24(Register::OutputPort0, memory.outputLatch.value);
+ }
+
+ bool
+ toggle(Pins pins)
+ {
+ memory.outputLatch.toggle(pins & ~memory.direction);
+ return write24(Register::OutputPort0, memory.outputLatch.value);
+ }
+
+ bool
+ set(Pins pins, bool value)
+ {
+ memory.outputLatch.update(pins & ~memory.direction, value);
+ return write24(Register::OutputPort0, memory.outputLatch.value);
+ }
+
+ bool inline
+ isSet(Pin pin)
+ { return memory.outputLatch.any(pin); }
+
+ modm::Gpio::Direction inline
+ getDirection(Pin pin)
+ {
+ // output is 0, input is 1
+ return memory.direction.any(pin) ?
+ modm::Gpio::Direction::In :
+ modm::Gpio::Direction::Out;
+ }
+
+public:
+ bool
+ setInput(Pins pins)
+ {
+ // output is 0, input is 1
+ memory.direction.set(pins);
+ return write24(Register::Configuration0, memory.direction.value);
+ }
+
+ /// Inverts the polarity of the given input pins in the input port register.
+ bool
+ setInvertInput(Pins pins)
+ {
+ memory.polarity.set(pins);
+ return write24(Register::PolarityInversion0, memory.polarity.value);
+ }
+
+ /// Restores the normal (non-inverted) polarity of the given input pins.
+ bool
+ resetInvertInput(Pins pins)
+ {
+ memory.polarity.reset(pins);
+ return write24(Register::PolarityInversion0, memory.polarity.value);
+ }
+
+ bool inline
+ read(Pin pin)
+ { return memory.gpio.any(pin); }
+
+ bool
+ readInput()
+ {
+ uint32_t value;
+ if (not read24(Register::InputPort0, value)) return false;
+ memory.gpio = Pins(value);
+ return true;
+ }
+
+public:
+ bool
+ writePort(PortType data)
+ {
+ // clear all outputs, but only those that are output
+ memory.outputLatch.reset(~memory.direction);
+ // set masked output values
+ memory.outputLatch.set(Pins(data) & ~memory.direction);
+ return write24(Register::OutputPort0, memory.outputLatch.value);
+ }
+
+ bool
+ readPort(PortType &data)
+ {
+ if (readInput())
+ {
+ data = memory.gpio.value;
+ return true;
+ }
+ return false;
+ }
+
+public:
+ Pins inline
+ getDirections()
+ { return ~memory.direction; }
+
+ Pins inline
+ getOutputs()
+ { return memory.outputLatch; }
+
+ Pins inline
+ getInputs()
+ { return memory.gpio; }
+
+ Pins inline
+ getPolarities()
+ { return memory.polarity; }
+
+public:
+ /// Enables a 100kOhm pull-up resistor on the given pins.
+ bool
+ setPullUp(Pins pins)
+ {
+ memory.pullSelect.set(pins);
+ if (not write24(Register::PullUpDownSelection0, memory.pullSelect.value))
+ return false;
+ memory.pullEnable.set(pins);
+ return write24(Register::PullUpDownEnable0, memory.pullEnable.value);
+ }
+
+ /// Enables a 100kOhm pull-down resistor on the given pins.
+ bool
+ setPullDown(Pins pins)
+ {
+ memory.pullSelect.reset(pins);
+ if (not write24(Register::PullUpDownSelection0, memory.pullSelect.value))
+ return false;
+ memory.pullEnable.set(pins);
+ return write24(Register::PullUpDownEnable0, memory.pullEnable.value);
+ }
+
+ /// Disconnects the pull-up/pull-down resistor from the given pins.
+ bool
+ disablePull(Pins pins)
+ {
+ memory.pullEnable.reset(pins);
+ return write24(Register::PullUpDownEnable0, memory.pullEnable.value);
+ }
+
+ /// Unmasks (enables) the interrupt for the given input pins.
+ bool
+ setInterrupt(Pins pins)
+ {
+ memory.interruptMask.reset(pins);
+ return write24(Register::InterruptMask0, memory.interruptMask.value);
+ }
+
+ /// Masks (disables) the interrupt for the given input pins.
+ bool
+ maskInterrupt(Pins pins)
+ {
+ memory.interruptMask.set(pins);
+ return write24(Register::InterruptMask0, memory.interruptMask.value);
+ }
+
+ /// Reads which input pins caused the last interrupt.
+ bool
+ readInterruptStatus(Pins &status)
+ {
+ uint32_t value;
+ if (not read24(Register::InterruptStatus0, value)) return false;
+ status = Pins(value);
+ return true;
+ }
+
+public:
+ /// Alias-templates for simpler use of the Pin
+ /// @{
+ template < Pcal6524 &object >
+ using P0_0 = GpioExpanderPin< Pcal6524, object, Pin::P0_0 >;
+ template < Pcal6524 &object >
+ using P0_1 = GpioExpanderPin< Pcal6524, object, Pin::P0_1 >;
+ template < Pcal6524 &object >
+ using P0_2 = GpioExpanderPin< Pcal6524, object, Pin::P0_2 >;
+ template < Pcal6524 &object >
+ using P0_3 = GpioExpanderPin< Pcal6524, object, Pin::P0_3 >;
+ template < Pcal6524 &object >
+ using P0_4 = GpioExpanderPin< Pcal6524, object, Pin::P0_4 >;
+ template < Pcal6524 &object >
+ using P0_5 = GpioExpanderPin< Pcal6524, object, Pin::P0_5 >;
+ template < Pcal6524 &object >
+ using P0_6 = GpioExpanderPin< Pcal6524, object, Pin::P0_6 >;
+ template < Pcal6524 &object >
+ using P0_7 = GpioExpanderPin< Pcal6524, object, Pin::P0_7 >;
+
+ template < Pcal6524 &object >
+ using P1_0 = GpioExpanderPin< Pcal6524, object, Pin::P1_0 >;
+ template < Pcal6524 &object >
+ using P1_1 = GpioExpanderPin< Pcal6524, object, Pin::P1_1 >;
+ template < Pcal6524 &object >
+ using P1_2 = GpioExpanderPin< Pcal6524, object, Pin::P1_2 >;
+ template < Pcal6524 &object >
+ using P1_3 = GpioExpanderPin< Pcal6524, object, Pin::P1_3 >;
+ template < Pcal6524 &object >
+ using P1_4 = GpioExpanderPin< Pcal6524, object, Pin::P1_4 >;
+ template < Pcal6524 &object >
+ using P1_5 = GpioExpanderPin< Pcal6524, object, Pin::P1_5 >;
+ template < Pcal6524 &object >
+ using P1_6 = GpioExpanderPin< Pcal6524, object, Pin::P1_6 >;
+ template < Pcal6524 &object >
+ using P1_7 = GpioExpanderPin< Pcal6524, object, Pin::P1_7 >;
+
+ template < Pcal6524 &object >
+ using P2_0 = GpioExpanderPin< Pcal6524, object, Pin::P2_0 >;
+ template < Pcal6524 &object >
+ using P2_1 = GpioExpanderPin< Pcal6524, object, Pin::P2_1 >;
+ template < Pcal6524 &object >
+ using P2_2 = GpioExpanderPin< Pcal6524, object, Pin::P2_2 >;
+ template < Pcal6524 &object >
+ using P2_3 = GpioExpanderPin< Pcal6524, object, Pin::P2_3 >;
+ template < Pcal6524 &object >
+ using P2_4 = GpioExpanderPin< Pcal6524, object, Pin::P2_4 >;
+ template < Pcal6524 &object >
+ using P2_5 = GpioExpanderPin< Pcal6524, object, Pin::P2_5 >;
+ template < Pcal6524 &object >
+ using P2_6 = GpioExpanderPin< Pcal6524, object, Pin::P2_6 >;
+ template < Pcal6524 &object >
+ using P2_7 = GpioExpanderPin< Pcal6524, object, Pin::P2_7 >;
+ /// @}
+
+ /// Alias-template for simpler use of a range of pins as a port.
+ template < Pcal6524 &object, Pin StartPin, uint8_t Width, GpioPort::DataOrder DataOrder = GpioPort::DataOrder::Normal >
+ using Port = GpioExpanderPort< Pcal6524, object, StartPin, Width, DataOrder >;
+
+private:
+ // the wire format of a 3-register group is 24bit, so this cannot be
+ // overlaid onto the 32bit `Pins::value` directly, unlike the 8/16bit
+ // GPIO expanders which pack their command byte and register value into
+ // one contiguous, `modm_packed` wire-format buffer.
+ bool
+ write24(Register reg, uint32_t value)
+ {
+ buffer[0] = i(reg);
+ buffer[1] = uint8_t(value);
+ buffer[2] = uint8_t(value >> 8);
+ buffer[3] = uint8_t(value >> 16);
+ return modm::I2cDevice::write(buffer, 4);
+ }
+
+ bool
+ read24(Register reg, uint32_t &value)
+ {
+ uint8_t addr = i(reg);
+ uint8_t data[3];
+ if (not modm::I2cDevice::writeRead(&addr, 1, data, 3))
+ return false;
+ value = uint32_t(data[0]) | (uint32_t(data[1]) << 8) | (uint32_t(data[2]) << 16);
+ return true;
+ }
+
+ struct modm_packed
+ Memory
+ {
+ Memory() :
+ direction(0xffffff),
+ polarity(0),
+ pullEnable(0),
+ pullSelect(0xffffff),
+ interruptMask(0xffffff),
+ outputLatch(0xffffff),
+ gpio(0)
+ {}
+
+ Pins direction; // Configuration0/1/2
+ Pins polarity; // Polarity Inversion0/1/2
+ Pins pullEnable; // Pull-up/down Enable0/1/2
+ Pins pullSelect; // Pull-up/down Selection0/1/2
+ Pins interruptMask; // Interrupt Mask0/1/2
+ Pins outputLatch; // Output Port0/1/2
+ Pins gpio; // Input Port0/1/2 (buffered readInput() result)
+ };
+
+ Memory memory;
+ uint8_t buffer[4];
+};
+
+} // namespace modm
diff --git a/src/modm/driver/gpio/pcal6524.lb b/src/modm/driver/gpio/pcal6524.lb
new file mode 100644
index 0000000000..3c75ed4a8b
--- /dev/null
+++ b/src/modm/driver/gpio/pcal6524.lb
@@ -0,0 +1,33 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2026, Niklas Hauser
+#
+# This file is part of the modm project.
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# -----------------------------------------------------------------------------
+
+
+def init(module):
+ module.name = ":driver:pcal6524"
+ module.description = """\
+# PCAL6524 24-Bit I2C I/O Expander
+
+Ultra low-voltage translating 24-bit Fm+ I2C-bus/SMBus I/O expander with
+three 8-bit ports (P0, P1, P2), independently selectable pull-up or
+pull-down resistors per pin, and a maskable interrupt output.
+"""
+
+def prepare(module, options):
+ module.depends(
+ ":architecture:gpio.expander",
+ ":architecture:i2c.device",
+ ":architecture:register")
+ return True
+
+def build(env):
+ env.outbasepath = "modm/src/modm/driver/gpio"
+ env.copy("pcal6524.hpp")
diff --git a/src/modm/driver/mux/max14661.hpp b/src/modm/driver/mux/max14661.hpp
new file mode 100644
index 0000000000..fb51192d14
--- /dev/null
+++ b/src/modm/driver/mux/max14661.hpp
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2026, Niklas Hauser
+ *
+ * This file is part of the modm project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+// ----------------------------------------------------------------------------
+
+#pragma once
+
+#include
+#include "max14661_transport.hpp"
+
+namespace modm
+{
+
+/// @ingroup modm_driver_max14661
+struct max14661
+{
+ static inline uint8_t address(uint8_t bits = 0)
+ { return 0x4C | (bits & 0b11); }
+
+ /// The 16 AB_ pins that can be routed to COMA and/or COMB.
+ enum class
+ Channel : uint16_t
+ {
+ Ch1 = Bit0,
+ Ch2 = Bit1,
+ Ch3 = Bit2,
+ Ch4 = Bit3,
+ Ch5 = Bit4,
+ Ch6 = Bit5,
+ Ch7 = Bit6,
+ Ch8 = Bit7,
+ Ch9 = Bit8,
+ Ch10 = Bit9,
+ Ch11 = Bit10,
+ Ch12 = Bit11,
+ Ch13 = Bit12,
+ Ch14 = Bit13,
+ Ch15 = Bit14,
+ Ch16 = Bit15,
+ };
+ MODM_FLAGS16(Channel);
+};
+
+/**
+ * MAX14661 Beyond-the-Rails 16:2 analog multiplexer.
+ *
+ * A serially controlled, dual-channel analog multiplexer allowing any of
+ * the 16 AB pins to be connected to either common pin (COMA, COMB)
+ * simultaneously in any combination. Beyond-the-Rails technology allows
+ * switching ±5.5V signals from a single +1.6V to +5.5V supply.
+ *
+ * Channels are addressed with the `max14661::Channel` flags, which can be
+ * combined with `|` to connect multiple channels to the same common pin
+ * at once:
+ *
+ * @code
+ * modm::Max14661> mux;
+ * mux.setChannelsA(max14661::Channel::Ch1 | max14661::Channel::Ch5);
+ * mux.setChannelsB(max14661::Channel::Ch2);
+ * @endcode
+ *
+ * @tparam Transport Either `Max14661TransportI2c` or `Max14661TransportSpi`.
+ *
+ * @see Max14661TransportI2c
+ * @see Max14661TransportSpi
+ *
+ * @ingroup modm_driver_max14661
+ * @author Niklas Hauser
+ */
+template < class Transport >
+class Max14661 : public max14661, public Transport
+{
+public:
+ /// Constructor.
+ /// @param address I2C slave address (7bit, unshifted), default is A1=A0=0.
+ /// Ignored when used with `Max14661TransportSpi`.
+ Max14661(uint8_t address = max14661::address())
+ : Transport(address) {}
+
+ /// Connect the given channels to COMA, disconnecting all others on
+ /// this bank.
+ bool inline
+ setChannelsA(Channel_t channels)
+ { return Transport::writeChannelsA(channels.value); }
+
+ /// Connect the given channels to COMB, disconnecting all others on
+ /// this bank.
+ bool inline
+ setChannelsB(Channel_t channels)
+ { return Transport::writeChannelsB(channels.value); }
+
+ /// Disconnect all channels from COMA.
+ bool inline
+ disableChannelsA()
+ { return setChannelsA(Channel_t()); }
+
+ /// Disconnect all channels from COMB.
+ bool inline
+ disableChannelsB()
+ { return setChannelsB(Channel_t()); }
+};
+
+} // namespace modm
diff --git a/src/modm/driver/mux/max14661.lb b/src/modm/driver/mux/max14661.lb
new file mode 100644
index 0000000000..c186e60bb1
--- /dev/null
+++ b/src/modm/driver/mux/max14661.lb
@@ -0,0 +1,41 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2026, Niklas Hauser
+#
+# This file is part of the modm project.
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# -----------------------------------------------------------------------------
+
+
+def init(module):
+ module.name = ":driver:max14661"
+ module.description = """\
+# MAX14661 Beyond-the-Rails 16:2 Multiplexer
+
+The MAX14661 is a serially controlled, dual-channel analog multiplexer
+allowing any of the 16 AB pins to be connected to either common pin
+(COMA, COMB) simultaneously in any combination. The device features
+Beyond-the-Rails capability so that ±5.5V signals can be passed with any
+single supply between +1.6V and +5.5V.
+
+The serial control is selectable between I2C and SPI, both of which are
+implemented as separate transport layers so the same driver can be used
+with either bus.
+"""
+
+def prepare(module, options):
+ module.depends(
+ ":architecture:register",
+ ":architecture:i2c.device",
+ ":architecture:spi.device",
+ ":architecture:fiber")
+ return True
+
+def build(env):
+ env.outbasepath = "modm/src/modm/driver/mux"
+ env.copy("max14661.hpp")
+ env.copy("max14661_transport.hpp")
diff --git a/src/modm/driver/mux/max14661_transport.hpp b/src/modm/driver/mux/max14661_transport.hpp
new file mode 100644
index 0000000000..13df17b7dd
--- /dev/null
+++ b/src/modm/driver/mux/max14661_transport.hpp
@@ -0,0 +1,158 @@
+/*
+ * Copyright (c) 2026, Niklas Hauser
+ *
+ * This file is part of the modm project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+// ----------------------------------------------------------------------------
+
+#pragma once
+
+#include
+#include
+#include
+
+namespace modm
+{
+
+/**
+ * MAX14661 I2C Transport Layer.
+ *
+ * In I2C mode the switches of each bank are updated through the shadow
+ * registers: the desired 16-bit channel mask is written into the two
+ * shadow registers of a bank, and then the "copy shadow registers to
+ * switches" command is issued for that bank. There is no command that
+ * copies both banks at once, so bank A and bank B always have to be
+ * updated with two separate register writes.
+ *
+ * The I2C interface is compliant with Fast Mode (up to 400kHz). The slave
+ * address is configured by the A0/A1 pins.
+ *
+ * @see Max14661
+ *
+ * @ingroup modm_driver_max14661
+ * @author Niklas Hauser
+ */
+template < class I2cMaster >
+class Max14661TransportI2c : public modm::I2cDevice< I2cMaster >
+{
+public:
+ Max14661TransportI2c(uint8_t address) : I2cDevice(address) {}
+
+protected:
+ /// Write the 16-bit channel mask into the bank A shadow registers
+ /// and copy them to the bank A switches.
+ bool
+ writeChannelsA(uint16_t mask)
+ {
+ // SHDW0/SHDW1 = 0x10/0x11, CMD_A = 0x14
+ return writeShadow(0x10, 0x14, mask);
+ }
+
+ /// Write the 16-bit channel mask into the bank B shadow registers
+ /// and copy them to the bank B switches.
+ bool
+ writeChannelsB(uint16_t mask)
+ {
+ // SHDW2/SHDW3 = 0x12/0x13, CMD_B = 0x15
+ return writeShadow(0x12, 0x15, mask);
+ }
+
+private:
+ bool
+ writeShadow(uint8_t shadowRegister, uint8_t commandRegister, uint16_t mask)
+ {
+ // SHDWx: low byte, SHDWx+1: high byte, auto-incrementing register address
+ buffer[0] = shadowRegister;
+ buffer[1] = uint8_t(mask);
+ buffer[2] = uint8_t(mask >> 8);
+ if (not this->write(buffer, 3))
+ return false;
+
+ // 0b10001 = copy shadow registers of this bank to the switches
+ buffer[0] = commandRegister;
+ buffer[1] = 0b10001;
+ return this->write(buffer, 2);
+ }
+
+ uint8_t buffer[3];
+};
+
+/**
+ * MAX14661 SPI Transport Layer.
+ *
+ * In SPI mode the MAX14661 has no addressable registers: it is a plain
+ * 32-bit shift register and all 32 switches (16 of bank A, 16 of bank B)
+ * are transitioned simultaneously on the rising edge of CS. This transport
+ * therefore caches both channel masks locally and always shifts out the
+ * complete 32-bit word, so that updating one bank does not disturb the
+ * other.
+ *
+ * The SPI interface requires Mode3 and can be clocked with up to ~10MHz.
+ *
+ * @see Max14661
+ *
+ * @tparam Cs connected chip select pin
+ *
+ * @ingroup modm_driver_max14661
+ * @author Niklas Hauser
+ */
+template < class SpiMaster, class Cs >
+class Max14661TransportSpi : public modm::SpiDevice< SpiMaster >
+{
+public:
+ Max14661TransportSpi(uint8_t)
+ {
+ Cs::setOutput(modm::Gpio::High);
+ }
+
+protected:
+ /// Set the bank A channel mask and shift the complete 32-bit switch
+ /// state into the device.
+ bool
+ writeChannelsA(uint16_t mask)
+ {
+ maskA = mask;
+ return writeShiftRegister();
+ }
+
+ /// Set the bank B channel mask and shift the complete 32-bit switch
+ /// state into the device.
+ bool
+ writeChannelsB(uint16_t mask)
+ {
+ maskB = mask;
+ return writeShiftRegister();
+ }
+
+private:
+ bool
+ writeShiftRegister()
+ {
+ modm::this_fiber::poll([&]{ return this->acquireMaster(); });
+ Cs::reset();
+
+ // Table 4: SW16B..SW09B, SW08B..SW01B, SW16A..SW09A, SW08A..SW01A
+ const uint8_t buffer[4]
+ {
+ uint8_t(maskB >> 8),
+ uint8_t(maskB),
+ uint8_t(maskA >> 8),
+ uint8_t(maskA),
+ };
+ SpiMaster::transfer(buffer, nullptr, 4);
+
+ if (this->releaseMaster())
+ Cs::set();
+
+ return true;
+ }
+
+ uint16_t maskA{0};
+ uint16_t maskB{0};
+};
+
+} // namespace modm