RX72N Envision Kit を使った音楽プレイヤー




はじめに

  • RX72N Envision Kit はまだ流通量が少ないようで入手が難しいようですが、CPは高いので、何かガジェットを作るのなら、是非入手した方が良いと思います。
  • 今回は、RX65N/RX72N Envision Kit、RX64M(DIY) ボードなどで動作可能なオーディオプレイヤーの紹介です。
  • 内部的な動作や要点を簡単に説明してあります、「音」と「画像」を扱う場合の参考に出来ると思います。

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

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

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

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

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

ソースコードなど一式

github AUDIO_sample project

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

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

全体の構成

全体は、以下のモジュールで構築されています。(-O3 の最適化で、バイナリー、900キロバイトくらいあります。)
- オーディオプレイヤー本体(main.cpp、audio_gui.hpp)
- FatFS (SD カードのファイルアクセス)
- 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 で動かすには、色々なポートの設定などを変更する必要があります。)


※RX65N Envision Kit で D/A 出力などの配線。

各ハードウェアー基本設定 (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_;
            }
                        cmd_service_();
#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++ クラスの場合、ある程度の汎用性をかなり自由に実現できる為と思います。

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

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


ライセンス

Audio Player: (MIT open source license)

FreeRTOS: (MIT open source license)
FatFs: BSD like
libmad: See libma/libmad/COPYRIGHT (G.P.L. v2)
libpng: See libpng/libpng/LICENSE (libpng license)
zlib: (zlib License)
picojpeg: Public domain, Rich Geldreich [email protected]