鳥なき里のマイコン屋(49) ARM Mbed その7、classの行方

開発ツールとしてのARM Mbedを調べてみるシリーズから「スピンオフ(フォークというべきか?)」する感じで、ターゲットボードにいろいろデバイスをつなげてみるシリーズを始めました。そのときに、真っ先に困ったのが、デバイスを繋げるための自分のコードを書くときに「どんなスタイルで書くのがいいの?」という点。組み込み用のマイコンのプログラミングというとCで書くのが「未だに」一般的じゃないかと思います。しかし、Mbedは基本C++で、結構カッコよくclassを使ってハードウエアの下の方を隠蔽している感じです。

※「鳥なき里のマイコン屋」投稿順Indexはこちら

「未だに」組み込みじゃCが優勢に見える理由にはいろいろあると思うのですが、実際は「結構まぜて使っている」んじゃないかと思います。どこの会社もいろいろ考え方があり、コーディング規約に反映されてしていることでしょう。基本的なスタイルをいくつか列挙しましょう。

    1. Cで「伝統的な手続き型のスタイル」で書く
    2. Cの範囲で書くけれど、心はOOPスタイル。
    3. C++コンパイラは使うのだけれど、ごくごく一部の機能のみ使う
    4. C++コンパイラで制限なく書く

1の場合、流石にK&Rは無いでしょうが、C11どころかANSI-Cくらい限定で、伝統的なスタイル、品質保証やら何やら長年の蓄積もあり、生成されるオブジェクトが「想像つき」、小さくて速いに違いない、という考え方。2は、今時のプログラマなら、OOPに慣れているので使いたいのだけれど、諸般の事情があってCコンパイラ限定せざるを得ない。そのため、Cの範囲で構造体とか関数ポインタ駆使してC++風に仕上げるというもの。3は、C++コンパイラの便利なところは使うのだけれど(ネームスペースとか)、基本メモリを食うような、あるいは動的なところには手を出さず、ごくごく控えめにC同様なオブジェクトが得られる範囲で書く。4は、newあり、templateあり、オーバライドにオーバロード何でもありですが、流石にリソースがタイトな組み込みではこれはまずありますまい。

今回、MbedでLCDパネルに出力するルーチンを書こうとしたときに、まず迷ったのがこんなことでした。そこで、いくつか書いて比べてみることにいたしました。

まずは手続き型、伝統Cスタイル

int lcdDisplaySTR(I2C *lcd, char Lin, char* message)
{
    char wdata[2];
    int status =0;
    int idx = 0;
    wdata[0] = 0x0;
    wdata[1] = Lin;
    status = lcd->write(LCD_ADDR, wdata, 2, 0);
    wdata[0] = 0x40;
    while ((status == 0) && (idx < LCD_MAXLEN) && (*message != 0)) {
        wdata[1] = *message++;
        status = lcd->write(LCD_ADDR, wdata, 2, 0);
        idx++;
    }
    return status;
}

引数としてMbed OSのオブジェクトであるI2Cのインスタンスへのポインタをとって(ポインタならCにもあるものね)、それに向けて出力するという形。これを呼び出す側はこんな感じ。

#include "mbed.h"
#include "AQM1602XA.h"

I2C i2c(I2C_SDA, I2C_SCL);
DigitalOut myled(LED2);
Serial pc(USBTX, USBRX);

int main() {
    int i=0;
    char buffer[2];
    int status;
    
    pc.printf("test AQM1602XA LCD\r\n");
    lcdInit(&i2c);
    lcdDisplaySTR(&i2c, LCD_HLINE, "Hello World!");
    
    while (1) {
        buffer[0]=0x20 + i;
        buffer[1]=0;
        status = lcdDisplaySTR(&i2c, LCD_LLINE, buffer);
        if (buffer[0] >= 0x7F) i = 0;
        pc.printf("%d --- %d \r\n",i, status);
        wait(0.5);
        i++;
        myled = !myled;
    }
}

Flashの消費は33.5kB、RAMは2.1kB。ま、いいんだけれど、ちょっとMbedっぽくはないね。

次にちょっとMbedぽくして、C++らしくクラスを導入

