Platform Integration Guide
This guide explains how to implement the hardware abstraction interface for the PCA9685 driver on your platform.
Understanding CRTP (Curiously Recurring Template Pattern)
The PCA9685 driver uses CRTP (Curiously Recurring Template Pattern) for hardware abstraction. This design choice provides several critical benefits for embedded systems:
Why CRTP Instead of Virtual Functions?
1. Zero Runtime Overhead
- Virtual functions: Require a vtable lookup (indirect call) = ~5-10 CPU cycles overhead per call
- CRTP: Direct function calls = 0 overhead, compiler can inline
- Impact: In time-critical embedded code, this matters significantly
2. Compile-Time Polymorphism
- Virtual functions: Runtime dispatch - the compiler cannot optimize across the abstraction boundary
- CRTP: Compile-time dispatch - full optimization, dead code elimination, constant propagation
- Impact: Smaller code size, faster execution
3. Memory Efficiency
- Virtual functions: Each object needs a vtable pointer (4-8 bytes)
- CRTP: No vtable pointer needed
- Impact: Critical in memory-constrained systems (many MCUs have <64KB RAM)
4. Type Safety
- Virtual functions: Runtime errors if method not implemented
- CRTP: Compile-time errors if method not implemented
- Impact: Catch bugs at compile time, not in the field
How CRTP Works
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Base template class (from pca9685_i2c_interface.hpp)
template <typename Derived>
class I2cInterface {
public:
bool Write(uint8_t addr, uint8_t reg, const uint8_t *data, size_t len) noexcept {
// Cast 'this' to Derived* and call the derived implementation
return static_cast<Derived*>(this)->Write(addr, reg, data, len);
}
bool Read(uint8_t addr, uint8_t reg, uint8_t *data, size_t len) noexcept {
return static_cast<Derived*>(this)->Read(addr, reg, data, len);
}
bool EnsureInitialized() noexcept {
return static_cast<Derived*>(this)->EnsureInitialized();
}
};
// Your implementation
class MyI2c : public pca9685::I2cInterface<MyI2c> {
public:
// This method is called directly (no virtual overhead)
bool Write(uint8_t addr, uint8_t reg, const uint8_t *data, size_t len) noexcept {
// Your platform-specific I2C code
}
bool Read(uint8_t addr, uint8_t reg, uint8_t *data, size_t len) noexcept {
// Your platform-specific I2C code
}
bool EnsureInitialized() noexcept {
// Lazy-initialize your I2C bus
}
};
The key insight: static_cast<Derived*>(this) allows the base class to call methods on the derived
class at compile time, not runtime.
Performance Comparison
| Aspect | Virtual Functions | CRTP |
|---|---|---|
| Function call overhead | ~5-10 cycles | 0 cycles (inlined) |
| Code size | Larger (vtables) | Smaller (optimized) |
| Memory per object | +4-8 bytes (vptr) | 0 bytes |
| Compile-time checks | No | Yes |
| Optimization | Limited | Full |
Interface Definition
The PCA9685 driver requires you to implement the I2cInterface template:
Location: inc/pca9685_i2c_interface.hpp (interface); driver class in inc/pca9685.hpp
1
2
3
4
5
6
7
8
template <typename Derived>
class I2cInterface {
public:
// Required methods (implement all three)
bool Write(uint8_t addr, uint8_t reg, const uint8_t *data, size_t len) noexcept;
bool Read(uint8_t addr, uint8_t reg, uint8_t *data, size_t len) noexcept;
bool EnsureInitialized() noexcept;
};
Note: I2cInterface is non-copyable and non-movable; use references or pointers to your concrete bus type.
Method Requirements:
Write(): Writelenbytes fromdatato registerregat I2C addressaddr(7-bit address)Read(): Readlenbytes intodatafrom registerregat I2C addressaddr(7-bit address)EnsureInitialized(): Lazy-initialize the I2C bus; return true if ready- Write/Read return
trueon success,falseon failure (NACK, timeout, etc.)
Optional retry delay: The driver can call an optional callback between I2C retries for bus
recovery. Your bus class can expose a static delay (e.g.
static void RetryDelay() noexcept { vTaskDelay(pdMS_TO_TICKS(1)); }). After constructing the
driver, the application calls driver->SetRetryDelay(MyBus::RetryDelay). If not set, no delay is
used between retries.
Implementation Steps
Step 1: Create Your Implementation Class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include "pca9685.hpp"
class MyPlatformI2c : public pca9685::I2cInterface<MyPlatformI2c> {
private:
// Your platform-specific members
i2c_handle_t i2c_handle_;
bool initialized_ = false;
public:
// Constructor
MyPlatformI2c(i2c_handle_t handle) : i2c_handle_(handle) {}
// Implement required methods (NO virtual keyword!)
bool Write(uint8_t addr, uint8_t reg, const uint8_t *data, size_t len) noexcept {
// Your I2C write implementation
return true;
}
bool Read(uint8_t addr, uint8_t reg, uint8_t *data, size_t len) noexcept {
// Your I2C read implementation
return true;
}
bool EnsureInitialized() noexcept {
if (initialized_) return true;
// Initialize I2C hardware...
initialized_ = true;
return true;
}
};
Step 2: Platform-Specific Examples
ESP32 (ESP-IDF)
Location: See examples/esp32/main/esp32_pca9685_bus.hpp
for a complete ESP32 implementation using ESP-IDFβs I2C master driver API.
For a complete working example, see examples/esp32/main/pca9685_comprehensive_test.cpp.
1
2
3
4
5
6
7
8
9
10
11
12
13
#include "driver/i2c_master.h"
#include "pca9685.hpp"
#include "esp32_pca9685_bus.hpp"
// Use the provided ESP32 bus implementation
auto i2c_bus = CreateEsp32Pca9685Bus();
pca9685::PCA9685<Esp32Pca9685Bus> pwm(i2c_bus.get(), 0x40);
// Initialize
if (!pwm.Reset()) {
// Handle error
return;
}
STM32 (HAL)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include "stm32f4xx_hal.h"
#include "pca9685.hpp"
extern I2C_HandleTypeDef hi2c1;
class STM32I2cBus : public pca9685::I2cInterface<STM32I2cBus> {
public:
bool Write(uint8_t addr, uint8_t reg, const uint8_t *data, size_t len) noexcept {
// STM32 HAL uses 8-bit address (7-bit << 1)
return HAL_I2C_Mem_Write(&hi2c1, addr << 1, reg,
I2C_MEMADD_SIZE_8BIT,
(uint8_t*)data, len,
HAL_MAX_DELAY) == HAL_OK;
}
bool Read(uint8_t addr, uint8_t reg, uint8_t *data, size_t len) noexcept {
return HAL_I2C_Mem_Read(&hi2c1, addr << 1, reg,
I2C_MEMADD_SIZE_8BIT,
data, len,
HAL_MAX_DELAY) == HAL_OK;
}
bool EnsureInitialized() noexcept { return true; /* HAL_I2C_Init done elsewhere */ }
};
Arduino
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <Wire.h>
#include "pca9685.hpp"
class ArduinoI2cBus : public pca9685::I2cInterface<ArduinoI2cBus> {
public:
bool Write(uint8_t addr, uint8_t reg, const uint8_t *data, size_t len) noexcept {
Wire.beginTransmission(addr);
Wire.write(reg);
Wire.write(data, len);
return Wire.endTransmission() == 0;
}
bool Read(uint8_t addr, uint8_t reg, uint8_t *data, size_t len) noexcept {
Wire.beginTransmission(addr);
Wire.write(reg);
if (Wire.endTransmission(false) != 0) return false;
Wire.requestFrom(addr, len);
for (size_t i = 0; i < len && Wire.available(); i++) {
data[i] = Wire.read();
}
return true;
}
bool EnsureInitialized() noexcept { Wire.begin(); return true; }
};
Common Pitfalls
β Donβt Use Virtual Functions
1
2
3
4
5
6
7
// WRONG - defeats the purpose of CRTP
class MyI2c : public pca9685::I2cInterface<MyI2c> {
public:
virtual bool Write(...) override { // β Virtual keyword not needed
// ...
}
};
β Correct CRTP Implementation
1
2
3
4
5
6
7
// CORRECT - no virtual keyword, PascalCase, noexcept
class MyI2c : public pca9685::I2cInterface<MyI2c> {
public:
bool Write(...) noexcept { // β
Direct implementation
// ...
}
};
β Donβt Forget the Template Parameter
1
2
3
4
// WRONG - missing template parameter
class MyI2c : public pca9685::I2cInterface { // β Compiler error
// ...
};
β Correct Template Parameter
1
2
3
4
// CORRECT - pass your class as template parameter
class MyI2c : public pca9685::I2cInterface<MyI2c> { // β
// ...
};
β Address Format Confusion
The driver uses 7-bit I2C addresses. Some platforms use 8-bit addresses (7-bit « 1):
1
2
3
4
5
// WRONG - if your platform uses 8-bit addresses
i2c_write(addr, ...); // β Should be addr << 1
// CORRECT
i2c_write(addr << 1, ...); // β
Convert 7-bit to 8-bit
Testing Your Implementation
After implementing the interface, test it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
MyPlatformI2c i2c;
pca9685::PCA9685<MyPlatformI2c> pwm(&i2c, 0x40);
if (pwm.Reset()) {
// Interface works!
pwm.SetPwmFreq(50.0f);
pwm.SetDuty(0, 0.5f);
} else {
// Check error flags (uint16_t bitmask)
auto flags = pwm.GetErrorFlags();
// Or use convenience accessor:
auto error = pwm.GetLastError();
// Debug your I2C implementation
pwm.ClearErrorFlags(flags); // Clear after handling
}
Next Steps
- See Configuration for driver configuration options
- Check Examples for complete usage examples
- Review API Reference for all available methods
Navigation β¬ οΈ Hardware Setup | Next: Configuration β‘οΈ | Back to Index