PR

Raspberry Pi PicoでSPIライブラリを使用する

組み込みエンジニア
本記事はプロモーションが含まれています。

こんにちは、ENGかぴです。

Raspberry Pi PicoでArduinoのSPIライブラリを使用すると音声合成ICであるATP3012(アクエスト)とSPI通信を行い音声データに変換して出力することができます。SPIを実装し音声を再生して動作確認を行いました。

Raspberry Pi Pico(以下Picoとする)と拡張基板のGrove Shield for Pi Picoを使用しています。音声合成ICはATP3012R6-PU(アクエスト製:秋月電子で購入 以下ATP3012とする)を使用しています。周辺回路で音声を増幅するためにD級アンプモジュールとしてAE-TPA2006モジュール(秋月電子)を使用しています。

下記記事では他のマイコンを使って音声再生したことをまとめています。

PICマイコン(PIC16F1827)のSPI通信で音声を再生する

Seeeduino XIAOのシリアル通信で音声を再生する

トワイライト(TWELITE)の無線を使って遠隔で音声再生する。

ESP32-WROOM-32Eのシリアル通信で音声を再生する

本記事ではPicoを使ってSPIライブラリの使用方法と実装例をまとめています。

SPI通信で音声を再生する

ATP3012はローマ字で送信したシリアルデータを音声に変換する音声合成ICです。アクエスト社の音声合成ミドルウェアであるAquesTalkをArduino UNOなどで使用されているマイコンに搭載した製品です。

ATP3012シリーズを使用する

ATP3012シリーズはクリスタルなどの発振子が必要です。別シリーズのATP3011があり内蔵の発振子で音声を出力するものもあります。

2つのシリーズを比較するとATP3012シリーズの方が音声が聞き取りやすいと感じていますが、ATP3011でも音声が聞き取りにくいことはないので部品点数を減らしたいのならATP3011を選択し少しでも音声を明瞭にしたい場合はATP3012を選択するとよいと思います。

今回使用するピン(電源を除く)は以下の通りです。

ピン番号機能内容
4、5SM0D0
SM0D1
動作モードを選択します。
SMOD0及びSMOD1をGNDに接続するとSPI通信となる。
9、10XTAL1 XTAL2クリスタル発振子を接続10MHz/16MHzを実装する。クリスタルの仕様によっては12-22pFほどのコンデンサが必要となる。
11CLK16動作クロック切り替え 16MHzを使用する場合HIGHにする。
15AOUT音声出力端子。D級アンプモジュールで信号を増幅して音声再生する。
スピーカの容量(抵抗のインピーダンスが高い)によっては直付け可能
16/SSスレーブセレクトの入力
17MOSIPicoからのデータを受信
18MISOPicoにデータを送信
ATP3012F6で使用するピンと内容説明

今回はSPI通信を使用していますが、Wireライブラリを使ったI2C通信やSerialライブラリを使ったシリアル通信を選択することができます。

PR:わからないを放置せず、あなたにあった最低限のスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!

SPIライブラリの初期化

以下ではSPIクラスのメンバー関数を赤文字で表記します。

#include <SPI.h>

#define SPI_SS 17
#define SPI_SS_ON digitalWrite(SPI_SS, LOW)
#define SPI_SS_OFF digitalWrite(SPI_SS, HIGH)

SPI.hをインクルードします。SPI通信では通信相手(スレーブ)を選択して通信をを行うためスレーブセレクトの/SSをDOとして出力します。/SSはLOWアクティブ(LOWになるとき選択中となる)なのでSPI_SS_ONをdigitalWrite()でLOWを出力するように定義しています。

#include <SPI.h>

void setup() {
  pinMode(SPI_SS, OUTPUT); //SSのDOを設定
  //SPI.beginTransaction( SPISettings(0, MSBFIRST, SPI_MODE0) );
  SPI.begin();
}

ATP3012はSSがLOWアクティブなのでGPIO(17ピン)をOUTPUTにした後の初期状態をHIGHにしています。

SPI通信のデフォルトはクロック4MHz、最上位ビットから順番に送信するMSBFIRST、SPIモード0です。これらの条件を変更する場合はbeginTransaction()関数の引数にSPISettingsクラスに従った指定方法で変更後の値を入力します。ATP3012はArduinoライブラリのデフォルトに合わせて動作するため変更の必要はありません。

begin()関数でSPIの初期化を行います。

SPIの送信と実装例

const uint8_t msg1[] = {"yorosikuonegaisimasu.\r"};

msg = &msg1[0];
sz = sizeof(msg1);
//SPI通信送信の例
SPI_SS_ON; //SSをLレベル

for(uint8_t i=0; i < sz; i++){
  SPI.transfer(*msg); //対象データを送信(書き込み)
  ++msg;
}

SPI_SS_OFF; //SSをHレベル

