ESP32にSPI to Ethernet モジュール USR-ES1を接続した話

ESP32 + USR-ES1から同一LAN内のPCにTCPでデータ転送を行ったとき、どれくらいの性能が出るのかを調べるのに使ったプログラムやスケッチと、測定時の画面コピーなどを掲載した。ブログとしての本文はこちら。USR-ES1(W5500)のコントロールには、Arduino IDEで容易にインストールできるEthernet2ライブラリをそのまま使っている。

本文側にも書いたが、有線LANで接続する際には送信側と受信側は同じスイッチングハブ(1000BASE-T) に接続している。受信(TCPのaccept() )側は、同じWindows 7 のノートPCを使っていて tcp/8888 でlisten() している。

Windows用プログラム

Windowsでは送信側と受信側双方とも同じプログラムを使う。以下のソースをVisual Studio 2017 (Community版)を使ってビルドした。再現する際には、WIN32コンソールアプリケーションとしてプロジェクトを新規作成し、好みの名前でC++ソースを新規追加して以下を貼り付けて保存すればよいはず。

#include "stdafx.h"
#include <stdio.h>
#include <winsock2.h>

#pragma warning(disable:4996)
#pragma comment(lib, "Ws2_32.lib")
#pragma comment(lib, "winmm.lib")

#define LISTEN_PORT	8888
#define MEGA	(1024 * 1024)

static void setup_buffer(char* p, int sz) {
	memset(p, 0, sz);
	for (int i = 0; i < sz; i++)
		*p++ = i % 256;
}

static bool check_buffer(char* p, int sz) {
	for (int i = 0; i < sz; i++) {
		if (*p++ != (char)(i % 256))
			return false;
	}
	return true;
}

