STM32F407ZET6音樂播放器

2022-01-13 10:00:01

一、主要功能

  1. SD 卡模組儲存至少 5 首以上音樂檔案(wav 格式);
  2. 片內 Flash 儲存 1-2 句短提示音(5-6 秒長度),比如「xxx 的音樂播放器歡迎你!」、
    「SD 檔案找不到!」;
  3. 最小系統板上電後自動查詢讀取 SD 卡上第 1 首音樂檔案,然後依次迴圈播放;
  4. 最小系統板外接一個紅外接收模組,接收紅外遙控器傳送的按鍵指令序列。紅外遙
    控器用來控制最小系統板的音樂播放,實現「暫停」、「播放」、「下一首」、 「上
    一首」、「回到第 1 首」等功能

二、flash讀寫

這裡選擇磁區5進行flash的讀寫
在這裡插入圖片描述

1.讀flash

/**
讀取flash
address 讀取起始地址
readBuf 讀取內容存放位置
size 讀取的大小
**/
void readFlash(uint32_t address,uint8_t *readBuf,uint16_t size)   	
{
	uint16_t i;
	uint16_t tmpBuf;
	printf("開始讀flash\r\n");
	for(i=0;i<size;i+=2)
	{
		tmpBuf=*(__IO uint16_t *)(address+i);
		//低八位
		readBuf[i]=tmpBuf&0xff;
		//高八位
		readBuf[i+1]=tmpBuf>>8;
	}
	printf("讀flash完畢\r\n");
}

2.寫flash

/**
寫flash
address 起始地址
data 資料
size 資料大小

**/
void writeFlash(u32 address,u8 *data,u16 size)
{
	FLASH_EraseInitTypeDef FlashSet;
	HAL_StatusTypeDef FlashStatus = HAL_OK;
	u16 tmpbuf;
	u16 i;
	u32 PageError = 0;
	
	//解鎖FLASH
	HAL_FLASH_Unlock();
	//擦除FLASH

	
	//第一次寫入之前呼叫擦除函數進行擦除,後續無需再呼叫,否則會吧之前寫入的資料擦除掉
	//除非需要覆蓋之前的flash 

	//初始化FLASH_EraseInitTypeDef
	//擦除方式
	FlashSet.TypeErase = FLASH_TYPEERASE_SECTORS;
	//擦除起始頁
	FlashSet.Sector  = 5;
	//擦除結束頁
	FlashSet.NbSectors  = 6;
	FlashSet.VoltageRange = FLASH_VOLTAGE_RANGE_3; 
	
	printf("擦除\r\n");
	//呼叫擦除函數
	HAL_FLASHEx_Erase(&FlashSet, &PageError);
	FlashStatus = FLASH_WaitForLastOperation(1000); //等待上次操作完成

	//對FLASH燒寫
	printf("開始寫flash\r\n");
	for( i= 0;i< size ;i+=2)
	{
		//把8位元轉16位元,得到半個位元組
		//每兩個8位元,一個當低八位,一個當高八位
		tmpbuf = (*(data + i + 1) << 8) + (*(data + i));
		//寫半個位元組
		HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD , address + i , tmpbuf);
	}

 
	//鎖住FLASH
	HAL_FLASH_Lock();
}

3.獲取wav格式音訊資料

參考前面的部落格Python提取wav格式檔案的data並轉為十六進位制格式重定向輸出提取資料,再分批次進行flash寫入。

4.操作

  1. 把「歡迎光臨」的提示音轉換成wav格式
  2. 通過python程式碼得到wav格式音訊的資料塊,以16進格式儲存

在這裡插入圖片描述
在這裡插入圖片描述

  1. 把得到的16進位制格式資料用陣列進行儲存

在這裡插入圖片描述

  1. main函數中呼叫writeFlash函數進行寫flash操作

在這裡插入圖片描述

  1. 重複3~4,直到寫完所有flash

三、播放音訊

1.原理

通過I2S介面和MCU進行音訊資料的傳輸 將得到的wav資料(PCM編碼)丟給WM8978(codec)即可播放聲音
在這裡插入圖片描述

