mbed ライブラリ内部構造

このノートブックは、mbed library internals の翻訳版です。

このドキュメントでは、mbed ライブラリの内部構造を解説します。以下の目的に向いています:

  • 新しいマイクロコントローラ向けに mbed ライブラリのポーティングを行う
  • 新しいペリフェラルのドライバを追加する
  • 新しいツールチェインのサポートを提供する

このドキュメントは、プログラミング言語C の知識があることを前提にして記述されていますが、マイクロコントローラの深い知識は必ずしも必要ではありません。

設計の概要

mbed ライブラリはマイクロコントローラー (MCU) ハードウェア(特にペリフェラルのドライバ)の抽象化を提供し、以下のソフトウェアレイヤーと API に分割します。

/media/uploads/emilmont/mbed_layers.png

mbed ライブラリの新しいマイクロコントローラへの移植では、上図で "MCU dependent" と記載されている2つのソフトウェアレイヤーを提供する必要があります。

ペリフェラルドライバーにおけるこれらのレイヤー各々の役割を示すために、LPC1768用の汎用入出力 (GPIO) のドライバーがボトムアップで、どのように組み立てられているかを示します。各々のレイヤーでは、同じシンプルなアプリケーション例(LEDをトグルする "blinky ")の実装を提供します。

オンライン IDE での mbed ライブラリのソース

mbed ライブラリをエディタで編集する場合は、プロジェクトから mbed ライブラリのバイナリビルドを消去し、ソースをインポートします:

Import librarymbed-dev

mbed library sources. Supersedes mbed-src.

ディレクトリ構造

以下のイメージ内から、オフィシャルに公開されている mbed ライブラリのソースコードのディレクトリ構造を参照することができます。 mbed github repository.

3つのターゲット非依存のディレクトリ:

  • mbed/api: 実際の mbed ライブラリ API を定義しているヘッダファイル
  • mbed/common: mbed 共通ソース
  • mbed/hal: 全てのターゲットで実装される HAL API

2つのターゲット依存のディレクトリ:

  • mbed/targets/hal: HAL の実装
  • mbed/targets/cmsis: CMSIS-CORE ソース

/media/uploads/emilmont/mbed_directories.png

MCU レジスタ

ペリフェラル

ソフトウェアのバックグランドがあれば、ソフトウェアに対してマイクロコントローラのペリフェラルがどのように振る舞うかを理解したいと思うでしょうし、個別のコアで実行されているソフトウェアスレッドをペリフェラルのようにして考えることもできます。スレッドとの唯一の通信チャネルはメモリの共有エリアです。このメモリエリア内で個々に単一のバイトアドレッシングが可能で、ハードウェアとの通信に使用される領域をレジスタと呼びます。これらのレジスタをリードライトすることで、ペリフェラルと通信することができます。

LPC17xx GPIO

NXP のような半導体ベンダーは、個々のペリフェラルのレジスタを記載した詳細なユーザーズマニュアルを提供しています。

この LPC17xx user manual ページは、GPIO ペリフェラルのレジスタが記載されています:

/media/uploads/emilmont/lpc17xx_gpio_reg.png

レジスタを直接叩く Blinky の例

mbed LPC1768 のレジスタを叩いて、LED がどのように点滅するかを見てみましょう:

/media/uploads/emilmont/lpc17xx_gpio_set.png

/media/uploads/emilmont/lpc17xx_gpio_clr.png

#include "mbed.h"

// Reuse initialization code from the mbed library
DigitalOut led1(LED1); // P1_18

int main() {
    unsigned int mask_pin18 = 1 << 18;
   
    volatile unsigned int *port1_set = (unsigned int *)0x2009C038;
    volatile unsigned int *port1_clr = (unsigned int *)0x2009C03C;
   
    while (true) {
        *port1_set |= mask_pin18;
        wait(0.5);
       
        *port1_clr |= mask_pin18;
        wait(0.5);
    }
}

CMSIS-CORE

CMSIS-CORE ヘッダは、低レベルレジスタへアクセスするための適切なデータ構造を提供します:

typedef struct {
    __IO uint32_t FIODIR;
    uint32_t RESERVED0[3];
    __IO uint32_t FIOMASK;
    __IO uint32_t FIOPIN;
    __IO uint32_t FIOSET;
    __O  uint32_t FIOCLR;
} LPC_GPIO_TypeDef;

#define LPC_GPIO_BASE   (0x2009C000UL)

#define LPC_GPIO0       ((LPC_GPIO_TypeDef *) (LPC_GPIO_BASE + 0x00000))
#define LPC_GPIO1       ((LPC_GPIO_TypeDef *) (LPC_GPIO_BASE + 0x00020))
#define LPC_GPIO2       ((LPC_GPIO_TypeDef *) (LPC_GPIO_BASE + 0x00040))
#define LPC_GPIO3       ((LPC_GPIO_TypeDef *) (LPC_GPIO_BASE + 0x00060))
#define LPC_GPIO4       ((LPC_GPIO_TypeDef *) (LPC_GPIO_BASE + 0x00080))

