Projects Research & Design Hub

White Hot: Measuring Color Temperature

Written by Jeff Bachiochi

Using an Arduino Mega

Centuries before the science of color temperature was understood, blacksmiths already knew how to read the temperature of a heated piece of metal by its color emissions. In this article, Jeff discusses the math and science behind color temperature. He then details his project that uses a color-light-to-digital sensor to measure the color temperature of a light source.

  • How to use a color-light-to-digital sensor to measure the color temperature of a light source
  • What is the theoretical “black-body radiator“?
  • How to understand additive and subtractive color
  • How does the TCS3472 sensor work?
  • How to use the TCS3472 sensor and its I2C interface
  • How to connect the sensor to a display for output
  • How to use the Heltec Wi-Fi Kit 8 to make our device portable
  • ams TCS3472 sensor
  • ams TSC34725 sensor
  • Arduino MEGA
  • Heltec Arduino with an integrated 4×20 graphic display 
  • Espressif Systems ESP8266
  •  PacTec enclosure
  • Heltec Wi-Fi Kit 8

“White as snow,“ “white chocolate,“ “white wedding dress,“ “white lightning“ and “white diamonds“ are common descriptions of items in our world that include the descriptor “white.“ You can probably think of many others. Some of these describe a whitish hue, while others don’t connote a color at all. The most truthful might be “white hot.“ While today we might use this to describe someone that is “drop-dead gorgeous,“ according to Merriam Webster, this was first used as meaning “being at or radiating white heat.“ If you’ve ever sat by a campfire or gazed at a candle, you have experienced heat through radiation and colors associated with the flame.

Before we can discuss temperature and color, we need to begin with the “black-body radiator“ [1]. This is a theoretical object that can absorb all radiation that falls upon it. Radiation is the emission or transmission of energy in the form of waves (or particles) through some medium. This includes: electromagnetic radiation, such as radio waves, microwaves, infrared, visible light, ultraviolet, x-rays and gamma radiation (γ). At absolute zero—the coldest temperature possible—no heat is generated in a black body. Absolute zero is -273.15°C (Celsius) or -459.67°F (Fahrenheit). Science often uses the K (Kelvin) temperature scale to make more sense.

K = °C + 273.27
so absolute zero = 0K
… and freezing = 273.16K

As a black-body radiator’s temperature rises, it gains energy and will emit radiation beginning at the low end of the electromagnetic spectrum. Figure 1 is a good illustration comparing radiation type and temperature for the electromagnetic spectrum [2]. We experience this emission as color once its wavelength enters the visible spectrum: 400nm-700nm. A blacksmith heats a piece of steel in his forge to affect its malleability. While it’s not a perfect black body, the physical properties of the metal change with temperature, and he can estimate its temperature by its color emission alone (Figure 2) [3].

FIGURE 1 – Shown here is the frequency/wavelength /color temperature of the electromagnetic spectrum of energy [2]. Note those areas of protection by our atmosphere.
FIGURE 2 – A blacksmith uses the color emissions given off by a heated metal to estimate its temperature [3]. This is key to being able to form and temper it.

We can therefore define the color temperature of a light source in terms of the absolute temperature of a black-body radiator or “chromaticity.“ Chromaticity is the quality of color, independent its brightness or luminescence. Chromaticity consists of two independent parameters, often specified as hue (h) and as colorfulness (s). Colorfulness is alternatively called saturation or chroma. A color space chromaticity diagram (Figure 3) defines all visible color based on these parameters [4]. Note: Temperature color has been indicated within the color space. Green and violet fall outside the black body path or Planckian locus (see physicist Max Planck), with green (secondary) being a combination of primary yellow and blue, and violet (secondary) being a combination of primary red and blue.

FIGURE 3 – The CIE (International Commission on Illumination) 1931 color chromaticity diagram was the first to demonstrate the link between wavelengths in the electromagnetic visible spectrum, and physiologically perceived colors of human color vision. Note the plot of chromaticities of black-body light sources of various temperatures [4].

