LoginSignup
7
3

More than 1 year has passed since last update.

SPRESENSE で音響通信(FSK)を試してみる

Last updated at Posted at 2021-12-19

SPRESENSEの最大の弱点は通信。IoT向けプロセッサーと言ってはいるものの肝心の通信機能がない。でも、SPRESENSEには音を出し、それをキャプチャすることができます。であれば、音で通信してしまえばいいんじゃね?ということで音響通信を試してみました。

SPRESENSE 音響通信実験

通信の基本「FSK」とは?

通信には様々な方法がありますが、まずは基本的な通信方式であるFSK(周波数変調方式)を試してみたいと思います。昔なつかしいモデムで採用されていた通信方式です。

FSKの概要

SPRESENSEで2つの周波数の音を出す

ビットのLOW/HIGHをそれぞれ周波数に割り当てて通信します。HIGHの状態を”MARK”、LOWの状態を”SPACE”といいます。SPRESENSEでFSKを実現するには、任意の周波数の正弦波を生成できなくてはなりません。

残念ながらSPRESENSEの標準ライブラリにはそのような機能は提供されていませんが、いつもウォッチしているTomonobuHayakawa氏が公開している"AudioOscillator"がこの問題を解決してくれました。

"AudioOscillator"を使って、2つの周波数の音を出力してみました。MARKには2000Hz、SPACEには1000Hzを割り当てています。

SPRESENSE_FSK_TX_Test.cpp
#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) {
  return; 
}

static void mixer_send_cb(int32_t id, bool is_end) { 
  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; // 16bits 2channel
    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); 
  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で2つの音(MARK/SPACE)を判別する

送信は目処がついたので、次は受信です。周波数にエンコードされたビット情報をSPRESENSEのマイクを使って判別します。SPRESENSEのFFTライブラリを使ってこの課題を解決します。受信した音をFFTで周波数空間に変換し、ピーク周波数を検波すれば実現できそうです。

先程の音をPCで録音・再生し、SPRESENSEでMARK/SPACEをピークで検出できるか次のコードを使って試してみました。期待どおり結果が得られました。

SPRESENSE_FSK_RX_Test.cpp
#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

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);
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) {
  return;
}

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);
  } 

  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));
}

ループバックで送受信を試してみる

単体で送信/受信ができることがわかりました。次は送受信を試してみます。SPRESENSEのスピーカー出力をマイク入力につなげループバックさせることで送受信できるか確認します。

ループバックテスト

送信・受信の2つのコードをマージするだけなのですが、送受信を並行して行うために、録音処理を別タスクにしました。実際に動かしてみたところ、SPACEが935Hz、MARKが2057Hzと若干ズレはありますが、きちんと判別できました。(このズレは周波数分解能に起因するズレなので問題ありません)

SPRESENSE_FSK_TXRX_Test.cpp
#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)

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);
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) {
  return;
}

static void mixer_done_cb(MsgQueId id, MsgType type, AsOutputMixDoneParam *p) {
  return;
}

static void mixer_send_cb(int32_t id, bool is_end) { 
  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;
    } 

    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!");
  task_create("audio recording", 120, 1024, audioReadFrames, NULL);
}


#define DURATION (20000) /* usec */
void loop() {

  theOscillator->set(0, MARK); 
  active();
  usleep(DURATION); 

  theOscillator->set(0, SPACE);
  active();
  usleep(DURATION); 
}

ループバックでデータを送受信してみる

原理は確認できたので、いよいよループバックでデータを送受信をしてみます。データの送受信にはシリアル通信を用います。シリアル通信は1バイト毎にスタートビット、ストップビットを付加する通信方式です。

FSK Start/Stop Bit
引用:http://www.picfun.com/pic14.html

今回はこのサイクルを管理するためにステートマシンを使うことにしました。

シリアル通信状態遷移

少しコードは複雑になりますが、実際にデータの送受信ができるか試してみました。受信データの表示をステートマシンのストップビット状態で処理している暫定的なコードですが、期待どおりデータを送受信できることが確認できました。

今回は、MARK/SPACEには、MARK(16875Hz)、SPACE(13125Hz)を割り当てました。1000/2000Hz は生活音など様々なノイズが多く誤り率が高くなってしまうことと、なにより”うるさい”ので、あまり気にならない10kHz以上としました。(年寄り丸出し…)

SPRESENSE_FSK_SERIAL_TEST.cpp
#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) {
  return true;
}

static void rec_attention_cb(const ErrorAttentionParam *p) {  
  return;
}

