0x6A Logbook

0x6A Logbook
Shi6a的筆記本
  1. 首頁
  2. 程式開發
  3. 正文

ESP32 WS2812 RGB LED 控制完整教學:從單線協定到 RMT DMA 實作

2026 年 6 月 14 日 23點熱度 0人點贊 0條評論

WS2812 是什麼?

ESP32 WS2812 RGB LED(又稱 NeoPixel)是物聯網視覺化專案中最受歡迎的可尋址 LED,每顆 LED 內部整合了驅動 IC,只需一條 GPIO 訊號線就能獨立控制每顆 LED 的顏色和亮度。單顆 WS2812 可顯示 24-bit 色彩(16,777,216 色),並支援無限級聯。

ESP32 WS2812 LED 控制系統架構

WS2812 單線通訊協定

WS2812 使用專屬的單線(One-Wire)通訊協定,時脈為 800kHz,透過精確的脈衝寬度編碼來區分邏輯 '0' 和 '1':

WS2812 單線通訊協定位元時序

關鍵時序參數(容差 ±150ns):

  • T0H = 0.35μs(code '0' 的高電位時間)
  • T0L = 0.90μs(code '0' 的低電位時間)
  • T1H = 0.70μs(code '1' 的高電位時間)
  • T1L = 0.60μs(code '1' 的低電位時間)
  • RESET = 最低 280μs 的低電位(標示訊框結束)

位元速率約 800 kbps,每個 LED 需 24 bits = 30μs 傳輸時間。

24-bit 資料訊框與級聯機制

WS2812 24-bit GRB 資料訊框與級聯

WS2812 的資料格式與常見的 RGB 順序不同:

  • 資料順序:G(7:0) → R(7:0) → B(7:0),由高位元(MSB)先傳
  • 綠色 = bit 23~16,紅色 = bit 15~8,藍色 = bit 7~0
  • 級聯(Daisy Chain):第一顆 LED 接收完整資料後,將後續資料透過 DOUT 轉發給下一顆
  • 因此傳送 N 顆 LED 的資料 = N × 24 bits + RESET(≥ 280μs)

用 delayMicroseconds() 實作(最簡單)

// ESP32 WS2812 — 最簡方式(delayMicroseconds)
#include <Adafruit_NeoPixel.h>

#define PIN        18
#define NUM_LEDS   64

Adafruit_NeoPixel strip(NUM_LEDS, PIN, NEO_GRB + NEO_KHZ800);

void setup() {
    strip.begin();
    strip.show();  // 初始化後全部熄滅
}

void loop() {
    // 彩虹漸變
    for (int i = 0; i < NUM_LEDS; i++) {
        int hue = (i * 360 / NUM_LEDS + millis() / 10) % 360;
        strip.setPixelColor(i, strip.gamma32(strip.ColorHSV(hue)));
    }
    strip.show();
    delay(20);
}

直接 GPIO 操作(理解底層原理)

// ESP32 WS2812 — 直接 GPIO 操作(理解協定原理)
#define WS2812_PIN  18
#define NUM_LEDS    16

// 使用 ESP32 GPIO 輸出暫存器(高速操作)
#define GPIO_OUT_REG (GPIO.out_w1ts.val)   // 設 HIGH
#define GPIO_CLR_REG (GPIO.out_w1tc.val)   // 設 LOW
#define BIT_MASK     (1ULL << WS2812_PIN)

// 精確延時(使用 ESP32 的 esp_timer 或 Xtensa 指令週期)
// ESP32 時脈 240MHz → 1 個指令 ≈ 4.17ns
// 但需考慮函式呼叫和 GPIO 操作的開銷