class LCDAQM1602 {
private:
    I2C* i2cRef;
    char wdata[2];
    int status;
public:
    LCDAQM1602(I2C* ch);
    int writeCommand(char cmd);
    int writeData(char dat);
    int clear();
    int displaySTR(char Lin, char* message);
    int init();
};

すると先ほどの関数は、

int LCDAQM1602::displaySTR(char Lin, char* message)
{
    int idx = 0;
    status =0;
    wdata[0] = 0x0;
    wdata[1] = Lin;
    status = i2cRef->write(LCD_ADDR, wdata, 2, 0);

    wdata[0] = 0x40;
    while ((status == 0) && (idx < LCD_MAXLEN) && (*message != 0)) {
        wdata[1] = *message++;
        status = i2cRef->write(LCD_ADDR, wdata, 2, 0);
        idx++;
   }
   return status;
}

そしてメインプログラムは、

#include "mbed.h"
#include "lcdAQM1602.h"

DigitalOut myled(LED2);
I2C i2c(I2C_SDA, I2C_SCL);
Serial pc(USBTX, USBRX);

int main() {
    LCDAQM1602 lcd(&i2c); 
    int i=0;
    char buffer[2];
    int status;

    pc.printf("AQM1602 LCD CLASS\r\n");
    lcd.init();
    lcd.displaySTR(LCD_HLINE, "Hello World!");

    while (1) {
        buffer[0]=0x20 + i;
        buffer[1]=0;
        status = lcd.displaySTR(LCD_LLINE, buffer);
        if (buffer[0] >= 0x7F) i = 0;
        pc.printf("IDX:%d --- STAT:%d \r\n",i, status);
        wait(0.5);
        i++;
        myled = !myled;
    }
}

こちらは、Flash 28kB、RAM1.8kB。先ほどの手続き型より小さくなっている。でもまだ、ポインタ使っているところがなんか汚い。

C++らしく参照型に変更

class AQM1602 {
private:
    I2C& i2cRef;
    char wdata[2];
    int status;
public:
    AQM1602(I2C& ch);
    int writeCommand(char cmd);
    int writeData(char dat);
    int clear();
    int displaySTR(char Lin, char* message);
    int init();
};

すると関数は、気持ちわずかにスッキリした?

int AQM1602::displaySTR(char Lin, char* message)
{
    int idx = 0;
    status =0;
    wdata[0] = 0x0;
    wdata[1] = Lin;
    status = i2cRef.write(LCD_ADDR, wdata, 2, 0);

    wdata[0] = 0x40;
    while ((status == 0) && (idx < LCD_MAXLEN) && (*message != 0)) {
        wdata[1] = *message++;
        status = i2cRef.write(LCD_ADDR, wdata, 2, 0);
        idx++;
    }
    return status;
}

そしてmain

#include "mbed.h"
#include "AQM1602.h"

DigitalOut myled(LED2);
I2C i2c(I2C_SDA, I2C_SCL);
Serial pc(USBTX, USBRX);

int main() {
    AQM1602 lcd(i2c); 
    int i=0;
    char buffer[2];
    int status;

    pc.printf("AQM1602 LCD TEST\r\n");
    lcd.init();
    lcd.displaySTR(LCD_HLINE, "Hello World!");

    while (1) {
        buffer[0]=0x20 + i;
        buffer[1]=0;
        status = lcd.displaySTR(LCD_LLINE, buffer);
        if (buffer[0] >= 0x7F) i = 0;
        pc.printf("IDX:%d --- STAT:%d \r\n",i, status);
        wait(0.5);
        i++;
        myled = !myled;
    }
}

Flash 33.6kB, RAM2.1kB。

ということで、クラスは使うのだけれど、参照型ではなくちょっと汚いポインタ型(組み込みなんだし、自分でテストできるのだからポインタのチェックなどしないよ!)があれよ一番サイズが小さいということになりました。オールドスタイルと参照型のスタイルがほぼ同着という結果。それで良いのか?

鳥なき里のマイコン屋(48) ARM Mbed その6 Make-GCC-ARMへイクスポート へ戻る

鳥なき里のマイコン屋(50) ARM Mbed その8 Mbed Simulator へ進む