C++ でI/Oデバイスの操作を考える

組み込みでもC++と言う事で、最近では、RXマイコンでC++0Xを使ってソフト開発をしていますが・・

ルネサスなどのサンプルでは、I/Oデバイスの操作は、C言語で使えるビットフィールドを多用しています。
サンプルに付属のハードウェアーデバイスを定義してあるヘッダー「iodefine_renesas.h」がその典型例です、しかしながら、規約では、8ビットや16ビットの定義は処理系依存であったと思います。
又、エンディアンが変わった場合も少なくと gcc では問題あります。
それなのに、このやり方が使われているのは大きな問題だと感じています。

ルネサスが試用版としてダウンロード出来るコンパイラでは問題無いようですが、gcc では問題となります。
※やった事は無いのですが、ルネサスのCコンパイラで boost が正しくコンパイルできるか不明ですし、C++11を使いたいし、IDEは嫌いだしで、積極的に使いたいとは思えません。
※時間が出来たら、RX用のLLVMをポートしたいです。
特に gcc では、最適化が施された場合に、32ビットでしかアクセス出来ないI/O空間に、バイトでアクセスするようなコードが出てくる場合もあります。
※これはRX用 gcc の問題なのかもしれません。

又、C++の強みを生かしたコーディングをしたいのもあります、今回RXマイコン用のコードを作る過程で、C++でI/Oデバイスを操作するライブラリーを実験、考えてみました。
自分のC++スキル以内の事しか出来ませんが、とりあえず、我慢できる程度の物が出来たので紹介します。

まず、どんな感じで書きたいか考えます。
RXマイコンに備わっているI/Oポートを例に考えてみます。

    device::PORT2::DDR.B3 = 1; // PORT23 output (/xCS)
    device::PORT2::DDR.B2 = 1; // PORT22 output (/xDCS/BSYNC)
    device::PORT4::DDR.B7 = 0; // PORT47 input (DREQ)
    device::PORT4::ICR.B7 = 1;
    device::PORT2::DR.B3 = 1; // xCS=H
    device::PORT2::DR.B2 = 1; // xDCS=H

とりあえず、こんな感じでしょうか?
※名前空間は「device」としています。

template <uint32_t base>
struct portx_t {
    typedef io8<base + 0x00> ddr_io;
    struct ddr_t : public ddr_io {
        using ddr_io::operator =;
        using ddr_io::operator ();
        using ddr_io::operator |=;
        using ddr_io::operator &=;

        bits_t<ddr_io, 0, 1>  B0;
        bits_t<ddr_io, 1, 1>  B1;
        bits_t<ddr_io, 2, 1>  B2;
        bits_t<ddr_io, 3, 1>  B3;
        bits_t<ddr_io, 4, 1>  B4;
        bits_t<ddr_io, 5, 1>  B5;
        bits_t<ddr_io, 6, 1>  B6;
        bits_t<ddr_io, 7, 1>  B7;
    };
    static ddr_t DDR;
};
typedef portx_t<0x0008c000> PORT0;
typedef portx_t<0x0008c001> PORT1;

--------
オペレーターの定義として、「=」、「()」、「&=」、「|=」を定義してあります。

「=」オペレーターの戻り値をvoidとして値を返さないようにしてあります。
PORT0::DDR = 0xf0;
と代入は出来ますが・・・
uint8_t data = PORT0::DDR;
とは出来ません、代わりに、
uint8_t data = PORT0::DDR();
とする事で値を取得できます。
これには、幾つかの側面があって、I/Oに対する「読み出し」と「書き込み」は同じアドレスであっても全く別の操作だからです。
又、最適化によって起こる問題を回避できると考えています。
※又、「=」オペレーターを無効にする事により、リードオンリーのデバイスを扱えます。(書き込もうとするとコンパイルエラーになります)

さて、バイト操作が出来たので、ビット操作を考えてみます。
※ bits_t の定義がそれに辺り、アクセス開始ビットと、アクセスするビット幅を定義してあります。

