Raspberry Pi picowでsocket APIを使う

## 1. はじめに
Raspberry Pi PicoのArduino環境(earlephilhower版)では、socket APIが使えません。これは、ネットワーク機能がRTOSを使わない設定になっているからです。SDKでは、設定を変えてRTOSを使うことができます(下記リンク先参照)が、Arduino環境ではその設定がライブラリ形式で提供されていて、直接変更することはできません。
そこで、Arduino環境でsocket APIが使えるように、lwIPのビルド環境を作ってみました。

https://github.com/raspberrypi/pico-examples#pico-w-networking

https://www.raspberrypi.com/documentation/pico-sdk/networking.html#pico_lwip

## 2. 設定の変更
### 2.1 変更が必要なパラメータ

ネットワーク機能(lwIP)がRTOS(FreeRTOS)を使用しない構成(`NO_SYS=1, PICO_CYW43_ARCH_THREADSAFE_BACKGROUND=1`)になっています。これを`NO_SYS=0, PICO_CYW43_ARCH_FREERTOS=1`に変更します。

### 2.2 lwIPの設定
NO_SYSの設定はファイルlwipopts.hの内で設定しています。これを直接変更するのではなく、このファイルのコピーをローカルなプロジェクト内( `${PROJECT_DIR}/include` )にコピーし、includeの順番を指定することで変更を反映させます。
以下は、その他必要な設定を行ったlwipopts.hです。

<details><summary>lwipopts.h</summary>

