rp2040の標準出力先を変更する(Platformio+arduino)

2023/4/12追記:
「3.3 出力処理を実装する」に説明を追記
2023/4/18修正：
「5. まとめ」を修正

## 1. 概要
PlatformIO+rp2040(framework:arduino)では、標準入出力がサポートされており、UART0に接続されています。これを別のデバイスに変更する方法を実装し、動作を確認しました。

## 2. 標準出力の出力処理はどこで行っているか
以下は、どのように調査を行ったかのメモです。
### 2.1 テストプログラムの作成
最初に現在の標準出力の処理を行っている箇所を調べます。以下のプログラムを作成します。
```C
#include <Arduino.h>
#include <stdio.h>

void
setup()
{
  printf("printf test\n");
}

void
loop()
{
  delay(500);
}
```
### 2.2 デバッガを使って調査
次にデバッガを起動し、setup関数の最初の行で停止した状態します。
注意が必要なのは、実行ファイルに対応するソースファイルの情報がない場合、printf文から内部にステップインできないことです。そのため、disassemble表示にしてステップ実行を行う必要があります。disassemble表示にするには、左下の「Swich to assembly」をクリックします。
![debugger1a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193997/db815faf-f441-c98d-27af-35fc2b917be4.png)
「Swich to assembly」をクリックすると、以下のようにdisassembleしたコードが表示されます。
![debugger2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193997/e5edb4de-c371-9b3a-0ab7-d4ef580d11c4.png)
あとはステップ実行を行い、どの関数で出力が行われているかを調べていきます。
その結果、以下の関数が出力を行っていることがわかりました。
コールスタックは以下のようになります。
```
serial_putc@0x1000a392 (\serial_putc.dbgasm:4)
DirectSerial::write(void const*, unsigned int)@0x10007ea8 (不明なソース:0)
write@0x100080cc (\write.dbgasm:19)
_write@0x10008210 (\_write.dbgasm:70)
_write_r@0x1000e080 (\_write_r.dbgasm:10)
__swrite@0x1000dec2 (\__swrite.dbgasm:24)
__sflush_r@0x1000c6f4 (\__sflush_r.dbgasm:124)
_fflush_r@0x1000c75c (\_fflush_r.dbgasm:34)
__swbuf_r@0x1000e032 (\__swbuf_r.dbgasm:55)
_puts_r@0x1000dbca (\_puts_r.dbgasm:86)
puts@0x1000dc04 (\puts.dbgasm:6)
setup@0x10000908 (\setup.dbgasm:4)
main@0x100029d4 (\main.dbgasm:11)
```
serial_putcのコードは、以下のようになっています。r3の値は0x40034000(=UART0_BASE)でしたので、UART0に出力していることがわかります。
```
0x1000a38c: 80 22           	movs	r2, #128	; 0x80
0x1000a38e: 03 68           	ldr	r3, [r0, #0]
0x1000a390: 98 69           	ldr	r0, [r3, #24]
0x1000a392: 10 42           	tst	r0, r2
0x1000a394: fc d0           	beq.n	0x1000a390 <serial_putc+4>
0x1000a396: c9 b2           	uxtb	r1, r1
0x1000a398: 19 60           	str	r1, [r3, #0]
0x1000a39a: 70 47           	bx	lr
```
## 3. 出力する関数を変更
### 3.1 まずはserial_putcを置き換える関数を作ってみる
以下のような関数を追加し、置き換え可能かを試します。関数のプロトタイプは、以下のファイルで定義されています。
C:\Users\\%USERNAME%\\.platformio\packages\framework-arduino-mbed\cores\arduino\mbed\hal\include\hal\serial_api.h