2.播放「歡迎光臨」提示音

/**
播放歡迎光臨提示
從flash讀取音訊檔

**/
void hello(){
	u8 res;
	//每次讀取flash大小
	u32 eachSize=8192;
	//音訊檔大小
	u32 fillnum=data_size;
	//flash起始地址
	u32 address=(u32)0x08020000;
	//分配記憶體
	audiodev.i2sbuf1=mymalloc(SRAMIN,eachSize);
	audiodev.i2sbuf2=mymalloc(SRAMIN,eachSize);
	audiodev.tbuf=mymalloc(SRAMIN,eachSize);
	
	WM8978_I2S_Cfg(2,0);	//飛利浦標準,16位元資料長度
	I2S2_Init(I2S_STANDARD_PHILIPS,I2S_MODE_MASTER_TX,I2S_CPOL_LOW,I2S_DATAFORMAT_16B_EXTENDED);	//飛利浦標準,主機傳送,時鐘低電平有效,16位元擴充套件幀長度
	I2S2_SampleRate_Set(8000);//設定取樣率
	I2S2_TX_DMA_Init(audiodev.i2sbuf1,audiodev.i2sbuf2,eachSize/2); //設定TX DMA
	i2s_tx_callback=wav_i2s_dma_tx_callback;			//回撥函數指wav_i2s_dma_callback
	
	audio_start();  
	printf("開始播放\r\n");
	
	while(1)     
	{ 
		//根據當前剩餘資料大小和每次播放資料大小來確定每次讀取buf的大小
		if(fillnum>=eachSize){
			fillnum-=eachSize;
		}
		else{
			eachSize=fillnum;
		}
		
		//讀取flash
		readFlash(address,audiodev.i2sbuf1,eachSize);
		readFlash(address,audiodev.i2sbuf2,eachSize);
		
		//根據當前剩餘資料大小和每次播放資料大小來計算讀取地址
		if(eachSize!=fillnum){
			address+=eachSize;
		}
		else{
			break;
		}
		
		while(wavtransferend==0);//等待wav傳輸完成; 
		wavtransferend=0;
		
		//播放轉換完畢的buf
		audiodev.status=1;
		while(1)
		{
			//判斷是否播放完當前buf
			if((audiodev.status&0X01)==0)delay_ms(10);
			else break;
		}
	

	}
	//停止播放
	audio_stop();
	printf("停止播放\r\n");	
	myfree(SRAMIN,audiodev.tbuf);	//釋放記憶體
	myfree(SRAMIN,audiodev.i2sbuf1);//釋放記憶體
	myfree(SRAMIN,audiodev.i2sbuf2);//釋放記憶體 
}

3.播放SD卡內的音訊檔

基於正點原子的 音樂播放器實驗-HAL庫函數版程式碼進行修改。先掃描是否存在SD卡,如果存在則掃描指定檔案裡面的歌曲資訊,歌曲格式為wav格式。如果存在歌曲,則把所有的歌曲資訊進行儲存,並且預設從第一首開始播放。播放原理就是通過讀取每次讀取指定大小的buf,然後通過SPI進行轉換,轉換為PCM編碼,再通過PCM模組進行播放。這裡只展示部分程式碼。最終程式碼見原始碼連結。

讀取SD資訊以及控制播放音樂