```cpp:lwipopts.h
#ifndef _LWIPOPTS_EXAMPLE_COMMONH_H
#define _LWIPOPTS_EXAMPLE_COMMONH_H

#ifdef __cplusplus
extern "C" {
#endif // __cplusplus

// Critical section protection
extern void noInterrupts();
extern void interrupts();
#define SYS_ARCH_DECL_PROTECT int
#define SYS_ARCH_PROTECT(lev) noInterrupts
#define SYS_ARCH_UNPROTECT(lev) interrupts

extern unsigned long get_rand_32(void);
#define LWIP_RAND() get_rand_32()

// Common settings used in most of the pico_w examples
// (see https://www.nongnu.org/lwip/2_1_x/group__lwip__opts.html for details)

#define NO_SYS                      0
#define LWIP_SOCKET                 1
#define MEM_LIBC_MALLOC             0

#define MEM_ALIGNMENT               4
#define MEM_SIZE                    16384
#define MEMP_NUM_TCP_SEG            32
#define MEMP_NUM_ARP_QUEUE          10
#define PBUF_POOL_SIZE              24
#define LWIP_ARP                    2
#define LWIP_ETHERNET               1
#define LWIP_ICMP                   1
#define LWIP_RAW                    1
#define TCP_WND                     (8 * TCP_MSS)
#define TCP_MSS                     1460
#define TCP_SND_BUF                 (8 * TCP_MSS)
#define TCP_SND_QUEUELEN            ((4 * (TCP_SND_BUF) + (TCP_MSS - 1)) / (TCP_MSS))
#define LWIP_NETIF_STATUS_CALLBACK  1
#define LWIP_NETIF_LINK_CALLBACK    1
#define LWIP_NETIF_HOSTNAME         1
#define LWIP_NETCONN                0
#define MEM_STATS                   0
#define SYS_STATS                   0
#define MEMP_STATS                  0
#define LINK_STATS                  0
// #define ETH_PAD_SIZE                2
#define LWIP_CHKSUM_ALGORITHM       3
#define LWIP_DHCP                   1
#define LWIP_IPV4                   1
#define LWIP_TCP                    1
#define LWIP_UDP                    1
#define LWIP_DNS                    1
#define LWIP_TCP_KEEPALIVE          1
#define LWIP_NETIF_TX_SINGLE_PBUF   1
#define DHCP_DOES_ARP_CHECK         0
#define LWIP_DHCP_DOES_ACD_CHECK    0

// See #1285
#define MEMP_NUM_UDP_PCB            6

#if LWIP_IPV6
#define LWIP_IPV6_DHCP6             1
#endif

// NTP
extern void __setSystemTime(unsigned long long sec, unsigned long us);
#define SNTP_SET_SYSTEM_TIME_US(sec, us)  __setSystemTime(sec, us)
#define SNTP_MAX_SERVERS                  2
//#define SNTP_SERVER_ADDRESS               "pool.ntp.org"
#define SNTP_SERVER_DNS                   1

#ifndef NDEBUG
#define LWIP_DEBUG                  1
#define LWIP_STATS                  1
#define LWIP_STATS_DISPLAY          1
#endif

#define ETHARP_DEBUG                LWIP_DBG_OFF
#define NETIF_DEBUG                 LWIP_DBG_OFF
#define PBUF_DEBUG                  LWIP_DBG_OFF
#define API_LIB_DEBUG               LWIP_DBG_OFF
#define API_MSG_DEBUG               LWIP_DBG_OFF
#define SOCKETS_DEBUG               LWIP_DBG_OFF
#define ICMP_DEBUG                  LWIP_DBG_OFF
#define INET_DEBUG                  LWIP_DBG_OFF
#define IP_DEBUG                    LWIP_DBG_OFF
#define IP_REASS_DEBUG              LWIP_DBG_OFF
#define RAW_DEBUG                   LWIP_DBG_OFF
#define MEM_DEBUG                   LWIP_DBG_OFF
#define MEMP_DEBUG                  LWIP_DBG_OFF
#define SYS_DEBUG                   LWIP_DBG_OFF
#define TCP_DEBUG                   LWIP_DBG_OFF
#define TCP_INPUT_DEBUG             LWIP_DBG_OFF
#define TCP_OUTPUT_DEBUG            LWIP_DBG_OFF
#define TCP_RTO_DEBUG               LWIP_DBG_OFF
#define TCP_CWND_DEBUG              LWIP_DBG_OFF
#define TCP_WND_DEBUG               LWIP_DBG_OFF
#define TCP_FR_DEBUG                LWIP_DBG_OFF
#define TCP_QLEN_DEBUG              LWIP_DBG_OFF
#define TCP_RST_DEBUG               LWIP_DBG_OFF
#define UDP_DEBUG                   LWIP_DBG_OFF
#define TCPIP_DEBUG                 LWIP_DBG_OFF
#define PPP_DEBUG                   LWIP_DBG_OFF
#define SLIP_DEBUG                  LWIP_DBG_OFF
#define DHCP_DEBUG                  LWIP_DBG_OFF

#if (NO_SYS == 0)
#define TCPIP_THREAD_PRIO 6
#define TCPIP_THREAD_STACKSIZE 1024
#define DEFAULT_THREAD_STACKSIZE 1024
#define DEFAULT_RAW_RECVMBOX_SIZE 8
#define TCPIP_MBOX_SIZE 8
#define DEFAULT_TCP_RECVMBOX_SIZE 10
#define DEFAULT_ACCEPTMBOX_SIZE 6
#define LWIP_TIMEVAL_PRIVATE 0

// not necessary, can be done either way
#define LWIP_TCPIP_CORE_LOCKING_INPUT 1

// ping_thread sets socket receive timeout, so enable this feature
#define LWIP_SO_RCVTIMEO 1
#endif

#ifdef __cplusplus
}
#endif // __cplusplus

#endif /* __LWIPOPTS_H__ */
```
</details>
一方、 `PICO_CYW43_ARCH_THREADSAFE_BACKGROUND=1` はコマンドラインで指定されていますので、build_unflagsとbuild_flagsを使って置き換えます。
その他の設定をした `platformio.ini` の `build_flags` と `build_unflags` は以下のようになります。