```C
void
serial_putc(serial_t *obj, int c)
{

}
```
ビルドしてみると、リンク時に以下のエラーが発生します(改行を追加しています)。
```
c:/users/%USERNAME%/.platformio/packages/toolchain-gccarmnoneeabi@1.90201.191206/bin
/../lib/gcc/arm-none-eabi/9.2.1/../../../../arm-none-eabi/bin/ld.exe: 
C:\Users\%USERNAME%\.platformio\packages\framework-arduino-mbed\variants\
RASPBERRY_PI_PICO\libs\libmbed.a(serial_api.o): in function `serial_putc':
serial_api.c:(.text.serial_putc+0x0): multiple definition of `serial_putc'; 
.pio\build\pico\src\main.cpp.o:main.cpp:(.text.serial_putc+0x0): first defined here
```
libmbed.a内のシンボルと重複しているというエラーになっています。通常はアーカイブ形式のシンボルより、\..oファイルの検索が優先されるはずではと思い、リンク時のコマンドラインをチェックしてみました。
すると、 --whole-archiveと--no-whole-archiveを指定しています。デフォルトではアーカイブから必要なオブジェクトファイルのみを検索するので、.oファイルに該当するシンボルがあれば探しに行きません。しかしこのオプションを措定するとアーカイブファイルに含まれるすべてのオブジェクトがリンクされてしまうため、シンボルが重複してしまうことがわかりました。

-Wl,--start-group **-Wl,--whole-archive** .pio\build\pico\libFrameworkArduinoVariant.a 
.pio\build\pico\libFrameworkArduino.a -lmbed **-Wl,--no-whole-archive** 
-lstdc++ -lsupc++ -lm -lc -lgcc -lnosys -Wl,--end-group

### 3.2 どの関数を置き換えるかを検討する
libmed.aに入っていない関数であれば置き換えが可能なので、コールスタックから_write_rを選びます。
関数のプロトタイプは、newlib以下にはありましたが、使っているビルド環境にはなかったので、newlibでの形式に合わせます。
さらにCリンケージになるよう、extern "C"をつけておきます。ビルドし、エラーにならないことを確認しました。

```C
extern "C" {
struct _reent;
}

extern "C" ssize_t _write_r(struct _reent *ptr, int fd, const void *buf, size_t n)
{
  (void)ptr;
  return 0;
}
```

### 3.3 出力処理を実装する
標準出力と標準エラー出力のときのみ、USBシリアルに出力するよう実装してみました。割込みコンテキストから呼ばれた場合には、何も出力しないでreturnするようにしました[^1]。割込みコンテキストから呼ばれたかの判断には、関数core_util_is_isr_active()を使っています。

[^1]: 割り込みコンテキストから、Serial.print()などが呼び出し可能か不明だったためこのような実装としました。

```C
#include <Arduino.h>
#include <platform/mbed_critical.h>
#include <errno.h>

extern "C" {
struct _reent;
}

extern "C"
ssize_t _write_r(struct _reent *ptr, int fd, const void *buf, size_t n)
{
  (void)ptr;
  if (core_util_is_isr_active()) {
    return 0;
  }
  const char *s = reinterpret_cast<const char *>(buf);
  if (fd == 1 || fd == 2) {
    for (uint32_t i = 0; i < n; i++) {
      Serial.print(*s++);
    }
  }
  return n;
}
```

## 4. 動作確認
以下のメインプログラムを作成し、標準出力および標準エラー出力がUSBシリアルに出力されるかを確認します。
```c
#include <Arduino.h>
#include <stdio.h>

void
setup()
{
  while (!Serial) {
    ;
  }
  Serial.println("stdio test");
}

void
loop()
{
  printf("stdout\n");
  fprintf(stderr, "stderr\n");
  delay(1000);
}
```
シリアルモニタ上に以下のメッセージが出力されました。
```
stdout
stderr
stdout
stderr
```

## 5. まとめ
当初は、APIのパラメータチェックにassertを使い、エラーが発生した時のメッセージをUSBシリアルに出そうとして、調査を始めました。しかし単にUSBシリアルに出力するよう変更しただけでは、うまくいかないことが分かったので、上記内容となりました。assertion fail時のメッセージを出力させる方法は、別の方法で実現しようとしています。