TCS3472x color sensor demo

Just poking around at the color sensor breakout. It wasn’t obvious how it worked at first, so I referenced this code.

// Select enable register(0x80)
// Power ON, RGBC enable, wait time disable(0x03)
[ 0x52 0x80 0x03]

// Select ALS time register(0x81)
// Atime = 700 ms(0x00)
[0x52 0x81 0x00]

// Select Wait Time register(0x83)
// WTIME : 2.4ms(0xFF)
[0x52 0x83 0xff]

// Select control register(0x8F)
// AGAIN = 1x(0x00)
[0x52 0x8F 0x00]

// Read 8 bytes of data from register(0x94)
// cData lsb, cData msb, red lsb, red msb, green lsb, green msb, blue lsb, blue msb
[0x52 0x94][0x53 r:8]

This is what wasn’t clear. For the byte following the write address, the 7th bit needs to be 1, then the register address goes in bits 4:0.

1 Like

The datasheet page 19 says this, and then there is no further mention of word or protocol bit anywhere else in the docs. It’s basic double buffering like 8 bit PIC uCs do when reading 16 bit ADC registers, but where is it?

It’s definitely not a bit in the command register.

The only web search hit is this result at Adafruit with no apparent resolution. Adafruit’s Arduino code doesn’t make any accommodation for reading the 16 bit color registers. Weird.

1 Like

Maybe the Auto-increment protocol transaction?

If bits 6:5 are set to 0b00, and bits 4:0 are set to the register address for the first byte, what happens when you read two bytes? My guess is it reads the same byte twice.

In constrast, when bits 6:5 are set to 0b01, and you read two bytes, my guess is that you’ll be reading a 16-bit value, and the read of the first register address will cause the shadow register to be populated at the register address + 0x01.

In fact, without reading the datasheet, I’d guess that there’s at least a one-byte gap between any 16-bit register address and its next-higher addressed registers. In other words, using totally made up addresses:

Register Address Name Actual purpose
0x40 16-bit Color Least significant 8 bits
0x41 Undocumented Most significant 8 bits shadow register (updated when read 0x40)
0x42 Foo Some other register

But of course, I’d have to experiment to see if this theory matched the device’s behavior.

2 Likes

Okay, I tested both options and… They’re the same? In both it seems that if you don’t send a new COMMAND, the next read transaction starts where the previous one started. For example you can set it to read from the first of 8 color registers once, and then each read transaction will start at the beginning of the color registers. This seems handy for fast reads without a write setup each time (save time!). In practice it doesn’t seem to matter if the bits 6:5 are 00 or 01, it is the same behavior.

It looks like the sensor has been taken over by AMS OSRAM. I’m going to write their support.

Forgot the link to the Adafruit forum topic about this.

I pushed a firmware update with a simple continuous sampling demo for this chip in I2C mode.

I2C> [0x52 0x92][0x53 r]

I2C START
TX: 0x52 ACK 0x92 ACK
I2C STOP
I2C START
TX: 0x53 ACK
RX: 0x44 NACK
I2C STOP
I2C> [0x53 r] [0x53 r] [0x53 r]

I2C START
TX: 0x53 ACK
RX: 0x44 NACK
I2C STOP
I2C START
TX: 0x53 ACK
RX: 0x44 NACK
I2C STOP
I2C START
TX: 0x53 ACK
RX: 0x44 NACK
I2C STOP

Register 0x12 is the chip ID (0x44). Reading it with command bits set to 00. Works as expected “Repeated byte protocol transaction”. Each new read resets back to register 0x12 and we keep reading 0x44.

I2C> [0x52 0xb2][0x53 r]

I2C START
TX: 0x52 ACK 0xB2 ACK
I2C STOP
I2C START
TX: 0x53 ACK
RX: 0x44 NACK
I2C STOP
I2C> [0x53 r] [0x53 r] [0x53 r]

I2C START
TX: 0x53 ACK
RX: 0x44 NACK
I2C STOP
I2C START
TX: 0x53 ACK
RX: 0x44 NACK
I2C STOP
I2C START
TX: 0x53 ACK
RX: 0x44 NACK
I2C STOP
I2C>

Setting command register bits 6:5 to 01, should be “Auto-increment protocol transaction”, but the behavior is exactly the same as 00. According to the datasheet each new read should increment the counter in this mode, so we defo shouldn’t see 0x44 over and over.

It’s possible this is entirely a ian mistake, but I have a hunch this datasheet was written for early silicon and not updated for the final design. I wrote AMS OSRAM about both issues.

1 Like

The Type bits would have zero impact when only reading a single byte… that is expected. I expect the Type bits may impact the results when reading more than one byte per read transaction, as each transaction would start at the configured register address.

Contrast the first two bytes vs. the last two bytes from:

[ 0x52 0b10110110 ] [ 0x53 r:2 ] [0x53 r] [0x53 r]

I wonder if the following would work:

