Arduinoと有線LANで通信する(ENC28J60 1)

ENC28J60チップを使った安価なArduino用のLANモジュール($2.82で買いました)を使って
RaspberryとArduino+ENC28J60の通信に成功しましたので紹介 します。
全体のイメージはこんな感じです。
Raspberry側は普通のLANを使います。









ArduinoでENC28J60を使う場合、「EtherCard」と「UIPEthernet」の2つのライブラリを選ぶことができます。 (他にも探せばあるかもしれません)
「EtherCard」は以下のプロトコルをサポートしています。
DHCP/DNS/UDP
一方、「UIPEthernet」は以下のプロトコルをサポートしています。
DHCP/DNS/UDP/TCP/ARP/ICMP

こ ちらのページで「Arduinoにイーサネット(ENC28J60)を追加するときのライブラリの正しい選び方」が紹介されています が、
「because it is fully compliant with Arduino Ethernet Lib」という理由で「UIPEthernet」がお薦めのようです。
そこで、私も「UIPEthernet」を使って、Raspberryがサーバー、Arduinoがクライアントの動作を試してみました。

【Raspberry側】

以下のコード(server.c)をコンパイルして実行します。
クライアントから受信した文字列を大文字に変換して送り返す単純なものです。
コンパイルは単純に「cc server.c」で、実行は「./a.out」です。

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <time.h>
#include <limits.h>

#define PORT 9876 //クライアントプログラムとポート番号を合わせてください

int main(int argc, char **argv){
  int i;
  int srcSocket; //自分
  int dstSocket; //相手
  // sockaddr_in 構造体
  struct sockaddr_in srcAddr;
  struct sockaddr_in dstAddr;
  socklen_t dstAddrSize;
  // 各種パラメータ
  int numrcv;
  char buf[1024];
  int loopMax;
  int loopCnt;

  time_t start_time=0, end_time;
  double dtime;

  loopMax=INT_MAX;
  if (argc == 2) {
    loopMax=atoi(argv[1]);
  }

  // sockaddr_in 構造体のセット
  bzero((char *)&srcAddr, sizeof(srcAddr));
  srcAddr.sin_port = htons(PORT);
  srcAddr.sin_family = AF_INET;
  srcAddr.sin_addr.s_addr = INADDR_ANY;
   
  // ソケットの生成(ストリーム型)
  srcSocket = socket(AF_INET, SOCK_STREAM, 0);
  // ソケットのバインド
  bind(srcSocket, (struct sockaddr *)&srcAddr, sizeof(srcAddr));
  // 接続の許可
  listen(srcSocket, 5);
 
  for(loopCnt=0;loopCnt<loopMax;loopCnt++) {
//  while(1){
    // 接続の受付け
    printf("接続を待っています\nクライアントプログラムを動かして下さい\n");
    dstAddrSize = sizeof(dstAddr);
    dstSocket = accept(srcSocket, (struct sockaddr *)&dstAddr, &dstAddrSize);
    printf("%s から接続を受けました\n",inet_ntoa(dstAddr.sin_addr));
    if (start_time == 0) time( &start_time );
       
    while(1) { // クライアントがSocketをクローズしてからこちらもクローズする
      //パケットの受信
      memset(buf,0,sizeof(buf));
      numrcv = read(dstSocket, buf, 1024);
      printf("numrcv=%d\n",numrcv);
      if(numrcv ==0 || numrcv ==-1 ){ // client close socket
        close(dstSocket); break;
      }
      printf("Recv=[%s]",buf);
      for (i=0; i< numrcv; i++){ // bufの中の小文字を大文字に変換
        if(isalpha(buf[i])) buf[i] = toupper(buf[i]);
      }
      // パケットの送信
      write(dstSocket, buf, numrcv);
      printf("->Send=[%s]\n",buf);
    } // end while
  } // end for
  time( &end_time );
  dtime=difftime( end_time, start_time );
  printf("loop=%d difftime=%f[sec] %f[min]\n",loopMax,dtime,dtime/60);
  return(0);
}

