Maker.io main logo

Make a Color Sensor with Measurements Displayed via an RGB LED Module, 2

76

2017-07-11 | By All About Circuits

License: See Original Project

Courtesy of All About Circuits

Learn about collecting and processing RGB data generated by the BH1745NUC color sensor IC.

Supporting Information

Previous Article in This Series

The Sensor

We discussed how to use a DAC and some negative feedback to precisely control the intensity of red, green, and blue LEDs in the previous article. Now we can use our RGB LED module as a single-pixel display. By manipulating the mixture of red, green, and blue light, we can produce a wide range of colors.

We can also make our LED module to duplicate the color of light illuminating our RGB sensor. We are using the BH1745NUC color sensor IC manufactured by Rohm (Abbreviated as BH1745). This is a surprisingly impressive device. The package is very small (about 2 mm × 2 mm), which is why we used a custom-designed PCB for this project (maybe you could solder jumper wires onto eight microscopic 0.5-mm-pitch lands, but I couldn’t). Despite this small size, this sensor incorporates extensive functionality and only requires a few external components. Below is the “typical application circuit” from the datasheet:

typical application circuit

This tiny sensor has four photodiodes, optical filters, an I2C interface, four separate 16-bit ADCs with signal-conditioning circuit, and interrupt logic that can be used to alert the microcontroller whenever the red, green, blue, or clear measurement goes above or below a customizable threshold—Pretty good for something that’s so tiny and inexpensive.

Here is the relevant portion of the schematic:

 

 

Gathering Data

The digital section of the BH1745 includes a bank of 21 8-bit registers. All interaction—except the interrupt functionality, which we won’t be using in this project—between the microcontroller and the BH1745 this is accomplished by writing to or reading from these registers via standard I2C transactions. For an abundance of general information and practical guidance related to the I2C protocol, refer to the articles listed under “Supporting Information.” Here we will focus on implementation details specific to the BH1745.

Controlling and retrieving data from the BH1745 requires three types of I2C transactions: write, write-before-read, and read.

  • Write: These transactions are used to load data into the BH1745’s register bank. The first byte after the slave-address-plus-R/nW byte is used to specify the register address, then the following byte is the data to be loaded into the register.

the following byte is the data loaded into register

  • Write-before-read: In the I2C protocol, a master cannot write and read data in one transaction. Every transaction is defined as a read or a write, so we can’t use a single transaction to indicate a register address then read back data from that register. Instead, we use two separate transactions. We first write data to the BH1745 to tell it which register we want to read, we then doh a read transaction to retrieve data from the register we specified. I like to call the first transition the write-before-read transaction.
  • Read: These transactions allow the master to read data from whichever register address was transmitted in the write-before-read transaction.

the following byte is data loaded into the register

As you can see from the image above, a read transaction is not limited to the one specified register address. If you continue to read bytes from the BH1745, it automatically increments the register address and sends data from the new register. You can actually do this same thing with write transactions as well:

the following byte is data loaded into the register

I generally avoid the automatic-increment functionality because I like to keep it simple. Keeping things simple is particularly relevant here because the BH1745’s registers are not arranged contiguously (i.e., invalid register addresses are mixed in amongst valid register addresses). I do, however, make use of the automatic-increment functionality when reading RGBC data—all 8 bytes are contiguous (starting at address 0x50), and it would be horribly inefficient to frequently collect RGBC data using 16 separate single-byte transactions (8 write-before-reads and 8 reads).

Note also that the write-before-read and read transactions can be implemented with a repeated start condition (as shown in the above diagram) instead of a stop condition followed by a start condition. This would be preferable if we had multiple masters on the I2C bus (for more information, see the “Start without a Stop” section in The I2C Bus: Firmware Implementation Details). In this project, though, we have only one master, so we will again invoke the keep-it-simple principle and use a typical stop-then-start approach.

Processing Data

The BH1745’s RGBC data arrives as four 16-bit words, as follows:

BH1745’s RGBC data arrives as four 16-bit words

We can ignore the clear data for this project; all we need to do is convert the R, G, and B words into 8-bit values that we can use to control the intensity of the R, G, and B LEDs. The first thing to realize is that the three color detectors in the BH1745 are not equally sensitive:

the three color detectors in the BH1745

From this plot we can see that R is about 0.72 and B is about 0.56 when G is 1. We’ll need to multiply the R and B values by the appropriate correction factor:

R is about 0.72 and B is about 0.56 when G is 1