SPIライブラリを使った送信の手順は以下の通りです。

  1. SSでデバイスを選択する
  2. transfer()関数でデータを送信
  3. SSのデバイス選択を解除

1.ATP3012はLOWアクティブなのでSPI通信の最初にLOWにしてデバイスを選択した状態にします。2.transfer()関数の引数に送信データを指定して送信します。例では準備した文字列の配列のアドレスを指定し配列のサイズ分繰り返すことでデータを送信しています。(8~11行目)

3.データ送信が終わったのでHIGHにしてデバイス選択を解除します。

PR:スキマ時間で自己啓発!スマホで学べる人気のオンライン資格講座【スタディング】まずは気になる講座を無料で体験しよう!

SPIの受信と実装例

SPI_SS_ON; //CSをLレベル
spi_rcv = SPI.transfer(0xff); //ダミーデータを送信(書き込み)
SPI_SS_OFF; //CSをHレベル

SPIライブラリを使った受信の手順は以下の通りです。

  1. SSでデバイスを選択する
  2. transfer()関数でダミーデータを送信してデータ(戻り値)を読み込む
  3. CSのデバイス選択を解除

1.と3.は送信と同様です。3.はtransfer()関数の戻り値が読み込みデータになるので引数にダミーデータ(0xFF:任意の値でよい)を指定してATP3012に同期クロックを供給してデータを取得します。

PicoからATP3012に向けて同期クロックを供給することが目的なのでダミーデータは任意の1バイトデータで問題ありません。

PR:RUNTEQ(ランテック )- マイベスト3年連続1位を獲得した実績を持つWebエンジニア養成プログラミングスクール

音声再生の方法

 switch(mode){
    case PLAY_INIT:
      if( digitalRead(PIN_PLAY)){
        timwait = TIM_PLAY;
        mode = PLAY_ON;
      }
      break;
    case PLAY_ON:
      if( timwait == TIME_UP ){
        spi_write(playno);
        timwait = TIM_PLAY;
        mode = PLAY_WAIT;
      }
      break;
    case PLAY_WAIT:
      if( timwait == TIME_UP ){
        timwait = TIM_PLAY2;
        mode = PLAY_END;
      }
      break;
    case PLAY_END:
      if( timwait == TIME_UP ){
        timwait = TIM_PLAY;

        if( digitalRead(PIN_PLAY) || spi_rcv == '>' ){
          mode = PLAY_IDLE;

          if( ++playno >= 7){
            playno = 0;
          }
        }

        spi_read();
      }
      break;
  }

音声の再生スタートから終了までをモードを切り替えて管理します。ATP3012の/PLAYピンは音声を再生していない場合はHIGHになっているので再生中でないことを確認(3行目)して次のモード(PLAY_ON)に進みます。

PLAY_ONモードでは自作の関数spi_write()でATP3012に音声の文字列のデータを送信し、次のモード(PLAY_WAIT)に進みます。

PLAY_WAITモードでは2秒のタイマをスタートして次のモード(PLAY_END)に進みます。音声の再生は数秒かかるため遅延を置いていますが、次のモードで再生の状態を確認するため省略しても問題ありません。

PLAY_ENDモードでは音声が再生中でない(再生終了)の状態を確認(25行目)して音声再生の待機モード(PLAY_IDLE)に戻ります。音声が再生されると/PLAYピンはLOWになって再生中の状態を取得することができます。

また自作のspi_read()関数で再生状態を確認し「*」であれば再生中「>」であれば再生終了となるため、双方をORして再生終了の判断をしています。

PR:(即戦力のスキルを身に着ける:DMM WEBCAMP 学習コース(はじめてのプログラミングコース))

よく使う音声記号

今回使用したアクセントや区切り記号についてまとめました。音声記号仕様はデータシートに記載されているものを一部抜粋してまとめています。

記号内容
.(0x2E)無音区間が入り、文の終わりを示す。
?(0x3F)無音区間が入り、分の終わりとなるが文末の声が高めになる。
;(0x3B)次のアクセントが高い音で始める。
/(0x2F)アクセントの区切りを指定する。
‘(0x27)アクセント記号で音の高さが「高→低」に変化する部分につける。
_(0x5F)母音のi,uが振動を伴わずに発音させる。
データシートの読み記号表に定義されている音のみ対応
ATP3012でよく使う音声記号まとめ(データシート抜粋)

音声記号は半角文字で指定します。文字の横の()内の値は記号に対応したアスキーコード(テキスト)を16進数で示したものです。

//音声パターンの例
const uint8_t msg1[] = {"yorosikuonegaisimasu.\r"};
const uint8_t msg2[] = {"konniti'wa.\r"};
const uint8_t msg3[] = {"ohayo-u.\r"};

