はじめに
CH32V003マイコンを使ってRGB LEDであるWS2812B(通称NeoPixel)を制御してみます。
MounRiver Studio Ⅱ(公式IDE)+公式SDKを利用し、LED制御にはコミュニティライブラリ ch32funのWS2812B制御用ライブラリー(SPI+DMA利用)を利用します。
利用機材
- マイコン:CH32V003F4P6(SSOP20ピン)(秋月電子・AliExpress)
- デバッガー・書き込み機:WCH-LinkE(秋月電子・AliExpress)
- RGB LED:WeAct Studio製 WS2812B搭載基板×1
扱いやすい1チップ搭載の基板タイプを使用

開発環境
- IDE:MounRiver Studio Ⅱ(VSCodeベースの公式開発環境)
- SDK:公式SDK(MRS2同梱)
配線
マイコン内臓のSPIペリフェラルを利用してLED制御を行うため、制御信号の出力にはSPIのMOSIピン(PC6)を利用します。
接続方法:
- 制御信号入力(DIN)→MOSI PC6(16番ピン)
- 電源→5V電源(WCH-LinkEより)
- GND→GND

これをミニブレッドボード上に実装すると次のようになります。右手前にあるのがRGB LEDのモジュールです。

NeoPixelの制御信号について
NeoPixel系LEDは独自のタイミング信号で色データを受信します。データの0/1をHigh/Low信号の長さで表現されます。