This has to do with the source of the color. If the source is projected, the three wavelengths used are RGB (red, green, blue) and are additive. If the source is reflected, then the pigments used are CMY (cyan, magenta, yellow) and are subtractive. Note: Many of us were taught early on that the three primary colors were RYB (red, yellow, blue). Figure 4 shows the computational effects of each system.


Advertise Here

FIGURE 4 – On the right is the RYB (red, yellow, blue) plot of colors we learned to mix with crayons in kindergarten. The pigments limit/filter any light reflected to our eyes. On the left is the RGB (red, green, blue) plot of a monitor reproducing color through the additive process of light sources. In the center the CMYK (cyan, magenta, yellow, black) plot shows how a printer mixes inks in a process similar to crayons. While equal parts of CMY equal K (black), it’s a waste of colored inks to make black, so when black is needed, a separate black ink is used.

These are the hues of chromaticity. The other aspect of chromaticity—colorfulness, saturation or chroma—has to do with its intensity. If our brains receive equal signal strengths from all three types of cones (color photoreceptors) in our eyes, then we would perceive white light. If these cone outputs were reduced from maximum to minimum, the resultant perception would be a fade to black. Likewise, if some other mix of cone outputs were perceived as, say, green, and these outputs were reduced to minimum while remaining in the same proportions, the perception would be a fade to gray.

We assume that a (white) source illuminating a subject is perfect. A perfect white source would give the expected result. The subject under observation would absorb/reflect the wavelengths, and we would perceive the expected colors. However, if the source were lacking some frequency, the resultant perception would be skewed. The missing frequency cannot be reflected, and so the result will be lacking. We have two options for a light source—the sun or an artificial source.

You’ve most likely noticed the temperature color is the opposite of the way we think of temperature and the visible color spectrum (as red = hot and blue = cold). With color temperature, red is of a lower temperature and therefore cooler than blue. The sun’s effective temperature, defined by the total radiative power per square unit, is about 5,800K. During sunrise, the tangential angle of light passes through more atmosphere, scattering the shorter-wavelength blue light from our view. As the path becomes a more direct angle, this scattering can be seen off-axis as blue skies.

Figure 5 shows how the source color temperature can vary between the sun and artificial lights. The color temperature value of a light source refers only to the visual appearance of the source. It does not necessarily describe the effect this source will have on photographs or digital images. The color temperature does not take into consideration the spectral distribution of a visible light source, and is not a reliable means of selecting suitable filters or creating look-up tables for color balance corrections. While different light sources may be described as having the same color temperature, without proper white balance, spectral output cannot determine the correct adjustments for color temperature balance. So, let’s look at one possible sensor that will allow us to get a feel for the spectral content of light exposed to it.

FIGURE 5 – This chart shows how the sun (outdoor source of light) and various types of artificial lights (indoors) produce color temperatures from a reddish (relaxing) tint to an bluish (intense) tint. In their art, photographers use these subtle tints of white light to produce an emotion.

The TCS3472 is one of many sensors manufactured by ams, a global leader in the design and manufacture of advanced sensor solutions. It consists of three sets of four filtered sensors protected by an IR filter. Three of the four filtered sensors have color filter RGB. Sensor values and control registers are available via an I2C interface.

The amount of light falling on each set of silicon photodiodes is measured by an integrating analog-to-digital converter (ADC), one for each color. All ADCs operate in parallel, so all ADCs use the same integration time. The device covers a wide signal range. For low-level signals, higher programmable gains and longer integration times can be used. At the other extreme, higher-level signals will require lower gains and shorter integration times.

The integration time period is chosen as a multiple of 2.4ms. A saturated device will present a count of 1,024 for each period. So, you want to make sure the total count (for any sensor) / number of periods is less than 1,024. This is an analog saturation check. A sensor’s total value is held in a byte register pair, and therefore is limited to a maximum of 65,535. It is possible to have no analog saturation, but choose a high number of integration periods, where the total count is greater than 65,535. This is a digital saturation check. You would need to adjust gain and/or number of integration periods to keep the results high enough to provide good resolution, without going into analog or digital saturation.

