11432|13

1381

帖子

2

TA的资源

五彩晶圆(初级)

楼主
 

【12月DIY】SD卡WAV音乐播放器 [复制链接]

 
本帖最后由 cruelfox 于 2016-12-11 10:12 编辑


  继上月底STM32F4上的SDIO模块读取SD卡搞通了https://bbs.eeworld.com.cn/thread-507474-1-1.html 以后,终于实现了一个简单的FAT32文件系统,然后读取卡上的WAV文件,从I2S输出信号给我的CS4398 DAC来播放音乐。尽管还简陋了不少,终于我自己的第一个硬件音乐播放器(摆脱电脑)诞生了。精力有限,能简化的先简化了,所以目前它的局限有:(1) 只支持44.1kHz/16-bit 的CD格式WAV音频文件(无压缩格式)播放
(2) 只支持格式化为FAT32文件系统的SD、SDHC卡
(3) 不能在一个文件中进行快进、倒带索引,只能从每个文件开始连续播放。只有播放/暂停、下一首、上一首、停止的按钮控制。
(4)没有屏幕显示,调试信息只通过UART输出,文件名字符不能输出汉字等。
(5)只检索根目录下的.wav文件,上限255个;不检索子目录。

  先上线路图:这是我的STM32F411开发板

  MCU外围不多,除了必要的电源部分,就是SD卡座、USB座、各种接口的插针,以及少数按钮和LED了。