Read six bytes, starting at register address 0b10110 (0x16 … aka Red data low). I am wondering if this would read bytes Rl, Rh, Gl, Gh, Bl, Bh, then pause a second, then read those bytes again, (and pause and read a third time).

[ 0x52 0b10110110 ] [ 0x53 r:6 ] D:1000 [ 0x53 r:6 ] D:1000 [ 0x53 r:6 ]

Can you share the output from real hardware, especially with changing what the sensor “sees” during the delays?

Update

Full script, from power-on, and now reading 8 bytes starting from register 0x14:

[ 0x52 0x80 0x03 ]
[ 0x52 0x81 0x00 ]
[ 0x52 0x83 0xFF ]
[ 0x52 0x8F 0x00 ]
[ 0x52 0xB4 ]
[ 0x53 r:8 ] D:1000 
[ 0x53 r:8 ] D:1000 
[ 0x53 r:8 ] D:1000 
1 Like
I2C> [ 0x52 0b10110010] [0x53 r:2] [0x53 r] [0x53 r]

I2C START
TX: 0x52 ACK
TX: 0b10110010 ACK
I2C STOP
I2C START
TX: 0x53 ACK
RX: 0x44 ACK 0x11 NACK
I2C STOP
I2C START
TX: 0x53 ACK
RX: 0x44 NACK
I2C STOP
I2C START
TX: 0x53 ACK
RX: 0x44 NACK
I2C STOP
I2C>

I would expect the address pointer to increment after every read, not just on multiple byte reads, but it doesn’t seem to increment on either.

I2C> [ 0x52 0b10110010] [0x53 r:8] [0x53 r] [0x53 r]

I2C START
TX: 0x52 ACK
TX: 0b10110010 ACK
I2C STOP
I2C START
TX: 0x53 ACK
RX: 0x44 ACK 0x11 ACK 0xB0 ACK 0x00 ACK 0x5E ACK 0x00 ACK 0x35 ACK 0x00 NACK

I2C STOP
I2C START
TX: 0x53 ACK
RX: 0x44 NACK
I2C STOP
I2C START
TX: 0x53 ACK
RX: 0x44 NACK
I2C STOP
I2C>

With an 8 byte read first.

I2C> [ 0x52 0xB4 ] [0x53 r:8] D:1000 [0x53 r:8] D:1000 [0x53 r:8]

I2C START
TX: 0x52 ACK 0xB4 ACK
I2C STOP
I2C START
TX: 0x53 ACK
RX: 0x7E ACK 0x0B ACK 0x3E ACK 0x0A ACK 0xFE ACK 0x00 ACK 0x2B ACK 0x01 NACK

I2C STOP
Delay: 1000ms
I2C START
TX: 0x53 ACK
RX: 0x6C ACK 0x0B ACK 0x39 ACK 0x0A ACK 0xF9 ACK 0x00 ACK 0x27 ACK 0x01 NACK

I2C STOP
Delay: 1000ms
I2C START
TX: 0x53 ACK
RX: 0xB0 ACK 0x0A ACK 0xDD ACK 0x09 ACK 0xC9 ACK 0x00 ACK 0x07 ACK 0x01 NACK

I2C STOP
I2C>

Wasn’t very successful moving between colors because the output doesn’t come until the transaction is complete, but this looks like it is resetting to me.

ETA: It occurs that the sensor on the board might not be genuine, but there’s no “domestic equivalent” parts listed on 1688 or Taobao.

1 Like

This seems to have reasonable behavior, even if the datasheet isn’t 100% correct.


It'd be tough to test for atomicity on the 16-bit reads (aka "shadow" register)

You’d need to have carefully-controlled conditions, and a sensor that’s been on for a while. Then, pick one color where you can cause the value to fluctuate over a range <= 0x40, and where the fluctuation causes the most significant 8 bit register to fluctuate between 0xNN and 0xNN+1.

Best setup: ability to intentionally fluctuate the value above / below the spot where MSB changes in that range… not sure how accurate this sensor can be. Maybe with IR emitter on the clear color?

Run the following, to intentionally delay between reading the two parts of the 16-bit value:

[ 0x52 0xB4 ]
[0x53  r  D:200 r]

If MSB == 0xNN, verify LSB >= 0xB0.
If MSB == 0xNN+1, verify LSB <= 0x50.

If either verification fails, then the shadow register feature is probably not implemented, and the values may jump sporadically sometimes.


Can you help me understand a compelling use case for a situation where you’d setup to read from one address/register, and want individual read commands to then cause the address/register to increment?

2 Likes

Upon completion of a conversion cycle, the
results are transferred to the data registers, which are double-buffered to ensure the integrity of the data.

The transfers are double-buffered to ensure that invalid data is not read during the transfer. After the transfer, the device automatically moves to the next state in accordance with the configured state machine.

Some of this stuff is probably related to beta silicon. This may indicate that the registers are all frozen during a single read.

I have absolutely no use case for any of it :wink: I explore every nook and cranny of the chip so I can regurgitate it in tutorial form. I probably notice these inconsistencies more than a typical user doing only what’s needed to get their project up and running. I want to be sure I convey the best/most accurate way to work with the chip cause it will be on the internet forever :slight_smile:

