以下の内容はhttps://negligible.hatenablog.com/entry/2025/12/24/125704より取得しました。


NeoPixelを光らせる ~ Raspberry Pi + Python編 ~

はじめに

前回PICマイコンによるポータブル気圧計をプリント基板化し,その際に,普通のLEDの代わりにNeoPixelを用いました。その下準備として,筆者はRaspberry PiPICマイコンのそれぞれによるNeoPixelの制御方法(光らせ方)をいろいろと試行錯誤しました。本記事ではまずRaspberry PipigpioライブラリPython)を用いたNeoPixelの制御方法について書いて参ります。PICマイコンについては次回に委ねたいと存じます。

図2に完成イメージとしてRaspberry Pi Zero 2 Wにて12個のNeoPixel (WS2812B)を制御した様子を示します。

図1: Raspberry Pi Zero 2 WでNeoPixelを制御

目次

NeoPixelとは

普通のフルカラーLEDとNeoPixel

フルカラーLEDとは,1つのパッケージに赤(R),緑(G),青(B)の3色のLEDを集積した発光デバイスです。例えばPWM (pulse-width modulation)によって(R, G, B)の輝度の割合を変えることで,様々な色を表現できます。図2は,筆者がPICマイコンを久しぶりに使ってみた際の練習として作った,フルカラーLEDをPIC12F1840で制御する小さな基板です。

図2: PIC12F1840で制御する(普通の)フルカラーLED
フルカラーLEDはR, G, BそれぞれのLEDを制御するため,PICなどのマイコンから3本のピン(およびR, G, Bに共通のアノードまたはカソード)で制御する必要があります。また,R, G, Bそれぞれに電流制限抵抗が必要です(図2でフルカラーLEDの近くに3本並んでいるのが分かります)。

一方,中国のWorldSemi社は2010年代前半あたりからNeoPixelと呼ばれるフルカラーLEDを発売しています。NeoPixelはR, G, Bの3色のLEDと同じパッケージ内に小さなマイコンを内蔵しており,シリアル通信(専用プロトコル)にて(R, G, B)の輝度をそれぞれ8 bitの分解能で設定できるという特長があります。さらに,いくつものNeoPixelをデイジーチェーン接続することが可能です。電飾をはじめとする各種の電子工作,多色表示を活用したインジケータ,そしてもちろんフルカラーLEDパネルなどの様々な製品にも近年広く使われています。しかも,秋月電子通商では2個で50円という安価で入手可能です。また,図2の普通のフルカラーLEDで必要だった電流制限抵抗も不要です。

NeoPixelのシリアル通信とその難しさ

図3にNeoPixelの1種,WS2812B*1のデータシートから引用したピン配置を示します。1番ピンはVDD,2番ピンはDOUT,すなわちデイジーチェーンに用いるデータの出力端子,3番ピンはVSS (=GND),4番ピンはDIN,すなわちデータの入力端子です。

図3: NeoPixel (WS2812B)のピン配置(WS2812Bのデータシートより引用)
前述のようにNeoPixelはシリアル通信にてR, G, Bの輝度を8 bitずつ,合計24 bitを受け取りますが,問題はその信号の速度です。図4にWS2812Bのデータシートより引用したNeoPixel 1個分のデータ構成とデイジーチェーンの様子を示します。NeoPixel 1個に対しては,G, R, Bの順でそれぞれ8 bitずつのデータ,合計24 bitを渡します。

NeoPixelをデイジーチェーンした場合の通信は,次のように説明できます。先頭のNeoPixelが最初のG, R, Bの24 bitを受け取った場合,自分の輝度を変更します。先頭のNeoPixelがもう1組の24 bitのデータを受け取ると,自分の輝度を変更することなく,2番目のNeoPixelにそのデータを渡します。先頭のNeoPixelがさらにもう1組の24 bitのデータを受け取ると,自分の輝度を変更することなく,それをそのまま2番目のNeoPixelに渡し,2番目のNeoPixelはやはり自分の輝度を変更することなく,3番目のNeoPixelにデータを渡します。このように,各NeoPixelは受け取ったデータの最初の24 bitを受け取って,それ以降を隣のNeoPixelに順次流していくように通信します。データが流れてこなくなって一定時間(> 280 μs)が経過すると,変更された輝度が実際にLEDに反映されます。

