STM32で始めるFreeRTOSとLwIP(3) -DHCP Client

[この記事](https://qiita.com/tom_S/items/c1b1bfc030a67fe7977a)の続きになります。前回はFreeRTOSとLwIPを使ったサンプルの初期化部分のソースコードが理解できたので、DHCP Clientのソースコードの中身を見ていきたいと思います。
なお、FreeRTOSの詳しい説明に関しては割愛します。FreeRTOSの内容については下記の記事にもまとめているので、こちらを参照していただければと思います。
https://qiita.com/tom_S/items/9d81f876fa83c66a6065

この記事ではDHCPの機能とDHCP Clientについて軽く説明した後、サンプルで実装されているDHCP Clientの実装を見ていきます。
前回の記事でも少々説明しましたが、DHCP Clientの実装は「DHCP_Thread」にて行っているので、こちらを見ていきます。

## DHCPの機能

DHCP(Dynamic Host Configuration Protocol)は、IPアドレスの設定を自動で行う仕組みです。また、設定するIPアドレスが他のネットワークインターフェースと重複しないようにIPアドレスを管理します。
DHCPを使用する際は、DHCP ClientとDHCP Serverが存在していないと動作しません。
IPアドレスの設定フローとしては下記となります。
1. DHCP ClientがDHCP Serverへ、IPアドレスの設定を要求するパケット(DHCP Discover)を送信
2. DHCPサーバーからDHCP Clientへ割り振られたIPアドレスを送信(DHCP Offer)
3. DHCP Clientは割り振られたIPアドレスを設定したことを示すパケット(DHCP Request)をDHCP Serverへ送信
4. DHCP Serverは、送ったIPアドレスが設定されたことを承認するパケット(DHCP ACK)をDHCP Clientへ送信

![DHCP.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266944/cbd5aec6-d4aa-f7b4-4364-7439abff722c.png)


## DHCP_Thread

上記で説明したDHCPのフローを基にサンプルのソースコード(DHCP_Thread)を確認していきます。
DHCP_Thread()はapp_ethernet.cに実装されています。下記の通りです。

```C
void DHCP_Thread(void const * argument)
{
 struct netif *netif = (struct netif *) argument;
 ip_addr_t ipaddr;
 ip_addr_t netmask;
 ip_addr_t gw;
 struct dhcp *dhcp;
#ifdef USE_LCD
 uint8_t iptxt[20];
#endif
 for (;;)
 {
   switch (DHCP_state)
   {
   case DHCP_START:
     {
       ip_addr_set_zero_ip4(&netif->ip_addr);
       ip_addr_set_zero_ip4(&netif->netmask);
       ip_addr_set_zero_ip4(&netif->gw);
       DHCP_state = DHCP_WAIT_ADDRESS;
#ifdef USE_LCD
       LCD_UsrLog ("  State: Looking for DHCP server ...\n");
#else
       BSP_LED_Off(LED1);
       BSP_LED_Off(LED2);
#endif
       /* DHCP Discoverパケットを送信(受信処理はLwIPの内部処理で行う) */
       dhcp_start(netif);
     }
     break;
   case DHCP_WAIT_ADDRESS:
     {
       /* DHCPでIPアドレスが取得できていない */
       if (dhcp_supplied_address(netif))
       {
         DHCP_state = DHCP_ADDRESS_ASSIGNED;
#ifdef USE_LCD
         sprintf((char *)iptxt, "%s", ip4addr_ntoa(netif_ip4_addr(netif)));
         LCD_UsrLog ("IP address assigned by a DHCP server: %s\n", iptxt);
#else
         BSP_LED_On(LED1);
         BSP_LED_Off(LED2);
#endif
       }
       else
       {
         dhcp = (struct dhcp *)netif_get_client_data(netif, LWIP_NETIF_CLIENT_DATA_INDEX_DHCP);
         /* DHCP timeout */
         if (dhcp->tries > MAX_DHCP_TRIES)
         {
           DHCP_state = DHCP_TIMEOUT;
           /* Static address used */
           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);
           netif_set_addr(netif, ip_2_ip4(&ipaddr), ip_2_ip4(&netmask), ip_2_ip4(&gw));
#ifdef USE_LCD
           sprintf((char *)iptxt, "%s", ip4addr_ntoa(netif_ip4_addr(netif)));
           LCD_UsrLog ("DHCP Timeout !! \n");
           LCD_UsrLog ("Static IP address: %s\n", iptxt);
#else
           BSP_LED_On(LED1);
           BSP_LED_Off(LED2);
#endif
         }
       }
     }
     break;
 case DHCP_LINK_DOWN:
   {
     DHCP_state = DHCP_OFF;
#ifdef USE_LCD
     LCD_UsrLog ("The network cable is not connected \n");
#else
     BSP_LED_Off(LED1);
     BSP_LED_On(LED2);
#endif
   }
   break;
   default: break;
   }
   /* wait 500 ms */
   osDelay(500);
 }
}
```

上記のスレッドの機能としては下記となります。
- Ethernetが接続された際にDHCP Clientとして動作し、Ethernetが切断されたら状態を「DHCP_state」の値が「DHCP_START」なら、dhcp_start()を呼び出し、「DHCP_state」の値を「DHCP_WAIT_ADDRESS」に変更 
  - [dhcp_start()](https://www.nongnu.org/lwip/2_1_x/group__dhcp4.html#ga0c50968d9811aa2aa67fadc0885d744f  )はLwIPのAPIで、DHCP Client制御用のインスタンスを作成し、IPアドレスの設定を要求するパケット(DHCP Discover)を送信する
- 「DHCP_state」の値が「DHCP_WAIT_ADDRESS」なら、dhcp_supplied_address()を呼び出してIPアドレスを取得できたかを確認。所得できた状態ならIPアドレスを設定し、「DHCP_state」の値を「DHCP_ADDRESS_ASSIGNED」に変更。タイムアウトしており、IPアドレスを取得できていない場合は、固定のIPアドレスを設定して「DHCP_state」の値を「DHCP_TIMEOUT」に変更
  - [dhcp_supplied_address()](https://www.nongnu.org/lwip/2_1_x/dhcp_8c.html#ae24a2529372218327ab9cb6592041c85 )はLwIPのAPIで、IPアドレスを取得したかどうかをbool値で返す
- 「DHCP_state」の値が「DHCP_LINK_DOWN」なら、「DHCP_state」の値を「DHCP_OFF」に変更

DHCPのステート「DHCP_state」は、このスレッド「DHCP_Thread()」とEthernetの接続状況が変わった際に呼ばれるコールバック「ethernet_link_status_updated()」で値が変わります。

## ethernet_link_status_updated

「ethernet_link_status_updated()」は[こちらの記事](https://qiita.com/tom_S/items/c1b1bfc030a67fe7977a )で説明した初期化時のサンプルコードで「netif_set_link_callback()」を使って、コールバックとして登録されています。実装は下記のとおりです。

```C
void ethernet_link_status_updated(struct netif *netif)
{
 if (netif_is_up(netif))
{
#if LWIP_DHCP
   /* Update DHCP state machine */
   DHCP_state = DHCP_START;
#elif defined(USE_LCD)
   uint8_t iptxt[20];
   sprintf((char *)iptxt, "%s", ip4addr_ntoa(netif_ip4_addr(netif)));
   LCD_UsrLog ("Static IP address: %s\n", iptxt);
#else
   BSP_LED_On(LED1);
   BSP_LED_Off(LED2);
#endif /* LWIP_DHCP */
 }
 else
 {
#if LWIP_DHCP
   /* Update DHCP state machine */
   DHCP_state = DHCP_LINK_DOWN;
#elif defined(USE_LCD)
   LCD_UsrLog ("The network cable is not connected \n");
#else
   BSP_LED_Off(LED1);
   BSP_LED_On(LED2);
#endif /* LWIP_DHCP */
 }
}
```

「ethernet_link_status_updated()」の機能としては、下記のようになります。LEDの制御も含まれているのですが、DHCPと関係ないので割愛します。
- Ethernetが接続状態になったら、「DHCP_state」の値を「DHCP_START」に変更する
- Ethernetが切断状態になったら、「DHCP_state」の値を「DHCP_LINK_DOWN」に変更する

## DHCP Clientの実装のまとめ

上記で示したDHCP Clientのサンプルソースコードにおける振る舞いをまとめると、下記のようになります。
- Ethernetケーブルが接続された際に、IPアドレスの設定を要求するパケット(DHCP Discover)を送信する
  - DHCP Offer、DHCP Request、DHCP ACKの処理はLwIP内で行っている
- Ethernetケーブルが切断された際は、IPアドレスは設定されていない状態に戻る
- Ethernetケーブルを切断→接続を行うことで、再度DHCPのIPアドレス取得フローが始まる
- DHCPでIPを取得できたかどうかは、スレッド内でポーリングして確認している

シーケンスを図にまとめると下記のようになります。

![DHCPサンプル.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266944/da5a515e-d701-6ac8-26f0-f9020ba254ae.png)



サンプルソースのためこのような実装になっているかと思うのですが、本来ならDHCPスレッドはメッセージボックスやイベントグループなどの通知で動作させる形にするのが妥当かと思います。
このあたりも、全てのサンプルソースコードが読み終わった際に修正してみたいと思います。

次の[記事](https://qiita.com/tom_S/items/9d0343c07acbe107c628)ではHTTP Serverのサンプルソースを見ていきます。

## 参考資料

DHCP
https://milestone-of-se.nesuke.com/nw-basic/grasp-nw/dhcp/
https://www.challenge-cf.jp/post/wireshark%E3%81%A7%E3%82%AF%E3%83%A9%E3%82%A4%E3%82%A2%E3%83%B3%E3%83%88%E3%81%8Cdhcp%E3%82%B5%E3%83%BC%E3%83%90%E3%81%8B%E3%82%89ip%E3%82%A2%E3%83%89%E3%83%AC%E3%82%B9%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B%E3%81%BE%E3%81%A7%E3%81%AE%E5%90%84%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E3%82%92%E8%A6%8B%E3%81%A6%E3%81%BF%E3%81%9F