On the flip side I note this warning, which is not followed by any of the example drivers I’ve looked at. All enable both AEN and PON in a single operation.

[0x52 0b10000000 0b1] D:3 [0x52 0b10000000 0b11]

I’m doing according to the datasheet in 2 step, 3ms apart.

3 Likes

TCS3472x I2C color sensor demo is now online.

Here’s the best measurement about 5mm away from my cheap multimeter. It’s okay…

I’ve been interested in these sensors for a while, but this demo was one long hard slog.

  • The color measurements are 16 bits, while most RGB LEDs and “color pickers” use 8 bits per color. Using 16x gain I was able to use only the upper byte of each color measurement and get a pretty good approximation.
  • I noticed that the gain setting will not take effect unless the RGBC timing register is set after the gain is set. If you change the gain setting, be sure to set the RGBC timing register afterwards. I found no documentation for this, but it drove me absolutely nuts figuring it out.
  • I hate the part number, it has no flow and I still don’t remember it. Writing this part number over and over hurt my brain.
  • These sensors are intended to be behind dark glass. Using in free air on a breakout board with a crappy LED in close proximity is really inconsistent. Often the color measurement looked more like the glare of a low quality white LED. The board with 2 LEDs was unusable, go for a board with one LED.
1 Like

Hey gang, sorry I’ve been away for a while…

I used this color sensor in a production project about 8 years ago :slight_smile: I don’t know if this is useful, but here’s some of my code for reference. It worked quite well and I was able to detect the color objects passing by the sensor at a pretty decent rate. It was a pretty integral part of the machine.

Yeah, I remember the datasheet for this being a real mess, and I spent more time that I should have getting it all to work.

The device doing the control was an ATMega328u4, basic 8-bit AVR, with USB. Basicallly, the sensor was on the side of a piece of transparent PVC tubing watching colored balls fall through (accelerated by gravity). The supervisory process would talk over a serial protocol, looking for a certain colored ball in an array of other colored balls.

header/defines
/******************************************************
* tcs34725
*******************************************************
* defines and methods for the TAOS TCS34725 color
* sensor chip
******************************************************/

#ifndef TCS_34725_H_
#define TCS_34725_H_

/**************************************
* SPI Defines for the chip
**************************************/

// correct chip ID
#define TCS34725_CHIP_ID        (0x44)  

// SPI address of chip
#define TCS34725_ADDRESS        (0x29)

// commands 
#define TCS34725_COMMAND_BIT        (0x80)                          // bit must be set for any command
#define TCS34725_CMD_REPEATED_BYTE  (TCS34725_COMMAND_BIT)          // read the same byte over and over and over
#define TCS34725_CMD_AUTO_INC       (TCS34725_COMMAND_BIT | 0x20)   // after read, read the next byte
#define TCS34725_CMD_SPECIAL        (TCS34725_COMMAND_BIT | 0x60)   // do something special (only special is next one)
#define TCS34725_CMD_ADDR_CLR_INT   (TCS34725_COMMAND_BIT | 0x06)   // clear any pending interrupts 

// Register addresses
#define TCS34725_ENABLE           (0x00)
#define TCS34725_ATIME            (0x01)    // Integration time 
#define TCS34725_WTIME            (0x03)    // Wait time (if TCS34725_ENABLE_WEN is asserted) 
#define TCS34725_AILTL            (0x04)    // Clear channel lower interrupt threshold 
#define TCS34725_AILTH            (0x05)
#define TCS34725_AIHTL            (0x06)    // Clear channel upper interrupt threshold 
#define TCS34725_AIHTH            (0x07)
#define TCS34725_PERS             (0x0C)    // Persistence register - basic SW filtering mechanism for interrupts 
#define TCS34725_CONFIG           (0x0D)
#define TCS34725_CONTROL          (0x0F)    // Set the gain level for the sensor 
#define TCS34725_ID               (0x12)    // 0x44 = TCS34721/TCS34725, 0x4D = TCS34723/TCS34727 
#define TCS34725_STATUS           (0x13)
#define TCS34725_CDATAL           (0x14)    // Clear channel data 
#define TCS34725_CDATAH           (0x15)
#define TCS34725_RDATAL           (0x16)    // Red channel data 
#define TCS34725_RDATAH           (0x17)
#define TCS34725_GDATAL           (0x18)    // Green channel data 
#define TCS34725_GDATAH           (0x19)
#define TCS34725_BDATAL           (0x1A)    // Blue channel data 
#define TCS34725_BDATAH           (0x1B)

// enable modes/commands for the enable register
typedef enum {
    TCS34725_ENABLE_AIEN        = 0x10, // RGBC Interrupt Enable 
    TCS34725_ENABLE_WEN         = 0x08, // Wait enable - Writing 1 activates the wait timer 
    TCS34725_ENABLE_AEN         = 0x02, // RGBC Enable - Writing 1 actives the ADC, 0 disables it 
    TCS34725_ENABLE_PON         = 0x01  // Power on - Writing 1 activates the internal oscillator, 0 disables it 
} tcs34725_enable_t;

