If you are building an Arduino color detector for sorting, robotics, or quality control, the TCS3200 (or TCS230) frequency-to-light converter is the most common hobbyist module on the market. However, its raw square-wave output and sensitivity to ambient light cause frequent headaches. This guide targets the Arduino Uno R3 (ATmega328P) and provides a production-ready approach to wiring, frequency scaling, and error handling that generic tutorials miss.
Project Overview & Difficulty Rating
Estimated Time: 45 minutes
Approximate Cost: $12 - $18 USD
Target Board: Arduino Uno R3 (ATmega328P DIP variant, 16MHz clock)
Hardware Spec Sheet & Parts List
Do not buy the bare TCS3200 IC; you need the breakout module with the integrated 4-LED white light array. The onboard LEDs provide a controlled illumination source, which is mandatory for consistent reflectance readings.
| Component | Exact Variant / Spec | Notes & Bench Tips |
|---|---|---|
| Microcontroller | Arduino Uno R3 (ATmega328P) | Nano or Mega work, but pin mappings in the code below must be adjusted. Uno R3 has the most stable 5V rail for the sensor LEDs. |
| Color Sensor | TCS3200 / TCS230 Module | Must include the 4-LED array and the 2.54mm header pins. Look for modules with a built-in power indicator LED. |
| Wiring | 24 AWG Female-to-Male Jumpers | Keep wire runs under 15cm (6 inches). Longer wires introduce capacitance that distorts the high-frequency square wave output. |
| Calibration Target | Matte White & Black Cardstock | Glossy paper causes specular reflection (glare) that blinds the photodiodes. |
Pin Mapping & Wiring Steps
The TCS3200 uses digital pins to select the internal photodiode filters (Red, Green, Blue, Clear) and to set the output frequency scaling. We use pinMode and digitalWrite to control it, and Arduino's pulseIn() function to read the output.
| TCS3200 Pin | Arduino Uno R3 Pin | Function |
|---|---|---|
| VCC | 5V | Power (Do not use 3.3V; the onboard LEDs will not reach full brightness). |
| GND | GND | Common ground reference. |
| S0 | D8 | Output Frequency Scaling Bit 1. |
| S1 | D7 | Output Frequency Scaling Bit 2. |
| S2 | D6 | Photodiode Type Selection Bit 1. |
| S3 | D5 | Photodiode Type Selection Bit 2. |
| OE | Not Connected | Output Enable (Active LOW). Leave unconnected to keep output always enabled. |
| OUT | D4 | Square wave output (read via pulseIn). |
Wiring Sequence:
- De-energize the board: Unplug the Arduino USB cable before wiring.
- Connect VCC to 5V and GND to GND. Verify the module's power LED illuminates when plugged in.
- Connect S0, S1, S2, and S3 to digital pins 8, 7, 6, and 5 respectively.
- Connect OUT to digital pin 4.
- Mount the sensor exactly 10mm to 15mm away from your target surface. The 4 white LEDs have a focal cone; moving further away drops the signal-to-noise ratio drastically.
Complete Calibrated C++ Code
This sketch targets the Arduino Uno R3. It sets the frequency scaling to 20% rather than 100%. Why? At 100% scaling, close-range white readings can exceed 600kHz. The Uno's pulseIn() function struggles to accurately time pulses shorter than 2 microseconds without jitter. 20% scaling keeps the frequency in a reliable 10kHz–120kHz range.
// Arduino Color Detector - TCS3200 with Error Handling
// Target: Arduino Uno R3 (ATmega328P)
#define S0 8
#define S1 7
#define S2 6
#define S3 5
#define OUT 4
// Calibration limits (Update these with your own white/black card readings)
// Format: {Red_Min, Red_Max, Green_Min, Green_Max, Blue_Min, Blue_Max}
const int calibMin[3] = {25, 30, 35};
const int calibMax[3] = {180, 210, 190};
// Pulse timeout in microseconds (100ms)
const unsigned long PULSE_TIMEOUT = 100000;
void setup() {
Serial.begin(115200);
pinMode(S0, OUTPUT);
pinMode(S1, OUTPUT);
pinMode(S2, OUTPUT);
pinMode(S3, OUTPUT);
pinMode(OUT, INPUT);
// Set frequency scaling to 20% (S0=HIGH, S1=LOW)
// This prevents pulseIn() overflow at close range
digitalWrite(S0, HIGH);
digitalWrite(S1, LOW);
Serial.println("Arduino Color Detector Initialized.");
Serial.println("Place target 10-15mm from sensor.");
}
void loop() {
int rawRGB[3];
int mappedRGB[3];
bool readError = false;
// 1. Read Red Filter (S2=LOW, S3=LOW)
digitalWrite(S2, LOW);
digitalWrite(S3, LOW);
rawRGB[0] = pulseIn(OUT, LOW, PULSE_TIMEOUT);
// 2. Read Green Filter (S2=HIGH, S3=HIGH)
digitalWrite(S2, HIGH);
digitalWrite(S3, HIGH);
rawRGB[1] = pulseIn(OUT, LOW, PULSE_TIMEOUT);
// 3. Read Blue Filter (S2=LOW, S3=HIGH)
digitalWrite(S2, LOW);
digitalWrite(S3, HIGH);
rawRGB[2] = pulseIn(OUT, LOW, PULSE_TIMEOUT);
// Error Handling: Check for timeouts
for (int i = 0; i < 3; i++) {
if (rawRGB[i] == 0) {
Serial.println("Error: Pulse timeout on OUT pin");
readError = true;
break;
}
}
if (!readError) {
// Map raw pulse widths to 0-255 RGB values
// Note: Lower pulse width = Higher frequency = Brighter light
// Therefore, we map Max->0 and Min->255
for (int i = 0; i < 3; i++) {
mappedRGB[i] = map(rawRGB[i], calibMax[i], calibMin[i], 0, 255);
mappedRGB[i] = constrain(mappedRGB[i], 0, 255);
}
Serial.print("RGB: ");
Serial.print(mappedRGB[0]); Serial.print(", ");
Serial.print(mappedRGB[1]); Serial.print(", ");
Serial.println(mappedRGB[2]);
identifyColor(mappedRGB[0], mappedRGB[1], mappedRGB[2]);
}
delay(250); // Read 4 times per second
}
void identifyColor(int r, int g, int b) {
// Simple heuristic color identification
if (r > 150 && g < 100 && b < 100) Serial.println(">> Detected: RED");
else if (g > 150 && r < 100 && b < 100) Serial.println(">> Detected: GREEN");
else if (b > 150 && r < 100 && g < 100) Serial.println(">> Detected: BLUE");
else if (r > 150 && g > 150 && b > 150) Serial.println(">> Detected: WHITE");
else if (r < 80 && g < 80 && b < 80) Serial.println(">> Detected: BLACK");
else Serial.println(">> Detected: MIXED/UNKNOWN");
}
Debugging: "Color Readings are Random or Zero"
The most common failure mode when commissioning this circuit is opening the Serial Monitor and seeing Error: Pulse timeout on OUT pin or a string of RGB: 0, 0, 0. If the sensor is failing to return valid pulse widths, check these three things in order:
- USB Brownout (VCC Drop): The 4 white LEDs on the TCS3200 module draw roughly 40mA-60mA combined. If you are powering the Uno R3 from a low-quality USB hub or a PC port limited to 500mA with other peripherals attached, the 5V rail can sag below 4.5V. The TCS3200 internal oscillator becomes unstable, and the OUT pin stops toggling. Fix: Power the Uno via the barrel jack with a 9V 1A wall adapter, or use a high-quality USB-C PD adapter.
- Floating S0/S1 Pins: If S0 and S1 are not explicitly driven HIGH/LOW in the
setup()block, the internal frequency scaler defaults to "Power Down" mode. The OUT pin goes high-impedance, andpulseIn()times out waiting for a falling edge. Fix: Verify your jumper wires for S0 and S1 are fully seated. A loose Dupont connector here is the culprit 80% of the time. - Ambient Light Wash-out: The TCS3200 lacks an integrated IR-blocking filter. If you are testing under direct sunlight or heavy incandescent room lighting, the photodiodes saturate before the onboard white LEDs can establish a baseline reflectance. Fix: Build a shroud. A simple 3D-printed tube or even a piece of black heat-shrink tubing wrapped around the sensor head to block peripheral light will stabilize your raw readings immediately.
Extending and Simplifying the Build
Depending on your end goal, you may want to pivot away from the raw TCS3200 module.
How to Simplify (The Modern Alternative):
If you are frustrated by the manual calibration and pulseIn() timing jitter, swap the TCS3200 for the TCS34725. As noted in the Adafruit Color Sensor Overview, the TCS34725 uses an I2C interface, features a built-in IR blocking filter, and handles the integration time math on-chip. It costs about $3 more but eliminates the need for frequency scaling and manual pulse timing entirely.
How to Extend:
For a sorting robot, raw RGB values aren't enough. Extend the build by converting the mapped RGB values into HSV (Hue, Saturation, Value) color space inside the Arduino code. Hue is largely immune to changes in lighting intensity (Value), meaning your robot can distinguish a dark red apple from a bright red apple reliably. You can also add an I2C OLED display (SSD1306) to print the detected color name in real-time without needing a PC tether.
Frequently Asked Questions
How accurate is an Arduino color detector for paint matching?
It is not accurate enough for professional paint matching. The TCS3200 uses broad-band RGB filters that overlap significantly. It cannot distinguish between subtle shades like "navy blue" and "royal blue" or detect metamerism (colors that match under one light source but not another). For paint matching, you need a dedicated spectrophotometer. The Arduino color detector is strictly for high-contrast sorting (e.g., red vs. green vs. blue parts on a conveyor).
Why does my TCS3200 color detector read white as yellow?
This happens because the onboard white LEDs have a warm color temperature (usually around 3000K-4000K), which naturally emits more red and green light than blue light. When reflecting off a true white surface, the sensor reads higher red/green frequencies, interpreting it as yellow. To fix this, you must perform a "white balance" calibration in your code: read a white card, calculate the ratio of R:G:B, and multiply all subsequent readings by those inverse ratios to force the white card to read as 255, 255, 255.
Can I use an Arduino Nano instead of the Uno R3 for this color detector?
Yes, the code provided above is 100% compatible with the Arduino Nano (ATmega328P variant) because the digital pin assignments (D4 through D8) map identically. However, if you are using a Nano clone with the CH340 USB-serial chip, be aware that the 5V regulator on cheap clones is often rated for only 300mA. If you add an LCD screen and a servo motor to your color sorting rig, you will exceed the Nano's onboard regulator capacity and cause brownouts. Power the Nano via the VIN pin with an external 7-9V supply if adding heavy peripherals.