The register set for the TCS3472 is presented in Figure 6. You’ll note that the COMMAND register is not really a physical register, but rather a pointer used to access registers 0x00-0x1B and alternately clear an interrupt. Besides pointer data, the COMMAND byte also enables/disables the auto increment of the pointer register function.

FIGURE 6 – These are the I2C registers of the TCS3472x. Note: the first data byte of a write will be a COMMAND byte, which sets the pointer to a register of interest. Additional data bytes are stored beginning at the pointer. A read will produce data, also beginning from that pointer.
(Click to enlarge)

The device has an interrupt output that can be triggered by a clear channel conversion count that is less than or greater than minimum and maximum values you program into registers 0x04/5and 0x06/7. This interrupt can be further limited by requiring a number of consecutive out-of-range counts from 1 to 60, programmed into register 0x0C. The status of a conversion is found in register 0x13. Channel counts follow in registers 0x14-0x1B. The only interrupt that can be generated is the out-of-bounds interrupt, and it is not used in this project. Note that the external LED is controlled via this interrupt output, and it can be enabled and disabled via the AIEN bit in the ENABLE register (0x00).


Advertise Here

You must set a number of registers to control the operation of the TCS3472. There are timing and amplification areas of configuration. First is the general timing of the device. Assuming the device has been enabled (AEN, WEN, POW), it will continuously idle and sample the sensors. The idle or wait time is set by the wCycles or WTIME. Each cycle adds 2.4ms to the operation. The cycle values are two’s complement values (inverts all bits and adds 1). Therefore, to set the minimum wCycles equal to 1 (2.4ms), 0xFF must be written to the WTIME register (0x03). The CONFIGURATION register (0x0D) holds the WLONG bit that, when enabled, will multiply the WTIME by 12. The maximum WTIME will be approximately 7 seconds. This allows the device to remain in idle condition for a large percentage of time, lowering the average current of an active device by a factor of 5. However, device current is only 0.3mA to begin with.

The integration time has a similar register (ATIME 0x02). You set the integration time in periods of 2.4ms. If you wanted to set the integration time to 50ms, you would use an aCycle count of 50ms / 2.4ms = 20.8 (or 21 periods). The two’s complement of 21 (0x15) is 235 (0xEB). There is no multiplier for the integration time. However, you can adjust the gain of the sensor amplifiers, which is similar to a multiplier, without actually adding any more time to the conversion.

You can see that if all the timing registers are set to maximum, one sample period would be WTIME = 0x00 (614ms), WLONG = 1 (times 12) 614ms × 12 = 7.4 seconds, plus ATIME = 0x00 (614ms) 7.4 seconds + 0.614 seconds = 8.014 seconds. Reading the resultant registers prior to this time would provide the data for the previous sample only.


Since the TSC3472 is an I2C device, it can be easily interfaced using a 3.3V Arduino (or a 5V Arduino with some level shifting on the I2C pins). I’ll begin this project with an Arduino MEGA. A library for the TCS3472 is available, but I won’t use it for this project. Instead I’ll break it all down into visible function calls.

Let’s begin with reading all of the registers. To do this we’ll need to write a command to the device. As we discussed previously, the COMMAND byte sets up the pointer to the register we want to communicate with (either to read from or to write to). It also instructs the device whether or not to increase this pointer automatically after each register access. Using a COMMAND value of 0xA0 will set the pointer to register 0x00 and enable auto increment. Since the device uses registers 0x00-0x1B, we will need to read 0x1C (28) registers values. Listing 1 shows the basic Arduino code needed to read all of the registers in the TSC3472x.

