GNU-RX 8.3.0 の最適化と倍精度浮動小数点オプション

Renesas GNU-RX 8.3.0 の最適化

Renesas GNU-RX の最適化は、通常の gcc (7.5.0) とは異なるようだー
より、深い最適化を行っている。

gcc-7.5.0 で、以下のプログラムをコンパイルすると・・

    typedef device::PORT<device::PORT0, device::bitpos::B1> LED;

    while(1) {
        utils::delay::milli_second(250);
        LED::P = 0;
        utils::delay::milli_second(250);
        LED::P = 1;
    }

論理演算を行ったコードが生成される。

ffc0029c:       cc 3e                           mov.b   [r3], r14
ffc0029e:       75 42 fa                        mov.l   #250, r2
ffc002a1:       75 2e fe                        and     #-2, r14
ffc002a4:       c3 3e                           mov.b   r14, [r3]

ffc002bc:       cc 3e                           mov.b   [r3], r14
ffc002be:       65 1e                           or      #1, r14
ffc002c0:       c3 3e                           mov.b   r14, [r3]

※中間に、「mov.l #250, r2」が挟まれてるのが多少痛いものの、まぁ普通だ。

Renesas GNU-RX では・・・

ffc00278:       f0 38                           bclr    #0, [r3].b

ffc00294:       f0 30                           bset    #0, [r3].b

ビット操作命令になっている。

RAYTRACE_sample が、361ms から 351ms に時間短縮したのも、他、細かい最適化によるものと思われる。
ルネサス社が魔改造したコンパイラは、高性能なのだと思われる。
※何故、gcc のオリジナルコードにマージしないのか?、疑問は残る・・・

Renesas GNU-RX 8.3.0 のオプション

倍精度浮動小数点命令を生成するには、コアが「RXv3」でなおかつ、「dfpu」を有効にすれば良いらしい。
※gcc のソースコードで、RX マイコンに関係する部分を少し読んでみた。

     -misa=v3 -mdfpu

なので、上記オプションを使って、簡単なコードで調べてみた。

    double a = 0.0;
    while(1) {
        utils::delay::milli_second(250);
        LED::P = 0;
        utils::delay::milli_second(250);
        LED::P = 1;
        a += 0.1;
        utils::format("%5.4f\n") % static_cast<float>(a);
    }

アセンブルリストを見てみると、確かに「倍精度浮動小数点命令」が生成されている、「a + 0.1;」の部分

ffc003d2:       fc c9 08 12 10                  dmov.D 72[r0], dr1
ffc003d7:       f9 03 00 9a 99 99 99            dmov.L #0x9999999a, drl0
ffc003de:       f9 03 02 99 99 b9 3f            dmov.L #0x3fb99999, drh0
ffc003e5:       76 90 00 11                     dadd  dr1, dr0, dr1
ffc003e9:       fc 79 08 12 10                  dmov.D dr1, 72[r0]

※ 64 ビットの IEEE-754 で定数 0.1 の表現は、0x3fb9'9999'9999'999a となる。
※ 64 ビットの定数をレジスターにロードするコストは大きい・・・

ちゃんと、倍精度の命令を生成しているので、RAYTRACE_sample を走らせてみた。

・・・逆に遅くなる。

調べると、dfpu を指定しない場合、常に32ビットで演算されていたものが、dfpu の指定で、部分的に64ビットの演算が行われる為のようだ。

実際にプログラムで利用する際は、細心の注意が必要だと思える・・・
※インテル CPU の場合、double を float にして計算すると、逆に遅くなるが、それとは対照的だ・・
でも、原理は理解は出来る。

それでも、64ビットの計算を、専用命令で行えるのは、ソフトエミュレーションに比べて格段に速いと思われるので、倍精度命令が生成される事実は心強い。

TFU についての調査

TFU については、仕様が公開されていないので、どのような物か、良く判らない・・・

gcc のソースコード「gcc/config/rx/rx.opt」を見ると・・・

EnumValue
Enum(rx_tfu_types) String(intrinsic) Value(RX_INTRINSIC)

EnumValue
Enum(rx_tfu_types) String(intrinsic,mathlib) Value(RX_MATHLIB)

とあるので、

    -mtfu=intrinsic

    -mtfu=intrinsic,mathlib

のどちらかを設定すれば良さそうだと判る。

「gcc/config/rx/rx.c」で、

    if (TARGET_TFU)
    {
      ADD_RX_TFU_BUILTIN (INIT, "__init_tfu", void);
      ADD_RX_BUILTIN3 (SINCOSF, "sincosf", void, float, float_ptr, float_ptr);
      ADD_RX_BUILTIN4 (ATAN2HYPOTF, "atan2hypotf", void, float, float, float_ptr, float_ptr);
      if (rx_tfu_type == RX_MATHLIB)
      {
        ADD_RX_BUILTIN1 (SINF, "sinf", float, float);
        ADD_RX_BUILTIN1 (COSF, "cosf", float, float);
        ADD_RX_BUILTIN2 (ATAN2F, "atan2f", float, float, float);
        ADD_RX_BUILTIN2 (HYPOTF, "hypotf", float, float, float);
      }
    }

となっている、
初期化の関数と思われる「__init_tfu」は、勝手に呼ぶコードが埋め込まれるのか不明だ。
※自動的に ctor に積まれるのかもしれない・・・

上記関数が演算器で演算されるものと思われるが、どのくらい効果があるのか不明・・・
また、現段階では、演算器が使われているかも不明で、何か、他に設定する事があるのかもとも思う。
とりあえず、TFU に関しては、調査を続ける。

Renesas GNU-RX 8.3.0 を使ってみる

はじめに

前の、投稿で、Renesas は GNU-RX 4.8.x ベースなので、サポートの可能性が低いと言ったが、あれは誤りだったーー

調べたら、「GNU Tools」 とゆー HP があり、ここに登録する事で、gcc-8.3.0 ベースの RX マイコン用ツールチェイン一式をダウンロード出来るようだ。
※ソースコードもダウンロード出来る。

登録が少し面倒で、登録したメールアドレスに起因するアクティベーションコードが発行され、そのコードが無いと、ツールをインストール出来ないようだが、コンパイラは制限無く使えるようだ。

ただ、gcc のソースツリーとは異なる実装を行っており、オプションが異なる。

「GNU Tools」は、「CyberTHOR Studios Limited」が保守管理しているようだが、ナゾの組織だ・・
※以前のKPITに類する物なのかもしれない。

それにしても、「知らない」とは恐ろしい・・・
このツールベースで、ルネサスの IDE から動かせば、E1エミュレータなどを使って、ステップ実行も出来そうに思う。

構成

このツールチェインは、以下のツールをまとめた物のようだ。

  • binutils_rx_2.24_2020q2
  • gcc_rx_8.3.0_2020q2
  • newlib_rx_3.1.0_2020q2
  • gdb_rx_7.8.2_2020q2

ターゲットヘルプの出力:

% rx-elf-gcc --target-help
The following options are target specific:
  -fpu                        Enable the use of RX FPU instructions.  This is
                              the default.
  -m32bit-doubles             Stores doubles in 32 bits.  This is the default.
  -m64bit-doubles             Store doubles in 64 bits.
  -mallow-string-insns        Enables or disables the use of the SMOVF, SMOVB,
                              SMOVU, SUNTIL, SWHILE and RMPA instructions.
                              Enabled by default.
  -mas100-syntax              Generate assembler output that is compatible with
                              the Renesas AS100 assembler.  This may restrict
                              some of the compiler's capabilities.  The default
                              is to generate GAS compatible syntax.
  -mbig-endian-data           Data is stored in big-endian format.
  -mcpu=                      Specify the target RX cpu type.
  -mdfpu                      Enable the use of RX DFPU instructions.
  -mgcc-abi                   Enable the use of the old, broken, ABI where all
                              stacked function arguments are aligned to 32-bits.
  -mint-register=             Specifies the number of registers to reserve for
                              interrupt handlers.
  -misa=                      Specify RX ISA version.
  -mjsr                       Always use JSR, never BSR, for calls.
  -mlarge-function-growth=    Permited value of the limit growth of large
                              function (in percent).
  -mlittle-endian-data        Data is stored in little-endian format.
                              (Default).
  -mlra                       Enable the use of the LRA register allocator.
  -mmax-constant-size=        Maximum size in bytes of constant values allowed
                              as operands.
  -mno-balign                 Do not use .balign
  -mpid                       Enables Position-Independent-Data (PID) mode.
  -mrelax                     Enable linker relaxation.
  -mrx-abi                    Enable the use the standard RX ABI where all
                              stacked function arguments are naturally aligned.
                              This is the default.
  -mrxpeephole                Coremark improvement.
  -mrxv2-fsqrt                Enable to use of FSQRT hardware instruction for
                              RXv2 instruction set
  -msave-acc-in-interrupts    Specifies whether interrupt functions should save
                              and restore the accumulator register.
  -msim                       Use the simulator runtime.
  -msmall-data-limit=         Maximum size of global and static variables which
                              can be placed into the small data area.
  -mtfu=                      Enable the use of RX TFU instructions.
  -mwarn-multiple-fast-interrupts Warn when multiple, different, fast interrupt
                              handlers are in the compilation unit.
  -nofpu                      Disable the use of RX FPU instructions.

Assembler options
=================

Use "-Wa,OPTION" to pass "OPTION" to the assembler.

 RX specific command line options:
  --mbig-endian-data
  --mlittle-endian-data [default]
  --m32bit-doubles [default]
  --m64bit-doubles
  --muse-conventional-section-names
  --muse-renesas-section-names [default]
  --msmall-data-limit
  --mrelax
  --mpid
  --mint-register=<value>
  --mcpu=<rx100|rx200|rx230|rx600|rx610|rx64m|rx66T|rx71m|rx72T>
  --misa=<v1|v2|v3>
  --mno-allow-string-insns  --mgcc-abi
  --mrx-abi [default]

Linker options
==============

Use "-Wl,OPTION" to pass "OPTION" to the linker.

elf32rx:
  --build-id[=STYLE]          Generate build ID note
  -z common-page-size=SIZE    Set common page size to SIZE
  -z defs                     Report unresolved symbols in object files.
  -z execstack                Mark executable as requiring executable stack
  -z max-page-size=SIZE       Set maximum page size to SIZE
  -z muldefs                  Allow multiple definitions
  -z noexecstack              Mark executable as not requiring executable stack
  --no-flag-mismatch-warnings Don't warn about objects with incompatible
                                endian or dsp settings
  --flag-mismatch-warnings    Warn about objects with incompatible
                                endian, dsp or ABI settings
  --ignore-lma                Ignore segment LMAs [default]
                                (for Renesas Tools compatibility)
  --no-ignore-lma             Don't ignore segment LMAs

注目するのは・・

--mcpu=<rx100|rx200|rx230|rx600|rx610|rx64m|rx66T|rx71m|rx72T>

RX72M、RX72N、RX66N などの CPU タイプは無いが、「RX72T」がある。

そして、

-mdfpu                      Enable the use of RX DFPU instructions.
-mtfu=                      Enable the use of RX TFU instructions.

多分、「倍精度浮動小数点命令」、「三角関数演算器」のサポートがある!

mcpu タイプに RX72N が無いのが、良く判らないが、gcc でも、サポートがされる事が判ったのがうれしい。
※まだ、有効にした場合の検証をしていない・・

MSYS2 からも、ツールのパスを設定すれば、使えるようだ・・

PATH=$PATH:/C/'Program Files (x86)'/'GCC for Renesas RX 8.3.0.202002-GNURX-ELF'/rx-elf/rx-elf/bin

ただ、

Assembler messages:
Warning: cannot compress debug sections (zlib not installed)

上記警告が出る・・・

お馴染み、レイトレースを走らせてみる

いつもの、ベンチマーク「レイトレース」を RX72N Envision Kit で走らせてみた。

