LoginSignup
0
0

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

Last updated at Posted at 2024-04-10

nRF9160 の開発ボードと SPI接続の有線 LAN モジュール (W5500,enc28j60menc624j600) で
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にドライバの登録方法が記されているので参考になる。

実装例 (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のデバイスドライバを使うことと思われる。

以上

0
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0