C32標準ライブラリ(libc)における入出力について 2013.8.15

     Microchip社が提供しているPIC32MX用のCコンパイラ、C32に関する話題です。PIC32MXを使ったあるプロジェクトを開発中に遭遇したライブラリに関する内容です。これから述べる内容は、既にご存知の方もおられるかと思います。

     C32に付属する標準ライブラリ(libc)では、printf(),fopen(),fclose(),fread(),fwrite(),fseek()などの高水準入出力関数を使うことができます。


     まず最初に printf() から。

     下記のような、定番プログラムを書いてコンパイルして見ると、何のエラーもなくコンパイルされ、リンクも成功してhexファイルが出来上がります。

    
      #include <stdio.h>
    
      void main(void)
      {
          printf("Hello world\n");
      }
    


     LinuxやWindowsの世界であれば、printf()の出力は、ターミナルに表示されますが、このプログラムを実行したら、printf()の出力はどうなるのでしょうか? 

     実は、C32では、デフォルトでは、printf()の出力は、_mon_putc()という関数を通じてUART2に出力されるようになっています。そのため、このプログラムを実行する場合には、事前にUART2を使えるようにしておかなければなりません。

     Microchipが提供するサンプルプログラムの多くでは、UART2がデバッグモニターの出力先として使われていますので、このような設計になっているのだと思います。

     この、_mon_putc()という関数は、weak属性が付いた関数なので、アプリケーションプログラムの中で同名の関数を記述すると、関数を置き換えることができます。

     そこで、次のようなプログラムを作成してやると、printf()の出力を、LCDに表示させたり、UART1に出力させたりすることができます。

    
      #include <stdio.h> 	
    
      extern void UART1PutChar(char c);  // UART1への1文字出力関数
      extern void LCDPutChar(char c);    // LCDへの1文字出力関数
    
      void _mon_putc(char c)
      {
           LCDPutChar(c);
      }
    
      void main(void)
      {
           printf("Hello world\n");
      }
    


     printf()の部分を、fprintf(stdout,...)やfprintf(stderr,...)と書いても問題なく動作します。 stdout,stderrは、ライブラリによってプログラムの実行開始時点でオープンされています。


     次に fopen(),fwrite(),fclose()です。

     次のようなプログラムを作成します。printf()の出力先は、UART1にしています。

    
      #include <stdio.h>
      #include <string.h>
    
      extern void UART1Init(void);         // UART1の初期化
      extern void UART1PutChar(char c);    // UART1への1文字出力関数
      extern void UART1PutString(char *s); // UART1への文字列出力関数
    
      void _mon_putc(char c)
      {
           UART1PutChar(c);
      }
    
      char *text = "This is test.\n";
    
      void main(void)  
      {
          FILE *fout;
    
          UART1Init();
    
          fout = fopen("test.txt", "w");
          if(fout == NULL){
               printf("file open error!\n");
               return;
          }
    
          fwrite(text, strlen(text), 1, fout); 
    
          fclose(fout);
      }
    

    コンパイルを行って見ると、エラーなくコンパイルされましたが、リンクで次のようなエラーが出力されました。

     c:/program files/microchip/mplabc32/v2.02/bin/../lib/gcc/pic32mx/4.5.1/../../../../pic32mx/lib\libc.a(freopen.o): In function `freopen': /home/c11067/work/C32/builds/pic32-microchip-release-2.02-freeze-20111128/pic32-libs/libc/src/freopen.c:(.text.freopen+0xe4): undefined reference to `open'


     freopen()関数が呼び出すopen()関数が無いというエラーです。プログラムでは、freopen()ではなく、fopen()を使っているのですが、きっとfopen()がfreopen()を呼び出し、freopen()が、open()を呼び出しているのだと思われます。

     C32に付属のライブラリマニュアルを読むと、open(),write(),read(),close()などの低水準入出力関数は、サポートしていないと書かれています。

     open(),write(),read(),close()などは、UNIX環境では、システムコールと呼ばれる関数です。fopen()やfwrite()から、open()やwrite()が呼び出されます。

     これらの関数がC32のライブラリの中では、サポートされていないというのですから、リンクでopen()が無いというエラーが出ても仕方ありません。

     それにしても、不思議な点があります。なぜか、write()やclose()が無いというエラーが出ていません。なんともおかしな話です。

     それでは、無いものは作りましょうということで、次のような動作確認のためのプログラムを書いてみます。printf()の出力先は、UART1です。

    
      #include <stdio.h>
      #include <string.h>
    
      extern void UART1Init(void);         // UART1の初期化
      extern void UART1PutChar(char c);    // UART1への1文字出力関数
      extern void UART1PutString(char *s); // UART1への文字列出力関数
    
      void _mon_putc(char c)
      {
           UART1PutChar(c);
      }
    
      int open(const char *pathname, unsigned int flags, unsigned int mode)
      {
          return -1;    // open 失敗を返す。
      }
    
      size_t write(int fd, const void *buf, size_t nbytes)
      {
          return nbytes;  // 書き込み要求サイズを全て書き込んだ。
      }
    
      size_t read(int fd, void *buf, size_t nbytes)
      {
          return nbytes;  // 読込要求サイズを全て読み込んだ。
      }
    
      int close(int fd)
      {
          return 0;    // close 成功を返す。
      }
    
      char *text = "This is test.\n";
    
      void main(void)  
      {
          FILE *fout;
    
          UART1Init();
    
          fout = fopen("test.txt", "w");
          if(fout == NULL){
               printf("file open error!\n");
               return;
          }
    
          fwrite(text, strlen(text), 1, fout); 
    
          fclose(fout);
      }
    


     このプログラムをコンパイルして見ると、エラーなくコンパイルされ、リンクも正常に終了し、hexファイルが出来上がりました。

     このプログラムを実行して見ます。open()が -1 を返しますので、fopen()の呼び出しは、エラーとなるはずです。そして、UART1で接続したPC上のターミナルには、printf()の出力する "file open error!" のメッセージが出力されるはずです。

     ところが、期待に反して、PC上のターミナルには、何も表示されません。どこかおかしいようです。printf()によるエラー出力部分を次のように書き換えて再度実行してみます。

    
        if(fout == NULL){
          // printf()を介さずに直接UART1に出力
          // printf("file open error!\n");
          UART1PutString("file open error!\n");
          return;
        }
    


     こちらの場合には、期待通りに、PC上のターミナルに "file open error!" が表示されました。どういうことなんでしょうか? 

     write()やread()を作成したらprintf()の出力が表示されなくなりました。

     printf()は、fprintf(stdout,...)です。fprintf()は、fwrite()、あるいは、fputc()などを使ってstdoutに出力していると思われます。そして、fwrite()やfputc()は、write()を呼び出しているはずです。あるいは、fprintf()が、write()を直接呼び出しているかも知れません。

     つまり、printf()は、なんらかの関数を通して、あるいは直接に、write()を呼び出していると思われます。そこで、write()関数を試しに次のように書き換えてみます。wtite()の第1引数のfdは、ファイルディスクリプタで、特に何もしていなければ、stdoutの場合は、1、stderrの場合は、2 のはずです。

    
      size_t write(int fd, const void *buf, size_t nbytes)
      {
        int cnt;
    
        // stdout, stderr ?
        if(fd == 1 || fd == 2){
          for(cnt = 0; cnt < nbytes; cnt++){
            _mon_putc(*(char *)buf++);
          }
        }
        return nbytes;  // 書き込み要求サイズを全て書き込んだ。
      }
    


    printf()によるエラー出力部分を元に戻して、コンパイル、実行して見ます。予想したとおり、今度は、PCのターミナル上に、先ほどと同じように "file open error!" が表示されました。

     以上の事から、write(),read(),などの低水準入出力関数は、open()を除いてライブラリの中にweak属性を持った関数として存在しているように思われます。そして、write()の中できっと_mon_putc()を呼び出しているのだと思います。 

     先に、open()が無いというリンクエラーは出るのにwrite()やclose()が無いというリンクエラーが出なかったことの説明にも繋がります。



     さて、以上のことから、何が言いたかったか、ということです。実は、ここからが本題です。

     PIC32MX695やPIC32MX795では、プログラムメモリが512Kバイトもありますから、printf()などを使ってもプログラムエリアが早々に不足するようなことは無いと思います。printf()を使えば、書式付きの出力が容易に実現できるので便利です。何よりも、printf()デバッグで変数の値などの表示を簡単に行えるのは嬉しい限りです。

     また、open()、write()などの関数をうまくコーディングすれば、UNIXのプログラミングの世界と同じように、LCDやUARTなどの外部デバイスとの入出力を、ファイル入出力で統一することができます。また、UNIXやLinux上で書かれた、fopen()などを使ったプログラムを移植するのも容易になります。

     PICマイコンで、手軽に扱える外部記憶装置にSDカードがありますが、SDカードを使う場合には、直接セクターを指定してデータを読み書きすることも出来ますが、通常は、PCとの互換性の観点から Fatファイルシステムを使います。

     PICマイコンでFatファイルシステムを扱う場合には、Microchipの提供する FSIO.c や、Chan氏が作成・公開されている ff.c がよく使われています。私もこのプログラムのお世話になっています。

     FSIO.cは、FSfopen(),FSfread(),FSfclose()のように関数名にFSのプリフィックスが付きますが、libcの関数とほぼ同じ仕様となっており判り易くていいのですが、少し重い気がします。

     一方、Chan氏公開の ff.c は軽量で軽快ですが、関数仕様が独自の仕様となっており、libc準拠のプログラムを移植する場合には、関数名は元より、関数引数の見直しが必要など、結構手間暇がかかります。

     そこで、open(),write(),read()などの低水準関数を、ff.c を使って作成しておけば、fopen()などの高水準関数からSDカードのFatファイルシステムに簡単にアクセスすることが出来るようになります。

     簡単なファイル入出力のプログラムに限定はされますが、Linuxなどの上で作成したソースプログラムを一切変更することなくPICマイコンで動かすことも出来る訳です。


    Chan氏作成の ff.c を使ってopen()などの低水準関数をコーディングしてみました。ファイルは、unixio.c です。

      unixio.c   低水準入出力関数プログラム

     このunixio.cは、ファイルアクセスに限定して作成しましたが、次のようなプログラムを書くこともできます。先に述べましたデバイスへの入出力をファイル入出力で統一する簡単な例です。

     "COM1:"というファイルをオープンすると、UART1にアクセスできます。"COM2:"ならUART2になります。

    
      #include <stdio.h>
      #include <string.h>
    
      #define  COM1_HANDLE  101
      #define  COM2_HANDLE  102
    
      extern void UART1Init(void);
      extern void UART2Init(void);
      extern void UART1PutChar(char c);
      extern void UART2PutChar(char c);
    
      void  main(void)
      {
          FILE *fout;
    
          UART1Init();
          UART2Init();
    
          fout = fopen("COM1:", "w");
          fprintf(fout, "print to UART1\n");
          fclose(fout);
      }
    
    
      int  open(const char *pathname, unsigned int flags, unsigned int mode)
      {
          if(strcmp(pathname, "COM1:") == 0){
              return COM1_HANDLE;
          }else if(strcmp(pathname, "COM2:") == 0){
              return COM2_HANDLE;
          }
      }
    
    
      size_t  write(int fd, const void *buf, size_t nbytes)
      {
          if(fd == COM1_HANDLE){
              for(cnt = 0; cnt < nbytes; cnt++){
                  UART1PutChar(*(char *)buf++);
              }
          }else if(fd == COM2_HANDLE){
              for(cnt = 0; cnt < nbytes; cnt++){
                  UART2PutChar(*(char *)buf++);
              }
          }
          return nbytes;
      }
    
    

     メモリリソースの少ないPICマイコンで、このようなコーディングをすることはないと思いますが、メモリの大きなPIC32MX695やPIC32MX795では、こんなことも出来るということを知っていても損することはないと思いますが、どうでしょうか。