PR

Raspberry Pi PicoでSPI通信を実装する

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

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

VSCodeの拡張機能であるPico SDKを使用するとRaspberry Pi PicoにSPI通信を実装することができます。音声合成ICであるATP3012(アクエスト)とSPI通信を行い、音声を再生させて動作確認しました。

Raspberry Pi Pico(以下Picoとする)と拡張基板のGrove Shield for Pi Picoを使用しています。

音声合成ICはATP3012R6-PU(アクエスト製:秋月電子で購入 以下ATP3012とする)を使用しています。周辺回路で音声を増幅するためにD級アンプモジュールとしてAdafruit STEMMA Speaker(秋月電子で購入)を使用しています。

本記事では、Pico SDK を使用した C/C++ プロジェクトを作成し、SPI通信を実装する方法を説明しています。下記記事で動作確認したことをPico SDKに置き換えて動作確認したものになります。

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

Picoを使用してArduino IDEやVSCodeで動作確認したことをまとめています。

Raspberry Pi Picoで学べるソフト開発

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にチェックする

プロジェクトの生成方法はRaspberry Pi Picoの開発をVSCodeで行う方法で説明していますが、SPIにチェックを入れます。プロジェクトを生成するとデフォルトでSPI関連の初期化と定義が自動で生成されます。

SPIライブラリの初期化

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

#include "hardware/spi.h"

spi_init(SPI_PORT, 1000*1000);
//spi_set_format(spi0, 8, SPI_CPOL_0, SPI_CPHA_0, SPI_MSB_FIRST);

gpio_set_function(PIN_MISO, GPIO_FUNC_SPI);
gpio_set_function(PIN_CS,   GPIO_FUNC_SIO);
gpio_set_function(PIN_SCK,  GPIO_FUNC_SPI);
gpio_set_function(PIN_MOSI, GPIO_FUNC_SPI); 

gpio_set_dir(PIN_CS, GPIO_OUT); 
gpio_put(PIN_CS, 1); //アクティブLOWなのでHighにする

初期化はデフォルトの条件から変更しない場合、修正する必要はありません。ここではSPI通信に必要な項目について説明します。

SPI通信に関連するAPIを使用するためspi.hをインクルードします。

spi_init()関数はSPI通信の初期化とボーレートを設定します。第1引数にSPI通信のハードウェアを指定します。デフォルトではspi0になります。第2引数に通信のボーレートを指定します。ボーレートは通信するスレーブの通信速度に合わせて指定します。

SPI通信のデフォルト条件はボーレート1MHz、データビット数は8ビット、SPIの動作モード0(クロック極性、クロック位相の組み合わせでモード0)、MSBファーストになっています。この条件から変更する場合はspi_set_format()関数で条件を引数に指定します。ATP3012はデフォルトの条件で使用できるため設定の変更は不要です。

gpio_set_function()関数はGPIOピンの機能切り替えで使用します。デフォルトはGPIO(DIO)になっていますが、SPIで使用する場合はそれぞれのピンに対するSPI機能を指定します。第1引数に対象のピン番号、第2引数にSPI通信の機能(GPIO_FUNC_SPI)を指定します。

11行、12行目はSPI通信のスレーブセレクトの/SSをDOで出力する設定です。ATP3012の/SSはLOWアクティブ(LOWになるとき選択中となる)なので11行目でSPI_CSを出力ピン設定し、12行目でSPI_CSをHIGHにしてスレーブセレクトをOFFにしています。

SPIの送信と実装例

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

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

for (int i = 0; i < sz - 1; i++) {
  spi_write_blocking(spi0, &msg[i], 1);
  sleep_us(30); // 必要なら少し待つ(デバイス依存)
}

SPI_SS_OFF; //SSをHレベル

SPI通信の送信手順は以下の通りです。

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

1.ATP3012はLOWアクティブなのでSPI通信の最初にLOWにしてデバイスを選択した状態にします。

2.spi_write_blocking()関数の引数に送信データを指定して送信します。第1引数にSPI通信のハードウェアを指定します。第2引数に文字列を格納した配列のアドレスを指定します。第3引数に送信するデータ数を指定します。

ATP3012は1文字ごとに20us以上の間隔をとる必要があります。例では1バイト送信した後マージンを考慮して30usのウェイトを置いています。

例では準備した文字列の配列のアドレスを更新して1バイトずつデータを送信しています。(8~11行目)

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

ATP3012へのSPI通信をDMAで置き換えようと考えていましたがハードの制約により20us以上のウェイトが必要なことからDMAを使用することができませんでした。

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

SPIの受信と実装例

SPI_SS_ON; //SSをLレベル
spi_read_blocking(spi0, 0xff, &spi_rcv,1);
sleep_us(30);
SPI_SS_OFF; //SSをHレベル

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

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