static inline void write_ws2812_byte(uint8_t byte) {
    for (int i = 7; i >= 0; i--) {  // MSB first
        if (byte & (1 << i)) {
            // 送出 '1': HIGH 0.70μs → LOW 0.60μs
            GPIO_OUT_REG = BIT_MASK;
            __asm__ __volatile__("nop; nop; nop; nop; nop; nop; nop;");  // ~0.7μs
            GPIO_CLR_REG = BIT_MASK;
            __asm__ __volatile__("nop; nop; nop; nop; nop;");           // ~0.6μs
        } else {
            // 送出 '0': HIGH 0.35μs → LOW 0.90μs
            GPIO_OUT_REG = BIT_MASK;
            __asm__ __volatile__("nop; nop; nop; nop;");                // ~0.35μs
            GPIO_CLR_REG = BIT_MASK;
            __asm__ __volatile__("nop; nop; nop; nop; nop; nop; nop; nop; nop; nop;");  // ~0.9μs
        }
    }
}

void ws2812_show(uint8_t leds[][3], int count) {
    // 關閉中斷,確保時序精確
    taskDISABLE_INTERRUPTS();

    for (int i = 0; i < count; i++) {
        write_ws2812_byte(leds[i][0]);  // G
        write_ws2812_byte(leds[i][1]);  // R
        write_ws2812_byte(leds[i][2]);  // B
    }

    taskENABLE_INTERRUPTS();

    // RESET ≥ 280μs
    GPIO_CLR_REG = BIT_MASK;
    delayMicroseconds(300);
}

使用 ESP32 RMT 外設(推薦方式)

直接 GPIO 操作的時序容易受中斷影響。ESP32 的 RMT 外設能硬體產生精確脈衝,不受 CPU 干擾:

ESP32 RMT DMA 自動發送

// ESP32 WS2812 — 使用 RMT 外設(專業做法)
#include <driver/rmt.h>

#define RMT_TX_CH  0
#define WS2812_PIN 18
#define NUM_LEDS   64

// 編碼 WS2812 的 '0' (T0H=0.35μs, T0L=0.90μs)
void setupRMT() {
    rmt_config_t config = {
        .rmt_mode = RMT_MODE_TX,
        .channel = (rmt_channel_t)RMT_TX_CH,
        .gpio_num = (gpio_num_t)WS2812_PIN,
        .clk_div = 4,  // 80MHz / 4 = 20MHz → 1 tick = 50ns
        .mem_block_num = 1,
        .tx_config = {
            .carrier_freq_hz = 0,     // 無載波
            .carrier_en = false,
            .idle_level = 0,
            .loop_en = false,
        }
    };
    rmt_config(&config);
    rmt_driver_install((rmt_channel_t)RMT_TX_CH, 0, 0);
}

// 用 RMT item 建構 WS2812 資料
// 邏輯 '0': T0H=7 ticks(350ns) + T0L=18 ticks(900ns)
// 邏輯 '1': T1H=14 ticks(700ns) + T1L=12 ticks(600ns)

#define T0H_TICKS  7
#define T0L_TICKS  18
#define T1H_TICKS  14
#define T1L_TICKS  12

void ws2812_rmt_show(rmt_item32_t* items, int num_leds,
                     uint8_t leds[][3]) {
    int idx = 0;
    for (int i = 0; i < num_leds; i++) {
        // G (MSB first)
        for (int b = 7; b >= 0; b--) {
            items[idx].level0 = 1;
            items[idx].level1 = 0;
            if (leds[i][0] & (1 << b)) {
                items[idx].duration0 = T1H_TICKS;
                items[idx].duration1 = T1L_TICKS;
            } else {
                items[idx].duration0 = T0H_TICKS;
                items[idx].duration1 = T0L_TICKS;
            }
            idx++;
        }
        // R
        for (int b = 7; b >= 0; b--) {
            items[idx].level0 = 1;
            items[idx].level1 = 0;
            if (leds[i][1] & (1 << b)) {
                items[idx].duration0 = T1H_TICKS;
                items[idx].duration1 = T1L_TICKS;
            } else {
                items[idx].duration0 = T0H_TICKS;
                items[idx].duration1 = T0L_TICKS;
            }
            idx++;
        }
        // B
        for (int b = 7; b >= 0; b--) {
            items[idx].level0 = 1;
            items[idx].level1 = 0;
            if (leds[i][2] & (1 << b)) {
                items[idx].duration0 = T1H_TICKS;
                items[idx].duration1 = T1L_TICKS;
            } else {
                items[idx].duration0 = T0H_TICKS;
                items[idx].duration1 = T0L_TICKS;
            }
            idx++;
        }
    }

    // 寫入 RESET (≥ 280μs = 5600 ticks)
    items[idx].duration0 = 0;
    items[idx].level0 = 0;
    items[idx].duration1 = 6000;
    items[idx].level1 = 0;
    idx++;

    rmt_write_items((rmt_channel_t)RMT_TX_CH, items, idx, true);
}