何と、「最速」をマークした・・・
※gcc-7.5.0 では、361ms くらいだった・・

-mcpu=rx71m

※RX64M、とRX71M でコアの違いは無いと思うが・・・、とりあえず、RX71M でコンパイルした。
※RX64M でコンパイルしたものと同じバイナリーが出る。

今後の開発環境

今後、アップデートも行われるようなので、このコンパイラを使っていきたいと思う。

Makefile のオプションが変わるので、自分でビルドした gcc とは異なる設定が必要。

# CC_DEFS       =   -mcpu=rx600 -Wa,-mcpu=rxv2
CC_DEFS     =   -mcpu=rx71m -dfpu
# CP_DEFS       =   -mcpu=rx600
CP_DEFS     =   -mcpu=rx71m

上記のように、オプションを変更する。

もう少し、評価したら、Makefile を修正後、コミットする予定なので、自分で gcc をビルドしている人は、GNU-RX 8.3.0 をダウンロードして準備する必要がある。

RXマイコンの開発環境をバージョンアップ

RXマイコンの開発環境をアップグレード

気がつくと、gcc は 10.1.0 がリリースされており、RX マイコンも RXv3 コアがリリースされた事などから、そろそろ、開発環境を見直す時期なのかもしれない。

RX72N の RXv3 コアで大きく変わった事と言えば、倍精度の浮動小数点演算命令がサポートされた事だと思う。
※RXv3 コアでも、倍精度浮動小数点演算をサポートしない、RX66T、RX72T、などがあり多少複雑だ。
※他に、三角関数演算器が追加された。(ただ、この演算器については、使い方が隠蔽されており、実際に使える状態になるのは、メーカーのサポートが必要になると思われる。)

CC-RX では、当然サポートされるだろうけど、GNU-RX でサポートされるかは、今の処不明となっている。
※GNU-RX は gcc-4.8.x をベースにしており、本流のツリーにマージしていない事から、gcc でサポートされる可能性は非常に低いと考えられる。

それもあって、倍精度の命令を生成する仕組みを gcc に実装するにはどうしたら良いかを考え始め、情報を集めている。

binutils-2.34(アセンブラ)は、RXv3 の命令をアセンブル出来るようになっており、問題は無いものと思う。

まずは、binutils をアップグレード

今まで、binutils-2.30 を利用してきたが、このバージョンでは、RX マイコンの RXv3 で新設された命令をアセンブルする事が出来ない。

GNU assembler (GNU Binutils) 2.30

 RX specific command line options:
  --mbig-endian-data
  --mlittle-endian-data [default]
  --m32bit-doubles [default]
  --m64bit-doubles
  --muse-conventional-section-names
  --muse-renesas-section-names [default]
  --msmall-data-limit
  --mrelax
  --mpid
  --mint-register=<value>
  --mcpu=<rx100|rx200|rx600|rx610|rxv2>
  --mno-allow-string-insns

そこで、binutils-2.34 を使う

GNU assembler (GNU Binutils) 2.34

 RX specific command line options:
  --mbig-endian-data
  --mlittle-endian-data [default]
  --m32bit-doubles [default]
  --m64bit-doubles
  --muse-conventional-section-names
  --muse-renesas-section-names [default]
  --msmall-data-limit
  --mrelax
  --mpid
  --mint-register=<value>
  --mcpu=<rx100|rx200|rx600|rx610|rxv2|rxv3|rxv3-dfpu>
  --mno-allow-string-insns

rxv3、rxv3-dfpu が追加されている。

C/C++ コンパイラは、gcc-7.5.0 を使う

現在、gcc-10.1.0 がリリースされているので、色々試してみたが・・・

gcc-9.x 系 ---> デバッグビルド(最適化 -O0 で、割り込みベクターを伴った関数で、gcc がストールする)
gcc-8.x 系 ---> デバッグビルド(最適化 -O0 で、割り込みベクターを伴った関数で、gcc がストールする)
gcc-7.5.0 ---> 問題無し

本来、gcc のストールは、フルバグレポートを送る必要があると思うが、レポートの作り方や送り方を調べるのは時間がかかりそうなので、それは追々対応してみたいと思う。

RX マイコンの専用コードは、6.4.0 とあまり変わらないが、とりあえず、7.5.0 をビルドして実行バイナリーの動作をテストしてみた。
※色々、テストしたが、7.5.0 が安定しているようだー

gcc version 7.5.0 (GCC)
The following options are target specific:
  -fpu                        Enable the use of RX FPU instructions.  This is
                              the default.
  -m32bit-doubles             Stores doubles in 32 bits.  This is the default.
  -m64bit-doubles             Store doubles in 64 bits.
  -mallow-string-insns        Enables or disables the use of the SMOVF, SMOVB,
                              SMOVU, SUNTIL, SWHILE and RMPA instructions.
                              Enabled by default.
  -mas100-syntax              Generate assembler output that is compatible with
                              the Renesas AS100 assembler.  This may restrict
                              some of the compiler's capabilities.  The default
                              is to generate GAS compatible syntax.
  -mbig-endian-data           Data is stored in big-endian format.
  -mcpu=                      Specify the target RX cpu type.
  -mgcc-abi                   Enable the use of the old, broken, ABI where all
                              stacked function arguments are aligned to 32-bits.
  -mint-register=             Specifies the number of registers to reserve for
                              interrupt handlers.
  -mjsr                       Always use JSR, never BSR, for calls.
  -mlittle-endian-data        Data is stored in little-endian format.
                              (Default).
  -mlra                       Enable the use of the LRA register allocator.
  -mmax-constant-size=        Maximum size in bytes of constant values allowed
                              as operands.
  -mpid                       Enables Position-Independent-Data (PID) mode.
  -mrelax                     Enable linker relaxation.
  -mrx-abi                    Enable the use the standard RX ABI where all
                              stacked function arguments are naturally aligned.
                              This is the default.
  -msave-acc-in-interrupts    Specifies whether interrupt functions should save
                              and restore the accumulator register.
  -msim                       Use the simulator runtime.
  -msmall-data-limit=         Maximum size of global and static variables which
                              can be placed into the small data area.
  -mwarn-multiple-fast-interrupts Warn when multiple, different, fast interrupt
                              handlers are in the compilation unit.
  -nofpu                      Disable the use of RX FPU instructions.

newlib は、2.4.0 のままとした。

newlib は、バージョンを上げる場合に注意を要するので、無難な選択として、2.4.0(変更しない)とした。

RX フレームワークをビルドする。

コンパイラが出来たら、全体のプロジェクトをビルドする、まず「debug」ビルドからー

sh all_project_build.sh -debug clean

...

sh all_project_build.sh -debug

問題無い!

次にリリースビルド・・・

sh all_project_build.sh clean

...

sh all_project_build.sh

これも問題無い!、いくつかのバイナリーを実際に動かしてみたが、特に問題になるような動作は認められなかった。

gcc に倍精度命令を出力させる件は時間がかかりそうなので、調査を続ける事として、当面、開発環境は、これで行く事にする。

binutils-2.34
gcc-7.5.0
newlib-2.4.0

MSYS2 で作成した gcc のバイナリーをリンクにアップロードしてある。

CP2102Nを使ったUSBシリアル基板

PCBGOGO で作成した基板

以前に、別件で、PCBGOGO で基板を作る際、同時に注文したもので、部品も揃っていたのだが、ハンダペーストが無かったのもあって、ほったらかしにしていた・・・
※10枚作って、$5だった、安!、送料は$17とかだった・・

それをようやく組み立てた。

ハンダ付けに苦労はしたが、何とか組み立て、接触不良無く、一発で動作した。
※パッドの形状などから、QFN20 のハンダ付けが一番不安だった・・・

KiCAD プロジェクト一式

Silicon Labs CP2102N について

中華の格安モジュール基板で「CP2102」は大量に出回っているのだけど、「CP2102N」は少ない。
仕様的には同じ部分も多いが、マイナーチェンジ版の「N」は、もう一息だった部分が改良されていて、申し分のないものになっている。

  • 一台の PC に複数のデバイスを繋いだ場合の挙動が改善されている。
  • FTDI のメジャーチップより高性能で、同じボーレートでも、送信も受信も速い。
  • カスタムするツールの導入が簡単で、専用の ID や文字列を埋め込める。
  • 安い!(QFN24 で @160 くらい)

QFN パッケージの憂鬱

CP2102N は、パッケージとして、QFN20、QFN24、QFN28 などがあるのだが、KiCADでトラックを引く前に、部品を発注してしまった・・

QFN パッケージでも、手ハンダで取り付け出来るのだが、QFN20 は、角のピンが隠れているし、裏面にGNDパターンがあり、手ハンダでは難しい事が判って、大いに後悔した・・・

QFN24 にしとけば良かった・・・

ハンダペーストを楊枝で、適当に塗って、コテライザーを使って、溶かして付ける。

念の為、フラックスを塗って、サイド側からハンダコテを当ててパッドにハンダを盛る。

これで、何とかなったようだ・・・

接触不良も無く、動いているようだー。

タカチのケースに入れる事を考えた形状

初めから、タカチのケースに入れる事を考えて作ったのだが、急いでいた事もあって、イマイチだった・・・

基板をリューターで削って、何とかなった。
※一応、隅に配線が通らないようにはしている。

3.3V のレギュレータ

RXマイコンでは、3.3Vで動かす事が多いので、3.3Vの三端子を載せてあり、電源電圧として、5V、3.3Vを切り替え出来る。

チップ部品は結構大変

あまり考えずに、1608サイズのチップ部品にしたが、手ハンダはキツイ!
何か、治具を作らないと綺麗に配置するのは難しいかもしれない、視力が衰えている事もあるが、ルーペで拡大しないとならないし、ハンダ付けする時部品を押さえておかないとならない。

rx_prog の不具合?

FTDI より 高速に送信、受信が出来るものの、大きなファイルを書き込むと、エラーで止まる現象が起こる。
※FTDIでも発生していたのだが・・

原因は調査中だが、CP2102Nの問題では無さそうで、MSYS2のPOSIX系シリアルドライバーのハンドリングにありそうだ、こちらは、簡単に原因を見つけられなかったので、解析中。

2020-06-08 01:10:15 Monday
問題は解決したものと思う:

  • erase-page、write-page のループで、間隔を空けないで、次のコマンドを送ると、RXマイコン側がストールするようだ。
  • erase-page の場合、ページ(256バイト)毎に2ミリ秒の待ちを入れた。
  • write-page の場合、ページ(256バイト)毎に5ミリ秒の待ちを入れた。
  • 上記待ちを入れた状態で、ストールせず、正常に書き込めた。
  • 待ちを入れない場合、RXマイコン側でバッファオーバーランなどが発生するのかも・・
  • この修正は、コミットした。(rx_prog)

2020-06-09 04:34:32 Tuesday

  • rx_prog を拡張して、erase-page-wait、write-page-wait パラメーターを追加。
  • rx_prog.conf にも変数を追加。
  • 設定は、マイクロ秒単位、標準で、それぞれ、2000、5000を設定してある。

FTDIとシリコンラボで、ボーレートが同じでも全体のパフォーマンスが異なるのは・・
以下のような理由によるものと思う:

  • 基本的にドライバーの違いのようだ。
  • FTDIでは、小さいパケットを送ると、「都度送る」
  • シリコンラボの場合は、「貯めて送る」と、「バッファにあったら送る」
  • USBサービスの関係から、「都度送る」と間隔が空いてしまうようだ。
  • ハンドシェークを行わない場合、受け手の仕様によっては、相性問題が発生するかもしれない。
  • 受け手の作りが「甘い」と、FTDIの方が相性が良い事になる感じか・・

FTDI と シリコンラボ速度比較

AUDIO_sample/RX64M/audio_sample.mot (628460 バイト)

rx-elf-size audio_sample.elf
   text    data     bss     dec     hex filename
 541476      48   86936  628460   996ec audio_sample.elf