Now we’ll need to modify the data in a way that emphasizes the color characteristics of the incident light. Our goal here is to “measure” color, regardless of the varying intensity of the light illuminating the photodetectors. We will need to scale the RGB values in a way that standardizes the absolute value of the measurements while still preserving the relative value—in other words, we maximize the overall intensity while maintaining the proportion of red, green, and blue in the incident light. We can accomplish this by multiplying the highest of the three measurements by whatever factor increases this highest measurement to the maximum value. We can then multiply the other two measurements by the same factor. This commented code excerpt clarifies the entire process:

Copy Code
//extract the 16-bit R, G, and B values from the received data
R_word = (I2C_RcvData[1] << 8) | I2C_RcvData[0];
G_word = (I2C_RcvData[3] << 8) | I2C_RcvData[2];
B_word = (I2C_RcvData[5] << 8) | I2C_RcvData[4];

//apply correction factors based on the relative sensitivity of the three photodetectors
R_intensity = R_word * 1.39;
G_intensity = G_word * 1;
B_intensity = B_word * 1.79;

//determine which intensity is the highest of the three
if(R_intensity >= G_intensity && R_intensity >= B_intensity)
MaxIntensity = R_intensity;
else if(G_intensity >= R_intensity && G_intensity >= B_intensity)
MaxIntensity = G_intensity;
else
MaxIntensity = B_intensity;

/*Now we scale each measurement into the range 0 to 100.
* This preserves the relative ratios of the three
* intensities but standardizes the absolute intensities.
* Thus, the appearance of the LED module is
* determined by the proportion of red, green,
* and blue light, not by the overall intensity of
* the incident light.*/
R_scaled = (R_intensity/MaxIntensity)*100;
G_scaled = (G_intensity/MaxIntensity)*100;
B_scaled = (B_intensity/MaxIntensity)*100;

Note that the final values are scaled such that the maximum is 100. You might recall from Part 1 that the DAC has 8-bit resolution, allowing us to go as high as 255. So why did I restrict the LED intensities to 100 instead of using the full 8-bit range? Because staring at this LED module cranked up to 20 mA messed with my vision! The light is so spectrally pure and focused that it can confuse the eye in an aggravating way.

Step by Step

This is the overall procedure for configuring the BH1745 sensor, gathering data, processing the data, and updating the DAC; you can find more details in the source code.

  1. Write to register 0x42 to enable RGBC conversions.
  2. Delay 1 second (or some other appropriate interval between measurements).
  3. Write register address 0x50 in preparation for reading RGBC data.
  4. Read 8 bytes of RGBC data (for this project we need only the first 6 bytes).
  5. Wait until the I2C transaction is complete.
  6. Read the 16-bit RGB data into three unsigned 16-bit variables.
  7. Convert these three variables into floating point values while multiplying each one by the appropriate correction factor.
  8. Find the maximum of the three and scale each value as described above; store the results in unsigned 8-bit variables.
  9. Update the LED color by loading the 8-bit values into the appropriate DAC channels.

Firmware

You can download a zip file containing all the source and project files on All About Circuits. You can open the “hwconf” file to access configuration details for the port pins and the peripherals. Also, note that these source files include some code that won’t be needed until later when we incorporate USB connectivity.

Here are some salient code sections. First, the main() routine:

Copy Code
int main(void) {

unsigned char R_scaled, G_scaled, B_scaled;
unsigned int R_word, G_word, B_word;
float R_intensity, G_intensity, B_intensity, MaxIntensity;

// Call hardware initialization routine
enter_DefaultMode_from_RESET();

//enable global interrupts
IE_EA = 1;

//tell the RGBC sensor to start performing measurements
I2C_MasterWrite(RGBC_Tx_EnableConv);

while (1)
{
Delay_10ms(100); //the LED is updated once per second

//load the proper register address (for reading) into the RGBC sensor
I2C_MasterWrite(RGBC_Tx_SetReadRGBC);

//read the RGBC data
I2C_MasterRead(RGBC_Rx_RGBC);

//wait until the read transaction is complete
while(I2C_State != MstR_DATA_READY);
I2C_State = IDLE;

//extract the 16-bit R, G, and B values from the received data
R_word = (I2C_RcvData[1] << 8) | I2C_RcvData[0];
G_word = (I2C_RcvData[3] << 8) | I2C_RcvData[2];
B_word = (I2C_RcvData[5] << 8) | I2C_RcvData[4];

//apply correction factors based on the relative sensitivity of the three photodetectors
R_intensity = R_word * 1.39;
G_intensity = G_word * 1;
B_intensity = B_word * 1.79;

//determine which intensity is the highest of the three
if(R_intensity >= G_intensity && R_intensity >= B_intensity)
MaxIntensity = R_intensity;
else if(G_intensity >= R_intensity && G_intensity >= B_intensity)
MaxIntensity = G_intensity;
else
MaxIntensity = B_intensity;

/*Now we scale each measurement into the range 0 to 100.
* This preserves the relative ratios of the three
* intensities but standardizes the absolute intensities.
* Thus, the appearance of the LED module is
* determined by the proportion of red, green,
* and blue light, not by the overall intensity of
* the incident light.*/
R_scaled = (R_intensity/MaxIntensity)*100;
G_scaled = (G_intensity/MaxIntensity)*100;
B_scaled = (B_intensity/MaxIntensity)*100;

/* This can be used to turn off the LED if the measured
* light intensity is too low. This helps to avoid distracting
* color changes related to irrelevant RGB variations detected
* during low-intensity lighting conditions. This functionality
* was used with the "Christmas lights" demonstration to make
* the LED turn off when the sensor was not illuminated by one
* of the lights.*/
if(MaxIntensity < 100)
{
R_scaled = 0;
G_scaled = 0;
B_scaled = 0;
}

//update each DAC channel with the scaled values
UpdateDAC(DAC_RGB_R, R_scaled);
UpdateDAC(DAC_RGB_G, G_scaled);
UpdateDAC(DAC_RGB_B, B_scaled);
}
}

