RGB LED(WS2812)をESP32+RMTを使ってLチカする

概要

最近のM5Stack製品などは単色LEDの変わりにWS2812や互換性のあるSK6812を使ったRGB LEDを搭載しています。そのため気軽にLチカすることができません。FastLEDなどのライブラリを使ったりもしますが、ちょっと大きいライブラリなので使いにくいですよね。てことでシンプルなライブラリを作ってみました。

WS2812とは?

NeoPixelなどと呼ばれる事が多いRGB LEDで、複数のLEDを数珠つなぎに接続することができるものです。

M5Stack ATOM Matrix ESP32 Development Kit
M5Stack
¥3,294(2024/06/12 19:07時点)
小型なM5Stack

同じようなRGB LEDとしてSK8612も有名です。ATOM Matrixには25個のWS2812が搭載されています。信号線が1つで25個制御できるので最近よく使われています。

Stamp系にはSK8612が搭載されています。使い方は同じです。

RGB LEDの仕組み

RGBを各8ビットの合計3バイトで色信号を送信します。数珠つなぎにしている場合には自分の分の色信号を受信した後は、後続のLEDにそのまま信号を伝達します。こうして3バイトずつ先頭から受信していくことで色信号を通信しています。つまり25個のRGB LEDに色信号を送信するためには75バイト分送信する必要があります。

RGBの各色は8ビットなのですが、最大値の255を指定してはいけない場合があります。WS2812の最後に2桁の12は電流を表します。12mAまで流れる製品になります。25個接続すると最大300mAも流れることになります。ただしATOM Matrixは放熱処理などが甘いので、全力で点灯させると壊れてしまうようです。また非常にあがるいため、そんなに強い光は必要ない場合が多いです。そのため、20%ぐらいの50ぐらいまでの明るさで十分みたいです。

つまり、RGBを全色50にすると普通の明るさの白になります。 RGBを全色25にすると、少し暗めの白になります。全部0ですと黒で光っていない状態ですね。明るさの調整はこのように各色の大きさを減らすことで行います。その分階調が減るので色の再現性はよろしくありません。

ESP32での制御方法

赤外線リモコンの送信などで使うRMTがWS2812にも使えます。RMTはLOWとHIGHを任意のパルス幅で出力するための機能になります。WS2812で使われているビット送信をRMTで再現して送信することで色信号を送信可能です。

RMTの仕組み

ざっくりと説明するとHIGHとLOWを任意のパルス幅で設定して送信する機能です。WS2812は3バイト(24ビット)で1個分の色情報を送信可能です。1ビット単位でRMTの送信パルス情報を準備して、DMA転送で送信するような仕組みになっています。

実際には全部のLEDの色情報を保存しておき、送信時にRMT用のデータ形式の変換しながら渡すことになります。

公式サンプル

  • https://github.com/espressif/esp-idf/blob/master/examples/peripherals/rmt/led_strip/main/led_strip_main.c

上記がESP-IDFのスケッチ例です。

  • https://github.com/espressif/esp-idf/tree/master/examples/common_components/led_strip

これがスケッチ例で使っているライブラリ?です。んー、ちょっと実用的なコードではないですよね。。。

Arduino用クラスを作ってみた

LedTest.ino

#include "RMT_WS2812.hpp"

#define CONFIG_RMT_CHANNEL                  RMT_CHANNEL_0
#define CONFIG_MAX_BRIGHTNESS               20
#define CONFIG_SLEEP_MS                     15

#if defined(ARDUINO_M5Stack_ATOM)
#define CONFIG_RMT_GPIO                     GPIO_NUM_27
#define CONFIG_LED_NUM                      25
#elif defined(ARDUINO_STAMP_C3)
#define CONFIG_RMT_GPIO                     GPIO_NUM_2
#define CONFIG_LED_NUM                      1
#endif

RMT_WS2812 led(CONFIG_RMT_CHANNEL, CONFIG_RMT_GPIO, CONFIG_LED_NUM);