この LPC_GPIO_TypeDef 構造体で示されるように GPIO ポートに関連する32バイトのレジスタは1対1でマップされています:

FIODIR   :  4 bytes
RESERVED0: 12 bytes
FIOMASK  :  4 bytes
FIOPIN   :  4 bytes
FIOSET   :  4 bytes
FIOCLR   :  4 bytes
tot      : 32 bytes = 0x20 bytes

例えば、GPIO のドキュメントにはポート1の FIOPIN レジスタのアドレスは 0x2009C034 と記載されています。

LPC_GPIO_TypeDef を使って、この場所の内容にアクセスするには次のようになります:

#include "mbed.h"
 
int main() {
    printf("LPC_GPIO1->FIOSET: %p\n", &LPC_GPIO1->FIOSET);   
}

上記プログラムは、以下のように表示を行います:

LPC_GPIO1->FIOSET: 2009c038

CMSIS-CORE を使用した Blinky の例

CMSIS-Core API を使用して、LPC1768 の LED がどのように点滅するかを見てみましょう:

#include "mbed.h"

// Reuse initialization code from the mbed library
DigitalOut led1(LED1); // P1_18

int main() {
    unsigned int mask_pin18 = 1 << 18;
   
    while (true) {
        LPC_GPIO1->FIOSET |= mask_pin18;
        wait(0.5);
       
        LPC_GPIO1->FIOCLR |= mask_pin18;
        wait(0.5);
    }
}

mbed で CMSIS-CORE に追加した処理

mbed ライブラリは、CMSIS-CORE レイヤーに対して特定の追加処理を提供しています:

mbed HAL API

ターゲット非依存 API

ターゲット非依存 HAL API は、mbed ターゲット非依存ライブラリの基盤となるものです。GPIO HAL API の例を以下に示します:

typedef struct gpio_s gpio_t;

void gpio_init (gpio_t *obj, PinName pin, PinDirection direction);

void gpio_mode (gpio_t *obj, PinMode mode);
void gpio_dir  (gpio_t *obj, PinDirection direction);

void gpio_write(gpio_t *obj, int value);
int  gpio_read (gpio_t *obj);


"Warning”

HAL API は、mbed ライブラリを新しいターゲットにポーティングするために役に立つ内部インターフェスで、変更されるかもしれません。もし「将来的な動作保証」が必要であれば、アプリケーション側からはこの API を使わず、代わりに mbed API を使用して下さい。

ターゲット依存部の実装

mbed HAL API の実装は、全てのターゲット依存部分に必要なコードを隠蔽します。

このような API を実装するには幾つかの方法があり、異なったトレードオフがあります。LPC プロセッサファミリ (LPC2368, LPC1768, LPC11U24) 向けの我々の実装では、全ての GPIO “メソッド” を小さく、速く、そしてターゲット非依存となるように、GPIO オブジェクトの初期化の中でこれらのプロセッサー間の違いをすべて保持することに決めました。加えて、実行速度を最適化するためにコンパイラによって GPIO 関数を完全にインライン展開しました。

このドキュメントでは、コードを明瞭に説明するために、ターゲット向けの条件付きコードとプリプロセッサ指示を削除しています。また、複数のソース(”.c” と “.h”)の関数定義はまとめて表示しています:

struct gpio_s {
    PinName  pin;
    uint32_t mask;

    __IO uint32_t *reg_dir;
    __IO uint32_t *reg_set;
    __IO uint32_t *reg_clr;
    __I  uint32_t *reg_in;
};

void gpio_init(gpio_t *obj, PinName pin, PinDirection direction) {
    if(pin == NC) return;

    obj->pin = pin;
    obj->mask = gpio_set(pin);

    LPC_GPIO_TypeDef *port_reg = (LPC_GPIO_TypeDef *) ((int)pin & ~0x1F);

    obj->reg_set = &port_reg->FIOSET;
    obj->reg_clr = &port_reg->FIOCLR;
    obj->reg_in  = &port_reg->FIOPIN;
    obj->reg_dir = &port_reg->FIODIR;

    gpio_dir(obj, direction);
    switch (direction) {
        case PIN_OUTPUT: pin_mode(pin, PullNone); break;
        case PIN_INPUT : pin_mode(pin, PullDown); break;
    }
}

void gpio_mode(gpio_t *obj, PinMode mode) {
    pin_mode(obj->pin, mode);
}

void gpio_dir(gpio_t *obj, PinDirection direction) {
    switch (direction) {
        case PIN_INPUT : *obj->reg_dir &= ~obj->mask; break;
        case PIN_OUTPUT: *obj->reg_dir |=  obj->mask; break;
    }
}

