こんにちは、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で動作確認したことをまとめています。
SPI通信で音声を再生する
ATP3012はローマ字で送信したシリアルデータを音声に変換する音声合成ICです。アクエスト社の音声合成ミドルウェアであるAquesTalkをArduino UNOなどで使用されているマイコンに搭載した製品です。
ATP3012シリーズを使用する
ATP3012シリーズはクリスタルなどの発振子が必要です。別シリーズのATP3011があり内蔵の発振子で音声を出力するものもあります。
2つのシリーズを比較するとATP3012シリーズの方が明瞭であったと思いますが、大きな差を感じませんでした。ATP3011でも音声が聞き取りにくいことはないので部品点数を減らしたいのならATP3011を選択し、音声を明瞭にしたい場合はATP3012を選択するとよいと思います。
今回使用するピン(電源を除く)は以下の通りです。
| ピン番号 | 機能 | 内容 |
|---|---|---|
| 4、5 | SM0D0 SM0D1 | 動作モードを選択します。 SMOD0及びSMOD1をGNDに接続するとSPI通信となる。 |
| 9、10 | XTAL1 XTAL2 | クリスタル発振子を接続10MHz/16MHzを実装する。クリスタルの仕様によっては12-22pFほどのコンデンサが必要となる。 |
| 11 | CLK16 | 動作クロック切り替え 16MHzを使用する場合HIGHにする。 |
| 15 | AOUT | 音声出力端子。D級アンプモジュールで信号を増幅して音声再生する。 スピーカの容量(抵抗のインピーダンスが高い)によっては直付け可能 |
| 16 | /SS | スレーブセレクトの入力 |
| 17 | MOSI | Picoからのデータを受信 |
| 18 | MISO | Picoにデータを送信 |
今回はSPI通信を使用していますが、Wireライブラリを使ったI2C通信やSerialライブラリを使ったシリアル通信を選択することができます。
PR:わからないを放置せず、あなたにあった最低限のスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!
プロジェクトの生成

プロジェクトの生成方法は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通信の送信手順は以下の通りです。
- SSでデバイスを選択する
- spi_write_blocking()関数でデータを送信
- SSのデバイス選択を解除
1.ATP3012はLOWアクティブなのでSPI通信の最初にLOWにしてデバイスを選択した状態にします。
2.spi_write_blocking()関数の引数に送信データを指定して送信します。第1引数にSPI通信のハードウェアを指定します。第2引数に文字列を格納した配列のアドレスを指定します。第3引数に送信するデータ数を指定します。
例では準備した文字列の配列のアドレスを更新して1バイトずつデータを送信しています。(8~11行目)
3.データ送信が終わったのでHIGHにしてデバイス選択を解除します。
PR:スキマ時間で自己啓発!スマホで学べる人気のオンライン資格講座【スタディング】まずは気になる講座を無料で体験しよう!
SPIの受信と実装例
SPI_SS_ON; //SSをLレベル
spi_read_blocking(spi0, 0xff, &spi_rcv,1);
sleep_us(30);
SPI_SS_OFF; //SSをHレベル
SPIライブラリを使った受信の手順は以下の通りです。
- SSでデバイスを選択する
- spi_read_blocking()関数でダミーデータを送信してデータを読み込む
- SSのデバイス選択を解除
1.と3.は送信と同様です。3.はspi_read_blocking()関数でATP3012に同期クロックを供給してデータを取得します。
第1引数にSPI通信のハードウェアを指定します。第2引数にダミーデータ(0xFF:任意の値でよい)を指定します。第3引数に読み込んだデータを格納する変数のアドレスを指定します。第4引数に読み込むデータ数を指定します。
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で学べるソフト開発と標準ライブラリの使い方
PR:企業で求められる即戦力技術を身に付ける テックキャンプエンジニア転職
最後まで、読んでいただきありがとうございました。

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