「ソフトウェアー・エンジニアリング」カテゴリーアーカイブ

ソフトウェアー関係の話題など・・

最近は、github にプッシュしてます~

RL78 のフレームワークを久々に改善

C++ は組み込みでは積極利用者が少ない事を実感する日々

最近、特に感じるが、何故か、C++ を組み込みマイコンで積極利用している人が少ないように感じる。

やはり、ハードルがあるのだろうと思う(自分は慣れの問題で感じなくなっている)

どう考えても、C で実装するより、間違いが少なく、構造的に作れて、再利用性が大きく、最適化性能が高い。

C++11 以降、痛い部分が改善されて、より良く自然に書けるようにもなった。

勉強しない人には、何かと敷居が高いのかもしれない・・・

ただ、C++ は、C である程度実装出来る人にとっては、「手っ取り早く、楽で魅力的」とは映らない面もある。
C++ のパワーを理解していないものと思う。

自分は、10年以上前に、C++ を「勉強しなくては」と心の底から思った出来事がある。

非常に複雑で、ボリュームが大きいプロジェクト内で、急遽、モニター用のプログラムを作る案件が生じたー

この案件に対して、C++師匠は、STL を駆使して、あっとゆーまに実装を終え、ちゃんと動くアプリを作った。
※自分がこの仕様を観た時は、これは、そこそこ大変だなーと思っていた・・

また、この師匠は、数ギガヘルツで動くようなCPUのアプリを、何でアセンブラに毛が生えたようなC言語でプログラムしないといけないの?
もっと相応しい作り方があるのでは?

と問うて来たー、この一件で、自分の C++ 敬遠傾向は、一蹴されたと思う。

それ以来、C++ を履修しなおし、現在に至る。

  • C++ の勉強会などに参加して、C++ に精通した人から、色々なレクチャーを受けた。
  • C++ は、正しい道を進まないで我流で進むと、とんでもない迷路にはまる事がある事例をいくつも聞かされた。
  • C++ の最適化で、人間が考えもしない独特の手法で、巧妙に行う事例をいくつも観た。
  • C++ になって大きなプログラムでも、メモリーリークは殆どしないようになった。
  • コンパイルが正常終了すると、大抵プログラムも思ったように動く事が多くなった。
  • テンプレートを少しは理解出来て、自分でもある程度作れるようになった。
  • 警告が出ない、綺麗なソースコードを書くようになった。
  • コードレビューをするようになった。
  • C++ のトレンドや、標準化委員会、boost などのレポートを読むようになった。

ただ、先は長い・・、自分の理解を超えるような考え方、概念がまだ沢山ある・・
まぁ、とりあえず、出来る範囲でゆっくり歩こうと思う。

ある人が言ってた、C++ を学ぶ最良の方法:

C++ をある程度出来る人にコードレビューしてもらう事。
間違った道から、正しい道に戻してもらう。


RL78 関係のフレームワークをメンテナンス

長い間、RX マイコンのフレームワークに集中していたので、RL78 のフレームワークには手を付けていなかった・・

しかし、最近、このフレームワークを使っている人が Qiita にコメントしていた。

使っている人がいるなら、RX マイコンのフレームワーク更新で改善されたエッセンスなどを RL78 にも反映したい。

そこで、それらの実装とテストを行った。

何故か、フラッシュが出来ない・・

Windows10 になって、RL78 の開発環境を作り直して、FIRST_sample を書こうと思ったが、エラーが出てフラッシュが出来ない。

CP2102N が悪いのかと思い、FTDI に変えてみたが、駄目で、エラーメッセージを頼りに、プロトコルのコードのパラメータを変えたり、色々したが駄目・・・

ルネサス・フラッシュ・プログラマーの最新版を導入して、それで書こうと思ったが、やはりうまく行かない・・・

※最新の RFP では、RL78 へのフラッシュ書き込みに、COM ポートで行う最小限のハードを使った場合をサポートしている。

ところが、FTDI で、再度やり直したら、今度は書き込みが正常に行われた・・・
※何が理由なのかさっぱり判らない(RFP のインストールが関係しているのかもしれない・・・)

改善した箇所

テンプレートクラスの実態を定義

以前の実装では、最適化しない(-O0)場合、テンプレート内スタティック変数の実態が無い為、リンクに失敗する。

テンプレートクラスでは、スタテック変数の実態を定義する必要があり、特殊な書き方が必要だ。
これは、書籍や C++ テンプレートの解説でも殆ど見ない方法だと思う。

※最適化を行うと、実態は必要無く(最適化すると、実態を経由しなくなる)問題にならない事が多い。

※デバッグが必要な場合、最適化は行わない。

namespace device {

    //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
    /*!
        @brief  インターバル・タイマー・クラス
    */
    //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
    template <class _>
    struct itm_t {

        //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
        /*!
            @brief  12ビット・インターバル・タイマ・コントロール・レジスタ(ITMC)
            @param[in]  T   アクセス・クラス
        */
        //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
        template <class T>
        struct itmc_t : public T {
            using T::operator =;
            using T::operator ();
            using T::operator |=;
            using T::operator &=;

            bit_rw_t<T, bitpos::B15>   RINTE;  ///< 12ビット・インターバル・タイマの動作制御
            bits_rw_t<T, bitpos::B0, 12> ITCMP;  ///< 12ビット・インターバル・タイマのコンペア値設定

        };
        typedef itmc_t< rw16_t<0xFFF90> > ITMC_;
        static ITMC_ ITMC;

...

    };
    // テンプレート内、スタティック定義、実態:
    template<class _> typename itm_t<_>::ITMC_ itm_t<_>::ITMC;

    typedef itm_t<void> itm;

↑のように実装する事でリンクエラーを回避出来る。

「itm_t」クラスは、疑似的にテンプレート化してある。
これは、ヘッダーのみで運用する場合、実態を定義するのに都合が良い。

PORT 定義クラスを更新

シングルポートを定義するテンプレートクラスを最新の物にした。

RX マイコンのフレームワークで得た、知見を組み込んである。

ただ、RL78 のポート関係レジスターは、構造的では無い構成なので、多少異なる構成となっている。

namespace {

