「R8C」カテゴリーアーカイブ

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

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

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

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

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

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

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

PSG とは?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

初期化

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

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

    start_audio_();

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

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

メインループ

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

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

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

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

楽曲の定義

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

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

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

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

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

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

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

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

まとめ

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

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

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

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

R8C で AD9851を試してみる

以前に、周波数シンセサイザ、AD9833 を試していたが、より高い周波数に対応した、AD9851も試してみた。
※最近RXマイコンばかりで久しぶりにR8Cを触った、このような実験には、小回りが利いて便利だ。

値段はかなり高く、モジュールで3500円程だった。

買ってから気がついたが、このICはサイン波のみで、三角波はサポートしていない。
※矩形波はコンパレーターがあるので作れるだろうか・・

ただ、内部は最大180MHzで駆動できる為、出力できる周波数を高く設定でき、周波数ステップも細かく設定可能。
※AD9850は最大125MHz
※AD9833は最大25MHz
※AD9851では、電源電圧により、最大動作周波数が異なる

モジュールでは、出力にLCRを使ったローパスフィルタが組んであるのだが、出力する周波数によって振幅が小さくなるので、結構扱いが面倒だ・・
※10MHzだと減衰がかなり大きい。
この手のICを実用的に使うとなると、一番ネックになるのが、出力の扱いだと思う。
中心をGNDにして、+-で振幅させたいとか、出力振幅やオフセットを設定したいとかするには、外部に何らかの回路を付けたいが、マイコンで制御できるようにするには、意外と単純では無い。

いつものようにテンプレートライブラリとしたが、周波数の計算で、倍精度の浮動小数点を使っている。
本来整数計算だけで出来ると思うが、参考にしたライブラリの手法をそのまま流用した、時間がある時にでも考えてみたい。

ICの制御は基本4本の制御線が必要で、外部基準発信器をどのようにするかを設定出来るようにしてある。

//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
/*!
    @brief  AD985X テンプレートクラス
    @param[in]  D7      ポート・クラス
    @param[in]  W_CLK   ポート・クラス
    @param[in]  FQ_UD   ポート・クラス(FQ_UpdDate)
    @param[in]  RESET   ポート・クラス
    @param[in]  BASEC   ベースクロック(AD9850:125, AD9851:180)
*/    //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
template <class D7, class W_CLK, class FQ_UD, class RESET, uint32_t BASEC>
class AD985X {

...
public:
    //-----------------------------------------------------------------//
    /*!
        @brief  レジスターを設定
        @param[in]  w0      W0 レジスター値
        @param[in]  freq    周波数
     */
    //-----------------------------------------------------------------//
    void set_reg(uint8_t w0, float freq)


};

サンプルでは、外部OSCが30MHzで、内部の6倍PLLを有効にする。

    // P1_0(20):
    typedef device::PORT<device::PORT1, device::bitpos::B0> D7;
    // P1_1(19):
    typedef device::PORT<device::PORT1, device::bitpos::B1> W_CLK;
    // P1_2(18):
    typedef device::PORT<device::PORT1, device::bitpos::B2> FQ_UP;
    // P1_3(17):
    typedef device::PORT<device::PORT1, device::bitpos::B3> RESET;

    // 180MHz
    typedef chip::AD985X<D7, W_CLK, FQ_UP, RESET, 180> AD9851;
    AD9851  ad9851_;



    {  // AD9851 開始
        ad9851_.start();
        ad9851_.reset();
    }




    char tmp[32];
    command_.get_word(1, sizeof(tmp), tmp);
    float a = 0.0f;
    if((utils::input("%f", tmp) % a).status()) {
        ad9851_.set_reg(0b00001001, a);  // Phase: 1, PLL 6x
    } else {
        error = true;
    }

市販の周波数ジェネレーターはそれなりに高いので、安価な実験用発信器が欲しかったのだが、実用的な物にするにはそれなりの工夫が必要で、それなりに考える必要がある。
AD9851は内部180MHz動作なのだが、三角波も出せないので、AD9833の方が良いのかもしれない・・・

AD985X.hpp
github AD9851_sample