```:platformio.ini(抜粋)
build_flags = 
  -Ic:/Users/${sysenv.USERNAME}/.platformio/packages/framework-arduinopico/pico-sdk/lib/lwip/contrib/ports/freertos/include
  -D PICO_CYW43_ARCH_FREERTOS=1
  -D LWIP_PROVIDE_ERRNO=1
  -D LWIP_TIMEVAL_PRIVATE=0

build_unflags =
  -D PICO_CYW43_ARCH_THREADSAFE_BACKGROUND=1
```

### 2.3 FreeRTOSの設定
FreeRTOSの設定は、ファイルFreeRTOSConfig.hで行います。関数xQueueGetMutexHolderが有効になるように設定します。以下が変更点です。
```diff
--- a/src/FreeRTOSConfig.h
+++ b/src/FreeRTOSConfig.h
@@ -122,6 +122,8 @@ extern void rtosFatalError(void);
 #define configSUPPORT_PICO_SYNC_INTEROP 1
 #define configSUPPORT_PICO_TIME_INTEROP 1

+#define INCLUDE_xQueueGetMutexHolder 1
+#define INCLUDE_xSemaphoreGetMutexHolder 1

 #include "rp2040_config.h"
 //#include "task.h"

```

FreeRTOSの設定を変更する方法は、下記リンク先をご参照ください。

https://qiita.com/yasuhiro-k/items/36b8fe56677c55c0b7c6

## 3. ビルド環境の構築
### 3.1 lwIP
lwIPのソースファイルは、framework-arduinopico/pico-sdk以下にあります。これらをlib以下にコピーすればビルド対象となりますが、lwipopts.h以外のファイルは変更不要なので#includeだけを行う.cや.hファイルを用意する方法を採用しました。こんな感じです。
```c:lwip_freertos.c
#include <../pico-sdk/src/rp2_common/pico_lwip/lwip_freertos.c>
```
:::note info
ソースコードは公開準備中です。
:::

### 3.2 cyw43ドライバ
cyw43ドライバ関連ファイルも同様な方式でファイルを作成します。変更が必要だったファイルはで、修正内容は以下の通りです。
```diff
--- a/src/pico_cyw43_arch/cyw43_arch_freertos.c
+++ b/src/pico_cyw43_arch/cyw43_arch_freertos.c
@@ -10,6 +10,10 @@
 #include "pico/cyw43_arch.h"
 #include "pico/cyw43_driver.h"
 #include "pico/async_context_freertos.h"
+#ifdef ARDUINO_ARCH_RP2040
+#include <FreeRTOS.h>
+#include <task.h>
+#endif

 #if CYW43_LWIP
 #include "pico/lwip_freertos.h"
@@ -40,6 +44,11 @@ async_context_t *cyw43_arch_init_default_async_context(void) {
 }

 int cyw43_arch_init(void) {
+#ifdef ARDUINO_ARCH_RP2040
+    if (xTaskGetCurrentTaskHandle() == NULL) {
+        return 0;
+    }
+#endif
     async_context_t *context = cyw43_arch_async_context();
     if (!context) {
         context = cyw43_arch_init_default_async_context();
```
:::note info
ソースコードは公開準備中です。
:::

## 4. 動作確認用プログラム
socket APIを使った単純なプログラムです。メイン側では、周期的にタスクの状態を表示させています。pico-probeを使用するため、USBシリアルポートの代わりにSerial1を使っています。
<details><summary>main.cpp</summary>