// This example code is in the public domain.
String SignOn = "TCS3472 Probe 5/20/2020";
#include <Wire.h>
byte I2CADDRESS = 0x29;
#define RB 0x00
// repeat same address
#define AI 0x20
// auto increment address
#define SF 0x60 // special function
byte TYPE = AI;
byte REGISTER = 0;
const int REGISTERCOUNT = 0x1C;
unsigned int BYTEARRAY[255];
const unsigned long I2CCLOCK = 400000;
// setup()
void setup()
// join i2c bus (address optional for master)
// start serial for output
// loop()
void loop()
// readI2C()
void readI2C()
ADDR = 0;
int o = 0;
if (Wire.requestFrom(I2CADDRESS, REGISTERCOUNT)) // request bytes from slave device
while (Wire.available())
// slave may send less than requested
int c =;
// receive a byte as character
Serial.println("Nobody There!");
// writeI2C(int c)
boolean writeI2C(byte c)
byte COMMAND = 0x80 | TYPE | ADDR;
boolean q = false;
// transmit to device
Wire.write(COMMAND); // sends one byte
Wire.write(BYTEARRAY[REGISTER + c]);
// sends one byte
if(Wire.endTransmission() == 0)
// stop transmitting
q = true;
return q;

In Listing 1 we’ve collected 28 registers, and they are in BYTEARRAY[]. Displaying these requires the majority of programming, and will therefore not be listed here. However, Listing 2 shows a typical display of the output from this function. There are actually two phases for each register. The first gives raw register info, register name and hex value found there. The second breaks this down—where necessary—indicating any special bit significance.

0x0 - Enable States value = 0x1b
Interrupt is Enabled
Wait is Enabled
Sensor is Enabled
Power is Enabled
0x1 - Integration Time value = 0xec
Integration Time = 48.00ms
0x2 - not implemented = 0x34
0x3 - Wait Time value = 0x9
Wait Time = 592.80ms
0x4 - Clear Interrupt low threshold value (LSB) = 0x0
0x5 - Clear Interrupt low threshold value (MSB) = 0xde
Clear channel low threshold = 56832
0x6 - Clear Interrupt high threshold value (LSB) = 0x0
0x7 - Clear Interrupt high threshold value (MSB) = 0x0
Clear channel high threshold = 0
0x8 - not implemented = 0x0
0x9 - not implemented = 0x0
0xa - not implemented = 0x0
0xb - not implemented = 0x0
0xc - Interrupt Persistence Filter value = 0x0
Consecutive additional Max/Min thresholds = 0 times
0xd - CONFIGURATION value = 0x2
Wait multiplier = 12
0xe - not implemented = 0x0
0xf - CONTROL value = 0x3
Gain = 60
0x10 - not implemented = 0x0
0x11 - not implemented = 0x1
0x12 - ID value = 0x44
0x13 - STATUS value = 0x11
Interrupt is Set
Sample is Valid
0x14 - Clear Data value (LSB) = 0x69
0x15 - Clear Data value (MSB) = 0xc
Clear channel raw data = 3177
0x16 - Red Data value (LSB) = 0xee
0x17 - Red Data value (MSB) = 0x5
Red channel raw data = 1518
0x18 - Green Data value (LSB) = 0x2
0x19 - Green Data value (MSB) = 0x5
Green channel raw data = 1282
0x1a - Blue Data value (lSB) = 0x2a
0x1b - Blue Data value (MSB) = 0x4
Blue channel raw data = 1066

A check of the value in the ID register (0x12) tells me the values are good. This device is actually a TSC34725 (a version that uses an I2CAddress of 0x29 and runs off 3.3V). Note that some registers are not used (or at least have not been defined). With the displayAllRegisters() function working, we can expand this program by adding a menu to allow us to choose alternate functions.

The next function that will be useful is to change a register’s contents. Only the first registers 0x00-0x0D are R/W (read/write) registers. Six of these are byte oriented, and four are word oriented. A sub menu will allow you choose which of these you want to change.

