DMA式 WAVプレーヤー
(→プロジェクトファイル(Harmony Ver.2.04版 ) ダウンロード)
ここで紹介するWAVプレーヤは、SDカードの中にあるWAVファイルのサウンドデータをPIC32MZのRAMにSPIで読込み、 このデータを 44.1KHz毎に別のSPIバッファに書き込んで、外付けのDAコンバータ(MCP4922)に転送して演奏しています。 DMA転送は RAMに読み込まれたデータをSPIのバッファーに書き込む部分で行われています。 |
<仕様> | |||||||||||||||||
・ | SDカードにあるWAVファイルを再生するプレーヤーをPIC32MZを用いて製作すること | ||||||||||||||||
・ | 再生対象のWAVファイルは、ステレオ16ビット、44.1KHzのみとする。 | ||||||||||||||||
・ | SPIモジュールのSPI4を用いて12ビットDAコンバータMCP4922にLチャンネルのデータのみのデータを転送、再生すること | ||||||||||||||||
・ | WAVファイルの読み出し、DAコンバータへの書き出しは以下のような”ダブルバッファ方式”とすること ➀SDカードの読み出しは SPI6とし、サウンドデータ読み出しのバッファは2個(仮称 バッファーA, バッファーB)とし、交互にデータを読みこむこと。 ➁バッファA、バッファBから44.1KHz毎にSPI4のバッファに書き出す ③バッファAへの読み込みはバッファAからSPI4への書き込みが完了した後に行う。 同様にバッファBへの読み込みはバッファBからSPI4への書き込みが完了した後に行う。 |
||||||||||||||||
・ | バッファA、及びバッファーBからSPI4への書き込みはDMA転送を用いること | ||||||||||||||||
・ | WAVファイルを再生/停止のボタンスイッチをもうけること | ||||||||||||||||
・ | 演奏曲を演奏中でもスキップできるボタンスイッチをもうけること。SDカード内のすべてのWAVファイルを再生したら最初のファイルに戻ること | ||||||||||||||||
・ | キャラクタ液晶に演奏中のWAVファイルのファイル名を表示すること | ||||||||||||||||
SDカード脱着、ファイルオープン失敗、ダブルバッファ動作状態に係るLED表示を行うこと (コンデンサを追加しないとスピーカアンプの入力に直流が印加されてしまいます。必ず挿入してから通電してください) |
|||||||||||||||||
・ | DAコンバータMCP4922の出力とアンプの間に 100μFのコンデンサを挿入すること (参考)以下の写真はRCAコネクタの信号部分に SMCの3216(L3.2 x W1.6 x H1.6mm) 6v 100μFの積層セラミックコンデンサをハンダ付けしたものです。 |
||||||||||||||||
<回路図> ( → PDF) | |||||||||||||||||
![]() |
|||||||||||||||||
<外観>PIC32MZ評価ボード(→購入方法)を使った実験品の外観です。 汎用モジュール評価ボード(段積みボード)には本テーマと関係ない部品が多々実装されています。 |
|||||||||||||||||
<動作結果> (→ サウンド付き動画) | |||||||||||||||||
![]() |
|||||||||||||||||
|
|||||||||||||||||
演奏中のファイル名の表示結果("エデンの東"を演奏中の場合): 上段が演奏中のファイル名です ![]() |
|||||||||||||||||
<解説>以下に、記載してある内容は要点だけです。 詳細はプロジェクトファイルを精読願います | |||||||||||||||||
(以下は、Harmony v2.04 をもとに作成しています。最新のバージョンとは異なることがあるかもしれませんので注意してください。) |
■ MHC設定
■ Options
■ Pin Settings
■ キャラクタ液晶表示のライブラリ 1lcd_lib_XC32.h と 1lcd_lib_XC32.cを main.cと同じフォルダにコピーして、プロジェクトに追加します。
(詳細→ キャラクタ液晶表示方法 参照)
■app.h を以下の要領で変更します。
①列挙型変数 APP_STATES の定義を以下のように変更します。
typedef enum
{
/* Application's state machine's initial state. */
APP_STATE_INIT=0,
APP_STATE_WAIT_FOR_BUS_ENABLE_COMPLETE,
APP_STATE_WAIT_FOR_DEVICE_ATTACH,
APP_STATE_SEARCH_FILE,
APP_STATE_IDLE,
APP_STATE_START_STOP,
APP_STATE_SKIP,
} APP_STATES;
②構造体 APP_DATAのメンバーを以下のように変更します。
typedef struct
{
/* The application's current state */
APP_STATES state;
/* SYS_FS File handle for 1st file */
SYS_FS_HANDLE fileHandle;
// /* Application's current state */
// APP_STATES state;
/* Application data buffer */
uint8_t data[1024];
/* Number of bytes written */
uint32_t nBytesWritten;
/* Number of bytes read */
uint32_t nBytesRead;
bool deviceIsConnected;
/* TODO: Define any additional data used by the application. */
} APP_DATA;
以下、app.h
■ app.cに、青字部分のように 追加、修正します。
➀インクルードファイルを追加します。stdio.hがないと、sprintf( )がコンパイラのバージョンによってコンパイルで警告となることがあります。
#include <stdio.h>
➁ 3個のDMAチャンネル(DMA0,DMA1,DMA2)ハンドル、ファイルハンドル、タイマハンドル、ダブルバッファメモリなど
所要の変数、ハンドルなどの宣言、定義を行います。
char Buf[32]; //液晶表示バッファー
int Num; //
int Numx; //演奏曲No
int Num_max; //SD収納曲数
APP_DATA appData;
SYS_FS_HANDLE dirHandle; //ディレクトリハンドル
SYS_FS_FSTAT stat;
char longFileName[300];
SYS_FS_RESULT Result;
SYS_FS_HANDLE fileHandle1; //ファイル ハンドル
char File_Name[30][300]; //ファイル名
DRV_HANDLE tmrHandle; //タイマ2ハンドル //44.1KHz
bool PlayFlag; //1:演奏中、 0:非演奏中
int Num_buf; //読込みバイト数
bool Flag_BufAB; //SDからの読み込むバッファモード 0:バッファーB 1:バッファーA
bool SD_Read; //SD読み出しフラグ 1:SDからデータを読み出す 0:SDからデータを読みださない
unsigned int ptr_A, ptr_B; //バッファーポインタ
short int Dummy;
__attribute__((coherent)) unsigned char Buf_A[4096]; // バッファA
__attribute__((coherent)) unsigned char Buf_B[4096]; // バッファB
__attribute__((coherent)) unsigned short SpiOut_A; // 16ビットサウンドSpiデータA
__attribute__((coherent)) unsigned short SpiOut_B; // 16ビットサウンドSpiデータB
//待ち時間設定
uint32_t Wait = 63; //周期レジスタ初期値 //最初の割り込みまでの時間をセット
//5 nsec x2 x63 x 8 = 5040nsec = 5.04μsec
int delay_Clock = 200000000; //200MHz
const unsigned char CS0[] = {0x00};
const unsigned char CS1[] = {0x04};
SYS_DMA_CHANNEL_HANDLE channelHandle0;
SYS_DMA_CHANNEL_HANDLE channelHandle1;
SYS_DMA_CHANNEL_HANDLE channelHandle2;
DRV_HANDLE Wait_Handle;
③ 関数のプロトタイプ宣言を行います
void delay_us(volatile unsigned int usec);
void delay_ms(volatile unsigned int msec);
void SwCheck(void);
……
……
④NOPを用いた 1μsec、1msecの遅延関数 delay_usec( )、delay_ms( )をつくります。
void delay_us(volatile unsigned int usec) //1μsec遅延
{
volatile int count;
count = (int)(delay_Clock/20000000)*usec;
……
……
⑤関数SwCheck()は、Bポートの変化割り込みハンドラーで呼び出されます。 この関数でどのボタンスイッチが押されたのか判別します。
void SwCheck(void) //SW クリック検出
{
if(appData.deviceIsConnected == true)
{
if(PORTBbits.RB0 == 0) //music スタート/ストップ
{
delay_ms(50);
if(PORTBbits.RB0 == 0)appData.state = APP_STATE_START_STOP;
}
if(PORTBbits.RB1 == 0) //music スキップ
{
delay_ms(50);
if(PORTBbits.RB1 == 0)
{
PlayFlag = 0;
appData.state = APP_STATE_SKIP;
}
}
}
}
⑥関数tmrISR2( )はtmrISR( )の中で呼び出される非繰り返しタイマのコールバック関数です。タイマ4がアクティブになってから
変数Waitによって設定されている5.04μsec後に実行されます。 この時間はSPI4のバッファへの書き込み後MPC1922への
転送が完了するまでの時間を確保するものです。 タイマ4が起動するとDMA2チャンネルが起動して RF2(MPC4922のCS)が
0 → 1 となり MP4922の内部でDA変換が開始されます。
(注) "RF: 1 → 0、MP4922へのシリアル転送完了待ち(5.04μsec)、RF: 0 → 1 " のMPC4922への書き込み動作は、
DMA0、 DMA1、 DMA2を連続的動作により実行しています。
void tmrISR2(uintptr_t context, uint32_t alarmCount ) //タイマ4コールバック関数
{
//ここにコードは記載されていませんが、このタイミングでDMA2チャンネルが起動しています。
}
⑦関数tmrISR( )は オーディオ再生中、タイマ割り込みにより44.1KHz毎に 呼び出されてます。
この関数がよびだされるとまずタイマ4がアクティブになります。
ダブルバッファ方式ですので SDカードから読み込まれたバッファA,バッファBのデータが交互にSPI4BUFレジスタに書き込まれます。
また、読み出されているバッファが空になると フラグをたててSDカードからの読み込みを催促します。
バッファA、バッファBにあるデータはRチャンネルのデータも含まれているので読み出し後 捨てています。
バッファA、バッファBにあるLチャンネルデータをそのままMCP4922に書き込むのではなく MCP4922は12ビットDAコンバータですので
16で割る演算をしています。 更にMCP4922には2つのDA出力ポートがあります。また出力ゲインを1倍or2倍の選択もあります。これらの
設定などをb12-b15ビットに演算してSPI4BUFに書き込んでいます。
void tmrISR(uintptr_t context, uint32_t alarmCount ) //タイマ2コールバック関数 //44.1KHz(22.7μsec)インターバル
{
DRV_TMR_AlarmRegister ( Wait_Handle, Wait, false, 0, tmrISR2 ); //非繰り返し呼び出し
DRV_TMR_Start(Wait_Handle); //タイマ4割り込み許可、タイマ4イネーブル
if(Flag_BufAB == 0){ // バッファAの場合
// LATFbits.LATF2 = 0; //MCP4922 CS = 0
SpiOut_A = ((((Buf_A[ptr_A+1] * 256 + Buf_A[ptr_A])/ 16) + 2048) | 0b0011000000000000); //Lチャンネル Soundデータ送信
//MP4922 制御コマンド
//VoutAにゲイン2で出力
//MCP4922への書き込みコマンド
//bit15(書き込みレジスタ選択) 1:VoutB 0:VoutA
//bit14(予備)
//bit13(出力ゲイン選択) 1:1倍 0:2倍
//bit12(出力オフ制御) 1:出力ON 0:出力OFF
//bit11 - bit0 データ MSBから順次送信される
// SPI4BUF = SpiOut_A;
//
// //Vout = 2.048×2×Dn/4095 Vout(max) = 4.096[V] ( > 3.3V(電源電圧)) 4.096×3.3/5 = 2.7V (< (3.3V時オペアンプ飽和電圧))
// //Vout = 2.7×Dn/4095 //32767
ptr_A += 2; //Sound data 2バイト → ポインタ +2
// while(SPI4STATbits.SPITBF); //: SPI Transmit Buffer Full Status
bit //SPI4SRレジスタへの書込み完了
// delay_us(5); //MCP4922の処理待ち(必須)
// LATFbits.LATF2 = 1; //MCP4922 CS = 1;
Dummy = Buf_A[ptr_A+1] * 256 + Buf_A[ptr_A]; //Rチャンネルデータ出力
ptr_A += 2; //Sound data 2バイト → ポインタ +2
if(ptr_A >= Num_buf){ // バッファAデータ読み出し完了の場合
Flag_BufAB = 1; // 読み出し先をバッファーBに変更
SD_Read = 1; // SD読み出しフラグセット
tr_A = 0; // ポインタAリセット
}
else
{ // バッファBの場合
// LATFbits.LATF2 = 0; //MCP4922 CS = 0
SpiOut_A = (((Buf_B[ptr_B+1] * 256 + Buf_B[ptr_B]) /16) + 2048 |
0b0011000000000000);
// SPI4BUF = (((Buf_B[ptr_B+1] * 256 + Buf_B[ptr_B]) /16) + 2048 | 0b0011000000000000);
ptr_B += 2; // ポインタB更新
// while(SPI4STATbits.SPITBF); //: SPI Transmit Buffer Full Status bit //SPI4SRレジスタへの書込み完了
//
Dummy = Buf_B[ptr_B+1] * 256 + Buf_B[ptr_B]; // 次のRchデータ出力
ptr_B += 2; // ポインタB更新
//
// delay_us(5); //MCP4922の処理待ち
//
// LATFbits.LATF2 = 1; //MCP4922 CS = 1;
if(ptr_B >= Num_buf){ // バッファ終了の場合
Flag_BufAB = 0; // バッファ切り替え
SD_Read = 1; // SD読み出しフラグセット
ptr_B = 0; // ポインタBリセット
}
}
}
⑧関数PlayStart( ) は プレースタートの場合に呼び出されます。 WAVファイルは先頭から44バイト目からサウンドデータ
となります。従ってサウンドデータを呼び出す場合の先頭アドレスは0x2C(= 44)となります。
void PlayStart(void) //演奏開始
{
fileHandle1 = SYS_FS_FileOpen(&File_Name[Numx][300], (SYS_FS_FILE_OPEN_READ));
if(fileHandle1 == SYS_FS_HANDLE_INVALID)LATDbits.LATD4 = 1; //ファイルオープン失敗の場合
RD4 LED点灯
Num_buf = SYS_FS_FileRead(fileHandle1, Buf_A, 4096);
Flag_BufAB = 0; //読込みバッファ:B
ptr_A = 0x2C; // WAVファイルの先頭指定 //オフセット44(0x2C)
ptr_B = 0; // バッファポインタリセット
SD_Read = 1; // SDカード読み込み許可
DRV_TMR_Start(tmrHandle); //タイマ2割り込み許可、タイマ1イネーブル //44.1KHz 割り込み開始
}
⑨関数PlayStop( ) は プレーストップの場合に呼び出されます。
void PlayStop(void) //演奏停止
{
int i;
SYS_FS_FileClose(fileHandle1); // ファイルクローズ
DRV_TMR_Stop(tmrHandle); //タイマ2割り込み禁止 //44.1KHz 割り込み禁止 // Lch割り込み禁止
// バッファクリア
for(i=0; i<4096; i++){
Buf_A[i] = 0;
Buf_B[i] = 0;
}
}
⑩関数SearchFile( ) はSDカードのルートディレクトリ内にあるWAVファイルを探索する関数です
void SearchFile(void) //SDカード(ルート)の *.wavファイル探索
{
dirHandle = SYS_FS_DirOpen("/mnt/myDrive1");
stat.lfname = longFileName;
stat.lfsize = 300;
Num = 0;
do
{
Result = SYS_FS_DirSearch(dirHandle, "*.wav", SYS_FS_ATTR_ARC,
&stat);
if(Result == SYS_FS_RES_SUCCESS) //SYS_FS_RES_SUCCESS = 0
{
Num++;
sprintf(&File_Name[Num][300],"%s",stat.lfname); //
}
}while(Result == SYS_FS_RES_SUCCESS);
Num_max = Num;
SYS_FS_DirClose(dirHandle);
}
⑪DMAチャンネル DMA0 DMA1 DMA2の諸元を設定して有効化している部分です
DMAが使われている部分は PIC32MZのSPI4からMCP4922への転送動作部分です。この部分は詳しく述べると以下となります。
1. RF2ポート出力: 1 → 0
2. SPI4BUFへの書き込み
3. シリアル転送完了までの時間待機(5μsec)
4. RF2ポート出力: 0 → 1
DMA0が 1. を実行しています。
DMA1が 2. を実行しています。
タイマ4が 3. を実行しています。
DMA2が 4. を実行しています。.
尚、プレー中、DMA0は44.1KHz毎に タイマ2から起動させらるようにしています。
関数SYS_DMA_ChannelTransferAdd( )で、どの値を どのアドレスにDMA転送するかと云ったことを設定しています。
関数SYS_DMA_ChannelSetup( )で、どのようなタイミングでDMAチャンネルを動作させるのか設定しています。
//DMAチャンネル諸元設定(Harmony ライブラリ関数)
//----------------------------------------------------------------------------------------------------
channelHandle0 = SYS_DMA_ChannelAllocate(DMA_CHANNEL_0); //RFポート用 DMAチャンネルオープン
& ハンドル取得
SYS_DMA_ChannelTransferAdd(channelHandle0, &CS0, sizeof(CS0), (const
void *)&LATF , 1, 1 ); //RF2 = 0(CS = 0)
SYS_DMA_ChannelSetup(
channelHandle0,
SYS_DMA_CHANNEL_OP_MODE_AUTO, //データ、アドレス自動インクリメント
DMA_TRIGGER_TIMER_2 //トリガソース:タイマ2
);
SYS_DMA_ChannelEnable(channelHandle0);
channelHandle1 = SYS_DMA_ChannelAllocate(DMA_CHANNEL_1); //RGポート用 DMAチャンネルオープン
& ハンドル取得
SYS_DMA_ChannelTransferAdd(channelHandle1, &SpiOut_A, sizeof(SpiOut_A),
(const void *)&SPI4BUF, 2, 2 ); //SPI4バッファーへ書き込み
SYS_DMA_ChannelSetup(
channelHandle1,
SYS_DMA_CHANNEL_OP_MODE_AUTO, //データ、アドレス自動インクリメント
DMA_TRIGGER_TIMER_2 //トリガソース :タイマ2
);
SYS_DMA_ChannelEnable(channelHandle1);
channelHandle2 = SYS_DMA_ChannelAllocate(DMA_CHANNEL_2); //RFポート用 DMAチャンネルオープン
& ハンドル取得
SYS_DMA_ChannelTransferAdd(channelHandle2, &CS1, sizeof(CS1), (const
void *)&LATF , 1, 1) ; //RF2 = 1(CS = 1)
SYS_DMA_ChannelSetup(
channelHandle2,
SYS_DMA_CHANNEL_OP_MODE_AUTO, //データ、アドレス自動インクリメント、
DMA_TRIGGER_TIMER_4 //トリガソース:タイマ4
);
SYS_DMA_ChannelEnable(channelHandle2);
⑫インターバルタイマ(44.1KHz)のハンドル取得、諸元設定
tmrHandle = DRV_TMR_Open ( DRV_TMR_INDEX_1, DRV_IO_INTENT_EXCLUSIVE );
//タイマハンドル取得
uint32_t divider = 2267; //周期レジスタ初期値 //最初の割り込みまでの時間をセット
//5 nsec x2 x 2267 = 22670nsec = 22.67μsec --> 44.111
KHz = 44.1 KHz
//5 nsec x2 x 39063 x 256 = 100.00128msec
DRV_TMR_AlarmRegister ( tmrHandle, divider, true, 0, tmrISR ); //繰り返し呼び出し
//コールバック関数繰り返し呼び出しをセットアップ
// bool DRV_TMR_AlarmRegister(
// DRV_HANDLE handle,
// uint32_t divider, //周期レジスタ初期値
// bool isPeriodic, //周期性の有無
// uintptr_t context,
// DRV_TMR_CALLBACK callBack
// );
// タイマハンドル取得
Wait_Handle = DRV_TMR_Open ( DRV_TMR_INDEX_2, DRV_IO_INTENT_EXCLUSIVE
); //タイマ4ハンドル取得
⑬SPI4モジュール有効化
PLIB_SPI_Enable(SPI_ID_4); //SPIモジュール イネーブル
// SPI4CONbits.ON = 1; //SPI2 有効
⑭SDカード挿入のチェックをしています。
void APP_SYSFSEventHandler(SYS_FS_EVENT event, void * eventData, uintptr_t context)
{
switch(event)
{
case SYS_FS_EVENT_MOUNT: //SDカード挿入済み
appData.deviceIsConnected = true;
LATGbits.LATG15 = 0; //RG15:消灯
break;
case SYS_FS_EVENT_UNMOUNT: //SDカード未挿入
appData.deviceIsConnected = false;
LATGbits.LATG15 = 1; //RG15:点灯
break;
default:
break;
}
}
⑮各ステートの処理を記載します。
void APP_Tasks ( void )
{
switch ( appData.state )
{
case APP_STATE_INIT:
{
bool appInitialized = true;
if (appInitialized)
{
SYS_FS_EventHandlerSet(APP_SYSFSEventHandler, (uintptr_t)NULL);
appData.state = APP_STATE_WAIT_FOR_DEVICE_ATTACH;
}
break;
}
case APP_STATE_WAIT_FOR_DEVICE_ATTACH:
if(appData.deviceIsConnected)
……
……
以下、app.c
■system_interrupt.c を以下の要領で変更します
➀ポートBの変化割り込みハンドラー
void __ISR(_CHANGE_NOTICE_B_VECTOR, ipl1AUTO) _IntHandlerChangeNotification_PortB(void) の中に
割り込み発生のポートを判別する関数SwCheck();を追加します。
void __ISR(_CHANGE_NOTICE_B_VECTOR, ipl1AUTO) _IntHandlerChangeNotification_PortB(void)
{
SwCheck();
PLIB_INT_SourceFlagClear(INT_ID_0,INT_SOURCE_CHANGE_NOTICE_B);
}
以下、interrupt.c