void gpio_write(gpio_t *obj, int value) {
    if (value)
        *obj->reg_set = obj->mask;
    else
        *obj->reg_clr = obj->mask;
}

int gpio_read(gpio_t *obj) {
    return ((*obj->reg_in & obj->mask) ? 1 : 0);
}

mbed の内部的な慣例

実際的な慣例として、各ターゲットの PinName enum 定義は、port および pin number の両方の情報を含んでいます。この情報がどのように格納されるかは、ターゲット依存です。

例えば、LPC1768 の PinName エントリは、port address に加算した下位5ビットの pin number です。LPC1768 の PinName からポートアドレスを抽出するには、単純にマスクを作成して下位の5ビットを無視しています:

LPC_GPIO_TypeDef *port_reg = (LPC_GPIO_TypeDef *) ((int)pin & ~0x1F);

mbed API

mbed API は、エンドユーザにとって取り扱いやすくオブジェクト指向な API です。これは、mbed プラットフォーム上で開発され、大多数のプログラムで使用される API です。

また、基礎的な型と代入処理用に直感的なキャストを提供する基本的な演算子も定義しました。

class DigitalInOut {

public:
    DigitalInOut(PinName pin) {
        gpio_init(&gpio, pin, PIN_INPUT);
    }

    void write(int value) {
        gpio_write(&gpio, value);
    }

    int read() {
        return gpio_read(&gpio);
    }

    void output() {
        gpio_dir(&gpio, PIN_OUTPUT);
    }

    void input() {
        gpio_dir(&gpio, PIN_INPUT);
    }

    void mode(PinMode pull) {
        gpio_mode(&gpio, pull);
    }

    DigitalInOut& operator= (int value) {
        write(value);
        return *this;
    }

    DigitalInOut& operator= (DigitalInOut& rhs) {
        write(rhs.read());
        return *this;
    }

    operator int() {
        return read();
    }

protected:
    gpio_t gpio;
};

mbed API を使用した Blinky の例

これが最後の例で、mbed ライブラリによって提供されるマイクロコントローラ非依存の GPIO の抽象化です:

#include "mbed.h"

DigitalOut led1(LED1);

int main() {
    while (true) {
        led1 = 1;
        wait(0.5);
   
        led1 = 0;
        wait(0.5);
    }
}

C ライブラリのリターゲット

C 標準ライブラリ stdio モジュールは、入出力用途に多数の関数を提供しています:

C 標準ライブラリ実装は、いくつかのツールチェインによって提供され、入出力がどのように指定されリダイレクトされるかといったカスタマイズが可能です。このカスタマイズは、”C ライブラリ・リターゲット” と呼ばれる事もあります。

mbed ライブラリは、この C ライブラリリターゲットを mbed/src/common/stdio.cpp ファイル中で定義しています。

これは、UART への(stdout、stdin 及び stderr)リターゲットの抜粋です:

#if DEVICE_SERIAL
extern int stdio_uart_inited;
extern serial_t stdio_uart;
#endif

static void init_serial() {
#if DEVICE_SERIAL
    if (stdio_uart_inited) return;
    serial_init(&stdio_uart, STDIO_UART_TX, STDIO_UART_RX);
    serial_format(&stdio_uart, 8, ParityNone, 1);
    serial_baud(&stdio_uart, 9600);
#endif
}

extern "C" FILEHANDLE PREFIX(_open)(const char* name, int openmode) {
    /* Use the posix convention that stdin,out,err are filehandles 0,1,2.
     */
    if (std::strcmp(name, __stdin_name) == 0) {
        init_serial();
        return 0;
    } else if (std::strcmp(name, __stdout_name) == 0) {
        init_serial();
        return 1;
    } else if (std::strcmp(name,__stderr_name) == 0) {
        init_serial();
        return 2;
    }
[...]

#if defined(__ICCARM__)
extern "C" size_t    __write (int        fh, const unsigned char *buffer, size_t length) {
#else
extern "C" int PREFIX(_write)(FILEHANDLE fh, const unsigned char *buffer, unsigned int length, int mode) {
#endif
    int n; // n is the number of bytes written
    if (fh < 3) {
#if DEVICE_SERIAL
        if (!stdio_uart_inited) init_serial();
        for (unsigned int i = 0; i < length; i++) {
            serial_putc(&stdio_uart, buffer[i]);
        }
#endif
        n = length;
    } else {
[...]

#if defined(__ICCARM__)
extern "C" size_t    __read (int        fh, unsigned char *buffer, size_t       length) {
#else
extern "C" int PREFIX(_read)(FILEHANDLE fh, unsigned char *buffer, unsigned int length, int mode) {
#endif
    int n; // n is the number of bytes read
    if (fh < 3) {
        // only read a character at a time from stdin
#if DEVICE_SERIAL
        *buffer = serial_getc(&stdio_uart);
#endif
        n = 1;
    } else {
[...]


Please log in to post comments.