—-Change Register—-
0 – 0x00 Enable
1 – 0x01 Integration Time
2 – 0x03 Wait Time
3 – 0x04/5 Low Threshold
4 – 0x06/7 High Threshold
5 – 0x0C Persistence
6 – 0x0D Configuration
7 – 0x0F Control

Menu selection?

When a choice is made here, you get to enter a value for the byte or word register pairs. REGISTER and REGISTERVALUE variables are used to update a single register in the TSC3472x.

The Main menu below gives you total control of the device at your fingertips. We can stop, start and compare conversions to find the optimum settings for any light condition.

—-TCS3472 Probe—-
1 – Read and display all I2C registers
2 – Change an I2C register
3 – Get a raw sample
4 – Toggle external LED
5 – + Gain
6 – – Gain
7 – + Integrate Time
8 – – Integrate Time
9 – Sample and auto adjust


Advertise Here

Menu selection?

Note that while complete control of all registers is available, the main menu has shortcuts that allow adjustments of the two main parameters that you will want to change. The Gain sensor and its Integration Time can be altered directly via single-keys commands. I added a command to toggle the on-board LED light source that can be used for reflective measurements. Command 3 retrieves a sensor sample using the present configuration. The final Command, 9, makes fine tuning and sampling automatic.


To make the magic happen, we need to make changes continuously to the Gain and Integration Time, and look for that point just before the system reaches saturation. Previous discussion showed both analog and digital saturation. It was identified that if the Integration Time exceeded a count of 63 (63 × 2.4ms = 151.2ms), unsaturated sample could occur up to the maximum counts of 65,535. For Integration Times less than 64 counts, the maximum counts allowed were substantially less. Using these rules, we can determine if saturation occurs for a conversion.

Since we want to determine the point just before saturation occurs, my algorithm begins with minimum Integration Time and maximum Gain. If saturation occurs, then we must turn the Gain down a notch, since the Integration Time is already at its minimum. Gain has four steps—60, 16, 4 and 1. Once we have a conversion with no saturation, the Gain is now valid, and we can ramp up the Integration Time until saturation again occurs.

The Integration Time has 256 steps 1-256 cycles (where 0=256). It could take a few seconds to try all possible steps. As each step increases the Integration Time, the conversion time also increases. While you could shorten the potential number of steps by using a different algorithm, I will keep this as simple as possible and just increase by a single step until saturation occurs, and then back off 1 step (count). We should now be at the maximum Gain and Integration Time without going into saturation.


Lux and color temperature calculations can be performed on any unsaturated conversion. The previous configurations changes have been done to optimize the conversion values for the best possible accuracy when calculating the lux (intensity) and color temperature (surface temperature in Kelvin). The intensity of light hitting the sensors (RGBC) can be translated into a value that closely matches the human eye, by totaling the adjusted red, green and blue conversions and applying a device factor. You can see that as long as the color outputs stay in proportion to one another, the Lux has virtually no effect on the Color Temperature, which is essentially a ratio of blue to red values.

So far, we’ve needed a PC connected to the TSC3472x to get a conversion. Heltec makes a DIP-style Arduino with an integrated 4×20 graphic display for around $10 that is based on the Espressif Systems ESP8266. The ESP8266 has Wi-Fi, which we won’t be using here, but otherwise it is perfect for this project, as you will quickly see. I did use Adafruit’s support for its SSD1306 display, because Heltec’s library doesn’t have good text support. No special changes to the Arduino program are necessary, beyond that of the display support. However, I want to make this simple and the menus will have to go. Well … not really.

I want to use two functions in conjunction with the display. I want the user to be able to start an automatic conversion, and also have control over the LED. To do this, I need a couple of pushbuttons and a way to convert the button push into a simulated menu selection. This was done in the getUserInput() routine that collects keyboard input. Normally, execution is waiting here for you to enter characters (menu choice). I added two digitalRead() tests—one for each button. A button press (grounded input) forces exit of the getUserInput() routine, returning the character “4“ (Toggle external LED) or “9“ (Sample and auto adjust). Other than how these buttons produce virtual keyboard input, nothing else changes.