-rw-r--r-- 1 hira hira 1353908  6月  8 03:13 audio_sample.mot

FTDI(FT232XS) @230 秋月電子:

time rx_prog -P COM5 -d RX64M --progress --erase --write --verify audio_sample.mot
Erase:  #################################################
Write:  #################################################
Verify: #################################################
0.39user 1.90system 5:57.08elapsed 0%CPU (0avgtext+0avgdata 9304maxresident)k
0inputs+0outputs (2442major+0minor)pagefaults 0swaps

※FTDIのメジャーデバイス、FT232RL(@400) でも結果は同じ。


SiliconLabs(CP2102N) @160 DigiKey:

time rx_prog -d RX64M --progress --erase --write --verify audio_sample.mot
Erase:  #################################################
Write:  #################################################
Verify: #################################################
0.28user 1.09system 1:51.59elapsed 1%CPU (0avgtext+0avgdata 9372maxresident)k
0inputs+0outputs (2460major+0minor)pagefaults 0swaps

上記のように3倍以上違い、値段も安い!

なので、CP2102N を使うべきー

RX72N Envision Kit での開発(その5)GUI編

GUI_sample (RX65N/RX72N Envision Kit)

C++ GUI フレームワーク

RX65N Envision Kit から採用された GUI ライブラリとして、emWin が既にあります。

ですが、これは C で実装されており、アプリケーションを作るには、ハードルが高いと思えます。

以前に PC 向けに、OpenGL を描画エンジンとして使った、GUI フレームワーク glfw3_app を実装した経緯があり、GUI 操作に必要な構成は研究して判っているつもりなので、それらの知見を使い、組み込みマイコンでも扱いやすいようにダイエットした GUI Widget のフレームワークを実装しました。

「漢字」が標準で使えるのも特徴です。
※現在は16x16ピクセルのフォント「東雲16ドット漢字フォント」を利用させてもらっています。
※メモリに余裕があるので、ROM 領域にビットマップとして持っています。(260キロバイト程度消費する)
※漢字が必要無い場合は、含めない事も出来ますし、又はSDカードから読み込んでキャッシュする事も出来ます。

描画に関しては、DRW2D エンジンが無くても利用可能なように、現在はソフトウェアーで処理しています。
※RX64M/RX71M などに LCD をバス接続した場合や、フレームバッファ内蔵の LCD を接続する場合を考慮しています。
※DRW2D でアクセレートする事も可能な構造にしてあります。(DRW2D 版は開発中)

このフレームワークでは、記憶割り当てを使わない事を念頭に設計してあり、比較的小規模なアプリ向けとして機能を絞ってあります。
もっと「リッチ」な物が必要なら、新たに実装して追加出来る余地も残してあります。

現状で、用意してあるのは以下の Widget です。
※ソースコードは、「RX/graphics」以下にあります。

Widget 機能 ソース 完成度
frame フレーム frame.hpp
button ボタン button.hpp
check チェックボックス check.hpp
radio ラジオボタン radio.hpp
slider スライダー slider.hpp
menu メニュー menu.hpp
spinbox スピンボックス spinbox.hpp ×
group グループ管理 group.hpp

「見た目」は、シンプルなものにしてあり、ピクセルデータを用意する事無く、プリミティブの組み合わせで描画しています。

今後、必要な widget を拡充して行く予定です。

全体の構成

GUI は一般的には、メッセージ通信により、機能を提供する事が一般的だと思います。
※代表的なのは Windows でしょうか・・

ただ、この方式は、冗長なコードになりやすいし、機能が複雑になるとメッセージの順番や、メッセージのマスクなど、トリッキーなコードになりやすいと思います。

このフレームワークでは、「同期式」と呼ばれる、リアルタイムなゲームで使われるようなシステムを使っています。

画面の更新は 60Hz 程度なので、それに合わせて、タッチパネルの情報を取得して順次処理を行っています。

又、C++ の機能を積極的に利用する事で、シンプルな構成に出来、アプリケーションを実装しやすくします。

GUI 部品管理

この GUI フレームワークでは、管理する Widget の数をテンプレートで定義しています(有限個)。

    // 最大32個の Widget 管理
    typedef gui::widget_director<RENDER, TOUCH, 32> WIDD;
    WIDD        widd_(render_, touch_);

new などを使い、動的に増やす事も出来ますが、メモリが足りなくなった場合の対応を考えると、「有限数」の方が管理し易いように思います。

LCD は 4.3 インチで、解像度も 480 x 272 程度と小さいので、PC のディスクトップのように、複雑でリッチな GUI は、当面必要無いと思える為です。


C++ での実装で、少し問題な点があります、現状の実装では、widget の追加と削除は、グローバル関数としています。

extern bool insert_widget(gui::widget* w);
extern void remove_widget(gui::widget* w);

この関数は、「widget_director」テンプレートクラス内の API「insert、remove」を呼ぶようにしなければなりません。
そこで、サンプルでは、以下のように、widget_director のインスタンスを置いてあるソースで定義してあります。

/// widget の登録・グローバル関数
bool insert_widget(gui::widget* w)
{
    return widd_.insert(w);
}

/// widget の解除・グローバル関数
void remove_widget(gui::widget* w)
{
    widd_.remove(w);
}

※他に良い方法を思い付かなかったので、このように、あまりスマートとは言えない方法になっています。
※こうしておけば、widget_director を複数持って、場合により、切り替える事も出来そうです。

widget_director は、描画ループの中から「update」を呼び出せば、全ての管理が行われます。

    while(1) {
        render_.sync_frame();
        touch_.update();

        widd_.update();

...

    }

※「touch_.update();」は、タッチパネルインターフェース(FT5206)のサービスです。
※widget_director テンプレートでは、レンダリングクラスと、タッチパネルクラスの型を必要とし、コンストラクター時、参照で与えます。

    // GLCDC 関係リソース
    typedef device::glcdc_mgr<device::GLCDC, LCD_X, LCD_Y, PIX> GLCDC;

    // フォントの定義
    typedef graphics::font8x16 AFONT;
//  for cash into SD card /kfont16.bin
//  typedef graphics::kfont<16, 16, 64> KFONT;
    typedef graphics::kfont<16, 16> KFON
    typedef graphics::font<AFONT, KFONT> FONT;

    // DRW2D レンダラー
//  typedef device::drw2d_mgr<GLCDC, FONT> RENDER;
    // ソフトウェアーレンダラー
    typedef graphics::render<GLCDC, FONT> RENDER;

    GLCDC       glcdc_(nullptr, reinterpret_cast<void*>(LCD_ORG));
    AFONT       afont_;
    KFONT       kfont_;
    FONT        font_(afont_, kfont_);
    RENDER      render_(glcdc_, font_);

    FT5206_I2C  ft5206_i2c_;
    typedef chip::FT5206<FT5206_I2C> TOUCH;
    TOUCH       touch_(ft5206_i2c_);

    // 最大32個の Widget 管理
    typedef gui::widget_director<RENDER, TOUCH, 32> WIDD;
    WIDD        widd_(render_, touch_);

widget 親子関係とグループ化

widget は、階層構造が可能なようにしてあり、座標管理も差分で行えるようにしてあります。
※その為、親、子、関係があります。

また、ラジオボタンのように、自分の変化を受けて、他も変化が必要な場合があり、この場合、グループ化が役立ちます。

以下のように、3つのラジオボタンをグループ化しておけば、チェック、アンチェックの管理は自動で行えます。
※ラジオボタンの実装では、自分の状態が変化した時、「自分の親」に登録されている、「子」で、ラジオボタンを調べて、その状態を変更しています。

    typedef gui::group<3> GROUP3;
    GROUP3      group_(vtx::srect(   10, 10+50*2, 0, 0));
    typedef gui::radio RADIO;
    RADIO       radioR_(vtx::srect(   0, 50*0, 0, 0), "Red");
    RADIO       radioG_(vtx::srect(   0, 50*1, 0, 0), "Green");
    RADIO       radioB_(vtx::srect(   0, 50*2, 0, 0), "Blue");

※ラジオボタンは、グループ化する為、グループ座標の差分となっている。
※各 widget では、サイズ指定で「0」を指定すると、標準的なサイズがロードされます、この定義は、各 widget 内で定義されています。

C++ では、オペレータを使って、特別な機能を割り当て出来ます。
この場合、「+」は、group への、radio ボタン登録として機能します。

    // グループにラジオボタンを登録
    group_ + radioR_ + radioG_ + radioB_;

コールバックとラムダ式

C++ では、C++11 からラムダ式が使えるようになりました。

GUI の操作では、何か「変化」が発生した場合に、コールバック関数が呼ばれます。

C++ では、std::function テンプレートを使っています。

たとえば、button widget では、以下のように定義してあります。

    typedef std::function<void(uint32_t)> SELECT_FUNC_TYPE;

ボタンが押された(ボタンをタッチして、離れた瞬間)時、押された回数をパラメータに、コールバック関数が呼ばれます。

C++ では、ラムダ式が使えるので、コールバック関数を登録しないで、ラムダ式により、直接動作を実装出来ます。

    button_.at_select_func() = [=](uint32_t id) {
        utils::format("Select Button: %d\n") % id;
    };

これは、非常に便利で、アプリケーションを作成する時は大いに役立ち、シンプルに実装出来ます。
※クラス内の場合は、キャプチャーを「[this]」とする事で、クラス内のメソッドを呼べるようになります。

GUI サンプルのメイン

サンプルでは、一通りの GUI を定義、登録して、各 widget にラムダ式を使って、挙動を表示(シリアル出力)するようにしています。

widget の定義と登録:
※各 widget のコンストラクターで、widget_director へ登録される。

    typedef gui::button BUTTON;
    BUTTON      button_  (vtx::srect(   10, 10+50*0, 80, 32), "Button");
    typedef gui::check CHECK;
    CHECK       check_(vtx::srect(   10, 10+50*1, 0, 0), "Check");  // サイズ0指定で標準サイズ
    typedef gui::group<3> GROUP3;
    GROUP3      group_(vtx::srect(   10, 10+50*2, 0, 0));
    typedef gui::radio RADIO;
    RADIO       radioR_(vtx::srect(   0, 50*0, 0, 0), "Red");
    RADIO       radioG_(vtx::srect(   0, 50*1, 0, 0), "Green");
    RADIO       radioB_(vtx::srect(   0, 50*2, 0, 0), "Blue");
    typedef gui::slider SLIDER;
    SLIDER      sliderh_(vtx::srect(200, 20, 200, 0), 0.5f);
    SLIDER      sliderv_(vtx::srect(440, 20, 0, 200), 0.0f);
    typedef gui::menu MENU;
    MENU        menu_(vtx::srect(120, 70, 100, 0), "ItemA,ItemB,ItemC,ItemD");