void setup() {
  Serial.begin(115200);
  delay(1000);

  led.begin();
  led.clear();
}

void loop() {
  static int8_t brightness = 0;
  static int8_t color = 0;
  brightness--;
  if (brightness < 0) {
    brightness = CONFIG_MAX_BRIGHTNESS;
    color--;
    if (color < 0) {
      color = 7;
    }
    for (int i = 0; i < CONFIG_LED_NUM; i++) {
      uint8_t red   = (color & 1) ? 255 : 0;
      uint8_t green = (color & 2) ? 255 : 0;
      uint8_t blue  = (color & 4) ? 255 : 0;
      led.setPixel(i, red, green, blue);
    }
  }

  led.setBrightness(brightness);
  led.refresh();

  delay(CONFIG_SLEEP_MS);
}

使い方はシンプルです。FastLEDは配列に自分で入れるタイプでしたが、setPixelで1個単位で設定する形で作っています。refresh()で実際に送信されます。ブロック関数ですので、送信が終わるまでブロックされます。

RMT_WS2812.hpp

#pragma once

#include "driver/rmt.h"

extern uint32_t  ws2812_t0h_ticks;
extern uint32_t  ws2812_t1h_ticks;
extern uint32_t  ws2812_t0l_ticks;
extern uint32_t  ws2812_t1l_ticks;
extern uint8_t   ws2812_brightness;
void IRAM_ATTR ws2812_rmt_adapter(const void *src, rmt_item32_t *dest, size_t src_size, size_t wanted_num, size_t *translated_size, size_t *item_num);

#ifndef RMT_WS2812_DEFINE_ONLY
uint32_t  ws2812_t0h_ticks;
uint32_t  ws2812_t1h_ticks;
uint32_t  ws2812_t0l_ticks;
uint32_t  ws2812_t1l_ticks;
uint8_t   ws2812_brightness = 20; // 0-100

void IRAM_ATTR ws2812_rmt_adapter(const void *src, rmt_item32_t *dest, size_t src_size, size_t wanted_num, size_t *translated_size, size_t *item_num) {
  if (src == NULL || dest == NULL) {
    *translated_size = 0;
    *item_num = 0;
    return;
  }
  const rmt_item32_t bit0 = {ws2812_t0h_ticks, 1, ws2812_t0l_ticks, 0};
  const rmt_item32_t bit1 = {ws2812_t1h_ticks, 1, ws2812_t1l_ticks, 0};
  size_t size = 0;
  size_t num = 0;
  uint8_t *psrc = (uint8_t *)src;
  rmt_item32_t *pdest = dest;
  while (size < src_size && num < wanted_num) {
    for (int i = 0; i < 8; i++) {
      if ((*psrc * ws2812_brightness / 100) & (1 << (7 - i))) {
        pdest->val =  bit1.val;
      } else {
        pdest->val =  bit0.val;
      }
      num++;
      pdest++;
    }
    size++;
    psrc++;
  }
  *translated_size = size;
  *item_num = num;
}
#endif

class RMT_WS2812 {
  public:
    RMT_WS2812(rmt_channel_t channel, gpio_num_t gpip, uint16_t ledNum) {
      _rmtChannel = channel;
      _rmtGpio = gpip;
      _ledNum = ledNum;
    };

