前回は、SPRESENSEで音響ループバックを試してみました。ただ単に音を出してマイクで音を拾うというレベルです。今回はいよいよ、FSKによるループバック・データ通信に挑みます。






■ 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;

FFTClass 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 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ブログって動画アップロードできたんですね。、今更知った… ̄▽ ̄;)


SpresenseTxTest.mp4


余談ですが、この実験を通して大文字”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;

FFTClass 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;
}
// 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”!


SpresenseTxRxTest.mp4


むちゃくちゃ遅いですけど動きましたねー。ちょっと感動です。やっぱり通信ができるって楽しいですね。せっかくなので、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








SONY SPRESENSE メインボード CXD5602PWBMAIN1

  • 出版社/メーカー: スプレッセンス(Spresense)
  • メディア: Tools & Hardware



SONY SPRESENSE 拡張ボード CXD5602PWBEXT1

  • 出版社/メーカー: スプレッセンス(Spresense)
  • メディア: Tools & Hardware