nRF9160とSPI有線LANモジュール(W5500,enc28j60,enc624j600)でLTE-ETHERNET-GATEWAYを作った話。

nRF9160 の開発ボードと SPI接続の有線 LAN モジュール (W5500,enc28j60menc624j600) で
LTE-ETHERNET-GATEWAYを作成した。

ソースコードはこちら。

https://github.com/Akihiro-Sakaniwa/nRF9160_LTE_Ethernet_Gateway


ethernet 関連の実装には Zephyr 付属のデバイスドライバを利用した。

理由は、簡単な実装でも動作が安定することと、プログラムのインターフェースを統一できるためだ。

Config (prj.conf,overlay.dts) を変更するだけでソースコードの変更なしに有線 LAN モジュールを変更できる。

というわけで Zephyr のデバイスドライバの使い方と raw-packet の送受信方法について、備忘録をまとめておく。

なお、NCS v2.5.2 を利用した。

# overlay.dts

```
&spi? {
	compatible = "nordic,nrf-spim";
	status = "okay";

    省略
	cs-gpios = <&gpio0 ? GPIO_ACTIVE_LOW>;
	eth0: eth0@0 {
		compatible = "microchip,enc424j600";
		//compatible = "microchip,enc28j60";
		//compatible = "wiznet,w5500";
		reg = <0>;
		spi-max-frequency = <80000000>;
        int-gpios = <&gpio0 ? GPIO_ACTIVE_LOW>;
        //local-mac-address = [?? ?? ?? ?? ?? ??];
    };
};
```
Zephyr には enc424j600 のドライバがある（enc624j600 はない）。
データシートを見た限り、enc424j600 と enc624j600 は互換性があるようなので
enc424j600 のドライバを利用した。
特に問題なく動作している。

```
compatible =
```
に メーカー名,チップ名 を記述すれば OK.

```
local-mac-address = 
```
は、MAC アドレスを内蔵していないCHIP、 W5500 と enc28j60 に必要。
enc424j600 と enc624j600 は MAC アドレスを内蔵しているので不要。

# prj.conf

```

省略

CONFIG_SPI=y
CONFIG_NRFX_SPIM?=y

CONFIG_NETWORKING=y
CONFIG_NET_SOCKETS=y

CONFIG_NET_L2_ETHERNET=y

CONFIG_ETH_ENC424J600=y
#CONFIG_ETH_ENC28J60=y
#CONFIG_ETH_W5500=y

CONFIG_NET_IPV6=n
CONFIG_NET_IPV4=y
CONFIG_NET_ARP=y
CONFIG_NET_UDP=y
CONFIG_NET_TCP=y
CONFIG_NET_CONFIG_SETTINGS=y
CONFIG_NET_CONFIG_NEED_IPV4=y
CONFIG_NET_CONFIG_MY_IPV4_ADDR="???.???.???.???"
CONFIG_NET_CONFIG_MY_IPV4_GW="??.???.???.???"
CONFIG_NET_CONFIG_MY_IPV4_NETMASK="???.???.???.???"

CONFIG_NET_NATIVE=y

CONFIG_NET_SOCKETS_PACKET=y
CONFIG_NET_DEFAULT_IF_ETHERNET=y

省略

```

必須項目を全て網羅してるか自信がないが、関係しそうなものだけピックアップしてみた。

CONFIG_ETH_<チップ名> で、デバイスドライバを指定する、
これを切り替えるだけでソースコードはそのままで LAN モジュールを変更できる。

```
CONFIG_NET_NATIVE=y
```
を指定しないと RAW-PACKET を読み取れないので注意。

なお、デバイスドライバ自体は、
ncs/v2.5.2/zephyr/drivers/ 以下に置かれている。
動作が怪しい時などはソースを変更することも可能なのはありがたい。

以下のURLのREADMEにドライバの登録方法が記されているので参考になる。

https://github.com/WojtaCZ/zephyr-w5100

# 実装例 (ethernet 初期化)

