Mirror with some correction
Dependencies: mbed FastIO FastPWM USBDevice
TLC59116/TLC59116.h
- Committer:
- arnoz
- Date:
- 2021-10-01
- Revision:
- 116:7a67265d7c19
- Parent:
- 87:8d35c74403af
File content as of revision 116:7a67265d7c19:
// 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