Mbed OS + SPI + NeoPixelで嵌まった話

本記事は[Mbed Advent Calendar 2017](https://qiita.com/advent-calendar/2017/mbed)の22日目の記事です．

# はじめに

ここ数年，秋～冬にかけては[MashupAwrads](http://mashupaward.jp/)という開発系のコンテストに出ていますが，気が付けば毎年Mbedを利用した作品を投稿していました（2015年は[ゲーム機のハック](https://hacklog.jp/works/3897)，2016年は[自作のトイレセンサ](https://hacklog.jp/works/48197)）. 特に前者はコンテスト後に[渡會さん@ARM](https://qiita.com/toyowata)にお誘いいただき[MFT2016で展示させていただいたり](https://os.mbed.com/users/MACRUM/notebook/ma-and-myself/)ととても思い出深いです．

今年は，[放り投げるとWeb APIで天気予報を取得し勝手に転がるゲタ](https://hacklog.jp/works/51791)というジョークネタを作成したのですが，制作時に **Mbed OS + SPI + neopixel の組みあわせで嵌りました**．"こういうケースもある"という ~~愚痴~~ 小話 として残しておきたいと思います．

## 3行で説明しろよ！

- Mbed OS + SPI + NeoPixelは動作するが，点灯異常が避けられなかった！
- ↑ Mbed OS にはシステムタイマ（`SysTick`）による1msec毎の割り込みがあるため
- Mbed classic + SPI + NeoPixelならいけた！ 

# つくったもの

[こんな感じ](https://hacklog.jp/works/51791)のものです．電気・電子系の開発もそうですがやったことのない木材加工に一番苦労しました...

![3.png](https://qiita-image-store.s3.amazonaws.com/0/156705/0a201e93-8497-b2eb-12f5-4835979ea09b.png)

マイコンを中心とした構成図は以下になります．小型BLEマイコン（Redbear BLE nano1）で全制御を行います．もともとは nano2 with Mbed OSでやり始めましたが，嵌って諦めたのが今回の話題です．

![overview.png](https://qiita-image-store.s3.amazonaws.com/0/156705/973ff8b2-5d04-4683-62a1-261ab580353f.png)

以降，思い出しながら時系列順に書いていこうと思います．

# 開発環境

- Redbear BLE nano2 + Mbed OS（最終的にはnano1 + Mbed classic）
- Mbed online compiler
    - nano2自体はサポートされていないためボードタイプは "Nordic nRF52-DK" を指定
- LEDテープ（WS2812）
- Windows 10

# 1. 混乱の始まり

- BLE通信
- 加速度センサ
- サーボ

ここまではまあまあのペースで実装できました．しかし全パーツをゲタ上に実装し動かしてみるといまいち地味だったので

**「締め切りまで時間あるし電飾でも作るか．．（電飾やったことないし）」**

ということで，スイッチサイエンスから[LEDテープ](https://www.switch-science.com/catalog/1399/)を買いさわりはじめました．

# 2. LEDテープの制御は達成，高速化を考え始める

いわゆるNeoPixel（WS2812）の制御が初めてだった私は，↓のタイミングチャートをみつつ，また[これを実現しているコード](https://github.com/lavallc/nrf51-neopixel)を見つけました．

![alt](https://cdn-learn.adafruit.com/assets/assets/000/021/333/medium640/components_neopixel-timing.png?1416522996)

これを利用することで，まず第一の目標，

**1. CPUでのNeoPixel制御** ← 達成！

は一晩で達成できました（nano2 + Mbed OS）．でもライブラリの中身をみると`nop`命令がいっぱいでCPU負荷が高そうです．さらに調べてみると [**"SPIの機能を利用して効率的にNeoPixelの制御を行う技"**](http://www.rogerclark.net/arduino-stm32-neopixels-ws2812b-using-spi-dma/)があり，[Mbed用ライブラリ](https://os.mbed.com/components/NeoPixel-LED-chain-using-high-speed-SPI/)もあるようでした．そこで新しい目標をたてました．

**1. ~~CPUでのNeoPixel制御~~** ← 達成済
**2. SPIでのNeoPixel制御** ← 新しい目標

高速制御ができるならば，ぱっぱか色を変えられるかもしれず，なんだか楽しそうな気がしていました．

# 3. 高速化のためライブラリに手を入れる

持ってきた[ライブラリ](https://os.mbed.com/components/NeoPixel-LED-chain-using-high-speed-SPI/)は残念ながら利用しているボード（Redbear BLE nano2）に対応していなかったのですが，手をいれてnano2でも利用できるようにしました（この辺から道を逸れ**手段が目標にかわっていきました**）．具体的には以下です．

- SPI周波数の調整
- SPIの転送ビット数を8（nano2だと8bitしか受け付けてくれなかった）
- Code:1/0の調整

これを使い LED 1個分の制御信号（=24bit）を1回のSPIバースト転送（`spi.transfer()`）で送ります．LEDが複数あるので，

1. spi.transfer (LED[0]むけ)
2. 転送完了のコールバック待ち
3. spi.transfer (LED[1]むけ)
4. 転送完了のコールバック待ち
5. ....

こんな感じの制御ルーチンも作成しました．

```c++:SPI転送部抜粋
// SPI初期化
PixelArray::PixelArray(PinName mosi, PinName miso, PinName sclk, ByteOrder byte_order)
    : spi_(mosi, miso, sclk), byte_order_(byte_order)
{
    // 500kHz bit encodings:
    //  '0': 00010000      mark: 0.25us
    //  '1': 01110000      mark: 0.75us
    spi_.frequency(4000000);    // 500kh x 8
    spi_.format(8,0);
    spi_.set_dma_usage((DMAUsage)DMA_USAGE_ALLWAYS);
    code0 = 0x20;
    code1 = 0x70;
    spi_busy = false;
    spi_callback.attach(this, &PixelArray::spi_event_handler);
}

// SPIバースト転送の完了通知コールバック
void PixelArray::spi_event_handler(int event){
    spi_busy = false;
}

// 24bit分のデータ転送
void PixelArray::send_pixel(Pixel& pixel)
{
    uint8_t color0 = (byte_order_ == BYTE_ORDER_RGB) ? pixel.red : pixel.green;
    uint8_t color1 = (byte_order_ == BYTE_ORDER_RGB) ? pixel.green : pixel.red;
    uint8_t color2 = pixel.blue;
    
    while(spi_busy == true){}
    spi_busy = true;

    uint8_t i = 0;
    for (; i<8; i++)
        rawbit_buffer[i] = (color0 & bitmask[i]) ? code1 : code0;
    for (; i<16; i++)
        rawbit_buffer[i] = (color1 & bitmask[i-8]) ? code1 : code0;
    for (; i<24; i++)
        rawbit_buffer[i] = (color2 & bitmask[i-16]) ? code1 : code0;

    spi_.transfer((void*)rawbit_buffer, 24, (void*)NULL, 0, spi_callback, SPI_EVENT_COMPLETE);
}
```

これにより

**1. ~~CPUでのNeoPixel制御~~** ← 達成済
**2. SPIでのNeoPixel制御** ← 達成！

できました．CPUを使った制御よりかなり高速になりました．

# 4. 異常点灯する？

完成したコードを使って長さ16のLEDテープを光らせると**異常点灯が目立ちました**．おおむね景気よく光るのですが，**時々色がずれたり**，**真っ白になったり**して電飾が台無しな感じでした．どう考えてもわからず簡易オシロを引っ張り出しての検証してみると，こんな感じの波形が得られました．

![systick_pic.png](https://qiita-image-store.s3.amazonaws.com/0/156705/f282340a-70d7-32c3-6dcc-08885a2219ff.png)

どうやらMbed OSの`SysTick`というシステムタイマが1msecごとに割り込みをかけてくるため信号転送が途切れてしまっていました．WS2812では50usec以上の間隔はreturn信号とみなされてしまうのですが割り込みによる影響でそれ以上の間隔があいていました．

割り込みが入らないように`__disable_irq()`で防止するかとも考えましたが制御ルーチンの中で割り込み信号を利用（バースト転送完了検知）していました．つまり，

- `__disable_irq()`使う ： SPIバースト転送の完了タイミングがわからず制御ルーチンが書けない
- `__disable_irq()`使わない ： `SysTick`に割り込まれてNeoPixel制御信号転送が途切れてしまう

という状況に陥いってしまいました．いろいろさがしてみるものの解決方法はわからず，結局 nano1 + Mbed classic + CPUでのNeoPixel制御でコンテストを乗り切りました（電飾の更新間隔を落としたりBLEのインターバルを長くしたりして回避）．

# 対策はある日突然に

というのが1ヶ月半ほど前で，その後もちょくちょく@tw_Inudaisukiさん相談にのっていただいていたりしていました．が，今朝丁度こんなメッセージをいただき，

![fb.png](https://qiita-image-store.s3.amazonaws.com/0/156705/c46e7466-9aa5-e084-6233-33b76f3c6045.png)

帰宅後にやってみたらみごと解決しました（Mbed classic + nano2 + SPIでのNeoPixel制御）．めでたしめでたし．

![classic.png](https://qiita-image-store.s3.amazonaws.com/0/156705/fcbdcb7a-ecff-51c3-7146-ac9ad6f1a2b3.png)

# 終わりに

「ひょっとして見落としや自身が知らない方法があったのかも」とか「実はMbed OSでもやり方があるのかも」とかもやもやする箇所はありますがひとまず解決してよかったです． @tw_Inudaisuki さん，ありがとうございます．

自分のようなハッカソンやその延長の日曜電子工作レベルならMbed classicで十分で，Mbed OSのスレッド機能とか（あまりよくわかっていない）はオーバースペック過ぎる気がしました．Mbed OS がイケてるのはどんな場面なのでしょうか．

あと，簡単そうに見えて電飾が以外に難しい（制御面でも電流面でも）ということがよくわかりました．勉強になりました．