アクセント記号を付けない場合は棒読みになってしまいますが、下手にアクセント記号を付けると棒読みよりもイントネーションがおかしくなり不自然な音声になってしまいます。

音声記号やアクセント記号をまだ使いこなせていないため、音声がぎこちない感じになっていますが、うまく使えるようになると聞き取りやすい音声パターンが作れそうです。

PR:無料トライアル実施中【PC専用】AIスライド資料作成ツールの利用:イルシル

動作確認

動作確認用の回路図
動作確認用の回路図

Picoを拡張基板のGrove Shield for Pi Picoに挿入します。SPIはデフォルトでGP16(SPI0RX SDA)、GP18(SPI0 SCK)、GP19(SPI0 TX)、GP17(SPI0 CSn)を使用します。GP17はスレーブセレクトですが、任意のDOを使用しても問題ありません。

GP20はATP3012の音声再生の状態をDIで判断します。GP26はデフォルトでA0ピンですがGrove Button(P)のDIとし、ボタンを押すと音声を切り替えて再生します。

ATP3012の15ピンのAOUT部分にノイズ除去のためにR1とC2を実装しています。C2は0.047uF程度がデータシートで回路例として示されていますが手持ちのものがなかったため0.1uFで代用しています。

音声のボリュームを調整する場合はC2よりTPA2006側に可変抵抗を入れて信号電圧を調整します。

スピーカのインピーダンスが高いものであれば直接接続することもできますが、大きな音を出したい場合はインピーダンスが低い8Ωまたは4Ωのスピーカを接続します。インピーダンスの低いスピーカに接続する場合はTPA2006のようなアンプを介して接続します。TPA2006は負荷が8Ωであれば最大で1.45Wの出力が可能なD級アンプモジュールです。

インピーダンスが低いスピーカを接続すると電流を多く引っ張る必要があるため直接接続するとマイコンに負担がかかるため注意が必要です。

電源をONすると「Raspberry Pi Pico」「Arduino IDE」をシリアルモニターに表示した後、「よろしくお願いします。」の音声を再生し、次の音声の再生を待機します。ボタンを押すとパターン登録しているメッセージを再生します。再生している文字列はシリアルモニタで確認できるようにしています。

シリアルモニターの結果
シリアルモニターの結果

ボタンを押すとシリアルモニターに再生している文字列を表示し、音声が再生されるのを確認しました。

スポンサーリンク

ソースコード全体

ソースコードは記事作成時点において動作確認できていますが、使用しているライブラリの更新により動作が保証できなくなる可能性があります。また、ソースコードを使用したことによって生じた不利益などの一切の責任を負いかねます。参考資料としてお使いください。

#include <SPI.h>

#define PIN_DI1 A0
#define PIN_PLAY 20
#define PIN_DO1 25
#define SPI_SS 17
#define SPI_SS_ON digitalWrite(SPI_SS, LOW)
#define SPI_SS_OFF digitalWrite(SPI_SS, HIGH)

#define TIME_UP 0
#define TIME_OFF -1
#define BASE_CNT 10 //10msがベースタイマとなる
#define TIM_PLAY 5
#define TIM_PLAY2 200
#define LED_ONOFF 50
#define FILT_MIN 1
#define DI_FILT_MAX 4

struct DIFILT_TYP{
  uint8_t wp;
  uint8_t buf[DI_FILT_MAX];
  uint8_t di1;
};

typedef enum{
    PLAY_IDLE = 0,
    PLAY_INIT,
    PLAY_ON,
    PLAY_WAIT,     
    PLAY_END  
}PLAY_MODE;

const uint8_t msg1[] = {"yorosikuonegaisimasu.\r"};
const uint8_t msg2[] = {"konniti'wa.\r"};
const uint8_t msg3[] = {"ohayo-u.\r"};
const uint8_t msg4[] = {"oyasuminasai.\r"};
const uint8_t msg5[] = {"itterasshai.\r"};
const uint8_t msg6[] = {"itadakimasu.\r"};
const uint8_t msg7[] = {"gotisousamadesita.\r"};

int8_t cnt10ms;
int8_t timled = LED_ONOFF;
int8_t timdifilt = TIME_OFF;
int16_t timwait = TIME_OFF;
uint32_t beforetimCnt = millis();
DIFILT_TYP difilt;
PLAY_MODE mode;
int8_t ledcnt;
bool  btnflg;
uint8_t playno;
uint8_t spi_rcv;

/* プロトコル宣言 */
void mainApp(void);
void mainTimer(void);
void DiFilter(void);
void spi_read(void);
void spi_write(uint8_t no);

