SPRESENSEの最大の弱点は通信。IoT向けプロセッサーと言ってはいるものの肝心の通信機能がない。でも、SPRESENSEには音を出し、それをキャプチャすることができます。であれば、音で通信してしまえばいいんじゃね?ということで音響通信を試してみました。
通信の基本「FSK」とは?
通信には様々な方法がありますが、まずは基本的な通信方式であるFSK(周波数変調方式)を試してみたいと思います。昔なつかしいモデムで採用されていた通信方式です。
SPRESENSEで2つの周波数の音を出す
ビットのLOW/HIGHをそれぞれ周波数に割り当てて通信します。HIGHの状態を”MARK”、LOWの状態を”SPACE”といいます。SPRESENSEでFSKを実現するには、任意の周波数の正弦波を生成できなくてはなりません。
残念ながらSPRESENSEの標準ライブラリにはそのような機能は提供されていませんが、いつもウォッチしているTomonobuHayakawa氏が公開している"AudioOscillator"がこの問題を解決してくれました。
"AudioOscillator"を使って、2つの周波数の音を出力してみました。MARKには2000Hz、SPACEには1000Hzを割り当てています。
#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をピークで検出できるか次のコードを使って試してみました。期待どおり結果が得られました。
#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と若干ズレはありますが、きちんと判別できました。(このズレは周波数分解能に起因するズレなので問題ありません)
#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バイト毎にスタートビット、ストップビットを付加する通信方式です。
引用:http://www.picfun.com/pic14.html
今回はこのサイクルを管理するためにステートマシンを使うことにしました。
少しコードは複雑になりますが、実際にデータの送受信ができるか試してみました。受信データの表示をステートマシンのストップビット状態で処理している暫定的なコードですが、期待どおりデータを送受信できることが確認できました。
今回は、MARK/SPACEには、MARK(16875Hz)、SPACE(13125Hz)を割り当てました。1000/2000Hz は生活音など様々なノイズが多く誤り率が高くなってしまうことと、なにより”うるさい”ので、あまり気にならない10kHz以上としました。(年寄り丸出し…)
#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側のプログラムは、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でエンコードして出力します。
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へ送る場面は非常に多いので活用できるケースは多そうですね。例えば、サブコアでセンサーの値を処理して、メインで送信するということができそうです。
ここでもPyAudio を使用します。PC側もシリアル通信受信用のステートマシンを使っているのでコードが多少複雑になっています。
処理については環境依存のところがあるようで、Windows10では問題なく動いているのですが、Windows11マシンでは残念ながら受信できせんでした。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):
#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で報告しますね。