部品 · 読了まで14分

ArduinoでTCS3200カラーセンサー:配線、キャリブレーション、コード

TCS3200カラーセンサーの仕組みを解説します。S0〜S3の設定、pulseInでのR・G・Bの読み取り、白と黒のキャリブレーション、色の仕分け、ESP32・Pico・TCS34725のコードを紹介します。

TCS3200カラーセンサーは、Arduinoのプロジェクトで最も広く使われている色検出モジュールです。前にある面の色を、赤・緑・青の成分に分けて測ります。フォトダイオードアレイが受けた光を、そのまま周波数に変換するのが特徴で、面が反射する赤い光が多いほど、赤チャンネルの周波数が高くなります。アナログ読み取りや特別な通信プロトコルは必要なく、ArduinoのpulseIn()命令を使えば数行でR・G・Bの値を読み取れます。お菓子の仕分け機、色を認識するロボット、床の色付きの目印で向きを変える車、前にかざした物の色をまねるランプなどは、どれもこのセンサーで作られています。

このガイドでは、センサーの内部構造、S0〜S3ピンの役割、Arduino Uno・ESP32・Raspberry Pi Picoへの配線、生の値の読み取り、白と黒のキャリブレーションの方法、そして読み取った値がどの色に当たるかを判定するアルゴリズムをステップごとに紹介します。続いて物を色ごとに仕分ける装置のロジックを組み立て、最後にI2Cで動くセンサーTCS34725と比較します。

TCS3200カラーセンサーモジュール
TCS3200カラーセンサーモジュール:中央に8ピンのTCS3200チップ、四隅に面を照らす白色LEDがあり、片側にS0、S1、OE、GNDピン、反対側にVCC、OUT、S2、S3ピンが並んでいます。

TCS3200カラーセンサーとは?仕組みは?

モジュールの中心にあるTCS3200チップ(旧版はTCS230と呼ばれます)の表面には、8×8の格子状に並んだ64個のフォトダイオードがあります。そのうち16個は赤のフィルター、16個は緑、16個は青のフィルターの下にあり、残りの16個はフィルターなし(クリア)で光の総量を見ます。各色の16個のフォトダイオードは並列につながっていて、S2ピンとS3ピンでどのグループを測るかを選びます。色の異なるフォトダイオードがアレイ全体に入り交じって配置されているため、面の一角に当たった光はすべての色のグループに同じように影響します。

選ばれたフォトダイオードのグループが生み出す小さな電流は、チップ内部の電流-周波数変換回路に送られます。この回路はデューティ比50%の矩形波を生成し、その周波数は光の強さに正比例します。つまり測っているのは電圧ではなく、1秒あたりのパルスの数です。真っ暗な中では数ヘルツ、白い面に近づけると数十キロヘルツになります。周波数出力の良いところは、ノイズに強く、Arduinoのどのデジタルピンにもつなげることです。光の強さだけを測りたいなら、LDR光センサーのほうが簡単です。TCS3200は同じことを3つの色ごとに別々に行います。

項目値
チップTCS3200(旧版TCS230、ピン互換)
フォトダイオードアレイ8×8 = 64:赤16、緑16、青16、クリア16
出力デューティ比50%の矩形波、周波数は光に比例
フルスケール周波数(標準)600 kHz(100%)、120 kHz(20%)、12 kHz(2%)
電源電圧2.7〜5.5 V
チップの電流標準1.4 mA(モジュールのLEDの電流は別途)
制御ピンS0、S1(周波数スケーリング)、S2、S3(カラーフィルター)、OE(出力イネーブル、アクティブLOW)
推奨距離約1〜3 cm、毎回の読み取りで同じ距離
照明モジュール上の白色LED 4個

S0〜S3ピン:周波数スケーリングとカラーフィルターの選択

TCS3200の動作は4本の制御ピンで決まります。S0とS1は出力周波数を下げる倍率を決めます。100%ではパルスが数マイクロ秒まで短くなり、Arduino Unoの測定精度の限界に近づきます。一方、2%ではパルスが長くなる代わりに、同じ時間に届くパルスの数が減ります。S2とS3は、どのカラーフィルターを読むかを選びます。これらのピンはArduinoのデジタル出力につなぎ、読み取りのたびにコードから設定します。

S0S1出力周波数のスケーリング使いどころ
LOWLOWパワーダウン電池で動くプロジェクトでセンサーを休止させる
LOWHIGH2%(標準で最大12 kHz)低速なプロセッサー、MicroPython、割り込みでのカウント
HIGHLOW20%(標準で最大120 kHz)ArduinoとpulseInに最適な設定
HIGHHIGH100%(標準で最大600 kHz)ハードウェアカウンターを使った高速な測定
S2S3読み取るフォトダイオードのグループ
LOWLOW赤のフィルター
LOWHIGH青のフィルター
HIGHLOWフィルターなし(クリア、光の総量)
HIGHHIGH緑のフィルター
パルス幅と周波数の関係は単純です。デューティ比が50%なので、LOWの部分は周期の半分です。f = 1 / (2 × t)の式を使えば、pulseInで得たマイクロ秒を周波数に変換できます。たとえばt = 25 µsなら、f = 1 / (2 × 0.000025 s) = 20 kHzです。20%のスケーリングでモジュールのLEDを点けた状態なら、10〜200 µsの値が普通です。

TCS3200の配線:Arduino Uno、ESP32、Pico