void setup() {

  pinMode( PIN_DO1, OUTPUT);
  pinMode(SPI_SS, OUTPUT); //SSのDOを設定
  SPI_SS_OFF; //Hレベルにする(アクティブLなのでHを初期値とする)
  pinMode( PIN_PLAY, INPUT_PULLUP);
  pinMode( PIN_DI1, INPUT_PULLUP);

  Serial.begin(115200);
  //SPI.beginTransaction( SPISettings(0, MSBFIRST, SPI_MODE0) );
  SPI.begin();

  delay(3000);
  Serial.println("Raspberry PI Pico");
  Serial.println("Arduino IDE");

  timdifilt = FILT_MIN;

  for( uint8_t i=0; i < 10; i++ ){
    mainTimer();
    DiFilter();
    delay(10);
  }

  spi_write(0);
  playno = 1;
}

void loop() {

  mainTimer();
  mainApp();
  DiFilter();
}

/* メイン処理 */
void mainApp(void){

  if( timled == TIME_UP ){
    timled = LED_ONOFF;
    ++ledcnt;

    if( ledcnt % 2 ){
      digitalWrite(PIN_DO1, HIGH);
    }
    else{
      digitalWrite(PIN_DO1, LOW);
    }
  }

  if(difilt.di1 == 1){
    if(btnflg){
      btnflg = false;
      if( mode == PLAY_IDLE ){
        mode = PLAY_INIT;
      }
    }
  }
  else{
    btnflg = true;
  }
  
  switch(mode){
    case PLAY_INIT:
      if( digitalRead(PIN_PLAY)){
        timwait = TIM_PLAY;
        mode = PLAY_ON;
      }
      break;
    case PLAY_ON:
      if( timwait == TIME_UP ){
        spi_write(playno);
        timwait = TIM_PLAY;
        mode = PLAY_WAIT;
      }
      break;
    case PLAY_WAIT:
      if( timwait == TIME_UP ){
        timwait = TIM_PLAY2;
        mode = PLAY_END;
      }
      break;
    case PLAY_END:
      if( timwait == TIME_UP ){
        timwait = TIM_PLAY;

        if( digitalRead(PIN_PLAY) || spi_rcv == '>' ){
          mode = PLAY_IDLE;

          if( ++playno >= 7){
            playno = 0;
          }
        }

        spi_read();
      }
      break;
  }
}

/* SPI送信 */
void spi_write(uint8_t no){
  uint8_t sz=0;
  const uint8_t *msg;

  switch(no){
    case 0:
      msg = &msg1[0];
      sz = sizeof(msg1);
      break;
    case 1:
      msg = &msg2[0];
      sz = sizeof(msg2);
      break;
    case 2:
      msg = &msg3[0];
      sz = sizeof(msg3);
      break;
    case 3:
      msg = &msg4[0];
      sz = sizeof(msg4);
      break;
    case 4:
      msg = &msg5[0];
      sz = sizeof(msg5);
      break;
    case 5:
      msg = &msg6[0];
      sz = sizeof(msg6);
      break;
    case 6:
      msg = &msg7[0];
      sz = sizeof(msg7);
      break;
    default:
      msg = &msg1[0];
      sz = sizeof(msg1);
      break;
  }

  SPI_SS_ON; //SSをLレベル

  for(uint8_t i=0; i < sz; i++){
    SPI.transfer(*msg); //対象データを送信(書き込み)
    Serial.write(*msg);
    ++msg;
  }

  Serial.println("");
  SPI_SS_OFF; //SSをHレベル
}

/* SPI受信 */
void spi_read(void){

  SPI_SS_ON; //SSをLレベル
  spi_rcv = SPI.transfer(0xff); //ダミーデータを送信(書き込み)
  //Serial.write(spi_rcv);
  //Serial.println("");
  SPI_SS_OFF; //SSをHレベル
}

/* タイマ管理 */
void  mainTimer(void){

  if ( millis() - beforetimCnt >= BASE_CNT ){
    beforetimCnt = millis();

    if( timwait > TIME_UP ){
      timwait--;
    }

    if( timdifilt > TIME_UP ){
      timdifilt--;
    }

    if( timled > TIME_UP ){
      timled--;
    }

  }
}

/* DiFilter function add */
void DiFilter(void){

  if( timdifilt == TIME_UP ){
    difilt.buf[difilt.wp] = digitalRead(PIN_DI1);
    
    if( difilt.buf[0] == difilt.buf[1] &&
        difilt.buf[1] == difilt.buf[2] &&
        difilt.buf[2] == difilt.buf[3] ){ //4回一致を確認
          difilt.di1 = difilt.buf[0];
        }
    
    if( ++difilt.wp >= DI_FILT_MAX ){
      difilt.wp = 0;
    }

    timdifilt = FILT_MIN;
  }
}

関連リンク

Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。

Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方

Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方

ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方

PR:企業で求められる即戦力技術を身に付ける テックキャンプエンジニア転職

最後まで、読んでいただきありがとうございました。

タイトルとURLをコピーしました