int main(int ac, char* av[]) {
	SOCKET work_socket;
	WSADATA wsaData;
	char dest_ip[20] = "";
	int megas = 1;
	int buf_size = 1024;
	if (ac < 2) {
		printf(" -M megas -B buffer_size [dest_ip_address]");
		return 1;
	}
	char last = 0;
	for (int i = 1; i < ac; i++) {
		char* p = av[i];
		if (*p == '-')
			last = *++p;
		else if (*p) {
			if (last == 'M')
				megas = atoi(p);
			if (last == 'B')
				buf_size = atoi(p);
			else if (!last)
				strncpy(dest_ip, p, sizeof(dest_ip));
			last = 0;
		}
	}
	double factor = 1024.0 / (double)buf_size;
	LONGLONG transfer_size = buf_size * 1024 * (megas * factor);
	printf("transfer_size=%lld bytes, dest=%s\n", transfer_size, dest_ip);

	char* pBuffer = (char*)malloc(buf_size);
	if (!pBuffer) {
		printf("buffer alloc failed.");
		return -1;
	}
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != NO_ERROR) {
		printf("WSAStartup() failed. %d\n", WSAGetLastError());
		return 1;
	}
	if (!*dest_ip) {
		SOCKET listen_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
		sockaddr_in listen_addr;
		memset((void*)&listen_addr, 0, sizeof(sockaddr_in));
		listen_addr.sin_port = htons(LISTEN_PORT);
		listen_addr.sin_family = AF_INET;
		listen_addr.sin_addr.s_addr = htonl(INADDR_ANY);
		if (bind(listen_socket, (SOCKADDR *)&listen_addr, sizeof(sockaddr_in)) != NO_ERROR) {
			printf("bind() failed. %d\n", WSAGetLastError());
			return 2;
		}
		for (;;) {
			printf("Listen start...");
			if (listen(listen_socket, 3) != NO_ERROR) {
				printf("listen() failed. %d\n", WSAGetLastError());
				closesocket(listen_socket);
				WSACleanup();
				return 3;
			}
			work_socket = accept(listen_socket, NULL, NULL);
			if (work_socket == INVALID_SOCKET) {
				printf("accept failed. %d\n", WSAGetLastError());
				closesocket(listen_socket);
				WSACleanup();
				return 4;
			}
			printf("\naccept socket\n");
			DWORD start = timeGetTime();
			LONGLONG byte_transed = 0;
			int cur = 0;
			while (byte_transed < transfer_size) {
				if (cur == 0)
					memset(pBuffer, 0, buf_size);
				int n = recv(work_socket, &pBuffer[cur], buf_size - cur, 0);
				if (n == SOCKET_ERROR) {
					printf("recv failed. %d\n", WSAGetLastError());
					closesocket(work_socket);
					closesocket(listen_socket);
					WSACleanup();
					return 5;
				}
				else if (n == 0) {
					printf("connection closed.\n");
					break;
				}
				byte_transed += n;
				if (n != buf_size) {
					cur += n;
					if (cur == buf_size) {
						cur = 0;
						n = buf_size;
					}
				}  else
					cur = 0;
				if ( n == buf_size && !check_buffer(pBuffer, buf_size)) {
					printf("recv data mismatch.\n");
					closesocket(work_socket);
					closesocket(listen_socket);
					WSACleanup();
					return 6;

				}
			}
			closesocket(work_socket);
			DWORD elapsed = timeGetTime() - start;
			double mbps = (double)(byte_transed  / (elapsed / 1000.0)) / MEGA;
			printf("receive %lld bytes in %d msec. %.2f MBytes /sec\n", byte_transed, elapsed, mbps);
		}
	}
	else {
		unsigned long addr = inet_addr(dest_ip);
		if (addr == INADDR_NONE) {
			printf("invalid destination address.\n");
			return 10;
		}
		sockaddr_in dest_addr;
		memset((void*)&dest_addr, 0, sizeof(sockaddr_in));
		dest_addr.sin_port = htons(LISTEN_PORT);
		dest_addr.sin_family = AF_INET;
		dest_addr.sin_addr.s_addr = addr;
		double acc_mbps = 0.0;
		int count = 0;
		while(count < 10) {
			printf("start %d : ", ++count);
			SOCKET send_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
			setup_buffer(pBuffer, buf_size);
			DWORD start = timeGetTime();
			LONGLONG byte_transed = 0;
			if (connect(send_socket, (SOCKADDR*)&dest_addr, sizeof(dest_addr)) != NO_ERROR) {
				printf("connect failed. %d\n", WSAGetLastError());
				closesocket(send_socket);
				WSACleanup();
				return 11;
			}
			int send_size = buf_size;
			while (byte_transed < transfer_size) {
				int n = send(send_socket, &pBuffer[buf_size - send_size], send_size, 0);
				if (n == SOCKET_ERROR) {
					printf("send failed. %d\n", WSAGetLastError());
					closesocket(send_socket);
					WSACleanup();
					return 12;
				}
				byte_transed += n;
				send_size = buf_size;
				if (n < buf_size)
					send_size -= n;
			}
			shutdown(send_socket, SD_SEND);
			closesocket(send_socket);
			DWORD elapsed = timeGetTime() - start;
			double mbps = (double)(byte_transed / (elapsed / 1000.0)) / MEGA;
			acc_mbps += mbps;
			printf("send %lld bytes completed. %d msec. %.2f MBytes/sec\n", byte_transed, elapsed, mbps);
		}
		printf("average = %.2f MBytes/sec\n", acc_mbps / count);

	}
	WSACleanup();
	free(pBuffer);
	return 0;
}

使い方および動作

Windowsのコマンドプロンプトを開き、実行ファイル名とオプションを指定して実行する。
オプション -B でバッファサイズをバイト単位で、-M で1回ごとの転送バイト数をメガバイト単位で指定する。バッファサイズは256の整数倍(256, 512, 1024, 2048, 4096, …) を指定すること。
オプション文字無しの引数があった場合、送信先IPアドレスとして使う。送信先IPアドレスがあれば送信側、なければ受信側として動作する。

送信側と受信側とでバッファサイズや転送バイト数は同じにしておく。送信側は-Mオプションで指定する転送バイト数の送信を10回繰り返し転送レートの平均値を表示する。受信側は転送バイト数を受信するごとに転送レートを表示し実行を続ける。

送信側の転送バッファには、setup_buffer() によって先頭から順に0から255 までの値が繰り返して格納されており、受信側はバッファサイズ分受信するたびごとにcheck_buffer() によって内容をチェックする。そのとき想定していないバイトが見つかれば、その時点でプログラムを終了する。

例えば実行ファイル名が SockTest.exe、受信側が 192.168.1.100 、バッファサイズ2048バイト、転送バイト数を100(メガバイト)とするときには以下のように実行する。