// wait times for the  wait time register
typedef enum {
    TCS34725_WTIME_2_4MS        = 0xFF,    // WLONG0 = 2.4ms   WLONG1 = 0.029s 
    TCS34725_WTIME_204MS        = 0xAB,    // WLONG0 = 204ms   WLONG1 = 2.45s  
    TCS34725_WTIME_614MS        = 0x00     // WLONG0 = 614ms   WLONG1 = 7.4s   
} tcs34725_wait_t;

// persistence values
typedef enum {
    TCS34725_PERS_NONE          = 0x00,   // Every RGBC cycle generates an interrupt                                
    TCS34725_PERS_1_CYCLE       = 0x01,   // 1 clean channel value outside threshold range generates an interrupt   
    TCS34725_PERS_2_CYCLE       = 0x02,   // 2 clean channel values outside threshold range generates an interrupt  
    TCS34725_PERS_3_CYCLE       = 0x03,   // 3 clean channel values outside threshold range generates an interrupt  
    TCS34725_PERS_5_CYCLE       = 0x04,   // 5 clean channel values outside threshold range generates an interrupt  
    TCS34725_PERS_10_CYCLE      = 0x05,   // 10 clean channel values outside threshold range generates an interrupt 
    TCS34725_PERS_15_CYCLE      = 0x06,   // 15 clean channel values outside threshold range generates an interrupt 
    TCS34725_PERS_20_CYCLE      = 0x07,   // 20 clean channel values outside threshold range generates an interrupt 
    TCS34725_PERS_25_CYCLE      = 0x08,   // 25 clean channel values outside threshold range generates an interrupt 
    TCS34725_PERS_30_CYCLE      = 0x09,   // 30 clean channel values outside threshold range generates an interrupt 
    TCS34725_PERS_35_CYCLE      = 0x0a,   // 35 clean channel values outside threshold range generates an interrupt 
    TCS34725_PERS_40_CYCLE      = 0x0b,   // 40 clean channel values outside threshold range generates an interrupt 
    TCS34725_PERS_45_CYCLE      = 0x0c,   // 45 clean channel values outside threshold range generates an interrupt 
    TCS34725_PERS_50_CYCLE      = 0x0d,   // 50 clean channel values outside threshold range generates an interrupt 
    TCS34725_PERS_55_CYCLE      = 0x0e,   // 55 clean channel values outside threshold range generates an interrupt 
    TCS34725_PERS_60_CYCLE      = 0x0f    // 60 clean channel values outside threshold range generates an interrupt 
} tcs34725_persistence_t;

// config for long wait times
#define TCS34725_CONFIG_WLONG     (0x02)    // Choose between short and long (12x) wait times via TCS34725_WTIME 

// status values
#define TCS34725_STATUS_AINT      (0x10)    // RGBC Clean channel interrupt 
#define TCS34725_STATUS_AVALID    (0x01)    // Indicates that the RGBC channels have completed an integration cycle 

typedef enum {
    TCS34725_INTEGRATIONTIME_2_4MS  = 0xFF,   //  2.4ms - 1 cycle    - Max Count: 1024  
    TCS34725_INTEGRATIONTIME_4_8MS  = 0xFE,   //  4.8ms - 2 cycles   - Max Count: 2048
    TCS34725_INTEGRATIONTIME_9_6MS  = 0xFC,   //  9.6ms - 4 cycles   - Max Count: 4096
    TCS34725_INTEGRATIONTIME_24MS   = 0xF6,   //  24ms  - 10 cycles  - Max Count: 10240 
    TCS34725_INTEGRATIONTIME_50MS   = 0xEB,   //  50ms  - 20 cycles  - Max Count: 20480 
    TCS34725_INTEGRATIONTIME_101MS  = 0xD5,   //  101ms - 42 cycles  - Max Count: 43008 
    TCS34725_INTEGRATIONTIME_154MS  = 0xC0,   //  154ms - 64 cycles  - Max Count: 65535 
    TCS34725_INTEGRATIONTIME_700MS  = 0x00    //  700ms - 256 cycles - Max Count: 65535 
} tcs34725_integration_time_t;

typedef enum {
    TCS34725_GAIN_1X                = 0x00,   //  No gain  
    TCS34725_GAIN_4X                = 0x01,   //  4x gain  
    TCS34725_GAIN_16X               = 0x02,   //  16x gain 
    TCS34725_GAIN_60X               = 0x03   //  60x gain 
} tcs34725_gain_t;

bool initTCS(tcs34725_integration_time_t intTime, tcs34725_gain_t gain);
bool tcsWrite8(unsigned char addr, uint8_t val);
uint8_t tcsRead8(unsigned char addr);
uint16_t tcsRead16(unsigned char addr);
bool getAllColors(color_array_t* colors);
void tcsWriteIntegrationTime(tcs34725_integration_time_t intTime);
void tcsWriteReceiveGain(tcs34725_gain_t gain);
#endif 