This code configures and initiates I2C transactions:

Copy Code
unsigned char I2C_SlaveAddr;	//global variable for current slave address
unsigned char I2C_NumReadBytes; //number of bytes to be read
unsigned char idata *I2C_WriteBufferPtr; //pointer to bytes to be transmitted
unsigned char I2C_FinalWriteAddress; //the ISR uses this to determine which byte is the final byte

/*These "transaction arrays" contain all the information needed for a particular I2C transaction*/

//write to register address 0x42 to enable RGBC conversions and keep the gain at default (1x)
unsigned char idata RGBC_Tx_EnableConv[4] = {RGB_SENS_ADDR, 2, 0x42, 0x10};

//write to register address 0x42 to enable RGBC conversions and set the gain to 16x
//(so far it appears that it is best to leave the gain at 1x)
unsigned char idata RGBC_Tx_EnableConv_16x[4] = {RGB_SENS_ADDR, 2, 0x42, 0x12};

//set the read address to 0x50, which is the beginning of the registers that hold RGBC data
unsigned char idata RGBC_Tx_SetReadRGBC[3] = {RGB_SENS_ADDR, 1, 0x50};

//read RGBC data (after setting the read address using RGBC_Tx_SetReadRGBC )
unsigned char idata RGBC_Rx_RGBC[3] = {RGB_SENS_ADDR, RGBC_DATA_LEN};


void I2C_MasterWrite(unsigned char* PtrtoCmdBuffer) //function argument is simply the name of the transaction array
{
//ensure that we are not interrupting an ongoing transaction
while(I2C_State != IDLE);

I2C_State = MstW_STA_SENT; //first state is "start condition generated"
I2C_SlaveAddr = PtrtoCmdBuffer[0]; //copy the slave address from the transaction array to the global variable
I2C_WriteBufferPtr = PtrtoCmdBuffer + 2; //set the address of the first data byte in the transaction array
I2C_FinalWriteAddress = I2C_WriteBufferPtr + (PtrtoCmdBuffer[1] - 1); //set the final address based on the number of bytes to be transmitted

SFRPAGE = SMB0_PAGE;
SMB0CN0_STA = 1; //initiate the transaction by setting the start-condition bit
}

void I2C_MasterRead(unsigned char* PtrtoCmdBuffer) //function argument is simply the name of the transaction array
{
//ensure that we are not interrupting an ongoing transaction
while(I2C_State != IDLE);

I2C_State = MstR_STA_SENT; //first state is "start condition generated"
I2C_SlaveAddr = PtrtoCmdBuffer[0]; //copy the slave address from the transaction array to the global variable
I2C_NumReadBytes = PtrtoCmdBuffer[1]; //copy the number of bytes to be read from the transaction array to the global variable

SFRPAGE = SMB0_PAGE;
SMB0CN0_STA = 1; //initiate the transaction by setting the start-condition bit
}

I2C transactions continue in the I2C state machine, which is incorporated into the interrupt service routine for the System Management Bus (SMBus) peripheral (the “Many Names, One Bus” section in Introduction to the I2C Bus explains the relationship between SMBus and I2C).

