はじめに
以前、RX65N Envision Kit で作っていた、オーディオプレイヤーを、RX72N Envision Kit でも動作するようにした。
※GUI は無いが、RX64M でも動作する。
また、操作方法を見直して、GUI での一般的な操作で出来るようにした。
※ファイラーは、多少独特だが、タッチ操作で完結する。
全体的にかなり色々修正、マルチプラットホームで共通化できるように色々な面で改修を行った。
以前は、オーディオインターフェースとして内蔵 D/A を利用していたが、SSIE を使った I2S 出力をサポートした。
※RX72N Envision Kit では、I2S からアナログに変換するデバイスが標準搭載された。
FreeRTOS で、オーディオファイルのデコードと、GUI 操作を別タスクで動かすようにした。
ソースコードなど一式
ソースコードは 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++ クラスの場合、ある程度の汎用性をかなり簡単に実現できる為と思う。
複数のプラットホームで共有できるのも、テンプレートクラスに依る部分が大きいと思える。
多くは新規に実装した物もあるが、他のオープンソースを多く利用して実現している、良い時代だ~