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++ クラスの場合、ある程度の汎用性をかなり簡単に実現できる為と思う。

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

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