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 + GUI

http://www.rvf-rc45.net/wordpress/?p=3720

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 GNU-RX 8.3.0 をインストールして利用する。
※Renesas の統合環境(CC-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++ クラスの場合、ある程度の汎用性をかなり簡単に実現できる為と思う。

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

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