Mirror with some correction
Dependencies: mbed FastIO FastPWM USBDevice
Diff: TLC59116/TLC59116.h
- Revision:
- 87:8d35c74403af
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/TLC59116/TLC59116.h Tue May 09 05:48:37 2017 +0000 @@ -0,0 +1,473 @@ +// TLC59116 interface +// +// The TLC59116 is a 16-channel constant-current PWM controller chip with +// an I2C interface. +// +// Up to 14 of these chips can be connected to a single bus. Each chip needs +// a unique address, configured via four pin inputs. (The I2C address is 7 +// bits, but the high-order 3 bits are fixed in the hardware, leaving 4 bits +// to configure per chip. Two of the possible 16 addresses are reserved by +// the chip hardware as broadcast addresses, leaving room for 14 unique chip +// addresses per bus.) +// +// EXTERNAL PULL-UP RESISTORS ARE REQUIRED ON SDA AND SCL. The internal +// pull-ups in the KL25Z GPIO ports will only work if the bus speed is +// limited to 100kHz. Higher speeds require external pull-ups. Because +// of the relatively high data rate required, we use the maximum 1MHz bus +// speed, requiring external pull-ups. These are typically 2.2K. +// +// This chip is similar to the TLC5940, but has a more modern design with +// several advantages, including a standardized and much more robust data +// interface (I2C) and glitch-free startup. The only downside vs the TLC5940 +// is that it's only available in an SMD package, whereas the TLC5940 is +// available in easy-to-solder DIP format. The DIP 5940 is longer being +// manufactured, but it's still easy to find old stock; when those run out, +// though, and the choice is between SMD 5940 and 59116, the 59116 will be +// the clear winner. +// + +#ifndef _TLC59116_H_ +#define _TLC59116_H_ + +#include "mbed.h" +#include "BitBangI2C.h" + +// Which I2C class are we using? We use this to switch between +// BitBangI2C and MbedI2C for testing and debugging. +#define I2C_Type BitBangI2C + +// register constants +struct TLC59116R +{ + // control register bits + static const uint8_t CTL_AIALL = 0x80; // auto-increment mode, all registers + static const uint8_t CTL_AIPWM = 0xA0; // auto-increment mode, PWM registers only + static const uint8_t CTL_AICTL = 0xC0; // auto-increment mode, control registers only + static const uint8_t CTL_AIPWMCTL = 0xE0; // auto-increment mode, PWM + control registers only + + // register addresses + static const uint8_t REG_MODE1 = 0x00; // MODE1 + static const uint8_t REG_MODE2 = 0x01; // MODE2 + static const uint8_t REG_PWM0 = 0x02; // PWM 0 + static const uint8_t REG_PWM1 = 0x03; // PWM 1 + static const uint8_t REG_PWM2 = 0x04; // PWM 2 + static const uint8_t REG_PWM3 = 0x05; // PWM 3 + static const uint8_t REG_PWM4 = 0x06; // PWM 4 + static const uint8_t REG_PWM5 = 0x07; // PWM 5 + static const uint8_t REG_PWM6 = 0x08; // PWM 6 + static const uint8_t REG_PWM7 = 0x09; // PWM 7 + static const uint8_t REG_PWM8 = 0x0A; // PWM 8 + static const uint8_t REG_PWM9 = 0x0B; // PWM 9 + static const uint8_t REG_PWM10 = 0x0C; // PWM 10 + static const uint8_t REG_PWM11 = 0x0D; // PWM 11 + static const uint8_t REG_PWM12 = 0x0E; // PWM 12 + static const uint8_t REG_PWM13 = 0x0F; // PWM 13 + static const uint8_t REG_PWM14 = 0x10; // PWM 14 + static const uint8_t REG_PWM15 = 0x11; // PWM 15 + static const uint8_t REG_GRPPWM = 0x12; // Group PWM duty cycle + static const uint8_t REG_GRPFREQ = 0x13; // Group frequency register + static const uint8_t REG_LEDOUT0 = 0x14; // LED driver output status register 0 + static const uint8_t REG_LEDOUT1 = 0x15; // LED driver output status register 1 + static const uint8_t REG_LEDOUT2 = 0x16; // LED driver output status register 2 + static const uint8_t REG_LEDOUT3 = 0x17; // LED driver output status register 3 + + // MODE1 bits + static const uint8_t MODE1_AI2 = 0x80; // auto-increment mode enable + static const uint8_t MODE1_AI1 = 0x40; // auto-increment bit 1 + static const uint8_t MODE1_AI0 = 0x20; // auto-increment bit 0 + static const uint8_t MODE1_OSCOFF = 0x10; // oscillator off + static const uint8_t MODE1_SUB1 = 0x08; // subaddress 1 enable + static const uint8_t MODE1_SUB2 = 0x04; // subaddress 2 enable + static const uint8_t MODE1_SUB3 = 0x02; // subaddress 3 enable + static const uint8_t MODE1_ALLCALL = 0x01; // all-call enable + + // MODE2 bits + static const uint8_t MODE2_EFCLR = 0x80; // clear error status flag + static const uint8_t MODE2_DMBLNK = 0x20; // group blinking mode + static const uint8_t MODE2_OCH = 0x08; // outputs change on ACK (vs Stop command) + + // LEDOUTn states + static const uint8_t LEDOUT_OFF = 0x00; // driver is off + static const uint8_t LEDOUT_ON = 0x01; // fully on + static const uint8_t LEDOUT_PWM = 0x02; // individual PWM control via PWMn register + static const uint8_t LEDOUT_GROUP = 0x03; // PWM control + group dimming/blinking via PWMn + GRPPWM +}; + + +// Individual unit object. We create one of these for each unit we +// find on the bus. This keeps track of the state of each output on +// a unit so that we can update outputs in batches, to reduce the +// amount of time we spend in I2C communications during rapid updates. +struct TLC59116Unit +{ + TLC59116Unit() + { + // start inactive, since we haven't been initialized yet + active = false; + + // set all brightness levels to 0 intially + memset(bri, 0, sizeof(bri)); + + // mark all outputs as dirty to force an update after initializing + dirty = 0xFFFF; + } + + // initialize + void init(int addr, I2C_Type &i2c) + { + // set all output drivers to individual PWM control + const uint8_t all_pwm = + TLC59116R::LEDOUT_PWM + | (TLC59116R::LEDOUT_PWM << 2) + | (TLC59116R::LEDOUT_PWM << 4) + | (TLC59116R::LEDOUT_PWM << 6); + static const uint8_t buf[] = { + TLC59116R::REG_LEDOUT0 | TLC59116R::CTL_AIALL, + all_pwm, + all_pwm, + all_pwm, + all_pwm + }; + int err = i2c.write(addr << 1, buf, sizeof(buf)); + + // turn on the oscillator + static const uint8_t buf2[] = { + TLC59116R::REG_MODE1, + TLC59116R::MODE1_AI2 | TLC59116R::MODE1_ALLCALL + }; + err |= i2c.write(addr << 1, buf2, sizeof(buf)); + + // mark the unit as active if the writes succeeded + active = !err; + } + + // Set an output + void set(int idx, int val) + { + // validate the index + if (idx >= 0 && idx <= 15) + { + // record the new brightness + bri[idx] = val; + + // set the dirty bit + dirty |= 1 << idx; + } + } + + // Get an output's current value + int get(int idx) const + { + return idx >= 0 && idx <= 15 ? bri[idx] : -1; + } + + // Send I2C updates + void send(int addr, I2C_Type &i2c) + { + // Scan all outputs. I2C sends are fairly expensive, so we + // minimize the send time by using the auto-increment mode. + // Optimizing this is a bit tricky. Suppose that the outputs + // are in this state, where c represents a clean output and D + // represents a dirty output: + // + // cccDcDccc... + // + // Clearly we want to start sending at the first dirty output + // so that we don't waste time sending the three clean bytes + // ahead of it. However, do we send output[3] as one chunk + // and then send output[5] as a separate chunk, or do we send + // outputs [3],[4],[5] as a single block to take advantage of + // the auto-increment mode? Based on I2C bus timing parameters, + // the answer is that it's cheaper to send this as a single + // contiguous block [3],[4],[5]. The reason is that the cost + // of starting a new block is a Stop/Start sequence plus another + // register address byte; the register address byte costs the + // same as a data byte, so the extra Stop/Start of the separate + // chunk approach makes the single continguous send cheaper. + // But how about this one?: + // + // cccDccDccc... + // + // This one is cheaper to send as two separate blocks. The + // break costs us a Start/Stop plus a register address byte, + // but the Start/Stop is only about 25% of the cost of a data + // byte, so Start/Stop+Register Address is cheaper than sending + // the two clean data bytes sandwiched between the dirty bytes. + // + // So: we want to look for sequences of contiguous dirty bytes + // and send those as a chunk. We furthermore will allow up to + // one clean byte in the midst of the dirty bytes. + uint8_t buf[17]; + int n = 0; + for (int i = 0, bit = 1 ; i < 16 ; ++i, bit <<= 1) + { + // If this one is dirty, include it in the set of outputs to + // send to the chip. Also include this one if it's clean + // and the outputs on both sides are dirty - see the notes + // above about optimizing for the case where we have one clean + // output surrounded by dirty outputs. + if ((dirty & bit) != 0) + { + // it's dirty - add it to the dirty set under construction + buf[++n] = bri[i]; + } + else if (n != 0 && n < 15 && (dirty & (bit << 1)) != 0) + { + // this one is clean, but the one before and the one after + // are both dirty, so keep it in the set anyway to take + // advantage of the auto-increment mode for faster sends + buf[++n] = bri[i]; + } + else + { + // This one is clean, and it's not surrounded by dirty + // outputs. If the set of dirty outputs so far has any + // members, send them now. + if (n != 0) + { + // set the starting register address, including the + // auto-increment flag, and write the block + buf[0] = (TLC59116R::REG_PWM0 + i - n) | TLC59116R::CTL_AIALL; + i2c.write(addr << 1, buf, n + 1); + + // empty the set + n = 0; + } + } + } + + // if we finished the loop with dirty outputs to send, send them + if (n != 0) + { + // fill in the starting register address, and write the block + buf[0] = (TLC59116R::REG_PWM15 + 1 - n) | TLC59116R::CTL_AIALL; + i2c.write(addr << 1, buf, n + 1); + } + + // all outputs are now clean + dirty = 0; + } + + // Is the unit active? If we have trouble writing a unit, + // we can mark it inactive so that we know to stop wasting + // time writing to it, and so that we can re-initialize it + // if it comes back on later bus scans. + bool active; + + // Output states. This records the latest brightness level + // for each output as set by the client. We don't actually + // send these values to the physical unit until the client + // tells us to do an I2C update. + uint8_t bri[16]; + + // Dirty output mask. Whenever the client changes an output, + // we record the new brightness in bri[] and set the + // corresponding bit here to 1. We use these bits to determine + // which outputs to send during each I2C update. + uint16_t dirty; +}; + +// TLC59116 public interface. This provides control over a collection +// of units connected on a common I2C bus. +class TLC59116 +{ +public: + // Initialize. The address given is the configurable part + // of the address, 0x0000 to 0x000F. + TLC59116(PinName sda, PinName scl, PinName reset) + : i2c(sda, scl, true), reset(reset) + { + // Use the fastest I2C speed possible, since we want to be able + // to rapidly update many outputs at once. The TLC59116 can run + // I2C at up to 1MHz. + i2c.frequency(1000000); + + // assert !RESET until we're ready to go + this->reset.write(0); + + // there are no units yet + memset(units, 0, sizeof(units)); + nextUpdate = 0; + } + + void init() + { + // un-assert reset + reset.write(1); + wait_us(10000); + + // scan the bus for new units + scanBus(); + } + + // scan the bus + void scanBus() + { + // scan each possible address + for (int i = 0 ; i < 16 ; ++i) + { + // Address 8 and 11 are reserved - skip them + if (i == 8 || i == 11) + continue; + + // Try reading register REG_MODE1 + int addr = I2C_BASE_ADDR | i; + TLC59116Unit *u = units[i]; + if (readReg8(addr, TLC59116R::REG_MODE1) >= 0) + { + // success - if the slot wasn't already populated, allocate + // a unit entry for it + if (u == 0) + units[i] = u = new TLC59116Unit(); + + // if the unit isn't already marked active, initialize it + if (!u->active) + u->init(addr, i2c); + } + else + { + // failed - if the unit was previously active, mark it + // as inactive now + if (u != 0) + u->active = false; + } + } + } + + // set an output + void set(int unit, int output, int val) + { + if (unit >= 0 && unit <= 15) + { + TLC59116Unit *u = units[unit]; + if (u != 0) + u->set(output, val); + } + } + + // get an output's current value + int get(int unit, int output) + { + if (unit >= 0 && unit <= 15) + { + TLC59116Unit *u = units[unit]; + if (u != 0) + return u->get(output); + } + + return -1; + } + + // Send I2C updates to the next unit. The client must call this + // periodically to send pending updates. We only update one unit on + // each call to ensure that the time per cycle is relatively constant + // (rather than scaling with the number of chips). + void send() + { + // look for a dirty unit + for (int i = 0, n = nextUpdate ; i < 16 ; ++i, ++n) + { + // wrap the unit number + n &= 0x0F; + + // if this unit is populated and dirty, it's the one to update + TLC59116Unit *u = units[n]; + if (u != 0 && u->dirty != 0) + { + // it's dirty - update it + u->send(I2C_BASE_ADDR | n, i2c); + + // We only update one on each call, so we're done. + // Remember where to pick up again on the next update() + // call, and return. + nextUpdate = n + 1; + return; + } + } + } + + // Enable/disable all outputs + void enable(bool f) + { + // visit each populated unit + for (int i = 0 ; i < 16 ; ++i) + { + // if this unit is populated, enable/disable it + TLC59116Unit *u = units[i]; + if (u != 0) + { + // read the current MODE1 register + int m = readReg8(I2C_BASE_ADDR | i, TLC59116R::REG_MODE1); + if (m >= 0) + { + // Turn the oscillator off to disable, on to enable. + // Note that the bit is kind of backwards: SETTING the + // OSC bit turns the oscillator OFF. + if (f) + m &= ~TLC59116R::MODE1_OSCOFF; // enable - clear the OSC bit + else + m |= TLC59116R::MODE1_OSCOFF; // disable - set the OSC bit + + // update MODE1 + writeReg8(I2C_BASE_ADDR | i, TLC59116R::REG_MODE1, m); + } + } + } + } + +protected: + // TLC59116 base I2C address. These chips use an address of + // the form 110xxxx, where the the low four bits are set by + // external pins on the chip. The top three bits are always + // the same, so we construct the full address by combining + // the upper three fixed bits with the four-bit unit number. + // + // Note that addresses 1101011 (0x6B) and 1101000 (0x68) are + // reserved (for SWRSTT and ALLCALL, respectively), and can't + // be used for configured device addresses. + static const uint8_t I2C_BASE_ADDR = 0x60; + + // Units. We populate this with active units we find in + // bus scans. Note that units 8 and 11 can't be used because + // of the reserved ALLCALL and SWRST addresses, but we allocate + // the slots anyway to keep indexing simple. + TLC59116Unit *units[16]; + + // next unit to update + int nextUpdate; + + // read 8-bit register; returns the value read on success, -1 on failure + int readReg8(int addr, uint16_t registerAddr) + { + // write the request - register address + auto-inc mode + uint8_t data_write[1]; + data_write[0] = registerAddr | TLC59116R::CTL_AIALL; + if (i2c.write(addr << 1, data_write, 1, true)) + return -1; + + // read the result + uint8_t data_read[1]; + if (i2c.read(addr << 1, data_read, 1)) + return -1; + + // return the result + return data_read[0]; + } + + // write 8-bit register; returns true on success, false on failure + bool writeReg8(int addr, uint16_t registerAddr, uint8_t data) + { + uint8_t data_write[2]; + data_write[0] = registerAddr | TLC59116R::CTL_AIALL; + data_write[1] = data; + return !i2c.write(addr << 1, data_write, 2); + } + + // I2C bus interface + I2C_Type i2c; + + // reset pin (active low) + DigitalOut reset; +}; + +#endif