// 全域緩衝區
rmt_item32_t ledItems[NUM_LEDS * 24 + 1];
uint8_t ledData[NUM_LEDS][3] = {0};

void loop() {
    // 製作彩虹效果
    for (int i = 0; i < NUM_LEDS; i++) {
        int hue = (i * 256 / NUM_LEDS + millis() / 5) & 0xFF;
        ledData[i][0] = hue;        // G
        ledData[i][1] = 255 - hue;   // R
        ledData[i][2] = (hue * 2) & 0xFF;  // B
    }
    ws2812_rmt_show(ledItems, NUM_LEDS, ledData);
    delay(30);
}

使用 Adafruit NeoPixel 函式庫(開發最快)

// ESP32 WS2812 — Adafruit NeoPixel 函式庫
#include <Adafruit_NeoPixel.h>

#define PIN        18
#define NUM_LEDS   256  // 8×8 矩陣或 4 條 64 燈燈條

Adafruit_NeoPixel strip(NUM_LEDS, PIN, NEO_GRB + NEO_KHZ800);

uint32_t Wheel(byte WheelPos) {
    WheelPos = 255 - WheelPos;
    if (WheelPos < 85) {
        return strip.Color(255 - WheelPos * 3, 0, WheelPos * 3);
    }
    if (WheelPos < 170) {
        WheelPos -= 85;
        return strip.Color(0, WheelPos * 3, 255 - WheelPos * 3);
    }
    WheelPos -= 170;
    return strip.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
}

void theaterChase(uint32_t color, int wait) {
    for (int a = 0; a < 10; a++) {
        for (int b = 0; b < 3; b++) {
            strip.clear();
            for (int c = b; c < strip.numPixels(); c += 3) {
                strip.setPixelColor(c, color);
            }
            strip.show();
            delay(wait);
        }
    }
}

void rainbow(int wait) {
    for (long firstPixelHue = 0; firstPixelHue < 5 * 65536; firstPixelHue += 256) {
        for (int i = 0; i < strip.numPixels(); i++) {
            int pixelHue = firstPixelHue + (i * 65536L / strip.numPixels());
            strip.setPixelColor(i, strip.gamma32(strip.ColorHSV(pixelHue)));
        }
        strip.show();
        delay(wait);
    }
}

void setup() {
    strip.begin();
    strip.setBrightness(50);
    strip.show();
}

void loop() {
    rainbow(10);
    theaterChase(strip.Color(255, 0, 0), 50);
    theaterChase(strip.Color(0, 255, 0), 50);
    theaterChase(strip.Color(0, 0, 255), 50);
}

硬體注意事項

  • 電源:每顆 WS2812 全白時需 60mA(5V)。100 顆 = 6A!務必使用獨立電源,不可從 ESP32 的 3.3V 穩壓器供電
  • 電容:LED 電源端並聯 1000μF 電解電容,防止瞬間大電流導致電壓驟降
  • 電平轉換:ESP32 GPIO 為 3.3V,WS2812 DIN 邏輯準位為 5V(Vih 最低 0.7VCC ≈ 3.5V),建議使用 3.3V→5V 電平移位器(如 74HCT125)
  • 串聯電阻:DIN 路徑串聯 300~500Ω 電阻,減少訊號反射和雜訊
  • 地線:ESP32 和 LED 電源必須共地

WS2812 色彩混合與功耗估算

ESP32 vs Arduino Uno:WS2812 控制能力對比