static void mixer_done_cb(MsgQueId id, MsgType type, AsOutputMixDoneParam *p) {
  return; 
}

static void mixer_send_cb(int32_t id, bool is_end) { 
  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;
    } 

    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.

    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(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();

  task_create("audio recording", 120, 1024, audioReadFrames, NULL);
}

void send_signal(uint16_t hz) {
  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
}

void send_char(uint8_t c) {
  send_signal(SPACE);  // send start bit
  for (int n = 0; n < 8; ++n, c = c >> 1) { /* LSB fast */
    if (c & 0x01) { /* mark (1) */
      send_signal(MARK);
    } else { /* space (0) */
      send_signal(SPACE);
    }
  } 
  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からSPRESENSEへデータを送信してみる

SPRESENSE同士でデータの送受信ができることはわかりました。でもそれだけではつまらないので、次はPCをからSPRESENSEへデータを送ってみたいと思います。

PC->SPRESENSE送信テスト

PC側のプログラムは、Pythonを使っています。PyAudio を使用していますが、公式は3.7以上のバージョンには対応していないようです。Pythonのバージョンが3.7以上の場合は、次のサイトからwhlファイルをダウンロードしてpipを使ってインストールしてください。

(base) PS D:\cygwin64\home> pip install .\PyAudio-0.2.11-cp37-cp37m-win_amd64.whl
Processing d:\cygwin64\home\pyaudio-0.2.11-cp37-cp37m-win_amd64.whl
Installing collected packages: PyAudio
Successfully installed PyAudio-0.2.11

データを送信するPythonのコードを示します。キーボードで入力した文字列をFSKでエンコードして出力します。

fsk_tx.py
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
#      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 = 480
      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 20msec
        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()

起動すると何もメッセージは表示しませんが、キーボードの入力待ち状態になります。エンターキーを押すと一文字ずつ送信します。

(base) PS D:\cygwin64\home> python .\fsk_tx.py
HELLO
H
E
L
L
O

SPRESENSEからPCへデータを送信してみる

最後にSPRESENSEからPCへデータ送信をしてみましょう。SPRESENSEで得られたデータをPCへ送る場面は非常に多いので活用できるケースは多そうですね。例えば、サブコアでセンサーの値を処理して、メインで送信するということができそうです。

SPRESENSE->PC送信テスト

ここでもPyAudio を使用します。PC側もシリアル通信受信用のステートマシンを使っているのでコードが多少複雑になっています。

処理については環境依存のところがあるようで、Windows10では問題なく動いているのですが、Windows11マシンでは残念ながら受信できせんでした。PyAudioのバージョンも関係しているのかもしれません。(おかげで投稿が遅れてしまいました)もう少し、環境によらない方法を考える必要がありそうです。

fsk_rx.py
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):
    #print(self.cur_state, end=',')
    #print(     sbit,      end=',')
    #print(self.bpos,      end=',')
    #print(self.frame_cnt)
    return

  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 > (SPACE-(fc-SPACE)) and peak_f < fc and space > mark): 
        #print("Space")
        sbit = 0
      elif (peak_f < (MARK+(MARK-fc)) and peak_f > fc 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()

実際に動かし、SPRESENSEからのデータ受信をすると次のように表示されます。

(base) PS C:\cygwin64\home\python> python .\fsk_rx.py
Start RX
HELLO
HOW ARE YOU?
Yoshino Taro
Thank you!

おわりに

SPRESENSEは豊富な計算資源とマイコンとしては比較的リッチなメモリのおかげで信号処理が簡単にできます。また、音を手軽に処理ができるためこのような通信実験を行う機材としても最適でした。SPRESENSEを使って通信の原理から組み上げることでより理解が深まった気がします。

ただ、SPRESENSEでも問題点はいくつかあります。ひとつは OS のTick時間が20ミリ秒単位になっているため、スレッドに分割するとそれ以上の時間間隔の処理ができないことです。現在は50bpsが限界になります。オーディオ機能をサブコアで処理できるようになれば、もっと通信速度を出せるようになると思います。

そのほか、誤り訂正が実装されていないなど足りない部分はあるので、少しずつ改善を加えて使えるものにしていきたいと思います。

ここで記述した以外にも多くの検討をしています。詳細については、私のブログで紹介していますので、興味をもたれた方はぜひ訪れてみてください。今後、改善内容についても少しずつ上げていきたいと思います。まとまったら、Qiitaで報告しますね。

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3