Copy Code
SI_INTERRUPT (SMBUS0_ISR, SMBUS0_IRQn)
{
SFRPAGE_SAVE = SFRPAGE;
SFRPAGE = SMB0_PAGE;

switch(I2C_State)
{
//Master Read===================================================
//start condition transmitted
case MstR_STA_SENT:
SMB0CN0_STA = 0; //clear start-condition bit
SMB0CN0_STO = 0; //make sure that stop-condition bit is cleared
SMB0DAT = (I2C_SlaveAddr<<1)|BIT0; //combine slave address with R/nW = 1
I2C_State = MstR_ADDR_SENT; //set state variable to next state
SMB0CN0_SI = 0; //clear interrupt flag
break;

//master transmitted "address + R/W" byte
case MstR_ADDR_SENT:
if(SMB0CN0_ACK == I2C_NACK) //if slave did not ACK
{
//cancel transmission and release bus, as follows:
SMB0CN0_STO = 1; //transmit stop condition
I2C_State = IDLE; //set current state as IDLE
}
else //if slave ACKed
{
if(I2C_NumReadBytes == 1) //if only one byte will be read
{
//master NACKs next byte to say "stop transmitting"
SMB0CN0_ACK = I2C_NACK;
}
else //if more than one byte will be read
{
//master ACKs next byte to say "continue transmitting"
SMB0CN0_ACK = I2C_ACK;
}
RcvdByteCount = 0; //this variable will be an index for storing received bytes in an array
I2C_State = MstR_READ_BYTE; //set next state
}
SMB0CN0_SI = 0; //clear interrupt flag
break;

//master received a byte
case MstR_READ_BYTE:
I2C_RcvData[RcvdByteCount] = SMB0DAT; //store received byte
RcvdByteCount++; //increment byte counter (which is also the array index)
SMB0CN0_SI = 0; //clear interrupt flag

if(RcvdByteCount == I2C_NumReadBytes) //if this was the final byte
{
//release bus, as follows:
SMB0CN0_STO = 1; //transmit stop condition
SMB0CN0_SI = 0; //clear interrupt flag
I2C_State = MstR_DATA_READY; //this state tells the while loop in main() that the received data is ready
}
else if(RcvdByteCount == (I2C_NumReadBytes-1)) //if the next byte is the final byte
{
SMB0CN0_ACK = I2C_NACK; //master NACKs next byte to say "stop transmitting"
}
else
{
SMB0CN0_ACK = I2C_ACK; //master ACKs next byte to say "continue transmitting"
}
break;

//Master Write===================================================
//start condition transmitted
case MstW_STA_SENT:
SMB0CN0_STA = 0; //clear start-condition bit
SMB0CN0_STO = 0; //make sure that stop-condition bit is cleared
SMB0DAT = (I2C_SlaveAddr<<1); //combine slave address with R/nW = 0
I2C_State = MstW_ADDR_SENT; //set state variable to next state
SMB0CN0_SI = 0; //clear interrupt flag
break;

//master transmitted "address + R/W" byte
case MstW_ADDR_SENT:
if(SMB0CN0_ACK == I2C_NACK) //if slave did not ACK
{
//cancel transmission and release bus, as follows:
SMB0CN0_STO = 1; //transmit stop condition
I2C_State = IDLE; //set current state as IDLE
}
else //if slave ACKed
{
SMB0DAT = *I2C_WriteBufferPtr; //write first byte to SMBus data register
I2C_State = MstW_BYTE_SENT; //set next state
}
SMB0CN0_SI = 0; //clear interrupt flag
break;

//master transmitted a byte
case MstW_BYTE_SENT:
if(SMB0CN0_ACK == I2C_NACK) //if slave NACKed
{
//stop transmission and release bus, as follows:
SMB0CN0_STO = 1; //transmit stop condition
I2C_State = IDLE; //set current state as IDLE
}
//if slave ACKed and this was the final byte
else if(I2C_WriteBufferPtr == I2C_FinalWriteAddress)
{
SMB0CN0_STO = 1; //transmit stop condition
I2C_State = IDLE; //set current state as IDLE
}
//if slave ACKed and this was not the final byte
else
{
I2C_WriteBufferPtr++; //increment pointer that points at data to be transmitted
SMB0DAT = *I2C_WriteBufferPtr; //write next byte to SMBus data register
}
SMB0CN0_SI = 0; //clear interrupt flag
break;
}

SFRPAGE = SFRPAGE_SAVE;
}

Conclusion

The videos below demonstrate the functionality of this project.

In the first video, the LED is initially illuminated by bright (though indirect) sunlight shining through a large window and has whitish with a hint of blue. The LED then changes to the color of a translucent plastic cap placed over the sensor.

In the second video, the LED replicates the color of five different Christmas lights. I did this in the dark because the little lights get overpowered by ambient illumination. The order of colors is magenta, yellowish-orange, green, blue, then red. The colors all match up pretty well, though in the video the LED looks more of a bluish when it should be green (it looked more green in real life).

 

 

 

Mfr Part # BH1745NUC-E2
COLOR SENSOR I2C 8-UDFN
Rohm Semiconductor
14,95 kr
View More Details
Add all DigiKey Parts to Cart
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.