プログラムを起動すると以下の表示となります。


【Arduino側】

以下のページから「arduino_uip-master.zip」をダウンロードし、解凍したら「arduino_uip」にフォルダー名を変 えて
フォルダーごとlibrariesフォルダーに移動します。
https://github.com/ntruchsess/arduino_uip

最初に「libraries\arduino_uip\utility\Enc28J60Network.cpp」を一部修正します。
この修正を加えないと、40回程度Connect/Disconnectを繰り返すと、Arduinoから二度とRaspberryに接続できな くなります。
理由はこ のページに紹介されていますが、読んでもよく分からないです。(アハハ)
この修正を行う前、一定時間動くとなぜかConnectできなくなったので、「ENC28J60 FREEZ」で検索したらこのページがヒットしました。
とにかく修正したら安定して動くようになりました。
修正前、修正後のソースを以下に示します。

修正前(抜粋)
void
Enc28J60Network::sendPacket(memhandle handle)
{
  memblock *packet = &blocks[handle];
  uint16_t start = packet->begin-1;
  uint16_t end = start + packet->size;

  // backup data at control-byte position
  uint8_t data = readByte(start);
  // write control-byte (if not 0 anyway)
  if (data)
    writeByte(start, 0);

#ifdef ENC28J60DEBUG
  Serial.print("sendPacket(");
  Serial.print(handle);
  Serial.print(") [");
  Serial.print(start,HEX);
  Serial.print("-");
  Serial.print(end,HEX);
  Serial.print("]: ");
  for (uint16_t i=start; i<=end; i++)
    {
      Serial.print(readByte(i),HEX);
      Serial.print(" ");
    }
  Serial.println();
#endif

  // TX start
  writeRegPair(ETXSTL, start);
  // Set the TXND pointer to correspond to the packet size given
  writeRegPair(ETXNDL, end);
  // send the contents of the transmit buffer onto the network
  writeOp(ENC28J60_BIT_FIELD_SET, ECON1, ECON1_TXRTS);
  // Reset the transmit logic problem. See Rev. B4 Silicon Errata point 12.
  if( (readReg(EIR) & EIR_TXERIF) )
    {
      writeOp(ENC28J60_BIT_FIELD_CLR, ECON1, ECON1_TXRTS);
    }

  //restore data on control-byte position
  if (data)
    writeByte(start, data);
}

修正後(抜粋)
void
Enc28J60Network::sendPacket(memhandle handle)
{
  memblock *packet = &blocks[handle];
  uint16_t start = packet->begin-1;
  uint16_t end = start + packet->size;

  // backup data at control-byte position
  uint8_t data = readByte(start);
  // write control-byte (if not 0 anyway)
  if (data)
    writeByte(start, 0);

#ifdef ENC28J60DEBUG
  Serial.print("sendPacket(");
  Serial.print(handle);
  Serial.print(") [");
  Serial.print(start,HEX);
  Serial.print("-");
  Serial.print(end,HEX);
  Serial.print("]: ");
  for (uint16_t i=start; i<=end; i++)
    {
      Serial.print(readByte(i),HEX);
      Serial.print(" ");
    }
  Serial.println();
#endif

  // TX start
  writeRegPair(ETXSTL, start);
  // Set the TXND pointer to correspond to the packet size given
  writeRegPair(ETXNDL, end);
  // send the contents of the transmit buffer onto the network
  writeOp(ENC28J60_BIT_FIELD_SET, ECON1, ECON1_TXRTS);
  // Reset the transmit logic problem. See Rev. B4 Silicon Errata point 12.
  if( (readReg(EIR) & EIR_TXERIF) )
    {
//      writeOp(ENC28J60_BIT_FIELD_CLR, ECON1, ECON1_TXRTS);

        writeOp(ENC28J60_BIT_FIELD_SET, ECON1, ECON1_TXRST);
        writeOp(ENC28J60_BIT_FIELD_CLR, ECON1, ECON1_TXRST);
        writeOp(ENC28J60_BIT_FIELD_CLR, EIR, EIR_TXERIF);
    }

  //restore data on control-byte position
  if (data)
    writeByte(start, data);
}