```cpp:main.cpp
#include <Arduino.h>
#include <WiFi.h>
#include <FreeRTOS_custom.h>
#include <FreeRTOS.h>
#include <task.h>
#include <LWIP_custom.h>
#include <PICO_cyw43.h>
#ifdef ARDUINO_ARCH_RP2040
#include <pico_cyw43_arch/cyw43_arch.h>
#endif
#include <lwip/sockets.h>

#ifdef USE_TINYUSB
#include <Adafruit_TinyUSB.h>
#endif
#if __has_include("ssid_passwd.h")
#include "ssid_passwd.h"
#endif


#ifndef STASSID
#define STASSID "your-ssid"
#define STAPSK "your-password"
#endif

#ifndef SOMAXCONN
#define SOMAXCONN (5)
#endif

#ifdef DEBUG_RP2040_PORT
#define SERIAL_PORT DEBUG_RP2040_PORT
#else
#define SERIAL_PORT Serial1
#endif

#define PERROR(str) my_perror(__FILE__, __LINE__, str)

static constexpr char const ssid[] = STASSID;
static constexpr char const password[] = STAPSK;

static int server_fd = -1;

static void
my_perror(const char *file, uint32_t lineno, const char *msg)
{
  SERIAL_PORT.printf("%s %d: %s %s\n", file, lineno, msg, lwip_strerr(errno));
}

static void
service_task(void *arg)
{
  static bool is_connected = false;
  static uint8_t buf[512];
  int client_fd;
  struct sockaddr_in addr_in;
  for (;;) {
    if (is_connected) {
      ssize_t n;
      n = read(client_fd, buf, sizeof(buf));
      if (n > 0) {
        write(client_fd, buf, n);
      } else {
        close(client_fd);
        is_connected = false;
        SERIAL_PORT.printf("disconnected\n");
      }
    } else {
      SERIAL_PORT.printf("waiting for connect\n");
      socklen_t addrlen = sizeof(addr_in);
      client_fd = accept(server_fd, reinterpret_cast<struct sockaddr*>(&addr_in), &addrlen);
      SERIAL_PORT.printf("connected from %s\n", IPAddress(addr_in.sin_addr.s_addr).toString().c_str());
      is_connected = true;
    }
  }
}


void
setup()
{
#ifdef ARDUINO_ARCH_RP2040
  cyw43_arch_init();
#endif
  SERIAL_PORT.begin(115200);
  while (!SERIAL_PORT) {
    ;
  }
  SERIAL_PORT.printf("build [%s %s]\n", __DATE__, __TIME__);
  WiFi.begin(ssid, password);
  SERIAL_PORT.println("");

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    SERIAL_PORT.print(".");
  }
  SERIAL_PORT.println("");
  SERIAL_PORT.print("Connected to ");
  SERIAL_PORT.println(ssid);
  SERIAL_PORT.print("IP address: ");
  SERIAL_PORT.println(WiFi.localIP());
  server_fd = socket(AF_INET, SOCK_STREAM, 0);
  struct sockaddr_in addr_in;
  addr_in.sin_family = AF_INET;
  addr_in.sin_addr.s_addr = INADDR_ANY;
  addr_in.sin_port = htons(23);
  int ret = bind(server_fd, reinterpret_cast<struct sockaddr*>(&addr_in), sizeof(addr_in));
  if (ret < 0) {
    PERROR("bind:");
  }
  ret = listen(server_fd, SOMAXCONN);
  if (ret < 0) {
    PERROR("listen:");
  }
  TaskHandle_t task_handle;
  BaseType_t result = xTaskCreateAffinitySet(service_task, "tcp_server", 0x400, NULL, 5, 0x1, &task_handle);
  if (result != pdPASS) {

  }
  static char buf[1024];
  vTaskList(buf);
  Serial.print(buf);
  SERIAL_PORT.println("task created");
}

void
loop()
{
  static char buf[1024];
  vTaskList(buf);
  SERIAL_PORT.println("\n----------------+-+-------+---------+-------+");
  SERIAL_PORT.print(buf);
  delay(5000);
}

void
loop1()
{
  SERIAL_PORT.print('#');
  delay(1000);
}
```
</details>

## 5. 動作結果
上記のプログラムを動作させると、タスク一覧が表示されます。TCP/IPのサーバタスクが接続待ち状態になっていることがわかります。
```
----------------+-+-------+---------+-------+
CORE0           X       4       788     2
IDLE0           X       0       235     7
IDLE1           R       0       235     8
Tmr Svc         B       2       975     6
tcpip_thr       B       6       76      10
CORE1           B       4       936     3
USB             B       6       186     1
IdleCore0       S       7       98      4
async_con       S       4       778     9
IdleCore1       S       7       98      5
tcp_serve       B       5       888     11

```