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