図4: NeoPixel 1個分のデータ構成とデイジーチェーン(WS2812Bのデータシートより引用)
さて,このNeoPixelを何百個も並べて電飾やディスプレイを構成しようとした場合,1つのNeoPixelあたりの処理時間が長いと全体の表示を更新する時間がどんどん長くなってしまいます。そこで,NeoPixelでは図4の24 bitを通信する際に,1つの符号の時間を2 μs以下としています。具体的には図5に示す通りです。符号「0」に対しては,4番ピン(DIN)に対してHighを220 ns ~ 380 ns,Lowを580 ns ~ 1 μsとします。符号「1」に対してはHighを580 ns ~ 1 μs,Lowを同じく580 ns ~ 1 μsとします。これによって,24 bitのデータが1つのNeoPixelを通過する時間は概ね2 μs × 24 = 48 μs以下となります。
図5: NeoPixelのシリアル通信における符号「0」と符号「1」(WS2812Bのデータシートより引用)
何となくですが,赤外線リモコンのプロトコルに似ていますね

改めて考えてみると,符号「0」の場合に必要となる220 ns ~ 380 nsというパルス幅はなかなか高速ですね。もちろん,100 MHz超で動いているマイコンをベアメタルで制御している場合は大したことはないかもしれませんが,サブマイクロ秒でRaspberry PiのGPIOを,それもPythonから制御しようとすると難しさが生じます。Raspberry PiのGPIO制御ライブラリの決定版(?)とも言えるpigpioライブラリにおいても,パルス生成機能(waveform機能)は1 μs単位でしか制御できません。いったいどうすれば良いのでしょうか…?

SPIのMOSIを利用したNeoPixelの制御

NeoPixelの1 bitをSPIの1 byteで模擬

ウェブやXをいろいろと検索してみると,図6のようにRaspberry PiのSPI (serial peripheral interface)を利用する方法がありそうだと分かりました(図6はNeoPixelが1個の場合です)。しかし,pigpioライブラリを使用する例を見つけられませんでしたので,自分で作ることにしました。

図6: Raspberry PiのSPI (MOSI)を1個のNeoPixelに接続
結論から言うと,SPIのボーレートを4 Mbaud(またはそれ以上の適切な値)に設定し,MOSI (master-out-slave-in) = GPIO10から,「NeoPixelから見て符号『0』や符号『1』に見えるようなビット列を出力する」という方法でNeoPixelを制御できることが分かりました。図7にSPIのビット列の例を示します。
図7: Raspberry PiのSPI (4 Mbaud)で符号「0」と符号「1」を生成
NeoPixelが「0」だと認識するようなRaspberry PiからのSPIのビット列として0b10000000 (= 0x80)を,NeoPixelが「1」だと認識するようなRaspberry PiからのSPIのビット列として0b11100000 (=0xe0)を用いることとしました。この場合,図7に示すように,符号「0」のHighは250 ns,Lowは1.75 μsとなります。同様に,符号「1」のHighは750 ns,Lowは1.25 μsとなります。いずれも,図5の表中に示したLowの時間を逸脱しますが,Highの時間が範囲内に収まっていれば問題なくNeoPixelを光らせることができることを確認しました*2。ボーレートやHigh (= 1)とするSPIのビット数を調整することで,もう少し厳格に制御できる可能性はあります。

以上の説明は1 bitの送出方法のみを述べておりますが,これを各色8 bitの合計24 bit,またそれをデイジーチェーンされたNeoPixelの個数分だけ繰り返すことによって,各NeoPixelの色や輝度を個別に制御可能です。

1個のNeoPixelを光らせるプログラム

以上を踏まえて,まずは1個のNeoPixelを光らせる最小のプログラムを書いてみると,以下のようになりました。pigpiodデーモンのハンドラをpiとして得てから,7行目で所望の(R, G, B)の輝度をcolというリストに格納します。ここではcol = [127, 0, 96],すなわち赤紫としました。

import pigpio

# Get pigpio handle
pi = pigpio.pi()

# Set color of the NeoPixel in [R, G, B] format
col = [127, 0, 96]

# Build SPI frame
frame = []
CODE_0 = 0b10000000
CODE_1 = 0b11100000

# Green
for i in range(8):
    frame.append(CODE_1 if col[1] & (0b10000000 >> i) != 0 else CODE_0)

# Red
for i in range(8):
    frame.append(CODE_1 if col[0] & (0b10000000 >> i) != 0 else CODE_0)

# Blue
for i in range(8):
    frame.append(CODE_1 if col[2] & (0b10000000 >> i) != 0 else CODE_0)