//播放音樂
void audio_play(void)
{
	
	u8 res;
 	DIR wavdir;	 			//目錄
	FILINFO *wavfileinfo;	//檔案資訊 
	u8 *pname;				//帶路徑的檔名
	u16 totwavnum; 			//音樂檔案總數
	u16 curindex;			//當前索引
	u8 key;					//鍵值		  
 	u32 temp;
	u32 *wavoffsettbl;		//音樂offset索引表
	
	WM8978_ADDA_Cfg(1,0);	//開啟DAC
	WM8978_Input_Cfg(0,0,0);//關閉輸入通道
	WM8978_Output_Cfg(1,0);	//開啟DAC輸出   
	
	//播放預設音訊
	hello();
	
	
	
 	while(f_opendir(&wavdir,"0:/MUSIC"))//開啟音樂資料夾
 	{	   
		//Show_Str(60,190,240,16,"MUSIC資料夾錯誤!",16,0);
		printf("MUSIC資料夾錯誤\r\n");
		delay_ms(200);				  
		//LCD_Fill(60,190,240,206,WHITE);//清除顯示	     
		delay_ms(200);				  
	} 									  
	totwavnum=audio_get_tnum("0:/MUSIC"); //得到總有效檔案數
  	while(totwavnum==NULL)//音樂檔案總數為0		
 	{	    
		printf("沒有音樂檔案\r\n");
		//Show_Str(60,190,240,16,"沒有音樂檔案!",16,0);
		delay_ms(200);				  
		//LCD_Fill(60,190,240,146,WHITE);//清除顯示	     
		delay_ms(200);				  
	}										   
	wavfileinfo=(FILINFO*)mymalloc(SRAMIN,sizeof(FILINFO));	//申請記憶體
  	pname=mymalloc(SRAMIN,_MAX_LFN*2+1);					//為帶路徑的檔名分配記憶體
 	wavoffsettbl=mymalloc(SRAMIN,4*totwavnum);				//申請4*totwavnum個位元組的記憶體,用於存放音樂檔案off block索引
 	while(!wavfileinfo||!pname||!wavoffsettbl)//記憶體分配出錯
 	{	 
		printf("記憶體分配失敗\r\n");		
		//Show_Str(60,190,240,16,"記憶體分配失敗!",16,0);
		delay_ms(200);				  
		//LCD_Fill(60,190,240,146,WHITE);//清除顯示	     
		delay_ms(200);				  
	}  	 
 	//記錄索引
    res=f_opendir(&wavdir,"0:/MUSIC"); //開啟目錄
	if(res==FR_OK)
	{
		curindex=0;//當前索引為0
		while(1)//全部查詢一遍
		{
			temp=wavdir.dptr;								//記錄當前index 
	        res=f_readdir(&wavdir,wavfileinfo);       		//讀取目錄下的一個檔案
	        if(res!=FR_OK||wavfileinfo->fname[0]==0)break;	//錯誤了/到末尾了,退出 		 
			res=f_typetell((u8*)wavfileinfo->fname);	
			if((res&0XF0)==0X40)//取高四位,看看是不是音樂檔案	
			{
				wavoffsettbl[curindex]=temp;//記錄索引
				curindex++;
			}    
		} 
	}   
   	curindex=0;											//從0開始顯示
   	res=f_opendir(&wavdir,(const TCHAR*)"0:/MUSIC"); 	//開啟目錄
	while(res==FR_OK)//開啟成功
	{	
		dir_sdi(&wavdir,wavoffsettbl[curindex]);				//改變當前目錄索引	   
        res=f_readdir(&wavdir,wavfileinfo);       				//讀取目錄下的一個檔案
        if(res!=FR_OK||wavfileinfo->fname[0]==0)break;			//錯誤了/到末尾了,退出		 
		strcpy((char*)pname,"0:/MUSIC/");						//複製路徑(目錄)
		strcat((char*)pname,(const char*)wavfileinfo->fname);	//將檔名接在後面
 		//LCD_Fill(60,190,lcddev.width-1,190+16,WHITE);			//清除之前的顯示
		//Show_Str(60,190,lcddev.width-60,16,(u8*)wavfileinfo->fname,16,0);//顯示歌曲名字 
		//audio_index_show(curindex+1,totwavnum);
		key=audio_play_song(pname); 			 		//播放這個音訊檔
		if(key==KEY2_PRES)		//上一曲
		{
			if(curindex)curindex--;
			else curindex=totwavnum-1;
 		}else if(key==KEY0_PRES)//下一曲
		{
			curindex++;		   	
			if(curindex>=totwavnum)curindex=0;//到末尾的時候,自動從頭開始
 		}else break;	//產生了錯誤 	 
	} 											  
	myfree(SRAMIN,wavfileinfo);			//釋放記憶體			    
	myfree(SRAMIN,pname);				//釋放記憶體			    
	myfree(SRAMIN,wavoffsettbl);		//釋放記憶體 
} 

