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