モジュールのピンArduino UnoESP32Raspberry Pi Pico
VCC5V3V33V3(OUT)、36番ピン
GNDGNDGNDGND
S0D4GPIO 32GP2
S1D5GPIO 33GP3
S2D6GPIO 25GP4
S3D7GPIO 26GP5
OUTD8GPIO 27GP6
OEGNDGNDGND
OEピンをフローティング状態にしないでください。このピンはアクティブLOWで、HIGHのときや何もつながっていないときは出力がハイインピーダンスになり、pulseInは0を返し続けます。また、ESP32とPicoのピンは3.3 Vで動作します。モジュールを5 Vで給電すると、OUTピンが5 Vのパルスを出し、ボードの入力に負担をかけます。これらのボードでは、モジュールを3V3から給電するか、OUTラインに分圧回路やレベル変換モジュールを入れてください。

モジュール上の4個の白色LEDは、読み取りのたびに面を同じように照らすので、部屋の明るさが変わっても結果が安定しやすくなります。LEDを点灯・消灯するためのLEDピンが別にあるボードもあれば、電源を入れるとすぐにLEDが点くボードもあります。自分のボードの表示を確認してください。最良の結果を得るには、センサーを黒いチューブや厚紙で囲い、物を毎回同じ距離に置き、光沢のある反射しやすい面は避けましょう。ボードのピンの並びはピン配置ツールで確認できます。

ステップごとに:pulseInでR・G・Bを読み取る

  1. 上の表のとおりにモジュールを配線します。OEはGNDに、VCCは5Vにつなぎます。
  2. S0をHIGH、S1をLOWにして、20%の周波数スケーリングを選びます。
  3. 赤を読むにはS2とS3をLOWにし、出力が安定するまで数ミリ秒待ちます。
  4. pulseIn(OUT, LOW)でパルスのLOWの部分の長さをマイクロ秒単位で測ります。ノイズを減らすため、10回の読み取りを平均します。
  5. 緑(S2 HIGH、S3 HIGH)と青(S2 LOW、S3 HIGH)も同じように測ります。
  6. 9600ボーでシリアルモニタを開いて結果を見ながら、いろいろな色の紙をセンサーの前にかざします。
C++ (Arduino IDE)
1// TCS3200 ile ham R, G, B okuma / Raw R, G, B reading with the TCS3200
2const int S0 = 4;
3const int S1 = 5;
4const int S2 = 6;
5const int S3 = 7;
6const int OUT_PIN = 8;
7
8// Filtreyi seçer, 10 darbenin ortalama LOW süresini döndürür (µs) / Selects a filter, returns the average LOW time of 10 pulses (µs)
9unsigned long readPulse(bool s2, bool s3) {
10  digitalWrite(S2, s2 ? HIGH : LOW);
11  digitalWrite(S3, s3 ? HIGH : LOW);
12  delay(5);                                           // çıkışın oturmasını bekle / let the output settle
13  unsigned long total = 0;
14  int count = 0;
15  for (int i = 0; i < 10; i++) {
16    unsigned long t = pulseIn(OUT_PIN, LOW, 50000UL); // 50 ms zaman aşımı / 50 ms timeout
17    if (t > 0) {
18      total += t;
19      count++;
20    }
21  }
22  return count > 0 ? total / count : 50000UL;         // darbe yoksa çok karanlık / no pulse means very dark
23}
24
25void setup() {
26  pinMode(S0, OUTPUT);
27  pinMode(S1, OUTPUT);
28  pinMode(S2, OUTPUT);
29  pinMode(S3, OUTPUT);
30  pinMode(OUT_PIN, INPUT);
31  digitalWrite(S0, HIGH);                             // S0=HIGH, S1=LOW: %20 ölçek / 20% scaling
32  digitalWrite(S1, LOW);
33  Serial.begin(9600);
34}
35
36void loop() {
37  unsigned long r = readPulse(false, false);          // kırmızı filtre / red filter
38  unsigned long g = readPulse(true, true);            // yeşil filtre / green filter
39  unsigned long b = readPulse(false, true);           // mavi filtre / blue filter
40  Serial.print("R: ");
41  Serial.print(r);
42  Serial.print("  G: ");
43  Serial.print(g);
44  Serial.print("  B: ");
45  Serial.println(b);
46  delay(500);
47}

このコードで最も大事なのは、読み取った数値が光に反比例することです。値が小さいほど、その色の光が多く届いています。赤い紙をかざすと「R: 24 G: 68 B: 52」のような出力になり、いちばん小さい数値が赤チャンネルに出ます。pulseInに渡している50,000 µsのタイムアウトは、パルスが来ないときにコードが初期設定の1秒間待ち続けるのを防ぎます。パルスがまったく捕まらなければ関数は50,000を返し、これは「とても暗い」という意味です。シリアル出力の読み取りでつまずいたら、Arduinoのシリアルモニタのガイドを見てください。

白と黒のキャリブレーション:生の値を0〜255に変換する

生の値だけでは判断を誤ります。3つのフォトダイオードのグループは感度が同じではなく、モジュールの白色LEDもすべての色を均等に出しているわけではないからです。そのため、真っ白な紙でも3つのチャンネルで異なる数値になります。キャリブレーションはこうした差をなくし、各チャンネルを0(黒)〜255(白)の範囲に収めます。カメラのホワイトバランスと同じ考え方です。キャリブレーションは、センサーを実際に使う場所で、同じ光、同じ距離で行ってください。机の上で行ったキャリブレーションは、装置のケースの中では通用しません。

  1. センサーを固定したホルダーに取り付け、測定距離(例:1.5 cm)を変わらないように決めます。
  2. 無地の白い紙を前に置き、上のコードでR・G・Bの値を何回か読み取って、その平均を白の基準値として記録します。これが最も小さい値です。
  3. 同じ位置につや消しの黒い紙かフェルトを置き、読み取った値を黒の基準値として記録します。これが最も大きい値です。
  4. この6つの数値を下のコードの先頭にあるWHITE_とBLACK_の定数に入力し、書き込みます。
  5. 白い紙で3つのチャンネルすべてが255近く、黒で0近くになるか確認します。ならない場合は、同じ光と距離で測定をやり直します。