布线还是我自己的风格,Gerber文件附在后面,可以用ViewMate、CAM350等软件来看。


  这个开发板就是按照做音乐播放器来设计的,当然DIY玩的不是做产品,就没考虑外观、用户界面那些了。MCU选了STM32F411RET6 的原因有:(1)M4F内核,又有128kB SRAM,做软解MP3、FLAC资源充足。(2)便宜,F407价格相似计算能力更强但是封装尺寸大一档。(3)SDIO, USB OTG FS都有。(4) I2S支持外部clock输入,也就是I2S做master但是和DAC的MCLK是绝对同步的。

  目前的主程序框架是很直接的,在必要的初始化之后就访问SD卡,读取根目录检索WAV文件。找到的可播放文件的信息建立一个playlist,然后就在 idle_process() 和 play_file() 两个函数之间切换,对应停止和播放两个状态。
  1. int main(void)
  2. {
  3.     int i;

  4.     config_pins();
  5.     config_clock();
  6.     uart_setup();
  7.     uart_wstr("\r\n-- UART enabled --");
  8.     config_i2s();
  9.     NVIC_EnableIRQ(DMA1_Stream4_IRQn);

  10.     if(sdio_init())
  11.     {
  12.         sdio_setmode(); // set 4-bit mode
  13.         uart_wstr("\r\nSet 4-bit mode");

  14.         if(fat32_init(0))
  15.         {
  16.             uint8_t n, i;
  17.             int8_t r;
  18.             myfile file;

  19.             n=create_playlist();
  20.             show_playlist(n);

  21.             for(;;)
  22.             {
  23.                 i=idle_process(r,n);
  24.                 open_file0(&file,playlist_mem[i*3],playlist_mem[i*3+1]);
  25.                 uart_wstr("\r\nPlay "");
  26.                 uart_wstr(playlist_mem[i*3+2]);
  27.                 uart_wstr(""");
  28.                 r=play_file(&file);
  29.             }
  30.         }
  31.         else
  32.             uart_wstr("\r\nCannot recognize file system");
  33.     }

  34.     for(;;)
  35.     {
  36.         __WFI();
  37.     }
  38. }
复制代码
   uart_wstr() 以及类似的 uart_whex()等函数是我用来调试的,也在没有LCD显示的时候用来当作UI的一部分了。

  FAT32文件系统,我考虑过用fatfs, 但又觉得稍嫌复杂,于是自己写了一个简化的FAT32支持函数库,只读的,不支持文件seek,呵呵,每次读文件只能读512字节。
  1. #include <string.h>
  2. #include "stm32f4xx.h"

  3. /* simplified FAT32 read-only access library */
  4. #include "fat32.h"

  5. extern void device_read_blk(uint32_t blk, void *buf);


  6. static unsigned char SPC_SHIFT;
  7. static unsigned int DATA_OFFSET;
  8. static unsigned int FAT1_OFFSET;
  9. static unsigned int ROOTDIR_POS;

  10. char fat32_init(uint32_t bootblk)
  11. {
  12.     struct BOOTSEC32 boot;
  13.     uint8_t tmp;

  14.     device_read_blk(bootblk, &boot);

  15.     if(boot.tail[420]!=0x55 || boot.tail[421]!=0xaa)
  16.         return 0;
  17.     if(strncmp(boot.filesysid, "FAT32   ",8)!=0)
  18.         return 0;
  19.     if(boot.BytesPerSec!=512)
  20.         return 0;
  21.     if(boot.NumFATs!=2)
  22.         return 0;

  23.     ROOTDIR_POS=boot.RootLocation;
  24.     tmp=boot.SecPerCluster;
  25.     SPC_SHIFT=0;
  26.     while(tmp>1)
  27.     {
  28.         tmp>>=1;
  29.         SPC_SHIFT++;
  30.     }
  31.     FAT1_OFFSET=bootblk+boot.ReservedSectors;
  32.     DATA_OFFSET=FAT1_OFFSET+2*boot.SecPerFAT-2*boot.SecPerCluster;  // cluster space starts from 2

  33.     return 1;
  34. }

  35. static uint32_t chain_next_cluster(uint32_t cluster)
  36. {
  37.     static uint32_t fat[128];
  38.     static uint32_t cache_fat;
  39.     uint32_t fat_blk = FAT1_OFFSET+cluster/128;
  40.     uint8_t offset=cluster%128;

  41.     if(fat_blk != cache_fat)
  42.     {
  43.         device_read_blk(fat_blk, fat);
  44.         cache_fat=fat_blk;
  45.     }
  46.     return fat[offset];
  47. }

  48. static uint32_t chain_next_blk(uint32_t blk)
  49. {
  50.     uint32_t blk_rel=blk-DATA_OFFSET;
  51.     uint8_t mask=(1<<SPC_SHIFT)-1;
  52.     uint32_t next_cluster;
  53.     if((blk_rel & mask)!=mask)
  54.         return blk+1;
  55.     next_cluster=chain_next_cluster(blk_rel>>SPC_SHIFT);
  56.     if((next_cluster & 0x0fffffff)!=0x0fffffff)
  57.         return DATA_OFFSET+(next_cluster<<SPC_SHIFT);
  58.     else    // End of file
  59.         return 0xffffffff;
  60. }

  61. char read_file_blk(myfile *f, void *buf)
  62. {
  63.     if(f->blk==0xffffffff)  // End of file
  64.         return 0;
  65.     device_read_blk(f->blk, buf);
  66.     f->blk=chain_next_blk(f->blk);
  67.     f->pos+=512;
  68.     if(f->pos>=f->length)
  69.         f->pos=f->length-1;
  70.     return 1;
  71. }

  72. void open_file0(myfile *f, uint32_t cluster, uint32_t len)  // open by cluster num
  73. {
  74.     f->blk = DATA_OFFSET+(cluster<<SPC_SHIFT);
  75.     f->pos = 0;
  76.     f->length = len;
  77.     f->cls_ini = cluster;
  78. }

  79. uint32_t get_rootdir(void)
  80. {
  81.     return ROOTDIR_POS;
  82. }
复制代码
  FAT32读取还算简单,因为文件是一个链表存放,顺藤摸瓜就可以了。链表每一节点是一个簇(Cluster),对应若干个扇区(磁盘的概念,也就是512字节单位的存储块). 从目录索引里面获得文件的起始簇号以后,一方面换算成扇区号去访问数据,另一方面到FAT表(文件分配表)去查下一个簇的编号。打开文件是以起始簇号为参数,而不是文件名,匹配文件名的过程交给主程序部分去负责了。

  只考虑播放WAV文件就简单了很多,跳过开头的44字节WAV文件头(我偷懒了都没判断一下是不是44.1k/16-bit立体声格式),后面就是PCM数据了,可以直接送给I2S模块输出。STM32的I2S硬件是和SPI复用的,在I2S模式下每次写数据寄存器只能是16-bit,也就是对应左或右声道一个PCM数据(因为CD格式也是16-bit),MCU可以在I2S数据发送成功后给一个中断,然后再写16-bit的数据,那么每秒要写88200次,这么做效率太低了,而且万一中断处理不及时就会造成破音。
  所以必须要用DMA来传输,DMA将要播放的PCM数据从SRAM当中搬运到I2S模块,这个过程不需要CPU参与。CPU可以在这个时候去干访问文件啊解码等等的活,也可以什么都不干进入SLEEP模式。因为I2S的数据是连续的,即使没有播放音乐,也需要让它不断发送"0",故将DMA配置为循环模式,从RAM中取数时,自动重置指针。DMA在内存缓冲区读完和读到一半的时候,可以给CPU一个中断请求,此时更新数据就可以了。
  我在RAM中开了2kB的空间作为PCM数据DMA缓冲
#define HALF_COUNT 256
uint32_t i2s_play_buf[2*HALF_COUNT];
volatile char low_half_fill;
low_half_fill 这个变量是由中断服务程序设置的,表示该写缓冲区的哪一半。负责填缓冲区的是 play_file() 这个函数:
  1. int8_t play_file(myfile *fp)
  2. {
  3.     uint32_t tbuf[128];
  4.     uint32_t *pcm_buf;
  5.     uint32_t lastpos=0;
  6.     int i, remain;
  7.     char fin;
  8.     char paused=0;
  9.     if(!read_file_blk(fp, tbuf))
  10.         return;
  11.     remain=128-11; // skip 44-byte WAV header
  12.     fin=0;

  13.     for(;;)
  14.     {
  15.         char button;
  16.         LED_off();
  17.         __WFI();    // wait DMA
  18.         button=get_button();
  19.         if(button)
  20.         {
  21.             if(button & BUTTON_PLAY)
  22.             {
  23.                 paused=!paused;
  24.                 if(paused)
  25.                     silence_buffer();
  26.             }
  27.             else
  28.             {
  29.                 silence_buffer();
  30.                 if(button & BUTTON_NEXT)
  31.                     return 1;
  32.                 if(button & BUTTON_PREV)
  33.                     return -1;
  34.                 return 0;
  35.             }
  36.         }
  37.         if(paused)
  38.             continue;

  39.         LED_on();
  40.         if(low_half_fill)
  41.             pcm_buf=i2s_play_buf;
  42.         else
  43.             pcm_buf=i2s_play_buf+HALF_COUNT;

  44.         for(i=0;i<remain;i++)   // remaining data
  45.             pcm_buf[i]=tbuf[i+(128-remain)];
  46.         for(;;)
  47.         {
  48.             int k;
  49.             if(!read_file_blk(fp, tbuf)) // end cluster
  50.             {
  51.                 for(;i<HALF_COUNT;i++)
  52.                     pcm_buf[i]=0;
  53.                 LED_off();
  54.                 __WFI();    // play rest
  55.                 silence_buffer();
  56.                 return 1;
  57.             }
  58.             k=0;
  59.             for(;i<HALF_COUNT && k<128;i++)
  60.             {
  61.                 pcm_buf[i]=tbuf[k];
  62.                 k++;
  63.             }
  64.             if(i==HALF_COUNT)
  65.             {
  66.                 remain=128-k;
  67.                 break;
  68.             }
  69.         }
  70.         if(fp->pos-lastpos>176400)
  71.         {
  72.             uart_wstr(".");
  73.             lastpos+=176400;
  74.         }
  75.     }
  76. }
复制代码
  读取文件的操作也在这个函数中进行。读SD卡会有I/O的延迟,但因为有缓冲,只要保证平均读取速度超过回放的速度就不会产生间断。读文件和DMA的同步依靠中断来实现:主程序完成一次缓冲区数据准备之后就用WFI指令进行休眠,当DMA取数据达到阈值时CPU唤醒——又有数据空缺可以干活了,然后主程序读SD卡取数据。此外,如果有按钮动作,或者文件读到尾部了,就把缓冲区清零然后返回。


  处理目录表的部分在 create_playlist() 这个函数当中。读文件和读目录使用的是同样的函数,顺序读取。每当找到一个WAV文件时,就把起始簇号、文件长度和文件名记录到一块内存 playlist_mem 中,后面播放时就不再访问根目录了。想了解实现细节的网友请下载源程序查看。

player_code.zip

141.9 KB, 阅读权限: 5, 下载次数: 65

GCC源程序

Gerber.zip

57.24 KB, 阅读权限: 5, 下载次数: 46

PCB Gerber文件

最新回复

謝謝分享.   详情 回复 发表于 2020-5-2 12:20

赞赏

3

查看全部赞赏

点赞 关注(4)
 

回复
举报

356

帖子

0

TA的资源

一粒金砂(中级)

沙发
 
 

回复

2721

帖子

0

TA的资源

纯净的硅(中级)

板凳
 
谢谢分享学习一下
 
 
 

回复

943

帖子

25

TA的资源

一粒金砂(中级)

4
 
电路图收藏了,以备后用。
 
 
 

回复

9184

帖子

6

TA的资源

管理员

5
 
自制无损音乐播放器
加EE小助手好友,
入技术交流群
EE服务号
精彩活动e手掌握
EE订阅号
热门资讯e网打尽
聚焦汽车电子软硬件开发
认真关注技术本身
 
 
 

回复

721

帖子

1

TA的资源

一粒金砂(高级)

6
 
 
 

回复

1403

帖子

1

TA的资源

纯净的硅(中级)

7
 
厉害了
个人签名HELLO_WATER
 
 
 

回复

565

帖子

0

TA的资源

一粒金砂(高级)

8
个人签名stm32/LoRa物联网:304350312
 
 
 

回复

7228

帖子

195

TA的资源

五彩晶圆(高级)

9
 
mARK下 可能要用到  
 
 
 

回复

655

帖子

29

TA的资源

版主

10
 
厉害!我曾经也做过WAV格式的音频播放,当时使用的是F401芯片,使用的STA350BW语音版,I2S接口,FatFs文件系统,双缓冲DMA模式,实际效果有点卡卡的,具体的演示在https://bbs.eeworld.com.cn/thread-489932-1-1.html,在语音驱动发面ST有专门的库文件,功能多多,自动识别音频格式和频率等等,有兴趣可以参考哦。ST有个ASI音频接口,据说效果更好呢。
个人签名QQ:252669569
 
 
 

回复

140

帖子

0

TA的资源

一粒金砂(中级)

11
 
厉害啊,之前也玩过stm32f407读取sd卡,读取的时候很不稳定,数据经常读不出来,开始以为是sd卡接触不良,但是重新上电有时又可以读出数据了,不知道是不是初始化程序有问题。
个人签名喜好电子DIY的小伙伴可以关注我的微信公众号:电子创客派
 
 
 

回复

23

帖子

0

TA的资源

一粒金砂(高级)

12
 
厉害厉害
 
 
 

回复

30

帖子

0

TA的资源

一粒金砂(初级)

13
 
謝謝分享
 
 
 

回复

1

帖子

0

TA的资源

一粒金砂(初级)

14
 

謝謝分享.

 
 
 

回复
您需要登录后才可以回帖 登录 | 注册

查找数据手册?

EEWorld Datasheet 技术支持

相关文章 更多>>
关闭
站长推荐上一条 1/7 下一条

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

About Us 关于我们 客户服务 联系方式 器件索引 网站地图 最新更新 手机版

站点相关: 国产芯 安防电子 汽车电子 手机便携 工业控制 家用电子 医疗电子 测试测量 网络通信 物联网

北京市海淀区中关村大街18号B座15层1530室 电话:(010)82350740 邮编:100190

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2025 EEWORLD.com.cn, Inc. All rights reserved
快速回复 返回顶部 返回列表