受信側 SocketTest.exe -B 2048 -M 100
送信側 SocketTest.exe -B 2048 -M 100 192.168.1.100

コマンドの実行やESP32のリセットによって新規に送信を開始するときには、あらかじめ受信側プログラムを起動し直しておく必要がある。受信側を停止するためには、コマンドプロンプトに対してCtrl+C を打ち込む。

WindowsPC – ノートPC間の実行時表示(送信側)

送信側にはWindows 10のデスクトップPCを使った。

...\Release>SocketApp1.exe -B 2048 -M 100 192.168.1.100
transfer_size=104857600 bytes, dest=192.168.1.100
start 1 : send 104857600 bytes completed. 900 msec. 111.11 MBytes/sec
start 2 : send 104857600 bytes completed. 1014 msec. 98.62 MBytes/sec
start 3 : send 104857600 bytes completed. 1012 msec. 98.81 MBytes/sec
start 4 : send 104857600 bytes completed. 1074 msec. 93.11 MBytes/sec
start 5 : send 104857600 bytes completed. 857 msec. 116.69 MBytes/sec
start 6 : send 104857600 bytes completed. 894 msec. 111.86 MBytes/sec
start 7 : send 104857600 bytes completed. 1132 msec. 88.34 MBytes/sec
start 8 : send 104857600 bytes completed. 850 msec. 117.65 MBytes/sec
start 9 : send 104857600 bytes completed. 839 msec. 119.19 MBytes/sec
start 10 : send 104857600 bytes completed. 907 msec. 110.25 MBytes/sec
average = 106.56 MBytes/sec

...\Release>

転送レートは毎秒106メガバイトといったところだった。毎回の転送レートにばらつきがあるのは、送信側のWindows PCで山のようにプロセスが動いているためと思われる。まあ、1秒間に100メガバイトも転送できていればいいだろう。

ESP32 + USR-ES1用テストスケッチ(有線接続)

ESP32 + USR-ES1の調査には送信側として以下のスケッチを使った。受信側は上と同じWindows用のプログラムを利用。Windows PC間の転送に比べるととても遅くて時間がかかるので、転送バイト数は10メガバイトとした。

#if defined(WIZ550io_WITH_MACADDRESS)
#undef WIZ550io_WITH_MACADDRESS
#endif
#include <Ethernet2.h>

#define LED 2
#define ETH_RESET 4
#define SPI_SS  5
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
IPAddress s_ip(192,168,1,100);
IPAddress myip(192, 168, 1, 123);
IPAddress mydns(192, 168, 1, 1);
IPAddress mygateway(192, 168, 1, 1);
IPAddress mysubnet(255, 255, 255, 0);

#define DEST_PORT 8888
#define MEGA  1024 * 1024
#define BUFFER_SIZE 2048
#define TRANSFER_BYTES 10 * MEGA

char buffer[BUFFER_SIZE];
EthernetClient client;

void die() {
  digitalWrite(LED, 1);
  delay(500);
  digitalWrite(LED, 0);
  delay(500);
}

void setup_buffer(char* p, int sz) {
  for(int i = 0; i < sz; i++)
    *p++ = (char)i % 256;
}

void setup() {
  delay(10);
  Serial.begin(115200);
  pinMode(LED, OUTPUT);
  pinMode(ETH_RESET, OUTPUT);
  digitalWrite(ETH_RESET, 0);
  delay(1);
  digitalWrite(ETH_RESET, 1);
  delay(50);
  Ethernet.init(SPI_SS);
  Ethernet.begin(mac, myip, mydns, mygateway, mysubnet);
}

int loop_count = 0;
float total_mbps = 0.0;
void loop() {
  unsigned long start = millis();
  Serial.printf("start %d : ", loop_count++ + 1);
  if (client.connect(s_ip, DEST_PORT) != 1) {
    Serial.printf("connection failed. status = %d\n", client.status());
    die();
  }
  setup_buffer(buffer, sizeof(buffer));
  int transed_bytes = 0;
  while(transed_bytes < TRANSFER_BYTES) {
    size_t n = client.write(buffer, sizeof(buffer));
    if (n == 0) {
      Serial.printf("connection closed or send failed. status = %d\n", client.status());
      die();
    }
    transed_bytes += n;
  }  
  client.flush();
  client.stop();
  unsigned long elapsed =  millis() - start;
  float mbps = (float)(transed_bytes / MEGA) / elapsed / 1000.0;
  total_mbps += mbps;
  Serial.printf("send %d bytes completed. %d msec. %.2f MBytes/sec\n", transed_bytes, elapsed,  mbps);
  if (loop_count == 10) {
    Serial.printf("average = %.2f MBytes/sec\n\n", total_mbps / loop_count);
    loop_count = 0;
    total_mbps = 0.0;
  }
  delay(10);
}