多くの例で見られる間違いに注意してください。map()でパルス幅をそのまま0〜255に変換しても正しい結果にはなりません。光はパルス幅ではなく周波数に比例するからです。正しい方法は、まずパルス幅を周波数(1/t)に変換してからスケーリングすることです:レベル = (f − f_黒) / (f_白 − f_黒) × 255。例で見てみましょう。白で20 µs、黒で200 µsを測り、物では40 µsになったとします。map()ではこれが226になりますが、周波数を使う方法では113になります。物の周波数はちょうど白の半分で、黒のレベルを差し引くとスケールの約44%、つまり113に当たります。一方map()は色を実際よりずっと明るく見せ、暗い色合いを見分けにくくしてしまいます。

色の分類アルゴリズム:この読み取り値は何色?

キャリブレーション済みのR・G・Bの値も、まだ光の量に左右されます。同じ赤い物がセンサーに少し近づくだけで、3つの値がそろって上がります。これを補正するため、それぞれの値を合計で割ります:r = R / (R + G + B)、gとbも同様です。こうした色の比率は、距離や明るさが少し変わってもほとんど一定のままです。あとは、色ごとに一度測って保存しておいた基準の比率と比べ、最近傍を選びます。比較には比率の差の二乗を足し合わせるだけで十分で、この数値が小さいほど色が似ています。いちばん近い基準でも一定のしきい値より離れていれば、結果は「不明」です。光の総量がとても少なければ黒、3つのチャンネルすべてがとても高ければ白とみなします。この2つのケースは比率では区別できないため、最初にチェックします。

C++ (Arduino IDE)
1// TCS3200 ile kalibrasyonlu renk tanıma / Calibrated color recognition with the TCS3200
2const int S0 = 4, S1 = 5, S2 = 6, S3 = 7, OUT_PIN = 8;
3
4// Kalibrasyonda ölçtüğün süreler (µs), kendi değerlerinle değiştir / Pulse widths from your calibration (µs), use your own
5const float WHITE_R = 16, WHITE_G = 20, WHITE_B = 18;     // beyaz kağıt / white paper
6const float BLACK_R = 140, BLACK_G = 170, BLACK_B = 150;  // siyah kağıt / black paper
7
8struct ColorRef {
9  const char *name;
10  float r, g, b;                                          // renk oranları, toplamı 1 / color ratios, sum is 1
11};
12
13// Örnek referanslar, kendi ölçümlerinle değiştir / Sample references, replace with your own measurements
14ColorRef refs[] = {
15  {"Kirmizi / Red",    0.58, 0.20, 0.22},
16  {"Turuncu / Orange", 0.55, 0.30, 0.15},
17  {"Sari / Yellow",    0.42, 0.40, 0.18},
18  {"Yesil / Green",    0.24, 0.46, 0.30},
19  {"Mavi / Blue",      0.18, 0.30, 0.52},
20  {"Mor / Purple",     0.40, 0.20, 0.40}
21};
22const int REF_COUNT = sizeof(refs) / sizeof(refs[0]);
23const float MAX_DIST = 0.02;                              // bundan uzaksa bilinmiyor / farther than this is unknown
24
25unsigned long readPulse(bool s2, bool s3) {
26  digitalWrite(S2, s2 ? HIGH : LOW);
27  digitalWrite(S3, s3 ? HIGH : LOW);
28  delay(5);
29  unsigned long total = 0;
30  int count = 0;
31  for (int i = 0; i < 10; i++) {
32    unsigned long t = pulseIn(OUT_PIN, LOW, 50000UL);
33    if (t > 0) {
34      total += t;
35      count++;
36    }
37  }
38  return count > 0 ? total / count : 50000UL;
39}
40
41// Süreyi frekansa çevirip 0-255 arasına ölçekler / Converts the pulse width to frequency and scales it to 0-255
42float toLevel(unsigned long t, float tWhite, float tBlack) {
43  float f = 1.0 / t;                                      // ışık frekansla orantılı / light is proportional to frequency
44  float fWhite = 1.0 / tWhite;
45  float fBlack = 1.0 / tBlack;
46  float level = (f - fBlack) / (fWhite - fBlack) * 255.0;
47  return constrain(level, 0.0, 255.0);
48}
49
50// En yakın referans rengi bulur / Finds the nearest reference color
51const char *classify(float R, float G, float B) {
52  float sum = R + G + B;
53  if (sum < 90) return "Siyah / Black";                  // çok az ışık / very little light
54  if (R > 200 && G > 200 && B > 200) return "Beyaz / White";
55  float r = R / sum, g = G / sum, b = B / sum;           // parlaklıktan bağımsız oranlar / brightness-independent ratios
56  int best = 0;
57  float bestDist = 1e9;
58  for (int i = 0; i < REF_COUNT; i++) {
59    float d = sq(r - refs[i].r) + sq(g - refs[i].g) + sq(b - refs[i].b);  // uzaklığın karesi / squared distance
60    if (d < bestDist) {
61      bestDist = d;
62      best = i;
63    }
64  }
65  if (bestDist > MAX_DIST) return "Bilinmiyor / Unknown";
66  return refs[best].name;
67}
68
69void setup() {
70  pinMode(S0, OUTPUT);
71  pinMode(S1, OUTPUT);
72  pinMode(S2, OUTPUT);
73  pinMode(S3, OUTPUT);
74  pinMode(OUT_PIN, INPUT);
75  digitalWrite(S0, HIGH);                                 // %20 ölçek / 20% scaling
76  digitalWrite(S1, LOW);
77  Serial.begin(9600);
78}
79
80void loop() {
81  float R = toLevel(readPulse(false, false), WHITE_R, BLACK_R);
82  float G = toLevel(readPulse(true, true), WHITE_G, BLACK_G);
83  float B = toLevel(readPulse(false, true), WHITE_B, BLACK_B);
84  float sum = R + G + B;
85  Serial.print("R=");
86  Serial.print(R, 0);
87  Serial.print(" G=");
88  Serial.print(G, 0);
89  Serial.print(" B=");
90  Serial.print(B, 0);
91  if (sum > 0) {                                          // referans için oranlar / ratios for your references
92    Serial.print("  r=");
93    Serial.print(R / sum, 2);
94    Serial.print(" g=");
95    Serial.print(G / sum, 2);
96    Serial.print(" b=");
97    Serial.print(B / sum, 2);
98  }
99  Serial.print("  -> ");
100  Serial.println(classify(R, G, B));
101  delay(500);
102}
コードの部分役割
WHITE_*とBLACK_*キャリブレーションで測った白と黒のパルス幅(µs)
toLevel()(レベル変換)パルス幅を周波数に変換し、0〜255にスケーリングする
refs[](基準値)色ごとの基準のr、g、bの比率。自分で測った値に置き換える
classify()(分類)まず黒と白を分け、次にいちばん近い基準を探す
MAX_DISTこの距離より離れた読み取り値は「Unknown」(不明)になる
基準値は推測せず、測ってください。各色の物をセンサーの前に5〜10回置き、シリアルモニタに表示されるr、g、bの比率を平均して、refs[]配列に書き込みます。測定は、装置を動かすときと同じ光と距離で行います。読み取り値がふらつく場合は、複数回の読み取りを平均するか、メディアンフィルタをかけると改善します。詳しくはセンサーデータのフィルタリングの記事を参照してください。