    // 吸い込みなので、三番目のパラメーターを「false」とする。
    // LED::P = 1 で、実際のポートは、0になり、LED が点灯する。
    typedef device::PORT<device::port_no::P4, device::bitpos::B3, false> LED;

}

int main(int argc, char* argv[])
{
    utils::port::pullup_all();  ///< 安全の為、全ての入力をプルアップ

    LED::DIR = 1;  // 出力設定

    bool f = true;
    while(1) {
        utils::delay::milli_second(250);  ///< 0.25 秒毎の点滅
        LED::P = f;
        f = !f;
    }
}

PSG_sample を追加

ついでなので、先日実装した PSG エミュレーションで音楽演奏のプロジェクトを追加した。

とりあえず、思ったように鳴っているようだ。

VScode の設定を追加

VScode でソースを編集する場合に、インテリセンスを活用する為、設定ファイルを追加してある。

ただ、理由が不明で、赤線がでる場合がある・・・

これは、今後の課題とする・・・


まとめ

RL78 フレームワークを利用している方には、色々細かく修正したので、最新版を取得すると、コンパイルが通らないかもしれない。

多分、修正にはそんなに多くの苦労は無いものと思うが、新しい版を受け入れてもらいたい。

RXマイコンで PSG 音源をエミュレーション

PSG 音源を生成するテンプレートクラス

最近、R8C 関係の GitHub を整理している。
将来的に、初心者(小学生、中学生)向けの電子工作向けボードを作る予定で、それに向けたものだ。
1000円くらいで、電子工作とプログラミングの初級が学べて、何か作れるような物をリリースしたい。

そこで、R8C 用色々なコンテンツを作成している、その一環で PSG 音源を使った音楽再生を行ってみた。

詳しい記事は、Qiita にある。

とりあえず、R8C でそれなりに鳴ったので、RX マイコンにも移植してみた。

実験したのは、RX72N Envision Kit で、このデバイスには、SSIE 接続の D/A があるので、簡単だ。
サンプリング周波数は 48KHz 固定なので、多少オーバースペックかもしれない。

PSG とは?

PSG は、大昔に、矩形波を基本とする音源で、音楽を演奏するデバイスが売られていたのがルーツと思う。
AY-3-8910 とか、そんなデバイスが発売されて、Apple][ 用のボードで演奏したのを覚えている・・
※多分高校生くらいだったから、電子音楽の衝撃は今でも忘れない。
※今考えると楽譜を入力するオーサリングツールは素晴らしく良く出来ていた。

ファミコンでは、それに似た仕様の音源が内蔵されている。

C++ テンプレートの柔軟性

元は、R8C 用に実装したものだが、ほぼ無改造でそのまま再利用出来た。
※実際は、サンプリング周波数が高く、内部で演算がオーバーフローしたので、多少改修した。

    //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
    /*!
        @brief  PSG Manager テンプレート class
        @param[in]  SAMPLE  サンプリング周期
        @param[in]  TICK    演奏tick(通常100Hz)
        @param[in]  BSIZE   バッファサイズ(通常512)
        @param[in]  CNUM    チャネル数(通常4)
    */
    //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
    template <uint16_t SAMPLE, uint16_t TICK, uint16_t BSIZE, uint16_t CNUM>
    class psg_mng : public psg_base {

psg_mng テンプレートクラスのプロトタイプは上記のようになっていいて、

  • サンプリング周期
  • 演奏 TICK
  • バッファサイズ
  • チャネル数

を与えるようになっている。
※バッファサイズはこのモジュールとは直接関係無いので、除く方が良さそうだが、とりあえず、R8C 版との互換でそのままにしてある。

TICK を 100Hz、サンプリングを 48KHz とすると、480 バイトのデータ列を生成する必要があるので、バッファは 512 バイト。
※1サンプルづつ波形を引っ張るのなら、バッファサイズは含めなくて良い。

R8C では、メモリを節約する為、psg_mng で波形を生成したデータを、直接、PWM ストリームのバッファにしていたが、RX マイコンでは、構成が異なるので、一旦テンポラリに生成して、それを、サウンド出力クラスに食わすようにしている。

        {
            uint32_t n = SAMPLE / TICK;
            psg_mng_.set_wav_pos(0);
            psg_mng_.render(n);
            typename SOUND_OUT::WAVE t;
            for(uint32_t i = 0; i < n; ++i) {
                uint8_t w = psg_mng_.get_wav(i);
                w -= 0x80;
                t.l_ch = t.r_ch = (static_cast<int16_t>(w) << 8) | ((w & 0x7f) << 1);
                sound_out_.at_fifo().put(t);
            }

            if(delay > 0) {
                delay--;
            } else {
                psg_mng_.service();
            }
        }

SSIE と波形出力テンプレート定義

これは、オーディオを扱うアプリで散々利用してきたテンプレートなので、定義も使い方も、変わる処が無い。
この辺りの柔軟性は C++ で実装した場合の特典みたいなものだと思う。

#ifdef USE_DAC
    typedef sound::dac_stream<device::R12DA, device::TPU0, device::DMAC0, SOUND_OUT> DAC_STREAM;
    DAC_STREAM  dac_stream_(sound_out_);

    void start_audio_()
    {
        uint8_t dmac_intl = 4;
        uint8_t tpu_intl  = 5;
        if(dac_stream_.start(48'000, dmac_intl, tpu_intl)) {
            utils::format("Start D/A Stream\n");
        } else {
            utils::format("D/A Stream Not start...\n");
        }
    }
#endif

#ifdef USE_SSIE
    typedef device::ssie_io<device::SSIE1, device::DMAC1, SOUND_OUT> SSIE_IO;
    SSIE_IO     ssie_io_(sound_out_);

    void start_audio_()
    {
        {  // SSIE 設定 RX72N Envision kit では、I2S, 48KHz, 32/16 ビットフォーマット固定
            uint8_t intr = 5;
            uint32_t aclk = 24'576'000;
            uint32_t lrclk = 48'000;
            auto ret = ssie_io_.start(aclk, lrclk, SSIE_IO::BFORM::I2S_32, intr);
            if(ret) {
                ssie_io_.enable_mute(false);
                ssie_io_.enable_send();  // 送信開始
                utils::format("SSIE Start: AUDIO_CLK: %u Hz, LRCLK: %u\n") % aclk % lrclk;
            } else {
                utils::format("SSIE Not start...\n");
            }
        }
    }
#endif

SSIE インターフェース用と、内蔵 D/A 用の二種類がある。
※RX72T、RX66T、RX24T、など、D/A 内臓のデバイスでも簡単に流用出来ると思う。

他、オーディオ出力コンテキストにサンプリング周波数を設定する関数を出してある。

    void set_sample_rate(uint32_t freq)
    {
#ifdef USE_DAC
        dac_stream_.set_sample_rate(freq);
#endif
#ifdef USE_SSIE
        sound_out_.set_output_rate(freq);
#endif
    }

初期化

初期化では、TICK タイマー、オーディオ出力など初期化する。
また、サンプルで入れた楽曲のスコアテーブルを設定する。

    {
        uint8_t intr = 4;
        cmt_.start(TICK, intr);
    }

    start_audio_();

    {  // サンプリング周期設定
        set_sample_rate(SAMPLE);
    }

    psg_mng_.set_score(0, score0_);
    psg_mng_.set_score(1, score1_);

メインループ

後は、psg_mng テンプレートクラスで波形を生成して、オーディオ出力に渡すだけとなる。
その際、8ビットの波形を16ビットに拡張している。
又、モノラルなので、ステレオにしている。

    uint8_t cnt = 0;
    uint8_t delay = 200;
    while(1) {
        cmt_.sync();
        {
            uint32_t n = SAMPLE / TICK;
            psg_mng_.set_wav_pos(0);
            psg_mng_.render(n);
            typename SOUND_OUT::WAVE t;
            for(uint32_t i = 0; i < n; ++i) {
                uint8_t w = psg_mng_.get_wav(i);
                w -= 0x80;
                t.l_ch = t.r_ch = (static_cast<int16_t>(w) << 8) | ((w & 0x7f) << 1);
                sound_out_.at_fifo().put(t);
            }

            if(delay > 0) {
                delay--;
            } else {
                psg_mng_.service();
            }
        }

        ++cnt;
        if(cnt >= 50) {
            cnt = 0;
        }
        if(cnt < 25) {
            LED::P = 0;
        } else {
            LED::P = 1;
        }
    }

楽曲の定義

最近覚えた、便利な使い方:

C++11 になって、不便で不完全な「enum」から解放された。
「enum class」は、型に厳密で、異なった型を受け付けない、もちろん、整数型から派生しているので、「static_cast」を使って変換可能だ。

厳密になった為、色々な「型」を組み合わせるようなデータ列では不便となる。

そこで「union」を使って、利用出来る型を受け付けるようにしておき、コンストラクターで、異なる型からデータ列を生成するようにした。

        //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
        /*!
            @brief  スコア・コマンド構造 @n
                    ・KEY, len @n
                    ・TR, num @n
                    ・TEMPO, num @n
                    ・FOR, num
        */
        //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
        struct SCORE {
            union {
                KEY     key;
                CTRL    ctrl;
                uint8_t len;
            };
            constexpr SCORE(KEY k) noexcept : key(k) { }
            constexpr SCORE(CTRL c) noexcept : ctrl(c) { }
            constexpr SCORE(uint8_t l) noexcept : len(l) { }
        };

これで、スコアデータ列を構造的に作れるようになった。
※コンパイル時に値を決定して ROM 化するので「constexpr」を付加している。

この簡単な構造体で以下のように楽譜データを記述出来る。

    constexpr PSG::SCORE score0_[] = {
        PSG::CTRL::VOLUME, 128,
        PSG::CTRL::SQ50,
        PSG::CTRL::TEMPO, 80,
        PSG::CTRL::ATTACK, 175,
        // 1
        PSG::KEY::Q,   8,
        PSG::KEY::E_5, 8,
        PSG::KEY::D_5, 8,
        PSG::KEY::E_5, 8,
        PSG::KEY::C_5, 8,
        PSG::KEY::E_5, 8,
        PSG::KEY::B_4, 8,
        PSG::KEY::E_5, 8,

まとめ

やはり、C++ の柔軟性と、再利用性は優れている。

8/16 ビットマイコン用ソースコードが、RX マイコンでも、ほぼそのまま再利用出来た。

当然だが、R8C の PWM 再生に比べると、高品質だ。

YouTube:
https://www.youtube.com/watch?v=4ZHuMYcSQko

Makefile を共通化

Makefile 共通化

RX マイコンの各プロジェクトで、Makefile で共有出来る部分を抜き出して、共通化を行った。

今まで、同じような「手順」を各プロジェクト毎にコピーしていたが、二度手間だし、プロジェクト数が多く、作業が大変だった。
※Makefile を複数に別けたく無かったとゆーこだわりがあったのだが、現在のように手順がそこそこ複雑だと、そうする「こだわり」はあまり意味が無い。

共通部分

# -*- tab-width : 4 -*-
#=======================================================================
#   @file
#   @brief  RX microcontroller share Makefile
#   @author 平松邦仁 (hira@rvf-rc45.net)
#   @copyright  Copyright (C) 2021 Kunihito Hiramatsu @n
#               Released under the MIT license @n
#               https://github.com/hirakuni45/RX/blob/master/LICENSE
#=======================================================================

# System include path for each environment
ifeq ($(OS),Windows_NT)
SYSTEM := WIN
# C++.boost root
LOCAL_PATH  =   /mingw64
else
  UNAME := $(shell uname -s)
  ifeq ($(UNAME),Linux)
    SYSTEM := LINUX
    LOCAL_PATH = /usr/local
  endif
  ifeq ($(UNAME),Darwin)
    SYSTEM := OSX
    OSX_VER := $(shell sw_vers -productVersion | sed 's/^\([0-9]*.[0-9]*\).[0-9]*/\1/')
    LOCAL_PATH = /opt/local
  endif
endif

まず、これは、Windows、Linux、OS-X の環境を判断して、微妙な違いを吸収する。
※一番重要な事は、boost のパスを各環境で同じように扱う事。


LIB_ROOT    =   ../../rxlib/lib

INC_SYS     =   ../../rxlib/include $(LOCAL_PATH)/include

PROG_VERIFY = --verify
  • LIB_ROOT は、RX マイコン専用ライブラリのパスを設定している。
  • INC_SYS は、RX マイコン専用ライブラリのインクルードパスと、ローカルのインクルードパス(boost)などを設定している。
  • PROG_VERIFY は、rx_prog でフラッシュ書き込みする際に、「VERIFY」を行う場合のキーワードとなっている。
  • RX24T は、通常の手順で、VERIFY が行われない為、無効にする必要がある。

ifeq ($(RX_DEF),SIG_RX24T)
  RX_CPU = RX24T
  RX_OPT = v2
  LDSCRIPT  =   ../../RX24T/$(DEVICE).ld
  PROG_VERIFY =
endif

ifeq ($(RX_DEF),SIG_RX64M)
  RX_CPU = RX64M
  RX_OPT = v2
  LDSCRIPT  =   ../../RX64M/$(DEVICE).ld
endif

ifeq ($(RX_DEF),SIG_RX71M)
  AS_OPT    +=  --defsym MEMWAIT=1
  RX_CPU = RX71M
  RX_OPT = v2
  LDSCRIPT  =   ../../RX71M/$(DEVICE).ld
endif

ifeq ($(RX_DEF),SIG_RX65N)
  LIB_ROOT += ../../RX600/drw2d
  INC_APP +=  ../../RX600/drw2d/inc/tes
  USER_LIBS += drw2d
  RX_CPU = RX65N
  RX_OPT = v2
  LDSCRIPT  =   ../../RX65x/$(DEVICE).ld
endif

ifeq ($(RX_DEF),SIG_RX66T)
  RX_CPU = RX66T
  RX_OPT = v3
  LDSCRIPT  =   ../../RX66T/$(DEVICE).ld
endif

ifeq ($(RX_DEF),SIG_RX72T)
  RX_CPU = RX72T
  RX_OPT = v3
  LDSCRIPT  =   ../../RX72T/$(DEVICE).ld
endif

ifeq ($(RX_DEF),SIG_RX72N)
  LIB_ROOT += ../../RX600/drw2d
  INC_APP +=  ../../RX600/drw2d/inc/tes 
  USER_LIBS += drw2d
  RX_CPU = RX72N
  RX_OPT = v3
  LDSCRIPT  =   ../../RX72N/$(DEVICE).ld
endif

ifeq ($(RX_DEF),SIG_RX72M)
  LIB_ROOT += ../../RX600/drw2d
  INC_APP +=  ../../RX600/drw2d/inc/tes
  USER_LIBS += drw2d
  RX_CPU = RX72M
  RX_OPT = v3
  LDSCRIPT  =   ../../RX72M/$(DEVICE).ld
endif

この条件文で、RX マイコン毎に異なる設定を行っている。
現状では、「SIG_xxx」のキーワード毎に行っているが、将来的には、DEVICE で IC の型番を設定しているので、それに従って行うのが妥当だと思う。

※型番で行えば、ピン数の違いによる変更をソースコードへ伝達する事が出来る。

RX71M だけは、ちょっと特殊で、スーパーバイザーモードでしかアクセス出来ないレジスタがある。
そこで、そのレジスタを、start.s アセンブラブログラム内で行っている。「--defsym MEMWAIT=1」

FreeRTOS の場合、ユーザーモードに移行しない事が重要だが、それは、各プロジェクト毎に設定する。

# for FreeRTOS option
AS_OPT      =   --defsym NOT_USER=1

# Renesas GNU-RX gcc compiler version check
TARGET_ISA_TEXT := $(shell rx-elf-gcc --target-help | grep ISA)

# Renesas GNU-RX (8.3.0) compiler 
ifeq ($(TARGET_ISA_TEXT),)
  # for gcc-7.5.0 current gcc source build
  AS_DEFS       =   -mcpu=rx600
  CC_DEFS       =   -mcpu=rx600 -Wa,-mcpu=rxv2
  CP_DEFS       =   -mcpu=rx600
else # Renesas GNU-RX gcc 8.3.0
  AS_DEFS       =   -misa=$(RX_OPT)
  CC_DEFS       =   -misa=$(RX_OPT)
  CP_DEFS       =   -misa=$(RX_OPT)
endif

これは、Renesas GNU-RX gcc コンパイラと、プレーンな gcc コンパイラで、オプションが異なるので、それを自動判別する。


# You should not have to change anything below here.
AS          =   rx-elf-as
CC          =   rx-elf-gcc
CP          =   rx-elf-g++
AR          =   rx-elf-ar
LD          =   rx-elf-ld
OBJCOPY     =   rx-elf-objcopy
OBJDUMP     =   rx-elf-objdump
SIZE        =   rx-elf-size

AFLAGS      =   $(AS_OPT) $(AS_DEFS)
CFLAGS      =   -std=gnu99 $(CC_OPT) $(OPTIMIZE) $(CC_DEFS) $(DEFS)
PFLAGS      =   -std=c++17 $(CP_OPT) $(OPTIMIZE) $(CP_DEFS) $(DEFS)
FLAGS       = $(AS_OPT) $(AS_DEFS) $(CC_OPT) $(CP_OPT) $(OPTIMIZE) $(CC_DEFS) $(CP_DEFS) $(DEFS)

# FLAGS_CMP := $(shell cat $(TARGET).opt)

override LDFLAGS = $(MCU_TARGET) -nostartfiles -Wl,-Map,$(TARGET).map -T $(LDSCRIPT)

OBJCOPY_OPT =   --srec-forceS3 --srec-len 32

OBJECTS =   $(addprefix $(BUILD)/,$(patsubst %.s,%.o,$(ASOURCES))) \
            $(addprefix $(BUILD)/,$(patsubst %.c,%.o,$(CSOURCES))) \
            $(addprefix $(BUILD)/,$(patsubst %.cpp,%.o,$(PSOURCES)))

DOBJECTS =  $(addprefix $(BUILD)/,$(patsubst %.c,%.o,$(CSOURCES))) \
            $(addprefix $(BUILD)/,$(patsubst %.cpp,%.o,$(PSOURCES)))

DEPENDS =   $(patsubst %.o,%.d, $(DOBJECTS))

# all, clean: optional make command
.PHONY: all clean clean_depend run text
.SUFFIXES :
.SUFFIXES : .hpp .s .h .c .cpp .d .o

all: $(BUILD) $(TARGET).elf text

$(TARGET).elf: $(OBJECTS) $(LDSCRIPT) Makefile
    $(CC) $(LDFLAGS) $(LIBINCS) -o $@ $(OBJECTS) $(LIBS)
    $(SIZE) $@

$(BUILD)/%.o: %.s
    mkdir -p $(dir $@); \
    $(AS) -c $(AOPT) $(AFLAGS) $(AINCS) -o $@ $<

$(BUILD)/%.o : %.c
    mkdir -p $(dir $@); \
    $(CC) -c $(COPT) $(CFLAGS) $(CINCS) $(CCWARN) -o $@ $<

$(BUILD)/%.o : %.cpp
    mkdir -p $(dir $@); \
    $(CP) -c $(POPT) $(PFLAGS) $(PINCS) $(CPWARN) -o $@ $<

$(BUILD)/%.d: %.c
    mkdir -p $(dir $@); \
    $(CC) -MM -DDEPEND_ESCAPE $(COPT) $(CFLAGS) $(APPINCS) $< \
    | sed 's/$(notdir $*)\.o:/$(subst /,\/,$(patsubst %.d,%.o,$@) $@):/' > $@ ; \
    [ -s $@ ] || rm -f $@

$(BUILD)/%.d: %.cpp
    mkdir -p $(dir $@); \
    $(CP) -MM -DDEPEND_ESCAPE $(POPT) $(PFLAGS) $(APPINCS) $< \
    | sed 's/$(notdir $*)\.o:/$(subst /,\/,$(patsubst %.d,%.o,$@) $@):/' > $@ ; \
    [ -s $@ ] || rm -f $@

clean:
    rm -rf $(BUILD) $(TARGET).elf $(TARGET).mot $(TARGET).lst $(TARGET).map

clean_depend:
    rm -f $(DEPENDS)

これは、gcc の設定で、従属規則を自動生成する仕組みを内包する。


lst: $(TARGET).lst

%.lst: %.elf
    $(OBJDUMP) -h -S $< > $@

# Rules for building the .text rom images

text: mot lst

lst: $(TARGET).lst
mot: $(TARGET).mot
bin: $(TARGET).bin

%.lst: %.elf
    $(OBJDUMP) -h -S $< > $@

%.mot: %.elf
    $(OBJCOPY) $(OBJCOPY_OPT) -O srec $< $@

%.bin: %.elf
    $(OBJCOPY) -O binary $< $@

これは、リンカーで実行バイナリーを作成後、リストファイル、モトローラーファイル、バイナリーファイルを作成する手順だ。


# Serial Flash write 
run:
    $(MAKE)
    rx_prog -d $(RX_CPU) --progress --erase --write $(PROG_VERIFY) $(TARGET).mot

最後は、シリアル接続で、フラッシュ書き込みを行うツールの設定などになっている。

個別部分

上記のように、共通出来る部分を追い出したので、個別部分はシンプルとなった。

# -*- tab-width : 4 -*-
#=======================================================================
#   @file
#   @brief  RX72N Makefile
#   @author 平松邦仁 (hira@rvf-rc45.net)
#   @copyright  Copyright (C) 2020, 2021 Kunihito Hiramatsu @n
#               Released under the MIT license @n
#               https://github.com/hirakuni45/RX/blob/master/LICENSE
#=======================================================================
TARGET      =   raytracer_sample

DEVICE      =   R5F572NN

RX_DEF      =   SIG_RX72N

BUILD       =   release
# BUILD     =   debug

VPATH       =   ../../

ASOURCES    =   common/start.s

CSOURCES    =   common/init.c \
                common/vect.c \
                common/syscalls.c

PSOURCES    =   RAYTRACER_sample/main.cpp \
                graphics/font8x16.cpp \
                graphics/color.cpp \
                common/stdapi.cpp

USER_LIBS   =

USER_DEFS   =

INC_APP     =   . ../ ../../

AS_OPT      =

CP_OPT      =   -Wall -Werror \
                -Wno-unused-variable \
                -Wno-unused-function \
                -fno-exceptions

CC_OPT      =   -Wall -Werror \
                -Wno-unused-variable \
                -fno-exceptions

ifeq ($(BUILD),debug)
    CC_OPT += -g -DDEBUG
    CP_OPT += -g -DDEBUG
    OPTIMIZE = -O0
endif

ifeq ($(BUILD),release)
    CC_OPT += -DNDEBUG
    CP_OPT += -DNDEBUG
    OPTIMIZE = -O3
endif

-include ../../common/makefile

-include $(DEPENDS)

これは、RX72T の RAYTRACER_sample の Makefile で、基本、リンクするファイルの記述が殆どだ。

リリースビルドと、デバッグビルドの違いで切り替える部分は共通化しなかった。
最適化を変えたり、プロジェクト毎に微妙な違いを許容する事が出来るように配慮した。


まとめ

共通化する過程で、オプションなどを変更した場合に、フルコンパイルが自動で行えるように出来ないか検討したが、make を完全に理解していない為、思ったように作れなかった・・

これは今後の課題にしたいと思う。

RXマイコン、クロックプロファイルクラスの導入

クロック設定を見直す

C++ でRXマイコンフレームワークを作り始めた頃、あまり考えずに、適当に作った部分が、未だに「悪い」見本として残っている。

RXマイコンのクロックジェネレータは、意外と複雑で、面倒な設定を要するデバイスとなっており、柔軟性も必要なのに、かなり適当な創りとなっている。

そこで、これを見直して、もう少し「サッパリ」としたより良い物に変更する。
意外と広範囲な修正になる事から、今まで、見なかった事にしてきたものの、「痛い」部分は速いうちに処置した方が良いので、修正を行った。

以前の設定

以前は、「Makefile」と「main.cpp」の両方で微妙な設定を行っていた・・・

Makefile の設定:

USER_DEFS   =   SIG_RX71M \
                F_ICLK=240000000 \
                F_PCLKA=120000000 F_PCLKB=60000000 F_PCLKC=60000000 F_PCLKD=60000000 \
                F_FCLK=60000000 F_BCLK=120000000

main.cpp:

#if defined(SIG_RX71M)
    typedef device::system_io<12'000'000, 240'000'000> SYSTEM_IO;

...

int main(int argc, char** argv)
{
    SYSTEM_IO::setup_system_clock();

ベースクリスタルの周波数は、ソースコードに埋め込んであるのに、クロックジェネレータの分周器による周期は「Makefile」で環境変数で設定してある・・・

これは、初期の実験コードで色々やっていた時、テスト的に行ったものが、「標準」となってしまい、見直す事が先延ばしになり現在に至ったもの。
第三者を交えて、コードレビューを行えば、かなり早い段階で「ツッコミ」を入れられたと思うが、の機会が無く、そのままになっていた・・

改修後

改修後、クロック設定プロファイルクラスを新規に作り、そこに定数として設定してある。

#pragma once
//=====================================================================//
/*! @file
    @brief  RX71M グループ・クロック。プロファイル @n
            クロックジェネレータで発生させる周波数の定義
    @author 平松邦仁 (hira@rvf-rc45.net)
    @copyright  Copyright (C) 2021 Kunihito Hiramatsu @n
                Released under the MIT license @n
                https://github.com/hirakuni45/RX/blob/master/LICENSE
*/
//=====================================================================//
#include <cstdint>

namespace device {

    //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
    /*!
        @brief  クロック・プロファイル・クラス
    */
    //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
    class clock_profile {
    public:
        static const uint32_t   BASE        =  12'000'000;      ///< 外部接続クリスタル
        static const uint32_t   PLL_BASE    = 240'000'000;      ///< PLL ベースクロック

        static const uint32_t   ICLK        = 240'000'000;      ///< ICLK 周波数
        static const uint32_t   PCLKA       = 120'000'000;      ///< PCLKA 周波数
        static const uint32_t   PCLKB       =  60'000'000;      ///< PCLKB 周波数
        static const uint32_t   PCLKC       =  60'000'000;      ///< PCLKC 周波数
        static const uint32_t   PCLKD       =  60'000'000;      ///< PCLKD 周波数
        static const uint32_t   FCLK        =  60'000'000;      ///< FCLK 周波数
        static const uint32_t   BCLK        = 120'000'000;      ///< BCLK 周波数
    };
}

このソースは、各プラットホーム毎に切り替えて、他ソースから参照するようにしてある。

クリスタルが特殊な場合は、このファイルに追加して、環境変数で切り替えれば良いだろうと思う。

PLL_BASE の周波数は、BASE 周波数の倍率(0.5単位)で割り切れる必要がある。
当然、他の周期も、PLL_BASE からの分周比で割り切れる周波数を設定する必要がある。

また、mainの最初で、クロック周波数を切り替える関数名は、

typedef device::system_io<> SYSTEM_IO;

int main(int argc, char** argv)
{
    SYSTEM_IO::boost_master_clock();

とした。

※RX71M は、クロック設定の特定レジスタが、スーパーバイザモードでアクセスする必要があり、その部分を「start.s」で行っている為、「Makefile」にアセンブラに渡す変数がある。

AS_OPT      =   --defsym MEMWAIT=1

クロックジェネレーターの周波数を参照する場合、以下のように行える。

    auto iclk = device::clock_profile::ICLK / 1'000'000;
    utils::format("Start test for '%s' %d[MHz]\n") % system_str_ % iclk;

内臓高速発信器を利用する場合

typedef device::system_io<device::system_base::OSC_TYPE::HOCO> SYSTEM_IO;

内蔵高速発信器は、通常、16MHz、18MHz、20MHzがあり、「BASE」にどの周波数を使うか指示する。

まとめ

かなり広範囲な修正だったが、それだけの価値はあると思う。

RXマイコン、割り込み関係整理

RX72T で CAN の検証で気がついた・・

RX72T で CAN の動作確認をした際、思ったように動作しない・・
※以前に CAN のサンプルを作成する際、RX64M 1 台で行っており、複数台で互いに通信する確認はしていなかった・・

  • データ送信しても、データが送られない。
  • データの受信もしない。

この感じは、割り込みぽぃなぁーと思い、
割り込み関係を確認したら、単純に CAN 関係割り込みが設定されていないだけだった・・
※本来、割り込みが正しく設定されない場合、初期化時にエラーを返す必要がある・・

  • 標準割り込みは、ちゃんと実装していない事を思い出す。
  • 新規にペリフェラルのマネージャーを実装した時に、追加していた。
  • なので、デバイスによりバラバラで、統一性が無い・・・
  • ここらで、ちゃんと実装しておこうと思う。

RX64M では、CAN 関係は、選択型割り込みBなので、問題無かった。
RX66T/RX72T では、CAN の割り込みは通常ベクターなので、通常ベクターの登録関数に CAN 関係の割り込みを追加する必要がある。
通常割り込みで、厄介なのは、割り込みベクター番号から、IER、IPR などの割り込み設定関係へのアクセスでは規則性が無い場合がある。
その為、対応する通常ベクターを全て実装しておく必要がある・・・
※単純には、割り込みベクター番号から、IER、IPR レジスターを推定出来ない・・

また、IPR は、シェアされて共通になっている場合もある。


割り込み関係の整理

初期の実装では、割り込み設定は、ペリフェラル別に行っていた。
※初期の実装では、ペリフェラルの定義に、割り込みベクターを「定数」として含めていなかったので、個別に対応する必要があった。
現在の実装では、割り込みベクター型や番号は、ペリフェラルの定義を参照する事で得られるようになっている。

↓現在のSCIクラステンプレートの実装:

    //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
    /*!
        @brief  SCI 定義基底クラス
        @param[in]  base    ベース・アドレス
        @param[in]  per     ペリフェラル型
        @param[in]  txv     送信割り込みベクター
        @param[in]  rxv     受信割り込みベクター
        @param[in]  INT     送信終了割り込みベクター型
        @param[in]  tev     送信終了割り込みベクター
        @param[in]  pclk    PCLK 周波数
    */
    //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
    template <uint32_t base, peripheral per, ICU::VECTOR txv, ICU::VECTOR rxv,
        typename INT, INT tev, uint32_t pclk>
    struct sci_t {

        static const auto PERIPHERAL = per; ///< ペリフェラル型
        static const auto TX_VEC = txv;     ///< 受信割り込みベクター
        static const auto RX_VEC = rxv;     ///< 送信割り込みベクター
        static const auto TE_VEC = tev;     ///< 送信終了割り込みベクター
        static const uint32_t PCLK = pclk;  ///< PCLK 周波数

SCI などでは、割り込みを使う場合、受信と送信、同時に使う仕様なので、それでも良かった。
しかし、割り込みベクター別に設定を行うべきなので、その仕様を改めた。

        //-----------------------------------------------------------------//
        /*!
            @brief  割り込みレベルを設定する
            @param[in]  per 周辺機器タイプ
            @param[in]  lvl 割り込みレベル(0の場合、割り込み禁止)
            @return 成功なら「true」
        */
        //-----------------------------------------------------------------//
        static bool set_level(peripheral per, uint8_t lvl) noexcept
        {
            bool ena = lvl != 0 ? true : false;
            switch(per) {

            case peripheral::SCI1:
                ICU::IPR.RXI1 = lvl;
                ICU::IER.RXI1 = ena;
                ICU::IPR.TXI1 = lvl;
                ICU::IER.TXI1 = ena;
                break;
            case peripheral::SCI5:
                ICU::IPR.RXI5 = lvl;
                ICU::IER.RXI5 = ena;
                ICU::IPR.TXI5 = lvl;
                ICU::IER.TXI5 = ena;
                break;

↑以前の実装では、ペリフェラル毎に割り込みを設定していた・・(今後、廃止する予定)

↓今後、割り込みベクター毎に登録する・・

        //-----------------------------------------------------------------//
        /*!
            @brief  割り込みレベルを設定する
            @param[in]  vec 割り込み要因
            @param[in]  lvl 割り込みレベル(0の場合、割り込み禁止)
            @return 成功なら「true」
        */
        //-----------------------------------------------------------------//
        static bool set_level(ICU::VECTOR vec, uint8_t lvl) noexcept
        {
            bool ena = lvl != 0 ? true : false;
            switch(vec) {

            case ICU::VECTOR::RXI1:
                ICU::IER.RXI1 = 0;
                ICU::IPR.RXI1 = lvl;
                ICU::IER.RXI1 = ena;
                break;
            case ICU::VECTOR::TXI1:
                ICU::IER.TXI1 = 0;
                ICU::IPR.TXI1 = lvl;
                ICU::IER.TXI1 = ena;
                break;

かなり、大掛かりな修正なので、時間がかかりそう・・・


作業実績

RX24T RX64M RX71M RX65N RX72N RX66T RX72T
icu.hpp O O O O O O O
icu_mgr.hpp O O O O O O O

all_compile (Debug)

Pass.

all_compile (Release)

Pass.

追記 (2021-05-11 08:12:55 Tuesday)

  • ハードウェアーマニュアルを良く読むと、IR レジスタ、IER レジスタは、割り込み要因の番号と一致するようだ。
  • IPR レジスタに関しては、割り込み要因番号が32以下の場合と、特定のレジスタ(RX24T)の場合に、例外的なアクセスを行えば良いらしい。

そこで、IPR クラスの [] オペレーターによるアクセスを少々工夫する事で、余分なコードを削減する事が出来た。

        //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
        /*!
            @brief  IPR レジスタ @n
                    全て、下位4ビットが有効
        */
        //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
        template <uint32_t base>
        struct ipr_t {

...

            //-------------------------------------------------------------//
            /*!
                @brief  []オペレータ
                @param[in]  vec     標準割り込みベクター型
                @return IPR レジスターの参照
            */
            //-------------------------------------------------------------//
            volatile uint8_t& operator [] (VECTOR vec) noexcept {
                uint32_t idx = 0;
                switch(vec) {
                case VECTOR::BUSERR: idx = 0; break;
                case VECTOR::RAMERR: idx = 0; break;
                case VECTOR::FIFERR: idx = 1; break;
                case VECTOR::FRDYI:  idx = 2; break;
                case VECTOR::SWINT2: idx = 3; break;
                case VECTOR::SWINT:  idx = 3; break;
                case VECTOR::CMI0:   idx = 4; break;
                case VECTOR::CMI1:   idx = 5; break;
                case VECTOR::CMWI0:  idx = 6; break;
                case VECTOR::CMWI1:  idx = 7; break;

                default: idx = static_cast<uint32_t>(vec); break;
                }
                return *reinterpret_cast<volatile uint8_t*>(base + idx);
            }
        };
        typedef ipr_t<0x00087300> IPR_;
        static IPR_ IPR;

icu_mgr クラスの「set_level()」関数では、以下のようにシンプルとなった。

        //-----------------------------------------------------------------//
        /*!
            @brief  割り込みレベルを設定する
            @param[in]  vec 通常割り込みベクター型
            @param[in]  lvl 割り込みレベル(0の場合、割り込み禁止)
        */
        //-----------------------------------------------------------------//
        static void set_level(ICU::VECTOR vec, uint8_t lvl) noexcept
        {
            bool ena = lvl != 0 ? true : false;
            ICU::IER.enable(vec, 0);
            ICU::IPR[vec] = lvl;
            ICU::IER.enable(vec, ena);
        }

RX72N Envision Kit での開発(その7)GPTW を使う

GPTW とは?

  • RX66T、RX72T、RX72N には、汎用 PWM 機能(GPTW)が備わっています。
  • 一般に PWM 生成は MTU を使いますが、GPTW はさらに細かい設定が可能で、MTU の拡張版的な扱いのようです。
  • カウンタは32ビットになっており、高いクロックでの駆動が可能なようになっています。
  • RX66T、RX72T は GPTW は 10 チャネル、RX72N には 4 チャネルあります。
  • RX66T、RX72T はより高いクロック(PCLKC、最大160MHz、200MHz)を分周器のクロックとして使えます。
  • RX72N は(PCLKA、最大120MHz)を分周器のクロックとして利用します。
  • 「汎用」とありますが、かなり細かい設定が可能で、主に FET や IGBT などのパワーデバイスの制御に向いています。
  • RX66T、RX72T には、さらに、高分解能波形成型器を通す事で、より細かい PWM 波形を生成できます。
  • インプットキャプチャーや、位相入力(エンコーダー入力)などにも使えます。
  • A/D コンバーターを同期して動かす事が出来るので、正確な電圧、電流の検出が行えます。

GPTW 用ポートの設定クラスを作る。

  • 最近の RX マイコンは、ポートのアサインを行う候補が増えて、より柔軟性が増したと思えます。

  • そこで、「port_map_gptw」クラスを新規に追加して、専用クラスを用意しました。

  • 候補のポリシーは、ハードウェアーマニュアルにある「MPC」にある説明に沿った物にしてあります。

  • 自分のフレームワークでは、別プログラムで設定を生成しないので、判りやすさと柔軟性を与える為、「候補」(ORDER)型を使い、設定します。

  • ポートマッピングオーダー型

        //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
        /*!
            @brief  ポート・マッピング・オーダー型
        */
        //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
        enum class ORDER : uint8_t {
            BYPASS,     ///< ポートマップの設定をバイパスする場合
            FIRST,      ///< 第1候補
            SECOND,     ///< 第2候補
            THIRD,      ///< 第3候補
            FOURTH,     ///< 第4候補
            FIFTH,      ///< 第5候補
            SIXTH,      ///< 第6候補
            SEVENTH,    ///< 第7候補
        };
  • GPTW0 A チャネルのポート候補
        static bool gptw0_(CHANNEL ch, bool ena, ORDER opt) noexcept
        {
            bool ret = true;
            uint8_t sel = ena ? 0b011110 : 0;
            switch(ch) {
            /// GTIOC0A (入出力)
            ///       224 176 144 100
            /// P23     ○   ○   ○   ○
            /// P83     ○   ○   ○   ×
            /// PA5     ○   ○   ○   ○
            /// PD3     ○   ○   ○   ○
            /// PE5     ○   ○   ○   ○
            /// PH6     ○   ×   ×   ×
            case CHANNEL::A:
                switch(opt) {
                case ORDER::FIRST:
                    PORT2::PMR.B3 = 0;
                    MPC::P23PFS.PSEL = sel;
                    PORT2::PMR.B3 = ena;
                    break;
                case ORDER::SECOND:
                    PORT8::PMR.B3 = 0;
                    MPC::P83PFS.PSEL = sel;
                    PORT8::PMR.B3 = ena;
                    break;
                case ORDER::THIRD:
                    PORTA::PMR.B5 = 0;
                    MPC::PA5PFS.PSEL = sel;
                    PORTA::PMR.B5 = ena;
                    break;
                case ORDER::FOURTH:
                    PORTD::PMR.B3 = 0;
                    MPC::PD3PFS.PSEL = sel;
                    PORTD::PMR.B3 = ena;
                    break;
                case ORDER::FIFTH:
                    PORTE::PMR.B5 = 0;
                    MPC::PE5PFS.PSEL = sel;
                    PORTE::PMR.B5 = ena;
                    break;
                case ORDER::SIXTH:
                    PORTH::PMR.B6 = 0;
                    MPC::PH6PFS.PSEL = sel;
                    PORTH::PMR.B6 = ena;
                    break;
                default:
                    ret = false;
                    break;
                }
                break;

制御クラス(gptw_mgr)

  • MTU のマネージャークラスと同じような構成にして、「gptw_mgr」クラスを実装しました。
  • 現状では、PWM 波形を出力するだけですが、テンプレートを使い設定を参照する事で、複数のマイコンでもシンプルな構成にする事が出来ます。

動作モード

  • 動作モードとして、以下の型があります。(今後拡張予定)
  • 現状、のこぎり波以外は実装されていません。
        //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
        /*!
            @brief  動作モード型
        */
        //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
        enum class MODE : uint8_t {
            PWM_S_HL,       ///< のこぎり波 PWM (AB: H --> L) 
            PWM_S_LH,       ///< のこぎり波 PWM (AB: L --> H)
            PWM_S_HL_LH,    ///< のこぎり波 PWM (A: H --> L, B: L --> H)
            PWM_S_LH_HL,    ///< のこぎり波 PWM (A: L --> H, B: H --> L)
            SINGLE,         ///< ワンショット・パルス
            PWM_T1,         ///< 三角波 PWM 1(谷32ビット転送)
            PWM_T2,         ///< 三角波 PWM 2(山/谷32ビット転送)
            PWM_T3,         ///< 三角波 PWM 3(谷64ビット転送)
        };

出力制御と型

  • 出力制御では、以下の型のどれかを設定出来ます。
  • RX66T、RX72T では「反転出力」を指定出来ます。
        //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
        /*!
            @brief  出力型 @n
                    ※反転出力は、RX66T、RX72T の場合にのみ有効
        */
        //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
        enum class  OUTPUT : uint8_t {
            NONE,   ///< 無効

            A,      ///< A を利用
            B,      ///< B を利用
            AB,     ///< AB

            NA,     ///< 反転 A を利用
            NB,     ///< 反転 B を利用
            NA_B,   ///< 反転 A, 正 B を利用
            A_NB,   ///< 正 A, 反転 B を利用
            NA_NB,  ///< 反転 A, 反転 B を利用
        };

gptw_mgr テンプレートクラス

  • gptw_mgr テンプレートのプロトタイプは以下のようになっています。
  • パラメータとして、GPTW のチャネル型、割り込み時に起動させる事が可能なファンクタクラスを指定します。
    //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
    /*!
        @brief  GPTW マネージャー・クラス
        @param[in]  GPTWn   GPTW[n] ユニット
        @param[in]  CMTASK  コンペアマッチタスク型
    */
    //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
    template <class GPTWn, class CMTASK = utils::null_task>
        class gptw_mgr : public gptw_base {

...

    };
  • gptw_mgr の宣言では、以下のようにします。(RX72N GPTW1 を使った場合)
  • RX72N Envision Kit では、PMod コネクタ CN6 に (7)PD0、(8)PD1 がアサインされており、GPTW1 の出力をマッピング出来ます。
    /// PMOD Connector: PD0(CN6-7), PD1(CN6-8)
    typedef device::GPTW1 GPTW_CH;
    const auto ORDER_A = device::port_map_gptw::ORDER::FOURTH;
    const auto ORDER_B = device::port_map_gptw::ORDER::FOURTH;

    typedef device::gptw_mgr<GPTW_CH, utils::null_task> GPTW;
    GPTW    gptw_;

GPTW の開始

  • プロトタイプは以下のようになっています。
  • 周期は、整数値で周波数を指定します。
  • RX72T、RX72N ではベースクロックが異なりますが、その定義は、GPTx インスタンスで指定されている為、自動で最適な値を計算します。
  • ポート候補で、A、B 出力の「候補」を指定します。
  • 「バッファー動作」は、DUTY 設定をバッファリングする事で、DUTYを変更した場合にノイズが出ません。
  • 通常、PWM 周期とは非同期に DUTY を変更すると思うので、標準でバッファー動作になっています。
  • 設定に反故があると「false」を返して失敗します。
        //-----------------------------------------------------------------//
        /*!
            @brief  開始
            @param[in]  mode    動作モード
            @param[in]  out     出力型
            @param[in]  freq    周期
            @param[in]  ord_a   ポート候補A
            @param[in]  ord_b   ポート候補B
            @param[in]  ilvl    割り込みレベル(0 なら割り込み無し)
            @param[in]  buffer  バッファー動作を無効にする場合「false」
            @return 設定が適正なら「true」
        */
        //-----------------------------------------------------------------//
        bool start(MODE mode, OUTPUT out, uint32_t freq, typename port_map_gptw::ORDER ord_a, typename port_map_gptw::ORDER ord_b,
            uint8_t ilvl = 0, bool buffer = true) noexcept

  • GPTW の開始では、動作モード、出力ポート候補、PWM 周波数、などを指定します。
  • モードは、PWM_S_HL(初期'H' で 'L' になる)
  • 出力は AB
  • 周期は 100KHz
  • 初期状態で、33%、66%のデューティー幅のパルスを出力します。
    {  // GPTW の開始 (PWM / AB 出力)
        uint32_t freq = 100'000;  // 100KHz
        if(gptw_.start(GPTW::MODE::PWM_S_HL, GPTW::OUTPUT::AB, freq, ORDER_A, ORDER_B)) {
            utils::format("GPTW%d start: freq: %u\n") % GPTW::value_type::CHANNEL_NO % freq;
            duty_a_ = 0.33f;
            gptw_.set_duty_a(duty_a_);
            duty_b_ = 0.66f;
            gptw_.set_duty_b(duty_b_);
        } else {
            utils::format("GPTW%d start fail...\n") % GPTW::value_type::CHANNEL_NO;
        }
    }

DUTY の変更

  • サンプルでは、ターミナルを接続して、コマンド入力で、A、B チャネルの DUTY を指定できるようにしてあります。
  • a duty(0 to 1.0)
  • b duty(0 to 1.0)
  • help
Start GPTW sample for 'RX72N Envision Kit' 240[MHz]
SCI PCLK: 60000000
SCI Baud rate (set):  115200
SCI Baud rate (real): 115384 (0.16 [%])
CMT rate (set):  100 [Hz]
CMT rate (real): 100 [Hz] (0.00 [%])
GPTW1 start: freq: 100000
# a
A: duty: 0.330
# b
B: duty: 0.660
# b 0.5
#

まとめ

  • PWM 波形の出力は、意外と複雑なので、マネージャークラスの介入が欠かせません。
  • 出来る範囲で、柔軟な設定が可能なように工夫してありますが、十分ではありません。
  • 足りない設定は、gptw_mgr クラスの構成に習って、改修すれば良いと思います。
  • ソースコードはコメントも多く、動作の概要を掴みやすいように実装されています。
  • 詳細な解説が無くてもソースコードやサンプルコードを少し眺めれば理解できるものと思います。
  • gpt_mgr クラスは、単一のソースなので、他のコードを余り意識しなくても構造が判ると思います。
  • 他に「RX600/gptw.hpp」、「RX72N/port_map_gptw.hpp」を参照する必要があるかもしれません。
  • 「レジスター名」はハードウェアーマニュアルと同一にしてあるので、何かの機能を追加する場合に実装しやすいと思います。

GPTW_sample

RX72Tを動かしてみる

RX72T

以前、RX72M 発表の際、デバイス単体で購入すべく、色々探して、最短で入手できる(マウサー)処から購入した。
その時、RX72T も販売していたので割高だったけど「ついでに」購入していた。

144ピンタイプで、変換基板が手元に無く、動かせていなかったが、変換基板を入手したので、動かしてみた。

RX72T は最大 200MHz で動作し、標準で USB を内蔵しており、エアコンなど家電向けのデバイスとなっている。
自分が買った時は、1500 円くらいだったと思うが、現在は 1000 円くらい( 100 ピンタイプ)で入手出来るようだ、RX66T と余り変わらない・・
※ RX66T は入手性が悪い。

チップワンストップ(RX72T)

基本的なスペック:

  • 3.3V~5V 動作
  • RXv3 コア
  • 最大 200MHz 動作
  • ROM (512K/1024K)
  • RAM 128KB
  • データフラッシュ 32KB
  • ECC 付 RAM 16KB
  • USB 内臓

※RXv3 コアだけど、DFPU はサポートしていない。


基本的なピン接続

最低限必要なピンだけ配線して、動作させた。
※自分はシリアル接続を基本としているので、SCI ブートモードを利用する。

ピン番は144ピンタイプのデバイスなので注意

ピン名 ピン番 通常動作 ブート時
MD/FINED 11 PU(1) PD(0)
P00/UB 9 PD(0) PD(0)
PD5/RXD1 25 TXD TXD
PD3/TXD1 27 RXD RXD
EMLE 7 PD(0) PD(0)
/RES 15 PU(1) PU(1)
P37/XTAL 16 16MHz 16MHz
P36/EXTAL 18 16MHz 16MHz
VCL 10 0.47uF 0.47uF

PD: プルダウン (4.7K)
PU: プルアップ (4.7K)
Vcc、Vss を全て接続して、バイパスコンデンサ(0.1uF)を接続する。
AVcc、AVss も同様に接続。
※ A/D変換で SN を上げる為には、アナログ系の電源に工夫をする必要がある。

  • 動作レベル設定では、直で Vcc、Vss に接続しない事、必ず適当な抵抗を介して接続する。(入出力の場合がある)
  • VCL は 0.47uF のセラミックコンデンサで Vss に接続。
  • クリスタルは 16MHz を選んだ。(共振コンデンサは、8pF)
  • 「/RES」にはリセット SW を設ける。
  • USB ブートの場合は、「P00/UB」端子を「High」とする。

詳しくは、「RX72Tグループ ユーザーズマニュアル ハードウェア編」、「45. フラッシュメモリ」、「45.7.1 ブートモード (SCI インタフェース )」を参照


RX72T 対応サンプルコード

USER_DEFS   =   SIG_RX72T \
                F_ICLK=192000000 \
                F_PCLKA=96000000 F_PCLKB=48000000 F_PCLKC=192000000 F_PCLKD=48000000 \
                F_FCLK=48000000 F_BCLK=48000000

RX72N 対応の時、RX72T も大体対応していたと思うので、FIRST_sample は普通に動作した。
※RX72T は、RX72N より、RX66T に仕様が近い。

#elif defined(SIG_RX72T)
    static const char* system_str_ = { "RX72T" };
    typedef device::system_io<16'000'000, 192'000'000> SYSTEM_IO;
    typedef device::PORT<device::PORT0, device::bitpos::B1> LED;
    typedef device::SCI1 SCI_CH;

FIRST_sample では、LED は P00 に接続するのが通例だったが、P00 は USB ブート時のサイン入力なので避け、P01 にしてある。

クリスタルは、USB 使用時は 192MHz 動作が必要で、USB を使わない最大速度 200MHz も可能なように 16MHz を選択した。

ソフトウェアーループの遅延を調整した。

    static void micro_second(uint32_t us)
        {
            while(us > 0) {

...

#elif defined(SIG_RX72T)
                // 192MHz: 250KHz: (63) 3008239 -> 253.304KHz
                // 192MHz: 250KHz: (64) 3000000 -> 249.357KHz
                for(uint32_t n = 0; n < (F_ICLK / 3000000); ++n) {
                    asm("nop");
                }

...

SCI_sample を試したら、上手く通信出来ない・・
調べると、通信速度が半分になっていた。
最初、クロックデバイダの不具合なのかと思い、system_io クラスを調べたが問題無い。

原因は、sci_io クラスで、ボーレートクロックを微調整するパラメーターの問題だった。
「微調整機構 (MDDR)」では、全体のボーレートを、n/256 で微調整する。
誤差が、1/256 以下の場合(誤差 0.39% 以下)の場合、微調整をバイパスする必要がある。

if(mddr >= 128) brme = true;

mddr は、誤差が0の場合、256 が来る、それで、MDDR には「0」が設定されてしまう・・・

以下のように修正した。

if(mddr >= 128 && mddr < 256) brme = true;

結構、実装には自信があったクラスだけに多少ショックを受けている・・、まだまだだなーと思う瞬間だった・・

ついでに、ボーレートクロックの精度を高めるようなコードを追加した。


現状でサポートして動作確認したサンプルは以下のようになっている。

  • FIRST_sample
  • SCI_sample
  • RAYTRACER_sample
  • CALC_sample

フラッシュ書き込みプログラム「rx_prog」は対応済みで、問題無く書き込めた。

まとめ

RX72T はチップ単体の価格が1000円くらいでありながら、極めて高性能で、それなりに RAM もあるので、小物を作る際には重宝しそうなデバイスだと思う。

5V でも動作して、200MHz で動くのは、それなりにメリットがあるものと思う。
USB も標準で持っているので、PC に接続するようなデバイスを作成する場合にも便利そうだー

今後、サンプルコード対応をしていく。

RX72N Envision Kit での開発(その6)FMシンセサイザー 編


FM シンセサイザー用 GUI を作る

以前に、DX7 FM シンセサイザー用オープンソースをポーティングしました。

当初は、スタンダード MIDI ファイルの演奏を考えていましたが、パースするのが意外と大変そうなので、ストールしていました。

その後、MIDI 演奏は、とりあえず後回しにして、鍵盤を作り、音色を変更する GUI を実装しました。

当初、鍵盤の GUI も、widget_director クラスで管理できるものと思いましたが、鍵盤の場合、同時押しなど、通常の GUI とは異なるケアを行う必要があります、現在の widget クラスを改修するのは、かなり複雑になると考え、専用のクラスを実装しました。

また、タッチパネル(FT5206)サービスクラスでは、同時二点押しまでしかサービスしていませんでしたが、FT5206 の最大数4点までのサービスに修正しました。
※和音を出すのに3点以上が必要です。

DX7 の音色ファイルのロードと切り替え

FM シンセサイザーのソースには、音色は1つだけしか設定されていません。

実機は最大32色まで変更が可能なバンクがあります。

音色ファイルは、「DX7_0628.SYX」というもので、ネットで見つけました。
※たったの4Kバイト程度です。

このアプリでは、SD カードのルートに「DX7_0628.SYX」に置いておくと、起動して3秒くらいしてからロードします。
※SDカードのマウントに遅延がある為

使える音色は以下のものです:

 0: 'PIANO 1   '
 1: 'FM PIANO A'
 2: 'PIANO 1   '
 3: 'HARD ROADS'
 4: 'PIANOBELL2'
 5: 'T 23      '
 6: 'RHODES-CHO'
 7: 'PIPES    A'
 8: 'PIPES   2 '
 9: 'ROADSFLUTE'
10: 'ROADFLUTE2'
11: 'PLUCKEDRUM'
12: 'LO/HI STR2'
13: 'OBEHIND   '
14: 'ANLGBRASS '
15: 'FATSYNTH A'
16: 'JL PONTY 1'
17: 'Strings #2'
18: 'Orchestra '
19: 'PLUCKIN'  '
20: 'OOH AHH EE'
21: 'T 22      '
22: 'BassFlute2'
23: 'BassFlute3'
24: 'TIGHTPIANO'
25: 'BASS FLUTE'
26: 'PAUL STRGS'
27: 'BASS/PIA.1'
28: '2 OR MORE '
29: 'TIMPANI  2'
30: 'MALE CHOIR'
31: 'F.CHORUS 2'

シンセサイザークラスに音色をセットするのは、内部的には、MIDI データとして食わせるようです。
※ロードしたデータをそのままMIDIのストリームに食わせるだけです。

    bool read_synth_color_(const char* filename) noexcept
    {
        utils::file_io fin;
        if(fin.open(filename, "rb")) {
            uint8_t tmp[4096 + 8];
            if(fin.read(tmp, sizeof(tmp)) == sizeof(tmp)) {
                ring_buffer_.Write(tmp, sizeof(tmp));
                {  // データを処理させる為、エンジンを動かす。
                    const uint32_t len = SYNTH_SAMPLE_RATE / 60;
                    int16_t tmp[len];
                    synth_unit_.GetSamples(len, tmp);
                }
                for(int i = 0; i < 32; ++i) {
                    char tmp[12];
                    synth_unit_.get_patch_name(i, tmp, 12 - 1);
                    tmp[11] = 0;
                    utils::sformat(" %2d: %s", &synth_color_name_[i * 16], 16) % (i + 1) % tmp;
                }
            }
            return true;
        } else {
            return false;
        }
    }

マルチタッチと発音

RX72N Envision Kit のタッチパネルは、静電容量タイプで、4点までのマルチタッチが可能なタイプです。

音の強弱は難しいとしても、キーボード(ピアノ)のようなインターフェースには向いています。

    void service_note_() noexcept
    {
        for(int i = 0; i < 21; ++i) {
            const auto& key = synth_gui_.get_keyboard().get(static_cast<SYNTH_GUI::KEYBOARD::key>(i));
            if(key.positive_) {
                uint8_t tmp[3];
                tmp[0] = 0x90;
                tmp[1] = 0x3C + i;
                tmp[2] = 0x7f;
                ring_buffer_.Write(tmp, 3);
            }
            if(key.negative_) {
                uint8_t tmp[3];
                tmp[0] = 0x80;
                tmp[1] = 0x3C + i;
                tmp[2] = 0x7f;
                ring_buffer_.Write(tmp, 3);
            }
        }
    }

GUI キーボードで得た、押した状態、離した状態で、それぞれ、MIDI データを作成して、食わせると音が鳴ります。
押している間、音が連続して鳴ります。

また、マルチタッチでは「和音」が鳴らせる事が大きいです。

流石に、このサイズ(1.5オクターブ程)では、簡単な曲しか演奏できませんが、ガジェットとして楽しいものです。


GUI を作成する要点

widget_director クラスでは、現在は、widget の配置ツールなどは無く、プログラムコードによって widget の配置などを行う必要があります。

例えば、電卓のような物なら、グリッド状にボタンが配置されているので、配置ツールが無くても簡単です。


今回は、音色を変更する為、二つのスイッチと、音色名を表示するボックスを作成しました。

このような場合、座標は、比較的簡単な計算で求められるので、定数を定義して、計算式で widget のリソースを生成しています。

とりあえず、ルールとして:

  • 中央の上部に配置する
  • 左右に音色を変更するボタンを配置する
  • 中央に音色名を表示する
  • オクターブボタンは、画面の端に表示する
  • オクターブ領域表示は、中央に表示する
        static const int16_t SC_NAME_LEN = 16;   ///< With EOT
        static const int16_t SC_NUM = 32;   ///< 音色最大数
        static const int16_t OCT_NUM = 5;   ///< オクターブ域

...

        static const int16_t SC_LOC = 10;   ///< ボタン関係、縦の位置
        static const int16_t SC_SPC = 10;   ///< ボタンとの隙間
        static const int16_t CENTER = 480/2;   ///< X 中心
        static const int16_t SC_BTN_SZ = 30;   ///< ボタンサイズ
        static const int16_t SC_TEX_W = 8 * SC_NAME_LEN;  ///< テキスト横幅
        static const int16_t SC_TEX_H = 24;       ///< テキスト高さ

        static const int16_t OCT_LOC = 40;   ///< オクターブ関係、縦の位置
        static const int16_t OCT_AREA_W = 300;
        static const int16_t OCT_AREA_H = 30;
        static const int16_t OCT_BTN_SZ = 50;   ///< ボタンサイズ

...

        typedef gui::widget WIDGET;
        typedef gui::button BUTTON;
        typedef gui::text TEXT;
        typedef gui::slider SLIDER;
        BUTTON      sc_idx_m_;
        TEXT        sc_name_;
        BUTTON      sc_idx_p_;

        BUTTON      octave_m_;
        SLIDER      octave_d_;
        BUTTON      octave_p_;

...

        synth_gui(RENDER& render, TOUCH& touch) noexcept :
            render_(render), touch_(touch), widd_(render, touch),
            keyboard_(render, touch),
            sc_idx_m_(vtx::srect(CENTER-SC_TEX_W/2-SC_BTN_SZ-SC_SPC, SC_LOC, SC_BTN_SZ, SC_BTN_SZ), "<"),
            sc_name_ (vtx::srect(CENTER-SC_TEX_W/2, SC_LOC+(SC_BTN_SZ-SC_TEX_H)/2, SC_TEX_W, SC_TEX_H),  ""),
            sc_idx_p_(vtx::srect(CENTER+SC_TEX_W/2+SC_SPC,        SC_LOC, SC_BTN_SZ, SC_BTN_SZ), ">"),
            octave_m_(vtx::srect(0,              OCT_LOC, OCT_BTN_SZ, OCT_BTN_SZ), "<<"),
            octave_d_(vtx::srect(CENTER-OCT_AREA_W/2, OCT_LOC+(OCT_BTN_SZ-OCT_AREA_H)/2, OCT_AREA_W, OCT_AREA_H)),
            octave_p_(vtx::srect(480-OCT_BTN_SZ, OCT_LOC, OCT_BTN_SZ, OCT_BTN_SZ), ">>"),
            sc_idx_(0), sc_idx_before_(0), sc_name_org_(nullptr),
            oct_idx_(2)
        { }

BUTTON、TEXT、SLIDER クラスは、コンストラクターで座標を計算して設定しています。

このように定数を設けて、それを起点に座標を生成すると、簡単なルールで、座標の設定を自動化出来て便利です。
ボタンの大きさや隙間など、パラメーターとしているので、簡単に変更出来ます。
計算で行うと、計算式が正しければ、理にかなった正しい表示が行えます。

C++17 では、より複雑なルールや条件分岐など、プログラム的な物を、constexpr を使って定義する事も出来ます。
※ constexpr については、ググッて下さい。
constexpr はコンパイル時に計算されるので、どんなに複雑でも、実機の処理負荷には影響を与えません。

まとめ

マルチタッチを有効に活用するアプリとして、「鍵盤」は、最適なアプリと思えます。

今後、MIDI ファイルの演奏など、アプリを充実させていきたいと思います。

SYNTH_sample


追記

Arduino 環境用に、スタンダード MIDI のプレイヤー(パーサー)があったので、ポーティングしてみました。
多少、改造しましたが、想定の範囲で演奏出来るようです。
ソースコードは、コミットしてあります。
スタンダード MIDI ファイルはネットにあるものが大体使えるようですが、演奏出来ないファイルもあり、その点は調査中です。

「@」ボタンを押すと、ファイラーが開くので、MIDIファイルを選択すれば演奏が始まります。

演奏中、音色を変更する事も出来ます。

RXマイコン、デジタルストレージオシロスコープ(その3)



アナログフロントエンドの実験

最終的には基板を作る予定だが、事前に実験を行い、定数を決めたりしなければならない。

とりあえず、入力アンプ、レベルシフター、カップリングと DC 結合などを実験してみた。

LTSpice でも、多少シュミレーションをしている。

とりあえず、手元にあったオーディオ用 OPA2134 を使ったが、スピードが少し足りないように思う。
それでも、この OP アンプは安い割には超高性能で CP が高いと思う。

計測器のフロントエンドに使うような高速で、SN が高く、ローノイズ、高入力インピーダンスとなると、値段が高い・・
サンプリングが 2MHz 程度なので OPA2134 でも十分な気もする・・

とりあえず、JDS6600 オシレータの波形を入れてみた。
周期は、問題無く正確だ、電圧は計算とかなり違う。


次に、MTU で作った 10KHz の矩形波を使って、各ポイントを計測してみた。

SIGLENT のオシロスコープで観測すると、インピーダンスのマッチングが取れていないようだ・・・

まずはそれを合わす事から・・

DSO で使うプローブは、50MHz 対応の物で 1X、10X 切り替え式だが、回路構成の都合で 10X は使わない予定。
※だったが、やはり、対応は必須なのかもしれないので、構成などを再考している・・

インピーダンスを合わすのは意外と難しい・・・

フレームバッファをダブルバッファにする

シュミレータでは、ダブルバッファ(トリプルバッファ)なので、問題ないが、実機で操作すると、描画時間との関連で、リアルタイムな描画で問題が起こる。
これを解決するには、フレームバッファをダブルバッファにして、描画用と表示用でフリッピングを行う必要がある。

この仕様にすると、RX65N では、別の方法を考えなければならないが、RX72N の場合、潤沢にメモリがあるので問題は無い。
※RX65N では、ダブルバッファ構成にする余分なメモリが無い・・・
※これから購入する人は、あえて RX65N を買う人はいないだろうから、RX65N は切っても良いかもしれない。


簡単なコードを glcdc_mgr クラスに突っ込んで実験したが、思ったように動作せず、大きくはまった・・・
単純な勘違いが重なって、悩んだが、何とか思った動作が出来るようにはなった。

ただ、元々、シングルバッファで運用しようと設計していた部分を大きく変更しなければならず、しばらく改修が続いた。
その過程で、他のアプリも、DRW2D で描画出来るように drw2d_mgr クラスを改修した。

widget 管理は、シングルバッファでの運用を考えて実装してあるので、ダブルバッファにした場合にどうするか・・
これも、widget クラスに機能を追加して、ダブルバッファ対応に改修した。

波形の描画などをダイナミックに行うには、ダブルバッファは必須だ・・・

DRW2D エンジンを利用

ダブルバッファにすると、基本的に、常に描画する方向なので、DRW2D エンジンを積極利用する方が良い。
普通に考えて、描画はソフトで行うより高速だろうと思う。

また、DRW2D エンジンでは、線の太さや、アンチエリアスも簡単に出来る。

ただ、資料が少ないので、フラグの効果や、描画パラメーターの効果については、試すしか無い。

ビットマップテクスチャーを描画する(主にフォント)のに悩んだ。
普通に描画するのは直ぐに出来たけど、ビット「0」の部分を描画せずに透過させたい・・
で、やっと判った・・

        //-----------------------------------------------------------------//
        /*!
            @brief  ビットマップイメージを描画する
            @param[in]  pos     開始点を指定
            @param[in]  img     描画ソースのポインター
            @param[in]  ssz     描画ソースのサイズ
            @param[in]  back    背景を描画する場合「true」
        */
        //-----------------------------------------------------------------//
        void draw_bitmap(const vtx::spos& pos, const void* img, const vtx::spos& ssz, bool back = false)
        noexcept {
            if(img == nullptr) return;

            const uint8_t* src = static_cast<const uint8_t*>(img);
            int16_t w = ssz.x;
            int16_t h = ssz.y;

            // setup_();
            d2_color clut[2];
            clut[0] = back_color_.rgba8.rgba;
            auto copyflag = d2_bf_filter;
            if(!back) {
                clut[0] &= 0xffffff;
                copyflag |= d2_bf_usealpha;
            }
            // d2_setalphaex(d2_, 0, 0);
            clut[1] = fore_color_.rgba8.rgba;
            d2_settexclut_part(d2_, clut, 0, 2);
            d2_setblitsrc(d2_, src, w, w, h, d2_mode_i1 | d2_mode_clut);
            d2_blitcopy(d2_, w, h,
                0, 0, w * 16, h * 16, pos.x * 16, pos.y * 16, copyflag);
        }

Widget 関係のケア

widget_director では、描画は、変更が必要な場合にだけ行うようにしている。
※描画はやはりコストが大きいので、必要な場合にしか行わないようにしている。

そこで、機能を追加して、描画が起こった事を検出して、それを次フレームに持ち越して再描画を行うようにした。
こうすると、両方のフレームが常に同じ状態に保たれる。
「refresh()」では、内部的に特殊なフラグを設けて、「refresh」で描画した場合に、描画ステートを残さないようにした。

        widd_last_ = widd_.update();
        // ダブルバッファ時の widget 管理のケア
        if(render_.is_double_buffer()) {
            if(!widd_last_) {
                widd_.refresh();
            }
        }

破線の描画

点線などを描画する場合など
※この API は利用方法が意外と難しく、かなり苦労した・・
※サンプルコードが無いので、ドキュメントを観ながらだったが、各 API がどのように機能するのか不明だった。
その為、色々試して、ようやく理解した。

とりあえず、縦、横にラインを引く場合のみサポートした。
斜めに引く場合、「d2_setpatternparam」で、方向ベクトルを設定する必要がある。

        void setup_stipple_(const vtx::spos& d)
        {
            if(stipple_ != 0xffffffff) {
                d2_setpatternsize(d2_, 8);
//              d2_setpatternalpha(d2_, 0, 255);
//              d2_setpatternalpha(d2_, 1, 255);
                d2_setpatternparam(d2_, 0, 0, d.x, d.y);
                d2_setpattern(d2_, stipple_);
                d2_setfillmode(d2_, d2_fm_pattern);
            }
        }

短径領域の転送

フレームバッファ内で、短径領域をコピーするのも DRW2D で出来る。

        //-----------------------------------------------------------------//
        /*!
            @brief  移動
            @param[in]  src     ソース位置と大きさ
            @param[in]  dst     転送位置
        */
        //-----------------------------------------------------------------//
        void move(const vtx::srect& src, const vtx::spos& dst) noexcept
        {
            d2_utility_fbblitcopy(d2_, src.size.x, src.size.y, src.org.x, src.org.y, dst.x, dst.y,
                d2_bf_filter);
        }

まとめ

今回は、ダブルバッファ関係と、DRW2D 関係のケアで、終止して、アナログフロントエンドをケア出来なかった・・
それでも、DRW2D の本格移行が出来た感じで、それはそれで大きい。

RX72N Envision Kit でダブルバッファとTinyGLの実験

ダブルバッファ

RX72N では、内蔵メモリが 512K + 512K と増えたので、480x272 の 16 ビットカラーで、ダブルバッファを使う事が出来る。

ダブルバッファは、デジタルストレージオシロスコープで波形をレンダリングするのに都合が良いので、実験する必要性があった。

RX65N では、フレームバッファが1枚分しかなく、16 ビットカラーでは、ダブルバッファにする事が出来ない。
※ 8 ビットカラーなら可能かもしれないが、ラインアドレスは 64 バイトの倍数にする制約があり変則的な設定となる。

描画オブジェクトが GUI のような性質ならまだやりようもあるが、ダイナミックな描画をしようとすると、どうしても、ダブルバッファが必要となる。
※描画速度を気にしなければ、他にも方法はある・・

ダブルバッファの描画では、毎フレーム以下の手順で描画を行う。

  • 表示フレーム同期
  • B のバッファを表示用にする
  • A のバッファを全面クリア
  • A のバッファに全てのオブジェクトを描画
  • A、B を入れ替える

簡単には以上の手順を繰り返す。

通常、全面クリア+全てのオブジェクト描画が、1フレーム(16.6ミリ秒)以内に収まれば、フルフレーム(60Hz)で遅延なくスムーズに描画出来る。
描画オブジェクトが物理的に増えて、1フレームに収まらないと、フレームレートはどんどん落ちていく。
それでも、描画は裏で行っており、見えないので、チラツキは出ない。

glcdc_mgr は、フレームバッファの先頭アドレスを管理している。
GLCDC ハードウェアーは、水平ラインのアドレスは64の倍数でなければならない・・
ダブルバッファが有効な時、ページフリップに従ったアドレスを戻す。

        static const int16_t line_width =
            (((width * static_cast<int16_t>(PXT) / 8) + 63) & 0x7fc0) / (static_cast<int16_t>(PXT) / 8);
        static const uint32_t frame_size =
            line_width * (static_cast<uint32_t>(PXT) / 8) * height;

        void* get_fbp() const noexcept
        {
            uint32_t ofs = 0;
            if(enable_double_) {
                ofs = (flip_count_ & 1) != 0 ? 0 : frame_size;
            }
            if(layer2_org_ != nullptr) {
                uint32_t org = reinterpret_cast<uint32_t>(layer2_org_);
                return reinterpret_cast<void*>(org + ofs);
            } else if(layer1_org_ != nullptr) {
                uint32_t org = reinterpret_cast<uint32_t>(layer1_org_);
                return reinterpret_cast<void*>(org + ofs);
            }
            return nullptr;
        }

垂直同期のタイミングで先頭アドレス(GR2FLM2)がロードされる。
※「GR2VEN」を有効にしておかないと、レジスタを書き換える事が出来ない。

        void sync_vpos() const noexcept
        {
            if(enable_double_) {
                uint32_t ofs = (flip_count_ & 1) != 0 ? 0 : frame_size;
                uint32_t org = reinterpret_cast<uint32_t>(layer2_org_);
                GLC::GR2VEN = 1;
                GLC::GR2FLM2 = org + ofs;
            }
            volatile auto n = ctrl_blk_.vpos_count;
            while(n == ctrl_blk_.vpos_count) {
                asm("nop");
            }
        }

また、RX651/RX65N から搭載された描画エンジン DRW2D は、ソフトの描画に比べて、格段に高速なので、これを有効に活用するのに適している。


OpenGL 的なフレームワーク

「ダブルバッファ」と言えば、やはり3Dグラフィックスだろうと思う。
※スプライトと言う人もいるが・・・

昔から、リアルタイム 3D グラフィックスには、注力しているので、最も馴染みが深い OpenGL を縮小にした API を実装して実験してみた。

とりあえず、L チカに相当するキューブの回転w

RX マイコン内蔵の DRW2D 描画エンジンは、2D の描画用だが、基本的な構造が、3D にもマッチするように構成されている。

オブジェクトの座標をマトリックス演算して、透視変換、スクリーン座標変換まで行えば、2D 座標となり、そのまま描画出来る。
※テクスチャマッピングは多少難解かもしれないが、透視変換に適した API があるようだ。(これは今後の課題)

ポリゴンの描画では、裏と表の概念があり、親和性が良い。
※殆ど何もする事が無いと言いたいところだけど、立体ボリュームに対するクリッピング、ライティングなどを考えると、ちゃんと実装するのはかなり大変ではある。
端折ったテスト的なレンダリングなら簡単!

ほぼ OpenGL と同じような手順で描画出来るようにした、C++ なので、色々と便利機能を実装出来る。

    float ax = 0.0f;
    float ay = 0.0f;
    while(1) {
        render_.sync_frame();

        render_.clear(DEF_COLOR::Black);

        auto& m = tgl_.at_matrix();
        m.set_viewport(0, 0, LCD_X, LCD_Y);
        m.set_mode(gl::matrixf::mode::modelview);
        m.identity();
        m.translate(0.0f, 0.0f, -10.0f);
        m.rotate(ax, 1.0f, 0.0f, 0.0f);
        m.rotate(ay, 0.0f, 1.0f, 0.0f);

        ax += 1.0f;
        if(ax >= 360.0f) ax -= 360.0f;
        ay += 1.5f;
        if(ay >= 360.0f) ay -= 360.0f;

        m.set_mode(gl::matrixf::mode::projection);
        m.identity();
        m.perspective(45.0f, static_cast<float>(LCD_X) / static_cast<float>(LCD_Y), 1.0f, 50.0f);

//      draw_box_(2.0f, TGL::PTYPE::LINE_LOOP);
        draw_box_(2.0f, TGL::PTYPE::QUAD);

        tgl_.renderring();
    }

RX72N TFU を有効に!

RX72N は、三角関数演算器を持っているので、これを有効に利用するコードを追加。

回転行列の演算では、以下の API を利用しているので、その部分で、専用 API を呼ぶようにした。

    static inline void deg_sin_cos_(float deg, float& si, float& co) noexcept
    {
#ifdef SIG_RX72N
        __builtin_rx_sincosf(deg  * vtx::deg2rad_f_, &si, &co);
#else
        si = std::sin(deg * vtx::deg2rad_f_);
        co = std::cos(deg * vtx::deg2rad_f_);
#endif
    }

むすび

非常に初歩的ではあるが、3D オブジェクトの描画をテストした。
実用的に使うには、まだまだこれから色々入れないとならない・・・

とりあえず、ダブルバッファの実験から脱線したので、このプロジェクトはストールして、デジタルストレージオシロスコープの実装に戻る。

DRW2D エンジンは、今までほとんど使って来なかったので、良いきっかけになったと思う。

DRW2D エンジンは、アンチエリアスもサポートされており、このハードウェアーの情報は少ないが、かなり良く出来ていると思う。