SNSへはこちら

マイコンでprintfを使う

皆さん、「マイコンプログラミングでは、プログラミングの基本となる printf は使わない」なんて言われていません/信じていませんか?この記事ではじゃあ使えるようにしてやるよといった内容です。対象は RX220、RX631。他のマイコンでも似たようなことをやればできると思います。

なお STM32 では以下の記事に従えば楽にできます。

STM32でUARTをやってみる6(float型printfをUART経由で出力) - ガレスタさんのDIY日記

さて、マイコン好きな人は、UART で文字を出力するオレオレprintfを愛用していることでしょう。僕もこれを結構愛用しています。

#include <stdio.h>
#include <stdarg.h>
__attribute__((format(printf, 1, 2))) void uart_printf(const char *format, ...) {
    va_list arg;
    va_start(arg, format);
    char buf[64];
    vsprintf(buf, format, arg);
    char *p = buf;
    while( *p != '\0' ) {
        uart_send(*p);
        p++;
    }
    va_end(arg);
}

ですが、今回はシステムコールを自分で定義して printf そのものを実行できるようにしてしまおう、というわけです。

とりあえず出力先なんかを考えずに Hello World(ハロワ)を書いてみます。例えば下の RX631 用のコード。

#include <stdio.h>

int main(void) {
    sysclk_init(); // 96MHz through PLL
    cmt0_init(); // initialize CMT0 for ms_wait
    while(1) {
        ms_wait(1000);
        printf("Hello\n");
    }
    return 0;
}

こんなの、誰がどう見ても組み込み用だと思いませんよね。とりあえずビルドしてみましょう。。。

/usr/local/rx-elf/rx-elf/lib/libg.a(lib_a-closer.o): In function `close_r':
../newlib/libc/reent/closer.c:53: warning: _close is not implemented and will always fail
/usr/local/rx-elf/rx-elf/lib/libg.a(lib_a-fstatr.o): In function `fstat_r':
../newlib/libc/reent/fstatr.c:62: warning: _fstat is not implemented and will always fail
/usr/local/rx-elf/rx-elf/lib/libg.a(lib_a-isattyr.o): In function `isatty_r':
../newlib/libc/reent/isattyr.c:58: warning: _isatty is not implemented and will always fail
/usr/local/rx-elf/rx-elf/lib/libg.a(lib_a-lseekr.o): In function `lseek_r':
../newlib/libc/reent/lseekr.c:58: warning: _lseek is not implemented and will always fail
/usr/local/rx-elf/rx-elf/lib/libg.a(lib_a-readr.o): In function `read_r':
../newlib/libc/reent/readr.c:58: warning: _read is not implemented and will always fail
/usr/local/rx-elf/rx-elf/lib/libg.a(lib_a-writer.o): In function `write_r':
../newlib/libc/reent/writer.c:58: warning: _write is not implemented and will always fail

いっぱい怒られました。そりゃそうだ。どこに出力するかも決めてないし。確か ARM だと怒られないんでしたっけ。いずれにしろ、以下の関数が足りないです。ビルド自体は通るのですが、実行時にコケます。

  • close
  • fstat
  • isatty
  • lseek
  • read
  • write

さて、実装しましょう。以下のサイトを参考にしてください。下の方に Definitions for OS interface という項目があります。ここではミニマルな実装がありますので、それぞれパクってどっかのファイルに定義しておいてください。というかそれだけで使えるようになります。

The Red Hat newlib C Library

...にしてもすごいですね。close に至っては常に失敗を返すという仕様にしてあるので。まあ特にすることがないから失敗なんでしょう。
さて、肝心なのは write の中身です。以下のようにすれば printf の出力を指定できます。ついでに '\n' と打つだけで CR+LF になりますよ。

int write(int file, char *ptr, int len) {
    int todo;

    for (todo = 0; todo < len; todo++) {
        if( *ptr == '\n' ) {
            uart_send('\r');
            uart_send('\n');
        }else{
            uart_send(*ptr);
        }
            ptr++;
    }
    return len;
}

続いてリンカスクリプトを多少修正します。というのもこの printfメモリを食うんです。なのでユーザースタック領域を拡張しておきましょう。

.ustack 0x400: AT(0x400)

最後に先程の関数に次を追加して、バッファリングを無効化します。

setbuf(stdout, NULL);
setbuf(stdin, NULL);

以上でできました。main関数の方はまとめてこんな感じです。

#include <stdio.h>

int main(void) {
    sysclk_init(); // 96MHz through PLL
    cmt0_init(); // initialize CMT0 for ms_wait
    setbuf(stdout, NULL);
    setbuf(stdin, NULL);

    while(1) {
        ms_wait(1000);
        printf("Hello\n");
    }
    return 0;
}

それでもまだ怒られる人はリンク時に -lc -lg -lgcc を追加すればいいと思います。

これで、1秒ごとにシリアルターミナルに Hello と出力されますね。そう、マイコンでも printf は使えるのだ!