物を色ごとに仕分ける装置を作る

TCS3200で最も人気のあるプロジェクトは、色付きのお菓子、ビーズ、LEGOのピースを色ごとに仕分ける装置です。仕組みは2つのサーボでできています。上の送りサーボは、物がちょうど1個入る穴の開いた円盤を回します。まずホッパーのチューブの下から物を1個受け取り、それをセンサーの前に運び、読み取りが終わったら下のシュートに落とします。下のシュートサーボは斜めのシュートを回して、物をその色の容器に送ります。サーボを初めて使うなら、まずサーボモーターとは?を読んでください。

  1. 物が1個ずつ送り円盤の上に落ちるように、ホッパーと、物より少し太いチューブを取り付けます。
  2. センサーが円盤の上の物を1〜2 cmの距離から見られるように取り付け、外の光が入らないように囲います。
  3. 送りサーボの角度を3つ決めます。受け取り、センサー、落下です。自分の機構で試しながら見つけてください。送りサーボはD9に、シュートサーボはD10につなぎます。
  4. シュートサーボに、色の容器ごとの角度を割り当てます(例:赤30°、緑75°、青120°、それ以外165°)。
  5. キャリブレーションは装置の中で、装置自身の照明を使って行い、各色の物をいくつか使ってテストします。
C++ (Arduino IDE)
1// TCS3200 ve iki servoyla renk ayırıcı / Color sorter with a TCS3200 and two servos
2#include <Servo.h>
3
4const int S0 = 4, S1 = 5, S2 = 6, S3 = 7, OUT_PIN = 8;
5const int FEEDER_PIN = 9;                 // besleme servosu / feeder servo
6const int CHUTE_PIN = 10;                 // yönlendirme servosu / chute servo
7
8const int LOAD_ANGLE = 160;               // huninin altı / under the hopper
9const int SENSOR_ANGLE = 95;              // sensörün önü / in front of the sensor
10const int DROP_ANGLE = 30;                // bırakma deliği / drop hole
11
12// Kalibrasyon süreleri (µs) / Calibration pulse widths (µs)
13const float WHITE_R = 16, WHITE_G = 20, WHITE_B = 18;
14const float BLACK_R = 140, BLACK_G = 170, BLACK_B = 150;
15
16Servo feeder;
17Servo chute;
18
19unsigned long readPulse(bool s2, bool s3) {
20  digitalWrite(S2, s2 ? HIGH : LOW);
21  digitalWrite(S3, s3 ? HIGH : LOW);
22  delay(5);
23  unsigned long total = 0;
24  int count = 0;
25  for (int i = 0; i < 10; i++) {
26    unsigned long t = pulseIn(OUT_PIN, LOW, 50000UL);
27    if (t > 0) {
28      total += t;
29      count++;
30    }
31  }
32  return count > 0 ? total / count : 50000UL;
33}
34
35float toLevel(unsigned long t, float tWhite, float tBlack) {
36  float f = 1.0 / t;
37  float level = (f - 1.0 / tBlack) / (1.0 / tWhite - 1.0 / tBlack) * 255.0;
38  return constrain(level, 0.0, 255.0);
39}
40
41// Baskın kanala göre kutu açısını seçer / Picks the bin angle from the dominant channel
42int binAngle(float R, float G, float B) {
43  if (R + G + B < 90) return 165;                     // çok koyu: diğer kutusu / too dark: "other" bin
44  if (R > G * 1.3 && R > B * 1.3) return 30;          // kırmızı kutusu / red bin
45  if (G > R * 1.3 && G > B * 1.3) return 75;          // yeşil kutusu / green bin
46  if (B > R * 1.3 && B > G * 1.3) return 120;         // mavi kutusu / blue bin
47  return 165;                                         // karar verilemedi / undecided
48}
49
50void setup() {
51  pinMode(S0, OUTPUT);
52  pinMode(S1, OUTPUT);
53  pinMode(S2, OUTPUT);
54  pinMode(S3, OUTPUT);
55  pinMode(OUT_PIN, INPUT);
56  digitalWrite(S0, HIGH);                             // %20 ölçek / 20% scaling
57  digitalWrite(S1, LOW);
58  feeder.attach(FEEDER_PIN);
59  chute.attach(CHUTE_PIN);
60  feeder.write(LOAD_ANGLE);
61  delay(500);
62}
63
64void loop() {
65  feeder.write(LOAD_ANGLE);                           // huniden bir nesne al / take one object from the hopper
66  delay(600);
67  feeder.write(SENSOR_ANGLE);                         // sensörün önüne getir / bring it to the sensor
68  delay(600);                                         // servo dursun, titreşim geçsin / let the servo settle
69  float R = toLevel(readPulse(false, false), WHITE_R, BLACK_R);
70  float G = toLevel(readPulse(true, true), WHITE_G, BLACK_G);
71  float B = toLevel(readPulse(false, true), WHITE_B, BLACK_B);
72  chute.write(binAngle(R, G, B));                     // kanalı doğru kutuya çevir / aim the chute at the right bin
73  delay(400);
74  feeder.write(DROP_ANGLE);                           // nesneyi kanala bırak / drop the object into the chute
75  delay(600);
76}
サーボをArduinoの5Vピンから給電すると、動いたときに電圧が下がってボードがリセットすることがあります。その電圧降下は色の読み取りも狂わせます。2つのサーボはどちらも別の5 V電源から給電し、そのGNDをArduinoのGNDにつないでください。サーボが動いている間は読み取らず、コードのようにサーボが位置に着いた後に短い待ち時間を入れます。