```

省略

#include <zephyr/net/net_pkt.h>
#include <zephyr/net/net_if.h>
#include <zephyr/net/net_l2.h>
#include <zephyr/net/net_ip.h>
#include <zephyr/net/net_core.h>
#include <zephyr/net/net_context.h>
#include <zephyr/net/socket.h>

省略

static struct net_if *eth_iface;
uint8_t my_eth_mac_addr[6] = {0,0,0,0,0,0};
uint8_t my_eth_ip_addr[4] = {0,0,0,0};
static struct net_context* ctx_rx;
static struct net_context* ctx_tx;

省略

int eth_start(void)
{
    int err = 0;

    省略

    struct net_if *iface = net_if_get_default();
    eth_iface = net_if_get_first_by_type(&NET_L2_GET_NAME(ETHERNET));
    if (iface != eth_iface)
    {
        net_if_set_default(eth_iface);
    }
    if (eth_iface->if_dev->link_addr.len != 6)
    {
        printk("error eth mac length =  %d\r\n",eth_iface->if_dev->link_addr.len);
        return -1;
    }

    // ethernetデバイスの MAC アドレスと IPアドレスを取得
    memcpy(my_eth_mac_addr,eth_iface->if_dev->link_addr.addr
    　　　　,eth_iface->if_dev->link_addr.len);
    memcpy(my_eth_ip_addr
    　　　　,eth_iface->config.ip.ipv4->unicast->address.in_addr.s4_addr,4);


    net_if_set_mtu(eth_iface,？？？？);

    // ルーターを作るので RAW-PACKET を読み込めるようにする
    // net_context_get は、socket関数のようなもの。
    // net_context_put は、close関数のようなもの。
    // contextはsocketのようなもの。
    err = net_context_get(AF_PACKET,SOCK_RAW,IPPROTO_RAW,&ctx_tx);
    if (0 > err)
    {
        printk("error net_context_get (tx)%d\r\n",err);
        return err;
    }

    err = net_context_get(AF_PACKET,SOCK_RAW,IPPROTO_IP,&ctx_rx);
    if (0 > err)
    {
        net_context_put(ctx_tx);
        printk("error net_context_get (rx)%d\r\n",err);
        return err;

    }
    net_context_set_iface(ctx_tx,eth_iface);
    net_context_set_iface(ctx_rx,eth_iface);

　　省略

    // スレッドを起動　（eth_txスレッドではLTE側パケットの受信を行う）
	eth_rx_thread_id = k_thread_create(&eth_rx_thread, eth_rx_thread_stack,
			K_THREAD_STACK_SIZEOF(eth_rx_thread_stack),
			eth_rx_thread_func, NULL, NULL, NULL,
			THREAD_PRIORITY, K_USER, K_NO_WAIT);
	eth_tx_thread_id = k_thread_create(&eth_tx_thread, eth_tx_thread_stack,
			K_THREAD_STACK_SIZEOF(eth_tx_thread_stack),
			eth_tx_thread_func, NULL, NULL, NULL,
			THREAD_PRIORITY, K_USER, K_NO_WAIT);
　　return 0;
}
```

デバイスドライバのインタフェースは POSIX の SOCKET とは異なる。
```
#include <zephyr/net/net_context.h>
#include <zephyr/net/net_pkt.h>
#include <zephyr/net/net_if.h>
```
に記述された関数の利用がメインになる。

# 実装例 (ethernet 受信)

```
////////////////////////////////////////////////////////////////////////////////
// パケットを受信するをコールバック関数がよばれる。
void eth_recv_cb(struct net_context *context, struct net_pkt *pkt
        , union net_ip_header *ip_hdr, union net_proto_header *proto_hdr
        , int status, void *user_data)
{

    if (status == 0 && pkt)
    {
        // 受信したパケットはここにある
        uint16_t len = pkt->buffer->len;
        uint8_t* data = pkt->buffer->data;

        // とりあえずダンプしてみる
        for(int i = 0; i < len;i++)
        {
            if (i % 16 == 0) printk("\r\n");
            printk("%02x ",pkt->buffer->b.data[i]);
        }
        printk("\r\n");

        省略（LTE側に送信する処理 send関数で送信 socketは受信側と共有している）

        // 必ず解放すること！
        net_buf_unref(pkt->buffer);
        net_pkt_unref(pkt);
    }
}
////////////////////////////////////////////////////////////////////////////////
// ethernet の受信スレッド
static void eth_rx_thread_func(void *p1, void *p2, void *p3)
{
	ARG_UNUSED(p1);
	ARG_UNUSED(p2);
	ARG_UNUSED(p3);

    while (!on_req_eth_thread_terminate)
    {

        net_context_recv(ctx_rx,eth_recv_cb,K_MSEC(100),NULL);
        k_sleep(K_MSEC(1));
    }
    on_eth_rx_thread_terminate = true;
    LOG_INF("ETH RX thread terminated");
}
```

