👋
LwIPの初期化・DHCPのIPアドレス取得・通信方法
Qiitaの記事となるのですが、下記の記事でSTM32でFreeRTOS+LwIPのサンプルソースコードを見てきました。
- CubeIDEで行うFreeRTOS+LwIPハンズオン
- サンプルソースにおけるLwIPの初期化処理の解説
- サンプルソースにおけるLwIPのDHCP Clientの解説
- サンプルソースにおけるLHTTP Serverの解説
上記の記事ではLwIPの使い方だけを記載しているわけではないので、この記事ではLwIPの使い方に関してまとめます。
なお、FreeRTOSを使う前提としているので、OSレスでLwIPを使う方法は想定していません。
LwIPの初期化処理
初期化時に行う処理としては、下記となります。
- TCPIPのプロトコルスタックの初期化
- ネットワークインターフェースの作成
- Ethernetドライバの送信関数、受信関数、接続に変化があった際に呼ばれるコールバック関数を、ネットワークインターフェースに登録
実際に呼び出すLwIPは下記のようになります。
- tcpipの初期化
- tcpip_init()呼び出し
- IPアドレスを設定
- ip_addr_set_zero_ip4, IP_ADDR4等の呼び出し
- ネットワークインターフェースを作成
- netif_add()呼び出し。第六引数に登録する関数は、Ethernetドライバの受信と送信を行う関数をネットワークインターフェースに登録する処理を実装しておく
- 作成したネットワークインターフェースをデフォルトで使用するよう設定
- ethernetが接続された際のコールバックを登録
下記に初期化のサンプルを示します。
#include "lwip/netif.h"
#include "lwip/tcpip.h"
void lwIPInitSample()
{
ip_addr_t ipaddr;
ip_addr_t netmask;
ip_addr_t gw;
struct netif gnetif; /* network interface structure */
/* TCP/IPのプロトコルスタックの初期化 */
tcpip_init(NULL, NULL);
#if LWIP_DHCP
/* DHCP clientに対応する場合 */
/* IPアドレス、ネットマスク、ゲートウェイを全て0で初期化 */
ip_addr_set_zero_ip4(&ipaddr);
ip_addr_set_zero_ip4(&netmask);
ip_addr_set_zero_ip4(&gw);
#else
/* DHCP clientに対応しない場合 */
/* ユーザーのIPアドレス、ネットマスク、ゲートウェイを設定 */
IP_ADDR4(&ipaddr,IP_ADDR0,IP_ADDR1,IP_ADDR2,IP_ADDR3);
IP_ADDR4(&netmask,NETMASK_ADDR0,NETMASK_ADDR1,NETMASK_ADDR2,NETMASK_ADDR3);
IP_ADDR4(&gw,GW_ADDR0,GW_ADDR1,GW_ADDR2,GW_ADDR3);
#endif /* LWIP_DHCP */
/*設定したIPアドレス、ネットマスク、ゲートウェイのネットワークインターフェースを作成 */
/* ethernet_initはイーサネットドライバで実装したもの */
netif_add(&gnetif, &ipaddr, &netmask, &gw, NULL, ðernetif_init, &tcpip_input);
/* 作成したネットワークインターフェースをデフォルトで使用するように設定 */
netif_set_default(&gnetif);
/* 接続状況に変化があった際のコールバックを登録 */
/* ethernet_link_status_upodatedはイーサネットドライバで実装したもの */
netif_set_link_callback(&gnetif, ethernet_link_status_updated);
}
LwIPのDHCP Client処理
主に行うのは、DHCP ClientのAPIを呼び出すのみです。IPアドレスを取得下かどうかに関しては、APIを呼び出して確認します。
- Ethernetケーブルが接続されている状態で、IPアドレスの設定を要求するパケット(DHCP Discover)を送信する
- dhcp_start()を呼び出す
- DHCPでIPアドレスを取得できたかどうかを確認する。
- dhcp_supplied_address()を呼び出す。返り値がtrueの場合はIPアドレスが取得できた状態となる
DHCP Offer、DHCP Request、DHCP ACKの処理はLwIP内で行っているため、アプリケーション側から何か別のAPIを呼び出す必要はありません。
下記にDHCP Clientのサンプルを示します。
#include "lwip/dhcp.h"
void DHCP_Thread( struct netif *netif)
{
/* DHCP Discoverパケットを送信(受信処理はLwIPの内部処理で行う) */
dhcp_start(netif);
while (!dhcp_supplied_address(netif))
{
osDelay(500);
}
}
LwIPのTCP通信処理
Client側とServer側で振る舞いが違うので分けて示します。
Server型処理
行うこととしては、下記となります。
- 制御用の構造体の作成
- 通信するIPアドレスとポート番号を、制御用構造体と紐づける
- TCPコネクション確立要求待ち
- TCPコネクションの確立
- データ通信
実際のフローとしては下記の通りとなります。
- TCPの通信制御用の構造体の作成
- netconn_newを、引数に「NETCONN_TCP」を指定して呼び出す
- 通信制御用の構造体と、IPアドレスとポート番号を紐づける
- netconn_bindを、第一引数をnetconn_new()で生成した構造体を指定し、第二引数で通信に使うIPアドレス、第三引数で通信に使うポート番号を指定して呼び出す
- TCPのコネクションの確立要求を待つ
- netconn_listenを、引数にnetconn_new()で生成した構造体を指定して呼び出す
- コネクションの確立要求があったデバイスを承認して接続する
- netconn_acceptを、第一引数をnetconn_new()で生成した構造体を指定し、第二引数で空のnetconn型構造体を指定する。接続先の情報は第二引数で指定したnetconn型構造体に格納される
- データの送受信を行う
-
netconn_recvとnetconn_writeを使ってデータの送受信を行う
- netconn_recvは第一引数をnetconn_new()で生成した構造体を指定し、第二引数でnetbuf型のポインタのポインタを指定する。受信データは第二引数で指定したnetbuf型のポインタのポインタが指し示すインスタンスに格納される
- netconn_writeは第一引数をnetconn_new()で生成した構造体を指定し、第二引数に送信データのポインタ、第三引数に送信データのサイズを指定する。第四引数では、送信時のオプション設定を指定する。
-
netconn_recvとnetconn_writeを使ってデータの送受信を行う
- 通信を終了し、TCPのコネクションを切断する(削除は行わない)
- netconn_closeを、引数にnetconn_accept()の第二引数に格納された構造体を指定して呼び出す
- 現在通信を行っている接続状態を完全に削除する
- netconn_deleteを、引数にnetconn_accept()の第二引数に格納された構造体を指定して呼び出す
下記にサンプルを示します。
#include "lwip/api.h"
static void server_netconn_thread(void *arg)
{
struct netconn *conn, *newconn;
err_t err, accept_err;
struct netbuf *inbuf;
err_t recv_err;
char* buf;
u16_t buflen;
/* 制御用構造体の作成 */
conn = netconn_new(NETCONN_TCP);
if (conn!= NULL)
{
/* ポート番号とIPアドレスを紐づけ(ここでは例としてポート番号80のみ指定) */
err = netconn_bind(conn, NULL, 80);
if (err == ERR_OK)
{
/* TCPコネクション確立要求待ち状態にする */
netconn_listen(conn);
while(1)
{
/* TCPコネクション確立 */
accept_err = netconn_accept(conn, &newconn);
if(accept_err == ERR_OK)
{
/* データ通信(サンプルなので受信データをそのまま返す) */
recv_err = netconn_recv(conn, &inbuf);
if (netconn_err(conn) == ERR_OK)
{
netbuf_data(inbuf, (void**)&buf, &buflen);
}
netconn_write(conn, (const unsigned char*)buf, (size_t)buflen, NETCONN_NOCOPY);
/* TCPコネクション切断 */
netconn_delete(newconn);
}
}
}
}
}
Client型処理
行うこととしては、下記となります。
- 制御用の構造体の作成
- 通信するIPアドレスとポート番号を、制御用構造体と紐づける
- TCPコネクション確立要求送信
- データ通信
実際のフローとしては下記の通りとなります。
-
- TCPの通信制御用の構造体の作成
- netconn_newを、引数に「NETCONN_TCP」を指定して呼び出す
- コネクションの確立要求を指定したIPアドレスとポート番号に送る
- netconn_connectを、第一引数をnetconn_new()で生成した構造体を指定し、第二引数と第三引数で接続先のIPアドレスとポート番号を指定する
- データの送受信を行う
-
netconn_recvとnetconn_writeを使う
- netconn_recvは第一引数をnetconn_new()で生成した構造体を指定し、第二引数でnetbuf型のポインタのポインタを指定する。受信データは第二引数で指定したnetbuf型のポインタのポインタが指し示すインスタンスに格納される
- netconn_writeは第一引数をnetconn_new()で生成した構造体を指定し、第二引数に送信データのポインタ、第三引数に送信データのサイズを指定する。第四引数では、送信時のオプション設定を指定する
-
netconn_recvとnetconn_writeを使う
- 通信を終了し、TCPのコネクションを切断する(削除は行わない)
- netconn_closeを、引数にnetconn_accept()の第二引数に格納された構造体を指定して呼び出す
- 現在通信を行っている接続状態を完全に削除する
- netconn_deleteを、引数にnetconn_accept()の第二引数に格納された構造体を指定して呼び出す
下記にサンプルを示します。
#include "lwip/api.h"
static void client_netconn_thread(void *arg)
{
struct netif *netif = (struct netif *) arg; /* 引数としてnetifをもらう前提 */
struct netconn *conn;
err_t err;
/* Create a new TCP connection handle */
conn = netconn_new(NETCONN_TCP);
/* DHCPでIPアドレスが設定されるのを待つ */
while(!dhcp_supplied_address(netif))
{
osDelay(100);
}
if (conn != NULL)
{
ip_addr_t targetIpaddr;
IP_ADDR4(&targetIpaddr,192,168,11,195); /* 例としてIPアドレスを192.168.11.195としている */
/* create connection */
while(1)
{
err = netconn_connect(conn,&targetIpaddr,1024);
if (err == ERR_OK)
{
break;
}
/* wait 500 ms */
osDelay(500);
}
/* send */
char buf[] = "test";
netconn_write(conn, (const unsigned char*)(buf), strlen(buf), NETCONN_NOCOPY);
/* Close the connection (server closes in HTTP) */
netconn_close(conn);
/* delete connection */
netconn_delete(conn);
}
}
Discussion