Raspberry Pi Picoで音声を鳴らす・光らせる

今回は久しぶりに作品の解説記事ではなく、シンプルにモジュールの使い方の紹介記事です。Raspberry Pi PicoでDFPlayerとNeoPixel (WS2812)を使う方法について、厳密に正しいかどうかはさておき、とりあえずこれで動いたよ、というのを備忘録として残しておきます。

前置き

Raspberry Pi Pico (以下、Pico) は今回初めて使用しました。Picoは低価格でパワフルではあるものの、正直なところ、これまで使ってきたArduino系ボードやESP32系ボードがあれば、作品作りに関しては事足りています。それでも今回使ってみようと思った理由は、「Pythonでの開発をやってみたかったから」に他なりません。

ESP32系のボードでもPythonを使った開発はやろうと思えばできるのは承知しているのですが、何となくハードルが高そうな印象があり、一方Picoの方はPythonでの開発の方が主流で環境構築もラクそうだったので、自分の武器を増やす意味も兼ねてPicoに手を出してみました。

なぜPythonでの開発をしようと思ったかですが、本業の方ではC系の言語よりもPythonを扱えることの方が大事で、また最近は本業の方で自分がコーディングする機会がほぼゼロになってしまったので、趣味のコーディング時間の方でPythonを扱うようにして、少しでも本業向けのスキルの低下を抑制したいなと思った次第です。

大部分の人にはどうでも良い前置きはこれぐらいにして、早速本題に移りますが、以下ではPicoのMicroPython開発環境は既に構築済みとします。環境構築についてはネット上にもたくさん情報があると思うので、ここでは割愛させて頂きます。

① DFPlayerを扱う

Picoでの音声再生にあたり、使用するのは電子工作では定番(?)のMP3プレイヤー、”DFPlayer”です。

Arduinoでこれを使用していたときにはライブラリに頼りっぱなしだったのですが、Pico向けの公式ライブラリのようなものは特に見当たらなかったので、真面目にシリアル通信コマンドを打ち込んで鳴らすことにしました。

作業にあたり、こちらの記事を大いに参考にさせて頂きつつ、シリアル通信におけるチェックサムの処理が省略されているところなどは少し気になったので、Arduinoのライブラリの実装なども参考にしながら、以下のような感じで実装してみました。

import machine
import utime

pin_btn = machine.Pin(16, machine.Pin.IN, machine.Pin.PULL_UP)
uart = machine.UART(1, baudrate=9600, tx=machine.Pin(4), rx=machine.Pin(5))

ON  = 0 # LOW
OFF = 1 # HIGH

btn = OFF
prev_btn = OFF

# DFPlayer シリアル通信データフォーマット (以下、各1byte)
#  1. スタートバイト (0x7E固定)
#  2. バージョン情報 (0xFF固定で良い)
#  3. データ長 (スタートバイト、チェックサム(上位、下位)、エンドバイトを除くので、ほぼ0x06固定)
#  4. コマンド (よく使うものは、0x06:ボリューム指定(0〜30)、0x12: "mp3"フォルダ内の曲番号指定再生、0x0E:曲停止)
#  5. フィードバック (0x00:フィードバック不要、0x01:フィードバック必要)
#  6. パラメータ1 (2byteの上位)
#  7. パラメータ2 (2byteの下位)
#  8. チェックサム1 (2byteの上位)
#  9. チェックサム2 (2byteの下位)
# 10. エンドバイト (0xEF固定)
# ※ 8,9のチェックサムは、0 - (2.〜7.の和)で算出されるマイナス値の2byteの2の補数表現
# 例) [7E, FF, 06, 09, 00, 00, 04, FE, EE, EF]のとき、チェックサムの[FE, EE]の部分は以下のように計算される。
#     [FF, 06, 09, 00, 00, 04]の合算値が274なので、チェックサムの値は 0 - 274 = -274。
#     274は2進数表現だと 0000 0001 0001 0010 なので、反転させると 1111 1110 1110 1101 (=1の補数)
#     2の補数はこれに1を加えるので 1111 1110 1110 1110 = 0xFEEE となる。

def calc_checksum(sum_data):
    temp = ~sum_data + 1 # 2の補数の計算(ビットを反転させて1を足す)
    h_byte = (temp & 0xFF00) >> 8
    l_byte = temp & 0x00FF
    return h_byte, l_byte

def send_data(command, param):
    ver      = 0xFF
    d_len    = 0x06
    feedback = 0x00
    param1  = (param & 0xFF00) >> 8
    param2  = param & 0x00FF
    cs1, cs2 = calc_checksum(ver + d_len + command + feedback + param1 + param2)
    sdata = bytearray([0x7E, ver, d_len, command, feedback, param1, param2, cs1, cs2, 0xEF])
    uart.write(sdata)
    #print(sdata)

