STM32F407ZET6音乐播放器

一、主要功能

  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,然后通过I2S进行转换,转换为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.捕获红外遥控

基于正点原子红外遥控解码驱动代码进行修改。使用定时器设置定时中断去扫描对面通道的值。根据扫描的值进行对应转换得到按键信息。

//定时器更新(溢出)中断回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
 if(htim->Instance==TIM1){
 		if(RmtSta&0x80)//上次有数据被接收到了
		{	
			RmtSta&=~0X10;						//取消上升沿已经被捕获标记
			if((RmtSta&0X0F)==0X00)RmtSta|=1<<6;//标记已经完成一次按键的键值信息采集
			if((RmtSta&0X0F)<14)RmtSta++;
			else
			{
				RmtSta&=~(1<<7);//清空引导标识
				RmtSta&=0XF0;	//清空计数器	
			}						 	   	
		}	
 
 }
}

//定时器输入捕获中断回调函数
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)//捕获中断发生时执行
{
 if(htim->Instance==TIM1)
{
	
 	if(RDATA)//上升沿捕获
	{
		TIM_RESET_CAPTUREPOLARITY(&TIM1_Handler,TIM_CHANNEL_1);   //一定要先清除原来的设置!!
        TIM_SET_CAPTUREPOLARITY(&TIM1_Handler,TIM_CHANNEL_1,TIM_ICPOLARITY_FALLING);//CC1P=1 设置为下降沿捕获
        __HAL_TIM_SET_COUNTER(&TIM1_Handler,0);  //清空定时器值   	  
		  	RmtSta|=0X10;					//标记上升沿已经被捕获
	}else //下降沿捕获
	{
        Dval=HAL_TIM_ReadCapturedValue(&TIM1_Handler,TIM_CHANNEL_1);//读取CCR1也可以清CC1IF标志位
        TIM_RESET_CAPTUREPOLARITY(&TIM1_Handler,TIM_CHANNEL_1);   //一定要先清除原来的设置!!
        TIM_SET_CAPTUREPOLARITY(&TIM1_Handler,TIM_CHANNEL_1,TIM_ICPOLARITY_RISING);//配置TIM5通道1上升沿捕获
		if(RmtSta&0X10)					//完成一次高电平捕获 
		{
			if(RmtSta&0X80)//接收到了引导码
			{
						
				if(Dval>300&&Dval<800)			//560为标准值,560us
				{
					RmtRec<<=1;	//左移一位.
					RmtRec|=0;	//接收到0	   
				}else if(Dval>1400&&Dval<1800)	//1680为标准值,1680us
				{
					RmtRec<<=1;	//左移一位.
					RmtRec|=1;	//接收到1
				}else if(Dval>2200&&Dval<2600)	//得到按键键值增加的信息 2500为标准值2.5ms
				{
					RmtCnt++; 		//按键次数增加1次
					RmtSta&=0XF0;	//清空计时器		
				}
				}else if(Dval>4200&&Dval<4700)		//4500为标准值4.5ms
				{
					RmtSta|=1<<7;	//标记成功接收到了引导码
					RmtCnt=0;		//清除按键次数计数器
				}						 
			}
		
		RmtSta&=~(1<<4);
		}				 		     	    
	}
}
//处理红外键盘
//返回值:
//	 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

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值