Ethernet.init(SPI_SS); によりスレーブセレクトにSPI_SS ( == 5) を使っていることを教えている。GPIO10を使うならば省略可能なのだが、ESP-WROOM-32ではGPIO10は内蔵フラッシュに接続されているので使えない。

Ethernet2ライブラリに含まれている EthernetClientクラスの write() メソッドを使って送信バッファの内容を送り出しているが、このメソッドに対してW5500の送信バッファの最大値(2048バイト)を超えるバイト数を指定した場合、先頭の2048バイトしか送信されず、実行上はエラーにもならないことに注意が必要だろう。
2048バイト以下の場合、send() の引数として指定したバイト数はすべて送信できますよ、というコードになっているので戻り値の評価はエラーの有無のみとしている。

そのあたりの実装は、Ethernet2ライブラリのutility/socket.cppのsend() を参照。

ESP32 + USR-ES1 – ノートPC間の実行時表示

リセットボタン(EN-SW)を押すとシリアルモニタには以下のように表示された。

ets Jun  8 2016 00:22:57

rst:0x1 (POWERON_RESET),boot:0x17 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:1
load:0x3fff0018,len:4
load:0x3fff001c,len:956
load:0x40078000,len:0
load:0x40078000,len:13256
entry 0x40078a90
start 1 : send 10485760 bytes completed. 23945 msec. 0.44 MBytes/sec
start 2 : send 10485760 bytes completed. 23336 msec. 0.45 MBytes/sec
start 3 : send 10485760 bytes completed. 23335 msec. 0.45 MBytes/sec
start 4 : send 10485760 bytes completed. 23330 msec. 0.45 MBytes/sec
start 5 : send 10485760 bytes completed. 23337 msec. 0.45 MBytes/sec
start 6 : send 10485760 bytes completed. 23334 msec. 0.45 MBytes/sec
start 7 : send 10485760 bytes completed. 23341 msec. 0.45 MBytes/sec
start 8 : send 10485760 bytes completed. 23338 msec. 0.45 MBytes/sec
start 9 : send 10485760 bytes completed. 23325 msec. 0.45 MBytes/sec
start 10 : send 10485760 bytes completed. 23330 msec. 0.45 MBytes/sec
average = 0.45 MBytes/sec

start 1 : send 10485760 bytes completed. 23327 msec. 0.45 MBytes/sec
...

転送レートは毎秒0.45メガバイトといったところだった。シリアル通信よりは格段に高速なことは間違いないが、ちょっと物足りないから、ライブラリを書き換えて高速化を試みてみた。それについては本文側で触れている。

ESP32 WiFi使用時のテストスケッチ

参考用にESP32をWIFI_STAとして使った場合の転送レートを得てみた。受信側ノートPCは、WiFiルーターのローカル側ハブと2台のスイッチングハブを介してLANケーブルで接続されている。なお、うちのWiFiルーターは古いので有線側リンク速度は100Mbpsである。

以下のようなスケッチを使った。

#include <WiFi.h>
#define LED 2
// wifi
const char* ssid = "ssid";
const char* password = "password";

#define DEST_PORT 8888
// destination
const IPAddress s_ip(192,168,1,100);
// static ip info
const IPAddress myip(192,168,1,99);
const IPAddress gwip(192,168,1,1);
const IPAddress mask(255,255,255,0);

WiFiClient client;

void die() {
  digitalWrite(LED, 1);
  delay(500);
  digitalWrite(LED, 0);
  delay(500);
}

#define MEGA  1024 * 1024
#define BUFFER_SIZE 2048
#define TRANSFER_BYTES 10 * MEGA
char buffer[BUFFER_SIZE];