tcs334725.c
/******************************************************
* tcs34725
*******************************************************
* methods for the TAOS TCS34725 color sensor chip
*******************************************************/

#include "tcs34725.h"

/**************************************
* get the chip up and running
**************************************/
bool initTCS(tcs34725_integration_time_t intTime, tcs34725_gain_t gain) {
    initTWI();
    
    tcsWriteIntegrationTime(intTime);
    tcsWriteReceiveGain(gain);

    // chip is in low power sleep mode when first powered up.  Tell
    // it to wake the heck up.
    return (tcsWrite8(TCS34725_ENABLE, TCS34725_ENABLE_PON | TCS34725_ENABLE_AEN));
}

/*********************************************
* write a single byte
*********************************************/
bool tcsWrite8(unsigned char addr, uint8_t val) {
    return (twiWriteSingleByte(addr, val, false));
}

/***********************************************
* read a single byte
***********************************************/
uint8_t tcsRead8(unsigned char addr) {
    uint16_t retVal = twiReadSingleWord(addr);
    
    return ((uint8_t)(retVal >> 8));
}

uint16_t tcsRead16(unsigned char addr) {
    return (twiReadSingleWord(addr));
}

/*******************************************
* integration time
*******************************************/
void tcsWriteIntegrationTime(tcs34725_integration_time_t intTime)
{
    tcsWrite8(TCS34725_ATIME, (unsigned char)intTime);
}

/*******************************************
* set receiver gain.  Gain is the lower
* 2 bits of the control register
********************************************/
void tcsWriteReceiveGain(tcs34725_gain_t gain)
{
    tcsWrite8(TCS34725_CONTROL, (unsigned char)gain);
}

bool getAllColors(color_array_t* colors) {
    if (twiReadMultipleWords(TCS34725_RDATAL, &colors->red, 3))
    {
        return (true);
    }
    
    return (false);
}
twi.h
/***************************************************
* TWI (I2C) Interface
***************************************************/

#ifndef TWI_H_
#define TWI_H_

void initTWI();
bool twiWriteSingleByte(unsigned char addr, unsigned char data, bool chain);
uint16_t twiReadSingleWord(unsigned char reg);
bool twiReadMultipleWords(unsigned char startReg, uint16_t* data, size_t len);

#endif // define TWI_H_
twi.c
/***************************************************
* TWI (I2C) Interface
***************************************************/

#include "project.h"
#include <util/twi.h>

/***********************************
* initTWI()
************************************
* Set the data and clock pins as 
* outputs and set them both high
***********************************/
void initTWI() {
    TWSR = 0;   // prescaler of one
    TWBR = 32;  // should make TWI freq 200KHz
    //TWBR = 12;  // 400KHz
}

/*******************************************
* write a single byte
********************************************
* set 'chain' bool parameter to true if you
* are going to chain another operation after
* this (essentially, don't send a STOP)
********************************************/
bool twiWriteSingleByte(unsigned char addr, unsigned char data, bool chain) {
    // Send the start
    TWCR = BIT(TWINT) | BIT(TWSTA) | BIT(TWEN);

    // wait for it
    while (!GETBIT(TWCR, TWINT));

    // OK?
    if (TW_STATUS != TW_START)
    {
#ifdef SENSOR_STDIO
        printf_P(PSTR("TWI-W::Start not sent properly, TWSR is 0x%02x\r\n"), TW_STATUS);
#endif
        TWCR = 0;
        return (false);
    }

    // SLA+W
    unsigned char SLA = TCS34725_ADDRESS;
    // seven bit address, shift left one
    SLA = SLA << 1;
    // clear LSB to indicate a write
    SLA &= 0xfe;
    TWDR = SLA | TW_WRITE;
    TWCR = BIT(TWINT) | BIT(TWEN);

    // wait
    while (!GETBIT(TWCR, TWINT));

    if (TW_STATUS != TW_MT_SLA_ACK)
    {
#ifdef SENSOR_STDIO
        printf_P(PSTR("TWI-W::SLA+W not sent properly, TWSR is 0x%02x\r\n"), TW_STATUS);
#endif
        TWCR = 0;
        return (false);
    }

    // send command code.  This is the logic or of the command and register address
    unsigned char commandCode = TCS34725_CMD_REPEATED_BYTE | addr;
    TWDR = commandCode;
    TWCR = BIT(TWINT) | BIT(TWEN);

    // wait
    while (!GETBIT(TWCR, TWINT));

    if (TW_STATUS != TW_MT_DATA_ACK)
    {
#ifdef SENSOR_STDIO
        printf_P(PSTR("TWI-W::command code not sent properly, TWSR is 0x%02x\r\n"), TW_STATUS);
#endif
        TWCR = 0;
        return (false);
    }
       
    if (!chain)
    { 
        TWDR = data;
        TWCR = BIT(TWINT) | BIT(TWEN);

        // wait
        while (!GETBIT(TWCR, TWINT));

        if (TW_STATUS != TW_MT_DATA_ACK)
        {
#ifdef SENSOR_STDIO
            printf_P(PSTR("TWI-W::data byte not sent properly, TWSR is 0x%02x\r\n"), TW_STATUS);
#endif
            TWCR = 0;
            return (false);
        }

        // send the stop
        TWCR = BIT(TWSTO) | BIT(TWINT) | BIT(TWEN);    
        
        // wait for TWI stop condition 
        while (!IS_SCL_HIGH && !IS_SDA_HIGH);

        TWCR = 0;
    }

    return (true);
}