ESP32でTCS3200:割り込みで周波数を測る

同じpulseInのコードはESP32でも動きますが、もっと安定した方法があります。パルスを1つずつ計るのではなく、一定時間に届くパルスの数を数えるのです。割り込みで立ち上がりエッジのたびにカウンターを1つ増やし、50 ms後のカウントに20を掛ければ、ヘルツ単位の周波数になります。この方法は数百個のパルスを平均するので、1つの不良なパルスに影響されません。ESP32の240 MHzのプロセッサーなら、20%のスケーリングで1秒間に数万回の割り込みも楽にこなせます。とても明るい光でカウントが大きくなりすぎる場合は、スケーリングを2%に下げられます。OUTを割り込みに対応したD2に移せば、Arduino Unoでも同じ方法が使えます。ESP32では、モジュールを3V3ピンから給電するのを忘れないでください。

C++ (Arduino IDE)
1// ESP32: kesmeyle TCS3200 frekans ölçümü / ESP32: TCS3200 frequency measurement with an interrupt
2const int S0 = 32, S1 = 33, S2 = 25, S3 = 26, OUT_PIN = 27;
3
4volatile uint32_t pulseCount = 0;
5
6void IRAM_ATTR onPulse() {
7  pulseCount++;                                       // her yükselen kenarda say / count every rising edge
8}
9
10// Seçilen filtrenin frekansını Hz olarak döndürür / Returns the frequency of the selected filter in Hz
11uint32_t readHz(bool s2, bool s3) {
12  digitalWrite(S2, s2 ? HIGH : LOW);
13  digitalWrite(S3, s3 ? HIGH : LOW);
14  delay(5);                                           // çıkış otursun / let the output settle
15  pulseCount = 0;
16  delay(50);                                          // 50 ms boyunca say / count for 50 ms
17  return pulseCount * 20;                             // 50 ms x 20 = 1 saniye / 1 second
18}
19
20void setup() {
21  Serial.begin(115200);
22  pinMode(S0, OUTPUT);
23  pinMode(S1, OUTPUT);
24  pinMode(S2, OUTPUT);
25  pinMode(S3, OUTPUT);
26  pinMode(OUT_PIN, INPUT);
27  digitalWrite(S0, HIGH);                             // %20 ölçek / 20% scaling
28  digitalWrite(S1, LOW);
29  attachInterrupt(digitalPinToInterrupt(OUT_PIN), onPulse, RISING);
30}
31
32void loop() {
33  uint32_t r = readHz(false, false);                  // kırmızı / red
34  uint32_t g = readHz(true, true);                    // yeşil / green
35  uint32_t b = readHz(false, true);                   // mavi / blue
36  Serial.print("R: ");
37  Serial.print(r);
38  Serial.print(" Hz  G: ");
39  Serial.print(g);
40  Serial.print(" Hz  B: ");
41  Serial.print(b);
42  Serial.println(" Hz");
43  delay(300);
44}

Raspberry Pi PicoとMicroPythonでTCS3200

PicoでArduinoのpulseInに当たるのはmachine.time_pulse_us()関数です。指定したレベルのパルスの長さをマイクロ秒で返し、タイムアウトしたときは負の数を返します。ピンがすでにそのレベルにあるときに呼び出すと、すぐに計測を始めてしまい、パルスの半分だけを測ることがあります。そのため、コードではまずHIGHの部分が終わるのを待ってから、LOWの時間を測っています。MicroPythonでは関数呼び出しのたびに数マイクロ秒かかるので、ここでは2%のスケーリング(S0=0、S1=1)を選びました。パルスが10倍長くなり、その遅れの影響が小さくなります。キャリブレーション値はHz単位で書いています。2%のスケーリングでは、白い紙で数kHz、黒で数百Hzになるのが普通です。モジュールはPicoの3V3(OUT)ピンから給電してください。Picoを初めて使うなら、Raspberry Pi Picoとは?でMicroPythonのインストール方法を説明しています。

MicroPython
1# Pico + TCS3200: süreden frekansa ve 0-255 seviyesine / Pico + TCS3200: from pulse width to frequency and a 0-255 level
2from machine import Pin, time_pulse_us
3import time
4
5s0 = Pin(2, Pin.OUT)
6s1 = Pin(3, Pin.OUT)
7s2 = Pin(4, Pin.OUT)
8s3 = Pin(5, Pin.OUT)
9out = Pin(6, Pin.IN)
10
11s0.value(0)                                    # S0=0, S1=1: %2 ölçek / 2% scaling
12s1.value(1)
13
14FILTERS = {"R": (0, 0), "G": (1, 1), "B": (0, 1)}   # S2, S3 değerleri / S2, S3 values
15WHITE_HZ = {"R": 3125, "G": 2500, "B": 2778}        # beyaz kağıtta Hz / Hz on white paper
16BLACK_HZ = {"R": 357, "G": 294, "B": 333}           # siyah kağıtta Hz / Hz on black paper
17
18def read_hz(channel, samples=10):
19    s2.value(FILTERS[channel][0])
20    s3.value(FILTERS[channel][1])
21    time.sleep_ms(5)                           # çıkış otursun / let the output settle
22    total = 0
23    count = 0
24    for _ in range(samples):
25        time_pulse_us(out, 1, 50000)           # HIGH bitsin, LOW başında ol / wait for HIGH to end
26        t = time_pulse_us(out, 0, 50000)       # LOW süresi, µs / LOW time, µs
27        if t > 0:                              # negatif = zaman aşımı / negative = timeout
28            total += t
29            count += 1
30    if count == 0:
31        return 0
32    return 1000000 / (2 * total / count)       # f = 1 / (2t)
33
34def level(channel, hz):
35    v = (hz - BLACK_HZ[channel]) / (WHITE_HZ[channel] - BLACK_HZ[channel]) * 255
36    return max(0, min(255, int(v)))
37
38while True:
39    r = level("R", read_hz("R"))
40    g = level("G", read_hz("G"))
41    b = level("B", read_hz("B"))
42    print("R:", r, "G:", g, "B:", b)
43    time.sleep(0.5)

TCS3200とTCS34725、どちらを選ぶ?

TCS34725は、同じ仕事をI2Cで行う新しいカラーセンサーです。測定をチップの内部で行い、結果をチャンネルごとに16ビットの数値で返すので、Arduinoがパルスを計る必要がありません。最も大きな違いは、フォトダイオードの上にある赤外線(IR)カットフィルターです。TCS3200にはこのフィルターがありません。シリコンのフォトダイオードは赤外線にも反応するので、太陽光や白熱電球のような赤外線の多い光では読み取り値がずれます。TCS34725はゲイン(1x、4x、16x、60x)と積分時間(2.4〜614 ms)をソフトウェアで設定できるため、とても暗い光から明るい光まで広い範囲で使えます。I2Cの仕組みが気になる場合は、I2Cとは?の記事を見てください。

TCS34725 I2Cカラーセンサーモジュール
TCS34725カラーセンサーモジュール:中央にIRフィルター付きのカラーセンサーチップ、その横に白色LEDがあり、LED、INT、SDA、SCL、3V3、GND、VINのピンが並んでいます。
項目TCS3200TCS34725
出力周波数(矩形波)I2C、アドレス0x29
信号線S0〜S3、OUT、OESDAとSCL(INTとLEDは任意)
分解能測定方法と時間によるチャンネルごとに16ビット
IRフィルターなしあり(チップ上)
設定3段階の周波数スケーリングとパワーダウン4段階のゲイン、2.4〜614 msの積分時間
電源2.7〜5.5 Vチップは3.3 V、レギュレーター付きのモジュールは3〜5 V
照明たいてい白色LED 4個たいてい白色LED 1個
プロセッサーの負荷パルスを計る間は手がふさがるチップが測定し、Arduinoは読むだけ
向いている用途学習、シンプルな色の仕分け機正確な色、色温度、照度の測定
C++ (Arduino IDE)
1// TCS34725 ile ham RGBC, renk sıcaklığı ve lüks / Raw RGBC, color temperature and lux with the TCS34725
2#include <Wire.h>
3#include <Adafruit_TCS34725.h>
4
5// 50 ms entegrasyon süresi, 4x kazanç / 50 ms integration time, 4x gain
6Adafruit_TCS34725 tcs = Adafruit_TCS34725(TCS34725_INTEGRATIONTIME_50MS, TCS34725_GAIN_4X);
7
8void setup() {
9  Serial.begin(9600);
10  if (!tcs.begin()) {                                 // 0x29 adresinde ara / look for it at 0x29
11    Serial.println("TCS34725 bulunamadi / not found");
12    while (true) {
13      delay(1000);
14    }
15  }
16}
17
18void loop() {
19  uint16_t r, g, b, c;
20  tcs.getRawData(&r, &g, &b, &c);                     // 16 bit ham değerler / 16-bit raw values
21  uint16_t kelvin = tcs.calculateColorTemperature_dn40(r, g, b, c);
22  uint16_t lux = tcs.calculateLux(r, g, b);
23  Serial.print("R: ");
24  Serial.print(r);
25  Serial.print("  G: ");
26  Serial.print(g);
27  Serial.print("  B: ");
28  Serial.print(b);
29  Serial.print("  C: ");
30  Serial.print(c);
31  Serial.print("  K: ");
32  Serial.print(kelvin);
33  Serial.print("  Lux: ");
34  Serial.println(lux);
35  delay(500);
36}

コードを書き込む前に、ライブラリマネージャからAdafruit TCS34725ライブラリをインストールしてください。依存ライブラリについて聞かれたら、「全てをインストール」を選びます。Arduino UnoではSDAをA4、SCLをA5に、ESP32ではGPIO 21と22を使います。getRawData()は積分時間だけ待ってから、赤・緑・青とクリア(C)の生の値を返します。calculateColorTemperature_dn40()はケルビン単位の色温度を、calculateLux()はおおよその明るさを返します。キャリブレーションと最も近い色を選ぶロジックは、このセンサーにもそのまま使えます。色だけでなく手のジェスチャーや近接も検出できるセンサーが欲しいなら、APDS-9960が良い選択肢です。

TCS3200を使ったプロジェクトのアイデア

  • 色の仕分け機:お菓子、ビーズ、LEGOのピースを色ごとに別々の容器に仕分けます。
  • 色をまねるランプ:センサーの前にかざした物の色を、RGB LEDやWS2812Bテープで再現します。
  • 色を読むロボット:ライントレースロボットに、床の赤い目印で止まり、緑の目印で進むように教えます。
  • 色を認識するロボットアーム:ロボットアームでキューブをつかみ、色ごとにそれぞれの容器に落とします。
  • しゃべる色検出器:検出した色を、DFPlayer Miniにあらかじめ録音した音声で知らせます。目の不自由な人の役に立つ補助具になります。
  • 水の色モニター:透明な容器に入れた水の色の変化を時間とともに記録し、水槽や水やり用タンクのために警告を出します。

よくある間違いと対策

症状考えられる原因対策
pulseInが常に0かタイムアウト値を返すOEがフローティング状態かHIGH、S0とS1が両方LOW(パワーダウン)、またはOUTが違うピンにつながっているOEをGNDにつなぎ、S0をHIGH、S1をLOWにして、OUTのピンをコードと照らし合わせる
値が飛び続ける部屋の光、ちらつく照明、変わる距離センサーを黒いチューブで覆い、距離を固定し、複数回の読み取りを平均する
どの物も同じ色と判定されるキャリブレーションしていない。チャンネルごとに感度が違う白と黒でキャリブレーションし、基準値を測り直す
日光の下で色がずれるTCS3200にはIRフィルターがない閉じた箱の中で測るか、TCS34725を使う
レベルが0か255に張り付く物がキャリブレーション時と違う距離にあるキャリブレーション時と同じ距離を保ち、必要ならキャリブレーションし直す
ESP32やPicoの動作がおかしいモジュールを5 Vで給電していて、OUTが5 Vを出しているモジュールを3V3から給電するか、レベル変換モジュールを使う
サーボが動くと読み取り値が狂うサーボの電流で電源電圧が下がるサーボには別の5 V電源を使い、GNDをつなぐ
光沢があって反射しやすい面(金属、つやのあるプラスチック、光沢紙)は、LEDの光をそのままセンサーに跳ね返し、色を実際より薄く見せます。センサーを真正面ではなく少し傾けて構えるか、つや消しの物を使えば、この問題は大きく減ります。黒と白を見分けるだけなら、TCRT5000のほうがシンプルで速い選択肢です。

TCS3200は、光が周波数に変わる仕組み、キャリブレーションが大切な理由、シンプルな分類アルゴリズムの作り方を学ぶのにぴったりのセンサーです。もっと精度が必要になったときにTCS34725へ移るのも簡単です。学んだキャリブレーションと最も近い色を選ぶロジックが、そのまま使えるからです。BlueGraysアプリには、RGB LEDで滑らかに色を変える、ESP32の調色ライト、LDRで光の強さを測定、サーボモーターの使い方といったプロジェクトが回路図とステップごとのコード付きでそろっています。カラーセンサーと組み合わせれば、自分だけの装置を作れます。

よくある質問

TCS3200カラーセンサーはどのくらいの距離まで色を検出できますか?

最も安定した結果が出るのは約1〜3 cmです。モジュールのLEDの光は、距離が離れるとすぐに弱くなるからです。距離そのものより大事なのは、毎回の読み取りで同じ距離を保つことで、キャリブレーションもその距離で行ってください。このセンサーは、離れた場所にある大きな面の色を読むのには向いていません。

TCS230とTCS3200の違いは何ですか?

TCS230は同じメーカーの旧型のカラーセンサーで、TCS3200はその改良版です。どちらも8×8のフォトダイオードアレイ、周波数出力、同じS0〜S3の制御ロジックを使うので、配線もコードも共通です。現在売られているモジュールのほとんどはTCS3200を搭載しています。

TCS3200でpulseInが0を返すのはなぜですか?

