標準入出力先をシリアルコンソールに設定する方法

ベアメタル用 C/C++ コンパイラツールチェーンでは、標準ライブラリの標準入出力機能は未サポートなのが普通です。

しかし、SOLID ツールチェーンは、ユーザーコードで write 関数を再定義することにより、標準入出力先をシリアルコンソールなどに設定することができます。

注釈

SOLID ツールチェーンは開発環境と統合されているため、VLINK 機能を有効にすることで、標準入出力先を SOLID-IDE のログウィンドウなどに設定することができます。また、VLINK の出力先を SOLID_QEMU シミュレータのコンソールに設定することも可能です。

注釈

exeClang s008 の RISC-V ターゲットは VLINK 未サポートです。

以下のサンプルコード(主要部分のみ)は弊社の SOLID-OS の ARM ターゲットの BSP から抜粋しました。シリアルコンソールは PL011 で、出力先は TeraTerm(送受信の改行コードは CR)を想定しています。stdout と stderr 以外への出力はサポートしていません。

注釈

write 関数を再定義する場合は、必ず VLINK 無効ライブラリ(exeGCC は noVLINK 版、exeClang は nosys 版ライブラリ)を使用してください。無関係な VLINK のサポートコードが動いてしまうと期待通りに動作しません。

// initialization function
void foo()
{
  ...
  setvbuf(stdout, NULL, _IONBF, 0);
  ...
}

...

int write(int fd, const void *p, int size)
{
  const char *buf = p;
  // stdout or stderr
  if (fd == 1 || fd == 2) {
    for (int i = 0; i < size; i++) {
      if ('\n' == buf[i] && ((i <= 0) || buf[i - 1] != '\r')) {
        while (read_pl011(UARTFR) & PL011_UARTFR_TXFF);
          write_pl011(UARTDR, '\r');
      }
      while (read_pl011(UARTFR) & PL011_UARTFR_TXFF);
      write_pl011(UARTDR, buf[i]);
    }
    return size;
  }
  // else ignore
  return -1;
}

ここでは簡単のため、stdout を setvbuf 関数を使って常にバッファリング無し(_IONBF)に設定しています。

注釈

NetBSD の stdio.h 関数は出力先がファイルか端末かでバッファリング方式を変更するのですが、その判別のために fstat 関数の実装を要求します。fstat 関数が実装されていない場合、常にファイルを対象とした処理(フルバッファリング)になります。そのため、本来は行バッファリング(改行文字が来たらバッファフラッシュ)になって欲しい stdout が期待通り動作しなくなります。

read 関数の場合は以下のようになります。(キーボード入力をエコーバックしています。)

// initialization function
void foo()
{
  ...
  setvbuf(stdin, NULL, _IONBF, 0);
  ...
}

int read(int fd, void *p, int size)
{
  char *buf = p;
  // stdin
  if (fd == 0) {
    for (int i = 0; i < size; i++) {
      while (!(read_pl011(UARTFR) & PL011_UARTFR_RXFF));
        buf[i] = read_pl011(UARTDR);
        while (read_pl011(UARTFR) & PL011_UARTFR_TXFF);
        write_pl011(UARTDR, buf[i]);
        if (buf[i] == '\r') {
          buf[i] = '\n';
          while (read_pl011(UARTFR) & PL011_UARTFR_TXFF);
          write_pl011(UARTDR, '\n');
        }
    }
    return size;
  }
  // else ignore
  return -1;
}

本来、標準入力はターミナルの仕様に依存する複雑なものなのですが、ここでは stdin を _IONBF に設定することにより、fgets 関数などがバッファリング無しで常に 1 文字ずつ読むようになり、read 関数の実装がシンプルになります。