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

組み込みエンジニア

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

音声合成ICであるATP3011(アクエスト)はSPI通信で送信した文字列を音声データに変換して出力することができます。ZigBee無線通信モジュールのトワイライト(TWELITE)で文字列を無線通信しSPIを使って音声再生しました。

音声合成ICはATP3011M6-PU(アクエスト製:秋月電子で購入)を使用しています。周辺回路で音声を増幅するためにD級アンプモジュールAE-PAM8012モジュール(秋月電子)を使用しています。下記記事では他のマイコンを使って音声再生したことをまとめています。

Arduinoライブラリと音声合成ICで音声を再生する

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

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

SPI通信で音声を再生する

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

Arduino UNOのマイコン(ATMEGA328P)を使用しているためArduino UNOの基板のマイコンを置き換えることで簡単に音声出力が確認できるのが特徴です。詳細はアクエスト社のHPをご確認ください。

AQUEST-音声合成LSI「AquesTalk pico LSI」

ATP3011シリーズを使用する

ATP3011シリーズはマイコン内蔵の発振子で動作するため外部の発振子は必要ありません。内部クロックはRCクロックであるため電源電圧の変化や温度変化の影響を受けてしまいます。UART(シリアル通信)で音声を再生する場合は正常に動作しないこともあるので注意が必要です。

データシートにはクロックのボーレートに影響されにくいI2C/SPI通信を使用することが推奨されています。

別のシリーズとしてATP3012がありクリスタルなどの発振子で動作しますが、音声のサンプリングが滑らかになるため音声の質がよくなり電源電圧の変化や温度変化の影響を受けにくくなりますが発振子など部品点数が増えてしまうデメリットがあります。

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

今回使用するピンは以下の通りです。

ピン番号機能内容
5SMOD1動作モードを選択します。
SMOD1をGNDに接続するとSPI通信(MODE3)が選択される。
12AOUT音声出力端子。D級アンプモジュールで信号を増幅して音声再生する。
スピーカの容量(インピーダンスが高い)によっては直付け可能
13/PLAY発音中にLOWになる。
音声再生待機中はHIGHになるためHIGHになるのを確認して再生スタートする。
16/SSSPIのスレーブセレクト。LOWアクティブ
17MOSISPIのマスターOUT、スレーブIN
18MISOSPIのマスターIN、スレーブOUT
19SCKSPIのシリアルクロック入力
ATP3011M6で使用するピンの機能まとめ

今回はSPIを使用しますがI2Cやシリアル通信でも音声再生することが可能です。ATP3011シリーズの内蔵クロックは使用環境の影響を受けやすいため、クロックに同期して動作するWire(I2C)やSPI通信がお勧めです。

音声再生の方法

音声再生の方法(全体構成)
音声再生の方法(全体構成)

MONOSTICKを音声再生の文字列を生成する子機とします。子機から無線通信で文字列を受信しATP3011とSPI通信を行う方を親機とします。以下ではMONOSTICKをTeraTermを使って接続することを前提とします。

Tera Termに文字列を記述して親機に文字列を無線通信します。ATP3011は制御文字のCRを受付けるまでは文字列をセットし続けます。TeraTerm上でリターンキーを押すと制御文字であるCRが無線通信されるため音声再生を開始します。

テキストファイルなどで文字列をあらかじめ準備しておきコピー&ペーストでTeraTermに貼り付けると楽です。トワイライトの無線通信はパケット当たり最大で90バイト(可能な限り少ない方が良い)となるため注意が必要です。

ATP3011は127バイトまでの指定となるため文字列が127バイト以下になるように音声データを構成する必要があります。長い文字列を再生したい場合は127バイト以下の音声を連続して発生させるようにします。

トワイライトの文字列を生成し無線通信する方法についてはトワイライトの無線通信の使い方で説明しています。

よく使う音声記号

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

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

音声記号は半角文字で指定します。文字の横の()内の値は記号に対応したアスキーコード(テキスト)を16進数で示したものです。文字列の例を以下に示します。CRはリターンキーを押すと入力できます。

よろしくおねがいします。:yorosiku;onegaisima_su. CR
元気ですか?:genki+desuka? CR
こんばんは。:konban’wa. CR

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

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

トワイライトでSPIのAPIを使用する