播放wav格式音訊

//播放某個WAV檔案
//fname:wav檔案路徑.
//返回值:
//KEY0_PRES:下一曲
//KEY2_PRES:上一曲
//其他:錯誤
u8 wav_play_song(u8* fname)
{
	u8 key;
	u8 key2;
	u8 t=0; 
	u8 res;  
	u32 fillnum; 
	audiodev.file=(FIL*)mymalloc(SRAMIN,sizeof(FIL));
	audiodev.i2sbuf1=mymalloc(SRAMIN,WAV_I2S_TX_DMA_BUFSIZE);
	audiodev.i2sbuf2=mymalloc(SRAMIN,WAV_I2S_TX_DMA_BUFSIZE);
	audiodev.tbuf=mymalloc(SRAMIN,WAV_I2S_TX_DMA_BUFSIZE);
	if(audiodev.file&&audiodev.i2sbuf1&&audiodev.i2sbuf2&&audiodev.tbuf)
	{ 
		res=wav_decode_init(fname,&wavctrl);//得到檔案的資訊
		if(res==0)//解析檔案成功
		{
//			printf("%s",fname);
			if(wavctrl.bps==16)
			{
				WM8978_I2S_Cfg(2,0);	//飛利浦標準,16位元資料長度
				I2S2_Init(I2S_STANDARD_PHILIPS,I2S_MODE_MASTER_TX,I2S_CPOL_LOW,I2S_DATAFORMAT_16B_EXTENDED);	//飛利浦標準,主機傳送,時鐘低電平有效,16位元擴充套件幀長度
			}else if(wavctrl.bps==24)
			{
				WM8978_I2S_Cfg(2,2);	//飛利浦標準,24位元資料長度
				I2S2_Init(I2S_STANDARD_PHILIPS,I2S_MODE_MASTER_TX,I2S_CPOL_LOW,I2S_DATAFORMAT_24B);	//飛利浦標準,主機傳送,時鐘低電平有效,24位元長度
			}
			I2S2_SampleRate_Set(wavctrl.samplerate);//設定取樣率
			I2S2_TX_DMA_Init(audiodev.i2sbuf1,audiodev.i2sbuf2,WAV_I2S_TX_DMA_BUFSIZE/2); //設定TX DMA
			i2s_tx_callback=wav_i2s_dma_tx_callback;			//回撥函數指wav_i2s_dma_callback
			
			audio_stop();
			
			res=f_open(audiodev.file,(TCHAR*)fname,FA_READ);	//開啟檔案
			if(res==0)
			{
				
				f_lseek(audiodev.file, wavctrl.datastart);		//跳過檔案頭
				fillnum=wav_buffill(audiodev.i2sbuf1,WAV_I2S_TX_DMA_BUFSIZE,wavctrl.bps);
				fillnum=wav_buffill(audiodev.i2sbuf2,WAV_I2S_TX_DMA_BUFSIZE,wavctrl.bps);
				
				
				audio_start();  
				while(res==0)
				{ 
					while(wavtransferend==0);//等待wav傳輸完成; 
					wavtransferend=0;
					if(fillnum!=WAV_I2S_TX_DMA_BUFSIZE)//播放結束?
					{
						res=KEY0_PRES;
						break;
					} 
 					if(wavwitchbuf)
						fillnum=wav_buffill(audiodev.i2sbuf2,WAV_I2S_TX_DMA_BUFSIZE,wavctrl.bps);//填充buf2
					else 
						fillnum=wav_buffill(audiodev.i2sbuf1,WAV_I2S_TX_DMA_BUFSIZE,wavctrl.bps);//填充buf1
					while(1)
					{	
						//掃描紅外遙控
						key2=Remote_Scan();
						//掃描按鍵
						key=KEY_Scan(0); 
						if(key==WKUP_PRES || key2==56)//暫停
						{
							printf("暫停\r\n");
							
							if(audiodev.status&0X01)audiodev.status&=~(1<<0);
							else audiodev.status|=0X01;  
						}
						if(key==KEY2_PRES||key2==24 )//上一曲
						{
//							printf("切換歌曲\r\n");
//							key=key2;
							
								printf("切換上一首歌曲\r\n");
								res=KEY2_PRES; 
							
							
							break; 
						}
						if(key==KEY0_PRES||key2==74 )//下一曲
						{
//							printf("切換歌曲\r\n");
//							key=key2;
							
								printf("切換下一首歌曲\r\n");
								res=KEY0_PRES; 
							
							
							break; 
						}
						wav_get_curtime(audiodev.file,&wavctrl);//得到總時間和當前播放的時間 
						//audio_msg_show(wavctrl.totsec,wavctrl.cursec,wavctrl.bitrate);
						t++;
						if(t==20)
						{
							t=0;
 							LED0=!LED0;
						}
						if((audiodev.status&0X01)==0)delay_ms(10);
						else break;
					}
				}
				audio_stop(); 
			}else res=0XFF; 
		}
		
		else {res=0XFF;}
	}else res=0XFF; 
	myfree(SRAMIN,audiodev.tbuf);	//釋放記憶體
	myfree(SRAMIN,audiodev.i2sbuf1);//釋放記憶體
	myfree(SRAMIN,audiodev.i2sbuf2);//釋放記憶體 
	myfree(SRAMIN,audiodev.file);	//釋放記憶體 
	return res;
} 