/********************************************
* read a single byte
********************************************/
uint16_t twiReadSingleWord(unsigned char reg) {
    uint16_t retVal;
    
    if (twiReadMultipleWords(reg, &retVal, 1))
    {
        return (retVal);
    }

    return (0);
}

bool twiReadMultipleWords(unsigned char startReg, uint16_t* data, size_t len) {
    if (twiWriteSingleByte(startReg, 0, true))
    {
        // send the repeated start to chain the write with a read
        TWCR = BIT(TWSTA) | BIT(TWINT) | BIT(TWEN);

        // wait for it
        while (!GETBIT(TWCR, TWINT));

        // resend the SLA+R, but with the lowest bit set to indicate a read
        unsigned char SLA = TCS34725_ADDRESS;
        // seven bit address, shift left one
        SLA = SLA << 1;
        // clear LSB to indicate a write
        SLA |= TW_READ;

        TWDR = SLA;
        TWCR = BIT(TWINT) | BIT(TWEN);

        // wait for it
        while (!GETBIT(TWCR, TWINT));

        if (TW_STATUS != TW_MR_SLA_ACK)
        {
#ifdef SENSOR_STDIO
            printf_P(PSTR("TWI-R::Failed to write SLA+R, TWSR is 0x%02x\r\n"), TW_STATUS);
#endif
            TWCR = 0;
            return (false);
        }

        for (int ii = 0; ii < len; ii++)
        {
            bool lastByte = false;
            if (ii + 1 == len)
            {
                lastByte = true;
            }

            // read the low byte and send a master ack
            TWCR = BIT(TWINT) | BIT(TWEA) | BIT(TWEN);

            // waiting...
            while (!GETBIT(TWCR, TWINT));

            if (TW_STATUS != TW_MR_DATA_ACK)
            {
#ifdef SENSOR_STDIO
                printf_P(PSTR("TWI-R::Failed to receive low byte, TWSR is 0x%02x\r\n"), TW_STATUS);
#endif
                TWCR = 0;
                return (false);
            }

            uint8_t lowByte = TWDR;

            // read the high byte.  
            TWCR = BIT(TWINT) | BIT(TWEN);

            // Set the master ack as long as this isn't the last word we're reading.  Not setting
            // the ack lets the slave know we're done
            if (!lastByte)
            {
                TWCR |= BIT(TWEA);
            }

            // waiting...
            while (!GETBIT(TWCR, TWINT));

            if (lastByte)
            {
                if (TW_STATUS != TW_MR_DATA_NACK)
                {
#ifdef SENSOR_STDIO
                    printf_P(PSTR("TWI-R::Failed to receive high byte, TWSR is 0x%02x\r\n"), TW_STATUS);
#endif
                    TWCR = 0;
                    return (false);
                }
            }
            else if (TW_STATUS != TW_MR_DATA_ACK)
            {
#ifdef SENSOR_STDIO
                printf_P(PSTR("TWI-R::Failed to receive low byte, TWSR is 0x%02x\r\n"), TW_STATUS);
#endif
                TWCR = 0;
                return (false);
            }

            data[ii] = ((uint16_t)(TWDR << 8) | (uint16_t)(lowByte & 0xff));
        }

        // send the stop 
        TWCR = BIT(TWINT) | BIT(TWSTO) | BIT(TWEN);

        // wait for TWI stop condition 
        while (!IS_SCL_HIGH && !IS_SDA_HIGH);
    }
    else
    {
#ifdef SENSOR_STDIO
        printf_P(PSTR("TWI-R::Failed on first part of combined\r\n"));
#endif
        return (false);
    }

    return (true);
}

implementing…

colorSense.h
/********************************************************
* colorSense
*********************************************************
* 22 June 2017 M.Brugman
*********************************************************
* sense color, duh.
********************************************************/

#ifndef COLOR_SENSE_H_
#define COLOR_SENSE_H_

// initial detection levels; these will be auto tuned
// as things run
#define R_DETECT_LEVEL      300
#define G_DETECT_LEVEL      300
#define B_DETECT_LEVEL      700

// struct to hold color values from sensor
struct _colorarray {
    uint16_t     red;
    uint16_t     grn;
    uint16_t     blu;
};
typedef struct _colorarray color_array_t;

// bit values for colors
#define RED         0
#define GRN         1
#define BLU         2