SPIのAPIを実装しやすいようにモノワイヤレス社がMWSTAGE開発環境においてAPIを準備しており使用することで簡単にSPIが実装できます。

モノワイヤレス社ーSPI関係のAPIについて

SPI.begin()を最初の初期化時にコールする必要があります。begin()の引数を指定するとSPIのバススピードやスレーブセレクトピンやSPIモードを指定できます。

void setup() {
    SPI.begin(0, SPISettings(1000000, SPI_CONF::MSBFIRST, SPI_CONF::SPI_MODE3));
}

ATP3011のデータシートによると最大クロックは1MHz・SPIモードはMode1またはMode3でありMSBファーストの8ビット対応です。上の例ではSPIのbegin()メソッドでクロック周波数は1MHzを指定、MSBファーストを指定、Mode3を指定しています。

トワイライト(親機)はATP3011に対して文字列を送信するマスターとなるためSPI通信では書き込みを使用します。トワイライトの開発環境ではSPI読み書き用のヘルパークラス版が実装されておりArduinoライブラリのようなbeginTransmisson()、endTransmission()などを呼びだしを気にする必要がありません。

void user_spi_write(uint8_t *reg_data, uint16_t len){

    if(auto&& trs = SPI.get_rwer()){ //SPI使用の準備
        for(uint16_t i=0; i < len; i++){
            trs << reg_data[i]; //受信したデータをSPIで送信する
        }
    }
}

get_rwer()メソッドで送信データの準備を行います。trsに書き込むデータをセットしていきます。無線で受信したデータをATP3011にSPI通信で送信します。

トワイライトのSPI読み書き用のヘルパークラス版はif文から抜けたときオブジェクトは破棄されるためソースコードの記述漏れなどが防ぐことができるメリットがあります。

トワイライトの無線通信の使い方

トワイライトの子機はTeraTermで入力した文字を無線通信し、親機は無線通信を受信するとSPI通信を使用してATP3011に音声再生のコマンドを送信します。子機と親機の動きを説明します。

子機の動作

void loop() { //子機側(文字列を準備)

    if( Serial.available()){ //モニターで入力した文字があるか
        buffer[rxcnt++] = Serial.read(); //文字を受信してセット
    }
}

TeraTermで文字を入力するとSerial通信のデータが受信します。available()メソッドでデータが取得できていることを確認しread()メソッドで受信データを読み込みます。受信データを一時的buffer[]に格納して無線通信用のデータを準備します。

MWX_APIRET transmit(void) {

    if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
	pkt << tx_addr(0x00)  
            << tx_retry(0x3) 
	    << tx_packet_delay(0,0,2);
 
	pack_bytes(pkt.get_payload(), make_pair(APP_FOURCHAR, 3));//ヘッダー
        for(int i = 0; i < rxcnt; i++){
            pack_bytes(pkt.get_payload(),buffer[i]);
	    Serial << buffer[i]; //送ったデータを確認する
        }
        rxcnt = 0; //buffer[]の保存位置を初期化
	return pkt.transmit(); //無線通信開始
    }
    return MWX_APIRET(false, 0);
}

transmit()関数は無線通信の条件を先に設定しpack_bytes()で送信データをパケット化します。最初に音声再生用のアクトである確認を親機で行うためヘッダーをセットします。

続けてシリアル通信で取得したデータであるbuffer[]をデータサイズ分セットします。transmit()メソッドで無線通信を開始します。

親機の動作

void receive() {
    uint8_t sz;
    uint8_t data;
    char chk[4];

    auto&& rx = the_twelite.receiver.read();
    auto&& np = expand_bytes(rx.get_payload().begin(), rx.get_payload().end()
               ,make_pair(chk,3)); //ヘッダー分の3バイトを先に取得
		
    if( strncmp(chk, APP_CHAR, 3)){ return; } // check header
    sz = rx.get_length() - 3;
    for( uint8_t i=0; i < sz ;i++){
	np = expand_bytes(np,rx.get_payload().end(),data);
	spiData.dat[spiData.wp] = data; //SPIで送信するデータ
	++spiData.wp; //保存場所を更新
    }
}

子機から受信したデータを解析してSPI通信用のデータを作成します。expand_bytes()によってペイロード(受信したデータのパケット)のデータを獲得します。最初にヘッダーの確認を行うため3バイト分のデータを取得します。

