非接触給電モジュールを試してみた! [SPRESENSE]
waves ワイヤレス 給電 モジュール 12V→5V 2A 無線 充電 無線送電
- 出版社/メーカー: waves(ウェイブス)
- メディア:
コイルが付いているほうが受電側で小ぶりのボードが送信側になります。送信側のボードは9V-12Vを供給します。受電側は出力が5Vで最大2Aまで電流を流せるようです。
安定化電源で12Vを給電し、まず何も考えずに低消費電力のSPRESENSEがどれくらいの距離まで動くか試してみました。推奨距離は3mm~6mmなのですが、以外にも1.5cmくらいまでLED点灯のプログラムが動きました。
距離と送信電力の相関が気になったので、受電モジュールがどれくらいの距離でどれくらいの電圧を出力しているのか測定をしてみました。測定の際には、送電・受電側の間に厚紙を積み重ねていくことで距離をとりました。
測定の結果、次のようなグラフが得られました。8.5mmを越えたあたりから電圧がドロップしていき、14.5mmあたりで急激にドロップしています。SPRESENSEはメインボードだけなら3.4Vくらいでもかろうじて動くようですね。
この結果から、多少厚めの障壁があっても、SPRESENSEなら十分動かすことができることがわかりました。どんなアプリケーションが作れるか考えてみたいと思います。(^^)/~
SONY SPRESENSE メインボード CXD5602PWBMAIN1
- 出版社/メーカー: スプレッセンス(Spresense)
- メディア: Tools & Hardware
四月の電子工作「SPRESENSEでデジタルエフェクターを作ろう!」ーSPRESENSEアンプ編ー [SPRESENSE]
前回との変更点が一点あります。前回、電源をデジタル電源3.3Vを利用しましたが、使用したオペアンプは1Vから動作可能ということに気づいたので、マイクバイアス電源(2V)を使うことにしました。これでマイク端子だけでエレキギターの音を取り込めます。
実装してSPRESENSEに装着をしてみました。プリアンプらしくボリュームはマイク入力へのレベル調整のためにつけています。これによってデジタル処理ではないですがオーバードライブが可能になります。
■ プリアンプの動作を確認する
何もせずにバイアス電源をあたってみると、なんと0V。あれ?ここって2V出てるんじゃなかったっけ?と思って慌てたのですが、サンプルのレコーダーアプリをインストールしてみたら、2Vが出力されていました。さすが省電力を意識した設計になっています。
プリアンプにギタープラグを入れて、マイク入力の波形をオシロスコープであたってみました。無事にアンプ出力が出ていることが確認できました。
■ SPRESENSEでエレキギターの音を出す
ハードウェアの準備は全てできましたので、いよいよソフトウェアの出番です。ギターの音をそのままスルーで出力するようにプログラミングします。なるべく遅延が小さくなるようにフレームはできるだけ小さくしてみました。
#include <MediaRecorder.h> #include <MediaPlayer.h> #include <OutputMixer.h> #include <MemoryUtil.h> MediaRecorder *theRecorder; MediaPlayer *thePlayer; OutputMixer *theMixer; const int32_t s_buffer_size = 4096*sizeof(int16_t); uint8_t s_buffer[s_buffer_size]; uint8_t out_buffer[s_buffer_size]; int request_size = 128 * sizeof(int16_t); int read_size; /* callback functions */ static void attention_callback(const ErrorAttentionParam *atprm) {} static bool mediarecorder_event_callback(AsRecorderEvent event, uint32_t result, uint32_t sub_result) { return true; } static void outputmixer_event_callback(MsgQueId requester_dtq, MsgType reply_of, AsOutputMixDoneParam *done_param) { } static void outmixer_rendering_callback(int32_t identifier, bool is_end) { AsRequestNextParam next; next.type = (!is_end) ? AsNextNormalRequest : AsNextStopResRequest; AS_RequestNextPlayerProcess(AS_PLAYER_ID_0, &next); return; } static bool mediaplayer_event_callback(AsPlayerEvent event, uint32_t result, uint32_t sub_result) { return true; } void mediaplayer_decode_callback(AsPcmDataParam pcm_param) { theMixer->sendData(OutputMixer0, outmixer_rendering_callback, pcm_param); } /* setup function from here */ void setup() { initMemoryPools(); createStaticPools(MEM_LAYOUT_RECORDINGPLAYER); Serial.println("Init Audio Library"); theRecorder = MediaRecorder::getInstance(); thePlayer = MediaPlayer::getInstance(); theMixer = OutputMixer::getInstance(); theRecorder->begin(); thePlayer->begin(); Serial.println("initialization MediaRecorder, MediaPlayer and OutputMixer"); theMixer->activateBaseband(); thePlayer->create(MediaPlayer::Player0, attention_callback); theMixer->create(attention_callback); theRecorder->activate(AS_SETRECDR_STS_INPUTDEVICE_MIC, mediarecorder_event_callback); thePlayer->activate(MediaPlayer::Player0, AS_SETPLAYER_OUTPUTDEVICE_SPHP, mediaplayer_event_callback); theMixer->activate(OutputMixer0, outputmixer_event_callback); usleep(100000); Serial.println("Initialize Recorder"); theRecorder->init(AS_CODECTYPE_LPCM, AS_CHANNEL_MONO, AS_SAMPLINGRATE_16000, AS_BITLENGTH_16, 0); Serial.println("Initialize Player"); thePlayer->init(MediaPlayer::Player0, AS_CODECTYPE_WAV, "/mnt/sd0/BIN", AS_SAMPLINGRATE_16000, AS_CHANNEL_MONO); theMixer->setVolume(-100, -100, -100); Serial.println("Start Recorder"); theRecorder->start(); /* fill the sound data to FIFO before starting the player */ int frame_size = 0; while (frame_size < request_size) { err_t err = theRecorder->readFrames(s_buffer, request_size, &read_size); if (err != 0) { Serial.println("Error! readFrames: read_size = " + String(read_size)); } if (read_size > 0) { memset(out_buffer, 0, s_buffer_size); memcpy(out_buffer, s_buffer, read_size); thePlayer->writeFrames(MediaPlayer::Player0, out_buffer, read_size); frame_size += read_size; } } Serial.println("Start Player"); thePlayer->start(MediaPlayer::Player0, mediaplayer_decode_callback); } /* loop function from here */ void loop() { int ret = theRecorder->readFrames(s_buffer, request_size, &read_size); if (ret != 0) { Serial.println("Error! readFrames: read_size = " + String(read_size)); } if (read_size > 0) { memset(out_buffer, 0, s_buffer_size); memcpy(out_buffer, s_buffer, read_size); thePlayer->writeFrames(MediaPlayer::Player0, out_buffer, read_size); } }
さて、実際のサウンドを聞いてみましょう。マイク入力のレンジ 0.9Vpp に対して 1.0Vpp 程度で入力してオーバードライブをかけてみました。それらしく聞こえるものですね。
今日の Maker Faire Kyoto 2021 に出しそびれてしまった作品
— よしのたろう 純国産ボード”スプレッセンス?エスプレゼンス?”応援団長(仮) (@Taro_Yoshino) May 1, 2021
「SPRESENSE DIGITAL EFFECTER」
なかなか香ばしい作品になったのでご賞味ください
私の下手くそな素人ギターも良い味わいを加えてくれています ? pic.twitter.com/QGHqHBzwg6
聞いていただいてわかるように遅延量が大きいです。いろいろエフェクトを考える前に、この遅延をなんとかしたいところですが、これ以上短くできる気がしない…
(´・ω・`)
JRC(新日本無線) 1回路 低雑音 オペアンプ NJM5534DD (5個セット)
- 出版社/メーカー: JRC(新日本無線)
- メディア:
SONY SPRESENSE メインボード CXD5602PWBMAIN1
- 出版社/メーカー: スプレッセンス(Spresense)
- メディア: Tools & Hardware
SONY SPRESENSE 拡張ボード CXD5602PWBEXT1
- 出版社/メーカー: スプレッセンス(Spresense)
- メディア: Tools & Hardware
四月の電子工作「SPRESENSEでデジタルエフェクターを作ろう!」ープリアンプの製作編ー [SPRESENSE]
Twitterで電子バイオリンをSPRESENSEで作ろうとチャレンジされている”愚鈍クレメルさん”に影響され、"SPRESENSEでエレキギターのデジタルエフェクター作れんじゃね?" と思い試してみることにしました。
V2.3にしてようやく弾けるようになりました。ちょっと感動。次回はSpresenseにつないでエレキにすることをめざします!
— 愚鈍クレメル (@Gudon_Kremer) March 28, 2021
雑な製作ですが、本物のバイオリンのすごいところがちょっとわかってきたような。
フルバージョン(6分ちょっと)はこちら:https://t.co/1uDx5W3gEh pic.twitter.com/TEQT2uE4Wx
今月は日数もあまりないので、能書き抜きでいきなり本題。まずエレキギターの出力をSPRESENSEに電気信号として入力してやるためのプリアンプを作っちゃろう!ということでやってみました。
■ プリアンプの基本設計
ギターのピックアップの出力は微小なので、プリアンプで増幅してあげる必要があります。プリアンプには安直にオペアンプを使うことにしました。アナログ回路素人でもオペアンプならあまり間違いはありません。
□ C1とR1の定数の設定(ハイパスフィルターの構成)
単一電源を扱えるように電源電圧の半分の電圧のバイアスを加えています。C1のカップリング用の電解コンデンサは、オペアンプ側ではバイアスされるので極性に注意します。またここの回路はハイパスフィルタになるので抵抗と容量を人の可聴領域の下限の20Hzに近づけます。
□ C2とR2の定数の設定(ローパスフィルターの構成)
負帰還の抵抗とコンデンサは増幅作用をするとともに、ローパスフィルタを構成します。人の可聴範囲の上限20kHzに近づけます。増幅率も勘案しR1とR2の比率を決めていきます。
□ C4とR5の定数の設定
SPRESENSEのマイク入力は交流なので、ここでカップリングコンデンサを挟みます。このときも極性に注意します。あと、この回路はハイバスフィルターを構成しますので、20Hz以上の音をカットしないように決めていきます。
接地抵抗のボリュームはレベル調整のために設けています。データシートによるとSPRESENSEのマイク入力は 0.9Vpp なので電圧を調整するためです。
■ ブレッドボードで試してみる
実際にブレッドボードに組んでみました。小さなブレッドボードしかなかったので、ほぼゴミのようにしか見えませんが、きちんと動いてくれているようです。^^;
ギターの弦を弾いてオシロスコープで波形を見てみました。きちんと増幅されていることが確認できました。
エレキギターの出力をそのまま測定してみた結果はこんな感じ。ほぼさざ波ですね。
次はエレキギターの音がSPRESENSEで録音できるか試してみたいと思います。ちなみに実験に使っているエレキギターは娘のものを拝借しています。
(^^)/~
「SPRESENSEでデジタルエフェクターを作ろう!」ープリアンプの製作編
「SPRESENSEでデジタルエフェクターを作ろう!」ー低遅延入出力の実現ー
「SPRESENSEでデジタルエフェクターを作ろう!」ーコンプレッサーとディストーションの実現ー
「SPRESENSEでデジタルエフェクターを作ろう!」ー空間系エフェクターの実現ー
「SPRESENSEでデジタルエフェクターを作ろう!」ーモジュレーション系エフェクターの実現ー
JRC(新日本無線) 1回路 低雑音 オペアンプ NJM5534DD (5個セット)
- 出版社/メーカー: JRC(新日本無線)
- メディア:
SONY SPRESENSE メインボード CXD5602PWBMAIN1
- 出版社/メーカー: スプレッセンス(Spresense)
- メディア: Tools & Hardware
SONY SPRESENSE 拡張ボード CXD5602PWBEXT1
- 出版社/メーカー: スプレッセンス(Spresense)
- メディア: Tools & Hardware
三月の電子工作「SPRESENSEでサーマルカメラを作ろう!」ー人感センサーで撮影するサーマルカメラ完成ー [SPRESENSE]
で、かなり遅れてしまったのですがついに完成しました。人勧センサーで撮影する低消費電力サーマルカメラです。なんとこれだけ色々ついていて待ち受け時の消費電流は10uAもありません。
ご覧になってわかるように、SPRESENSE本体の筐体は、一月の電子工作「タイムラプスカメラ」で制作した”プレッセ君”の流用です。では、制作過程をご紹介しましょう。
■ 人感センサー付きサーマルカメラのカメラ部のメカ
カメラ部は、SPRESENSEカメラとサーマルセンサアレイAMG8833、人感センサーをまとめて一つのパッケージにすることしました。
カメラ部の3Dデザインはかなり困難を極めました。AMG8833ボードの外形図、人感センサーボードの外形図がないことに加え、SPRESENSEカメラとAMG8833の光軸をなるべく近づけるために目合わせで調整しなければならず、3回作り直すことになりました。
面倒なことに、AMG8833用の電源とI2Cライン、人感センサー用の電源と割り込み用GPIOの合計7本の線が必要です。電源とGNDを共通にするために、間にボードを挟むことにしました。
メインボードからの電源は、3.3V電源でなく EXT_VDD(4.0V)を使っています(3.3VはSDカードが使用)。AMG8833の消費電力は10mA程度、人感センサーは10uA程度なので、電圧を下げるために間に100?を挟んでおきました。
このボードを格納するためのランドセルを作りカメラ部に接着剤でつけて背負わせることにしました。実は、このためのケーブルを作るのが一番面倒だった…。
■ 人感センサー付きサーマルカメラのSPRESENSE部
SPRESENSE本体は、一月に作ったタイムラプスカメラの筐体を流用しますが、ケーブルを通すための穴が必要です。穴をあけて新しい3Dプリンタで出力しました。
カメラ部をホールドするためのアームが必要なのですが、カメラ部のランドセルが干渉するので曲げることにしました。ロボットのようでちょっとかわいいです。
カメラ部と SPRESENSEメインボードのケーブルは次のように結線されています。
■ SPRESENSEカメラの出力をBMPで保存する
今までSPRESENSEカメラはJPEGの出力を使っていたのでそのまま保存できたのですが、今回はAMG8833のサーマル画像を重畳しなければならないので、ビットマップでそのまま保存したい。ということで、16bit の BMPで保存するようにしました。
#include <SPI.h> #include <SPISD.h> #include <Camera.h> SpiSDClass SD(SPI5); static int gCounter = 0; /* Bitmap definition of RGB565 */ #define IMG_WIDTH (320) #define IMG_HEIGHT (240) #define BITS_PER_PIXEL (16) #define BI_BITFIELD (3) #define IMG_SIZE (IMG_WIDTH*IMG_HEIGHT*(BITS_PER_PIXEL/8)) #define HEADER_SIZE (54) #define INFO_HEADER_SIZE (40) #define MASK_SIZE (12) /* Bitmap Header parameters */ uint16_t bfType = 0x4D42; /* "BM" */ uint32_t bfSize = IMG_SIZE + HEADER_SIZE + MASK_SIZE; /* image size + 54 + 12*/ uint16_t bfReserved1 = 0; uint16_t bfReserved2 = 0; uint32_t bfOffBits = HEADER_SIZE + MASK_SIZE; uint32_t biSize = INFO_HEADER_SIZE; uint32_t biWidth = IMG_WIDTH; uint32_t biHeight = IMG_HEIGHT; uint16_t biPlanes = 1; uint16_t biBitCount = BITS_PER_PIXEL; uint32_t biCompression = BI_BITFIELD; uint32_t biSizeImage = IMG_SIZE; uint32_t biXPelsPerMeter = 4724; /* dummy */ uint32_t biYPelsPerMeter = 4724; /* dummy */ uint32_t biClrUsed = 0; uint32_t biClrImportant = 0; uint32_t biRmask = 0x0000f800; /* 16bit mask */ uint32_t biGmask = 0x000007e0; /* 16bit mask */ uint32_t biBmask = 0x0000001f; /* 16bit mask */ uint8_t WORD[2]; uint8_t* swap16(uint16_t word16) { WORD[0] = (uint8_t)(word16 & 0x00ff); WORD[1] = (uint8_t)((word16 & 0xff00) >> 8); return WORD; } uint8_t DWORD[4]; uint8_t* swap32(uint32_t dword32) { DWORD[0] = (uint8_t)(dword32 & 0x000000ff); DWORD[1] = (uint8_t)((dword32 & 0x0000ff00) >> 8); DWORD[2] = (uint8_t)((dword32 & 0x00ff0000) >> 16); DWORD[3] = (uint8_t)((dword32 & 0xff000000) >> 24); return DWORD; } uint8_t BMP_HEADER[HEADER_SIZE+MASK_SIZE]; void make_bmp_header() { uint8_t* word16; uint8_t* word32; int n = 0; word16 = swap16(bfType); /* "BM" */ for (int i = 0; i < 2; ++i) { BMP_HEADER[n++] = word16[i]; } word32 = swap32(bfSize); /* File Size */ for (int i = 0; i < 4; ++i) { BMP_HEADER[n++] = word32[i]; } word16 = swap16(bfReserved1); /* Reserved */ for (int i = 0; i < 2; ++i) { BMP_HEADER[n++] = word16[i]; } word16 = swap16(bfReserved2); /* Reserved */ for (int i = 0; i < 2; ++i) { BMP_HEADER[n++] = word16[i]; } word32 = swap32(bfOffBits); /* Offset to image data */ for (int i = 0; i < 4; ++i) { BMP_HEADER[n++] = word32[i]; } word32 = swap32(biSize); /* Bitmap info structure size (40) */ for (int i = 0; i < 4; ++i) { BMP_HEADER[n++] = word32[i]; } word32 = swap32(biWidth); /* Image width */ for (int i = 0; i < 4; ++i) { BMP_HEADER[n++] = word32[i]; } word32 = swap32(biHeight); /* Image height */ for (int i = 0; i < 4; ++i) { BMP_HEADER[n++] = word32[i]; } word16 = swap16(biPlanes); /* Image plane (almost 1) */ for (int i = 0; i < 2; ++i) { BMP_HEADER[n++] = word16[i]; } word16 = swap16(biBitCount); /* Pixel per bits */ for (int i = 0; i < 2; ++i) { BMP_HEADER[n++] = word16[i]; } word32 = swap32(biCompression); /* Complession type */ for (int i = 0; i < 4; ++i) { BMP_HEADER[n++] = word32[i]; } word32 = swap32(biSizeImage); /* Image size */ for (int i = 0; i < 4; ++i) { BMP_HEADER[n++] = word32[i]; } word32 = swap32(biXPelsPerMeter); /* Resolution (dummy) */ for (int i = 0; i < 4; ++i) { BMP_HEADER[n++] = word32[i]; } word32 = swap32(biYPelsPerMeter); /* Resolution (dummy) */ for (int i = 0; i < 4; ++i) { BMP_HEADER[n++] = word32[i]; } word32 = swap32(biClrUsed); /* Color used */ for (int i = 0; i < 4; ++i) { BMP_HEADER[n++] = word32[i]; } word32 = swap32(biClrImportant); /* Important Color */ for (int i = 0; i < 4; ++i) { BMP_HEADER[n++] = word32[i]; } word32 = swap32(biRmask); /* Bitmask for red in case of 16bits color */ for (int i = 0; i < 4; ++i) { BMP_HEADER[n++] = word32[i]; } word32 = swap32(biGmask); /* Bitmask for green in case of 16bits color */ for (int i = 0; i < 4; ++i) { BMP_HEADER[n++] = word32[i]; } word32 = swap32(biBmask); /* Bitmask for blue in case of 16bits color */ for (int i = 0; i < 4; ++i) { BMP_HEADER[n++] = word32[i]; } if (n != HEADER_SIZE + MASK_SIZE) { Serial.println("HEADER_SIZE ERROR"); exit(1); } } void setup() { Serial.begin(115200); if (!SD.begin(SPI_FULL_SPEED)) { Serial.println("SD.begin() failed"); return; } make_bmp_header(); theCamera.begin(); CamErr err = theCamera.setStillPictureImageFormat( IMG_WIDTH ,IMG_HEIGHT ,CAM_IMAGE_PIX_FMT_YUV422); if (err != CAM_ERR_SUCCESS) { Serial.println("setStillPictureImageFormat Error: " + String(err)); return; } sleep(1); } void loop() { if (gCounter > 0) { Serial.println("Capturing End"); while(1){}; } Serial.println("Take Picture"); digitalWrite(LED0, HIGH); uint32_t start_time = millis(); CamImage img = theCamera.takePicture(); if (img.isAvailable()) { img.convertPixFormat(CAM_IMAGE_PIX_FMT_RGB565); char filename[16] = {0}; sprintf(filename, "PICT%03d.BMP", gCounter); if (SD.exists(filename)) { Serial.println("remove " + String(filename)); SD.remove(filename); } SpiFile myFile = SD.open(filename,FILE_WRITE); /* write bitmap header */ myFile.write(BMP_HEADER, (HEADER_SIZE+MASK_SIZE)*sizeof(uint8_t)); myFile.write(img.getImgBuff(), img.getImgSize()); myFile.close(); ++gCounter; } else { Serial.println("Capture Image Failed"); } digitalWrite(LED0, LOW); uint32_t duration = millis() - start_time; Serial.println("time (ms) = " + String(duration)); }
■ 人感センサーで動作するサーマルカメラを実現する
全ての材料が揃ったのでコーディングします。プログラムは少し長いので、GitHubにあげておきました。興味がある人は見てください。
YoshinoTaro/Spresense-HumanDetection-ThermalCamera Contribute to YoshinoTaro/Spresense-HumanDetection-ThermalCamera development by creating an account on GitHub. github.com |
さて実際に動かして撮れ高を見てみましょう。
これが撮影した画像です。320x240なので少し小さいです。(フォーマットがBMPではないのは、SSブログが受け付けてくれないためです…)
少し画像とサーマルイメージの位置関係がズレてしまっていますが、目合わせであわせこんだのでこんなものでしょう。
■ 作ってみた感想
もう少し小さく作れるかなと思ったのですが、思いのほか大きくなってしまいました。そのせいかタイムラプスカメラを作った時のような興奮がなかったのは残念です。でも人感センサーが思いのほか性能がよかったことと(きちんと調整できればですが)、16ビットのままBMPファイルで保存できるようになったのは大きな収穫です。
庭とかに放置して何がとれるか試してみようかな。
さて、4月は何を作ろう。ちょっと最近忙しいからあまり手間がかかるのはやめたいな…
(^^)
■ 関連記事
三月の電子工作「SPRESENSEでサーマルカメラを作ろう!」
https://makers-with-myson.blog.ss-blog.jp/2021-03-07
三月の電子工作「SPRESENSEでサーマルカメラを作ろう!」カメラ画像にサーマル画像を重畳してみた
https://makers-with-myson.blog.ss-blog.jp/2021-03-14
三月の電子工作「SPRESENSEでサーマルカメラを作ろう!」人感センサーを試してみた!
https://makers-with-myson.blog.ss-blog.jp/2021-03-21
- 出版社/メーカー: スイッチサイエンス
- メディア:
SONY SPRESENSE メインボード CXD5602PWBMAIN1
- 出版社/メーカー: スプレッセンス(Spresense)
- メディア: Tools & Hardware
SONY SPRESENSE カメラモジュール CXD5602PWBCAM1
- 出版社/メーカー: スプレッセンス(Spresense)
- メディア: Tools & Hardware
三月の電子工作「SPRESENSEでサーマルカメラを作ろう!」ー人感センサーを試してみた!ー [SPRESENSE]
■ SPRESENSEに「HC-SR501」をつなげてみたら動かない
せっかくならメインボードのみで動かせないかと思い、何も考えずにつなげてみました。IO が 3.3V なので、電源が 3.3 Vでも動くんじゃね?と思ったのですが、、、甘かった。まぁ、動かないよねと思ったのですが、やっぱり動かない。
回路図を見てみたら、しっかりと LDO が載っています。5~20V を 3.3V にレベルダウンをしています。
これじゃ動かないよね、ということで、立派な LDO ですが、今回は邪魔なので取っ払ってしまいました。
導通しても問題なし。これで電源はなんとかなりました。
■ 感度と出力保持時間の調整が難しい
「SR-HC501」は ”ボリュームで感度と出力保持時間を調整できる” となってはいるのですが、これがなかなか難しい。
SR501 のセンサー部の PIR は内部にキャパシタンスがあり、それが充電できるまで検出できないようです。同じものかどうか分かりませんが、村田のセンサーは起動まで30数秒程度の時間が必要とデータシートにありました。それを知らずに、動かねぇ~と試行錯誤の連続。
”出力保持時間” も曲者です。なかなか検出しないので、検出できるようにと時計回りにまわすと、検出もしていないのにHIGHになりっぱなしになります。これに気づかず、ハンダを失敗したのかと試行錯誤することになりました。
割り込みで検出する場合、”出力保持時間”は、関係ないので最短(反時計回りいっぱい)に設定しておきました。
それは良しとしても、まだうまく動きません。ググってみたら検出間隔を短くするために抵抗をつけかえると良いと書いてあったので、思い切ってやってみましたが、これはNG。一度検出するとずっと検出しっぱなしになってしまいました。
気を取り直して、いろいろと調整してみたところ、やっと見つけたスイートスポット。ようやく期待どおりに動くようになりました。
買ってきてポン付けで動くと思ったのですが甘かった。このセンサー用のLSI(BISS0001)の中身はオペアンプでなので、むしろ自作したほうが良いかもしれません。
■ 人感センサーでSPRESENSEを起動する
いよいよ、人感センサーを割り込みを使ってSPRESENSEを起動するようにしたいと思います。構成は、人感センサーとSPRESENSEメインボードだけです。(SPRESENSEのIO電圧1.8Vに対し3.3Vを突っ込むので、自己責任でやってくださいねー)
電源コントロールに LowPower ライブラリを用いました。SPRESENSEは割り込み起動も簡単にできるので便利ですね。
#include <LowPower.h> #include <EEPROM.h> #define DET_PIN 16 int eeprom_address = 0; int value; bool bDetect = false; void detect() { bDetect = true; } void setup() { Serial.begin(115200); LowPower.begin(); attachInterrupt(digitalPinToInterrupt(DET_PIN), detect, RISING); bootcause_e bc = LowPower.bootCause(); if ((bc == POR_SUPPLY) || (bc == POR_NORMAL)) { Serial.println("Example for GPIO wakeup from cold sleep"); } else { Serial.println("wakeup from cold sleep"); } LowPower.enableBootCause(DET_PIN); sleep(1); } void loop() { static int n = 0; if (bDetect) { digitalWrite(LED0, HIGH); value = EEPROM.read(eeprom_address); Serial.println("DETECT!" + String(value)); ++value; EEPROM.write(eeprom_address, value); bDetect = false; } else { digitalWrite(LED0, LOW); } delay(300); digitalWrite(LED0, LOW); // Cold sleep Serial.print("Go to cold sleep..."); delay(1000); LowPower.coldSleep(); }
気になる消費電力は、私のしょぼい電圧電流計で計測したところ、待受状態では 10μA 程度で、動いている状態で 20μA 程度でした。動作時 "20uA" は反応の悪い電圧計で測定したので一定時間の平均となっています。瞬間的には SPRESENSE には 6mA 程度流れているはずです。
しかし、この人感センサーは低消費電力ですね。最大消費電流 100uA となっていましたが偽りなしですね。これはSPRESENSEと相性よいかも…
■ 動かしてみた感想
簡単に動かせると思った人感センサーですが、思いの外苦戦をしてしまいました。ただ、使いこなせるようになると、強い味方になりそうです。そのためには、RIP Motion Sensor そのものの特性を熟知しておく必要がありそうです。
BISS0001は、アナログ回路に慣れていない人には便利なのかも知れませんが、少し使いにくい印象です。むしろオペアンプで増幅回路を自作したほうがよいのかもなと思いました。時間があったらチャレンジしてみようかな。
次回は、この人感センサーとカメラシステムを組み合わせてみたいと思います。
(^^)/~
■ 関連記事
三月の電子工作「SPRESENSEでサーマルカメラを作ろう!」
https://makers-with-myson.blog.ss-blog.jp/2021-03-07
三月の電子工作「SPRESENSEでサーマルカメラを作ろう!」カメラ画像にサーマル画像を重畳してみた
https://makers-with-myson.blog.ss-blog.jp/2021-03-14
三月の電子工作「SPRESENSEでサーマルカメラを作ろう!」人感センサーで撮影するサーマルカメラ完成
https://makers-with-myson.blog.ss-blog.jp/2021-04-06
SONY SPRESENSE メインボード CXD5602PWBMAIN1
- 出版社/メーカー: スプレッセンス(Spresense)
- メディア: Tools & Hardware
SONY SPRESENSE カメラモジュール CXD5602PWBCAM1
- 出版社/メーカー: スプレッセンス(Spresense)
- メディア: Tools & Hardware
三月の電子工作「SPRESENSEでサーマルカメラを作ろう!」ーカメラ画像にサーマル画像を重畳してみたー [SPRESENSE]
■ SpresenseとAMG8833とLCDディスプレイの配線
AMG8833はI2Cインターフェースになっていますので接続は簡単です。その際に拡張ボードのIO電源設定が3.3Vになっていることを確認してください。USB端子の近くのジャンパーです。I2Cはプルアップ抵抗が必要かも知れませんので、必要に応じて追加してください。
■ AMG8833用の Arduino ライブラリをインストールする
AMG8833のライブラリは、Arduinoの公式ライブラリからインストールできます。また、ILI9341とGFXライブラリは、あらかじめ Spresense用の LI8341 とGFXライブラリを kzhioki 氏のサイトのものをインストールしましょう。
公式のものをインストールするとフレームレートがめちゃくちゃ遅いです。kzhioki 氏のものは Spresense に最適化されているようです。
■ Spresense用のスケッチを作成する
Spresenseサーマルカメラのスケッチを Github にアップロードしておきました。
YoshinoTaro/Spresense-AMG8833-CameraOverlap Contribute to YoshinoTaro/Spresense-AMG8833-CameraOverlap development by creating an account on GitHub. github.com |
AMG8833は 8x8 しか解像度がありません。SPRESENSEカメラに大きさをあわせるために、hkoffer氏が作成したInterpolation のプログラムを取り込んでいます。
アルファブレンディングを変えたい場合は、”ALPHA_BLENDING”の値を変えてください。サーマル画像とカメラ画像の割合を変更することができます。
■ 作ってみた感想
AMG8833は、I2Cなのでフレームレートが遅いですね。だいたい 2 fps あるかないか位でしょうか。でもサーマル画像だけでは何なのか分からなかったものが、映像に重畳すると何に反応しているのか分かるのでちょっと面白いですね。
次はどうしようかな。人感センサーと組み合わせて見ようかな。
(。-`ω´-)うーん
■ 関連記事
三月の電子工作「SPRESENSEでサーマルカメラを作ろう!」
https://makers-with-myson.blog.ss-blog.jp/2021-03-07
三月の電子工作「SPRESENSEでサーマルカメラを作ろう!」人感センサーを試してみた!
https://makers-with-myson.blog.ss-blog.jp/2021-03-21
三月の電子工作「SPRESENSEでサーマルカメラを作ろう!」人感センサーで撮影するサーマルカメラ完成
https://makers-with-myson.blog.ss-blog.jp/2021-04-06
- 出版社/メーカー: スイッチサイエンス
- メディア:
SONY SPRESENSE メインボード CXD5602PWBMAIN1
- 出版社/メーカー: スプレッセンス(Spresense)
- メディア: Tools & Hardware
SONY SPRESENSE 拡張ボード CXD5602PWBEXT1
- 出版社/メーカー: スプレッセンス(Spresense)
- メディア: Tools & Hardware
SONY SPRESENSE カメラモジュール CXD5602PWBCAM1
- 出版社/メーカー: スプレッセンス(Spresense)
- メディア: Tools & Hardware
三月の電子工作「SPRESENSEでサーマルカメラを作ろう!」 [SPRESENSE]
これは、パナソニックのサーマルセンサー「AMG8833」と、SPRESENSEを組み合わせたカメラです。SPRESENSEのカメラの画像とAMG8833の出力をオーバーレイさせてるので、どこに熱源があるか一発でわかるようになっています。
同じことを繰り返してもつまらないので、ここはSPRESENSEメインボードだけで実現して、できればWiFiで画像を飛ばすようにしようかな。LTE-Mで飛ばすのも面白いかな。
どこまで広げるかは仕事の忙しさ次第ということで。お楽しみに~。(^^)/~
■ 関連記事
三月の電子工作「SPRESENSEでサーマルカメラを作ろう!」カメラ画像にサーマル画像を重畳してみた
https://makers-with-myson.blog.ss-blog.jp/2021-03-14
三月の電子工作「SPRESENSEでサーマルカメラを作ろう!」人感センサーを試してみた!
https://makers-with-myson.blog.ss-blog.jp/2021-03-21
三月の電子工作「SPRESENSEでサーマルカメラを作ろう!」人感センサーで撮影するサーマルカメラ完成
https://makers-with-myson.blog.ss-blog.jp/2021-04-06
- 出版社/メーカー: スイッチサイエンス
- メディア:
SONY SPRESENSE メインボード CXD5602PWBMAIN1
- 出版社/メーカー: スプレッセンス(Spresense)
- メディア: Tools & Hardware
SONY SPRESENSE カメラモジュール CXD5602PWBCAM1
- 出版社/メーカー: スプレッセンス(Spresense)
- メディア: Tools & Hardware
二月の電子工作「SPRESENSEで音響通信」ーPCとSPRESENSE間の音響無線通信の実現!ー [SPRESENSE]
さらに、今回は誤り率を大幅に改善し、さらにビットレートもあげてみました。まずはそこから説明したいと思います。
■ SPACE/MARKの周波数判定の精度をあげる
前回の "SPACE/MARK" の判定は、中心周波数よりピークが上だったら "MARK"、下だったら "SPCE" という非常にざっくりとしたもの。
uint8_t sbit; if (peakFs > FSK_CENTER) { // MARK sbit = 0x01; } else if (peakFs <= FSK_CENTER) { // SPACE sbit = 0x00; }
これでは雑音を拾いまくりです。特に生活雑音は低周波に大きな成分をもつので、これはマズイ。なんともお恥ずかしいコードです。なので、ピンポイントに周波数を見るようにしました。
48kHz の 256 tap で FFTをしているので、周波数分解能は187.5Hz。ですので、その整数倍を "MARK" と "SPACE" に割り当て、その近辺のパワースペクトルを見れば精度よく判定できそうです。
低周波はノイズのパワーが大きいので避けたいのと、可聴領域で実験していると耳にやさしくないので、負担のかからない 10kHz 以上にしました。("SPACE"=13125 hz, "MARK"=16875 hz)
これで、誤り率はは格段に減らすことができました。
■ ビットレートを 25 bps から 50 bps に倍速にする
前回はテストということもあり、安全を見て 25bps にしました。安定して動かすことができるようになったので2倍の 50bps にあげることにしました。取りこぼしをなくすため、変調間隔を FFT 解析周期 5.3msec の整数倍(4倍)の 21.2msec に調整し、フェッチング間隔を4回にしています。
■ 音響無線通信をループバックで実現する
次は、音響無線通信をループバックで試してみたいと思います。
”SPACE/MARK の判定方法の改善” と、”ビットレート改善” を盛り込んだコードは次のようになりました。デバッグコードなどもあり、少し長めなのはご容赦ください。
#include <MediaRecorder.h> #include <MemoryUtil.h> #include <AudioOscillator.h> #include <OutputMixer.h> #include <arch/board/board.h> #include <FFT.h> #define ANALOG_MIC_GAIN (200) /* default +0dB */ #define SPEAKER_VOLUME (-160) /* default -160 */ #define FFT_LEN 256 #define CHANNEL_NUM 1 #define SPACE (13125) #define MARK (16875) #define ACTIVE_DURATION (10000) /* usec */ #define IDLE_STATE (0) #define STARTBIT_STATE (1) #define BITREC_STATE (2) #define STOPBIT_STATE (3) #define FETCH_INTERVAL (4) #define MSBBIT_INDEX (7) // #define DEBUG_ENABLE Oscillator *theOscillator; OutputMixer *theMixer; MediaRecorder *theRecorder; FFTClass<CHANNEL_NUM, FFT_LEN> FFT; static float pDst[FFT_LEN]; static const uint32_t rec_bitrate = AS_BITRATE_48000; static const uint32_t rec_sampling_rate = AS_SAMPLINGRATE_48000; static const uint8_t rec_channel_num = CHANNEL_NUM; static const uint8_t rec_bit_length = AS_BITLENGTH_16; static const int32_t buffer_size = FFT_LEN * sizeof(int16_t); static const uint32_t rec_wait_usec = buffer_size * (1000000 / rec_sampling_rate) / 2; static uint8_t s_buffer[buffer_size*2]; static uint8_t frame_cnt = 0; static uint8_t fetch_timing = 1; static uint8_t bpos = 0; static uint8_t cur_state = IDLE_STATE; static char output = 0; static bool rec_done_cb(AsRecorderEvent e, uint32_t r1, uint32_t r2) { // printf("mp cb %x %x %x\n", e, r1, r2); return true; } static void rec_attention_cb(const ErrorAttentionParam *p) { if (p->error_code >= AS_ATTENTION_CODE_WARNING) { Serial.println("Attention!"); } } static void mixer_done_cb(MsgQueId id, MsgType type, AsOutputMixDoneParam *p) { // Serial.println("mixer done callback"); return; } static void mixer_send_cb(int32_t id, bool is_end) { // Serial.println("mixer send callback"); return; } void debug_print(uint8_t sbit) { #ifdef DEBUG_ENABLE static bool first_print = true; if (first_print) { Serial.println("state, sbit, bpos, fcnt"); first_print = false; } Serial.print(String(cur_state)); Serial.print("," + String(sbit)); Serial.print("," + String(bpos)); Serial.print("," + String(frame_cnt)); Serial.println(); #endif } void idle_phase(uint8_t sbit) { if (sbit == 0) { cur_state = STARTBIT_STATE; } frame_cnt = 0; fetch_timing = 1; output = 0; return; } void startbit_phase(uint8_t sbit) { ++frame_cnt; if (frame_cnt != fetch_timing) return; debug_print(sbit); cur_state = BITREC_STATE; fetch_timing += FETCH_INTERVAL; return; } void bitrec_phase(uint8_t sbit) { ++frame_cnt; if (frame_cnt != fetch_timing) return; debug_print(sbit); output = output | (sbit << bpos); fetch_timing += FETCH_INTERVAL; if (++bpos > MSBBIT_INDEX) { cur_state = STOPBIT_STATE; } return; } bool stopbit_phase(uint8_t sbit) { ++frame_cnt; if (frame_cnt != fetch_timing) return; debug_print(sbit); Serial.write(output); // interim implementation frame_cnt = 0; bpos = 0; cur_state = IDLE_STATE; return; } static void audioReadFrames() { uint32_t read_size; int er; while (true) { er = theRecorder->readFrames(s_buffer, buffer_size, &read_size); if (er != MEDIARECORDER_ECODE_OK && er != MEDIARECORDER_ECODE_INSUFFICIENT_BUFFER_AREA) { Serial.println("Recording Error"); theRecorder->stop(); theRecorder->deactivate(); theRecorder->end(); break; } // Serial.println("buffer_size:" + String(buffer_size) + " read_size:" + String(read_size)); if (read_size < buffer_size){ usleep(rec_wait_usec); continue; } FFT.put((q15_t*)s_buffer, FFT_LEN); FFT.get(pDst, 0); uint32_t index; float maxValue; int max_line = FFT_LEN/2.56; arm_max_f32(pDst, max_line, &maxValue, &index); float peakFs = index * (rec_sampling_rate / FFT_LEN); const float fc = (SPACE + MARK) / 2; float Space = 0.5*pDst[69] + pDst[70] + pDst[71] + 0.5*pDst[72]; float Mark = 0.5*pDst[89] + pDst[90] + pDst[91] + 0.5*pDst[92]; uint8_t sbit; if (peakFs < fc && Space > 0.5 && Space > Mark) sbit = 0; else if (peakFs > fc && Mark > 0.5 && Mark > Space) sbit = 1; else sbit = 1; // no signal noise detected. #if 0 Serial.print(String(Space,3) + ", " + String(Mark, 3) + ", "); Serial.println(sbit); #else switch(cur_state) { case IDLE_STATE: idle_phase(sbit); break; case STARTBIT_STATE: startbit_phase(sbit); break; case BITREC_STATE: bitrec_phase(sbit); break; case STOPBIT_STATE: stopbit_phase(sbit); break; } #endif } } static bool active() { const uint32_t sample = 480; int er; for (int i = 0; i < 5; i++) { AsPcmDataParam pcm; /* get PCM */ er = pcm.mh.allocSeg(S0_REND_PCM_BUF_POOL, (sample*2*2)); if (er != ERR_OK) break; theOscillator->exec((q15_t*)pcm.mh.getPa(), sample); pcm.identifier = 0; pcm.callback = 0; pcm.bit_length = 16; pcm.size = sample*2*2; pcm.sample = sample; pcm.is_end = false; pcm.is_valid = true; er = theMixer->sendData(OutputMixer0, mixer_send_cb, pcm); if (er != OUTPUTMIXER_ECODE_OK) { Serial.println("OutputMixer send error: " + String(er)); return false; } } return true; } void setup() { Serial.begin(115200); initMemoryPools(); createStaticPools(MEM_LAYOUT_RECORDINGPLAYER); theOscillator = new Oscillator(); theOscillator->begin(SinWave, 1); theOscillator->set(0, 0); // theOscillator->set(0, 700, 200, 50, 400); // attack=0, decay=700, sustain=50, release=400 theOscillator->set(10, 10, 100, 50, 10); // attack=0, decay=700, sustain=50, release=400 theOscillator->lfo(0, 4, 2); theMixer = OutputMixer::getInstance(); theMixer->activateBaseband(); theMixer->create(); theMixer->setRenderingClkMode(OUTPUTMIXER_RNDCLK_NORMAL); theMixer->activate(OutputMixer0, HPOutputDevice, mixer_done_cb); theMixer->setVolume(SPEAKER_VOLUME, 0, 0); board_external_amp_mute_control(false); /* Unmute */ FFT.begin(WindowRectangle, rec_channel_num, 0); theRecorder = MediaRecorder::getInstance(); theRecorder->begin(rec_attention_cb); theRecorder->setCapturingClkMode(MEDIARECORDER_CAPCLK_NORMAL); theRecorder->activate(AS_SETRECDR_STS_INPUTDEVICE_MIC, rec_done_cb); theRecorder->init(AS_CODECTYPE_LPCM, rec_channel_num , rec_sampling_rate, rec_bit_length, rec_bitrate, "/mnt/sd0/BIN"); theRecorder->setMicGain(ANALOG_MIC_GAIN); theRecorder->start(); // Serial.println("rec wait usec: " + String(rec_wait_usec)); task_create("audio recording", 120, 1024, audioReadFrames, NULL); } void send_signal(uint16_t hz, int repeat = SIGNAL_DURATION) { theOscillator->set(0, hz); // interim measures active(); usleep(ACTIVE_DURATION); // I'm not sure why usleep takes 20msec? delayMicroseconds(1200); // adjustment to fit FFT period #if 0 static uint32_t last_time = 0; uint32_t cur_time = millis(); Serial.println(String(cur_time - last_time)); last_time = cur_time; #endif } void send_char(uint8_t c, int n = SIGNAL_DURATION) { send_signal(SPACE, n); // send start bit for (int n = 0; n < 8; ++n, c = c >> 1) { /* LSB fast */ if (c & 0x01) { /* mark (1) */ send_signal(MARK, n); } else { /* space (0) */ send_signal(SPACE, n); } } send_signal(MARK, n); // send stop bit } void loop() { send_signal(MARK); // default level is mark if (Serial.available()) { char c = Serial.read(); send_char(c); } }
実際の動きは次のようになりました。前回に比べると格段に早くなりました。これからどんどん改善を入れて、ゆくゆくは画像を送信できるようにしたいところです。
再生できない場合、ダウンロードは🎥こちら
ここまでは、まだループバックなのでまだ面白みがありません。いよいよPCとの通信にチャレンジしたいと思います。
■ PyAudio を使って PC から SPRESENSE へデータ送信!
当初、SPRESENSE 同士で通信ということも考えたのですが、SPRESENSE 同士で通信しても面白みはないので、PC とデータ通信することにしました。手始めに PC から SPRESENSE へデータを送信してみることにしました。
PC 側はいろいろと考えたのですが、Raspberry Pi との通信も考慮して PyAudio を使うことにしました。ひさしぶりの Python プログラミングなので色々と苦労しましけど。(詳しくはTwitterへ!)
PC 側の Python コードです。ちなみに、PyAudio は Python 3.6 しか正式サポートされていないので、Anaconda で 3.6 の仮想環境を使って実装しています。
import wave import struct import numpy as np import pyaudio import pandas as pd from matplotlib.pylab import * def createSineWave (A, f0, fs, length): # A: Amplitude # f0: Frequency # f1: Frequency # fs: Sampling rate # length: Playing time " data = [] # Genrating sinwave ranging [-1.0, 1.0] for n in np.arange(length * fs): s = A * (np.sin(2 * np.pi * f0 * n / fs)) # Clipping if s > 1.0: s = 1.0 if s < -1.0: s = -1.0 data.append(s) # Tasnsforming the sinwave in range of integer [-32768, 32767] data = [int(x * 32767.0) for x in data] # Trasforming into binary data data = struct.pack("h" * len(data), *data) return data def sendSignal(freq, time): data = createSineWave(amp, freq, fs, time) length = len(data) sp = 0 chunk = 1024 buffer = data[sp:sp+chunk] while sp < length : stream.write(buffer) sp = sp + chunk buffer = data[sp:sp+chunk] return if __name__ == "__main__" : while True: try: sdata = input() sdata = sdata + "\n" length = 100 n = 0 fs = 48000 # sampling rate mark = 16875 # Hz space = 13125 # Hz amp = 1.0 p = pyaudio.PyAudio() stream = p.open(format=pyaudio.paInt16, channels=1, rate=int(fs), output= True) # ready to send signal sendSignal(mark, 0.1) mod_period = 0.0212 for c in sdata: print(c) # start bit 40msec sendSignal(space, mod_period) ic = ord(c) for x in range(8): if (ic & 0x01): sendSignal(mark, mod_period) else: sendSignal(space, mod_period) ic = ic >> 1 # stop bit sendSignal(mark, mod_period) # finish sending signal sendSignal(mark, 0.1) except KeyboardInterrupt: break stream.close() p.terminate()
実際の動きはこちら、しっかりデータ通信ができています。
再生できない場合、ダウンロードは🎥こちら
■ PyAudio を使って、PC で SPRESENSE からの音響FSK無線通信を受けてみる
次は SPRESENSE からの無線通信を PC で受信します。PC にピンマイクをつなげて受信してみます。
SRESENSE の受信ルーチンをそのまま Python コードに置き直しただけのコードで試してみました。受信も PyAudio を使っています。
import pyaudio import numpy as np import time from scipy.signal import argrelmax from matplotlib import pyplot as plt CHUNK = 256 RATE = 48000 # sampling rate dt = 1/RATE freq = np.linspace(0,1.0/dt,CHUNK) MARK = 16875 SPACE = 13125 fc = (MARK + SPACE) / 2 IDLE_STATE = 0 STARTBIT_STATE = 1 BITREC_STATE = 2 STOPBIT_STATE = 3 class StateMachine(): FETCH_INTERVAL = 4 MSBBIT_INDEX = 7 cur_state = IDLE_STATE frame_cnt = 0 fetch_timing = 1 output = 0 bpos = 0 def __init__(self): print("Start RX") def state(self): return self.cur_state def debug_print(self, sbit): return print(self.cur_state, end=',') print( sbit, end=',') print(self.bpos, end=',') print(self.frame_cnt) def idle_phase(self, sbit): self.frame_cnt = 0 self.fetch_timing = 1 self.output = 0 if (sbit == 0): self.cur_state = STARTBIT_STATE def startbit_phase(self, sbit): self.frame_cnt = self.frame_cnt + 1 if (self.frame_cnt != self.fetch_timing): return self.debug_print(sbit) self.bpos = 0 self.cur_state = BITREC_STATE self.fetch_timing = self.fetch_timing + self.FETCH_INTERVAL def bitrec_phase(self, sbit): self.frame_cnt = self.frame_cnt + 1 if (self.frame_cnt != self.fetch_timing): return self.debug_print(sbit) self.output = self.output | (sbit << self.bpos) self.fetch_timing = self.fetch_timing + self.FETCH_INTERVAL self.bpos = self.bpos + 1 if (self.bpos > self.MSBBIT_INDEX): self.cur_state = STOPBIT_STATE def stopbit_phase(self, sbit): self.frame_cnt = self.frame_cnt + 1 if (self.frame_cnt != self.fetch_timing): return self.debug_print(sbit) coutput = chr(self.output) print(coutput, end="") self.frame_cnt = 0 self.bpos = 0 self.cur_state = IDLE_STATE if __name__=='__main__': p = pyaudio.PyAudio() stream = p.open(format=pyaudio.paInt16 , channels=1 , rate=RATE , frames_per_buffer=CHUNK , input=True , output=False) state = StateMachine() while stream.is_active(): try: input = stream.read(CHUNK, exception_on_overflow=False) ndarray = np.frombuffer(input, dtype='int16') ndarray = ndarray.astype(float)/float(2**15-1) f = np.fft.fft(ndarray) f_abs = np.abs(f) index = np.argmax(f_abs[:(int)(CHUNK/2)]) peak_f = index * (RATE / CHUNK) #print(peak_f) space = 0.5*f_abs[69] + f_abs[70] + f_abs[71] + 0.5*f_abs[72] mark = 0.5*f_abs[89] + f_abs[90] + f_abs[91] + 0.5*f_abs[92] sbit = 1 if (peak_f < fc and space > 1.0 and space > mark): #print("Space") sbit = 0 elif (peak_f > fc and mark > 1.0 and mark > space): #print("Mark") sbit = 1 if (state.state() == IDLE_STATE): state.idle_phase(sbit) elif (state.state() == STARTBIT_STATE): state.startbit_phase(sbit) elif (state.state() == BITREC_STATE): state.bitrec_phase(sbit) elif (state.state() == STOPBIT_STATE): state.stopbit_phase(sbit) except KeyboardInterrupt: break stream.stop_stream() stream.close() P.terminate()
実際の動きはこちらです。こちらもしっかりと通信できました。
再生できない場合、ダウンロードは🎥こちら
■ 全体を通じて
PCーSPRESENSE 間を音響無線通信を実現することができました。まだ 50bps と低いレートですが、まだまだ通信速度をあげる余地はありそうです。無線の場合、だいたい 2m くらいまでは通信ができそうですが、安定しているのは 1m ちょいといったところです。
あと大事なのはスピーカーとマイク。こちらはデバイスによってはまったく通信が出来ないので注意が必要です。ノートPC のペラッペラのスピーカーでは誤り率が格段にあがりますし、マイクもモノによっては駄目なものもあります。
特に中華製の100円マイクは品質がばらばらで同じ製品でも誤り率に大きな差が出ていました。一方、スピーカーは低音のきちんと出るものであれば Bluetooth スピーカーでも問題ありませんでした。
低速ですが無線通信ができると楽しいものですね。そのうち、AFSK や PSK にもチャレンジしてみようかな!
(^^)/~
関連リンク
二月の電子工作「SPRESENSEで音響通信」
https://makers-with-myson.blog.ss-blog.jp/2021-02-02
二月の電子工作「SPRESENSEで音響通信」ー送受信の検討ー
https://makers-with-myson.blog.ss-blog.jp/2021-02-07
二月の電子工作「SPRESENSEで音響通信」ー音響ループバックを試すー
https://makers-with-myson.blog.ss-blog.jp/2021-02-14
二月の電子工作「SPRESENSEで音響通信」ーFSKによるループバック・データ通信の実現ー
https://makers-with-myson.blog.ss-blog.jp/2021-02-22
SPRESENSE でピンマイクを使えるようにしてみた
https://makers-with-myson.blog.ss-blog.jp/2019-10-05
SPRESENSE にピンマイクをつけて録音してみた!
https://makers-with-myson.blog.ss-blog.jp/2019-10-12
SONY SPRESENSE メインボード CXD5602PWBMAIN1
- 出版社/メーカー: スプレッセンス(Spresense)
- メディア: Tools & Hardware
SONY SPRESENSE 拡張ボード CXD5602PWBEXT1
- 出版社/メーカー: スプレッセンス(Spresense)
- メディア: Tools & Hardware
二月の電子工作「SPRESENSEで音響通信」ーFSKによるループバック・データ通信の実現ー [SPRESENSE]
■ FSKによるデータ送信部(モジュレータ)を実装
まずは送信の実装です。今回も前回と同じく”MARK”を 2kHz、”SPACE”を 1kHz に設定します。データの送信は、シリアルコンソールから入力された文字を送信するようにしました。
今回、受信側はサンプリングレート 48 kHz で 256 tap のFFTを使おうと考えているので、データ判定のために、1/48000 x 256 = 5.33msec 必要です。余裕をもって変調間隔は 40msec で試すことにしました。
いろいろ試行錯誤したのですが、どうも Oscillator ライブラリは 10msecスリープ毎に active() 関数を呼ぶとFIFOのオーバーフローが起きなさそう。(※ハマりポイント その1)
処理時間を測ると、一つの active() とスリープで 20msec程度かかっている模様。で、普通ループで処理をまわそうと思いますよね、、、でも、なぜか処理時間がばらつきます。ここのコードです。
uint32_t last_time = 0; void send_signal(uint16_t hz, int repeat = SIGNAL_DURATION) { theOscillator->set(0, hz); #if 0 for (int i = 0; i < repeat; ++i) { active(); usleep(ACTIVE_DURATION); } #else // interim measures active(); usleep(ACTIVE_DURATION); active(); usleep(ACTIVE_DURATION); #endif #if 0 uint32_t cur_time = millis(); Serial.println(String(cur_time - last_time)); last_time = cur_time; #endif }
送信間隔を測ってみると、active関数とusleep関数を直接二度呼んだ場合は問題ありませんが、for ループでは処理時間がばらばら。なんで???(※ハマりポイント その2)
実際に、受信側にどれだけ影響があるか確認するために、"0" と "1" が綺麗にならぶ大文字 'U' を送信し、FFTの結果を表示しててみました。
※acitve関数を2回コール(正常)
ご覧のようにループをすると結果がおかしくなり、使い物になりません。うーん、なんでだろう?ちょっと奥が深そうなので、ここは深追いせずに実装を優先します。
※for loop で active関数を2回コール(異常)
少し格好悪いですがactive関数を二重に呼ぶようにようにしてとりあえず実装完了としました。
#include <MediaRecorder.h> #include <MemoryUtil.h> #include <AudioOscillator.h> #include <OutputMixer.h> #include <arch/board/board.h> #include <FFT.h> #define ANALOG_MIC_GAIN 0 /* +0dB */ #define FFT_LEN 256 #define CHANNEL_NUM 1 #define SPACE (1000) #define FSK_CENTER (1500) #define MARK (2000) #define ACTIVE_DURATION (10000) /* usec */ #define SIGNAL_DURATION (2) /* x ACTIVE_DURATION */ Oscillator *theOscillator; OutputMixer *theMixer; MediaRecorder *theRecorder; FFTClassFFT; static float pDst[FFT_LEN]; static const uint32_t rec_bitrate = AS_BITRATE_48000; static const uint32_t rec_sampling_rate = AS_SAMPLINGRATE_48000; static const uint8_t rec_channel_num = CHANNEL_NUM; static const uint8_t rec_bit_length = AS_BITLENGTH_16; static const int32_t buffer_size = FFT_LEN * sizeof(int16_t); static const uint32_t rec_wait_usec = buffer_size * (1000000 / rec_sampling_rate) / 2; static uint8_t s_buffer[buffer_size*2]; static bool rec_done_cb(AsRecorderEvent e, uint32_t r1, uint32_t r2) { // printf("mp cb %x %x %x\n", e, r1, r2); return true; } static void rec_attention_cb(const ErrorAttentionParam *p) { if (p->error_code >= AS_ATTENTION_CODE_WARNING) { Serial.println("Attention!"); } } static void mixer_done_cb(MsgQueId id, MsgType type, AsOutputMixDoneParam *p) { // Serial.println("mixer done callback"); return; } static void mixer_send_cb(int32_t id, bool is_end) { // Serial.println("mixer send callback"); return; } static void audioReadFrames() { uint32_t read_size; int er; while (true) { er = theRecorder->readFrames(s_buffer, buffer_size, &read_size); if (er != MEDIARECORDER_ECODE_OK && er != MEDIARECORDER_ECODE_INSUFFICIENT_BUFFER_AREA) { Serial.println("Recording Error"); theRecorder->stop(); theRecorder->deactivate(); theRecorder->end(); break; } // Serial.println("buffer_size:" + String(buffer_size) + " read_size:" + String(read_size)); if (read_size < buffer_size){ usleep(rec_wait_usec); continue; } FFT.put((q15_t*)s_buffer, FFT_LEN); FFT.get(pDst, 0); uint32_t index; float maxValue; int max_line = FFT_LEN/2.56; arm_max_f32(pDst, max_line, &maxValue, &index); float peakFs = index * (rec_sampling_rate / FFT_LEN); uint8_t sbit; if (peakFs > FSK_CENTER) { /* MARK */ sbit = 0x01; } else if (peakFs <= FSK_CENTER) { /* SPACE */ sbit = 0x00; } Serial.println(sbit); } } static bool active() { const uint32_t sample = 480; int er; for (int i = 0; i < 5; i++) { AsPcmDataParam pcm; /* get PCM */ er = pcm.mh.allocSeg(S0_REND_PCM_BUF_POOL, (sample*2*2)); if (er != ERR_OK) break; theOscillator->exec((q15_t*)pcm.mh.getPa(), sample); pcm.identifier = 0; pcm.callback = 0; pcm.bit_length = 16; pcm.size = sample*2*2; pcm.sample = sample; pcm.is_end = false; pcm.is_valid = true; er = theMixer->sendData(OutputMixer0, mixer_send_cb, pcm); if (er != OUTPUTMIXER_ECODE_OK) { Serial.println("OutputMixer send error: " + String(er)); return false; } } return true; } void setup() { Serial.begin(115200); initMemoryPools(); createStaticPools(MEM_LAYOUT_RECORDINGPLAYER); theOscillator = new Oscillator(); theOscillator->begin(SinWave, 1); theOscillator->set(0, 0); theOscillator->set(0, 700, 200, 50, 400); // attack=0, decay=700, sustain=50, release=400 theOscillator->lfo(0, 4, 2); theMixer = OutputMixer::getInstance(); theMixer->activateBaseband(); theMixer->create(); theMixer->setRenderingClkMode(OUTPUTMIXER_RNDCLK_NORMAL); theMixer->activate(OutputMixer0, HPOutputDevice, mixer_done_cb); theMixer->setVolume(-160, 0, 0); board_external_amp_mute_control(false); /* Unmute */ FFT.begin(WindowRectangle, rec_channel_num, 0); theRecorder = MediaRecorder::getInstance(); theRecorder->begin(rec_attention_cb); theRecorder->setCapturingClkMode(MEDIARECORDER_CAPCLK_NORMAL); theRecorder->activate(AS_SETRECDR_STS_INPUTDEVICE_MIC, rec_done_cb); theRecorder->init(AS_CODECTYPE_LPCM, rec_channel_num , rec_sampling_rate, rec_bit_length, rec_bitrate, "/mnt/sd0/BIN"); theRecorder->setMicGain(ANALOG_MIC_GAIN); theRecorder->start(); // Serial.println("rec wait usec: " + String(rec_wait_usec)); task_create("audio recording", 120, 1024, audioReadFrames, NULL); } uint32_t last_time = 0; void send_signal(uint16_t hz, int repeat = SIGNAL_DURATION) { theOscillator->set(0, hz); #if 0 for (int i = 0; i < repeat; ++i) { active(); usleep(ACTIVE_DURATION); } #else // interim measures active(); usleep(ACTIVE_DURATION); active(); usleep(ACTIVE_DURATION); #endif #if 0 uint32_t cur_time = millis(); Serial.println(String(cur_time - last_time)); last_time = cur_time; #endif } void send_char(uint8_t c, int n = SIGNAL_DURATION) { send_signal(SPACE, n); // send start bit for (int n = 0; n < 8; ++n, c = c >> 1) { /* LSB fast */ if (c & 0x01) { /* mark (1) */ send_signal(MARK, n); } else { /* space (0) */ send_signal(SPACE, n); } } send_signal(MARK, n); // send stop bit } void loop() { send_signal(MARK); // default level is mark if (Serial.available()) { char c = Serial.read(); send_char(c); } }
せっかくなので、いろいろなデータを入力したときのFFTの出力波形をキャプチャしてみました。こういう絵面って、見てるだけでも飽きませんよね…。
(ところで、SSブログって動画アップロードできたんですね。、今更知った… ̄▽ ̄;)
再生できない場合、ダウンロードは🎥こちら
余談ですが、この実験を通して大文字”U"が大好きになりました。データ処理システムを作る人にとって、はなくてはならない相棒ですね。^^
■ FSKのデータ受信部(デモジュレータ)を実装
データ受信部はサンプリング周波数 48 kHz で、FFTが 256 tap の 5.3msec で単位で処理を行います。どのようににしてビット情報をフェッチするか色々と悩んだのですが、極めてハードウェア的なアプローチで行うことにしました。
スタートビットからカウンターを回して、ある間隔ごとにデータをフェッチするという方法です。(この結論に辿り着くまでに半日かかった…シンプル・イズ・ザ・ベスト!)
それに加えて、データ処理にステートマシンがあったほうが設計が楽になるので、簡単なステートマシンも設計しました。
実装は送信側もあるのでかなり長くなってしまっていますが晒しておきます。ライブラリ化したらGitHub にでも置こうと思います。
#include <MediaRecorder.h> #include <MemoryUtil.h> #include <AudioOscillator.h> #include <OutputMixer.h> #include <arch/board/board.h> #include <FFT.h> #define ANALOG_MIC_GAIN 0 /* +0dB */ #define FFT_LEN 256 #define CHANNEL_NUM 1 #define SPACE (1000) #define FSK_CENTER (1500) #define MARK (2000) #define ACTIVE_DURATION (10000) /* usec */ #define SIGNAL_DURATION (2) /* x ACTIVE_DURATION */ #define IDLE_STATE (0) #define STARTBIT_STATE (1) #define BITREC_STATE (2) #define STOPBIT_STATE (3) #define FETCH_INTERVAL (8) #define MSBBIT_INDEX (7) // #define DEBUG_ENABLE Oscillator *theOscillator; OutputMixer *theMixer; MediaRecorder *theRecorder; FFTClassFFT; static float pDst[FFT_LEN]; static const uint32_t rec_bitrate = AS_BITRATE_48000; static const uint32_t rec_sampling_rate = AS_SAMPLINGRATE_48000; static const uint8_t rec_channel_num = CHANNEL_NUM; static const uint8_t rec_bit_length = AS_BITLENGTH_16; static const int32_t buffer_size = FFT_LEN * sizeof(int16_t); static const uint32_t rec_wait_usec = buffer_size * (1000000 / rec_sampling_rate) / 2; static uint8_t s_buffer[buffer_size*2]; static uint8_t frame_cnt = 0; static uint8_t fetch_timing = 1; static uint8_t bpos = 0; static uint8_t cur_state = IDLE_STATE; static char output = 0; static bool rec_done_cb(AsRecorderEvent e, uint32_t r1, uint32_t r2) { // printf("mp cb %x %x %x\n", e, r1, r2); return true; } static void rec_attention_cb(const ErrorAttentionParam *p) { if (p->error_code >= AS_ATTENTION_CODE_WARNING) { Serial.println("Attention!"); } } static void mixer_done_cb(MsgQueId id, MsgType type, AsOutputMixDoneParam *p) { // Serial.println("mixer done callback"); return; } static void mixer_send_cb(int32_t id, bool is_end) { // Serial.println("mixer send callback"); return; } void debug_print(uint8_t sbit) { #ifdef DEBUG_ENABLE static bool first_print = true; if (first_print) { Serial.println("state, sbit, bpos, fcnt"); first_print = false; } Serial.print(String(cur_state)); Serial.print("," + String(sbit)); Serial.print("," + String(bpos)); Serial.print("," + String(frame_cnt)); Serial.println(); #endif } void idle_phase(uint8_t sbit) { if (sbit == 0) { cur_state = STARTBIT_STATE; } // debug_print(sbit); frame_cnt = 0; fetch_timing = 1; output = 0; return; } void startbit_phase(uint8_t sbit) { ++frame_cnt; if (frame_cnt != fetch_timing) return; debug_print(sbit); cur_state = BITREC_STATE; fetch_timing += FETCH_INTERVAL; return; } void bitrec_phase(uint8_t sbit) { ++frame_cnt; if (frame_cnt != fetch_timing) return; debug_print(sbit); output = output | (sbit << bpos); fetch_timing += FETCH_INTERVAL; if (++bpos > MSBBIT_INDEX) { cur_state = STOPBIT_STATE; } return; } bool stopbit_phase(uint8_t sbit) { ++frame_cnt; if (frame_cnt != fetch_timing) return; debug_print(sbit); Serial.write(output); // interim implementation frame_cnt = 0; bpos = 0; cur_state = IDLE_STATE; return; } static void audioReadFrames() { uint32_t read_size; int er; while (true) { er = theRecorder->readFrames(s_buffer, buffer_size, &read_size); if (er != MEDIARECORDER_ECODE_OK && er != MEDIARECORDER_ECODE_INSUFFICIENT_BUFFER_AREA) { Serial.println("Recording Error"); theRecorder->stop(); theRecorder->deactivate(); theRecorder->end(); break; } // Serial.println("buffer_size:" + String(buffer_size) + " read_size:" + String(read_size)); if (read_size < buffer_size){ usleep(rec_wait_usec); continue; } FFT.put((q15_t*)s_buffer, FFT_LEN); FFT.get(pDst, 0); uint32_t index; float maxValue; int max_line = FFT_LEN/2.56; arm_max_f32(pDst, max_line, &maxValue, &index); float peakFs = index * (rec_sampling_rate / FFT_LEN); uint8_t sbit; if (peakFs > FSK_CENTER) { /* MARK */ sbit = 0x01; } else if (peakFs <= FSK_CENTER) { /* SPACE */ sbit = 0x00; } switch(cur_state) { case IDLE_STATE: idle_phase(sbit); break; case STARTBIT_STATE: startbit_phase(sbit); break; case BITREC_STATE: bitrec_phase(sbit); break; case STOPBIT_STATE: stopbit_phase(sbit); break; } } } static bool active() { const uint32_t sample = 480; int er; for (int i = 0; i < 5; i++) { AsPcmDataParam pcm; /* get PCM */ er = pcm.mh.allocSeg(S0_REND_PCM_BUF_POOL, (sample*2*2)); if (er != ERR_OK) break; theOscillator->exec((q15_t*)pcm.mh.getPa(), sample); pcm.identifier = 0; pcm.callback = 0; pcm.bit_length = 16; pcm.size = sample*2*2; pcm.sample = sample; pcm.is_end = false; pcm.is_valid = true; er = theMixer->sendData(OutputMixer0, mixer_send_cb, pcm); if (er != OUTPUTMIXER_ECODE_OK) { Serial.println("OutputMixer send error: " + String(er)); return false; } } return true; } void setup() { Serial.begin(115200); initMemoryPools(); createStaticPools(MEM_LAYOUT_RECORDINGPLAYER); theOscillator = new Oscillator(); theOscillator->begin(SinWave, 1); theOscillator->set(0, 0); theOscillator->set(0, 700, 200, 50, 400); // attack=0, decay=700, sustain=50, release=400 theOscillator->lfo(0, 4, 2); theMixer = OutputMixer::getInstance(); theMixer->activateBaseband(); theMixer->create(); theMixer->setRenderingClkMode(OUTPUTMIXER_RNDCLK_NORMAL); theMixer->activate(OutputMixer0, HPOutputDevice, mixer_done_cb); theMixer->setVolume(-160, 0, 0); board_external_amp_mute_control(false); /* Unmute */ FFT.begin(WindowRectangle, rec_channel_num, 0); theRecorder = MediaRecorder::getInstance(); theRecorder->begin(rec_attention_cb); theRecorder->setCapturingClkMode(MEDIARECORDER_CAPCLK_NORMAL); theRecorder->activate(AS_SETRECDR_STS_INPUTDEVICE_MIC, rec_done_cb); theRecorder->init(AS_CODECTYPE_LPCM, rec_channel_num , rec_sampling_rate, rec_bit_length, rec_bitrate, "/mnt/sd0/BIN"); theRecorder->setMicGain(ANALOG_MIC_GAIN); theRecorder->start(); // Serial.println("rec wait usec: " + String(rec_wait_usec)); task_create("audio recording", 120, 1024, audioReadFrames, NULL); } /* Actually, sending data */ uint32_t last_time = 0; void send_signal(uint16_t hz, int repeat = SIGNAL_DURATION) { theOscillator->set(0, hz); #if 0 int i = 0; do { active(); usleep(ACTIVE_DURATION); ++i; } while (i < repeat); #else // interim measures active(); usleep(ACTIVE_DURATION); active(); usleep(ACTIVE_DURATION); #endif #if 0 uint32_t cur_time = millis(); Serial.println(String(cur_time - last_time)); last_time = cur_time; #endif } void send_char(uint8_t c, int n = SIGNAL_DURATION) { send_signal(SPACE, n); // send start bit for (int n = 0; n < 8; ++n, c = c >> 1) { /* LSB fast */ if (c & 0x01) { /* mark (1) */ send_signal(MARK, n); } else { /* space (0) */ send_signal(SPACE, n); } } send_signal(MARK, n); // send stop bit } void loop() { send_signal(MARK); // default level is mark if (Serial.available()) { char c = Serial.read(); send_char(c); } }
さて、デバッグ出力で動作も念入りに確認し、問題なさそうです。いよいよ入魂の”HELLO WORLD”!
再生できない場合、ダウンロードは🎥こちら
むちゃくちゃ遅いですけど動きましたねー。ちょっと感動です。やっぱり通信ができるって楽しいですね。せっかくなので、SPRESENSEの”HELLO WORLD”の声を録音しておきました。
さて、次はループバックから抜け出していよいよSPRESENSE同士でおしゃべりさせてみようかな。
^^
関連リンク
二月の電子工作「SPRESENSEで音響通信」
https://makers-with-myson.blog.ss-blog.jp/2021-02-02
二月の電子工作「SPRESENSEで音響通信」ー送受信の検討ー
https://makers-with-myson.blog.ss-blog.jp/2021-02-07
二月の電子工作「SPRESENSEで音響通信」ー音響ループバックを試すー
https://makers-with-myson.blog.ss-blog.jp/2021-02-14
二月の電子工作「SPRESENSEで音響通信」ーPCとSPRESENSE間の音響無線通信の実現!ー
https://makers-with-myson.blog.ss-blog.jp/2021-02-28
SPRESENSE でピンマイクを使えるようにしてみた
https://makers-with-myson.blog.ss-blog.jp/2019-10-05
SPRESENSE にピンマイクをつけて録音してみた!
https://makers-with-myson.blog.ss-blog.jp/2019-10-12
はじめてのモールス通信: アナログとデジタルのハーモニー (アマチュア無線運用シリーズ)
- 出版社/メーカー: CQ出版
- 発売日: 2011/09/01
- メディア: 単行本
SONY SPRESENSE メインボード CXD5602PWBMAIN1
- 出版社/メーカー: スプレッセンス(Spresense)
- メディア: Tools & Hardware
SONY SPRESENSE 拡張ボード CXD5602PWBEXT1
- 出版社/メーカー: スプレッセンス(Spresense)
- メディア: Tools & Hardware
二月の電子工作「SPRESENSEで音響通信」ー音響ループバックを試すー [SPRESENSE]
それらの動作を検証するため、SPRESENSE単体で音をループバックして試してみました。
■ SPRESENSEで2つの音を出力する
まずは単体動作での確認です。SPRESENSEで2つの音を出力させるために、TomonobuHayakawa氏作成の「AudioOscillator」を使って試してみます。サンプルコードを参考にスケッチを作成してみました。2つの音は分かりやすくするため「1kHz」と「2kHz」にしました。
#include <OutputMixer.h> #include <MemoryUtil.h> #include <arch/board/board.h> #include <AudioOscillator.h> #define MARK (2000) #define SPACE (1000) Oscillator *theOscillator; OutputMixer *theMixer; static void mixer_done_cb(MsgQueId id, MsgType type, AsOutputMixDoneParam *p) { Serial.println("mixer done callback"); return; } static void mixer_send_cb(int32_t id, bool is_end) { //Serial.println("mixer send callback"); return; } static bool active() { const uint32_t sample = 480; AsPcmDataParam pcm; int er; for (int i = 0; i < 5; i++) { er = pcm.mh.allocSeg(S0_REND_PCM_BUF_POOL, (sample*2*2)); if (er != ERR_OK) { Serial.println("PCM memory allocate error"); return false; } theOscillator->exec((q15_t*)pcm.mh.getPa(), sample); /* Set PCM parameters */ pcm.identifier = 0; pcm.callback = 0; pcm.bit_length = 16; pcm.size = sample * 2 * 2; pcm.sample = sample; pcm.is_end = false; pcm.is_valid = true; /* Send PCM */ er = theMixer->sendData(OutputMixer0, mixer_send_cb, pcm); if (er != OUTPUTMIXER_ECODE_OK) { Serial.println("OutputMixer send error: " + String(er)); return false; } } return true; } void setup() { Serial.begin(115200); initMemoryPools(); createStaticPools(MEM_LAYOUT_PLAYER); theOscillator = new Oscillator(); theOscillator->begin(SinWave, 1); theOscillator->set(0, 0); theOscillator->set(0,700,200,50,400); /* attack=1000,decay=700, sustain=30, release=300 */ theOscillator->lfo(0, 4, 2); theMixer = OutputMixer::getInstance(); theMixer->activateBaseband(); theMixer->create(); theMixer->setRenderingClkMode(OUTPUTMIXER_RNDCLK_NORMAL); theMixer->activate(OutputMixer0, HPOutputDevice, mixer_done_cb); theMixer->setVolume(-160, 0, 0); board_external_amp_mute_control(false); /* Unmute */ Serial.println("Start Oscillator"); } void loop() { theOscillator->set(0, MARK); active(); usleep(10000); /* 10 msec */ theOscillator->set(0, SPACE); active(); usleep(10000); /* 10 msec */ }
SPRESENSEの出力を直接PCのマイク入力につなげて音を録音してみました。
PCでキャプチャすると想定通りの波形となっています。
PCで録音してみました。未知との遭遇っぽくていいですねぇ~(古いか)
■ SPRESENSEの音を受信してFFTをかける
PCの音声出力をSPRESENSEのマイク入力につないでFFTをかけてみます。FFTはSignalProcessingライブラリのサンプルを参考にして作ってみました。サンプルはマルチコアを使っていますが、ここではメインコアのみで処理しています。
#include <MediaRecorder.h> #include <MemoryUtil.h> #include <FFT.h> #define ANALOG_MIC_GAIN 0 /* +0dB */ MediaRecorder *theRecorder; bool err_cb = false; #define FFT_LEN 256 #define CHANNEL_NUM 1 FFTClassFFT; static float pDst[FFT_LEN]; static const uint32_t rec_bitrate = AS_BITRATE_48000; static const uint32_t rec_sampling_rate = AS_SAMPLINGRATE_48000; static const uint8_t rec_channel_num = CHANNEL_NUM; static const uint8_t rec_bit_length = AS_BITLENGTH_16; static const int32_t buffer_size = FFT_LEN * sizeof(int16_t); static const uint32_t rec_wait_usec = buffer_size * (1000000 / rec_sampling_rate); static uint8_t s_buffer[buffer_size*2]; static bool rec_done_cb(AsRecorderEvent e, uint32_t r1, uint32_t r2) { return true; } static void rec_attention_cb(const ErrorAttentionParam *p) { Serial.println("Attention!"); if (p->error_code >= AS_ATTENTION_CODE_WARNING) { err_cb = true; } } void setup() { Serial.begin(115200); initMemoryPools(); createStaticPools(MEM_LAYOUT_RECORDINGPLAYER); FFT.begin(WindowRectangle, rec_channel_num, 0); theRecorder = MediaRecorder::getInstance(); theRecorder->begin(rec_attention_cb); theRecorder->setCapturingClkMode(MEDIARECORDER_CAPCLK_NORMAL); theRecorder->activate(AS_SETRECDR_STS_INPUTDEVICE_MIC, rec_done_cb); theRecorder->init(AS_CODECTYPE_LPCM, rec_channel_num , rec_sampling_rate, rec_bit_length, rec_bitrate, "/mnt/sd0/BIN"); theRecorder->setMicGain(ANALOG_MIC_GAIN); theRecorder->start(); Serial.println("Start Recording"); } void loop() { uint32_t read_size; int err; err = theRecorder->readFrames(s_buffer, buffer_size, &read_size); if (err != MEDIARECORDER_ECODE_OK && err != MEDIARECORDER_ECODE_INSUFFICIENT_BUFFER_AREA) { Serial.println("Recording Error"); theRecorder->stop(); theRecorder->deactivate(); theRecorder->end(); exit(1); } // Serial.println("buffer_size:" + String(buffer_size) + " read_size:" + String(read_size)); if (read_size < buffer_size){ usleep(rec_wait_usec); return; } FFT.put((q15_t*)s_buffer, FFT_LEN); FFT.get(pDst, 0); uint32_t index; float maxValue; int max_line = FFT_LEN/2.56; arm_max_f32(pDst, max_line, &maxValue, &index); float peakFs = index * (rec_sampling_rate / FFT_LEN); Serial.println(String(peakFs)); }
PCで録音した音の出力をSPRESENSEのマイク入力につなげてFFTで検波できるか試してみます。
出力を見ると、それっぽい値を出しています。ただ矩形の長さが微妙に違うので周波数変調間隔とFFTの受信範囲の調整が必要そうです。(この辺りが難しそう)
■ SPRESENSEのループバックで単体送受信を実現
これで送信・受信の単体テストはうまく行ったのでいよいよSPRESENSEの出力とマイク入力をつなぎあわせてループバックさせます。
送信、受信のスケッチを合体してみました。録音はスレッドにしてみました。タイミング問題がでるかなと思ったのですが、usleepを使ってうまく動かせたようです。
#include <MediaRecorder.h> #include <MemoryUtil.h> #include <AudioOscillator.h> #include <OutputMixer.h> #include <arch/board/board.h> #include <FFT.h> #define ANALOG_MIC_GAIN 0 /* +0dB */ Oscillator *theOscillator; OutputMixer *theMixer; MediaRecorder *theRecorder; bool err_cb = false; #define FFT_LEN 256 #define CHANNEL_NUM 1 #define SPACE (1000) #define MARK (2000) FFTClassFFT; static float pDst[FFT_LEN]; static const uint32_t rec_bitrate = AS_BITRATE_48000; static const uint32_t rec_sampling_rate = AS_SAMPLINGRATE_48000; static const uint8_t rec_channel_num = CHANNEL_NUM; static const uint8_t rec_bit_length = AS_BITLENGTH_16; static const int32_t buffer_size = FFT_LEN * sizeof(int16_t); static const uint32_t rec_wait_usec = buffer_size * (1000000 / rec_sampling_rate); static uint8_t s_buffer[buffer_size*2]; static bool rec_done_cb(AsRecorderEvent e, uint32_t r1, uint32_t r2) { // printf("mp cb %x %x %x\n", e, r1, r2); return true; } static void rec_attention_cb(const ErrorAttentionParam *p) { Serial.println("Attention!"); if (p->error_code >= AS_ATTENTION_CODE_WARNING) { err_cb = true; } } static void mixer_done_cb(MsgQueId id, MsgType type, AsOutputMixDoneParam *p) { Serial.println("mixer done callback"); return; } static void mixer_send_cb(int32_t id, bool is_end) { // Serial.println("mixer send callback"); return; } static void audioReadFrames() { uint32_t read_size; int er; while (true) { er = theRecorder->readFrames(s_buffer, buffer_size, &read_size); if (er != MEDIARECORDER_ECODE_OK && er != MEDIARECORDER_ECODE_INSUFFICIENT_BUFFER_AREA) { Serial.println("Recording Error"); theRecorder->stop(); theRecorder->deactivate(); theRecorder->end(); break; } // Serial.println("buffer_size:" + String(buffer_size) + " read_size:" + String(read_size)); if (read_size < buffer_size){ usleep(rec_wait_usec); continue; } FFT.put((q15_t*)s_buffer, FFT_LEN); FFT.get(pDst, 0); uint32_t index; float maxValue; int max_line = FFT_LEN/2.56; arm_max_f32(pDst, max_line, &maxValue, &index); float peakFs = index * (rec_sampling_rate / FFT_LEN); Serial.println(String(peakFs)); } } static bool active() { const uint32_t sample = 480; int er; for (int i = 0; i < 5; i++) { AsPcmDataParam pcm; /* get PCM */ er = pcm.mh.allocSeg(S0_REND_PCM_BUF_POOL, (sample*2*2)); if (er != ERR_OK) break; theOscillator->exec((q15_t*)pcm.mh.getPa(), sample); pcm.identifier = 0; pcm.callback = 0; pcm.bit_length = 16; pcm.size = sample*2*2; pcm.sample = sample; pcm.is_end = false; pcm.is_valid = true; er = theMixer->sendData(OutputMixer0, mixer_send_cb, pcm); if (er != OUTPUTMIXER_ECODE_OK) { Serial.println("OutputMixer send error: " + String(er)); return false; } } return true; } void setup() { Serial.begin(115200); initMemoryPools(); createStaticPools(MEM_LAYOUT_RECORDINGPLAYER); theOscillator = new Oscillator(); theOscillator->begin(SinWave, 1); theOscillator->set(0, 0); theOscillator->set(0, 700, 200, 50, 400); // attack=0, decay=700, sustain=50, release=400 theOscillator->lfo(0, 4, 2); theMixer = OutputMixer::getInstance(); theMixer->activateBaseband(); theMixer->create(); theMixer->setRenderingClkMode(OUTPUTMIXER_RNDCLK_NORMAL); theMixer->activate(OutputMixer0, HPOutputDevice, mixer_done_cb); theMixer->setVolume(-160, 0, 0); board_external_amp_mute_control(false); /* Unmute */ FFT.begin(WindowRectangle, rec_channel_num, 0); theRecorder = MediaRecorder::getInstance(); theRecorder->begin(rec_attention_cb); theRecorder->setCapturingClkMode(MEDIARECORDER_CAPCLK_NORMAL); theRecorder->activate(AS_SETRECDR_STS_INPUTDEVICE_MIC, rec_done_cb); theRecorder->init(AS_CODECTYPE_LPCM, rec_channel_num , rec_sampling_rate, rec_bit_length, rec_bitrate, "/mnt/sd0/BIN"); theRecorder->setMicGain(ANALOG_MIC_GAIN); theRecorder->start(); Serial.println("Recording Start!"); //Serial.println("rec wait usec: " + String(rec_wait_usec)); task_create("audio recording", 120, 1024, audioReadFrames, NULL); } #define DURATION (10000) /* usec */ void loop() { theOscillator->set(0, MARK); active(); usleep(DURATION); theOscillator->set(0, SPACE); active(); usleep(DURATION); }
Arduinoのプロッタ出力はこんな感じになります。地味な映像ですけど、ちょっと感動。
あとは変調間隔と受信間隔の調整でなんとかデータを送受信ができそうです。
(^^)/~
関連リンク
二月の電子工作「SPRESENSEで音響通信」
https://makers-with-myson.blog.ss-blog.jp/2021-02-02
二月の電子工作「SPRESENSEで音響通信」ー送受信の検討ー
https://makers-with-myson.blog.ss-blog.jp/2021-02-07
二月の電子工作「SPRESENSEで音響通信」ーFSKによるループバック・データ通信の実現ー
https://makers-with-myson.blog.ss-blog.jp/2021-02-22
二月の電子工作「SPRESENSEで音響通信」ーPCとSPRESENSE間の音響無線通信の実現!ー
https://makers-with-myson.blog.ss-blog.jp/2021-02-28
SPRESENSE でピンマイクを使えるようにしてみた
https://makers-with-myson.blog.ss-blog.jp/2019-10-05
SPRESENSE にピンマイクをつけて録音してみた!
https://makers-with-myson.blog.ss-blog.jp/2019-10-12
SONY SPRESENSE メインボード CXD5602PWBMAIN1
- 出版社/メーカー: スプレッセンス(Spresense)
- メディア: Tools & Hardware
SONY SPRESENSE 拡張ボード CXD5602PWBEXT1
- 出版社/メーカー: スプレッセンス(Spresense)
- メディア: Tools & Hardware