nimphea/dev/leddriver

Search:
Group by:
Source   Edit  

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

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  
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 delayMs(ms: uint32) {.importcpp: "daisy::System::Delay(@)",
                           header: "sys/system.h", ...raises: [], tags: [],
                           forbids: [].}
Source   Edit  
proc delayTicks(ticks: uint32) {.importcpp: "daisy::System::DelayTicks(@)",
                                 header: "sys/system.h", ...raises: [], tags: [],
                                 forbids: [].}
Source   Edit  
proc delayUs(us: uint32) {.importcpp: "daisy::System::DelayUs(@)",
                           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)
Source   Edit  
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