登録された GUI を有効にして、コールバック関数に、ラムダ式で挙動を実装する。

    void setup_gui_()
    {
        button_.enable();
        button_.at_select_func() = [=](uint32_t id) {
            utils::format("Select Button: %d\n") % id;
        };

        check_.enable();
        check_.at_select_func() = [=](bool ena) {
            utils::format("Select Check: %s\n") % (ena ? "On" : "Off");
        };

        // グループにラジオボタンを登録
        group_ + radioR_ + radioG_ + radioB_;
        group_.enable();  // グループ登録された物が全て有効になる。
        radioR_.at_select_func() = [=](bool ena) {
            utils::format("Select Red: %s\n") % (ena ? "On" : "Off");
        };
        radioG_.at_select_func() = [=](bool ena) {
            utils::format("Select Green: %s\n") % (ena ? "On" : "Off");
        };
        radioB_.at_select_func() = [=](bool ena) {
            utils::format("Select Blue: %s\n") % (ena ? "On" : "Off");
        };
        radioG_.exec_select();  // 最初に選択されるラジオボタン

        sliderh_.enable();
        sliderh_.at_select_func() = [=](float val) {
            utils::format("Slider H: %3.2f\n") % val;
        };
        sliderv_.enable();
        sliderv_.at_select_func() = [=](float val) {
            utils::format("Slider V: %3.2f\n") % val;
        };

        menu_.enable();
        menu_.at_select_func() = [=](uint32_t pos, uint32_t num) {
            char tmp[32];
            menu_.get_select_text(tmp, sizeof(tmp));
            utils::format("Menu: '%s', %u/%u\n") % tmp % pos % num;
        };

まとめ

やはり、GUI のような構造的な体系には、C++ が必要だと痛感します。

今回の GUI フレームワークで、widget は「継承」を使っていますが、GUI の部品はスタティックに定義してあり、「new」や「delete」もしません。
※実際は、偶然そうなっているのでは無く、「しなくて済むよう」に工夫しています。

複雑な構成のアプリケーションを実装したい場合、シーン管理を使って、各シーンで登場する GUI を定義、実装すれば、シーン毎に GUI の定義を別ける事が出来ます。
※「LOGGER_sample」を参照。(未完成で実装中です)

シーンの定義については、「common/scene.hpp」テンプレートクラスを参照して下さい。

RX72N Envision Kit での開発(その4)SDCARD 編

RX72N Envision Kit の再販が始まったようだ・・・

チップワンストップ:
2020-06-04 17:31:31 Thursday:
※在庫8個だったけど、1日たったら、完売してた・・・、まだまだ潤沢に流通していないようだ・・

SD-CARD 操作と、関係ユーティリティの使い方

組み込みマイコンでは、SDカードのアクセスは必須な機能となっています。

ChaN さんによる FatFs ライブラリがオープンソースとして公開されており、非常に簡単に利用する事が出来ます。
※FatFs ライブラリは、常に進化しており、なるべく最新版を使う事が望まれます、自分のフレームワークでは、ff13c を利用しています。

SDカードにアクセスするには、簡易的な SPI モードと、SD モードがあります。

このサンプルでは、以下のように、各デバイスで、SDカードのアクセスを行います。

RX24T RX64M DIY RX64M GR-KAEDE RX65N RX72N
RSPI0 Soft SPI RSPI SDHI SDHI

RX64M から搭載(RX64M/Rx71M はオプション)された SDHI インターフェースを使った、SD モードのネィティブモードドライバーも利用可能です。
SD モードでは、4ビットバスを使った高速な転送が可能となっています。
※RX64M は SDHI はオプションになっており、自分が実験したデバイスは、「SDHI なし」なので、Soft-SPI で利用しています。

SDカードのアクセスでは、なるべく、簡単で柔軟性のある宣言で、実装が出来るように工夫してあり、簡単に扱う事が出来ます。

SDCARD_sample プロジェクト

インターフェースの宣言

RX24T

    typedef device::rspi_io<device::RSPI0> SDC_SPI;
    typedef device::PORT<device::PORT6, device::bitpos::B5> SDC_SELECT; ///< カード選択信号
    typedef device::PORT<device::PORT6, device::bitpos::B4, 0> SDC_POWER;   ///< カード電源制御
    typedef device::PORT<device::PORT6, device::bitpos::B3> SDC_DETECT; ///< カード検出
    typedef device::NULL_PORT SDC_WPRT;  ///< カード書き込み禁止ポート設定
    typedef fatfs::mmc_io<SDC_SPI, SDC_SELECT, SDC_POWER, SDC_DETECT, SDC_WPRT> SDC;
    SDC_SPI sdc_spi_;
    SDC     sdc_(sdc_spi_, 20'000'000);
  • RX24T では、RSPI0 を使います。
  • RSPI では、MISO、MOSI、SCLK の3つの信号を利用します。
  • 「カード選択信号」は、PORT6、B5 を使います。
  • 「カード電源制御」は、PORT6、B4 で、アクティブ Low の信号としています。
  • 「カード検出信号」は、PORT6、B3 を使います。
  • 「書き込み禁止信号」は、使わない為「NULL_PORT」を指定します。
  • 「fatfs::mmc_io」クラスは、SPI 制御の為のインターフェースクラスです。
  • 上記のように、RSPI0 の定義を、mmc_io にパラメータとして定義し、インスタンスをコンストラクターに渡します。
  • RSPI の上限のクロック速度を指定します。

RX64M / GR-KAEDE

  #ifdef GR_KAEDE
    typedef device::rspi_io<device::RSPI> SDC_SPI;
    typedef device::PORT<device::PORTC, device::bitpos::B4> SDC_SELECT; ///< カード選択信号
    typedef device::NULL_PORT  SDC_POWER;   ///< カード電源制御(常に電源ON)
    typedef device::PORT<device::PORTB, device::bitpos::B7> SDC_DETECT; ///< カード検出
    typedef device::NULL_PORT SDC_WPRT;  ///< カード書き込み禁止
  #else
    typedef device::PORT<device::PORTC, device::bitpos::B3> MISO;
    typedef device::PORT<device::PORT7, device::bitpos::B6> MOSI;
    typedef device::PORT<device::PORT7, device::bitpos::B7> SPCK;
    typedef device::spi_io2<MISO, MOSI, SPCK> SDC_SPI;  ///< Soft SPI 定義
    typedef device::PORT<device::PORTC, device::bitpos::B2> SDC_SELECT; ///< カード選択信号
    typedef device::PORT<device::PORT8, device::bitpos::B2, 0> SDC_POWER;   ///< カード電源制御
    typedef device::PORT<device::PORT8, device::bitpos::B1> SDC_DETECT; ///< カード検出
    typedef device::NULL_PORT SDC_WPRT;  ///< カード書き込み禁止
  #endif
    SDC_SPI sdc_spi_;
    typedef fatfs::mmc_io<SDC_SPI, SDC_SELECT, SDC_POWER, SDC_DETECT, SDC_WPRT> SDC;
    SDC     sdc_(sdc_spi_, 25'000'000);
  • DIY ボードでは、ソフト SPI の定義をしています。
  • GR-KAEDE では RSPI の定義をしています。
  • GR-KAEDE の RSPI/SD カードインターフェースは、E1 デバッガー端子と共有になっておりプルダウン抵抗があります。
  • SD カードの初期化時、プルアップが必要なので、対策する必要があります。
  • 対策方法は、main.cpp の説明を参照して下さい。
  • ソフト SPI では、クロックの生成など、全てソフトウェアーで行う為、自由度がありますが、速度は RSPI に比べてかなり遅いです。
  • MISO、MOSI、SPCK のポートを定義して、ソフト SPI クラス (spi_io2) として宣言します。
  • spi_io2 クラスは、SD カードの SPI 制御向けに特化したクラスとなっています。
  • 「カード選択信号」は、PORTC、B2 を使います。
  • 「カード電源制御」は、PORT8、B2 で、アクティブ Low の信号としています。
  • 「カード検出信号」は、PORT8、B1 を使います。
  • 「書き込み禁止信号」は、使わない為「NULL_PORT」を指定します。
  • 「fatfs::mmc_io」クラスは、SPI 制御の為のインターフェースクラスです。
  • 上記のように、Soft-SPI の定義を、mmc_io にパラメータとして定義し、インスタンスをコンストラクターに渡します。
  • ソフト SPI の上限のクロック速度を指定していますが、目安程度でしかありません。

RX65N Envision Kit

    typedef device::PORT<device::PORT6, device::bitpos::B4, 0> SDC_POWER;  ///< 「0」でON
    typedef device::NULL_PORT SDC_WPRT;  ///< カード書き込み禁止ポート設定
    typedef fatfs::sdhi_io<device::SDHI, SDC_POWER, SDC_WPRT, device::port_map::option::THIRD> SDC;
    SDC     sdc_;
  • SDHI を使う場合、ポートは決められた組み合わせの中から選択する必要があります。
  • SDHI が使うポートの詳細は、RX65x/port_map.hpp を参照して下さい、「候補3」です。
  • 「カード電源制御」は、PORT6、B4 で、アクティブ「Low」です。
  • 「書き込み禁止信号」は、使わない為「NULL_PORT」を指定します。
  • RX65N Envision Kit では、SDカードインターフェースは未実装となっています。
  • 電源制御は、専用 IC を使わず、Pチャネルの MOS-FET をスイッチとして流用している為、アクティブ「Low」となっています。
  • クロック速度は、現在の実装では、30MHz となっており、ハードコードになっています。

RX72N Envision Kit

    typedef device::PORT<device::PORT4, device::bitpos::B2> SDC_POWER;
    typedef device::NULL_PORT SDC_WPRT;  ///< カード書き込み禁止ポート設定
    typedef fatfs::sdhi_io<device::SDHI, SDC_POWER, SDC_WPRT, device::port_map::option::THIRD> SDC;
    SDC     sdc_;
  • SDHI を使う場合、ポートは決められた組み合わせの中から選択する必要があります。
  • SDHI が使うポートの詳細は、RX72N/port_map.hpp を参照して下さい、「候補3」です。
  • 「カード電源制御」は、PORT4、B2 で、アクティブ「High」です。
  • 「書き込み禁止信号」は、使わない為「NULL_PORT」を指定します。
  • クロック速度は、現在の実装では、30MHz となっており、ハードコードになっています。
  • 実験では、60MHz での動作も確認しましたが、RX65N Envision Kit では、不安定だったので、余裕を持って 30MHz としています。
  • クロック信号における、インピーダンスのマッチングを行えば、60MHz でも安定して利用可能と思います。

FatFs とのインターフェース定義

  • FatFs は、初期化(マウント)、セクター単位の読出し、書き込みなどの API を用意するだけで良く、以下のように、API を宣言します。
  • FatFs は C のプログラムなので、「extern "C"」で、C から呼べるようにしておきます。
  • ファイル書き込み時、タイムスタンプで使う「時間」が必要です、このフレームワークでは、GMT を使っています。
extern "C" {

    DSTATUS disk_initialize(BYTE drv) {
        return sdc_.disk_initialize(drv);
    }

    DSTATUS disk_status(BYTE drv) {
        return sdc_.disk_status(drv);
    }

    DRESULT disk_read(BYTE drv, BYTE* buff, DWORD sector, UINT count) {
        return sdc_.disk_read(drv, buff, sector, count);
    }

    DRESULT disk_write(BYTE drv, const BYTE* buff, DWORD sector, UINT count) {
        return sdc_.disk_write(drv, buff, sector, count);
    }

    DRESULT disk_ioctl(BYTE drv, BYTE ctrl, void* buff) {
        return sdc_.disk_ioctl(drv, ctrl, buff);
    }

    DWORD get_fattime(void) {
        time_t t = 0;
#if defined( ENABLE_RTC) || defined(ENABLE_I2C_RTC)
        rtc_.get_time(t);
#else
        t = utils::str::make_time(nullptr, nullptr);
#endif
        return utils::str::get_fattime(t);
    }
}

初期化、及び、SDカード挿入の監視

  • このサンプルでは、1/100 秒のタイマーを使って、ループを行い、その中で、SD カードの監視を行います。
  • ループ間隔は、スイッチの状態をサンプリングしており、チャタリングを除去するフィルターとして働きます。
    while(1) {
        cmt_.at_task().sync_100hz();

        sdc_.service();

        command_();
    }

簡単なシェル(SDカード操作)

  • 1/100 のループ中、シリアル入力も監視しており、バッファリングしています。
  • これを利用して簡単なコマンド操作として機能するようにしています。
    -「help」と打つと簡単なヘルプが表示されます。
  • 簡易的な実装で、厳密な入力のチェックを行っていません。
コマンド オプション 機能
ls -l ディレクトリのリスティング
cd カレントディレクトリの移動
pwd カレントディレクトリパスの表示
free SDカードの全容量、空き容量の表示
Start SD-CARD Access sample for 'RX72N' 240[MHz]
# ls
SoftShut.wad    TFALL.WAD       TIMER.WAD       TOFF.WAD        TON.WAD
VSOFF.WAD       VSON.WAD        TEST.BIN        System Volume Information/
inv_roms/       inv_wavs/       Dragon_Quest2_fix.nes
DragonQuest_J_fix.nes           Galaga_J.nes    GALAXIAN.NES    GRADIUS.NES
kfont16.bin     NoImage.jpg     Pac-Man.nes     Solstice_J.nes
Super_mario_brothers.nes        XEVIOUS.NES     HRC_logo_s.bmp
Super Mario USA (Japan).nes     Audios/
/ # cd Audios
/Audios # ls
AlbumArtSmall.jpg               Folder.jpg      Thumbs.db       AlfredBrendel/
CarpentersBest/ Chopin_Recital/ Denim/          DRACULA/        GeorgeMichael/
Glenn Gould/    JAnime/         KNOCK ON MY DOOR/               Make It Big/
Michael Jackson/                Mornin' After/  NarcisoYepes/
Over The Top_ Original Motion Picture Soundtrack/               PRIMO/
Rachmaninov Symphonies No_1-3 L_Maazel&Berliner Philharmoniker/
Rachmaninov_ Piano Concertos #2 & 3/            SoundTrack/
The Best Of Mission_ Impossible/
THE ORIGINAL~Songs for Scheherazade~/           Vienna Recital/
うる星やつら ジュークボックス/  のだめカンタービレ Best 100/
もってけ! セーラーふく/         イーハトーヴ交響曲/
コンピレーション/               スペシャル/     ドラゴンクエスト/
ボーカロイド/   ラフマニノフ_ ピアノ協奏曲 #1 & #2/
ラフマニノフ_ ピアノ協奏曲 #3 & #4/
ラフマニノフ_ ピアノ協奏曲第1番 & 第2番/        リシッツァ/
ルパン三世’71 ME TRACKS/           今井美樹/       仲道郁代/
倉木麻衣/       國府田マリ子/   坂本真綾/       堀江由衣/       太田貴子/
奥慶一/         宇多田ヒカル/   宮里久美/       工藤静香/       庄司紗矢香/
林原めぐみ/     椎名へきる/     相川七瀬/       西山ギター課題/ 金月真美/
/Audios # cd ..
/ # ls -l
     16384 May  4 2018 06:08  SoftShut.wad
     16384 May  4 2018 06:08  TFALL.WAD
     16384 May  4 2018 06:08  TIMER.WAD
     16384 May  4 2018 06:08  TOFF.WAD
     16384 May  4 2018 06:08  TON.WAD
     16384 May  4 2018 06:08  VSOFF.WAD
     16384 May  4 2018 06:08  VSON.WAD
   1048576 Jan  1 1980 09:00  TEST.BIN
           Nov  1 2017 07:25 /System Volume Information
           Mar 10 2019 07:37 /inv_roms
           Mar 10 2019 07:37 /inv_wavs
    131088 Feb 16 2017 03:00  Dragon_Quest2_fix.nes
     65552 Feb 16 2017 02:56  DragonQuest_J_fix.nes
     24592 Jun 21 2000 07:56  Galaga_J.nes
     40976 Feb 28 1997 17:26  GALAXIAN.NES
     65680 Apr 26 1997 01:53  GRADIUS.NES
    249824 Jun 25 2018 18:14  kfont16.bin
     49785 Nov 14 2018 06:58  NoImage.jpg
     24592 Dec 24 1996 20:32  Pac-Man.nes
    131088 Sep 20 1999 10:59  Solstice_J.nes
     40976 Jul  8 2018 23:59  Super_mario_brothers.nes
     40976 Jul 17 1997 23:31  XEVIOUS.NES
     86072 Sep  5 2018 19:57  HRC_logo_s.bmp
    262160 Jul  1 2018 12:29  Super Mario USA (Japan).nes
           Nov 18 2019 01:56 /Audios
Total 25 files
/ # free
1335360/15629312 [KB] (8.5%)
/ # write test.bin
Write: 'test.bin'
Write Open:  280 [ms]
Write: 395 KBytes/Sec
Write Close: 4 [ms]
/ # read test.bin
Read: 'test.bin'
Read Open:  1 [ms]
Read: 1171 KBytes/Sec
Read Close: 0 [ms]
/ # help
    ls [-l] [file]      list current directory (-l: long)
    pwd                 current directory path
    cd [file]           change current directory
    free                list disk space
    write filename      test for write
    read filename       test for read
    time [yyyy/mm/dd hh:mm[:ss]]   set date/time
  • write コマンドは、書き込み速度を計測します。
  • read コマンドは、読み込み速度を計測します。
  • time コマンドは、RTC への時間設定を行います。
  • RX65N Envision Kit/RX72N Envision Kit では、RTC は無効になっており利用出来ません。
  • サンプルには、I2C 接続の RTC を扱う場合を実装してあります。

Makefile での FatFs 設定

  • Makefile で、「-DFAT_FS -DFAT_FS_NUM=2」を設定し、コンパイル時に渡す必要があります。
    -「FAT_FS_NUM」は、POSIX の open 関数で同時にオープン出来るファイル数です。
  • 大きくすると、管理領域としてメモリを消費します。

まとめ

  • C++ のテンプレートクラスは、強力で、C 言語では実現できないような柔軟性を提供しています。
  • SDカードのインターフェースは、それなりに複雑ですが、サンプルでは、RSPI、ソフトSPI、SDHI に対応する方法を提示しています。
  • 異なるハードウェアーでも、同じように扱う工夫は、まさにオブジェクト指向的な概念によるものです。
  • この例からも、C++ が組み込みマイコンのプログラムに向いている事が判ると思います。
  • 問題なのは、C++ を学ぶのは、実際にはそれ程簡単では無く、ある程度の修練が必要です。
  • C++ は C 言語から派生したので、C 言語を自由に扱えるレベルの人でも、判ったつもりになっている人が多いようです。
  • 「C++ は C 言語とは全く違うプログラム言語である」ここから始める必要があると思います。
  • ただ、学ぶハードルは、それなりに低くなっており、色々な勉強会や、講習会に参加する事もできます。
  • 学ぶには、ある程度の時間や、修練は必要でしょうが、そのメリットは十分にあるものと思います。

RX72N Envision Kit での開発(その3)CMT 編

コンペアマッチタイマ・テンプレートの使い方(cmt_mgr.hpp)

前回の SCI 編で既に使っていますが、CMT を使う場合を、詳しく説明したいと思います。

CMT は一定間隔でイベントを発生するタイマーで、割り込みを発生させる事が出来ます。
通常、マスタークロックは、PCLKB が使われます。
PCLKB が 60MHz の場合、1.7Hz~7.5MHz 程度の時間間隔をプログラム出来ます。
※分周比は制限があり、自由な周波数を設定出来るわけではありません。

このテンプレートクラス(common/cmt_mgr.hpp)も、RX マイコンの C++ フレームワークの中では初期の段階から改修されてきているクラスです。
通常行いたい事を、最低限の手順で出来るように工夫してあります。

FreeRTOS のようなマルチタスクOSを使わない場合で、平行処理的な制御を扱う場合には欠かせない物です。
比較的簡単に平行処理的な概念を利用する事が出来るので、シンプルなアプリケーションには、逆に使いやすいものです。

RX マイコンでは、CMT は標準装備なので、どんな RX マイコンでも利用可能です。

通常 CMT0 から CMT3 までの4チャネルを使う事が出来ます。
※ICU の解説では、CMT0 は、OS 用で予約してあるのですが、OS(FreeRTOS と思われる)を使わない場合、どれを使っても問題無いと思えます。

FreeRTOS では、以下のように初期化しています。

    extern void vTickISR(void);
    extern void vSoftwareInterruptISR(void);

    void vApplicationSetupTimerInterrupt(void)
    {
        uint8_t intr = configKERNEL_INTERRUPT_PRIORITY;
        cmt_.start(configTICK_RATE_HZ, intr, vTickISR);

        device::icu_mgr::set_task(device::ICU::VECTOR::SWINT, vSoftwareInterruptISR);
        device::icu_mgr::set_level(device::ICU::VECTOR::SWINT, configKERNEL_INTERRUPT_PRIORITY);
    }

通常、タイマーの割り込み処理から呼ばれる関数をアタッチ出来ます。
C++ では、「ファンクタ」と呼ばれる手法を良く使います。
cmt_mgr も、割り込み内から呼ぶ関数を、ファンクタとして扱うようにしています。
cmt_mgr のプロトタイプは以下のようになっています。

    //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
    /*!
        @brief  CMT マネージャー・クラス
        @param[in]  CMT チャネルクラス
        @param[in]  TASK    タイマー動作クラス
    */
    //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
    template <class CMT, class TASK = utils::null_task>
    class cmt_mgr {

ここで、「TASK」はファンクタとして機能し、以下のように、() オペレータを使って動かす関数を定義しておきます。

    class my_cmt_task {
    public:
        void operator() () {
            // 処理
        }
    };

CMT1 を使い、cmt_mgr の定義は、上記クラスを使い、以下のようにします。

    typedef utils::cmt_mgr<device::CMT1, my_cmt_task> CMT;
    CMT     cmt_;

これで、割り込み処理で、この部分が処理されます。
※デフォルトでは、「utils::null_task」クラスが指定されます、これは、何もしないファンクタです。
※何もしないファンクタは、最適化で、完全に無くなります。
C++ のテンプレートでは、このような仕組みにする事で、処理する関数を呼び出すオーバーヘッドを節約出来ます。


CMT の起動

CMT は、間隔(周波数)と、割り込みレベルだけです。
※割り込みレベルが「0」の場合、割り込みを使いません。

    {
        uint8_t intr_lvl = 3;
        uint32_t freq = 1000;  // 1000Hz
        cmt_.start(freq, intr_lvl);
    }

割り込みとの同期

割り込み処理と、メイン側で同期を取りたい場合は、「sync()」を使います。

    while(1) {
        cmt_.sync();

    }

上記のループは、1000Hzの間隔でループする事になります。

割り込み回数の取得

また、割り込み回数が欲しいなら、「get_counter()」を使います。
※このカウンターは、外部割込み関数を登録した場合には無効になります。

    auto n = cmt_.get_counter();

※カウンターは32ビットで1周します。

設定周波数の取得(リアルな値)

「cmt_mgr」クラスでは、設定した周波数になるべく近い周期を生成しようと努力します。
ですが、マスタークロックに対して、割り切れない場合、実際の周期には誤差があります。

以下の関数で、設定した周波数と、実際に運用されている周波数を表示します。

    uint32_t freq = 850;
    cmt_.start(freq, 4);

    utils::format("Freq  (set): %u Hz\n") % cmt_.get_rate();
    utils::format("Freq (real): %u Hz\n") % cmt_.get_rate(true);
Freq  (set): 850 Hz
Freq (real): 849 Hz

C++ でのインスタンスの考え方

よく、例題などで、クラスのインスタンスを「new」を使って作成して、「delete」で廃棄する例を見ます。
また、C++ をあまり良く判っていない人は、インスタンスを作るコストが大きいので、C++ は小規模システム向きでは無いと思っている人が多いようです。
でも実際は、動的に作らなければ良いだけの事で、テンプレートなどを利用する事で、柔軟性があり、「動的な割り当て」をほぼ「0」に出来ます。

このような勘違いや思い込みは、C++ の場合非常に沢山あり、それが、C++ を敬遠する理由にもなっているようです。
※公開している RX マイコンフレームワークには「new、delete、malloc、free」は一切ありません。
※外部ライブラリで使われている場合があり、その対応としては多少あります。

    MyClass* my_class = new MyClass();

    my_class->xxx();

    delete my_class;

「new」すれば、必ず「delete」しなければならず、管理が面倒です、常に使う物は、単に宣言すれば良く、コンパイラが、メモリを自動で割り当てます。
※シェアードポインターで包む事で、廃棄忘れを防止出来ますが、「動的」に確保する必要が無い場合は、実態を書けば良いだけです。
※関数内の場合は、割り当てはスタック上ですが、関数外に置けば、ワークメモリに割り当ててくれます。

    {
        MyClass my_class;

        my_class.xxx();

    }

※上記の場合、MyClass の実態「my_class」は、スコープを抜ければ、自動で廃棄されます。

このように、C++ では、「割り当て」は極力減らし、それに伴うリスクを避ける事が出来ます。

RX72N Envision Kit で名機DX7を鳴らす

はじめに

ネットでDX7のエミュレーターを見つけたので、RX72N Envision Kit にポートしてみた。

DX7は、1983年5月に発売されたヤマハのシンセサイザーで、まだ、メモリの容量や価格が制限されていた時代に登場した画期的な名機で、FM 音源独特の、音を始めて聴いた時の感じを今でも覚えている。
現在は、サンプリングが主流となり、シンセサイザーの形態も様変わりしたが、今でも、その音色は色あせる事は無いと思う。

DX7は、6オペレーター、32アルゴリズムであるが、後年、それをスケールダウン(4オペレーター)した YM2151 が業務用ゲーム機やパソコンに採用された。

ただ、FM 音源方式では、自分が思った音色を作るのが、難しい難点があった。
※2151の時代でも、サウンドコンポーザが、音色を作るのに難儀していたのを覚えている。
※アタリのゲームで、口笛のような音を表現していて、ビックリした事があるw

ソースを眺める

まず、ソースコードを取って来て中身を見てみる。

どうやら、エンジン部は C++ のようで、Android で鳴らすのがメインのようだ。
※ARM/NEON 用に最適化されたコードも含まれているようだ。

いきなりターゲットで実験するのは大変そうなので、自分のフレームワーク glfw3_app で実験コードを作って実験してみる。

  • エンジンのソースで必要そうな物をコンパイル、リンクする。
  • 一応 C++ だけど、かなり痛い部分があるようだー、まぁこれは、後々修正するとして、とりあえず鳴らしてみないと・・
  • ※名前空間も定義されていないし、C++ 的に見ると、ツッコミ処が多いが、FM 音源のエンジンは優秀なのだろうと思う。
  • 自分は、警告をかなり厳しくしているので、駄目な部分は、引っかかって止まり、修正を繰り返す。

そんなこんなで、コンパイルが通り、リンクするまで来た。

早速、インスタンスを宣言して動かすのだが、勝手が判らないので、OS-X 用のソース(main.mm)があったので参考にした。
色々、しらべた結果、以下のようにする事が判った。(非常にシンプルなエンジンだ!)

使い方

        RingBuffer      ring_buffer_;
        SynthUnit       synth_unit_(&ring_buffer_);
  • 二つのインスタンスを定義。
    -「RingBuffer」は、MIDI データを食わせるクラスで、ノートのOn/Offなどほぼ全ての機能は、それで完結するようだ。
    -「SynthUnit」が本体のエンジンとなっている。
  • 初期化時、「RingBuffer」のポインターを食わせる。(ポインターなのが、駄目駄目w)
    SynthUnit::Init(44100);
  • 初期化を行う。
  • エンジンで運用するサンプリング周波数を設定する。
    const uint32_t n = 44100 / 60;
    int16_t wave[n];
    synth_unit_.GetSample(wave, n);
  • glfw3_app の「updata」ループ(60Hz)で、一定時間間隔でサービスする。
  • GetSample を呼び出す事で、エンジンで生成した波形(モノラル)を取得する。
  • この波形データを、システムの発音ジェネレータにキューイングする。

たったこれだけ・・・

鳴らしてみる

とりあえず、音を鳴らしてみたいが、どうするのか判らないのでエンジンのソースを見てみる。

どうやら、MIDI のノートを送れば、発音するようだ。

そこで、以下のコードを用意して、キーボードを押して鳴らしてみた。

            for(int i = 0; i < 13; ++i) {
                if(key_[i] && !key_back_[i]) {
                    uint8_t key[3] = { 0x90, 0x3C, 0x7F };
                    key[1] = 0x3C + i;
                    ring_buffer_.Write(key, 3);
                }
                if(!key_[i] && key_back_[i]) {
                    uint8_t key[3] = { 0x80, 0x3C, 0x7F };
                    key[1] = 0x3C + i;
                    ring_buffer_.Write(key, 3);
                }
            }

key_[] には、キーボードを押した状態が、「true」、「false」で入っている。

鳴った!、思った通りの音だ!

和音の響きも良い!


実験用アプリ:

※このアプリでは、MIDI キーボードを繋いで、キーボードから鳴らす事が出来るようにしている。

音色の変更(program change)

DX7 には、標準で32くらいはプリセットがあったと思い、音色を変更してみた。

            if(dev.get_positive(gl::device::key::_1)) {
                uint8_t tmp[2];
                tmp[0] = 0xc0;
                tmp[1] = 1;
                ring_buffer_.Write(tmp, sizeof(tmp));
            }

「1」のキーを押すと、1の音色

しかし、何も鳴らない・・・

調べると、初期の状態では、0番にしか、音色が設定されていなかった。

ネットを探すと、DX7 用の音色ファイルが沢山ある事が判った。
そこで、「DX7_0628.SYX」なるファイルを見つけて、ダウンロードした。

この音色、どうやって組み込むのか?

エンジンコアのソースを読むと、単に MIDI データとして渡す事が判った。

        utils::file_io fin;
        if(fin.open(file, "rb")) {
            uint8_t tmp[4096 + 8];
            if(fin.read(tmp, sizeof(tmp)) == sizeof(tmp)) {
                ring_buffer_.Write(tmp, sizeof(tmp));
            }
        }
  • 1音色128バイトで、32音色が1セットになっている。
  • 8バイトはヘッダーとサム(2バイト)で、サムは、検証していないようだ。

これで、音色を変えて発音できる事が判ったー


RX72N にポートしてみる。

RX マイコン用にプロジェクトを作成して、ソースを持ってきて、コンパイルしてみた。

  • RX マイコン用に、少し修正。
  • 修正は、そんなに多くは無いが、やはり鬼門の「define」・・・
  • #define N (1 << LG_N) 「N」は駄目!、当たる・・・

修正して、コンパイルし、エラーが無くリンクが通ったー

早速、鳴らしてみる。

普通に鳴る~

ターミナルを接続して、'z' から ','、's' から 'j' を押すと、1秒間発音する。

改造

  • RX72N では鳴るが、RX65N だと、メモリが厳しい・・
  • 少し調べると、sawtooth.cpp で、メモリを多く消費しているようだ。
  • とりあえず、「#define LG_N_SAMPLES 10」と分解能として10ビットになっており、その配列がある。
  • 上記パラメーターは二次元配列になっている。
#define N_SLICES 36

int32_t sawtooth[N_SLICES][N_SAMPLES];

となっているので、10ー>9にしてみた、聴いた感じ、音の劣化を感じなかったので、とりあえず、それで・・

処理負荷

  • 一応、120MHz の RX65N Envision Kit でも動作するのだが、120MHz では厳しいようだ。
  • 同時発音数が増えると、簡単に処理オーバーする。
  • かと言って、RX72N の 240MHz でも余裕とは言えない。
  • DSP 命令とかを駆使すれば、波形の合成処理を最適化出来るかもしれない。
  • この感じだと、ゲームやアプリに組み込んで、効果音や BGM として鳴らすには厳しいかもしれない。
  • 一応、RX65N の場合、発音数は8(通常16)にしてある。

※現状のコードはプッシュ済み

スタンダード MIDI ファイルをパース

「鳴る」事は判ったので、何か演奏させてみようと思い、スタンダード MIDI ファイルのパースをしてみる。

簡単だと思ったら、結構面倒そうで、今回はここまで・・・

※自分で作らなくても、MIDI シーケンサくらいは、色々ありそう、ちょっと探してみるか・・・

RX72N Envision Kit でオーディオプレイヤー

はじめに

以前、RX65N Envision Kit で作っていた、オーディオプレイヤーを、RX72N Envision Kit でも動作するようにした。
※GUI は無いが、RX64M でも動作する。

また、操作方法を見直して、GUI での一般的な操作で出来るようにした。
※ファイラーは、多少独特だが、タッチ操作で完結する。

全体的にかなり色々修正、マルチプラットホームで共通化できるように色々な面で改修を行った。

以前は、オーディオインターフェースとして内蔵 D/A を利用していたが、SSIE を使った I2S 出力をサポートした。
※RX72N Envision Kit では、I2S からアナログに変換するデバイスが標準搭載された。

FreeRTOS で、オーディオファイルのデコードと、GUI 操作を別タスクで動かすようにした。

ソースコードなど一式

AUDIO_sample

ソースコードは Github にプッシュしてある。

※コンパイルするには、RX フレームワーク全体が必要なので、RX プロジェクトをクローンする必要がある。
※MSYS2 による、RX マイコン用 gcc をビルドする必要がある。
※Renesas の統合環境(CC-RX、GNU-RX)では、コンパイルする事は出来ない。

全体の構成

全体は、以下のモジュールで構築されている。(-O3 の最適化で、バイナリー、900キロバイトある)

  • オーディオプレイヤー本体(main.cpp、audio_gui.hpp)
  • libmad (MP3 のデコードを行う)
  • libpng (PNG 画像のデコードを行う)
  • zlib (libpng が利用する)
  • picojpeg (JPEG 画像のデコードを行う)

ハードウェアーの構成

  • RX65N Envision Kit、RX64M ではチップ内蔵の12ビットD/Aからオーディオ出力する。
  • RX72N Envision Kit では、内蔵オーディオから出力する。
  • RX64M では、D/A 出力、SD カードハードウェアー、シリアル入出力などを接続する必要がある。

※RX65N Envision Kit では、D/A 出力を出して、アンプを入れたり、SD カードソケットを取り付ける改造が必要となる。
※RX72N Envision Kit では、改造は一切必要なく、SD カードにオーディオファイルを用意するだけ。
※RX64M は、DIY ボード向けのものになっている。(GR-KAEDE で動かすには、色々なポートの設定などを変更する必要がある)

各ハードウェアー基本設定 (main.cpp)

RX64M DIY:

  • 水晶発振子 12MHz
  • インジケーター LED 、PORT0、B7
  • コンソール接続 SCI は SCI1
  • SD カード MISO、PORTC、B3
  • SD カード MOSI、PORT7、B6
  • SD カード SPCK、PORT7、B7
  • SD カード選択、PORTC、B2
  • SD カード電源制御、PORT8、B2、アクティブ Low
  • SD カード検出、PORT8、B1
  • SD カードの書き込み禁止は使わない
  • SDHI インターフェースによる SD カード制御(候補3のポートマップ)
  • D/A 出力用波形バッファのサイズ指定(8192、1024)
    ※RX64M DIY ボードでは、SD カードのインターフェースとして、ソフト SPI を使っている。
#if defined(SIG_RX64M)
    typedef device::system_io<12'000'000> SYSTEM_IO;
    typedef device::PORT<device::PORT0, device::bitpos::B7> LED;
    typedef device::SCI1 SCI_CH;
    static const char* system_str_ = { "RX64M" };

    // SDCARD 制御リソース
    typedef device::PORT<device::PORTC, device::bitpos::B3> MISO;
    typedef device::PORT<device::PORT7, device::bitpos::B6> MOSI;
    typedef device::PORT<device::PORT7, device::bitpos::B7> SPCK;
    typedef device::spi_io2<MISO, MOSI, SPCK> SDC_SPI;  ///< Soft SPI 定義
    SDC_SPI sdc_spi_;
    typedef device::PORT<device::PORTC, device::bitpos::B2> SDC_SELECT;   ///< カード選択信号
    typedef device::PORT<device::PORT8, device::bitpos::B2, 0> SDC_POWER; ///< カード電源制御
    typedef device::PORT<device::PORT8, device::bitpos::B1> SDC_DETECT;   ///< カード検出
    typedef device::NULL_PORT SDC_WPRT;  ///< カード書き込み禁止
    typedef fatfs::mmc_io<SDC_SPI, SDC_SELECT, SDC_POWER, SDC_DETECT, SDC_WPRT> SDC;
    SDC     sdc_(sdc_spi_, 25'000'000);

    // マスターバッファはでサービスできる時間間隔を考えて余裕のあるサイズとする(8192)
    // DMAC でループ転送できる最大数の2倍(1024)
    typedef sound::sound_out<int16_t, 8192, 1024> SOUND_OUT;
    static const int16_t ZERO_LEVEL = 0x8000;

    #define USE_DAC

RX65N Envision Kit:

  • 水晶発振子 12MHz
  • インジケーター LED 、PORT7、B0
  • コンソール接続 SCI、SCI9
  • SD カードの電源制御、PORT6、B4、アクティブ Low
  • SD カードの書き込み禁止は使わない
  • SDHI インターフェースによる SD カード制御(候補3のポートマップ)
  • D/A 出力用波形バッファのサイズ指定(8192、1024)
#elif defined(SIG_RX65N)
    /// RX65N Envision Kit
    typedef device::system_io<12'000'000> SYSTEM_IO;
    typedef device::PORT<device::PORT7, device::bitpos::B0> LED;
    typedef device::SCI9 SCI_CH;
    static const char* system_str_ = { "RX65N" };

    typedef device::PORT<device::PORT6, device::bitpos::B4, 0> SDC_POWER; ///< '0'でON
    typedef device::NULL_PORT SDC_WP;       ///< 書き込み禁止は使わない
    // RX65N Envision Kit の SDHI ポートは、候補3で指定できる
    typedef fatfs::sdhi_io<device::SDHI, SDC_POWER, SDC_WP, device::port_map::option::THIRD> SDC;
    SDC         sdc_;

    // マスターバッファはでサービスできる時間間隔を考えて余裕のあるサイズとする(8192)
    // DMAC でループ転送できる最大数の2倍(1024)
    typedef sound::sound_out<int16_t, 8192, 1024> SOUND_OUT;
    static const int16_t ZERO_LEVEL = 0x8000;

    #define USE_DAC
    #define USE_GLCDC

RX72N Envision Kit:

  • 水晶発振子 16MHz
  • インジケーター LED 、PORT4、B0
  • コンソール接続 SCI、SCI2(内蔵 USB シリアルインターフェース)
  • SD カードの電源制御、PORT4、B2、アクティブ High
  • SD カードの書き込み禁止は使わない
  • SDHI インターフェースによる SD カード制御(候補3のポートマップ)
  • SSIE 出力用波形バッファのサイズ指定(8192、1024)
#elif defined(SIG_RX72N)
    /// RX72N Envision Kit
    typedef device::system_io<16'000'000> SYSTEM_IO;
    typedef device::PORT<device::PORT4, device::bitpos::B0> LED;
    typedef device::PORT<device::PORT0, device::bitpos::B7> SW2;
    typedef device::SCI2 SCI_CH;
    static const char* system_str_ = { "RX72N" };

    typedef device::PORT<device::PORT4, device::bitpos::B2> SDC_POWER;    ///< '1'でON
    typedef device::NULL_PORT SDC_WP;  ///< カード書き込み禁止ポート設定
    // RX72N Envision Kit の SDHI ポートは、候補3で指定できる
    typedef fatfs::sdhi_io<device::SDHI, SDC_POWER, SDC_WP, device::port_map::option::THIRD> SDC;
    SDC         sdc_;

    // マスターバッファはサービスできる時間間隔を考えて余裕のあるサイズとする(8192)
    // SSIE の FIFO サイズの2倍以上(1024)
    typedef sound::sound_out<int16_t, 8192, 1024> SOUND_OUT;
    static const int16_t ZERO_LEVEL = 0x0000;

    #define USE_SSIE
    #define USE_GLCDC

描画ハードウェアー設定(RX65N/RX72N Envision Kit)audio_gui.hpp

  • LCD_DISP、LCD の選択
  • LCD_LIGHT、LCD バックライト
  • LCD_ORG、描画ハードウェアー、GLCDC 開始アドレス
  • FT5206_RESET、タッチパネルインターフェース、リセット信号
  • FT5206_I2C、タッチパネルインターフェース、SCI(I2C) ポート
#if defined(SIG_RX65N)
        typedef device::PORT<device::PORT6, device::bitpos::B3> LCD_DISP;
        typedef device::PORT<device::PORT6, device::bitpos::B6> LCD_LIGHT;
        static const uint32_t LCD_ORG = 0x0000'0100;
        typedef device::PORT<device::PORT0, device::bitpos::B7> FT5206_RESET;
        typedef device::sci_i2c_io<device::SCI6, RB64, SB64, device::port_map::option::FIRST_I2C> FT5206_I2C;
#elif defined(SIG_RX72N)
        typedef device::PORT<device::PORTB, device::bitpos::B3> LCD_DISP;
        typedef device::PORT<device::PORT6, device::bitpos::B7> LCD_LIGHT;
        static const uint32_t LCD_ORG = 0x0080'0000;
        typedef device::PORT<device::PORT6, device::bitpos::B6> FT5206_RESET;
        typedef device::sci_i2c_io<device::SCI6, RB64, SB64, device::port_map::option::THIRD_I2C> FT5206_I2C;
#endif

メイン部

今回 FreeRTOS を利用して、オーディオコーデックのデコード部と、GUI 操作部を分け、スレッドで平行動作させている。
FreeRTOS ベースなので、起動したら、二つのタスクを生成後、それらを起動する。

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

    {  // SCI 設定
        static const uint8_t sci_level = 2;
        sci_.start(115200, sci_level);
    }

    {  // SD カード・クラスの初期化
        sdc_.start();
    }

    utils::format("\r%s Start for Audio Sample\n") % system_str_;

    {
        uint32_t stack_size = 4096;
        void* param = nullptr;
        uint32_t prio = 2;
        xTaskCreate(codec_task_, "Codec", stack_size, param, prio, nullptr);
    }

    {
        uint32_t stack_size = 8192;
        void* param = nullptr;
        uint32_t prio = 1;
        xTaskCreate(main_task_, "Main", stack_size, param, prio, nullptr);
    }

    vTaskStartScheduler();
}

オーディオ・コーデック・タスク

  • name_t クラスを使って、GUI タスクから、再生ファイル名を受け取っている。
  • 受け取った名前は、コーデックマネージャーに渡して、オーディオ再生している。
    void codec_task_(void *pvParameters)
    {
        // オーディオの開始
        start_audio_();

        while(1) {
            if(name_t_.get_ != name_t_.put_) {
                if(strlen(name_t_.filename_) == 0) {
                    codec_mgr_.play("");
                } else {
                    if(std::strcmp(name_t_.filename_, "*") == 0) {
                        codec_mgr_.play("");
                    } else {
                        codec_mgr_.play(name_t_.filename_);
                    }
                }
                ++name_t_.get_;
            }
            codec_mgr_.service();

            vTaskDelay(10 / portTICK_PERIOD_MS);
        }
    }

操作 (GUI) タスク

  • GUI (GLCDC) を使う場合と、コンソールのみの場合を分けている。
  • GUI では、ファイル名が選択されたら、それを、コーデックのタスクに転送している。
  • GUI では、オーディオファイル再生時、再生経過時間を受け取って、表示に反映している。
  • シリアル入出力のコマンドライン操作をサポートしている。
  • SD カードの操作系をサービスしている(SD カードの抜き差しによるマウント操作など)
    void main_task_(void *pvParameters)
    {
        cmd_.set_prompt("# ");

        LED::DIR = 1;
#ifdef USE_GLCDC
        gui_.start();
        gui_.setup_touch_panel();
        gui_.open();  // 標準 GUI
        volatile uint32_t audio_t = audio_t_;
#endif
        while(1) {
#ifdef USE_GLCDC
            if(gui_.update(sdc_.get_mount(), codec_mgr_.get_state())) {
                // オーディオ・タスクに、ファイル名を送る。
                strncpy(name_t_.filename_, gui_.get_filename(), sizeof(name_t_.filename_));
                name_t_.put_++;
            }
            if(audio_t != audio_t_) {
                gui_.render_time(audio_t_);
                audio_t = audio_t_;
            }
#else
            // GLCDC を使わない場合(コンソールのみ)
            auto n = cmt_.get_counter();
            while((n + 10) <= cmt_.get_counter()) {
                vTaskDelay(1 / portTICK_PERIOD_MS);
            }
            if(codec_mgr_.get_state() != sound::af_play::STATE::PLAY) {
                cmd_service_();
            }
#endif
            sdc_.service();
            update_led_();
        }
    }

FreeRTOS 対応のシリアル入出力

  • FreeRTOS では、共有するシリアルの入出力を排他制御する必要がある。
  • あまり効率は良くないが、その対応をしている。
  • 非常に簡単な方法で、ロック用オブジェクトを作成して、それをロックしてからアクセスし、終わったらロックを外す。
  • 「volatile」を付ける事で、最適化されても、オブジェクトの操作が無効にならないようにしている。
    void sci_putch(char ch)
    {
        static volatile bool lock_ = false;
        while(lock_) ;
        lock_ = true;
        sci_.putch(ch);
        lock_ = false;
    }

    void sci_puts(const char* str)
    {
        static volatile bool lock_ = false;
        while(lock_) ;
        lock_ = true;
        sci_.puts(str);
        lock_ = false;
    }

    char sci_getch(void)
    {
        static volatile bool lock_ = false;
        while(lock_) ;
        lock_ = true;
        auto ch = sci_.getch();
        lock_ = false;
        return ch;
    }

同期オブジェクトを使った通信

  • オーディオコーデック側は、「状態」を生成している。
  • GUI 側は、その状態を見て、操作を切り替えている。
  • GUI 側は、本来、内蔵ハードウェアー(DRW2D)エンジンを使う事が望まれるが、簡潔に済ます為、ソフトウェアーでフレームバッファに直接描画している。
  • また、GUI 側から、コーデック側を制御するオブジェクトを定義してある。
        struct th_sync_t {
            volatile uint8_t    put;
            volatile uint8_t    get;
            th_sync_t() : put(0), get(0) { }
            void send() { ++put; }
            bool sync() const { return put == get; }
            void recv() { get = put; }
        };

※単方向で良いので、簡易な方法を使っている、これなら、オブジェクトを同時にアクセスする事が無いので、競合が発生しない。

送る側:(FF ボタンが押された場合)

            ff_.at_select_func() = [this](uint32_t id) {
                play_ff_.send();
            };

受け取る側:(コーデックの制御を行う)

            if(!play_ff_.sync()) {
                play_ff_.recv();
                return sound::af_play::CTRL::NEXT;
            }

最後に

かなり、ツギハギ感があるが、とりあえず、何とかなっている。

実質的なソースコードは、main.cpp と audio_gui.hpp しか無いが、フレームワークが提供するクラスを色々使っている。

C++ テンプレートや、C++ クラスの場合、ある程度の汎用性をかなり簡単に実現できる為と思う。

複数のプラットホームで共有できるのも、テンプレートクラスに依る部分が大きいと思える。

多くは新規に実装した物もあるが、他のオープンソースを多く利用して実現している、良い時代だ~

RX72N Envision Kit での開発(その2)SCI 編

シリアルコミュニケーション

組み込みマイコンでは、最も手軽に、外部との通信を行えるインターフェースであると言えます。

このフレームワークでは、common/sci_io.hpp (device::sci_io テンプレートクラス)として、「良く使うだろう」場面を想定して実装されています。
C++ テンプレートを使って最初に作り始めたクラスでもあり、感銘深いものがあります。
現在は、その時実装した時から既に数年は経過していて、細かい部分を色々改善して現在に至っています。
なので、この実装は、組み込みマイコンと C++ の親和性が良く発揮されるものだと思います。

まず、「sci_io.hpp」をインクルードします。

#include "common/sci_io.hpp"

C++ では、ヘッダーに全ての実装を書く事が出来、ソースファイルの指定や、ライブラリをリンクする必要は無く、メインのソースにヘッダーをインクルードするだけです。
※当然ですが、使わなければ(実態になる物を定義しなければ)、余分なメモリも消費しません。

SCI を使う為に必要な定義は、以下のようなものです。

    typedef device::SCI2 SCI_CH;  // SCI チャネルの定義
    typedef utils::fixed_fifo<char, 512> RXB;  // RX (RECV) バッファの定義
    typedef utils::fixed_fifo<char, 256> TXB;  // TX (SEND) バッファの定義
    typedef device::sci_io<SCI_CH, RXB, TXB> SCI;  // sci_io の定義

    SCI     sci_;
  • 利用する SCI チャネルを指定します。
  • 受信、送信バッファのサイズを指定します。(16バイト以上が必要)
  • 何も指定しないと、「第一候補」のポートが選択されます。
  • sci_io クラスの定義を typedef して、実態を記述します。

この場合、SCI2 を使い、受信バッファに 512 バイト、送信バッファに 256 バイトを割り当てています。
「バッファ」は、FIFO(First In First Out)で、リングバッファになっていて、固定長です。
※組み込みマイコンの制御では非常に多く利用する頻度があり、組み込み用に実装した専用クラスですが、別にこのクラスを使わなくても、自分でカスタムしたクラスを使う事も出来ます。

RX マイコンでは、SCI2 で利用できるポートが複数あります。
何も指定しないと、port_map クラスで指定されている、第一候補が選択されます。
※これは、sci_io テンプレートのプロトタイプが以下のようになっている為です。

template <class SCI, class RBF, class SBF, port_map::option PSEL = port_map::option::FIRST, class HCTL = NULL_PORT>
class sci_io {

また、SCI のポートモード設定を自分で行いたい場合は、「port_map::option::BYPASS」を選択する事もできます。
この指定を行うと、ポートのモード設定を「バイパス」して何も行われません。

port_map クラス内では、以下のようになっていて、P13(TXD)、P12(RXD) が使われます。

           uint8_t sel = enable ? 0b001010 : 0;
           PORT1::PMR.B3 = 0;
           MPC::P13PFS.PSEL = sel;  // TXD2/SMISO2/SSCL2 (P13 LQFP176: 52)
           PORT1::PMR.B3 = enable;
           PORT1::PMR.B2 = 0;
           MPC::P12PFS.PSEL = sel;  // RXD2/SMOSI2/SSDA2 (P12 LQFP176: 53)
           PORT1::PMR.B2 = enable;

第二候補を選択する場合、SCI の typedef を以下のようにします。

     typedef device::sci_io<SCI_CH, RXB, TXB, device::port_map::option::SECOND> SCI;

この場合、P50(TXD)、P52(RXD) となります。(詳しくは port_map.hpp 参照の事)

定義が出来たら、SCI を使える状態にします。
※ SCI の省電力切り替え等も内部で自動的に行われます。

    {  // SCI の開始
        uint8_t intr = 2;        // 割り込みレベル
        uint32_t baud = 115200;  // ボーレート
        sci_.start(baud, intr);
    }

これで、115200 bps で受信も送信も出来る状態になります。
ボーレートは整数で指定します、もし、内部分周器の能力を超えた場合(設定出来ない場合)「false」を返して失敗します。
RX マイコン内蔵の SCI は、チャネルによってベースとなるクロックが異なる場合があります。
※PCLKA、PCLKB
SCI の各チャネルの定義内には、どのクロックを使うかなどの情報が内包されていて、ボーレートを計算する時にそのクロック値を使う為、どのチャネルでも、全く同じように使えます。
割り込みレベルは、「2」を使っていますが大きな理由はありません。
※割り込みレベルのポリシーは、システム全体で考える必要のある問題なので、ここでは詳しい方法は延べません。

sci_io クラス内部は、色々な計算、条件分岐などあり、内部レジスタに値を直接入れる事に執着する C プログラマーがいます。
しかし、sci_io はテンプレートなので、コンパイル時に行う最適化により、実行時に必要無い計算や分岐は極限まで排除されます。
※アセンブラコードを見ると良く判ります。
※最適化は、「人」がするものでは無く、「マシン」が行うべき問題です。(もちろん例外はあります)


通信フォーマットは、何も指定しないと、8ビット、1ストップビットになります。
変更したい場合は、以下のように指定します。(8ビット、Even、2ストップビットの場合)

    sci_.start(baud, intr, SCI::PROTOCOL::B8_E_2S);

※PROTOCOL は、sci_io.hpp 内で定義されているものが使えます。

後は、データを送信したり、受信したりするだけです。

    auto ch = sci_.get_ch();  // 1 文字を受信

    sci_.put_ch(ch);  // 1 文字送信

    auto len = sci_.recv_length();  // 受信バッファに格納されている文字数

このように、C++ テンプレートクラスを使ったフレームワークでは、複雑な設定を隠蔽して、アプリケーションを実装する事が出来ます。
別プログラムで、設定を生成する事も無く簡単に扱えると思います。

文字を出力する仕組み

通常、printf などを使った場合、データはどのように処理されているのでしょうか?
※C++ では iostream を使います。
POSIX では「標準出力」と呼ばれる物が設定されており、そこに出力するようになっています。
これは、ファイルディスクリプタで、ファイルに書く事と同じ扱いです。

通常、アプリケーションをコンパイルすると、「libc.a」と呼ばれるライブラリがリンクされます。
この中に、ファイルとのやりとりを行う関数が内包されており、OS の制御下に置かれています。
組み込みでは、通常、この仕組みは使わないので、自分で用意する必要があります。

common/syscalls.c

に、libc.a に代わる組み込み専用の仕組みを用意しています。(syscalls.c のコンパイルと、リンクが必要)

実際にする事は、SCI の出力と、繋ぐ実装をするだけです。

main.cpp 内に、以下のように実装します。

extern "C" {

    // syscalls.c から呼ばれる、標準出力(stdout, stderr)
    void sci_putch(char ch)
    {
        sci_.putch(ch);
    }

    void sci_puts(const char* str)
    {
        sci_.puts(str);
    }

    // syscalls.c から呼ばれる、標準入力(stdin)
    char sci_getch(void)
    {
        return sci_.getch();
    }

    uint16_t sci_length()
    {
        return sci_.recv_length();
    }
}

これで、printf で文字を出力すると、SCI に出力される事になります。

C++ では printf を使わない

printf は便利ですが、重大な欠点があります。
それは可変引数を使ってパラメータを受け渡す為、スタックを経由する点です。
この問題については、ここでは詳しく述べませんので、興味があればご自分で調べてみて下さい。

C++ では、それに代わって、文字を扱う方法として、iostram クラスを使う方法が推奨されています。
iostream クラスは、便利で強力なクラスなのですが、組み込みマイコンのような環境では別の問題が発生します。

それは、メモリ消費が大きい事。

    std::cout << "Hello!" << std::endl;

   text    data     bss     dec     hex filename
 508864   47644    8812  565320   8a048 hello.elf

---
    printf("Hello!\n");

   text    data     bss     dec     hex filename
  13864      48    1924   15836    3ddc hello.elf

上記は、RX マイコンで、iostream と、レガシーな printf を使った場合のメモリ消費の違いです。

これでは、流石に、「常用」するには、問題があります。

そこで、printf に近い使い方が出来て、メモリサイズが小さくなるクラスを実装してあります。
元々は、「boost/format.hpp」のアイデアを真似て作り始めた物ですが、現在は色々な機能を入れてあります。
サイズもそこそこ小さくなります、通常の使い方では、ほぼ printf と遜色なく使えると思います。

    utils::format("Hello!\n");

   text    data     bss     dec     hex filename
   6700      48    2136    8884    22b4 hello.elf

プロジェクトは、別にありますが、common/format.hpp にコピーしてあります。
詳しくは、format.hpp プロジェクトを参照して下さい。

詳しくは、上記プロジェクトを参照してもらえばと思います。
※このクラスは、他の環境(VC、mingw64、Linux)でもインクルードするだけで普通に使えます。

たとえば、以下のように使えます。

    int a = 1000;
    utils::format("%d\n") % a;

実装がヘッダーに集中する事による大きなメリット

C++ で最も改善されたと思う一例は、ヘッダーと実装を分ける必要性が無くなった事にあります。

しかし、現実には、C++ なのに、ヘッダーとソース(実装)に分けている人が多いようです。
※典型的なのは、Arduino のスケッチなど

ヘッダーに実装を書く事で、コンパイル時に全てのコードが評価される為、インクルードヘッダーが多くなると、コンパイル時間が多くかかると思っている人がいると思います。
ですが、実際には、その逆で、全体のコンパイル時間は、ソースの数が多い程その差は大きく、極端に短くなります。

人間の感覚や常識は当てにならないものです。

C++ のクラスをソースに分けて実装する場合、冗長な書き方も必要になり、非常に面倒です。

また、全てがヘッダーにあるので、管理が非常に簡単になり、何かの機能を持ちだす場合など、ヘッダーが一つあれば済むので、コピー忘れなどが減り、修正した場合なども二重に管理する必要もありません、そして、ソースをプロジェクトに追加する必要が無いので利便性が増します。

良い事ずくめなのですが、どんな実装方法でも可能な訳ではなく、「コツ」のような物は必要です。
自分のフレームワークでは、ほぼ、ヘッダーのみなので参考にしてもらえればと思います。

SCI_sample サンプルプログラムの使い方

SCI の使い方を大まかに扱ったサンプルを用意してあります。

SCI_sample

RX72N Envision Kit では、

W10./d/Git/RX % cd SCI_sample
W10./d/Git/RX/SCI_sample % ls
main.cpp  README.md  READMEja.md  RX24T  RX64M  RX65N  RX66T  RX71M  RX72N
W10./d/Git/RX/SCI_sample % cd RX72N
W10./d/Git/RX/SCI_sample/RX72N % make

...

W10./d/Git/RX/SCI_sample/RX72N % ls
Makefile  release  sci_sample.elf  sci_sample.lst  sci_sample.map  sci_sample.mot
  • RX72N Envision Kit の CN8 USB ポートと、PC をマイクロ USB ケーブルで接続する。
  • TeraTerm などのターミナルソフトを起動する。
  • ルネサスの COM ポート、115200 Baud、8 ビット、1 ストップビットに設定。
  • sci_sample.mot をターゲットに書き込みます。

※ PC に、ルネサス社の USB シリアルドライバーがインストールされている必要がありますが、Flash Programmer v3 をインストールする際にインストールされる。

# Start SCI (UART) sample for 'RX72N' 240[MHz]
Baud rate (set):  115200
Baud rate (real): 115355 (0.13 [%])
CMT rate (set):  100 [Hz]
CMT rate (real): 100 [Hz] (0.00 [%])
#

※設定ボーレートと実際のボーレート表示
RX マイコンの SCI は、ボーレートクロック生成の分周器が粗いので、高いボーレートでは、誤差がそれなりにあります。
MDDR ビットレート補正を使って、設定周期に近い値に調整してはいるけど、115200 では、上記値が限界となっています。
歩調同期式の場合、1バイトの転送で、最後のストップビット(10ビット分)までで、ズレが許容されれば、エラーは出ないので、上記の誤差は問題無いと思います。

  • 115200 = 8.7マイクロ秒
  • 8.7 x 10 x 0.0013 = 0.113 マイクロ秒
  • 許容範囲は、8.7 の 2/5 と考えると、3.5 マイクロ秒なので、十分な誤差範囲と言えると思う。

このサンプルでは、キーボードから入力された文字をエコーバックします。
また、「RETURN」キーを押すと、入力された文字列を表示します。

Just another WordPress site