# Send SPI frame
h = pi.spi_open(0, 40000000, 3)
pi.spi_write(h, frame)
pi.spi_close(h)

# Stop pigpio
pi.stop()

pigpioライブラリのSPIでは,フレームをリストに格納してから,spi_writeメソッドにてMOSIから送出します。10行目でframeという空のリストを作り,11,12行目でCODE_0,CODE_1という変数にそれぞれ前節で述べた0b10000000と0b11100000を入れておきます。 14行目以降で,リストcolの2つ目の要素col[1](= 緑(G)の輝度)の上位ビットから下位ビットに向かって,それが1であるか0であるかを確認しながら,リストframeにCODE_1またはCODE_0を追加していきます。これを同様に赤(R),青(B)に対しても繰り返します。これによって,1個のNeoPixelに対する24 bitのデータを模擬するSPIのフレームが完成します。

26行目でspi_openメソッドにてSPIのハンドラhを得てから(ボーレートを4 Mbaudに設定する),spi_writeメソッドにてリストframeに格納されたデータをMOSI (GPIO10)から送出します。直後にspi_closeメソッドでSPIの通信を閉じます。最後にpigpiodデーモンへのハンドラを停止(stop)します。

図8: 1個のNeoPixelをRaspberry PiのSPIで光らせた様子

図8に示すように,このプログラムを実行すると1個のNeoPixelを光らせることができます。複数のNeoPixelがデイジーチェーンされている場合,先頭のNeoPixelのみが点灯します。

複数のNeoPixelを光らせたい場合,2番目のNeoPixel以降のG, R, Bの輝度をリストframeに順次appendし,SPIのフレームをどんどん長くすることで対応できそうです。

自作Pythonモジュールのソースコード

上の原理に基づいたpigpioライブラリを用いたPythonモジュールを作りました。短いですので以下にソースコード全文を示します。このプログラムはモジュールとして他のプログラムからimportすることができます。NeoPixelというクラスを定義しましたので,デイジーチェーンされた複数のNeoPixelの列を1つのオブジェクト(インスタンス)として操作できます。

multiRGBメソッドはcolorsという2次元リスト(リストのリスト)に格納されているそれぞれのNeoPixelの(R, G, B)の輝度を確認して順次SPIのフレームを生成し,MOSIから送出します。

turnOffメソッドは変数numに格納された個数分のNeoPixelに輝度(0, 0, 0)を送出して消灯します。

#-------------------------------------------------------
# neopixel.py
# A module to control NeoPixel full-color LEDs
# (c) 2025 @RR_Inyo
# Released under the MIT license
# https://opensource.org/licenses/mit-license.php
#-------------------------------------------------------

import time
import pigpio

class NeoPixel:
    # Class variables
    __SPI_CHANNEL   = 0
    __BAUD_RATE     = 4000000
    __SPI_MODE      = 3
    __CODE_0        = 0b10000000
    __CODE_1        = 0b11100000

    # Constructor
    def __init__(self, pi):
        self.__pi = pi

    # Destructor
    def __del__(self):
        self.__pi.stop()

    # Send SPI frame to multiple daisy-chained NeoPixels
    def multiRGB(self, colors):
        # Create frame data
        frame = []

        for color in colors:
            # Green
            for i in range(8):
                frame.append(NeoPixel.__CODE_1 if color[1] & (0b10000000 >> i) != 0 else NeoPixel.__CODE_0)

            # Red
            for i in range(8):
                frame.append(NeoPixel.__CODE_1 if color[0] & (0b10000000 >> i) != 0 else NeoPixel.__CODE_0)

            # Blue
            for i in range(8):
                frame.append(NeoPixel.__CODE_1 if color[2] & (0b10000000 >> i) != 0 else NeoPixel.__CODE_0)

        # Send to SPI (MOSI)
        self.__h = self.__pi.spi_open(0, NeoPixel.__BAUD_RATE, NeoPixel.__SPI_MODE)
        self.__pi.spi_write(self.__h, frame)
        self.__pi.spi_close(self.__h)
        time.sleep(0.001)

    def turnOff(self, num):
        dark = [[0, 0, 0]] * num
        self.multiRGB(dark)

