Using the TCS3771 family of devices with OpenLPC on lpc1114

TCS3771 and alike are a range of I2c RGB sensors allowing one to read not only light intensity but also it’s color. With a bit of care and consideration, the light intensity can be calculated with quite a precision. They provides red, green, blue, and clear (RGBC) light sensing and proximity detection (when coupled with an external IR LED). They detect light intensity under a variety of lighting conditions and through a variety of attenuation materials.

The device contains a 4 × 4 photodiode array, integrating amplifiers, ADCs, accumulators, clocks, buffers, comparators, a state machine, and an I2C interface. The 4 × 4 photodiode array is composed of red-filtered, green-filtered, blue-filtered, and clear photodiodes – four of each type. Four integrating ADCs simultaneously convert the amplified photodiode currents to a digital value providing up to 16 bits of resolution. Upon completion of the conversion cycle, the conversion result is transferred to the data registers. The transfers are double-buffered to ensure that the integrity of the data is maintained. Communication to the device is accomplished through a fast (up to 400kHz), two-wire I2C serial bus for easy connection to a microcontroller or embedded controller.

In this article we’ll hook it up to a LPC1114 controller and try to use it’s internal I2C device. There is no reason to reinvent the wheel, so we will use the OpenLPC’s I2C driver.

Note: From the catalog it seems there are multiple variations of this device, with two I2C addreses (0x29 and 0x39) and with two I2C bus voltages, 1.8v and Vdd. I’m not sure (and the datasheet does not state if the 1.8v version would or wouldn’t work at higher voltages outputed by the LPC1114.

Hooking it up to the LPC1114 is quite simple, we need to connect the suppy lines and the SCL and SDA I2C wires (don’t forget the I2C protocol-required pull-up resistors) and we are in business.

On the software side, first we have to configure the GPIO pins to use I2C function.The SDA and SCL are connected to the standard I2C pins, which on case of LPC1114 are P0.4 and P0.5:

Chip_IOCON_PinMuxSet(LPC_IOCON, IOCON_PIO0_4, IOCON_FUNC1 | IOCON_DIGMODE_EN );
Chip_IOCON_PinMuxSet(LPC_IOCON, IOCON_PIO0_5, IOCON_FUNC1 | IOCON_DIGMODE_EN );

Then we need to reset the I2C peripheral, initialize it and set clock rate:

Chip_SYSCTL_PeriphReset(RESET_I2C0);
  Chip_I2C_Init(I2C0);
  Chip_I2C_SetClockRate(I2C0, SPEED_400KHZ);

And finally, we need to decide how we’ll use the hardware (with interrupts or polling) and configure the driver accordingly. In this case we use interrupts, which we enable at the end:

Chip_I2C_SetMasterEventHandler(I2C0, Chip_I2C_EventHandler);
NVIC_ClearPendingIRQ(I2C0_IRQn);
NVIC_EnableIRQ(I2C0_IRQn);

The last missing thing from having it working is the IRQ state handler, which we will provide, by defining the function:

void I2C_IRQHandler(void) {
  if (Chip_I2C_IsMasterActive(I2C0)) {
    Chip_I2C_MasterStateHandler(I2C0);
  }
  else {
    Chip_I2C_SlaveStateHandler(I2C0);
  }
}

Now having this set up, we can start reading some informations from the sensor. Looking to the datasheet, we can see the information we require (the R,G,B and C values) are in the registers 0x14-0x1b. It also details how the registers should be addressed:

  • First we write to the COMMAND register to specify the register address we want to work with (either read or write).  For accessing the COMMAND register, we write a byte to I2C bus, with the 7th bit set and the lower 4 bits containing the register we want to work with.
  • Once the register is selected, we can read from it or write to it by sending data on the I2C bus.

In order to ease reading succesive registers without first setting their address, bits 5,6 of the value written to the COMMND register specify the ‘protocol’ used, a fancy word for what to do with an internal counter:

  • 00 –  counter stays fix, repeated reading will read same register.
  • 01 – auto-increment, after reading the register is incremented, so next reading will read next register.

The plan is to set the lowest register’s address and then read all the 8 values (low/hi byte for R,G,B,C) with autoincrement ‘protocol’.

First we define some handy functions to work with the I2C:

static I2C_XFER_T xfer;

/* Transfer and Receive buffers */
static uint8_t tx[10], rx[10];

void i2c_sendByte(unsigned char slaveAddr, unsigned char byte) {

  /* Setup I2C parameters to send 1 byte of data */
  xfer.slaveAddr = slaveAddr;
  xfer.txSz = 1;
  tx[0] = byte;
  xfer.txBuff = &tx[0];

  Chip_I2C_MasterSend(I2C0, xfer.slaveAddr, xfer.txBuff, xfer.txSz);
}

void i2c_sendBytes2(unsigned char slaveAddr, unsigned char byte1, unsigned char byte2) {

  /* Setup I2C parameters to send 2 bytes of data */
  xfer.slaveAddr = slaveAddr;
  xfer.txSz = 2;
  tx[0] = byte1;
  tx[1] = byte2;
  xfer.txBuff = &tx[0];

  Chip_I2C_MasterSend(I2C0, xfer.slaveAddr, xfer.txBuff, xfer.txSz);
}

void i2c_sendBytes3(unsigned char slaveAddr, unsigned char byte1, unsigned char byte2,
    unsigned char byte3) {

  /* Setup I2C parameters to send 3 bytes of data */
  xfer.slaveAddr = slaveAddr;
  xfer.txSz = 3;
  tx[0] = byte1;
  tx[1] = byte2;
  tx[2] = byte3;
  xfer.txBuff = &tx[0];

  Chip_I2C_MasterSend(I2C0, xfer.slaveAddr, xfer.txBuff, xfer.txSz);
}

void i2c_sendBytes4(unsigned char slaveAddr, unsigned char byte1,
    unsigned char byte2, unsigned char byte3, unsigned char byte4) {

  /* Setup I2C parameters to send 4 bytes of data */
  xfer.slaveAddr = slaveAddr;
  xfer.txSz = 4;
  tx[0] = byte1;
  tx[1] = byte2;
  tx[2] = byte3;
  tx[3] = byte4;
  xfer.txBuff = &tx[0];

  Chip_I2C_MasterSend(I2C0, xfer.slaveAddr, xfer.txBuff, xfer.txSz);
}

unsigned char i2c_receiveByte(unsigned char slaveAddr) {

  xfer.slaveAddr = slaveAddr;
  /* Setup I2C parameters to receive 1 bytes of data */
  xfer.rxBuff = &rx[0];
  xfer.rxSz = 1;
  Chip_I2C_MasterRead(I2C0, xfer.slaveAddr, xfer.rxBuff, xfer.rxSz);
  return xfer.rxBuff[0];
}

void i2c_receiveBytes(unsigned char slaveAddr, unsigned char* buf, char bytes) {

  int i;

  xfer.slaveAddr = slaveAddr;
  /* Setup I2C parameters to receive 2 bytes of data */
  xfer.rxBuff = &rx[0];
  xfer.rxSz = bytes;
  Chip_I2C_MasterRead(I2C0, xfer.slaveAddr, xfer.rxBuff, xfer.rxSz);

  for(i=0;i<bytes;i++) {
    buf[i] = rx[i];
  }
}

Before reading however, we need to power on the chip and enable the ADC by setting the appropiate flags (PON and AEN, bits 0 and 1) in the ENABLE (0x00) register:

i2c_sendBytes2(0x29, TCS3772_COMMAND_BIT | TCS3772_ENABLE, TCS3772_PON | TCS3772_AEN);

now chip is on and we can query it for values. We can also retrieve the ID of the chip with a code like the following:

i2c_sendByte(0x29, TCS3772_COMMAND_BIT | TCS3772_ID);
unsigned char id = i2c_receiveByte(0x29);

or the status of the device as follows:

i2c_sendByte(0x29,  TCS3772_COMMAND_BIT | TCS3772_STATUS);
status = i2c_receiveByte(0x29);

The ADC values can be read as follows (in a single go, reading 8 bytes from the device):

unsigned char buf[8];
// c
i2c_sendByte(0x29,  TCS3772_COMMAND_BIT | TCS3772_CDATA | TCS3722_COMMAND_AUTO_INCREMENT);
i2c_receiveBytes(0x29, &buf[0], 8);

unsigned int c = buf[0]+ buf[1]*256;
unsigned int r = buf[2]+ buf[3]*256;
unsigned int g = buf[4]+ buf[5]*256;
unsigned int b = buf[6]+ buf[7]*256;

For completition here are the defines used (from the Datasheet):

#define TCS3772_ADDRESS 0x29

#define TCS3772_COMMAND_BIT 1<<7
#define TCS3722_COMMAND_AUTO_INCREMENT 1<<5

#define TCS3772_ENABLE 0x00

#define TCS3772_AEN 1<<1
#define TCS3772_PON 1

#define TCS3772_ID 0x12

#define TCS3772_STATUS 0x13
#define TCS3772_CDATA 0x14

Some useful documents about how to compute stuff: