2026年8月16日 星期日

[水井村USR]WS2812B讓水井三寶會發光:ESP32燈光控制原理、接線與程式實作

WS2812B讓水井三寶會發光:ESP32燈光控制原理、接線與程式實作

水井三寶智慧互動展覽系統|智慧互動層燈光控制專題

在水井三寶智慧互動展覽系統中,聲音負責說故事,燈光則讓觀眾「看見故事正在發生」。這篇把WS2812B燈光控制獨立整理,從工作原理、接線、LED分區,到白馬追光、烏龜呼吸與姻緣花綻放的程式邏輯,完整做成可直接上課與實作的教材。

一、WS2812B是什麼?

WS2812B是一種可定址RGB LED,每顆LED內部整合驅動控制電路。多顆串接時,只需要一條DATA線,就能逐顆指定顏色與亮度。Arduino端使用Adafruit_NeoPixel函式庫,水井三寶目前採 NEO_GRB + NEO_KHZ800,由ESP32 GPIO13控制62顆LED。

ESP32 DATA ↓ LED #0 → LED #1 → LED #2 → … → LED #61 每一顆取走自己的顏色資料,再把後續資料傳給下一顆

二、正式接線方式

ESP32 × WS2812B 正式接線圖|水井三寶 V1.7 外接 5V 電源 +5VGND ESP32 GPIO13 GND 邏輯電平轉換 74AHCT125 等 330–470Ω WS2812B 5V DIN GND 500–1000µF 62顆 LED 分區 0–23:底座 24顆 24–33:白馬 10顆 34–45:烏龜 12顆 46–61:姻緣花 16顆
展覽版電源原則:LED燈條使用獨立5V電源;ESP32與燈條必須共地。DATA進入第一顆LED前建議串接300~500Ω電阻,電源端可加500~1000µF電容。ESP32是3.3V邏輯,5V燈條的長期展覽版建議加入適當的邏輯電平轉換器。

三、62顆LED不是一整條,而是四個燈區

燈區LED編號數量角色
底座0–2324待機與環境柔光
白馬24–3310暖金追光
烏龜34–4512水藍呼吸
姻緣花46–6116粉紅漸次綻放

四、燈光控制不是裝飾,而是故事的第二語言

故事燈效意義
001 白馬暖金追光向前、行動、奔跑感
002 烏龜藍色呼吸沉穩、守水、守護土地
003 姻緣花粉紅逐顆點亮花朵慢慢綻放
004 水井三寶總故事三角色輪替白馬→烏龜→姻緣花依序出場
005 創作者資訊三區共同暖光溫暖、穩定、不搶作品主體

五、為什麼白馬用追光?

白馬的主光點會沿著10顆LED移動,後方保留較暗的尾光,因此不是整區同時亮,而是形成「奔跑」的速度感。

六、為什麼烏龜用呼吸燈?

烏龜燈區使用藍色,亮度從低到高、再由高到低反覆變化,形成穩定呼吸感。這比快速閃爍更適合「守水、沉穩、長久陪伴土地」的角色設定。

七、姻緣花如何「開花」?

姻緣花不是整區一起閃,而是控制目前亮起的LED數量,由1顆慢慢增加到16顆,再逐步縮回。這種漸次點亮的動畫更接近「花開」而不是一般警示燈。

八、燈效如何跟故事狀態機連動?

IDLE ↓ 底座暖色微呼吸 READY ↓ 三角色低亮提示 選擇故事 ↓ JQ6500 PLAYING + 對應角色燈效 ↓ 故事播放完成 ↓ COOLDOWN → READY / IDLE

正式系統由 currentStory 決定燈效,因此聲音與燈光會同步對應故事。燈光也成為「狀態可視化」介面:即使觀眾沒有看手機,也能知道目前正在播放哪個角色。

九、程式設計重點:不要用長時間delay()

水井三寶還要同時處理ToF、按鈕、JQ6500、Edge Web與Wi-Fi,因此燈效大量使用 millis() 控制更新節奏,避免動畫阻塞其他任務。

教學重點:Arduino入門常用 delay() 做動畫;真正的IoT系統要進一步學會非阻塞式時間控制。燈光再漂亮,也不能讓感測器、Web或故事播放停住。

十、完整獨立測試程式

下面程式只保留WS2812B燈光模組,適合先測接線、燈區與動畫。正式系統再把這些函式整合回Layer 2~Layer 4。


#include <Arduino.h>
#include <Adafruit_NeoPixel.h>

#define LED_PIN 13
#define LED_COUNT 62
#define BASE_START 0
#define BASE_COUNT 24
#define HORSE_START 24
#define HORSE_COUNT 10
#define TURTLE_START 34
#define TURTLE_COUNT 12
#define FLOWER_START 46
#define FLOWER_COUNT 16

Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);

