Version 03.00.01 — Lightweight, zero-allocation Arduino helper library for register-based I2C devices.
I2C_Functions wraps every common I2C transaction — byte, word, buffer, bit, and command — behind a clean single-header API. It works with the standard Wire bus or any custom TwoWire instance and optionally manages a TCA9548-style I2C multiplexer channel automatically before each transaction.
- Features
- Installation
- Quick Start
- API Families
- Constructor and Lifecycle
- Read Operations
- Write Operations
- Bit Operations
- Multiplexer Support
- Predefined Constants
- Project Structure
- Notes and Limitations
| Feature | Details |
|---|---|
| Zero heap allocation | No new, malloc, or String — safe for constrained MCUs |
| Single-header | Drop in src/I2C_Functions.h, done |
| Dual API | Status-returning Try_* variants for production + simple wrappers for prototyping |
| Byte operations | Read / write single byte registers |
| Word operations | Read / write 16-bit registers with configurable endianness (LSB-first or MSB-first) |
| Buffer operations | Read / write multi-byte payloads with 8-bit or 16-bit register addressing |
| Command operations | Send raw command bytes without a register address |
| Bit operations | Set, clear, toggle, read, and write individual register bits (read-modify-write) |
| Mux support | Automatic TCA9548 channel switching before each transaction |
| Bus flexibility | Accepts any TwoWire* pointer — Wire, Wire1, or a software I2C instance |
| Platform agnostic | Arduino AVR, megaAVR, ESP32, and any Arduino-compatible target |
- Open Sketch → Include Library → Manage Libraries
- Search for
I2C_Functions - Click Install
Add to your platformio.ini:
lib_deps =
akkoyun/I2C_Functions- Download or clone this repository
- Copy the folder into your Arduino
libraries/directory - Restart the IDE
#include <I2C_Functions.h>
// Create an object for a device at 0x40 (e.g. HDC2010)
I2C_Functions sensor(__I2C_Addr_HDC2010__);
void setup() {
Serial.begin(115200);
// Start Wire and probe the device
sensor.Begin();
if (!sensor.Detect()) {
Serial.println(F("Device not found"));
return;
}
// Read register 0x00 — explicit status (recommended)
uint8_t value = 0x00;
if (sensor.Try_Read_Register(0x00, &value)) {
Serial.print(F("Register 0x00 = 0x"));
Serial.println(value, HEX);
}
}
void loop() {}uint16_t raw = 0x0000;
// Pass true for MSB-first (big-endian) — common for most sensors
if (sensor.Try_Read_Register_Word(0x02, &raw, true)) {
Serial.println(raw);
}// Set bit 3 of register 0x0F
sensor.Set_Register_Bit(0x0F, 3);
// Clear bit 3 of register 0x0F
sensor.Clear_Register_Bit(0x0F, 3);
// Toggle bit 3 of register 0x0F
sensor.Toggle_Register_Bit(0x0F, 3);
// Write a specific value to bit 3 (set or clear depending on bool)
sensor.Write_Register_Bit(0x0F, 3, true);// Channel 3 on a TCA9548 at the default address (0x70)
I2C_Functions mux_sensor(__I2C_Addr_HDC2010__, true, 3);
void setup() {
// The library switches the mux to channel 3 automatically
// before every read or write transaction
mux_sensor.Begin();
}The library exposes two parallel families for every operation.
These functions return bool: true on success, false on any failure (device absent, NACK, buffer overflow, bad pointer). Use these in production code.
uint8_t val = 0x00;
if (sensor.Try_Read_Register(0x00, &val)) {
// val is valid
}These functions return the value directly and silently fall back to 0 / false on failure. Useful when you don't need error handling, but hiding failures can mask bus issues.
uint8_t val = sensor.Read_Register(0x00);
// val is 0x00 on failure — indistinguishable from a real 0x00 read
// Use Try_Read_Register for error-aware codeSeveral write and multi-byte read functions expose a stop parameter:
| Value | Effect |
|---|---|
true (default) |
Sends a STOP condition — ends the transaction normally |
false |
Sends a repeated START — keeps the bus active for devices that require it |
Consult your device's datasheet to know which is needed.
Word (16-bit) read and write functions support configurable byte order:
| Value | Byte order | Use when |
|---|---|---|
false (default) |
Little-endian — LSB first | Device sends/expects LSB at lower address |
true |
Big-endian — MSB first | Most I2C sensors (MPU, BMP, HDC, etc.) |
explicit I2C_Functions(uint8_t address, bool mux_enable = false, uint8_t mux_channel = 0)| Parameter | Type | Description |
|---|---|---|
address |
uint8_t |
I2C address of the target device |
mux_enable |
bool |
true to enable automatic mux channel switching |
mux_channel |
uint8_t |
Channel number 1–8; values outside that range disable the mux |
I2C_Functions sensor(0x40); // Direct device
I2C_Functions muxed(0x40, true, 3); // Behind mux channel 3
I2C_Functions wire1_dev(0x52, false, 0); // Will use Wire1 via Begin(&Wire1)Starts the TwoWire bus and runs an initial device detection.
void Begin(TwoWire *twi = &Wire)sensor.Begin(); // Uses Wire (default)
sensor.Begin(&Wire1); // Uses Wire1Stops the active TwoWire bus.
void End(void)Re-runs device detection and updates the internal state. Call this if the device may appear after startup or following a bus reset.
void Detect_Device(void)Returns the cached detection result (updated by Begin and Detect_Device).
bool Detect(void)
// Returns: true if the device acknowledged its address, false otherwiseReturns the configured device address.
uint8_t Address(void)Query multiplexer configuration.
bool Mux_Enable(void) // true if mux is active for this object
uint8_t Mux_Channel(void) // resolved bitmask (0x01–0x80), or 0x00 if disabled// Status-returning — recommended
bool Try_Read_Register(uint8_t reg, uint8_t *data)
// Simple wrapper — returns 0x00 on failure
uint8_t Read_Register(uint8_t reg)uint8_t val = 0x00;
if (sensor.Try_Read_Register(0x00, &val)) {
// val holds the register content
}// Status-returning
bool Try_Read_Register_Word(uint8_t reg, uint16_t *data, bool msb_first = false)
// Simple wrapper — returns 0x0000 on failure
uint16_t Read_Register_Word(uint8_t reg, bool msb_first = false)uint16_t raw = 0x0000;
// Little-endian device (LSB first) — default
sensor.Try_Read_Register_Word(0x04, &raw);
// Big-endian device (MSB first) — most sensors
sensor.Try_Read_Register_Word(0x04, &raw, true);bool Read_Multiple_Register(uint8_t reg, uint8_t *data, uint8_t length, bool stop = true)uint8_t buf[6];
if (sensor.Read_Multiple_Register(0x3B, buf, 6)) {
// buf contains 6 bytes starting from register 0x3B
}For devices that use a two-byte register pointer (EEPROMs, some image sensors).
bool Read_Multiple_Register_u16(uint16_t reg, uint8_t *data, uint8_t length, bool stop = true)The register address is sent MSB-first (high byte then low byte) as required by most devices with 16-bit addressing.
uint8_t page[16];
sensor.Read_Multiple_Register_u16(0x0100, page, 16);For devices that stream data immediately after a read request, without needing a register address first.
bool Read_Multiple_Register_NoCMD(uint8_t *data, uint8_t length)uint8_t frame[4];
sensor.Read_Multiple_Register_NoCMD(frame, 4);// Status-returning — bit value written to output pointer
bool Try_Read_Register_Bit(uint8_t reg, uint8_t bit_number, bool *bit_value)
// Simple wrapper — returns false on failure
bool Read_Register_Bit(uint8_t reg, uint8_t bit_number)bool data_ready = false;
if (sensor.Try_Read_Register_Bit(0x00, 7, &data_ready)) {
if (data_ready) { /* process */ }
}bool Write_Register(uint8_t reg, uint8_t data, bool stop = true)sensor.Write_Register(0x0F, 0x22);bool Write_Register_Word(uint8_t reg, uint16_t data, bool stop = true, bool msb_first = false)// Little-endian write (default)
sensor.Write_Register_Word(0x10, 0x1234);
// Big-endian write — sends 0x12 then 0x34
sensor.Write_Register_Word(0x10, 0x1234, true, true);bool Write_Multiple_Register(uint8_t reg, const uint8_t *data, uint8_t length)uint8_t config[] = {0x01, 0x80, 0x00};
sensor.Write_Multiple_Register(0x10, config, 3);The Wire internal buffer is 32 bytes on AVR. Writing more bytes than the buffer holds will abort the transaction and return false.
For devices that accept command bytes directly without a preceding register field.
bool Write_Command(uint8_t command, bool stop = true)sensor.Write_Command(0xFE); // soft reset command, for examplebool Write_Multiple_Command(const uint8_t *command, uint8_t length)uint8_t seq[] = {0xAC, 0x33, 0x00};
sensor.Write_Multiple_Command(seq, 3);All bit operations perform a read-modify-write cycle. The bit number must be in the range 0–7 (bit 0 = LSB).
Reads the register, sets the target bit if it is not already set, and writes back. Skips the write if the bit is already 1.
bool Set_Register_Bit(uint8_t reg, uint8_t bit_number, bool stop = true)sensor.Set_Register_Bit(0x0F, 3); // set bit 3Reads the register, clears the target bit if it is not already clear, and writes back. Skips the write if the bit is already 0.
bool Clear_Register_Bit(uint8_t reg, uint8_t bit_number, bool stop = true)sensor.Clear_Register_Bit(0x0F, 3); // clear bit 3Reads the register, inverts the target bit, and writes back unconditionally.
bool Toggle_Register_Bit(uint8_t reg, uint8_t bit_number, bool stop = true)sensor.Toggle_Register_Bit(0x0F, 3); // flip bit 3Convenience wrapper that calls Set_Register_Bit or Clear_Register_Bit based on the boolean value.
bool Write_Register_Bit(uint8_t reg, uint8_t bit_number, bool bit_value, bool stop = true)bool enable = true;
sensor.Write_Register_Bit(0x0F, 3, enable); // sets bit 3
sensor.Write_Register_Bit(0x0F, 3, !enable); // clears bit 3The library integrates directly with a TCA9548A (or compatible) I2C multiplexer at the default address 0x70.
When mux support is enabled, every read and write transaction automatically:
- Reads the current mux channel register
- Switches to the configured channel only if it has changed
- Runs the actual I2C transaction with the target device
This makes it transparent — you call Try_Read_Register and the mux is handled for you.
// Channel numbers 1–8 map to TCA9548 bitmasks 0x01–0x80
I2C_Functions temp_sensor(__I2C_Addr_SHT21__, true, 2); // channel 2
I2C_Functions pres_sensor(__I2C_Addr_MPL3115A2__, true, 5); // channel 5
void setup() {
temp_sensor.Begin();
pres_sensor.Begin();
}
void loop() {
uint8_t t_raw;
temp_sensor.Try_Read_Register(0x00, &t_raw); // auto-switches to channel 2
uint16_t p_raw;
pres_sensor.Try_Read_Register_Word(0x01, &p_raw, true); // auto-switches to channel 5
}src/Definition.h provides typed constexpr uint8_t constants for commonly used devices and mux configuration.
constexpr uint8_t __I2C_Addr_TCA9548__ = 0x70; // I2C Multiplexer
constexpr uint8_t __I2C_Addr_RV3028C7__ = 0x52; // RTC
constexpr uint8_t __I2C_Addr_DS28C__ = 0x50; // EEPROM
constexpr uint8_t __I2C_Addr_HDC2010__ = 0x40; // Temp / Humidity
constexpr uint8_t __I2C_Addr_MPL3115A2__ = 0x60; // Pressure / Altitude
constexpr uint8_t __I2C_Addr_TSL2561__ = 0x39; // Light
constexpr uint8_t __I2C_Addr_SI1145__ = 0x60; // UV / IR / Visible
constexpr uint8_t __I2C_Addr_MAX17055__ = 0x36; // Fuel Gauge
constexpr uint8_t __I2C_Addr_BQ24298__ = 0x6B; // Battery Charger
constexpr uint8_t __I2C_Addr_SHT21__ = 0x40; // Temp / Humidity
constexpr uint8_t __I2C_Addr_SDP810__ = 0x25; // Differential Pressure
constexpr uint8_t __I2C_Addr_PCF8574_A__ = 0x3F; // I/O Expander
constexpr uint8_t __I2C_Addr_PCF8574_B__ = 0x3B; // I/O Expander
constexpr uint8_t __I2C_Addr_PCF8574_C__ = 0x39; // I/O Expander
constexpr uint8_t __I2C_Addr_PCF8574_D__ = 0x3E; // I/O Expander
constexpr uint8_t __I2C_Addr_NA2302__ = 0x3A; // Humidityconstexpr uint8_t __Mux_Address__ = 0x70; // TCA9548 default address
constexpr uint8_t __Mux_Channel_Off__ = 0x00;
constexpr uint8_t __Mux_Channel_1__ = 0x01;
constexpr uint8_t __Mux_Channel_2__ = 0x02;
constexpr uint8_t __Mux_Channel_3__ = 0x04;
constexpr uint8_t __Mux_Channel_4__ = 0x08;
constexpr uint8_t __Mux_Channel_5__ = 0x10;
constexpr uint8_t __Mux_Channel_6__ = 0x20;
constexpr uint8_t __Mux_Channel_7__ = 0x40;
constexpr uint8_t __Mux_Channel_8__ = 0x80;Using constexpr instead of #define gives you full type safety — the compiler checks that you are passing uint8_t values, and debuggers can display the constant by name.
src/
Definition.h constexpr address and mux constants
I2C_Functions.h Full single-header library implementation
examples/
Device_Control/
Device_Control.ino Basic read and optional write demonstration
library.properties Arduino Library Manager metadata
library.json PlatformIO registry metadata
VERSION Single source of truth for the release version
platformio.ini Local build and validation configuration
- Transport only — This library handles the I2C framing. It does not interpret sensor data, apply calibration, or implement device-specific protocols.
- Register safety — Writing to a register that is not documented as writable can lock or damage a device. Always consult the datasheet.
- Wire buffer limit — The Arduino Wire library buffers up to 32 bytes per transaction on AVR.
Write_Multiple_RegisterandWrite_Multiple_Commanddetect a buffer overflow (Wire'swrite()returns 0) and abort cleanly, always sending a STOP before returningfalse. - Mux address — The mux address defaults to
0x70. If your TCA9548 is at a different address, override__Mux_Address__before including the library. - Simple wrappers hide errors —
Read_Register,Read_Register_Word, andRead_Register_Bitreturn0/falseon failure. Use theTry_*variants when error handling matters. - Endianness is your responsibility — Pass
msb_first = truefor big-endian sensors (the majority) andmsb_first = falsefor little-endian ones. The library does not auto-detect byte order.