nRF52のPWMでNeoPixelをLチカする


この記事は、nRF52でBLEデバイスを開発するの関連記事です。BLE使わないので、オフトピ記事として書きます。

はじめに

 元記事で使用するために購入したAdafruit Feather nRF52840にNeoPixel WS2812B (データシート)が載っていたので、ドライバを書いてみました。

 Arduinoの方はお帰りください。nRF52-SDKのための記事となります。

NeoPixel WS2812B とは

 データシート見てください。

 ではあんまりなので、簡単に説明すると、

  • 4pin (VDD/GND/DIN/DOUT)の三色LED
  • 制御にはDINの1pinしか使わない
  • DIN→DOUTと繋ぐことで複数のNeoPixelを制御可能

というちょっとインテリジェントなLEDです。

 ちょっと面白いのは、制御線一本で三色のLEDをコントロールするところ。パルス長固定で、デューティ比の長短を1/0と解釈して24bit分のデータをシリアルで伝送するところです。
 マイコン的には、ウエイトを取りながらGPIOを上下させるという解決方法が思い浮かびますが、そんな泥臭い方法はとりません。PWMでやります。

目標

 以下のような波形を出すことです。

実装

定数

enum {
    CLOCK      = NRF_PWM_CLK_16MHz,
    TOP        = 20,
    DUTY0      = 6,
    DUTY1      = 13,
};

 データシートより、パルス長1.25us、0の時のHパルスが0.4us、1の時のHパルスが0.8usとなります。
 動作クロックを16MHzとして
  20カウント=1.25us
  6カウント=0.375us
  13カウント=0.8125us
 となります。±150nsの誤差が許されるので、これで許容範囲です。

PWMモジュールの初期化

static nrf_drv_pwm_t m_pwm0 = NRF_DRV_PWM_INSTANCE(NEOPIXEL_INSTANCE);

void neopixel_init(uint8_t pin, neopixel_handler_t handler) {
  m_handler = handler;
  nrf_drv_pwm_config_t const config0 =
      {
          .output_pins =
              {
                  pin,                      // channel 0
                  NRF_DRV_PWM_PIN_NOT_USED, // channel 1
                  NRF_DRV_PWM_PIN_NOT_USED, // channel 2
                  NRF_DRV_PWM_PIN_NOT_USED  // channel 3
              },
          .irq_priority = APP_IRQ_PRIORITY_LOWEST,
          .base_clock = CLOCK,
          .count_mode = NRF_PWM_MODE_UP,
          .top_value = TOP,
          .load_mode = NRF_PWM_LOAD_COMMON,
          .step_mode = NRF_PWM_STEP_AUTO};
  APP_ERROR_CHECK(nrf_drv_pwm_init(&m_pwm0, &config0, pwm_handler));
}

 pwm0のインスタンスを確保しておき、初期設定します。
 output_pinsには、4ch分のGPIOが設定可能ですが、今回はch0のみ使用します。以下のようにすると、信号を出していない時はHが出力されます。

    pin | NRF_DRV_PWM_PIN_INVERTED, // channel 0

 nrf_drv_pwm_init()の第三引数はコールバック関数のポインタを指定できます。(パルスの出力完了イベントが拾えます)

送信

static nrf_pwm_values_common_t pwm_sequence_values[NEOPIXEL_MAX_CHAINS * NEOPIXEL_BYTES + 1];

 LED1個24bitの出力をするのに24個分のバッファが必要です。リセット信号を出すために1個分余計に確保します。

void neopixel_write(uint32_t *colors, uint32_t chain) {
  if (chain > NEOPIXEL_MAX_CHAINS) {
    chain = NEOPIXEL_MAX_CHAINS - 1;
  }

  nrf_pwm_values_common_t *ptr = pwm_sequence_values;
  for (uint32_t led = 0; led < chain; led++) {
    uint32_t color = colors[led];
    for (uint8_t i = 0; i < NEOPIXEL_BYTES; ++i) {
      uint16_t value = 0;
      if ((color & 0x800000) == 0) {
        value = DUTY0;
      } else {
        value = DUTY1;
      }
      *ptr++ = value | 0x8000;
      color <<= 1;
    }
  }
  *ptr++ = 0x8000;

  nrf_pwm_sequence_t const seq0 =
      {
        .values.p_common = pwm_sequence_values,
        .length = NEOPIXEL_BYTES * chain + 1,
        .repeats = 0,
        .end_delay = 0
      };

  (void)nrf_drv_pwm_simple_playback(&m_pwm0, &seq0, 1, NRF_DRV_PWM_FLAG_STOP);
}

 各ビットをDUTY0/DUTY1のbyte列に変換します。 0x8000をorすると、パルスがH→Lで出力されます。
 nrf_drv_pwm_simple_playback()で、設定したデータが出力されます。各データはeasyDMAにより自動的にポートに出力されるので、アプリ側でタイミングを取る必要はありません。また、ここで処理がブロックされることもありません。

 これで、NeoPixelが光ります。

タイマー

 光るだけではつまらないので、Lチカさせましょう。

 まず、lfclkを確保してapp_timerを設定します。クロックソースが32KHzなので、そんなに精度は高くないです。

static void lfclk_request(void)
{
    ret_code_t err_code = nrf_drv_clock_init();
    APP_ERROR_CHECK(err_code);
    nrf_drv_clock_lfclk_request(NULL);
}
    lfclk_request();

    uint32_t ret = app_timer_init();
    APP_ERROR_CHECK(ret);

    ret = app_timer_create(&m_timer_0, APP_TIMER_MODE_REPEATED, timer_handle);
    APP_ERROR_CHECK(ret);

    ret = app_timer_start(m_timer_0, APP_TIMER_TICKS(10), NULL);
    APP_ERROR_CHECK(ret);

10msのタイマーでHSVのVを加算減算して、スムーズな点滅をさせます。Vが0になったら、Hを乱数で変更します。HSVをGRBに変換してPWMに突っ込みます。

static void timer_handle(void *p_context) {
  UNUSED_PARAMETER(p_context);

  if (sign == 1) {
    v++;
    if (v == 100){
      sign = 0;
    }
  } else {
    v--;
    if (v == 0){
      sign = 1;
      h = rand() % 360;
    }
  }

  uint32_t color = neopixel_HSVtoGRB(h, s, v);
  neopixel_write(&color, 1);
}

 NeoPixel明るすぎるんじゃ

ソース

 ソースはgithubに上げてあります。(SDK)/examples/peripheral/pwm_driver も併せて読むといいです。

ぐちっぽいの

 nRF52-SDKのnrf_xxx()系のドライバライブラリはドキュメントがあまり充実していません。が、レジスタと対比しながらソースを読むと理解が早くなります。動かしながら学んでいくしかありません。

おわりに

 ということで、一般に言われる「PWMでLチカ」というお題とはかけ離れたものを作ってみました。
 NeoPixelのカスケード接続にも対応したつもりですが、現物がないのでここまでです。

 FeatherにはQSPI FLASHも載っているので、また今度やります。

以上です。