Color sensing doesn't have much practical use for a regular electronics enthusiast, being used mostly in automation, but it can be a fun project when learning or tinkering with electronics. Although there are affordable dedicated color sensors, this article focuses on sensing colors using just a regular RGB led and a Cadmium sulfide (CdS) Light Dependent Resistor (LDR) with a bit of math.
The color accuracy is pretty good and depends on the hardware setup and calibration described later. Also matte paper is more accurately scanned than glossy paper.
Hardware and schematic
Components required are few. Below is the schematic which illustrates an ATmega328PB but any microcontroller with an ADC will work.
In a standard LDR photoresistor, resistance drops when light hits it and increases in the dark. Here LDR is placed on the high-side so when the light is brighter resistance drops and voltage rises. Having a higher ADC reading correlate to a brighter reflection is much more intuitive when debugging live sensor values.
Resistor R1 is used to create a voltage divider and pulls the voltage to ground when no light hits the sensor. I found that 100k works better than 10k because it uses more ADC dynamic range. When using 10k, maximum reading was around 150, but a 10-bit ADC has 1024 values. Changing to a 100k raised maximum ADC reading to 800 which provides more accurate colors.
In my setup I used 220 ohm current limiting resistors for red and blue leds, and 1k for the green led. That's because I had green color bleed for colors that don't have green such as magenta. Also, I believe the CdS LDR is more sensitive to green so using a higher resistor decreases the current and thus the brightness.
Even though the RGB led is connected to ADC pins, analog pins are not needed for this so any digital pin will work.
Below is an overview of the hardware used during testing. The white and black matte paper was used for calibration. The rest are color samples where most of them are glossy and a few matte.
Close-up with the color sensor.
The enclosure is build out of black foam board to prevent inside reflections, to keep ambient light out and to set a consistent height between the scanned object and the sensor. In the middle is a separator to prevent light from the led to reach the LDR sensor directly. It is shorter in height than the enclosure walls so that the light can reflect from the paper to the sensor. Because the foam board is glossy, I placed some black paper around the led and the sensor to reduce light reflection.
The idea is that the light from the led should not reach the sensor directly but only when reflected by the scanned object. If the paper is glossy there will be some reflected light even though the specific color might not contain that color. This will make the scanned color less accurate.
How RGB color reading works
A photoresistor can only measure the total intensity of light hitting their surface. So how to extract full 24-bit RGB values?
Objects do not possess inherent color; rather, they reflect specific wavelengths of light while absorbing others. A red surface looks red because it reflects red wavelengths (~620-750 nm) and absorbs green and blue wavelengths. A white surface reflects nearly all visible wavelengths equally. A black surface absorbs almost all wavelengths and reflects very little back. When light shines onto a surface, the amount of light that bounces off and reaches our sensor depends directly on how much of that specific wavelength the material reflects.
Instead of using a complex camera sensor, we use a single 3-in-1 RGB led with a CdS Light Dependent Resistor (LDR) inside an enclosed chamber. Because the LDR cannot distinguish between colors, we split the color detection into three steps:
Step 1: switch on only the red led. The red light reflects from the target surface. The LDR measures the intensity of the bounced red light and converts it into a digital ADC value.
Step 2: we turn off the red led and switch on only the green led. The LDR measures how much green light reflects back.
Step 3: switch off the green led and turn on only the blue led to measure the reflected blue light.
By measuring the reflected light intensity across three distinct wavelengths, we construct a 3-channel dataset: reflected red, reflected green, reflected blue.
The light detector is a Cadmium Sulfide (CdS) Photoresistor. CdS sensors have a spectral response curve peak around 540-560 nm, which matches human visual sensitivity (peak green-yellow perception). Because it is slightly less sensitive to deep blues and reds, calibration is needed to balance the readings across the spectrum.
Calibration (white and black balance)
Because variables like ambient room light, led brightness differences, and sensor distance affect voltage values, the raw ADC readings cannot be converted into RGB values directly.
We need two calibration reference points:
White Calibration (100% reflectivity): Placing a pure white target in front of the sensor gives the maximum possible reflected reading for red, green, and blue individually.
Black Calibration (0% reflectivity): Placing a pure black target gives the minimum baseline reading (dark current/stray ambient light).
By mapping any new surface's raw reading linearly between its calibrated black (0) and white (255) endpoints, we cancel out environmental glare and sensor non-linearities.
Software
Formula
Calibration values are stored in two arrays, one called _whiteArray for storing measured ADC values when each color reflects of a white paper and _blackArray that stores values from black paper reflections. To calculate the scanned color following linear map formula is used:
colourArray[i] = ((readingADC - _blackArray[i]) * 255) / greyDiff
- greyDiff = _whiteArray[i] - _blackArray[i]: This calculates the dynamic range which is the total span of possible valid sensor readings.
- readingADC - _blackArray[i]: This offsets the raw reading. It removes the baseline noise or minimum reflection so that a pure black reading shifts down to 0.
- Dividing by greyDiff: This normalizes the reading into a fraction between 0.0 and 1.0, showing where the current color sits relative to the min/max limits.
- Multiplying by 255: This scales that 0.0 to 1.0 fraction up to a standard 8-bit digital color value (0 to 255).
Why is it ordered as ((reading - black) * 255) / greyDiff? In 8-bit microcontrollers working with floating-point math (decimals) is slow and eats up memory. By multiplying by 255 before dividing by greyDiff, the code performs integer math, preserving accuracy without needing decimals.
Library
The library is written in C++ and supports classic AVRs Class 0 and also Class 1 UPDI devices. Since there is a major difference between the two architectures, the library will do a pre-processor check to detect the class type and include the proper code.
Although each RGB color is 8-bit (0-255), the ADC is set to 10-bit (0-1023) for more precision.
The ADC voltage reference is set to VCC since internal 1.1V is too low. This works for a stable supply voltage. For a battery powered device an external voltage reference is needed on the AREF pin and the ADC setup in the constructor updated to use that.
Constructor
ColorSensor(volatile uint8_t& RGB_ddr, uint8_t red_pin, uint8_t green_pin, uint8_t blue_pin, uint8_t LDR_pin, uint16_t eepromAddr)
Configures RGB led pins as outputs and the ADC. ADC prescaler is set to 128 so for a 16MHz CPU frequency will produce 125kHz ADC clock (16000000 / 128 = 125000). If other CPU frequency is used, check the resulting ADC clock and modify the prescaler if needed.
RGB_ddr or RGB_port
DDR (Class 0) or PORT (Class 1) where the 3 pins of the RGB led are.
red_pin, green_pin, blue_pin
Pin number of each led color.
LDR_pin
ADC channel number where the LDR pin is connected.
eepromAddr
Since the calibration values are stored on the EEPROM, this is the starting address where to read and write the EEPROM. Optional value. Default is 0.
Calibration
void calibrate()
Used to calibrate the system. You need a piece of white and black paper. First the blue and green leds will blink indicating that it expects a white sample. After 5 seconds it will scan the white sample by pulsing each color.
Next the red led will blink indicating it expects a black sample. The scanning will again start after 5 seconds.
A delay of minimum 100ms between pulsing each led color is needed to give time for the LDR to react. Value is set by LDR_LATENCY_DELAY_MS. If ENABLE_UART_DEBUG in colorSensor.cpp is set to 1, the calibration values will be printed on a serial terminal over UART. Set this to 0 if UART library is not available or needed.
The calibration values will be saved on the EEPROM starting at the address defined in the constructor (default 0).
Read calibration
bool readCalibration()
Reads calibration values into the _whiteArray and _blackArray from the EEPROM. Always returns true. Array values can be accessed directly and printed for example over UART.
Read color
void readColor(uint8_t &r, uint8_t &g, uint8_t &b)
Scans the color by pulsing red, green and blue leds and updates the pass by reference variables.
Code example
#include <stdio.h> #include <stdlib.h> #include "colorSensor.h" #include "uart.h" int main (int argc, char** argv) { uint8_t red = 0; uint8_t green = 0; uint8_t blue = 0; char hexBuffer[8]; // Room for "#RRGGBB\0" // UART UART_begin(&uart0, 115200, UART_ASYNC, UART_NO_PARITY, UART_8_BIT); // Color sensor #if (AVR_CLASS == 0) // PORTC pins PC1, PC2, PC3 for RGB, PC0 for ADC ColorSensor sensorRGB(DDRC, PC1, PC2, PC3, PC0); #else // PORTA pins PA1, PA2, PA3 for RGB, PA0 for ADC ColorSensor sensorRGB(PORTA, 1, 2, 3, 0); #endif if (sensorRGB.readCalibration()) { // Read the arrays for debugging or UI display UART_sendString(&uart0, "White ADC calibration values: "); UART_sendString(&uart0, "rgb("); UART_sendInt(&uart0, sensorRGB._whiteArray[0]); UART_sendString(&uart0, ","); UART_sendInt(&uart0, sensorRGB._whiteArray[1]); UART_sendString(&uart0, ","); UART_sendInt(&uart0, sensorRGB._whiteArray[2]); UART_sendString(&uart0, ")\n"); UART_sendString(&uart0, "Black ADC calibration values: "); UART_sendString(&uart0, "rgb("); UART_sendInt(&uart0, sensorRGB._blackArray[0]); UART_sendString(&uart0, ","); UART_sendInt(&uart0, sensorRGB._blackArray[1]); UART_sendString(&uart0, ","); UART_sendInt(&uart0, sensorRGB._blackArray[2]); UART_sendString(&uart0, ")\n"); } else { // Run calibration sensorRGB.calibrate(); } while (1) { // Read color sensorRGB.readColor(red, green, blue); // RGB color UART_sendString(&uart0, "rgb("); UART_sendInt(&uart0, red); UART_sendString(&uart0, ","); UART_sendInt(&uart0, green); UART_sendString(&uart0, ","); UART_sendInt(&uart0, blue); UART_sendString(&uart0, ")\n"); // HEX color format snprintf(hexBuffer, sizeof(hexBuffer), "#%02X%02X%02X", red, green, blue); UART_sendString(&uart0, hexBuffer); UART_sendString(&uart0, "\n"); _delay_ms(2000); } return (0); }
Links
|
Link includes: - colorSensor.h, colorSensor.cpp - adc.h, adc.c - Serial_Display_Color.pde Sketch for Procesing app used to display the color on a PC. |
|
| v1.0 | colorSensor |
| Other links | |
| uart | |
| Processing | |
| Changelog | |
| v1.0 |
29-07-2026 Public release under GNU GPL v3 license. |





No comments:
Post a Comment