def init_sd():
    send_data(0x3F, 0x02) # 0x02でSDカードのみ有効化
    utime.sleep_ms(1000)

def set_volume(volume):
    send_data(0x06, volume)
    utime.sleep_ms(500)

def play_sound(num):
    # "mp3"という名称のフォルダ内に保存された、"0001.mp3"のような名称のファイルを再生
    print("Play {}".format(num))
    send_data(0x12, num)
    #utime.sleep_ms(500)

###################################

init_sd()
print("Ready.")
set_volume(15)

num= 0

while True:
    btn = pin_btn.value()

    if prev_btn == OFF and btn == ON:
        num += 1
        if num >= 7:
            num = 1
        play_sound(num)

    prev_btn = btn

    utime.sleep_ms(1)

機能としては自分が最低限必要なものに絞っていて、

  • 初期化
  • 音量設定
  • SDカードのルート直下にある”mp3″フォルダの中に置いてある”0001.mp3″のような名称のファイルの番号を指定して再生

しか実装していません。とは言え、こちらのシリアル通信のコマンド仕様を見れば、「停止する」「1曲戻る」「1曲送る」などのコマンドを追加するのは容易だと思います。

ただ、この仕様書が曲者で、チェックサムの計算方法を書いてくれていないし、書いているところも計算結果が間違っている、となっていて、何が正しいのかを確認するのが大変でした。。。このあたりを調べた結論は、コードの中にコメントとして残しています。

あと、肝心なのは、PicoとDFPlayerの配線です。以下のようになります。動作確認用に、スイッチONで音を鳴らすためのボタンも配線しています。

ポイントは、「DFPlayerのVccには、3.3V(OUT)ではなくVBUSの方から電力を供給する」ということです。自分は初め3.3V(OUT)の方に繋いで動作させようとしていたのですが、音が鳴るときもあれば鳴らないときもある、という不安定な状態で、プログラムが悪いのか何なのかよくわからない状況が長く続いてしまいました。過去にArduinoでDFPlayerを扱っていたときも、Arduinoの3.3V端子から電力を供給すると動作が安定しない、ということがあったのを思い出して、電力の供給元をVBUSの方に切り替えると、安定して音声が鳴るようになりました。以下のような感じです。

② NeoPixel (WS2812) を扱う

続いてフルカラーLEDの定番、NeoPixel (WS2812)を使います。これについては、Picoの公式ドキュメントに使い方が書かれていたり、ライブラリもいくつかバリエーションが存在しているようです。いくつかパラパラと見てみたのですが、パッと見た感じで使いやすそうな、こちらのライブラリを使用することにしました。このライブラリの詳しい使い方についてはこちらに記載がありますので、必要に応じてご参照ください。なお、このリンク先にある”XIAO RP2040″は、Picoと同じチップを積んでいるのでPicoと同じように開発できるのですが、GPIOの本数をだいぶ減らしている分、Picoに比べてかなりコンパクトになっています。とにかくスペースが最優先、という案件であれば、XIAO RP2040はかなり良い選択肢になると思います。

配線はこんな感じです。

プログラムはこんな感じです。少しだけ変えていますが、先のサイトのサンプルほぼそのままです。

# NeoPixelのライブラリは以下を使用
# https://files.seeedstudio.com/wiki/XIAO-RP2040/img/micropython/ws2812.py
# "ws2812.py"をRaspberry Pi Picoに保存する必要あり

from ws2812 import WS2812
import utime
import machine
pin_led = 3

BLACK = (0, 0, 0)
RED = (255, 0, 0)
YELLOW = (255, 150, 0)
GREEN = (0, 255, 0)
CYAN = (0, 255, 255)
BLUE = (0, 0, 255)
PURPLE = (180, 0, 255)
WHITE = (255, 255, 255)
COLORS = (BLACK, RED, YELLOW, GREEN, CYAN, BLUE, PURPLE, WHITE)

led = WS2812(pin_led,1) # WS2812(pin_num,led_count)

while True:
    print("Beautiful color")
    for color in COLORS:
        led.pixels_set(0, color)
        led.pixels_show()
        utime.sleep(0.2)

動作はこんな感じです。先のDFPlayerを繋いだものにそのまま付け足したので配線が少々ごちゃついていますが、ご了承ください。

 

以上、 Raspberry Pi Picoでの音声の鳴らし方&フルカラーLEDの光らせ方のご紹介でした。Picoで自分がこれまでやってきたような「光る!鳴る!」系の玩具工作をしようと思うと、まずこの2つができることが必須要件になるので、とりあえずこれだけは自分の備忘録として残しておきました。Pythonで玩具改造したい、という他の方にも役立つかもしれません。