template <class T, uint8_t pos, uint8_t len>
struct bits_t {
    static typename T::value_type get() {
        return (T::read() >> pos) & ((1 << len) - 1);
    }
    static void set(typename T::value_type v) {
        typename T::value_type m = ((1 << static_cast(len)) - 1) << pos;
        T::write((T::read() & ~m) | (v << pos));
    }

    void operator = (typename T::value_type v) { set(v); }
    typename T::value_type operator () () { return get(); }
};

テンプレートに渡すT型のクラスは、8ビット、16ビット、32ビットのアクセス幅により決定します。

    typedef io8<base + 0x00> ddr_io;
    bits_t<ddr_io, 0, 1> B0;

※上の例では、io8 クラスを使っています。

io8 はこんな感じです。

template <uint32_t adr>
struct io8 {
    typedef uint8_t value_type;
    static void write(uint8_t data) { wr8_(adr, data); }
    static uint8_t read() { return rd8_(adr); }void operator = (value_type data) const { write(data); }

    value_type operator () () const { return read(); }
    void operator |= (value_type data) const { write(read() | data); }
    void operator &= (value_type data) const { write(read() & data); }
};

実際にハードウェアー上のアドレスにアクセスするのは、以下です。

static inline void wr8_(uint32_t adr, uint8_t data) {
    *reinterpret_cast<volatile uint8_t*>(adr) = data;
}
static inline uint8_t rd8_(uint32_t adr) {
    return *reinterpret_cast<volatile uint8_t*>(adr);
}

まだ冗長な部分がありますが、とりあえずやりたい事は出来ています。

※完全なソースコードは、AKI-RX62 でMP3再生(VS1063a)を参照して下さい。

※参考文献:
C++テンプレートテクニック

追記、修正: 2013,7,21
operator は継承出来ないので、冗長となっていた部分、using 文で、シンプルに書ける事が判った為修正。
※using ってこんな感じで使えるのか・・
※RX のソースアーカイブは、別の作業(RSPIの割り込み化)をしているので、それが終わってから更新します。

追記、修正: 2013,7,25
コンパイルした場合にどのようなアセンブリコードが出てくるか検証しました。

int main(int argc, char** argv)
{
    // ICLK=96MHz, BCLK=48MHz, PCLK=48MHz SDCLK出力,BCLK出力
    device::SYSTEM::SCKCR = 0x00010100;
    device::SYSTEM::MSTPCRA.MSTPA15 = 0;  // B15 (CMT)のストップ状態解除
    device::SYSTEM::MSTPCRB.MSTPB31 = 0;  // B31 (SCI0)のストップ状態解除
    device::PORT3::DDR.B0 = 1;            // PORT3:B0 output
    device::PORT2::ICR.B1 = 1;            // PORT2:B1 input (RXD0)
}

-O2 で最適化した場合のアセンブリコードです。

fff80a64 <_main>:
fff80a64: fb ee 20 00 08         mov.l    #0x80020, r14
fff80a69: f8 ee 00 01 01         mov.l    #0x10100, [r14]
fff80a6e: fb 4e 10 00 08         mov.l    #0x80010, r4
fff80a73: ec 4e                  mov.l    [r4], r14
fff80a75: fb 3e 14 00 08         mov.l    #0x80014, r3
fff80a7a: 77 2e ff 7f ff         and      #0xffff7fff, r14
fff80a7f: e3 4e                  mov.l    r14, [r4]
fff80a81: ec 3e                  mov.l    [r3], r14
fff80a83: fb 4e 03 c0 08         mov.l    #0x8c003, r4
fff80a88: 74 2e ff ff ff 7f      and      #0x7fffffff, r14
fff80a8e: e3 3e                  mov.l    r14, [r3]
fff80a90: cc 43                  mov.b    [r4], r3
fff80a92: fb ee 62 c0 08         mov.l    #0x8c062, r14
fff80a97: 65 13                  or       #1, r3
fff80a99: c3 43                  mov.b    r3, [r4]
fff80a9b: cc e4                  mov.b    [r14], r4
fff80a9d: 66 01                  mov.l    #0, r1
fff80a9f: 65 24                  or       #2, r4
fff80aa1: c3 e4                  mov.b    r4, [r14]
fff80aa3: 02                     rts

及第点でしょうか・・

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください