一、项目简介
基于 ESP32-S3 系统通过 TCS3200 颜色传感器 读取物体颜色,并实现以下互动:
板载 RGB LED(NeoPixel):同步显示识别到的颜色;
蜂鸣器模块:发出该颜色对应的音符。

二、硬件清单
三、核心功能
颜色识别(TCS3200)
RGB视觉反馈(NeoPixel)
声音输出(蜂鸣器与音阶映射)
主要逻辑流程:
按D0键
启动检测;
TCS3200 取 R,G、B 三通道频率;
判断主色,点亮对应颜色 LED;
根据蜂鸣器不同颜色发声。
import board
import digitalio
import displayio
import terminalio
import neopixel
import time
import pwmio
from adafruit_display_text import label
pixel = neopixel.NeoPixel(board.NEOPIXEL, 1)
pixel.brightness = 0.3
pwm = pwmio.PWMOut(board.D13, duty_cycle=0, frequency=440, variable_frequency=True)
DEBOUNCE_TIME = 0.2
NOTE_DURATION = 0.5
# 简谱音名与频率映射
note_freq = {
" ": 1,
"do": 131, # 低音do
"re": 147, # 低音re
"mi": 165, # 低音mi
"fa": 175, # 低音mi
"so": 180, # 低音mi
"la": 220, # 低音mi
"1": 262, # do
"2": 294, # re
"3": 330, # mi
"4": 349, # fa
"5": 360, # so
"6": 440, # la
"7": 494, # xi
"1'": 523, # 高音do
"2'": 587, # 高音re
"3'": 659, # 高音mi
"4'": 698, # 高音fa
"5'": 784, # 高音so
}
class TCS3200(object):
"""
This class reads RGB values from a TCS3200 colour sensor.
GND Ground.
VDD Supply Voltage (2.7-5.5V)
LED 1: LEDs on, 0: LEDs off
/OE Output enable, active low. When OE is high OUT is disabled
allowing multiple sensors to share the same OUT line.
OUT Output frequency square wave.
S0/S1 Output frequency scale selection.
S2/S3 Colour filter selection.
OUT is a square wave whose frequency is proprtional to the
intensity of the selected filter colour.
S2/S3 selects between red, green, blue, and no filter.
S0/S1 scales the frequency at 100%, 20%, 2% or off.
To take a reading the colour filters are selected in turn for a
fraction of a second and the frequency is read and converted to
Hz.
Default connections:
TCS3200 WeMos GPIO
S0 D3 17
S1 D4 16
S2 D8 05
S3 D5 18
OUT D6 19
LED D7 23
OE GND
"""
# class variables
ON = True # on for debugging and the leds
OFF = False # off
RED = (0,0) # S2 and S3 low
BLUE = (0,1) # S2 low, S3 high
GREEN = (1,1) # S2 and S3 high
CLEAR = (1,0) # S2 high and S3 low
POWER_OFF = (0,0) # S0 and S1 low
TWO_PERCENT = (0,1) # S0 low, S1 high
TWENTY_PERCENT = (1,0) # S0 high, S1 low
HUNDRED_PERCENT = (1,1) # S0 and S1 high
def __init__(self, OUT=16, S2=5, S3=18, S0=None, S1=None, LED=None,OE=None,timeout=1.0):
"""
The gpios connected to the sensor OUT, S2, and S3 pins must
be specified. The S0, S1 (frequency) and LED and OE (output enable)
gpios are optional.
The OE pin is missing on some TCS3200 boards
self._OUT = Pin(OUT,Pin.IN,Pin.PULL_UP)
self._S2 = Pin(S2,Pin.OUT)
self._S3 = Pin(S3,Pin.OUT)
self._S0 = S0
self._S1 = S1
self._OE = OE
self._LED = LED
if S0 and S1 :
self._S0 = Pin(S0,Pin.OUT)
self._S1 = Pin(S1,Pin.OUT)
if LED :
self._LED = Pin(LED,Pin.OUT)
self._LED.on()
if OE :
self._OE = Pin(OE,Pin.OUT)
# define the timer for measurement timeout
self._tim = Timer(0)
"""
pinmap = {
5: board.D5,
6: board.D6,
8: board.D8,
9: board.D9,
10: board.D10,
11: board.D11,
12: board.D12,
13: board.D13,
14: board.D14,
15: board.D15,
16: board.D16,
17: board.D17,
18: board.D18
}
self._OUT = digitalio.DigitalInOut(pinmap[OUT])
self._OUT.direction = digitalio.Direction.INPUT
self._OUT.pull = digitalio.Pull.UP
self._S2 = digitalio.DigitalInOut(pinmap[S2])
self._S2.direction = digitalio.Direction.OUTPUT
self._S3 = digitalio.DigitalInOut(pinmap[S3])
self._S3.direction = digitalio.Direction.OUTPUT
self._S0 = S0
self._S1 = S1
self._OE = OE
self._LED = LED
self._timeout = timeout # 超时时长(秒)
self._tim_start = None # 记录定时器启动时间
if S0 and S1 :
self._S0 = digitalio.DigitalInOut(pinmap[S0])
self._S0.direction = digitalio.Direction.OUTPUT
self._S1 = digitalio.DigitalInOut(pinmap[S1])
self._S1.direction = digitalio.Direction.OUTPUT
if LED :
self._LED = digitalio.DigitalInOut(pinmap[LED])
self._LED.direction = digitalio.Direction.OUTPUT
self._LED.value = True # 点亮LED
if OE :
self._OE = digitalio.DigitalInOut(pinmap[OE])
self._OE.direction = digitalio.Direction.OUTPUT
self._debug = self.OFF
self._cycles = 10 # the number of cycles of the out signal for which the time is measured
self._cycle = 0
self._freq_div = self.POWER_OFF
self._start_tick = 0
self._end_tick = 0
meas_finished = False
self._timeout = 2000 # timeout in ms
@property
def debugging(self) :
return self._debug
@debugging.setter
def debugging(self,onOff) :
if onOff:
print("Debugging switched on")
else :
print("Debugging switched off")
self._debug = onOff
# controls the illumination LEDs
@property
def led(self):
# get the current state of the illumination leds
return self._LED.value()
def on(self):
if self._LED:
self._LED.value = True
def off(self):
if self._LED:
self._LED.value = False
@led.setter
def led(self,onOff):
if onOff:
self.on()
else:
self.off()
# sets the filters
@property
def filter(self):
current_setting = (self._S2.value(),self._S3.value())
if self._debug:
if current_setting == self.RED:
print("Red filter is set")
elif current_setting == self.GREEN:
print("Green filter is set")
elif current_setting == self.BLUE :
print("Blue filter is set")
else:
print("No filters are set. The filter setting is clear")
return current_setting
@filter.setter
###
def filter(self,filter_setting):
if self._debug:
print("Setting S2 to {:d} and S3 to {:d}".format(filter_setting[0],filter_setting[1]))
self._S2.value(filter_setting[0])
self._S3.value(filter_setting[1])
###
def filter(self, filter_setting):
if self._debug:
print("Setting S2 to {:d} and S3 to {:d}".format(filter_setting[0], filter_setting[1]))
self._S2.value = filter_setting[0]
self._S3.value = filter_setting[1]
# Sets the frequency divider
@property
def freq_divider(self):
"""
if not self._S0 or not self._S1:
print("S0 or S1 signal is not connected. The frequency divider is therefore fixed")
return
current_freq_div = (self._S0.value(),self._S1.value())
"""
if self._S0 is None or self._S1 is None:
print("S0 or S1 signal is not connected. The frequency divider is therefore fixed")
return
current_freq_div = (self._S0.value, self._S1.value)
if self._debug:
if current_freq_div == self.POWER_OFF:
print("Device set to sleep mode")
elif current_freq_div == self.TWO_PERCENT:
print("Frequency divided by a factor 50")
elif current_freq_div == self.TWENTY_PERCENT:
print("Frequency divided by a factor 5")
else:
print("Frequency at 100%")
return current_freq_div
@freq_divider.setter
def freq_divider(self,freq_div):
if not self._S0 or not self._S1:
print("S0 or S1 signal is not connected. The frequency divider is therefore fixed and cannot be set")
return
if self._debug:
print("Setting S0 to {:d} and S1 to {:d}".format(freq_div[0],freq_div[1]))
#self._S0.value(freq_div[0])
#self._S1.value(freq_div[1])
self._S0.value = freq_div[0]
self._S1.value = freq_div[1]
# Puts the TCS3200 into sleep mode
def power_off(self):
self.freq_divider = self.POWER_OFF
# defines the number of pulses to wait for in the measurement IRQ handler
@property
def cycles(self):
return self._cycles
@cycles.setter
def cycles(self,no_of_cycles):
if no_of_cycles < 1:
print("The number of cycles must be at least 1")
return
self._cycles = no_of_cycles
if self._debug:
print("No of cycles to be measured was set to {:d}".format(self._cycles))
# Starts the measurement
@property
def meas(self):
if self._debug:
if self._meas:
print("Measurement is started")
else:
print("Measurement is stopped")
return self._meas
def wait_for_rising(pin, callback):
last_state = pin.value
while True:
current_state = pin.value
if not last_state and current_state: # 检测上升沿
callback()
last_state = current_state
time.sleep(0.001) # 1ms轮询
# 使用方法
# wait_for_rising(self._OUT, self._cbf)
def start_timer(self):
self._tim_start = time.monotonic()
def timer_expired(self):
if self._tim_start is None:
return False
return (time.monotonic() - self._tim_start) > self._timeout
def stop_timer(self):
self._tim_start = None
def example_usage(self):
self.start_timer()
while not self.timer_expired():
# 执行你的任务
pass
print("Timeout reached!")
@meas.setter
def meas(self,startStop):
if startStop:
self._meas = True
self._cycle = 0
self._start_tick = 0
self._end_tick = 0
if self._debug:
print("Measurement handler started")
# self._OUT.irq(trigger=Pin.IRQ_RISING,handler=self._cbf)
# wait_for_rising(self._OUT, self._cbf)
# start the timeout counter
self._tim.init(period=self._timeout, mode=Timer.ONE_SHOT, callback=self._timeout_handler)
self._tim_start = time.monotonic()
"""
用 self._tim_start = time.monotonic() 记录定时器启动时间,替代硬件定时器初始化。
后续可用 if time.monotonic() - self._tim_start > self._timeout: 判断是否超时。
如需回调,可在主循环中检测超时并手动调用 self._timeout_handler()。
"""
else:
self._meas=False
self._OUT.irq(trigger=Pin.IRQ_RISING,handler=None)
if self._debug:
print("Measurement handler stopped")
# disarm the timeout
self._tim.deinit()
# returns the frequency measured
@property
def measured_freq(self):
duration = self._end_tick - self._start_tick # measurement duration
frequency = 1000000 * self._cycles/duration # duration is measured in us
return frequency
@property
def timeout(self):
return self-_timeout
@timeout.setter
def timeout(self,timeout_ms):
self._timeout = timeout_ms
# This is the callback function that measures the time taken by a predefined no of cycles of the out signal
def _cbf(self,src):
t = time.ticks_us()
if self._cycle == 0:
self._start_tick = t
if self._cycle >= self._cycles: # the number of cycles has been reached
self._end_tick = t
self.meas=self.OFF
return
self._cycle += 1
# The timeout handler raises a timeout exception
def _timeout_handler(self,src):
# stop the measurement
self._OUT.irq(trigger=Pin.IRQ_RISING,handler=None)
# raise the timeour exception
raise Exception("Measurement Timeout!")
display = board.DISPLAY
main_group = displayio.Group()
BORDER = 20
FONTSCALE = 2
BACKGROUND_COLOR = 0x000000 # Bright Green
FOREGROUND_COLOR = 0xAA0088 # Purple
TEXT_COLOR = 0xFFFFFF
display = board.DISPLAY
# 整个背景
splash = displayio.Group()
display.root_group = main_group
bg_bitmap = displayio.Bitmap(120, 120, 1)
bg_palette = displayio.Palette(1)
bg_palette[0] = 0x000000
FONTSCALE = 2
bg_sprite = displayio.TileGrid(bg_bitmap, pixel_shader=bg_palette)
main_group.append(bg_sprite)
text_label = label.Label(
terminalio.FONT,
text="EEPW-DigiKey ColorTest",
color=0xFFFFFF,
scale=1
)
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)
# 初始化板载按键D1
button1 = digitalio.DigitalInOut(board.D1)
button1.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 > 5 else white_r
white_g = fg if fg > 5 else white_g
white_b = fb if fb > 5 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_note1(note_index):
if 0 <= note_index < len(note_names):
freq = frequencies[note_index]
pwm.frequency = int(freq)
pwm.duty_cycle = 2 ** 15
def play_note(note, duration):
frequency = note_freq.get(note, 440)
pwm.frequency = frequency
# if note.strip() == " ":
# 空音名,不发声,休止
# time.sleep(duration)
# else:
pwm.duty_cycle = 2 ** 15 # = 32768 # 50% 占空比
time.sleep(duration*2)
pwm.duty_cycle = 0
time.sleep(0.05)
def stop_playing():
pwm.duty_cycle = 0
def color_to_note_name(r, g, b):
# 先判断是否为休止(太暗)
if max(r, g, b) < 10:
return " "
# 细化分量法,优先级:高音 > 中音 > 低音
# 你可以根据实际需要调整阈值
if r > 220:
return "1'" # 高音do
elif r > 180:
return "7" # xi
elif r > 150:
return "6" # la
elif r > 120:
return "5" # so
elif r > 90:
return "4" # fa
elif r > 60:
return "3" # mi
elif r > 30:
return "2" # re
elif r > 10:
return "1" # do
if g > 220:
return "2'" # 高音re
elif g > 180:
return "7" # xi
elif g > 150:
return "6" # la
elif g > 120:
return "5" # so
elif g > 90:
return "4" # fa
elif g > 60:
return "3" # mi
elif g > 30:
return "2" # re
if b > 220:
return "3'" # 高音mi
elif b > 180:
return "7" # xi
elif b > 150:
return "6" # la
elif b > 120:
return "5" # so
elif b > 90:
return "4" # fa
elif b > 60:
return "3" # mi
elif b > 30:
return "2" # re
return " " # 其它情况休止
def rgb_to_note_name(r, g, b):
# 归一化
rf, gf, bf = r/255, g/255, b/255
h, s, v = colorsys.rgb_to_hsv(rf, gf, bf)
# h范围0~1,分为N段
note_names = ["1", "2", "3", "4", "5", "6", "7", "1'"]
idx = int(h * len(note_names)) % len(note_names)
return note_names[idx]
print("TCS3200 上电 EEPW-DigiKey ColorTest.")
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 button1.value:
print("D1")
time.sleep(0.02)
led.value = not led.value
time.sleep(0.02)
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 = "watting..."
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 = "repeat Test or open Led"
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}")
r, g, b = freq_to_rgb(fr, fg, fb)
text_label.text = f"R:{r}\nG:{g}\nB:{b}"
text_label.anchor_point = (0.5, 1.0)
text_label.anchored_position = (display.width - 70, display.height //2)
bg_palette[0] = (r << 16) | (g << 8) | b
pixel.fill((r, g, b))
print(f"Color: R={r}, G={g}, B={b}")
note_name = color_to_note_name(r, g, b)
freq = note_freq[note_name]
print(f"检测到颜色(R={r},G={g},B={b}),对应音名:{note_name},频率:{freq}Hz")
play_note1(note_index) #(note_index,0.5)
time.sleep(DEBOUNCE_TIME)
stop_playing()
else:
time.sleep(0.1)
time.sleep(0.01)
找到拖延症伙伴了,共勉
我要赚赏金