# Test bench to control a 12-LED NeoPixel ring
if __name__ == '__main__':

    # Triangular wave
    def triangle(u):
        if u < 4:
            y = u
        else:
            y = 8 - u if u <= 8 else 0

        return y

    # Get pigpio handle
    pi = pigpio.pi()

    # Get NeoPixel handle
    neopixel = NeoPixel(pi)

    # Create color pattern
    colors = []
    for i in range(12):
        colors.append([triangle(i) * 20, triangle((i + 4) % 12) * 20, triangle((i + 8) % 12) * 20])

    try:

        # Send to 12-LED ring and rotating colors
        for i in range(1000):
            neopixel.multiRGB(colors)
            colors = [colors[-1]] + colors[:-1]
            time.sleep(0.5 - i / 2000)

        # End test
        neopixel.turnOff(12)
        pi.stop()

    except:
        # Close pigpio
        neopixel.turnOff(12)
        pi.stop()

このプログラムにはif __name__ == '__main__':以降のテストベンチを設けており,単体で実行した場合は12個のNeoPixelにて虹色を表示し,それを順次回転していくようなプログラムとなります。これを実行した様子が図9となります(図1の再掲)。

図9: Raspberry Pi Zero 2 WでNeoPixelを制御(図1の再掲)

このモジュールを外部のプログラムから呼び,3個のNeoPixelをR, G, Bそれぞれの最大輝度で点灯するシンプルなプログラムを作ると下記となります。

import pigpio
import neopixel

pi = pigpio.pi()
led = neopixel.NeoPixel(pi)
led.multiRGB([[255, 0, 0], [0, 255, 0], [0, 0, 255]])

これを実行すると,図10のように点灯します。

図10: 3個のNeoPixelを自作モジュール(neopixel.py)で制御した様子

波形の測定〔追記: 2025-12-25〕

Raspberry Pi Zero 2 WのMOSI (GPIO10)からNeoPixelに流れている波形をオシロスコープ(OWON PDS5022S)にて測定しました。図11に測定波形を示します。

図11: Raspberry Pi Zero 2 WのMOSI (GPIO10)の出力波形
図11はトリガの掛かった瞬間から4 bit分のみを捉えているため,恐らく緑(G)の最初の4 bitと思われます。一見して短いパルスと長いパルスが見えますが,短いパルスが符号「0」のHigh,長いパルスが符号「1」のHighと思われます。概略ですが,表1に示すように測定したパルス幅をまとめました。
表1: 符号「0」と符号「1」のパルス幅

符号HIGHLOW
データシート測定結果データシート測定結果
“0”220 ~ 380 ns200 ns580 ns ~ 1 μs1.1 μs
“1”580 ns ~ 1 μs500 ns580 ns ~ 1 μs800 ns

符号「0」については,Highのパルス幅がデータシートより僅かに短く,Lowのパルス幅がデータシートより僅かに長くなっています。符号「1」のついては,Highのパルス幅がデータシートより僅かに短く,Lowのパルス幅がデータシートに合致している状態になっておりました。それぞれの符号に用いたSPIのビット列としては,符号「0」に0b11000000,符号「1」に0b11110000とした方が適切かもしれませんね。もしくはボーレートとセットでの見直しが必要になるかもしれませんね。

まとめ

以上,Raspberry PiのpigpioライブラリとPythonによってNeoPixelを制御(光らせる)方法について述べました。pigpioライブラリでのパルス生成機能(waveform機能)では1 μs単位でしかパルスを制御できませんが,NeoPixelはサブマイクロ秒でのパルス幅制御を必要とします。そこで,ボーレートを適切に設定(本記事では4 Mbaud)したSPIを用いてMOSI (GPIO10)からビット列を送出し,NeoPixelから見て符号「0」や符号「1」に見えるようなパルスを生成することによって,Raspberry PiからNeoPixelを制御できることを説明しました。本記事の内容はpigpioを用いたC言語C++言語での制御にも応用できると考えます。

NeoPixelと本当のSPIデバイス(例えばカラー液晶モジュールなど)を共存したい場合に問題ないかなど,用途と環境によってはさらなる調査が必要かもしれませんね(チップセレクトがある普通のSPIデバイスであればデバイス側は大丈夫な気がしますが,デバイスに送出したSPIのフレームでNeoPixelが不用意に点灯することがあるかもしれません…)。

次回はPICマイコンにてNeoPixelを点灯制御する方法(SPIやCLC (configurable logic cell)などは用いず,ただI/Oポートを直接叩く方法です💧)について書きたいと思います。

*1:他にもWS2815BやWS2313Bなどがあります。

*2:Highの時間も少し余裕はあるようです。SPIのボーレートを2.5 Mbaudとしても正常に点灯することを確認しています。




以上の内容はhttps://negligible.hatenablog.com/entry/2025/12/24/125704より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14