子機と親機でヘッダーを同一の文字列にしておくことで音声再生用以外の関係のない無線データを無効にすることができます。

ヘッダーが一致していることを確認した後は残りのサイズ分を取得してSPI通信用のデータとしてセットします。

共通事項

トワイライトで無線通信するための共通事項をまとめています。

#include <NWK_SIMPLE>

const uint32_t APP_ID = 0x1234abcd;
const uint8_t CHANNEL = 13;
const char APP_CHAR[] = "ATP";

void setup() {
    the_twelite
        << TWENET::appid(APP_ID)    //アプリケーションID
        << TWENET::channel(CHANNEL) //チャンネル設定
        << TWENET::rx_when_idle();  //無線データを受信するために必要

    auto&& nwksmpl = the_twelite.network.use<NWK_SIMPLE>();
    nwksmpl << NWK_SIMPLE::logical_id(0x00); //0x00にすると親機設定
    the_twelite.begin();
}

無線通信のライブラリを使用するためNWK_SIMPLEをインクルードする必要があります。APP_IDとチャンネル及びアクトの判断に使用するヘッダーを子機と親機で共通にします。

the_tweliteにおいてアプリケーションID・チャンネル設定を行います。無線を受信する場合はrx_when_idle()を設定する必要があります。rx_when_idle()は送信専用の場合は不要です。

ネットワークのlogical_idに0x00をセットすることで親機として動作します。子機の場合は0xFEにするとアドレスの区別をしない子機になります。begin()メソッドで無線通信がスタートします。

動作確認

動作確認用の回路図(親機)
動作確認用の回路図(親機)

親機はトワイライトとATP3011及びD級アンプ(スピーカー含む)を組み合わせたものです。子機から受信した文字列をATP3011にSPI通信で送信し音声を再生します。 SPIスレーブ選択ピンの/SSは常に使用するためGNDに接続しています 。

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

音声のボリュームを調整したい場合はC2よりPAM8012モジュール側に可変抵抗を入れて信号電圧を調整するとよいでしょう。

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

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

インピーダンスが低いスピーカで大きな音を出したい場合はアンプを使用してATP3011の音声出力を信号増幅する必要があります。今回はPAM8012を使って信号増幅しています。PAM8012は負荷が8Ωであれば最大で2Wの出力が可能なD級アンプモジュールです。

Tera Termでポートを接続しシリアルポート設定を行います。トワイライトのシリアル通信のスピードはデフォルトで115200bpsであるためスピード(E)を115200に変更します。他のデータ(D)やパリティ(A)はデフォルトのままであれば変更の必要はありません。

動作確認(Tera Term)
動作確認(Tera Term)

Tera Termで文字列を入力しリターンキーを押すと音声が再生されるのを確認しました。テキストファイルにあらかじめ文字列を作成しておきコピー&ペーストすると任意の音声を再生しやすくなります。

遠隔で任意の音声が流せるのは面白いと感じましたが遠くに配置すると音声が聞こえなくなるため無事に再生できているかわからないケースもありました。入力ミスによる変な音声も含めて楽しめました。

ソースコード全体

以下のソースコードはコンパイルして動作確認をしております。コメントなど細かな部分で間違っていたりやライブラリの更新などにより動作しなくなったりする可能性があります。参考としてお使いいただければと思います。

MONOSTICK(子機)のソースコード:

#include <TWELITE>
#include <NWK_SIMPLE>

#define TIME_UP 0
#define TIME_OFF -1
#define ZIG_WAIT_MAX 1000

const uint32_t APP_ID = 0x1234abcd; // application ID
const uint8_t CHANNEL = 13; // channel
const char APP_CHAR[] = "ATP";
uint8_t buffer[100];
uint8_t rxcnt;
int16_t timzigwait = TIME_OFF;
/*** function prototype */
MWX_APIRET transmit(void);