特性 Arduino Uno ESP32
最高 LED 數 ~300(軟體 PWM) ~3000(RMT + DMA)
更新率(300 LEDs) ~30 fps ~100 fps
中斷影響 中斷時畫面閃爍 RMT 硬體發送,不受影響
同時驅動燈條數 1 條 8 條(RMT 8 通道)
WiFi 並行 不支援 RMT 背景發送,CPU 可跑 WiFi
Gamma 校正 需軟體實作 內建 gamma32() 硬體加速

進階:RMT + DMA + 雙緩衝

對於 > 500 顆 LED 的大型專案(如 LED 矩陣牆),需要使用 DMA 雙緩衝技術:

// ESP32 WS2812 — RMT + DMA + 雙緩衝(大型專案)
#include <driver/rmt.h>

#define RMT_TX_CH  0

static void rmt_tx_done_cb(rmt_channel_t channel, void* arg) {
    // DMA 傳輸完成回呼
    xSemaphoreGiveFromISR((SemaphoreHandle_t)arg, NULL);
}

void setupRMT_DMA() {
    rmt_config_t config = {
        .rmt_mode = RMT_MODE_TX,
        .channel = (rmt_channel_t)RMT_TX_CH,
        .gpio_num = (gpio_num_t)WS2812_PIN,
        .clk_div = 4,
        .mem_block_num = 8,
        .tx_config = {
            .loop_en = false,
            .idle_level = 0,
            .carrier_en = false,
        }
    };
    rmt_config(&config);
    rmt_driver_install((rmt_channel_t)RMT_TX_CH, 4096, 0);
    rmt_register_tx_end_callback(rmt_tx_done_cb, NULL);
}

實戰:WiFi 控制 LED 矩陣

// ESP32 WS2812 — WiFi + Web Server 控制
// 結合之前介紹的 ESP32 Web Server,用手機控制 LED 顏色

#include <WiFi.h>
#include <WebServer.h>
#include <Adafruit_NeoPixel.h>

#define NUM_LEDS 64
#define PIN      18

Adafruit_NeoPixel strip(NUM_LEDS, PIN, NEO_GRB + NEO_KHZ800);
WebServer server(80);

// HTML + JavaScript 從 R"rawliteral(...)rawliteral" 取得

void handleRoot() { server.send(200, "text/html", html); }

void handleSet() {
    String color = server.arg("color");
    int bright = server.arg("bright").toInt();
    long rgb = strtol(color.c_str(), NULL, 16);
    uint8_t r = (rgb >> 16) & 0xFF;
    uint8_t g = (rgb >> 8) & 0xFF;
    uint8_t b = rgb & 0xFF;
    strip.setBrightness(bright);
    for (int i = 0; i < NUM_LEDS; i++) {
        strip.setPixelColor(i, strip.Color(r, g, b));
    }
    strip.show();
    server.send(200, "text/plain", "OK");
}

void setup() {
    WiFi.begin("SSID", "PASSWORD");
    while (WiFi.status() != WL_CONNECTED) delay(500);
    strip.begin(); strip.show();
    server.on("/", handleRoot);
    server.on("/set", handleSet);
    server.begin();
}

void loop() { server.handleClient(); }

總結

WS2812 以其簡潔的單線通訊協定和豐富的色彩表現,成為 IoT 視覺化專案的首選。ESP32 的 RMT 外設讓 WS2812 控制變得簡單可靠,不受 CPU 中斷影響,同時支援 WiFi 連線實現遠端控制。

選擇指引:

  • 小型專案(≤ 64 LEDs):Adafruit NeoPixel 函式庫,簡單快速
  • 中型專案(64~500 LEDs):RMT 外設 + 直接記憶體操作
  • 大型專案(≥ 500 LEDs):RMT + DMA + 雙緩衝,搭配 PSRAM
標籤: 教學
最後更新:2026 年 6 月 14 日

shi6a

這個人很懶,什麼都沒留下

點贊
< 上一篇
下一篇 >

文章評論

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回覆

COPYRIGHT © 2026 0x6A Logbook. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang