Skip to content

File leddriver.h

File List > dev > leddriver.h

Go to the documentation of this file

Source Code

#pragma once
#ifndef SA_LED_DRIVER_H
#define SA_LED_DRIVER_H 
#ifdef __cplusplus

#include <stdint.h>
#include "per/i2c.h"
#include "per/gpio.h"

namespace daisy
{
template <int numDrivers, bool persistentBufferContents = true>
class LedDriverPca9685
{
  public:
    struct __attribute__((packed)) PCA9685TransmitBuffer
    {
        uint8_t registerAddr = PCA9685_LED0;
        struct __attribute__((packed))
        {
            uint16_t on;
            uint16_t off;
        } leds[16];
        static constexpr uint16_t size = 16 * 4 + 1;
    };
    using DmaBuffer = PCA9685TransmitBuffer[numDrivers];

    void Init(I2CHandle i2c,
              const uint8_t (&addresses)[numDrivers],
              DmaBuffer dma_buffer_a,
              DmaBuffer dma_buffer_b,
              Pin       oe_pin = Pin(PORTX, 0))
    {
        i2c_             = i2c;
        draw_buffer_     = dma_buffer_a;
        transmit_buffer_ = dma_buffer_b;
        oe_pin_          = oe_pin;
        for(int d = 0; d < numDrivers; d++)
            addresses_[d] = addresses[d];
        current_driver_idx_ = -1;

        InitializeBuffers();
        InitializeDrivers();
    }

    constexpr int GetNumLeds() const { return numDrivers * 16; }

    void SetAllTo(float brightness)
    {
        const uint8_t intBrightness
            = (uint8_t)(clamp(brightness * 255.0f, 0.0f, 255.0f));
        SetAllTo(intBrightness);
    }

    void SetAllTo(uint8_t brightness)
    {
        const uint16_t cycles = gamma_table_[brightness];
        SetAllToRaw(cycles);
    }

    void SetAllToRaw(uint16_t rawBrightness)
    {
        for(int led = 0; led < GetNumLeds(); led++)
            SetLedRaw(led, rawBrightness);
    }

    void SetLed(int ledIndex, float brightness)
    {
        const uint8_t intBrightness
            = (uint8_t)(clamp(brightness * 255.0f, 0.0f, 255.0f));
        SetLed(ledIndex, intBrightness);
    }

    void SetLed(int ledIndex, uint8_t brightness)
    {
        const uint16_t cycles = gamma_table_[brightness];
        SetLedRaw(ledIndex, cycles);
    }

    void SetLedRaw(int ledIndex, uint16_t rawBrightness)
    {
        const auto d  = GetDriverForLed(ledIndex);
        const auto ch = GetDriverChannelForLed(ledIndex);
        // mask away the "full on" bit
        const auto on                = draw_buffer_[d].leds[ch].on & (0x0FFF);
        draw_buffer_[d].leds[ch].off = (on + rawBrightness) & (0x0FFF);
        // full on condition
        if(rawBrightness >= 0x0FFF)
            draw_buffer_[d].leds[ch].on = 0x1000 | on; // set "full on" bit
        else
            draw_buffer_[d].leds[ch].on = on; // clear "full on" bit
    }

    void SwapBuffersAndTransmit()
    {
        // wait for current transmission to complete
        while(current_driver_idx_ >= 0) {};

        // swap buffers
        auto tmp         = transmit_buffer_;
        transmit_buffer_ = draw_buffer_;
        draw_buffer_     = tmp;

        // copy current transmit buffer contents to the new draw buffer
        // to keep the led settings (if required)
        if(persistentBufferContents)
        {
            for(int d = 0; d < numDrivers; d++)
                for(int ch = 0; ch < 16; ch++)
                    draw_buffer_[d].leds[ch].off
                        = transmit_buffer_[d].leds[ch].off;
        }

        // start transmission
        current_driver_idx_ = -1;
        ContinueTransmission();
    }

  private:
    void ContinueTransmission()
    {
        current_driver_idx_ = current_driver_idx_ + 1;
        if(current_driver_idx_ >= numDrivers)
        {
            current_driver_idx_ = -1;
            return;
        }

        const auto    d       = current_driver_idx_;
        const uint8_t address = PCA9685_I2C_BASE_ADDRESS | addresses_[d];
        const auto    status  = i2c_.TransmitDma(address,
                                             (uint8_t*)&transmit_buffer_[d],
                                             PCA9685TransmitBuffer::size,
                                             &TxCpltCallback,
                                             this);
        if(status != I2CHandle::Result::OK)
        {
            // TODO: fix this :-)
            // Reinit I2C (probably a flag to kill, but hey this works fairly well for now.)
            i2c_.Init(i2c_.GetConfig());
        }
    }
    uint16_t GetStartCycleForLed(int ledIndex) const
    {
        return (ledIndex << 2) & 0x0FFF; // shift each led by 4 cycles
    }

    uint8_t GetDriverForLed(int ledIndex) const { return ledIndex >> 4; }

    uint8_t GetDriverChannelForLed(int ledIndex) const
    {
        return ledIndex & 0x0F;
    }

    void InitializeBuffers()
    {
        for(int led = 0; led < GetNumLeds(); led++)
        {
            const auto d                     = GetDriverForLed(led);
            const auto ch                    = GetDriverChannelForLed(led);
            const auto startCycle            = GetStartCycleForLed(led);
            draw_buffer_[d].registerAddr     = PCA9685_LED0;
            draw_buffer_[d].leds[ch].on      = startCycle;
            draw_buffer_[d].leds[ch].off     = startCycle;
            transmit_buffer_[d].registerAddr = PCA9685_LED0;
            transmit_buffer_[d].leds[ch].on  = startCycle;
            transmit_buffer_[d].leds[ch].off = startCycle;
        }
    }

    void InitializeDrivers()
    {
        // init OE pin and pull low to enable outputs
        if(oe_pin_.port != PORTX)
        {
            oe_pin_gpio_.Init(oe_pin_, GPIO::Mode::OUTPUT);
            oe_pin_gpio_.Write(0);
        }

        // init the individual drivers
        for(int d = 0; d < numDrivers; d++)
        {
            const uint8_t address = PCA9685_I2C_BASE_ADDRESS | addresses_[d];
            uint8_t       buffer[2];
            buffer[0] = PCA9685_MODE1;
            buffer[1] = 0x00;
            i2c_.TransmitBlocking(address, buffer, 2, 1);
            System::Delay(20);
            buffer[0] = PCA9685_MODE1;
            buffer[1] = 0x00;
            i2c_.TransmitBlocking(address, buffer, 2, 1);
            System::Delay(20);
            buffer[0] = PCA9685_MODE1;
            // auto increment on
            buffer[1] = 0b00100000;
            i2c_.TransmitBlocking(address, buffer, 2, 1);
            System::Delay(20);
            buffer[0] = PCA9685_MODE2;
            // OE-high = high Impedance
            // Push-Pull outputs
            // outputs change on STOP
            // outputs inverted
            buffer[1] = 0b000110110;
            i2c_.TransmitBlocking(address, buffer, 2, 5);
        }
    }

    // no std::clamp available in C++14.... remove this when C++17 is available
    template <typename T>
    T clamp(T in, T low, T high)
    {
        return (in < low) ? low : (high < in) ? high : in;
    }

    // an internal function to handle i2c callbacks
    // called when an I2C transmission completes and the next driver must be updated
    static void TxCpltCallback(void* context, I2CHandle::Result result)
    {
        auto drv_ptr = reinterpret_cast<
            LedDriverPca9685<numDrivers, persistentBufferContents>*>(context);
        drv_ptr->ContinueTransmission();
    }

    I2CHandle              i2c_;
    PCA9685TransmitBuffer* draw_buffer_;
    PCA9685TransmitBuffer* transmit_buffer_;
    uint8_t                addresses_[numDrivers];
    Pin                    oe_pin_;
    GPIO                   oe_pin_gpio_;
    // index of the dirver that is currently updated.
    volatile int8_t current_driver_idx_;
    const uint16_t  gamma_table_[256] = {
        0,    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};

    static constexpr uint8_t PCA9685_I2C_BASE_ADDRESS = 0b01000000;
    static constexpr uint8_t PCA9685_MODE1
        = 0x00; // location for Mode1 register address
    static constexpr uint8_t PCA9685_MODE2
        = 0x01; // location for Mode2 reigster address
    static constexpr uint8_t PCA9685_LED0
        = 0x06; // location for start of LED0 registers
    static constexpr uint8_t PRE_SCALE_MODE
        = 0xFE; //location for setting prescale (clock speed)
};

} // namespace daisy

#endif
#endif