« GCCでmbedの実行ファイルをコンパイル(2) | トップページ | FEZ Domino Get »

mbed + GCCでprintfを使う

タイトルの通り、newlibをリンクしてgccベースでprintfなど標準Cライブラリを使えるようにしました。printf, putchar, puts, getchar, gets程度しか試していないのですが、newlibが使えるようになった過程を記載します。

syscalls.cを実装する

newlibやglibcを使うためには、ライブラリの基本入出力をOSのシステムコールに渡してやる必要があります。組み込みでOSを使っていない場合は、入出力のハードをたたくコードを書く必要があります。syscalls.cというモジュールに所定の関数を定義することで、ライブラリとのインタフェースを行うことができます。

syscalls.cの中では、read、write、sbrk(ヒープの管理)などの関数を実装しますが、関数に2種類あります。
マルチタスク(スレッド)環境で、中断中に別タスクからの呼び出しを可能とするリエントラント型と、非リエントラント型です。リエントラント型はread_rのように、関数名に_rのサフィックスがつきます。また、引数にstruct _reent構造体へのポインタを持ちます。

今回の用途ではリエントラント型にする必要はありませんが、リエントラント型で実装してみました。syscalls.cのコードを以下に示します。fileopenなどハード的に機能がない関数は、-1を返すだけとかの、最小限の実装にします。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/reent.h>
#include "lpc17xx_uart.h"

#define ECHOBACK

extern int errno;
extern unsigned char _end;
unsigned char *heap_end;
register unsigned char *stack_ptr asm ("sp");
LPC_UART_TypeDef *Uart = (LPC_UART_TypeDef *)LPC_UART0;

int _read_r(struct _reent *r, int file, char *ptr, int len)
{
    int  i;
    unsigned char *p = (unsigned char*)ptr;

    for (i = 0; i < len; i++) {
        UART_Receive(Uart, p, 1, BLOCKING);

        #ifdef ECHOBACK
            UART_Send(Uart, p, 1, BLOCKING);
        #endif

        if (*p++ == '\r' && i <= (len - 2)) /* 0x0D */
        {
            *p = '\n';                        /* 0x0A */
            #ifdef ECHOBACK
              UART_Send(Uart, p, 1, BLOCKING);     /* 0x0A */
            #endif
            return i + 2;
        }
    }
    return i;
}

int _lseek_r(struct _reent *r, int file, int ptr, int dir)
{
  return 0;
}

int _write_r(struct _reent *r, int file, const void *ptr, size_t len)
{
    int i;
    unsigned char *p = (unsigned char*) ptr;

    for (i = 0; i < len; i++) {
        if (*p == '\n' ) {
            UART_Send(Uart, (uint8_t*)'\r', 1, BLOCKING);
        }
        UART_Send(Uart, p++, 1, BLOCKING);
    }
    return len;
}

int _close_r(struct _reent *r, int file)
{
  return -1;
}

caddr_t _sbrk_r(struct _reent *r, int incr)
{
    unsigned char *prev_heap_end;

/* initialize */
    if( heap_end == 0 ) {
        heap_end = &_end;
    }
    prev_heap_end = heap_end;

#if 1
    if( heap_end + incr > stack_ptr ) {
       /* heap overflow  */
    UART_Send(Uart, (uint8_t*)"Heap Overflow\r\n", 15, BLOCKING);
    return (caddr_t) -1;
    }
#endif

    heap_end += incr;
#if 0 // Debug
    char buff[32];
    sprintf(buff,"incr:%d, heap:%x\n", incr, heap_end);
    UART_Send(Uart, (uint8_t*)buff, strlen(buff), BLOCKING);
#endif
    return (caddr_t) prev_heap_end;
}

int _fstat_r(struct _reent *r, int file, struct stat *st)
{
  st->st_mode = S_IFCHR;
  return 0;
}

int _open_r(struct _reent *r, const char *path, int flags, int mode)
{
  return -1;
}

int _isatty(int fd)
{
  return 1;
}

char *__exidx_start;
char *__exidx_end;

サンプルを動かしてみる

printfでもろもろの情報を表示した後で、getchar, putcharを使って文字の入出力を行う、ついでにTimer1割り込みを使ってLEDを1sec周期で点滅する、サンプルを作ってみました。ソースコードをここに置きます

ヒープやスタックが正しく動いているか(メモリーリークなどないか)を調べるためのコードを入れてみました。初回のprintfを実行した時点でheaptopが0x10001000となり、.data/.bssエリアと合計になりますが、4KbyteのRAMを消費しています。getcharを使用すると、read_r関数が呼ばれ引数に入力bufferのポインターと文字数が飛んできますが、getcharのような1文字入力でもread関数には1000byteのbuffer付きで呼び出しをかけてきます。メモリーリソースはバンバン使ってくると言えます。

BINファイルのサイズもprintfをリンクしただけでぐっと大きくなり、40Kbyteを超えます。LPC17XXクラスならROMの消費量は気になりませんが、ヒープを結構消費するので、RAMの使用量は注意が必要です。(AVRだとちょっと使えないですね)

試しに、mallocで2048byteのメモリーを確保すると、heaptopが0x10002000なり、heap領域を新たに4Kbyte確保します。確保したメモリーをfreeで開放してもheap領域は縮小せずそのままキープされます。mallocで確保したメモリーはnewlibのメモリー管理によってchunkと呼ばれる単位で管理され、一定の条件を満たすとheap領域の返却を行うようですが、頻繁にheapサイズの変更を行わない作りになっているみたいです。

サンプルを起動した際の、ヒープの変動を以下に示します。

Start
Now called printf
Stack:10007fe8
Heap:10001000

malloc 64byte
Heap:10001000
free 64byte
Heap:10001000
malloc 2048byte
Heap:10002000
free 2048byte
Heap:10002000

 

今回はまったこと

write_r関数は引数に送信バッファを取ります。関数内でCMSISライブラリの1文字出力(UART_SendData)をforループで呼んでいたのですが、UART_SendDataは送信FIFOが空になることを確認せずにreturnしてくるんです。それに気がつかず、UART_SendDataをforループで連続して呼び出すとFIFOがオーバーフローして出力文字のお尻が欠落する現象が発生します。出力文字列の長さが一定以上になると現象が発生するため、最初は、ヒープの管理やスタックの初期設定などの要因でメモリーを壊していないかなど色々調べて時間を要しました。

こんなことで悩むのも楽しみ(頭の体操)の内ではありますが、我ながら初歩的なところではまっているなぁと思います。ちゃんとしたデバック環境がない(mbedでは持てない)からだめなんだな、と言い訳してみる・・

参考情報

  1. Embedding GNU: Newlib, Part 2
  2. ねむいさんのブログ - 今頃LPC2388基板(CQ- FRK-NXPARM)とかいぢってみる ... 今回もお世話になりました
  3. malloc(3) のメモリ管理構造 VA Linux Systems Japan

« GCCでmbedの実行ファイルをコンパイル(2) | トップページ | FEZ Domino Get »

mbed」カテゴリの記事

コメント

コメントを書く

コメントは記事投稿者が公開するまで表示されません。

(ウェブ上には掲載しません)

« GCCでmbedの実行ファイルをコンパイル(2) | トップページ | FEZ Domino Get »

2017年2月
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28        
無料ブログはココログ