/*** the setup procedure (called on boot) */
void setup() {

    the_twelite
        << TWENET::appid(APP_ID)    
	<< TWENET::channel(CHANNEL) 
	<< TWENET::rx_when_idle();

    auto&& nwksmpl = the_twelite.network.use<NWK_SIMPLE>();
    nwksmpl << NWK_SIMPLE::logical_id(0xFE);
    the_twelite.begin(); // start twelite!
}
/*** the loop procedure (called every event) */
void loop() {

    if( TickTimer.available()){
        if(timzigwait > TIME_UP){
            --timzigwait;
        }
    }

    if( Serial.available()){
        timzigwait = ZIG_WAIT_MAX;
        buffer[rxcnt++] = Serial.read();
    }

    if( timzigwait == TIME_UP ){
        timzigwait = TIME_OFF;
        transmit();
    }
}
/* transmit a packet */
MWX_APIRET transmit(void) {

    if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
        pkt << tx_addr(0x00)  
	    << tx_retry(0x3) 
	    << tx_packet_delay(0,0,2);
 
        pack_bytes(pkt.get_payload(), make_pair(APP_CHAR, 3));//ヘッダー
        for(int i = 0; i < rxcnt; i++){
            pack_bytes(pkt.get_payload(),buffer[i]);
	    Serial << buffer[i]; //モニター表示用
        }
        rxcnt = 0;
	return pkt.transmit();
    }
	return MWX_APIRET(false, 0);
}

TWELITE DIP(親機)のソースコード:

#include <TWELITE>
#include <NWK_SIMPLE>

typedef struct{
    uint8_t wp;
    uint8_t dat[100];
}SEND_MNG;

const uint8_t DI_PLAY = mwx::PIN_DIGITAL::DIO12;
const uint32_t APP_ID = 0x1234abcd; // application ID
const uint8_t CHANNEL = 13; // channel
const char APP_CHAR[] = "ATP";
SEND_MNG spiData;
/*** function prototype */
void receive();
void user_spi_write(uint8_t *reg_data, uint16_t len);

/*** the setup procedure (called on boot) */
void setup() {

    pinMode(DI_PLAY,INPUT_PULLUP);
    SPI.begin(0, SPISettings(1000000, SPI_CONF::MSBFIRST, SPI_CONF::SPI_MODE3));

    the_twelite
	<< TWENET::appid(APP_ID)    
	<< TWENET::channel(CHANNEL) 
	<< TWENET::rx_when_idle();

    auto&& nwksmpl = the_twelite.network.use<NWK_SIMPLE>();
    nwksmpl << NWK_SIMPLE::logical_id(0x00);
    the_twelite.begin(); // start twelite!
}
/*** the loop procedure (called1 every event) */
void loop() {

    while (the_twelite.receiver.available()) {
        receive();
    }

    if( digitalRead(DI_PLAY) == HIGH){
	if( spiData.wp >= 1 ){
            user_spi_write(&spiData.dat[0],spiData.wp);
	}
    }
}
/* 無線通信を受信して処理する */
void receive() {
    uint8_t sz;
    uint8_t data;
    char chk[4];

    auto&& rx = the_twelite.receiver.read();
    auto&& np = expand_bytes(rx.get_payload().begin(), rx.get_payload().end()
		,make_pair(chk,3)); 
		
    if( strncmp(chk, APP_CHAR, 3)){ return; } // check header
			
	sz = rx.get_length() - 3;
	Serial << (int)sz; //モニター表示用
	for( uint8_t i=0; i < sz ;i++){
	    np = expand_bytes(np,rx.get_payload().end(),data);
	    Serial << data; //モニター表示用
	    spiData.dat[spiData.wp] = data; //SPIで送信するデータ
	    ++spiData.wp; //保存場所を更新
    }
}
/* SPI通信(書き込み ATP3011へのコマンド) */
void user_spi_write(uint8_t *reg_data, uint16_t len){

    if(auto&& trs = SPI.get_rwer()){
        for(uint16_t i=0; i < len; i++){
            trs << reg_data[i];
            Serial << reg_data[i] << mwx::crlf << mwx::flush;
        }
	spiData.wp = 0;
    }else{
        Serial << "data_NG" << mwx::crlf;      
    }
}

関連リンク

エナジーハーベストはIoT社会実現のために必要な技術であると考えています。電池レスでIoTモジュールが起動できるようになれば応用範囲が広がることが期待できます。エナジーハーベストの検討の一環として検討しているトワイライト(TWELITE)に関する記事をまとめています。

エナジーハーベスト技術がIoT社会実現に必須になりえる理由

トワイライト(TWELITE)のソフト開発と無線通信でできること

テックジム-将来のためにプログラミングを学ぶ

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

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