四、紅外遙控

1.原理

  • NEC協定特徵

(1)8位元地址和8位元指令長度;
(2)地址和命令2次傳輸(確保可靠性)
(3)PWM脈衝寬度調變,以發射紅外載波的佔空比代表「0」和「1」;
(4)載波頻率為38Khz;
(5)位時間為1.125ms或2.25ms。

  • NEC碼的位定義

一個脈衝對應560us的連續載波,一個邏輯1傳輸需要2.25ms(560us脈衝+1680us低電平),一個邏輯0的傳輸需要1.125ms(560us脈衝+560us低電平)。而遙控接收頭在收到脈衝的時候為低電平,在沒有脈衝的時候為高電平,這樣,我們在接收頭端收到的訊號為:邏輯1應該是560us低+1680us高,邏輯0應該是560us低+560us高。

具體詳情可以參考部落格https://blog.csdn.net/li_little7/article/details/89950161

2.捕獲紅外遙控

基於正點原子紅外遙控解碼驅動程式碼進行修改。使用定時器設定定時中斷去掃描對面通道的值。根據掃描的值進行對應轉換得到按鍵資訊。

//處理紅外來鍵盤
//返回值:
//	 0,沒有任何按鍵按下
//其他,按下的按鍵鍵值.
u8 Remote_Scan(void)
{        
	u8 sta=0;       
	u8 t1,t2;  
	if(RmtSta&(1<<6))//得到一個按鍵的所有資訊了
	{ 	
	    t1=RmtRec>>24;			//得到地址碼
	    t2=(RmtRec>>16)&0xff;	//得到地址反碼 
 	    if((t1==(u8)~t2)&&t1==REMOTE_ID)//檢驗遙控識別碼(ID)及地址 
	    { 
	        t1=RmtRec>>8;
	        t2=RmtRec; 	
	        if(t1==(u8)~t2)sta=t1;//鍵值正確	 
		}   
		if((sta==0)||((RmtSta&0X80)==0))//按鍵資料錯誤/遙控已經沒有按下了
		{
		 	RmtSta&=~(1<<6);//清除接收到有效按鍵標識
			RmtCnt=0;		//清除按鍵次數計數器
		}
	}  
    return sta;
}

五、原始碼

https://github.com/TangtangSix/MusicPlayer
https://gitee.com/tangtangsix/MusicPlayer