Arduino-IDEを1.6.13にアップデートしたら、こんなワーニングが出るようになりました。
警告:ライブラリUIPEthernetのカテゴリ「」は有効ではありません。「Uncategorized」に設定します。
Invalid version found: 1.04

こちら
に対処方法が紹介されていますが、ライブラリフォルダーの「library.properties」に以下の変更を加える必要 が有ります。
category=Communication → 1行追加
version=1.04 → version=1.4 に変更


ENC28J60とArduinoの接続は以下の通りです。
ENC28J60 Arduino
SO Pin #12
SI Pin #11
SCK Pin #13
CS Pin #10
VCC 3.3V(※)
GND Gnd

(※)今回UNOを使いましたが、UNO互換機の 3.3Vは信用できないので、 5V→3.3Vのレギュレータを使って3.3Vを供給しています。

Arduino側のスケッチは以下の通りです。
相手側(今回はRaspberry)とポート番号(今回は9876番を使用)とIPアドレスを合わせて下さい。
ここが合っていないと正しく通信できません。
赤字の部分はRaspberryのIPアドレスに変更してください。
サーバー側(Raspberry側)のプログラムが動いていないときは、接続をリトライします。

/*
 * ENC28J60 Ethernet Module TcpClient example.
 */

#include <UIPEthernet.h>

EthernetClient client;
unsigned long next;
bool connect;

void EthernetBegin() {
  uint8_t mac[6] = {0x00,0x01,0x02,0x03,0x04,0x05};

  while(1) {
    Serial.print("begin....");
    int Ret = Ethernet.begin(mac);
    Serial.print(Ret);
    Serial.print("...");
    if (Ret) {
      Serial.println("ok....");
      Serial.print("localIP: ");
      Serial.println(Ethernet.localIP());
      Serial.print("subnetMask: ");
      Serial.println(Ethernet.subnetMask());
      Serial.print("gatewayIP: ");
      Serial.println(Ethernet.gatewayIP());
      Serial.print("dnsServerIP: ");
      Serial.println(Ethernet.dnsServerIP());
      break;
    } // end if
    Serial.println("faild....");
    delay(1000);
  } // end while
}

void setup() {

  Serial.begin(9600);
  Serial.println("Client Start");
  connect = 0;
  next = 0;
}

void loop() {
  static int num = 0;
  char smsg[30];
  uint8_t rmsg[30];

  if ( (millis() - next) > 0) {
    if (!connect) EthernetBegin();
    next = millis() + 5000;
    Serial.print("Client connect....");
    // replace hostname with name of machine running tcpserver.pl
    if (client.connect(IPAddress(192,168,111,20),9876)) {
      connect = 1;
      Serial.print("Client Connected....");
      sprintf(smsg,"data from arduino %05d",num);
      num++;
      client.print(smsg);
 
      // wait for responce
      while(client.available() == 0) {
        if (next - millis() < 0) {
          connect = 0;
          goto close;
        } // end if
      } // end while

      int size;
      while((size = client.available()) > 0) {
        Serial.print("Receive Size=");
        Serial.println(size);
        size = client.read(rmsg,size);
        Serial.write(smsg,size);
        Serial.write("->");
        Serial.write(rmsg,size);
        Serial.println("");
      } // end while

close:
      //disconnect client
      Serial.println("Client disconnect");
      client.stop();
    } else {
      Serial.println("Client connect failed");
      connect = 0;
    }
  }
}

このスケッチを実行するとArduinoのシリアルモニターに以下が表示されます。
Arduinoから送った「data from arduino」の文字が「DATA FROM ARDUINO」となって返ってきます。
その後ろの5桁の数字はパケットの送信回数です。



Raspberry側は以下の様に表示されます。



次は、Raspberryがクライアント、Arduinoがサーバーの動作を試して みます。

続く...