项目概述
本项目设计并实现了一款的低功耗,简易,可手持操作的颜色识别设备,核心目标是解决传统 TCS3200 颜色传感器在实际使用中因环境光干扰、白点漂移导致的色彩失真问题。设备集成 OLED 显示屏、蜂鸣器音阶反馈、休眠、LED 补光控制等功能,适用多种场景。
硬件
主控板:AdafruitESP32-S3ReverseTFTFeather
传感器:TCS3200 模块
外设:蜂鸣器、接近传感器(磁簧开关)
电源:3.7V 锂电池或 USB 供电
系统连线图

对应的引脚连线
| 硬件 | ESP32 | 功能 | |
| TCS3200 | S0 | D11 | 输出使能控制 |
| S1 | D10 | 频率缩放控制 | |
| S2 | D9 | 颜色通道选择 | |
| S3 | D6 | 颜色通道选择 | |
| OUT | D5 | 频率输出,接输入 | |
LED | D12 | 控制 TCS3200 板载白光 LED | |
| VCC | 3.3V | 电源 | |
| GND | GND | 接地 | |
| GND | GND(蜂鸣器的GND) | 接地 | |
| 接近传感器 | 引脚 1 | GND | 接地 |
| 引脚 2 | D14 | GPIO输入,内部上拉 | |
| 蜂鸣器 | 正极 | 3.3V | 电源 |
| 负极 | GND(TCS3200的GND) | 接地 | |
| PWM | D13 | PWM输出 |
操作
单击 D0:测量当前物体颜色,屏幕显示 RGB,背光同步变色
双击 D0:开启/关闭 TCS3200 补光 LED(暗处使用)
按 D2:播放 C4→C5 音阶(测试蜂鸣器)
接近传感器:靠近磁铁,休眠
代码部分
import time
import digitalio
import board
import displayio
import terminalio
from adafruit_display_text import label
import neopixel
import pwmio
# ==============================
# 初始化硬件
# ==============================
pixel = neopixel.NeoPixel(board.NEOPIXEL, 1)
pixel.brightness = 0.3
pwm = pwmio.PWMOut(board.D13, duty_cycle=0, frequency=440, variable_frequency=True)
note_names = ['C4', 'D4', 'E4', 'F4', 'G4', 'A4', 'B4', 'C5']
frequencies = [261, 293, 329, 349, 392, 440, 493, 523]
DEBOUNCE_TIME = 0.2
NOTE_DURATION = 0.5
# ==============================
# TCS3200 类
# ==============================
class TCS3200:
def __init__(self, s0, s1, s2, s3, out_pin):
self.s0 = s0; self.s1 = s1; self.s2 = s2; self.s3 = s3; self.out = out_pin
for pin in (s0, s1, s2, s3):
pin.direction = digitalio.Direction.OUTPUT
out_pin.direction = digitalio.Direction.INPUT
def set_frequency(self, mode):
self.s0.value = (mode >> 1) & 1
self.s1.value = mode & 1
def set_color_channel(self, color):
if color == 'R':
self.s2.value, self.s3.value = False, False
elif color == 'G':
self.s2.value, self.s3.value = True, True
elif color == 'B':
self.s2.value, self.s3.value = False, True
def measure_frequency(self, duration=0.08):
start = time.monotonic()
count = 0
last = self.out.value
while (time.monotonic() - start) < duration:
cur = self.out.value
if cur != last:
count += 1
last = cur
return (count / 2) / duration if duration > 0 else 0
tcs = TCS3200(
s0=digitalio.DigitalInOut(board.D11),
s1=digitalio.DigitalInOut(board.D10),
s2=digitalio.DigitalInOut(board.D9),
s3=digitalio.DigitalInOut(board.D6),
out_pin=digitalio.DigitalInOut(board.D5)
)
tcs.set_frequency(1)
# TCS3200 板载 LED (D12) —— 上电默认关闭
led = digitalio.DigitalInOut(board.D12)
led.direction = digitalio.Direction.OUTPUT
led.value = False # <<< 上电即关闭
# ==============================
# 显示初始化
# ==============================
display = board.DISPLAY
main_group = displayio.Group()
display.root_group = main_group
bg_bitmap = displayio.Bitmap(display.width, display.height, 1)
bg_palette = displayio.Palette(1)
bg_palette[0] = 0x000000
bg_sprite = displayio.TileGrid(bg_bitmap, pixel_shader=bg_palette)
main_group.append(bg_sprite)
text_label = label.Label(
terminalio.FONT,
text="Ready",
color=0xFFFFFF,
scale=2
)
text_label.anchor_point = (0.5, 0.5)
text_label.anchored_position = (display.width // 2, display.height // 2)
main_group.append(text_label)
reed_switch = digitalio.DigitalInOut(board.D14)
reed_switch.switch_to_input(pull=digitalio.Pull.UP)
d0_button = digitalio.DigitalInOut(board.D0)
d0_button.switch_to_input(pull=digitalio.Pull.UP)
d2_button = digitalio.DigitalInOut(board.D2)
d2_button.switch_to_input(pull=digitalio.Pull.DOWN)
# ==============================
# 白卡参考值
# ==============================
white_r = 700.0
white_g = 600.0
white_b = 580.0
def measure_avg(channel, n=3):
vals = []
for _ in range(n):
tcs.set_color_channel(channel)
f = tcs.measure_frequency(duration=0.08)
if f > 5:
vals.append(f)
time.sleep(0.02)
return sum(vals) / len(vals) if vals else 0
# ==============================
# 颜色转换
# ==============================
def freq_to_rgb(fr, fg, fb):
r_wb = fr / white_r if white_r > 10 else 0
g_wb = fg / white_g if white_g > 10 else 0
b_wb = fb / white_b if white_b > 10 else 0
max_val = max(r_wb, g_wb, b_wb, 0.001)
r = min(255, int(255 * r_wb / max_val))
g = min(255, int(255 * g_wb / max_val))
b = min(255, int(255 * b_wb / max_val))
return r, g, b
# ==============================
# 白卡校准
# ==============================
def calibrate_white():
global white_r, white_g, white_b
print("Place WHITE card over sensor. Calibrating in 2s...")
text_label.text = "Put WHITE card\nCalibrating..."
pixel.fill((255, 255, 255))
time.sleep(2)
fr = measure_avg('R')
fg = measure_avg('G')
fb = measure_avg('B')
white_r = fr if fr > 10 else white_r
white_g = fg if fg > 10 else white_g
white_b = fb if fb > 10 else white_b
print(f" White calibrated:\n R={white_r:.1f}, G={white_g:.1f}, B={white_b:.1f}")
text_label.text = f"White OK!\nR{int(white_r)} G{int(white_g)} B{int(white_b)}"
pixel.fill((255, 255, 0))
time.sleep(2)
# ==============================
# 音符控制
# ==============================
def play_note(note_index):
if 0 <= note_index < len(note_names):
freq = frequencies[note_index]
pwm.frequency = int(freq)
pwm.duty_cycle = 2 ** 15
def stop_playing():
pwm.duty_cycle = 0
# ==============================
# 电源管理
# ==============================
def enter_sleep_mode():
global is_awake
if is_awake:
display.brightness = 0.0
bg_palette[0] = 0x000000
pixel.fill((0, 0, 0))
text_label.text = ""
is_awake = False
# 注意:不操作 led.value,保持用户设置
def enter_wake_mode():
global is_awake
if not is_awake:
display.brightness = 1.0
bg_palette[0] = 0x000000
pixel.fill((0, 0, 0))
text_label.text = "D0: Scan\nLong(3s): Calib\nDouble: LED"
is_awake = True
# ==============================
# 主循环
# ==============================
is_awake = True
note_index = 0
last_d2_state = False
note_start_time = time.monotonic()
last_d0_press = 0
pending_d0_click = False
d0_press_start = None
D0_DOUBLE_DELAY = 0.3
D0_LONG_PRESS_TIME = 3.0
print("System started.")
print(f"Initial white ref: R={white_r}, G={white_g}, B={white_b}")
# 初始进入唤醒状态(但 LED 保持关闭)
enter_wake_mode()
while True:
reed_open = reed_switch.value
if reed_open:
enter_wake_mode()
else:
enter_sleep_mode()
if is_awake:
d0_current = d0_button.value
if not d0_current: # 按下 D0
d0_press_start = time.monotonic()
while not d0_button.value:
if time.monotonic() - d0_press_start > D0_LONG_PRESS_TIME:
break
press_duration = time.monotonic() - d0_press_start
d0_press_start = None
if press_duration > D0_LONG_PRESS_TIME:
calibrate_white()
text_label.text = "D0: Scan\nLong(3s): Calib\nDouble: LED"
else:
now = time.monotonic()
if pending_d0_click:
pending_d0_click = False
led.value = not led.value # 切换 TCS3200 LED
status = "ON" if led.value else "OFF"
text_label.text = f"TCS LED {status}"
# No NeoPixel feedback for LED toggle
time.sleep(0.3)
else:
last_d0_press = now
pending_d0_click = True
if pending_d0_click and (time.monotonic() - last_d0_press > D0_DOUBLE_DELAY):
pending_d0_click = False
text_label.text = "Scanning..."
fr = measure_avg('R')
fg = measure_avg('G')
fb = measure_avg('B')
if fr < 10 and fg < 10 and fb < 10:
r, g, b = 0, 0, 0
text_label.text = "Too dark!"
bg_palette[0] = 0x000000
pixel.fill((0, 0, 0))
print("Color: Too dark! (R=0, G=0, B=0)")
else:
r, g, b = freq_to_rgb(fr, fg, fb)
text_label.text = f"R:{r}\nG:{g}\nB:{b}"
bg_palette[0] = (r << 16) | (g << 8) | b
pixel.fill((r, g, b))
print(f"Color: R={r}, G={g}, B={b}")
# --- D2 音阶测试 ---
d2_current = d2_button.value
if d2_current and not last_d2_state:
stop_playing()
play_note(note_index)
note_index = (note_index + 1) % len(note_names)
note_start_time = time.monotonic()
time.sleep(DEBOUNCE_TIME)
if time.monotonic() - note_start_time > NOTE_DURATION:
stop_playing()
note_start_time = time.monotonic()
last_d2_state = d2_current
else:
time.sleep(0.1)
time.sleep(0.01)
我要赚赏金