1.と3.は送信と同様です。3.はspi_read_blocking()関数でATP3012に同期クロックを供給してデータを取得します。

第1引数にSPI通信のハードウェアを指定します。第2引数にダミーデータ(0xFF:任意の値でよい)を指定します。第3引数に読み込んだデータを格納する変数のアドレスを指定します。第4引数に読み込むデータ数を指定します。

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

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

音声再生の方法

switch(mode){
  case PLAY_INIT:
    if( gpio_get(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( gpio_get(PIN_PLAY) || spi_rcv == '>' ){
        mode = PLAY_IDLE;
      }
      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して再生終了の判断をしています。

スポンサーリンク

動作確認

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

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を実装しています。

音声のボリュームはAdafruit STEMMA Speakerの可変抵抗で調整ができます。

スピーカのインピーダンスが高いものであれば直接接続することもできますが、大きな音を出したい場合はインピーダンスが低い8Ωまたは4Ωのスピーカを接続します。

インピーダンスの低いスピーカに接続する場合はAdafruit STEMMA Speakerに実装されているようなD級アンプモジュールを介して接続します。

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

電源をONすると音声の再生を待機します。ボタンを押すとパターン登録しているメッセージを再生します。再生している文字列はシリアルモニターで確認できるようにしています。

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

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

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

ソースコード全体

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

#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/spi.h"

#define SPI_PORT spi0
#define PIN_MISO 16
#define PIN_CS   17
#define PIN_SCK  18
#define PIN_MOSI 19

#define PIN_DI1 26
#define PIN_PLAY 20
#define PIN_DO1 25
//#define SPI_SS 17
#define SPI_SS_ON gpio_put(PIN_CS, 0)
#define SPI_SS_OFF gpio_put(PIN_CS, 1)

#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 = 0;//TIME_OFF;
int16_t timwait = TIME_OFF;
uint32_t beforetimCnt;
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);

int main()
{
    stdio_init_all();
    spi_init(SPI_PORT, 1000*1000);
    //spi_set_format(spi0, 8, SPI_CPOL_0, SPI_CPHA_0, SPI_MSB_FIRST);

    gpio_set_function(PIN_MISO, GPIO_FUNC_SPI);
    gpio_set_function(PIN_CS,   GPIO_FUNC_SIO);
    gpio_set_function(PIN_SCK,  GPIO_FUNC_SPI);
    gpio_set_function(PIN_MOSI, GPIO_FUNC_SPI); 
    gpio_set_dir(PIN_CS, GPIO_OUT);
    gpio_put(PIN_CS, 1);
    
    gpio_init(PIN_DO1);     // ピン初期化
    gpio_set_dir(PIN_DO1, GPIO_OUT);    //DOピン
    gpio_init(PIN_DI1);     // ピン初期化
    gpio_set_dir(PIN_DI1, GPIO_IN);    //DIピン
    gpio_init(PIN_PLAY);     // ピン初期化
    gpio_set_dir(PIN_PLAY, GPIO_IN);    //DIピン
    gpio_set_pulls(PIN_PLAY,true,false);

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

    while (true) {
      mainTimer();
      mainApp();
      DiFilter();
    }
}

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

  if( timled == TIME_UP ){
    timled = LED_ONOFF;
    gpio_put(PIN_DO1, !gpio_get(PIN_DO1));
  }

  if(difilt.di1 == 1){

    if(btnflg){
      btnflg = false;
      if( mode == PLAY_IDLE ){
        mode = PLAY_INIT;
      }
    }
  }
  else{
    btnflg = true;
  }
  
  switch(mode){
    case PLAY_INIT:
      if( gpio_get(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( gpio_get(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 (int i = 0; i < sz - 1; i++) {
    spi_write_blocking(spi0, &msg[i], 1);
    sleep_us(30); // 必要なら少し待つ(デバイス依存)
  }

  SPI_SS_OFF; //SSをHレベル
  printf("message = %s \n", msg );
}

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

  SPI_SS_ON; //SSをLレベル
  spi_read_blocking(spi0, 0xff, &spi_rcv,1);
  sleep_us(30);
  SPI_SS_OFF; //SSをHレベル
}

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

  if ( to_ms_since_boot(get_absolute_time()) - beforetimCnt >= BASE_CNT ){
    beforetimCnt = to_ms_since_boot(get_absolute_time());

    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] = gpio_get(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;
  }
}

メインのファイルの内容をコピーして置き換えることで使用できます。

プロジェクトを生成時のCMakeファイルの構成によって動作しない場合があります。CMakeLists.txtの構成で条件が不足している可能性があるため必要に応じて修正してください。

関連リンク

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

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

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

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

Raspberry Pi Picoで学べるソフト開発

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

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

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