この記事の続きになります。前回はFreeRTOSとLwIPを使ったサンプルを動作させることを確認できたので、このサンプルのソースコードの中身を見ていきたいと思います。
なお、FreeRTOSの詳しい説明に関しては割愛します。FreeRTOSの内容については下記の記事にもまとめているので、こちらを参照していただければと思います。
https://qiita.com/tom_S/items/9d81f876fa83c66a6065
main.c
まずは、サンプルのプロジェクトにある「Application/user/main.c」から確認していきます。main関数から見ていきます。
main()
実装を下記に示します。コメント文はわかりやすさのため日本語に変えていますが、処理は変えていません。
int main(void)
{
/* STM32F4xxのハードウェアをST社製のライブラリ・ドライバを使用して初期化 */
HAL_Init();
/* システムクロックを 180 MHz に設定(デフォルト設定) */
SystemClock_Config();
/* LCDとLEDの初期化。使用しているボードはLCDがないので、LEDだけ初期化される*/
BSP_Config();
/* LwIPとサンプルの初期化用のスレッド「StartThread」を作成 */
#if defined(__GNUC__)
osThreadDef(Start, StartThread, osPriorityNormal, 0, configMINIMAL_STACK_SIZE * 5);
#else
osThreadDef(Start, StartThread, osPriorityNormal, 0, configMINIMAL_STACK_SIZE * 2);
#endif
osThreadCreate (osThread(Start), NULL);
/* FreeRTOSのスケジューラを起動。作成したスレッド「StartThread」が動作する */
osKernelStart();
/* ここにはFreeRTOSに異常がない限り来ない */
for( ;; );
}
やっていることとしては、サンプルに入っているハードウェア関連のライブラリ・ドライバの初期化と、LwIPの初期化を行うスレッド(タスク)の実行のみです。
ハードウェア関連のライブラリ・ドライバに関してはLwIPとあまり関わりはないのでこの記事では割愛します。その他の処理としてはLwIPの初期化をするスレッド「StartThread」の作成のみなので、このスレッドの実装を見ていきます。
StartThread()
「StartThread」の実装を下記に示します。
static void StartThread(void const * argument)
{
/* TCP/IPのプロトコルスタックの初期化 */
tcpip_init(NULL, NULL);
/* LwIPのネットワークインターフェイスを初期化 */
Netif_Config();
/* HTTPサーバーの初期化 */
http_server_netconn_init();
for( ;; )
{
/* スレッドを削除(引数にNULLを指定で呼び出したスレッドを削除する)。削除後はここに戻って来ないので無限ループにしている */
osThreadTerminate(NULL);
}
}
呼び出している関数を一つずつ見ていきます。まずは「tcpip_init」です。こちらはLwIPのAPIであり、公式ページの仕様はこちらです。引数に指定するのは、初期化が完了した際に呼ばれる関数ポインタとその関数ポインタが呼ばれた際の引数です。
このサンプルでは初期化が完了した後に行う処理がないので、引数にはどちらもNULLを指定しています。
次に「Netif_Config」です。
Netif_Config()
こちらはmain.c内で実装されているので、実装を下記に示します。
static void Netif_Config(void)
{
ip_addr_t ipaddr;
ip_addr_t netmask;
ip_addr_t gw;
#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に対応しない場合。こちらのパスが呼ばれる */
/* main.hで定義されている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アドレス、ネットマスク、ゲートウェイのネットワークインターフェースを作成 */
netif_add(&gnetif, &ipaddr, &netmask, &gw, NULL, ðernetif_init, &tcpip_input);
/* 作成したネットワークインターフェースをデフォルトで使用するように設定 */
netif_set_default(&gnetif);
ethernet_link_status_updated(&gnetif);
#if LWIP_NETIF_LINK_CALLBACK
netif_set_link_callback(&gnetif, ethernet_link_status_updated);
osThreadDef(EthLink, ethernet_link_thread, osPriorityNormal, 0, configMINIMAL_STACK_SIZE *2);
osThreadCreate (osThread(EthLink), &gnetif);
#endif
#if LWIP_DHCP
/* DHCPのクライアントのスレッドを作成 */
osThreadDef(DHCP, DHCP_Thread, osPriorityBelowNormal, 0, configMINIMAL_STACK_SIZE * 2);
osThreadCreate (osThread(DHCP), &gnetif);
#endif
}
サンプル自体はDHCP Clientのサンプルなのですが、マクロ「LWIP_DHCP」が「1」の場合は、ユーザーが指定したIPアドレスでも動作するようです。
DHCPを使用する場合の処理
まずは、「LWIP_DHCP」が「1」の場合での処理を見ていきます。
#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
こちらは、ip_addr_set_zero_ip4を呼び出しているだけですね。これはLwIPのAPIであり、公式ページの仕様はこちらです。IPアドレスをIPv4に設定し、0.0.0.0で初期化するAPIです。DHCPクライアントを使用するため、IPアドレスは0で初期化しています。
DHCPを使用しない場合の処理
次に、「LWIP_DHCP」が0以下の場合の処理です。
#else
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 */
こちらは、main.hで定義されているIPアドレスとネットマスク、ゲートウェイを、マクロ「IP_ADDR4」で設定しています。これもLwIPのAPIです。公式ページの仕様はこちらです。第二引数以降で指定した値をIPアドレスとして設定しています。
LwIPのネットワークインターフェース設定
次に、LwIPのネットワークインターフェースを設定します。このような、TCP/IPのプロトコルスタックでいうネットワークインターフェースというのは、ファームウェア内における、TCP/IPの通信の窓口みたいなものです。アプリケーションやミドルウェアからTCP/IPの通信を行いたいときはネットワークインターフェースを通じて行います。
実装としては下記のようになっています。
/*設定したIPアドレス、ネットマスク、ゲートウェイのネットワークインターフェースを作成 */
netif_add(&gnetif, &ipaddr, &netmask, &gw, NULL, ðernetif_init, &tcpip_input);
/* 作成したネットワークインターフェースをデフォルトで使用するように設定 */
netif_set_default(&gnetif);
こちらは、netif_addとnetif_set_defaultを呼び出しています。これらはLwIPのAPIであり、公式ページの仕様は二つともこちらにあります。
netif_add
ネットワークインターフェースを作成するAPIです。
第一引数がネットワークインターフェースのインスタンスとなり、第二引数~第四引数にて作成するネットワークインターフェースのIPアドレス、ネットマスク、ゲートウェイを指定します。
第五引数はデバイスドライバで更新するステートを指定します。今回はEthernetのステートを確認しないのでNULLを指定しています。
第六引数は通信を行うデバイスドライバの初期化関数のポインタを指定します。この初期化関数の中で、EthernetのパケットをLwIPのTCP/IPプロトコルスタックに渡す処理を追加しておきます。
第七引数はパケットを受信した際に呼ばれる関数のポインタを指定します。TCPのスレッドでデータを受信する場合は「tcpip_input()」を指定するのが推奨されています。HTTP serverを使用するにはTCP/IPのプロトコルを使って通信を行うので、「tcpip_input()」を指定しています。もし、TCP/IPのプロトコルスタックを介さず、自作したアプリケーション側で受信したい場合等では「netif_input()」を使用するようです。
netif_set_default
引数で指定したネットワークインターフェースをデフォルトで使用する設定にします。netif_addにて作成したネットワークインターフェースしか使用しないので、これを引数に指定しています。
Ethernetの設定
次の処理はEthernetの設定です。実装としては下記です。
ethernet_link_status_updated(&gnetif);
まずは、「ethernet_link_status_updated」です。こちらはLwIPのソースコードではなく、サンプルの一部となります。
実装自体は「app_ethernet.c」にあります。行っていることとしては、「netif_is_up」を呼び出し、返り値によってLEDの制御とDHCP_stateを更新しています。(細かい実装内容はEthernetドライバの役目となるので、LwIPから少々離れるのでここでは割愛します。)
「netif_is_up」はLwIPのAPIで、仕様はこちらです。引数で指定したネットワークインターフェースがアクティブだとtrueを返し、アクティブでないとfalseを返します。今回のサンプルでは、ネットワークインターフェイスはEtherneなので、Ethernetでルータと接続されていればtrueを返します。
実装の意図としては、サンプルのファームウェアが起動する前でもEthernetに接続している場合でもLEDに変化があるようにしています。
一度、「Netif_Config」の実装に戻ります。「ethernet_link_status_updated」の後の実装は下記のとおりです。
#if LWIP_NETIF_LINK_CALLBACK
netif_set_link_callback(&gnetif, ethernet_link_status_updated);
osThreadDef(EthLink, ethernet_link_thread, osPriorityNormal, 0, configMINIMAL_STACK_SIZE *2);
osThreadCreate (osThread(EthLink), &gnetif);
#endif
「netif_setlink_callback()」はLwIPのAPIで、仕様はこちらです。第一引数で指定したネットワークインターフェースが有効化or無効化された際に、第二引数で指定した関数を呼び出します。
Ehternetの接続or切断イベントがあった際に、先ほど実装を示した「ethernet_link_status_updated」を呼び出してLEDを制御します。
次の「osThreadDef」と「osThreadCreate」はFreeRTOSのAPIで、「ethernet_link_thread」を実装とするスレッドを作成します。
「ethernet_link_thread」はLwIPのソースコードではなく、サンプルの一部となります。
この関数は、ケーブルが接続されたor切断された際に適切な設定を行います。細かい実装内容はEthernetドライバの役目となるので、LwIPから少々離れるのでここでは割愛します。
DHCPクライアントのスレッド作成
あとは、DHCPクライアントのスレッドを作成して、「Netif_Config」の処理は終了です。
#if LWIP_DHCP
/* DHCPのクライアントのスレッドを作成 */
osThreadDef(DHCP, DHCP_Thread, osPriorityBelowNormal, 0, configMINIMAL_STACK_SIZE * 2);
osThreadCreate (osThread(DHCP), &gnetif);
#endif
こちらでは、「DHCP_Thread」を実装とするスレッドを作成します。「DHCP_Thread」はDHCPクライアントとして動作するスレッドで、「app_ethernet.c」に実装されています。こちらは別の記事で詳しく読んでいきたいのでここでは割愛します。
次に「http_server_netconn_init」です。
http_server_netconn_init()
実装は下記のようになっています。「sys_thread_new」はFreeRTOSのライブラリ「osThreadCreate」を呼び出すためのラッパー関数です。
void http_server_netconn_init()
{
sys_thread_new("HTTP", http_server_netconn_thread, NULL, DEFAULT_THREAD_STACKSIZE, WEBSERVER_THREAD_PRIO);
}
よって、行っているのは、HTTPサーバーのスレッド「http_server_netconn_thread」を作成しているのみとなります。HTTPサーバーのソースコードに関しては別記事にて説明します。
これで初期化スレッド「StartThread」の処理は終了です。
LwIPの初期設定フローのまとめ
初期化時のソースコードとしてはすべて確認できたので、LwIPの初期化時の呼び出しが必要な処理を下記に示します。
#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);
}
上記の実装の流れを下記に示します。
- tcpipの初期化
- tcpip_init()呼び出し
- IPアドレスを設定
- ip_addr_set_zero_ip4, IP_ADDR4等の呼び出し
- ネットワークインターフェースを作成
- netif_add()呼び出し。第六引数に登録する関数は、Ethernetドライバの受信と送信を行う関数をネットワークインターフェースに登録する処理を実装しておく
- 作成したネットワークインターフェースをデフォルトで使用するよう設定
- ethernetが接続された際のコールバックを登録
やっていることをざっくりまとめると、下記の三つになります。
- TCPIPのプロトコルスタックの初期化
- ネットワークインターフェースの作成
- Ethernetドライバの送信関数、受信関数、接続に変化があった際に呼ばれるコールバック関数を、ネットワークインターフェースに登録
次の記事では、DHCPクライアントのスレッドのソースコードを見ていきます。