    void begin(void) {
      rmt_config_t rmtConf = RMT_DEFAULT_CONFIG_TX(_rmtGpio, _rmtChannel);
      rmtConf.clk_div = 2; // Set 40MHz

      ESP_ERROR_CHECK(rmt_config(&rmtConf));
      ESP_ERROR_CHECK(rmt_driver_install(rmtConf.channel, 0, 0));

      uint32_t buffSize = _ledNum * 3;
      _buffer = (uint8_t*)calloc(1, buffSize);
      if (_buffer == NULL) {
        ESP_LOGE(TAG, "%s(%d): " "calloc failed", __FUNCTION__, __LINE__);
        return;
      }

      uint32_t counter_clk_hz = 0;
      if (rmt_get_counter_clock(_rmtChannel, &counter_clk_hz) != ESP_OK) {
        ESP_LOGE(TAG, "%s(%d): " "rmt_get_counter_clock failed", __FUNCTION__, __LINE__);
        return;
      }

      // ns -> ticks
      float ratio = (float)counter_clk_hz / 1e9;
      ws2812_t0h_ticks = (uint32_t)(ratio * WS2812_T0H_NS);
      ws2812_t0l_ticks = (uint32_t)(ratio * WS2812_T0L_NS);
      ws2812_t1h_ticks = (uint32_t)(ratio * WS2812_T1H_NS);
      ws2812_t1l_ticks = (uint32_t)(ratio * WS2812_T1L_NS);

      // set ws2812 to rmt adapter
      rmt_translator_init(_rmtChannel, ws2812_rmt_adapter);

      ESP_LOGV(TAG, "%s(%d): " "RMT_WS2812 begin channel(%d), gpip(%d), ledNum(%d)", __FUNCTION__, __LINE__, _rmtChannel, _rmtGpio, _ledNum);
    };

    esp_err_t clear(uint32_t timeout_ms = 200) {
      memset(_buffer, 0, _ledNum * 3);
      return refresh(timeout_ms);
    }

    esp_err_t refresh(uint32_t timeout_ms = 200) {
      if (rmt_write_sample(_rmtChannel, _buffer, _ledNum * 3, true) != ESP_OK) {
        ESP_LOGE(TAG, "%s(%d): " "rmt_write_sample failed", __FUNCTION__, __LINE__);
        return ESP_FAIL;
      }

      return rmt_wait_tx_done(_rmtChannel, pdMS_TO_TICKS(timeout_ms));
    }

    esp_err_t setPixel(uint32_t index, uint8_t red, uint8_t green, uint8_t blue) {
      if (_ledNum < index) {
        ESP_LOGE(TAG, "%s(%d): " "index(%d) is out of range(%d)", __FUNCTION__, __LINE__, index, _ledNum);
        return ESP_ERR_INVALID_ARG;
      }

      uint32_t start = index * 3;
      _buffer[start + 0] = green & 0xFF;
      _buffer[start + 1] = red & 0xFF;
      _buffer[start + 2] = blue & 0xFF;
      return ESP_OK;
    }

    void setBrightness(uint8_t brightness) {
      ws2812_brightness = brightness % 100;
    }

    const uint16_t WS2812_T0H_NS    =  350;
    const uint16_t WS2812_T0L_NS    = 1000;
    const uint16_t WS2812_T1H_NS    = 1000;
    const uint16_t WS2812_T1L_NS    =  350;
    const uint16_t WS2812_RESET_US  =  280;

  private:
    rmt_channel_t _rmtChannel;
    gpio_num_t _rmtGpio;
    uint32_t _ledNum;
    uint8_t *_buffer;
    const char *TAG = "RMT_WS2812";
};

ざっくりと、必要最低限っぽいところだけサンプルをベースに作ってみました。もともとは明るさ設定はなかったのですが、送信時にRMT用のデータを作成するところで明るさを反映させています。

とはいえ、setBrightness()は全体の明るさなので、特定のLEDの明るさを触る場合には自分で計算して小さい値にする必要があります。

まとめ

RMTは以外に使いやすいかもしれません。DMA転送っぽくてデータがなくなるとコールバック関数が呼ばれて、そこでRMT用にデータを変換してから送るって感じになります。

M5系ライブラリで使おうとすると、配列型にしたほうがいいかもしれませんが個人的にはこれぐらいシンプルなクラスの方が使いやすいかな。。。

あとはESP-IDFとArduinoの両方で使えるクラスがいいなと思って作ってみました。もう少し整理してちゃんとライブラリ化するかもしれませんが、クラスファイルだけコピーしてプロジェクトに追加すれば使えるってぐらいシンプルなのも使いやすいかなとも思っています。

コメント