void setup_buffer(char* p, int sz) {
  for(int i = 0; i < sz; i++)
    *p++ = (char)i % 256;
}

#define WAIT_MS 50
#define WAIT_LIMIT  (int)(15000 / WAIT_MS)
bool ap_connect() {
  int n;
  int count = 0;
  int retry = 0;
  Serial.printf("\nstart ap_connect()\n");
  WiFi.config(myip, gwip, mask, gwip);
  WiFi.mode(WIFI_STA);
  WiFi.disconnect();
  delay(10);
  WiFi.begin(ssid, password); 
  while ((n = WiFi.status()) != WL_CONNECTED) {
    Serial.print(n);
    delay(WAIT_MS);
    if (n == WL_NO_SSID_AVAIL || n == WL_CONNECT_FAILED) {
      WiFi.reconnect();
      Serial.print("+");
      count = 0;
      retry++;
    }
    if (count++ > WAIT_LIMIT || retry > 3) {
      Serial.print("\nap_connect() failed\n");
      return false;
    }
  }
  Serial.print("\nap_connect() done.\n");
  return true;
}

void setup() {
  delay(1);
  pinMode(LED, OUTPUT);
  digitalWrite(LED, 0);
  Serial.begin(115200);
  Serial.println("esp32_sta_speed1 start.");
  if (!ap_connect()) {
    Serial.print("wifi connect failed.\n");
    die();
  }
}

int loop_count = 0;
float total_mbps = 0.0;
void loop() {
  unsigned long start = millis();
  Serial.printf("start %d\n", loop_count++ + 1);
  if (client.connect(s_ip, DEST_PORT) != 1) {
    Serial.printf("connection failed. errno = %d\n", errno);
    die();
  }
  setup_buffer(buffer, sizeof(buffer));
  int transed_bytes = 0;
  int left = sizeof(buffer);
  int n = 0;
  while(transed_bytes < TRANSFER_BYTES) {
    size_t n = client.write(&buffer[n], left);
    if (n == 0) {
      Serial.printf("connection closed or send failed. errno = %d\n", errno);
      die();
    }
    transed_bytes += n;
    left -= n;
    if (left != 0)
      continue;        
    n = 0;
    left = sizeof(buffer);
  }  
  client.flush();
  client.stop();
  unsigned long elapsed =  millis() - start;
  float mbps = (float)(transed_bytes / MEGA) / elapsed / 1000.0;
  total_mbps += mbps;
  Serial.printf("send %d bytes completed. %d msec. %.2f MBytes/sec\n", transed_bytes, elapsed,  mbps);
  if (loop_count == 10) {
    Serial.printf("average = %.2f MBytes/sec\n\n", total_mbps / loop_count);
    loop_count = 0;
    total_mbps = 0.0;
  }
  delay(10);
}

いつも書いているような典型的なWIFI_STAスケッチである。

WiFiClientwrite(); の実装では、指定の転送バイト数を10回まわるループ内で送信し終わらなければ、送信できたバイト数を返すようになっている(これが普通だと思うが)。そのため、一度に送信できなかった残りについても考慮している。

ESP32 WiFi-ノートPC間の実行結果

start ap_connect()
666666666666666666666666666666666666666666666666666666666666666
ap_connect() done.
start 1 : send 10485760 bytes completed. 11791 msec. 0.89 MBytes/sec
start 2 : send 10485760 bytes completed. 8475 msec. 1.24 MBytes/sec
start 3 : send 10485760 bytes completed. 8572 msec. 1.22 MBytes/sec
start 4 : send 10485760 bytes completed. 8392 msec. 1.25 MBytes/sec
start 5 : send 10485760 bytes completed. 8077 msec. 1.30 MBytes/sec
start 6 : send 10485760 bytes completed. 8170 msec. 1.28 MBytes/sec
start 7 : send 10485760 bytes completed. 8154 msec. 1.29 MBytes/sec
start 8 : send 10485760 bytes completed. 8238 msec. 1.27 MBytes/sec
start 9 : send 10485760 bytes completed. 8037 msec. 1.30 MBytes/sec
start 10 : send 10485760 bytes completed. 8071 msec. 1.30 MBytes/sec
average = 1.23 MBytes/sec
...

転送レートは毎秒約1.23メガバイトだった。なんか遅い気もするが、USR-ES1での有線接続よりは速かった。