// colors based on combiation of primary color bit values
#define C_BLACK     0
#define C_RED       1
#define C_GREEN     2
#define C_YELLOW    3
#define C_BLUE      4
#define C_MAGENTA   5
#define C_CYAN      6
#define C_WHITE     7

extern const char* colorNames[];
extern const char colorShortNames[];

void initColorSense(void);
bool processColorSensor();
bool newColorDetected(void);
uint8_t getCurrentColor(void);
const char*  printCurrentValues(void);
void updateThresholdValues(uint16_t red, uint16_t green, uint16_t blue);
void readBaselineLight();

#endif // COLOR_SENSE_H_
colorSense.c
/********************************************************
* colorSense
*********************************************************
* 22 June 2017 M.Brugman
*********************************************************
* sense color, duh.
********************************************************/

#include "project.h"

//#define DEBUG_OUT

// color names based on bit values         BLU GRN RED (val)
const char* colorNames[] = {"Black",    //  0   0   0    0
                            "Red",      //  0   0   1    1
                            "Green",    //  0   1   0    2
                            "Yellow",   //  0   1   1    3
                            "Blue",     //  1   0   0    4
                            "Magenta",  //  1   0   1    5
                            "Cyan",     //  1   1   0    6
                            "White"};   //  1   1   1    7

const char colorShortNames[] = {'b', 'r', 'g', 'y', 'b', 'm', 'c', 'w'};

static color_array_t colors;
static color_array_t baselineColors;
static bool newColorSeen;
static uint8_t currentColorValue;
static uint16_t saveTime;

void initColorSense(void) {
    memset((void*)&baselineColors, 0, sizeof(color_array_t));
    memset((void*)&colors, 0, sizeof(color_array_t));

    newColorSeen = false;
    currentColorValue = C_BLACK;
    saveTime = milliseconds;
}

// gets the current readings from the color sensor.  Stores them for
// differential comparisons and also sets the threshold detect levels.
void readBaselineLight() {
    getAllColors(&baselineColors);

    // set threshold levels.  Blue sensor is much more sensitive than
    // red or green.  What's up with that?
    xConfig.redThreshold = baselineColors.red * 3 / 2;
    xConfig.greenThreshold = baselineColors.grn * 3 / 2;
    xConfig.blueThreshold = baselineColors.blu * 3;
}

bool processColorSensor() {
    bool retVal = false;

    if (getAllColors(&colors))
    {
        uint16_t* newColorPtr = &colors.red;
        uint16_t* baseColorPtr = &baselineColors.red;
        uint16_t* threshold = &xConfig.redThreshold;

        // loop through the three primary colors
        for (size_t ii = RED; ii <= BLU; ii++)
        {
            // differential between the current color values and the baseline values
            int16_t diff = (int16_t)*newColorPtr - (int16_t)*baseColorPtr;

            // if this color has currently been seen, see if the color went away
            if (GETBIT(currentColorValue, ii))
            {
                if (diff < ((int16_t)*threshold / 2))
                {
                    CLRBIT(currentColorValue, ii);
                    newColorSeen = true;
#ifdef DEBUG_OUT
                    printf_P(PSTR("%s off %d \r\n"),
                        (ii == 0) ? "Red" : (ii == 1) ? "Green" : (ii == 2) ? "Blue" : "", diff);
#endif
                }
            }
            // otherwise, if the color wasn't present last time, see if it is there now
            else
            {
                if (diff > (int16_t)*threshold)
                {
                    SETBIT(currentColorValue, ii);
#ifdef DEBUG_OUT
                    printf_P(PSTR("%s on %d/%d\r\n"),
                        (ii == 0) ? "Red" : (ii == 1) ? "Green" : (ii == 2) ? "Blue" : "", diff, *baseColorPtr);
#endif
                    newColorSeen = true;
                }
            }

            ++newColorPtr;
            ++baseColorPtr;
            ++threshold;
        }

        retVal = true;
    }
    return (retVal);
}

// Latches the state that a new color was seen until it is read
bool newColorDetected(void) {
    if (newColorSeen)
    {
        newColorSeen = false;
        return (true);
    }

    return (false);
}

// return the last latched color
uint8_t getCurrentColor(void) {
    return (currentColorValue);
}

// debug serial out
const char* printCurrentValues(void) {
    static char retColors[40];
    sprintf(retColors, "R%d G%d B%d", colors.red, colors.grn, colors.blu);

#ifdef SENSOR_STDIO
    printf_P(PSTR("%s\r\n"), retColors);
#endif

    return (retColors);
}