最小で400 ns(2.5 MHz)のHigh信号を送る必要があるためタイミングの制約が厳しく、マイコンでこの信号を出すためには特殊な実装が必要となります。
マイコンでNeoPixel系LEDを制御する方法には大きく2パターンあります。
- GPIOの直接制御(BitBang方式):NOP命令でタイミング調整してプログラム的にGPIOをオンオフ操作する
- 任意のGPIOピンが利用できる
- 0.1 μs オーダーなのでDelay関数が基本使えず、NOP命令の回数によるタイミング微調整が必要となる(オシロやロジアナの出番)
- プログラム的に操作するので信号を送っている間はCPUを占有する。タイミングがズレてしまうのでその間割り込みは使えない
- マイコンの内蔵ペリフェラル(タイマーやSPI)を利用
SPIの場合は必要な信号が出力されるようにビットを並べた特殊な出力データを用意し、DMA(Direct Memory Access)機能を利用してペリフェラルのデータレジスターに逐次転送することで信号を生成する。
- 信号生成をペリフェラルにオフロードするのでCPUを占有しない
- ペリフェラルの出力に対応する特定のピンのみ利用可能
- DMAがほぼ必須なため実装が複雑になる。DMAを使わないとプログラムでデータを逐次入れる必要があり利点が薄れる。
今回は後者の、CH32V003マイコンのSPIペリフェラルとDMA機能を利用して制御信号を生成する手法を採用します。
幸いライブラリーとしてCH32VのオープンソースSDKであるch32funのWS2812B制御ライブラリーがあるので、これを流用します。
NeoPixel LED制御ライブラリーの準備
このリンクのws2812b_dma_spi_led_driver.h
をプロジェクトのUserフォルダーに配置します。このライブラリーは単一のヘッダーファイルで構成されており、main.cなどでインクルードして利用します。
公式SDKとch32funでは一部レジスターの定義が異なっているみたいなので、コードの次の箇所を変更する必要があります。
233行目
< GPIOC->CFGLR |= (GPIO_Speed_10MHz | GPIO_CNF_OUT_PP_AF)<<(4*6);
---
> GPIOC->CFGLR |= (GPIO_Speed_10MHz | GPIO_Mode_AF_PP)<<(4*6);
244行目
< SPI1->CTLR1 |= CTLR1_SPE_Set;
---
> SPI1->CTLR1 |= SPI_CTLR1_SPE;
プログラム
以下のサンプルプログラムを見つつ、動作確認用のプログラムを作成します。
ライブラリーのインクルード
このライブラリーを使う場所(main関数から使う場合はmain.c)の上の方で次のように書いてライブラリーのヘッダーファイルをインクルードします。
#include "debug.h" // 元のこのインクルード文の下に書く
#define WS2812DMA_IMPLEMENTATION
#define WSGRB // WS2812Bの場合(RGBデータの送信順)
#include "ws2812b_dma_spi_led_driver.h"
LED色設定用コールバック関数
ライブラリー内の割り込み関数から呼ばれるコールバック関数WS2812BLEDCallback
を追加します。この関数では、LEDの位置ledno
を受け取り、RGBの順に下位ビットから8bitずつ色の値が入った24bitの数値を返すように実装します。
今回は複雑なことをせずに全てのLEDで単一のグローバル変数ledVal
を返すだけにします。このグローバル変数は次に書くmain関数から書き換えるようにします。
static volatile uint32_t ledVal = 0; // 色の設定値(グローバル変数)
// 色設定用のコールバック関数
uint32_t WS2812BLEDCallback(int ledno)
{
return ledVal; // 24bitのRGB値を返す
}
メイン処理
メイン処理をmain関数に書きます。WS2812BDMAInit
関数で初期化して、WS2812BDMAStart
関数を呼び出すことでLEDがledVal
で設定した色に光ります。
デバッグ機能を利用する前提なので、不要であればprintfの記述を消してください。
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
SystemCoreClockUpdate();
Delay_Init();
#if (SDI_PRINT == SDI_PR_OPEN)
SDI_Printf_Enable();
#else
USART_Printf_Init(115200);
#endif
printf("SystemClk:%d\r\n",SystemCoreClock);
printf( "ChipID:%08x\r\n", DBGMCU_GetCHIPID() );
// 初期化関数
WS2812BDMAInit();
// 点灯パターン
static const uint32_t ledPatterns[] = {
0x00000000, // Off
0x000000FF, // Red
0x0000FF00, // Green
0x00FF0000, // Blue
0x0000FFFF, // R + G = Yellow
0x00FFFF00, // G + B = Cyan
0x00FF00FF, // R + B = Magenta
};
// 各色を順番に表示
for (unsigned int i = 0; i < sizeof(ledPatterns) / sizeof(ledPatterns[0]); i++) {
// グローバル変数に色の値を代入
ledVal = ledPatterns[i];
// デバッグ出力
printf("LED val: %08X\r\n", ledVal);
// DMA転送スタート(1=LED数)
WS2812BDMAStart(1);
// 待機
Delay_Ms(500);
}
}
動作確認
このプログラムを動作した際の様子です。(全部0xFFで最大に光らせているので眩しいです)
プログラムの実行により、赤→緑→青→黄→シアン→マゼンタの順番に点灯しているのが分かると思います。

まとめ
CH32V003マイコンを使ってWS2312B LEDを制御することができました。今回は1つしかLEDを繋いでいませんが、このライブラリーを使えばLEDテープなどの複数LEDも制御可能です。
このマイコンで何を作ろうかはまだ思いついていないですが、次はI2Cあたりを使ってみたいと思います。
参考サイト
- ch32funのWS2812B制御ライブラリー(SPI+DMA版):https://github.com/cnlohr/ch32fun/blob/master/extralibs/ws2812b_dma_spi_led_driver.h
- ↑のライブラリーを使ったサンプルプロジェクト:https://github.com/cnlohr/ch32fun/tree/master/examples/ws2812bdemo
- ch32funのWS2812B制御ライブラリー(GPIO操作版):https://github.com/cnlohr/ch32fun/blob/master/extralibs/ws2812b_simple.h
- WS2812Bのデータシート(Adafruit社のページより)※リビジョン違いがあり、新しいモジュールは信号タイミングなどが異なる:https://cdn-shop.adafruit.com/datasheets/WS2812B.pdf