LED Driver Module - PCA9685 12-bit PWM LED Driver
This module provides support for the PCA9685 12-bit PWM LED driver chip via I2C. It supports multiple chips daisy-chained on a single I2C bus, with double-buffered DMA transfers for flicker-free LED updates.
Features:
- 16 channels per chip, multiple chips supported
- 12-bit PWM resolution (0-4095)
- Built-in gamma correction (8-bit brightness)
- Double-buffered DMA for smooth updates
- Configurable buffer persistence
Example:
import panicoverride import nimphea import nimphea/per/i2c import nimphea/dev/leddriver # Allocate DMA buffers in D2 memory var bufferA {.section: ".sram_d2".}: LedDriverDmaBuffer[2] var bufferB {.section: ".sram_d2".}: LedDriverDmaBuffer[2] # Configure for 2 PCA9685 chips (32 LEDs total) var config: LedDriverConfig[2] config.i2c_config.periph = I2C_1 config.i2c_config.speed = I2C_400KHZ config.i2c_config.scl = D11() config.i2c_config.sda = D12() config.addresses = [0'u8, 1'u8] # Chip addresses config.oe_pin = D10() # Optional output enable pin var driver: LedDriverPca9685[2] driver.init(config, addr(bufferA), addr(bufferB)) # Set LED brightness (0.0 - 1.0) driver.setLed(0, 0.5) # LED 0 at 50% brightness driver.setAllTo(1.0) # All LEDs full brightness # Update LEDs (non-blocking DMA transfer) driver.swapBuffersAndTransmit()
Types
FieldLedDriver = LedDriverPca9685[2, true]
- Source Edit
LedDriverConfig[N] = object i2c_config*: I2CConfig ## I2C peripheral configuration addresses*: array[N, uint8] ## I2C addresses for each chip (0-63, ORed with base address) oe_pin*: Pin ## Optional output enable pin (active low)
- Source Edit
LedDriverDmaBuffer[N] = array[N, Pca9685TransmitBuffer]
- Source Edit
LedDriverPca9685[N; PersistentBuffer] = object i2c*: I2CHandle drawBuffer*: ptr LedDriverDmaBuffer[N] transmitBuffer*: ptr LedDriverDmaBuffer[N] addresses*: array[N, uint8] oePin*: Pin oePinGpio*: GPIO currentDriverIdx*: int8 ## -1 when idle, 0..N-1 during transmission
- Source Edit
Pca9685TransmitBuffer = object registerAddr*: uint8 ## Register address (always PCA9685_LED0) leds*: array[16, tuple[on: uint16, off: uint16]] ## 16 LED on/off values
- Source Edit
Consts
GAMMA_TABLE = [0'u16, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 4, 4, 5, 5, 6, 7, 8, 8, 9, 10, 11, 12, 13, 15, 16, 17, 18, 20, 21, 23, 25, 26, 28, 30, 32, 34, 36, 38, 40, 43, 45, 48, 50, 53, 56, 59, 62, 65, 68, 71, 75, 78, 82, 85, 89, 93, 97, 101, 105, 110, 114, 119, 123, 128, 133, 138, 143, 149, 154, 159, 165, 171, 177, 183, 189, 195, 202, 208, 215, 222, 229, 236, 243, 250, 258, 266, 273, 281, 290, 298, 306, 315, 324, 332, 341, 351, 360, 369, 379, 389, 399, 409, 419, 430, 440, 451, 462, 473, 485, 496, 508, 520, 532, 544, 556, 569, 582, 594, 608, 621, 634, 648, 662, 676, 690, 704, 719, 734, 749, 764, 779, 795, 811, 827, 843, 859, 876, 893, 910, 927, 944, 962, 980, 998, 1016, 1034, 1053, 1072, 1091, 1110, 1130, 1150, 1170, 1190, 1210, 1231, 1252, 1273, 1294, 1316, 1338, 1360, 1382, 1404, 1427, 1450, 1473, 1497, 1520, 1544, 1568, 1593, 1617, 1642, 1667, 1693, 1718, 1744, 1770, 1797, 1823, 1850, 1877, 1905, 1932, 1960, 1988, 2017, 2045, 2074, 2103, 2133, 2162, 2192, 2223, 2253, 2284, 2315, 2346, 2378, 2410, 2442, 2474, 2507, 2540, 2573, 2606, 2640, 2674, 2708, 2743, 2778, 2813, 2849, 2884, 2920, 2957, 2993, 3030, 3067, 3105, 3143, 3181, 3219, 3258, 3297, 3336, 3376, 3416, 3456, 3496, 3537, 3578, 3619, 3661, 3703, 3745, 3788, 3831, 3874, 3918, 3962, 4006, 4050, 4095'u16]
- Source Edit
PCA9685_I2C_BASE_ADDRESS = 0b01000000'u8
- Source Edit
PCA9685_LED0 = 0x06'u8
- Source Edit
PCA9685_MODE1 = 0x00'u8
- Source Edit
PCA9685_MODE2 = 0x01'u8
- Source Edit
PCA9685_PRESCALE = 0xFE'u8
- Source Edit
Procs
proc clearAllLeds(driver: var FieldLedDriver) {....raises: [], tags: [], forbids: [].}
- Turn off all Field LEDs (26 total) Source Edit
proc continueTransmission[N, P](driver: var LedDriverPca9685[N, P])
- Source Edit
proc delayTicks(ticks: uint32) {.importcpp: "daisy::System::DelayTicks(@)", header: "sys/system.h", ...raises: [], tags: [], forbids: [].}
- Source Edit
proc getDriverChannelForLed[N, P](driver: LedDriverPca9685[N, P]; ledIndex: int): int {. inline.}
- Get which channel (0..15) on a chip the LED uses Source Edit
proc getDriverForLed[N, P](driver: LedDriverPca9685[N, P]; ledIndex: int): int {. inline.}
- Get which PCA9685 chip contains the given LED (0..N-1) Source Edit
proc getNumLeds[N, P](driver: LedDriverPca9685[N, P]): int {.inline.}
- Returns total number of LEDs (16 per chip) Source Edit
proc getStartCycleForLed[N, P](driver: LedDriverPca9685[N, P]; ledIndex: int): uint16 {. inline.}
- Get staggered start cycle for LED (reduces current spikes) Source Edit
proc init[N, P](driver: var LedDriverPca9685[N, P]; config: LedDriverConfig[N]; dmaBufferA: ptr LedDriverDmaBuffer[N]; dmaBufferB: ptr LedDriverDmaBuffer[N])
-
Initialize the LED driver with DMA buffers
DMA buffers must be allocated in D2 memory:
var bufferA {.section: ".sram_d2".}: LedDriverDmaBuffer[2] var bufferB {.section: ".sram_d2".}: LedDriverDmaBuffer[2] driver.init(config, addr(bufferA), addr(bufferB))
Source Edit proc initializeBuffers[N, P](driver: var LedDriverPca9685[N, P])
- Initialize both DMA buffers with staggered LED start cycles Source Edit
proc initializeDrivers[N, P](driver: var LedDriverPca9685[N, P])
- Initialize all PCA9685 chips via I2C Source Edit
proc setAllTo[N, P](driver: var LedDriverPca9685[N, P]; brightness: float32)
- Set all LEDs to gamma-corrected brightness (0.0-1.0) Source Edit
proc setAllTo[N, P](driver: var LedDriverPca9685[N, P]; brightness: uint8)
- Set all LEDs to gamma-corrected brightness (0-255) Source Edit
proc setAllToRaw[N, P](driver: var LedDriverPca9685[N, P]; rawBrightness: uint16)
- Set all LEDs to raw 12-bit brightness (0-4095) Source Edit
proc setLed(driver: var FieldLedDriver; ledNum: int; brightness: float32) {. ...raises: [], tags: [], forbids: [].}
-
Set Field LED brightness (0.0-1.0) with gamma correction
Parameters:
- ledNum - LED index 0-25 (16 keyboard + 8 knob + 2 switch LEDs)
- brightness - Brightness level 0.0 (off) to 1.0 (full)
LED Mapping:
- LEDs 0-15: Keyboard LEDs (Chip 0, channels 0-15)
- LEDs 16-23: Knob LEDs (Chip 1, channels 0-7)
- LEDs 24-25: Switch LEDs (Chip 1, channels 8-9)
proc setLed[N, P](driver: var LedDriverPca9685[N, P]; ledIndex: int; brightness: float32)
- Set a single LED to gamma-corrected brightness (0.0-1.0) Source Edit
proc setLed[N, P](driver: var LedDriverPca9685[N, P]; ledIndex: int; brightness: uint8)
- Set a single LED to gamma-corrected brightness (0-255) Source Edit
proc setLedRaw[N, P](driver: var LedDriverPca9685[N, P]; ledIndex: int; rawBrightness: uint16)
- Set a single LED to raw 12-bit brightness (0-4095) Source Edit
proc swapBuffersAndTransmit(driver: var FieldLedDriver): bool {.inline, ...raises: [], tags: [], forbids: [].}
-
Swap buffers and transmit to Field LEDs via DMA
Returns: true if transmission started successfully
Note: Uses 100ms timeout for DMA transfer initialization
Source Edit proc swapBuffersAndTransmit[N, P](driver: var LedDriverPca9685[N, P]; timeoutMs: int = 100): bool
-
Swap draw and transmit buffers, then start DMA transmission to all chips This is non-blocking - transmission happens in background via DMA
Parameters:
- timeoutMs - Maximum time to wait for previous transmission (default 100ms)
Returns:
- true if buffers swapped and transmission started successfully
- false if timeout occurred waiting for previous transmission
Note: On timeout, the driver state is reset to prevent hangs, but the previous transmission may have been incomplete.
Source Edit