2016年1月11日月曜日

ESP-WROOM-02でSPI接続のADCから取得したデータをUDPで送出


緑のマイク基板と赤いESP基板の間にあるのがMCP3002。赤い基板上で右側のLEDがうすぼんやりと点灯しているのは、SPIのDIN信号。

50kspsでサンプリングしたデータをUDPに激しく送信中。

■SPI接続のADC■

ADCもいろいろありますが、数十Ksps以上のシリアルはSPI接続のものが多いです。

今回、MCP3002という精度10bit、入力2chのADCを使います。これは5v電源なら200kspsまで動き(2.7vだと75ksps)、そのわりに秋月で180円という大変おトクなチップです。マイクをつなげばお手頃な音声入力などが可能です。

SPIはあまり使ったことがないので、こちらの記事(ESP8266 (ESP-WROOM-02) でセンサーを扱う)を参考にさせていただきました。ありがとうございます。

SPIドライバはこちらからダウンロードします(MetalPhreak/ESP8266_SPI_Driver)。通常のArduinoライブラリとは形式が異なるので、以下の作業でArduino IDEが認識できるようにします(もっといい方法をご存知の方、ご教示いただければ幸いです)。

  • ダウンロードしたらzipを解凍する
  • フォルダの名前をESP8266_SPI_Driverに変更する
  • フォルダの中のspi_register.h, spi.h, spi.cをESP_SPI_Driver直下に移動
  • Arduinoのlibrariesフォルダ直下へ移動
  • Arduino IDEを再起動

動作させてみたところ、1回のデータ取得に要する時間は11-12μ秒程度でした(関数を100回呼び出して計測)。

■ESP-WROOM-02からUDP■

1回のデータ送信に要する時間は60-70m秒程度でした(関数呼び出し)。

■ESP用のソース■

なるべく一定の速度でサンプリングしつつ、UDPで送信しなければならないのですが、両者のスピードが違いすぎるので何らかのマルチタスクっぽい仕組みが必要です。Arduinoではこういう場合Tickerを使うのですが、Tickerは最小単位がミリ秒なので、1秒間に1000データしか取れません。それではせっかく高速のSPIを使う意味がありません。

幸いなことにESPにはos_timer_arm_usというμ秒単位のインターバルが用意されていますので、これを使います。なお、これを使う場合には、setupの始めの方でsystem_timer_reinit()を使って初期化し直す必要がありますので、ご注意を。これを使わないとミリ秒でしか動いてくれません。

ということで、おおまかな構成としては

  • 初期化ルーチンでWifi接続してから、20μ秒ごとのインターバルを開始
  • インターバルで呼ばれたらSPIからADCのデータを取り込みバッファにしまう
  • バッファが一杯になったらフラグを立てて別のバッファに切り替える
  • loop()ではバッファ一杯になったかどうかのフラグをチェックし、いっぱいになっていたらUDPでデータを送信します。

#define USE_US_TIMER 1
extern "C" {
#include <spi.h>
#include <spi_register.h>
#include "user_interface.h"
}
#include <ESP8266WiFi.h>
#include <WiFiUDP.h>
static WiFiUDP wifiUdp;
static const char *kRemoteIpadr = "192.168.0.70";
static const int kRmoteUdpPort = 5431;
#define MaxBuffer 256
#define ShouldSend0 0
#define ShouldSend1 1
#define ShouldNotSend 2
int shouldSend = ShouldNotSend;
int selector = 0;
unsigned short *buf[2];
static unsigned short buf1[MaxBuffer];
static unsigned short buf2[MaxBuffer];
int counter[2];
ETSTimer Timer;
int seq = 0;
int temp = 0;
void fetchFunction(void *temp) {
buf[selector][counter[selector]++] = check(0);
if (counter[selector] >= MaxBuffer) {
buf[selector][0] = counter[selector];
shouldSend = selector;
buf[selector][1] = seq++;
if (seq > 500) seq = 0;
selector = 1 - selector;
counter[selector] = 1; // start from 1. [0] is counter
}
}
void setup() {
Serial.begin(115200);
Serial.print("\n");
system_timer_reinit(); // to use os_timer_arm_us
//
// wifi setup
//
static const int kLocalPort = 7000;
WiFi.begin("ssid", "password");
while( WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print('.');
}
Serial.println("connected");
wifiUdp.begin(kLocalPort);
//
// buffer clean up
//
buf[0] = buf1;
buf[1] = buf2;
counter[0] = counter[1] = 1;
selector = 0;
//
// adc setup
//
spi_init(HSPI);
Serial.println(" end.");
// fetch every 20uSec
os_timer_setfn(&Timer, fetchFunction, NULL);
os_timer_arm_us(&Timer, 20, true);
}
void loop() {
if (shouldSend != ShouldNotSend) {
wifiUdp.beginPacket(kRemoteIpadr, kRmoteUdpPort);
char *conv = (char *)buf[shouldSend];
wifiUdp.write(conv, counter[shouldSend]*2);
wifiUdp.endPacket();
Serial.print("should send = ");
Serial.print(shouldSend);
Serial.print(", counter = ");
Serial.print(counter[shouldSend]);
Serial.print(", size = ");
Serial.println(sizeof(uint16));
shouldSend = ShouldNotSend;
}
}
uint32 check(int channel) {
// start bit (1 bit) : 1
// SGL/DIFF bit (1 bit) : SGL:1
// select the input channel (3 bit) : CH0:0, CH1:1
uint8 cmd = channel == 0 ? 0b1101 : 0b1111;
const uint32 COMMAND_LENGTH = 4;
const uint32 RESPONSE_LENGTH = 12;
uint32 retval = spi_transaction(HSPI, 0, 0, 0, 0, COMMAND_LENGTH, cmd, RESPONSE_LENGTH, 0);
retval = retval & 0x3FF; // mask to 10-bit value
return retval;
}


■検証用のJavaコード■

UDPでちゃんとデータを送り出せているのかを検証するために受信側のコードをJavaで作りました。UDPを受信して、連番と最小値/最大値などを表示するだけです。グラフ化したり音声を出力する根性がなかったので値だけです。

こちらの記事を参考にさせていただきました。ありがとうございます。
http://k-hiura.cocolog-nifty.com/blog/2011/07/java-6e01.html

// UDP受信プログラムより
// http://yuu7777.fc2web.com/javanet6/12.htm
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketAddress;
public class UDPRecTest{
//UDP受信用ソケット
DatagramSocket recSocket = new DatagramSocket(5431);
//相手先アドレス
SocketAddress sockAddress;
//
// main
//
public static void main(String [] args) throws Exception{
UDPRecTest test = new UDPRecTest();
// 受信ループ
while(true) test.receive();
}
//
// 受信クラス
//
public UDPRecTest() throws Exception {
}
//
// byteを符号なしの整数値に変換
//
public int btoi(byte inValue) {
return inValue & 0xFF;
}
//
// パケット受信
//
public void receive() throws Exception {
byte buf[] = new byte[2048];
DatagramPacket packet= new DatagramPacket(buf,buf.length);
recSocket.receive(packet);
sockAddress = packet.getSocketAddress();
int len = packet.getLength(); // 受信バイト数取得
// 先頭に入っているデータサイズを取得
int size = (btoi(buf[1]) << 8) + btoi(buf[0]);
// 2番目に入っているseq番号(0-511)を取得
int seq = (btoi(buf[3]) << 8) + btoi(buf[2]);
// データの最小値と最大値を検出
int low = 1024;
int high = 0;
for (int i = 2; i < size; i++) {
int value = (btoi(buf[i*2+1]) << 8) + btoi(buf[i*2]);
if (low > value) low = value;
if (high < value) high = value;
}
// 取得したデータのサマリを表示
System.out.println("seq="+String.valueOf(seq)
+ ",low="+String.valueOf(low)+",high="+String.valueOf(high)
+ ",size=" + String.valueOf(size) + ",len=" + String.valueOf(len) + ",from:"+ sockAddress.toString());
}
}
view raw UDPRecTest.java hosted with ❤ by GitHub

■ハマりどころ■

  • 自家製ESPモジュールからIO15が出てない。IO15のプルダウン抵抗近くのviaホールにハンダ付けして引っ張り出す。
  • SPIが動かない。これは当初参考にしていた記事の配線図が間違っていたため。私の2時間を返せ。ということで著者さんには間違ってますよ、とメールしておいた。まだ直ってないなぁ。
  • 接続直後にESPが暴走する。あれさっきまで動いてたのに…?? と思ったら、初歩的なC言語的間違い。30年ぶりぐらいにやらかした。
      char **buf; buf[0] = ptr1; buf[1] = ptr2; 
  • ESPはos_timer_arm_usってAPIでマイクロ秒単位のTickerを使えるはずなのに、ミリ秒単位でしか動いてくれない。改めてSDKマニュアルを読んだら、system_timer_reinit()で初期化しないとダメよと書いてあった。
  • Javaの符号なし整数にまたハマってしまった。いい加減覚えろよ>俺

5 件のコメント:

  1. この記事大変参考になりました。ありがとうございました。
    大変恐縮ですが2点質問させてください。
    1)現在のStaging ESP8266 coreではSPIライブラリが
    サポートされているようなのですが、
    MetalPhreakさんのライブラリを使った理由はなんでしょう?
    速度ですか?
    2)記事の配線図はどこのサイトのことでしょうか? IO12:MISO, IO13:MOSI, IO14:CLKで正しいのでしょうか?

    突然不躾なコメントで申し訳なく。お手漉きにコメントいただければ幸いです。

    返信削除
    返信
    1. お役に立てて何よりです。ご質問の件ですが、

      1) 最初試した時にうまく通信ができなかったので、まずSPIを疑ってみました。最初に試したMetalPhreakさんのライブラリでうまく行ったのでそのままです。ぜひ標準のライブラリでも試してみてください。

      2)大変失礼しました。リンク修正しました。配線は下記のとおりです。

      // define pin connections
      #define CS_PIN 15
      #define CLOCK_PIN 14
      #define MOSI_PIN 13
      #define MISO_PIN 12

      削除
  2. 標準のSPIライブラリでも問題なく動作しました。

    詳細については後日記事にまとめます。とりあえずお知らせまで。

    返信削除
    返信
    1. 質問しっぱなしで恐縮です。
      遅ればせながらESP8266のSPI周辺の挙動をまとめました(URL)
      結論から言えばSPI, HSPIどちらでも使えますが
      HSPIにはちょっと癖がありました。

      また、最高速度はHSPIの方が高いようです。
      H/Wの実現方法の違いというより、ライブラリの出来の違いによるところが大きいようですが・・・
      素人調べゆえ釈迦に説法とは思いますが、とりあえずご笑納ください。

      削除
    2. わざわざレポートありがとうございます。

      このブログサービスはURLの入力ができない仕様のようです、申し訳ありません。

      その後、標準SPIライブラリに移行してしまったので追試できていないのですが、私はHSPIだけしか試していませんでした。I2Cと比べるとSPIはいろいろ方言が多くて、基本的な通信が通るまで大変な気がします。フォーマットそのものの方言もありますが、用語も微妙に違ったり負論理が前提だったり逆だったり、で頭を抱えることも多いです。

      私もこんなレベルですので、今後とも情報交換よろしくお願い致します。

      削除

注: コメントを投稿できるのは、このブログのメンバーだけです。