👋

LwIPの初期化・DHCPのIPアドレス取得・通信方法

2024/06/24に公開

Qiitaの記事となるのですが、下記の記事でSTM32でFreeRTOS+LwIPのサンプルソースコードを見てきました。

  1. CubeIDEで行うFreeRTOS+LwIPハンズオン
  2. サンプルソースにおけるLwIPの初期化処理の解説
  3. サンプルソースにおけるLwIPのDHCP Clientの解説
  4. サンプルソースにおけるLHTTP Serverの解説

上記の記事ではLwIPの使い方だけを記載しているわけではないので、この記事ではLwIPの使い方に関してまとめます。
なお、FreeRTOSを使う前提としているので、OSレスでLwIPを使う方法は想定していません。

LwIPの初期化処理

初期化時に行う処理としては、下記となります。

  • TCPIPのプロトコルスタックの初期化
  • ネットワークインターフェースの作成
  • Ethernetドライバの送信関数、受信関数、接続に変化があった際に呼ばれるコールバック関数を、ネットワークインターフェースに登録

実際に呼び出すLwIPは下記のようになります。

  1. tcpipの初期化
  2. IPアドレスを設定
  3. ネットワークインターフェースを作成
    • netif_add()呼び出し。第六引数に登録する関数は、Ethernetドライバの受信と送信を行う関数をネットワークインターフェースに登録する処理を実装しておく
  4. 作成したネットワークインターフェースをデフォルトで使用するよう設定
  5. 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, &ethernetif_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を呼び出して確認します。

  1. Ethernetケーブルが接続されている状態で、IPアドレスの設定を要求するパケット(DHCP Discover)を送信する
  2. 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コネクションの確立
  • データ通信

実際のフローとしては下記の通りとなります。

  1. TCPの通信制御用の構造体の作成
    • netconn_newを、引数に「NETCONN_TCP」を指定して呼び出す
  2. 通信制御用の構造体と、IPアドレスとポート番号を紐づける
    • netconn_bindを、第一引数をnetconn_new()で生成した構造体を指定し、第二引数で通信に使うIPアドレス、第三引数で通信に使うポート番号を指定して呼び出す
  3. TCPのコネクションの確立要求を待つ
    • netconn_listenを、引数にnetconn_new()で生成した構造体を指定して呼び出す
  4. コネクションの確立要求があったデバイスを承認して接続する
    • netconn_acceptを、第一引数をnetconn_new()で生成した構造体を指定し、第二引数で空のnetconn型構造体を指定する。接続先の情報は第二引数で指定したnetconn型構造体に格納される
  5. データの送受信を行う
    • netconn_recvnetconn_writeを使ってデータの送受信を行う
      • netconn_recvは第一引数をnetconn_new()で生成した構造体を指定し、第二引数でnetbuf型のポインタのポインタを指定する。受信データは第二引数で指定したnetbuf型のポインタのポインタが指し示すインスタンスに格納される
      • netconn_writeは第一引数をnetconn_new()で生成した構造体を指定し、第二引数に送信データのポインタ、第三引数に送信データのサイズを指定する。第四引数では、送信時のオプション設定を指定する。
  6. 通信を終了し、TCPのコネクションを切断する(削除は行わない)
    • netconn_closeを、引数にnetconn_accept()の第二引数に格納された構造体を指定して呼び出す
  7. 現在通信を行っている接続状態を完全に削除する
    • 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コネクション確立要求送信
  • データ通信

実際のフローとしては下記の通りとなります。

    1. TCPの通信制御用の構造体の作成
    • netconn_newを、引数に「NETCONN_TCP」を指定して呼び出す
  1. コネクションの確立要求を指定したIPアドレスとポート番号に送る
    • netconn_connectを、第一引数をnetconn_new()で生成した構造体を指定し、第二引数と第三引数で接続先のIPアドレスとポート番号を指定する
  2. データの送受信を行う
    • netconn_recvnetconn_writeを使う
      • netconn_recvは第一引数をnetconn_new()で生成した構造体を指定し、第二引数でnetbuf型のポインタのポインタを指定する。受信データは第二引数で指定したnetbuf型のポインタのポインタが指し示すインスタンスに格納される
      • netconn_writeは第一引数をnetconn_new()で生成した構造体を指定し、第二引数に送信データのポインタ、第三引数に送信データのサイズを指定する。第四引数では、送信時のオプション設定を指定する
  3. 通信を終了し、TCPのコネクションを切断する(削除は行わない)
    • netconn_closeを、引数にnetconn_accept()の第二引数に格納された構造体を指定して呼び出す
  4. 現在通信を行っている接続状態を完全に削除する
    • 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