enum Story {
  STORY_NONE=0, STORY_HORSE=1, STORY_TURTLE=2,
  STORY_FLOWER=3, STORY_ALL=4, STORY_CREATOR=5
};
Story currentStory = STORY_NONE;

void setZone(int start, int count, uint32_t color){
  for(int i=0;i<count;i++) strip.setPixelColor(start+i,color);
}

void ledIdle(){
  static unsigned long last=0;
  static int b=4, dir=1;
  if(millis()-last<80) return;
  last=millis();
  b+=dir;
  if(b>=14) dir=-1;
  if(b<=4) dir=1;
  strip.clear();
  setZone(BASE_START,BASE_COUNT,strip.Color(b,b/2,0));
  strip.show();
}

void ledReady(){
  strip.clear();
  setZone(HORSE_START,HORSE_COUNT,strip.Color(20,8,0));
  setZone(TURTLE_START,TURTLE_COUNT,strip.Color(0,8,18));
  setZone(FLOWER_START,FLOWER_COUNT,strip.Color(18,0,7));
  strip.show();
}

void ledHorse(){
  static unsigned long last=0;
  static int pos=0;
  if(millis()-last<80) return;
  last=millis();
  strip.clear();

  for(int i=0;i<HORSE_COUNT;i++){
    int dist=(i-pos+HORSE_COUNT)%HORSE_COUNT;

    if(dist==0)
      strip.setPixelColor(HORSE_START+i,strip.Color(100,45,0));
    else if(dist<=2)
      strip.setPixelColor(HORSE_START+i,strip.Color(30,10,0));
  }

  pos++;
  if(pos>=HORSE_COUNT) pos=0;
  strip.show();
}

void ledTurtle(){
  static unsigned long last=0;
  static int b=10, dir=1;
  if(millis()-last<70) return;
  last=millis();
  b+=dir;
  if(b>=80) dir=-1;
  if(b<=10) dir=1;
  strip.clear();
  setZone(TURTLE_START,TURTLE_COUNT,strip.Color(0,b/2,b));
  strip.show();
}

void ledFlower(){
  static unsigned long last=0;
  static int count=1;
  static bool growing=true;

  if(millis()-last<120) return;
  last=millis();
  strip.clear();

  for(int i=0;i<count;i++)
    strip.setPixelColor(FLOWER_START+i,strip.Color(80,5,25));

  strip.show();

  if(growing){
    count++;
    if(count>=FLOWER_COUNT){count=FLOWER_COUNT;growing=false;}
  }else{
    count--;
    if(count<=2){count=2;growing=true;}
  }
}

void ledAllStory(){
  static unsigned long last=0;
  static int phase=0;
  if(millis()-last<900) return;
  last=millis();
  phase++;
  if(phase>2) phase=0;

  strip.clear();

  if(phase==0)
    setZone(HORSE_START,HORSE_COUNT,strip.Color(70,30,0));
  else if(phase==1)
    setZone(TURTLE_START,TURTLE_COUNT,strip.Color(0,25,70));
  else
    setZone(FLOWER_START,FLOWER_COUNT,strip.Color(70,5,25));

  strip.show();
}

void ledCreator(){
  strip.clear();
  uint32_t warm=strip.Color(45,28,10);
  setZone(HORSE_START,HORSE_COUNT,warm);
  setZone(TURTLE_START,TURTLE_COUNT,warm);
  setZone(FLOWER_START,FLOWER_COUNT,warm);
  strip.show();
}

void updateStoryLED(){
  switch(currentStory){
    case STORY_HORSE: ledHorse(); break;
    case STORY_TURTLE: ledTurtle(); break;
    case STORY_FLOWER: ledFlower(); break;
    case STORY_ALL: ledAllStory(); break;
    case STORY_CREATOR: ledCreator(); break;
    default: ledReady(); break;
  }
}

void setup(){
  Serial.begin(115200);
  strip.begin();
  strip.setBrightness(100);
  strip.clear();
  strip.show();
}

void loop(){
  unsigned long section=(millis()/10000UL)%6;

  switch(section){
    case 0: ledIdle(); break;
    case 1: currentStory=STORY_HORSE; updateStoryLED(); break;
    case 2: currentStory=STORY_TURTLE; updateStoryLED(); break;
    case 3: currentStory=STORY_FLOWER; updateStoryLED(); break;
    case 4: currentStory=STORY_ALL; updateStoryLED(); break;
    case 5: currentStory=STORY_CREATOR; updateStoryLED(); break;
  }
}

十一、從RGB燈條到文化互動語言

WS2812B真正有價值的不是「顏色很多」,而是每顆LED都可以被程式個別控制。當白馬用追光、烏龜用呼吸、姻緣花用綻放來呈現各自故事時,LED就不再只是裝飾,而成為文化敘事的一部分。

設計理念:聲音說故事,燈光表達故事情緒,作品本體仍然是主角。科技不取代文化,而是讓觀眾多一種理解文化的方式。

沒有留言:

張貼留言