net_context_recv関数で コールバック関数を設定して
受信するとコールバック関数が呼ばれる。


コールバック関数ないで、pkt がNULLではない時は、
必ず、pkt->buffer と pkt を解放してあげる必要がある。
解放しないと HEAP 足りなくなって受信できなくなる。

# 実装例 (ethernet 送信)

```
////////////////////////////////////////////////////////////////////////////////
// 送信コールバック
static void eth_send_cb(struct net_context *context, int status, void *user_data)
{
    printk("send_cb: %d\r\n",status);
}
////////////////////////////////////////////////////////////////////////////////
// 送信スレッド（LTE受信処理）
static void eth_tx_thread_func(void *p1, void *p2, void *p3) // LTE_RX
{
	ARG_UNUSED(p1);
	ARG_UNUSED(p2);
	ARG_UNUSED(p3);

 　　省略
    

    
    while (!on_req_eth_thread_terminate)
    {
    　　省略
    
        // LTE側の RAW-SOCKETの受信処理
        lte_sock = socket(AF_PACKET, SOCK_RAW, 0);
        if (lte_sock < 0)
        {
            LOG_ERR("socket(AF_PACKET,SOCK_RAW,0) failed: (%d)", -errno);
            k_sleep(K_MSEC(1000));
            continue;
        }
        while (!on_req_eth_thread_terminate)
        {
            省略(poll,recvなどでLTEがわのRAWパケットを監視＆受信)

            if (!on_eth_rx_thread_terminate)
            {
                uint8_t mac[6];

　　　　　　　　　 省略（ARPで相手先のMACアドレスを取得する）
                
                memcpy(&pkt.data[0],mac,6);
                memcpy(&pkt.data[6],my_eth_mac_addr,6);
                pkt.data[12] = 0x08;
                pkt.data[13] = 0x00;

                // RAW-PACKETを送信する場合は struct sockaddr_ll を使う必要がある
                struct sockaddr_ll tgt_addr;
                memset(&tgt_addr,0,sizeof(tgt_addr));
                memcpy(tgt_addr.sll_addr,mac,6);
                tgt_addr.sll_family = AF_PACKET;
                tgt_addr.sll_protocol = 0x0800;
                tgt_addr.sll_hatype = 1;
                tgt_addr.sll_ifindex = 0;
                tgt_addr.sll_halen = 6;
                tgt_addr.sll_pkttype = PACKET_HOST;

                // ethernet側に送信
                int r = net_context_sendto(ctx_tx, &pkt.data[0], pkt.size,(struct sockaddr *)&tgt_addr,sizeof(tgt_addr),eth_send_cb,K_MSEC(1000),NULL);
                if (r <= 0)
                {
                    LOG_ERR("ETH_TX error send (%d)",r);
                }
            }    
        }
        close(lte_sock);
        k_sleep(K_MSEC(100));
    }
    on_eth_tx_thread_terminate = true;
    LOG_INF("ETH TX thread terminated");
}
```

net_context_sendto で送信できる。
送信結果はコールバック関数で確認できるがなくても良い気がする。

# 

上記のようにすれば、Zephyrのデバイスドライバでパケットの送受信ができる。

特筆すべきは安定性。
メーカーのライブラリを移植してSPIを直接叩くよりもお手軽に安定した実装ができる。
（W5500が別物に思えるくらい安定した）

SPI有線LANモジュールをZephyrで利用する上での正解は、
おそらくZephyrのデバイスドライバを使うことと思われる。

# 以上