// save the threshold values to EEPROM
void updateThresholdValues(uint16_t red, uint16_t green, uint16_t blue) {
    xConfig.redThreshold = red;
    xConfig.greenThreshold = green;
    xConfig.blueThreshold = blue;

    saveEEPROM();
}```
finally, from main.c
int main(void)
{
    cfgSerial(S_BAUD_115200, S_FORMAT_8N1);

#ifdef SENSOR_STDIO
    stdin=stdout=&usart0stdio;
#endif

#ifdef SENSOR_SERIAL
    initSerialComm();
#endif

    setupTimer2();
    initEEPROM(); 
 
#ifdef SENSOR_STDIO
    printf_P(PSTR("\r\nColor Detector %s, reset %ld\r\n\r\n"), VERSION, xConfig.resetCount);  
#endif

    WDTCSR = BIT(WDCE);
    WDTCSR = BIT(WDP3) | BIT(WDP0);
    wdt_reset();
    
    initColorSense();

    SET_BACKLIGHT_OFF();

    if (initTCS(TCS34725_INTEGRATIONTIME_9_6MS, TCS34725_GAIN_60X))
    {
        sensorStatus = TC_READY;
    }  
    
    sei(); 

    // TCS34725 dev board has a white balanced LED for target illumination.  Turn it on
    // now and just leave it on
    SET_BACKLIGHT_ON();

    uint16_t count = 0;
    uint32_t lastCountTime = milliseconds;
    bool grabbedBaseline = false;

    // init array of balls
    resetCount();
    
    while (true)
    {
        // get the first baseline light & color levels
        if (!grabbedBaseline && milliseconds > 2500)
        {
            grabbedBaseline = true;
            readBaselineLight();
            printCurrentValues();
        }
        // don't bother trying to detect colors until we've set
        // baseline and target threshold levels
        else
        {
            processColorSensor();
            if (checkForBall())
            {
                lastCountTime = milliseconds;
                ++count;
            }
        }

        // reset ball count, baseline, and threshold a short time
        // after the last ball was seen
        if (count && (milliseconds - lastCountTime > 5000))
        {
            count = 0;
            readBaselineLight();
            printCurrentValues();
            pulseDebugLED(500);
        }

        // any periodic debug stuff
        if (!(milliseconds % 1000))   
        {
            //printCurrentValues();
        }

        if (grabbedBaseline)
        {
            processSerial();
        }

        if (ledCount)
        {
            --ledCount;
            if (!ledCount)
            {
                SET_DEBUG_LED(false);
            }
        }

        // one half millisecond tick (or task) time
        do 
        {
            ;
        } while (isrMilliseconds == isrLastMilleseconds);
        
        wdt_reset();
    }

    return 0;
}

/*****************************************************
* System tick ISR.  Once each half millisecond
*****************************************************/
ISR(TIMER2_COMPA_vect, ISR_BLOCK)
{
    static volatile uint8_t even = 0;

    if (!((++even) % 2))
    {
        ++milliseconds;
    }

    ++isrMilliseconds;
}

/*********************************************************
* checkForBall()
**********************************************************
* See if any balls were detected recently
*********************************************************/
bool checkForBall()
{
    bool ballFound = false;

    // see if there was a new ball
    if (newColorDetected())
    {
        uint8_t newColor = getCurrentColor();

        // black would mean a ball just went away; don't bother annunciating that
        if (newColor != C_BLACK)
        {
            ballFound = true;
            pulseDebugLED(100);

            // don't overrun
            if (ballArrayIndex >= BALL_ARRAY_SIZE - 1)
            {
                sensorStatus = TC_FULL;
            }
            else
            {
                // separate values with a colon
                if (ballCount)
                {
                    ballArray[ballArrayIndex++] = ':';
                    sensorStatus = TC_COUNTING;
                }

                // add a char for the ball detected
                ballArray[ballArrayIndex++] = colorShortNames[newColor];
                ++ballCount;
            }

#ifdef SENSOR_STDIO
            printf_P(PSTR("%d: %s\r\n"), ballCount, colorNames[newColor]);
#endif
        }
    } 

    return (ballFound);
}

I think I still have some of the sensors around from the proto days in a box somewhere. I can hook one up if you need more data, but I’m afraid I’m late to this party…

2 Likes

Thank you for sharing! Very interesting project, sounds like a hands on science museum instillation.

These are really aggressive settings, good to know it’s possible.

2 Likes

It was a fun project! The biggest problem was dust buildup on the sensor :confused:.

The settings were aggressive because the balls passed by the sensor quickly; there was only about 100ms to catch the color before the next ball came through.

2 Likes

I2C> tcs3472 -h
usage:
tcs3472 [-g <gain:1,4,16*,60x>] [-i <integration cycles:1-256*>] [-h(elp)]
- read tcs3472x color sensor, show colors in terminal and on Bus Pirate LEDs
- 3.3volt device, pull-up resistors required
Read with default* 16x gain, 256 integration cycles: tcs3472
Read with 60x gain, 10 integration cycles: tcs3472 -g 60 -i 10

I2C> 

Added tcs3472 command that samples the sensor and displays the current color on the Bus Pirate LEDs. Gain and integration cycles are command line flags. It only uses the upper 8 bits of the color sensor data, so gain should be a least 16, probably 60.

Docs have been updated.

Wow, that’s impressive, 10 balls a second!

2 Likes

I’ll have to see if I can dig one or and try it. I hadn’t thought about it in years!

2 Likes