pulseInは、指定したタイムアウト時間内にパルスを検出できないと0を返します。最も多い原因は、OEピンがGNDにつながっていない、S0とS1が両方LOWのまま(センサーがパワーダウンモードになる)、OUTがコードとは別のピンにつながっている、の3つです。センサーの前が真っ暗だと周波数が数ヘルツまで下がり、短いタイムアウトではパルスを捕まえられません。

TCS3200はESP32やRaspberry Pi Picoで使えますか?

はい。TCS3200は2.7〜5.5 Vで動作するので、モジュールをボードの3V3ピンから給電でき、そうすればOUTピンも3.3 Vのパルスを出します。モジュールを5 Vで給電する場合は、OUTラインに分圧回路かレベル変換モジュールが必要です。PicoでMicroPythonを使って測る場合は、2%の周波数スケーリングのほうが正確な結果になります。

TCS3200とTCS34725はどちらが優れていますか?

正確な色の測定ならTCS34725が優れています。赤外線カットフィルター、チャンネルごとに16ビットの分解能、調整可能なゲインを備え、I2Cの2本の線だけでつながります。TCS3200は、周波数の測定やキャリブレーションを学んだり、シンプルな色の仕分け機を作ったりするための、よく使われる十分な選択肢です。

TCS3200はいくつの色を見分けられますか?

照明を管理し、距離を固定すれば、TCS3200は赤、オレンジ、黄、緑、青、紫、黒、白のようにはっきり異なる6〜8色を確実に見分けられます。明るい緑と暗い緑のような近い色合いを見分けることもできますが、より丁寧なキャリブレーション、色合いごとの多くの基準値、厳しいしきい値が必要になります。

全プロジェクトを見る
LDRで光の強さを測定
#11初級

LDRで光の強さを測定

部屋の明るさを数値で見てみませんか?光に反応する小さな部品、LDRで周囲の光を測定し、Arduinoが0.5秒ごとにその値をコンピューターの画面に表示します。手で覆うと数値が変わる様子を確認できます。自動で点灯する常夜灯などのプロジェクトへの第一歩です。

4.6(81)
138
ESP32とLDRモジュールで光の強さを読み取る
#113初級

ESP32とLDRモジュールで光の強さを読み取る

LDRモジュールの生のADC値と、暗い状態・明るい状態の基準値から計算した相対的な明るさの割合を、シリアルモニタでリアルタイムに確認します。この割合はルクスの測定値ではありません。モジュールの校正の仕組みを学べます。しきい値の決定に使うため、プロジェクト10の前に取り組むのが適しています。

4.6(36)
55
RGB LEDの使い方
#39初級
プレミアム

RGB LEDの使い方

1つのLEDに3つの色が入っています。RGB LEDが赤、緑、青の順に、それぞれ0.5秒点灯して0.5秒消灯し、このサイクルを繰り返します。ArduinoにRGB LEDを1つと抵抗を3つ接続するだけです。色とりどりの光を使うプロジェクトを、手軽に始められます。

4.9(73)
98
RGB LEDで滑らかに色を変える
#40初級
プレミアム

RGB LEDで滑らかに色を変える

1つのRGB LEDが赤から緑、緑から青、そして再び赤へとゆっくり変わり、虹色のサイクルを繰り返します。1回の色の変化には約2.5秒かかり、色が混ざることで、その間に黄色、ターコイズ、紫が自然に現れます。光が滑らかに暗くなったり明るくなったりする様子を、自分の目で確かめられます。

4.8(58)
76
ESP32の調色ライト(可変抵抗3個 + RGB LED + OLED)
#138上級
プレミアム

ESP32の調色ライト(可変抵抗3個 + RGB LED + OLED)

ESP32と3個の可変抵抗で、好みの色を作れるスマート調色ライトです。赤・緑・青の可変抵抗を回すと、カソードコモンのRGB LEDの色がリアルタイムに変わります。SSD1306 OLEDディスプレイには、各色の0~255の値、色の強さを示すバー、#FF8040などのWeb用HEXカラーコードをリアルタイムで表示します。小さなプロ仕様のカラースタジオとして、RGBの色の仕組み、ADCによるアナログ値の読み取り、PWMによる明るさの制御、I2Cディスプレイの表示設計を1つのプロジェクトで学べます。

4.7(6)
10
ESP32でRGB LEDを制御
#111初級

ESP32でRGB LEDを制御

3個の個別のLEDの代わりに、1個のRGB LEDで無数の色を作ります。Arduinoと同じようにanalogWrite()でR/G/Bの値を調整し、虹色に変化させます。赤と緑を混ぜると黄色になるなど、色の仕組みを実際に確認できます。

4.6(29)
51
サーボモーターの使い方
#35初級
プレミアム

サーボモーターの使い方

小型サーボモーターのアームが、一方の端、中央、もう一方の端の3つの位置へ順番に回り、それぞれの位置で2秒間止まります。Arduinoが保持する角度をモーターに指示すると、モーターはその位置まで動いて止まります。ロボットアームや自動開閉するふたが動く仕組みを学べます。

4.7(48)
67
TCRT5000で床面の白黒を判別
#78中級
プレミアム

TCRT5000で床面の白黒を判別

ライントレースロボットの目となる小さなセンサーで、床面が黒か白かを判別できます。TCRT5000センサーは目に見えない光を床面に当て、戻ってくる光の量を調べます。白い面では緑のLED、黒い面では赤のLEDが点灯します。自分でロボットを作る前に学んでおきたい大切なステップです。

4.7(40)
49