Although the display is graphic in nature, you have control over every pixel on the screen. I am using it as a four-line-by-20 character display. Sign-on messages are produced on the first two lines, and the second two lines are used to show the progress of the conversion process. This should end up with a display as shown in Figure 7 when the conversion is complete. To produce text characters, the library routine is responsible for providing the character glyph (picture) of each character, as well as where to place the character within the display’s screen.

FIGURE 7 – Shown here is the resultant display (SSD1306) for a sample of the morning sky prior to the sun breaking the horizon. There is very little light (Lux), with a reddish color temperature of approximately 4,382K.

The SSD1306 uses separate display and buffer spaces. This allows the user to completely draw a screen in the buffer space before transferring it to the display space. Updates therefore look instantaneous. The buffer remains intact, and you may continue to add to it or clear it in preparation for new text (or graphics).


Up to this point we’ve received power from the USB port, while attached to the PC. We added buttons to make it a stand-alone device, but we must do something about power! The Heltec device not only has a battery input, but also—because everything works on 3.3V—a single cell Li-Ion battery is a perfect mate. The Heltec Wi-Fi Kit 8 also has a Li-Ion battery charger built in to truly make all of this work with “no strings (or wires) attached.“

I mounted the Wi-Fi Kit 8 in a PacTec enclosure (Figure 8). I wanted to be able to aim the sensor independently from the display, and found an itty-bitty book light that looked like it would work well. The four-wire sensor cable had to be replaced with a couple of pieces of flat cable that I zipped down to two conductors wide. These could be fed through the sensor arm quite nicely, without any pinching to the wires.

To test this rig out, I got up before sunrise to make an early morning reading before the sun broke the horizon. Look back at Figure 7 for this sample. You’ll note the Lux value is less than 1 and the color temperature is 4,382K. This is just what I would suspect for sunrise. A clear, mid-day sky produced a high color temperature of 5,732K. You get a good sense of the many shades of white while driving at night. The range in color temperature for various styles of headlights is 3,000K to more than 12,000K!

Picking out a perfect shade of white is a nightmare come to life. Pure white? Off white? Simply white? Benjamin Moore has more than 35 shades of white! The shade of white (or other color) you apply to your walls will greatly influence the way you feel in that room.

Remember to wash your hands! Too much to do, too little time. 


[2]  InductiveloadNASA, 2007. File: EM Spectum Properties edit.svg. Wikimedia Commons. This file is licensed under the Creative CommonsAttribution-Share Alike 3.0 Unported license.
[3] Jeff Kubina, 2008. File:BlacksmithWorking.jpg. Wikimedia Commons. This file is licensed under the Creative CommonsAttribution-Share Alike 2.0 Generic license.
[4] en:User:PAR – en:User:PAR,  2005. File:PlanckinaLocus.png. Wikimedia Commons. Public Domain.

Adafruit |
ams |
Espressif Systems |
Heltec Automation |
PacTec Enclosures |


Keep up-to-date with our FREE Weekly Newsletter!

Don't miss out on upcoming issues of Circuit Cellar.

Note: We’ve made the May 2020 issue of Circuit Cellar available as a free sample issue. In it, you’ll find a rich variety of the kinds of articles and information that exemplify a typical issue of the current magazine.

Would you like to write for Circuit Cellar? We are always accepting articles/posts from the technical community. Get in touch with us and let's discuss your ideas.

Sponsor this Article
Website | + posts

Jeff Bachiochi (pronounced BAH-key-AH-key) has been writing for Circuit Cellar since 1988. His background includes product design and manufacturing. You can reach him at: [email protected] or at:

Supporting Companies


Upcoming Events

Copyright © 2021 KCK Media Corp.

White Hot: Measuring Color Temperature

by Jeff Bachiochi time to read: 18 min