<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      《ESP32-S3使用指南—IDF版 V1.6》第四十一章音樂播放器實驗

      第四十一章音樂播放器實驗

      1)實驗平臺:正點原子DNESP32S3開發板

      2)章節摘自【正點原子】ESP32-S3使用指南—IDF版 V1.6

      3)購買鏈接:https://detail.tmall.com/item.htm?&id=768499342659

      4)全套實驗源碼+手冊+視頻下載地址:http://www.openedv.com/docs/boards/esp32/ATK-DNESP32S3.html

      5)正點原子官方B站:https://space.bilibili.com/394620890

      6)正點原子DNESP32S3開發板技術交流群:132780729

      155537c2odj87vz1z9vj6l

      155537nfqovl2gg9faaol9

      正點原子DNESP32S3開發板擁有串行音頻接口(SAI),支持SAI、LSB/MSB對齊、PCM/DSP、TDM和AC’97等協議,且外擴了一顆HIFI級CODEC芯片:ES8388,支持最高192K 24BIT的音頻播放,并且支持錄音(下一章介紹)本章,我們將利用DNESP32S3開發板實現一個簡單的音樂播放器(僅支持WAV播放)。
      本章分為如下幾個小節:
      41.1 WAV&ES8388&SAI簡介
      41.2 硬件設計
      41.3 程序設計
      41.4 下載驗證

      41.1 WAV&ES8388&SAI簡介
      本章知識點比較多,包括:WAV、ES8388和SAI等三個知識點。下面我們將分別向大家介紹。
      41.1.1 WAV簡介
      WAV即WAVE文件,WAV是計算機領域最常用的數字化聲音文件格式之一,它是微軟專門為Windows系統定義的波形文件格式(Waveform Audio),由于其擴展名為"*.wav"。它符合RIFF(ResourceInterchange File Format)文件規范,用于保存Windows平臺的音頻信息資源,被Windows平臺及其應用程序所廣泛支持,該格式也支持MSADPCM,CCITT A LAW 等多種壓縮運算法,支持多種音頻數字,取樣頻率和聲道,標準格式化的WAV文件和CD格式一樣,也是44.1K的取樣頻率,16 位量化數字,因此在聲音文件質量和CD相差無幾!
      WAV一般采用線性PCM(脈沖編碼調制)編碼,本章,我們也主要討論PCM的播放,因為這個最簡單。
      WAV 文件是由若干個Chunk組成的。按照在文件中的出現位置包括:RIFF WAVE Chunk、Format Chunk、 Fact Chunk(可選)和Data Chunk。每個Chunk由塊標識符、數據大小和數據三部分組成,如圖 41.1.1 所示:

      image001

      圖41.1.1 Chunk組成結構
      對于一個基本的WAVE文件而言,以下三種Chunk是必不可少的:文件中第一個Chunk是RIFF Chunk,然后是FMT Chunk,最后是Data Chunk。對于其他的Chunk,順序沒有嚴格的限制。使用WAVE文件的應用程序必須具有讀取以上三種chunk信息的能力,如果程序想要復制WAVE文件,必須拷貝文件中所有的chunk。本章,我們主要討論PCM,因為這個最簡單,它只包含3個Chunk,我們看一下它的文件構成,如圖41.1.2。

      image003

      圖41.1.2 PCM格式的wav文件構成
      可以看到,不同的Chunk有不同的長度,編碼文件時,按照Chunk的字節和位序排列好之后寫入文件頭,加上wav的后綴,就可以生成一個能被正確解析的wav文件了,對于PCM結構,我們只需要把獲取到的音頻數據填充到Data Chunk中即可。我們將利用ES8388實現16位,8Khz采樣率的單聲道WAV錄音(PCM格式)。
      首先,我們來看看RIFF塊(RIFF WAVE Chunk),該塊以“RIFF”作為標示,緊跟wav文件大小(該大小是wav文件的總大小-8),然后數據段為“WAVE”,表示是wav文件。RIFF塊的Chunk結構如下:

      typedef__PACKED_STRUCT
      {
          uint32_t ChunkID;        /*chunk id;這里固定為"RIFF",即0X46464952*/
          uint32_t ChunkSize;     /* 集合大小;文件總大小-8 */
          uint32_t Format;         /* 格式;WAVE,即0X45564157 */
      }ChunkRIFF;                 /*RIFF塊 */
      

      接著,我們看看Format塊(Format Chunk),該塊以“fmt”作為標示(注意有個空格!),一般情況下,該段的大小為16個字節,但是有些軟件生成的wav格式,該部分可能有18個字節,含有2個字節的附加信息。Format塊的Chunk結構如下:

      typedef__PACKED_STRUCT
      {
          uint32_t ChunkID;            /*chunk id;這里固定為"fmt ",即0X20746D66*/
          uint32_t ChunkSize ;         /* 子集合大小(不包括ID和Size);這里為:20. */
          uint16_t AudioFormat;        /* 音頻格式;0X01,表示線性PCM;0X11表示IMA ADPCM */
          uint16_t NumOfChannels;     /* 通道數量;1,表示單聲道;2,表示雙聲道; */
          uint32_t SampleRate;         /* 采樣率;0X1F40,表示8Khz */
          uint32_t ByteRate;           /* /字節速率; */
          uint16_t BlockAlign;         /* 塊對齊(字節); */
          uint16_t BitsPerSample;     /* 單個采樣數據大小;4位ADPCM,設置為4 */
      }ChunkFMT;                         /* fmt塊 */
      

      接下來,我們再看看Fact塊(Fact Chunk),該塊為可選塊,以“fact”作為標示,不是每個WAV文件都有,在非PCM格式的文件中,一般會在Format結構后面加入一個Fact塊,該塊Chunk結構如下:

      typedef__PACKED_STRUCT
      {
          uint32_t ChunkID;             /*chunk id;這里固定為"fact",即0X74636166;*/
          uint32_t ChunkSize ;         /* 子集合大小(不包括ID和Size);這里為:4. */
          uint32_t NumOfSamples;       /* 采樣的數量; */
      }ChunkFACT;                        /*fact塊 */
      

      DataFactSize是這個Chunk中最重要的數據,如果這是某種壓縮格式的聲音文件,那么從這里就可以知道他解壓縮后的大小。對于解壓時的計算會有很大的好處!不過本章我們使用的是PCM格式,所以不存在這個塊。
      最后,我們來看看數據塊(Data Chunk),該塊是真正保存wav數據的地方,以“data”作為該Chunk的標示,然后是數據的大小。數據塊的Chunk結構如下:

      typedef__PACKED_STRUCT
      {
          uint32_t ChunkID;            /*chunk id;這里固定為"data",即0X5453494C*/
          uint32_t ChunkSize;          /* 子集合大小(不包括ID和Size) */
      }ChunkDATA;                        /* data塊 */
      

      ChunkSize后緊接著就是wav數據。根據Format Chunk中的聲道數以及采樣bit數,wav數據的bit位置可以分成如表41.1.1.1所示的幾種形式:

      QQ截圖20251009103427

      表41.1.1.1 WAVE文件數據采樣格式
      本章,我們播放的音頻支持:16位和24位,立體聲,所以每個取樣為4/6個字節,低字節在前,高字節在后。在得到這些wav數據以后,通過SAI丟給ES8388,就可以欣賞音樂了。

      41.1.2 ES8388簡介
      ES8388是上海順芯推出的一款高性能、低功耗、高性價比的音頻編解碼器,有2個ADC通道和2個DAC通道,麥克風放大器,耳機放大器,數字音效以及模擬混合和增益功能組成。
      ES8388的主要特性有:
      ●SAI接口,支持最高192K,24bit音頻播放
      ●DAC信噪比96dB;ADC信噪比95dB
      ●支持主機和從機模式
      ●支持立體聲差分輸入/麥克風輸入
      ●支持左右聲道音量獨立調節
      ●支持40mW耳機輸出,無爆音
      ES8388的控制通過I2S接口(即數字音頻接口)同MCU進行音頻數據傳輸(支持音頻接收和發送),通過兩線(CE=0/1,即IIC接口)或三線(CE腳產生一個下降沿,即SPI接口)接口進行配置。ES8388的SAI接口,由4個引腳組成:
      ASDOUT:ADC數據輸出
      DSDIN:DAC數據輸入
      LRC:數據左/右對齊時鐘
      SCLK:位時鐘,用于同步
      ES8388可作為SAI主機,輸出LRC和SLCK時鐘,不過我們一般使用ES8388作為從機,接收LRC和SLCK。另外,ES8388的SAI接口支持4種不同的音頻數據模式:左(MSB)對齊標準、右(LSB)對齊標準、飛利浦(SAI)標準、DSP/PCM。本章,我們用飛利浦標準來傳輸SAI數據。
      飛利浦(SAI)標準模式,數據在跟隨LRC傳輸的BCLK的第二個上升沿時傳輸MSB,其他位一直到LSB按順序傳輸。傳輸依賴于字長、BCLK頻率和采樣率,在每個采樣的LSB和下一個采樣的MSB之間都應該有未用的BCLK周期。飛利浦標準模式的SAI數據傳輸協議如圖41.1.2.1所示:

      image005

      圖41.1.2.1 飛利浦標準模式SAI數據傳輸圖
      圖中,fs即音頻信號的采樣率,比如44.1Khz,因此可以知道,LRC的頻率就是音頻信號的采樣率。另外,ES8388還需要一個MCLK,本章我們采用DNESP32S3為其提供MCLK時鐘,MCLK的頻率必須等于256fs,也就是音頻采樣率的256倍。
      ES8388的框圖如圖41.1.2.2所示:

      image007

      圖41.1.2.2 ES8388框圖
      從上圖可以看出,ES8388內部有很多的模擬開關,用來選擇通道,同時還有一些運放調節器,用來設置增益和音量。
      本章,我們通過IIC接口(CE=0)連接ES8388,ES8388的IIC地址為:0X10。關于ES8388的IIC詳細介紹,請看其數據手冊第10頁5.2節。
      這里我們簡單介紹一下要正常使用ES8388來播放音樂,應該執行哪些配置。
      1,寄存器R0(00h),是芯片控制寄存器1,需要用到的位有:最高位SCPRese(bit7)用于控制ES8388的軟復位,寫0X80到該寄存器地址,即可實現軟復位ES8388,復位后,再寫0X00,ES8388恢復正常。VMIDSEL[1:0]位用于控制VMID(校正噪聲用),我們一般設置為10,即用500KΩ校正。
      2,寄存器R1(01h),是芯片控制寄存器2,主要要設置PdnAna(bit3),該位設置為1,模擬部分掉電,相當于復位模擬部分;設置為0,模擬部分才會工作,才可以聽到聲音。
      3,寄存器R2(02h),是芯片電源管理控制寄存器,所有位都要用到:adc_DigPDN(bit7)和dac_DigPDN(bit6)分別用于控制ADC和DAC的DSM、DEM、濾波器和數字接口的復位,1復位,0正常;adc_stm_rst(bit5)和dac_stm_rst(bit4)分別用于控制ADC和DAC的狀態機掉電,1掉電,0正常;ADCDLL_PDN(bit3)和DACDLL_PDN(bit2)分別用于控制ADC和DAC的DLL掉電,停止時鐘,1掉電,0正常;adcVref_PDN(bit1)和dacVref_PDN(bit0)分別控制ADC和DAC的模擬參考電壓掉電,1掉電,0正常;因此想要ADC和DAC都正常工作,R2寄存器必須全部設置為0,否則ADC或者DAC就會不能正常工作。
      4,寄存器R3(03h),是ADC電源管理控制寄存器,需要用到的位有:PdnAINL(bit7)和PdnAINR(bit6)用于控制左右輸入模擬通道的電源,1掉電,0正常;PdnADCL(bit5)和PdnADCR(bit4)用于控制左右通道ADC的電源,1掉電,0正常;pdnMICB(bit3)用于控制麥克風的偏置電源,1掉電,0正常;PdnADCBiasgen(bit2)用于控制偏置電源的產生,1掉電,0正常;這里6個位,我們全部設置為0,ADC部分就可以正常工作了。
      5,寄存器R4(04h),是DAC電源管理控制寄存器,需要用到的位有:PdnDACL(bit7)和PdnDACR(bit6)分別用于左右聲道DAC的電源控制,1掉電;0正常;LOUT1(bit5)和ROUT1(bit4)分別用于控制通道1的左右聲道輸出是能,1使能,0禁止;LOUT2(bit3)和ROUT2(bit2)分別用于控制通道2的左右聲道輸出是能,1使能,0禁止;我們一般設置PdnDACL和PdnDACR為0,使能左右聲道DAC,另外,兩個輸出通道則根據自己的需要設置。
      6,寄存器R8(08h),是主模式控制寄存器,需要用到的位有:MSC(bit7)用于控制接口模式,0從模式,1主模式;MCKDIV2(bit6)用于控制MCLK的2分頻,0不分頻,1二分頻;BCLK_INV(bit5)用于控制BCLK的反相,0不反相;1,反相;一般設置這3個位都為0。
      7,寄存器R9(09h),是ADC控制寄存器1,所有位都要用到:MicAmpL(bit7:4)和MicAmpR(bit3:0),這兩個分別用于控制MIC的左右通道增益,從0開始,3dB一個檔,最大增益為24dB,我們一般設置MicAmpR/L[3:0]=1000,即24dB。
      8,寄存器R10(0Ah),是ADC控制寄存器2,需要用到的位有:LINSE(bit7:6)和RINSE(bit5:4)分別選擇左右輸入通道,0選擇通道1,1選擇通道2。
      9,寄存器R12(0Ch),是ADC控制寄存器4,全部位都要用到:DATSEL(bit7:6)用于選擇數據格式,一般設置為01,左右邊數據等于左右聲道ADC數據;ADCLRP(bit5)在I2S模式下用于設置數據對其方式,一般設置為0,正常極性;ADCWL(bit4:2)用于選擇數據長度,我們設置011,選擇16位數據長度;ADCFORMAT(bit1:0)用于設置ADC數據格式,一般設置為00,選擇I2S數據格式。
      10,寄存器R13(0Dh),是ADC控制寄存器5,全部位都要用到:ADCFsMode(bit7)用于設置Fs模式,0單速模式,1雙倍速模式,一般設置為0;ADCFsRatio(bit4:0)用于設置ADC的MCLK和FS的比率,我們設置00010,即256倍關系。
      11,寄存器R16(10h)和R17(11h),這兩個寄存器分別用于控制ADC左右聲道的音量衰減,LADCVOL(bit7:0)和RADCVOL(bit7:0)分別控制左聲道和右聲道ADC的衰減,0.5dB每步,我們一般設置為0,即不衰減。
      12,寄存器R18(12h),是ADC控制寄存器10,全部位都要用到:ALCSEL(bit7:6)用于控制ALC,00表示ALC關閉,01表示ALC僅控制左聲道,10表示ALC僅控制右聲道11表示ALC立體聲控制;我們一般設置為11。
      13,寄存器R23(17h),是DAC控制寄存器1,需要用到的位有:DACLRSWAP(bit7)用于控制左右聲道數據交換,0正常,1互換,一般設置為0;DACLRP(bit6) 在I2S模式下用于設置數據對其方式,一般設置為0,正常極性;DACWL(bit5:3)用于選擇數據長度,我們設置011,選擇16位數據長度;ADCFORMAT(bit1:0)用于設置DAC數據格式,一般設置為00,選擇I2S數據格式。
      14,寄存器R24(18h),是DAC控制寄存器2,全部位都要用到:DACFsMode(bit7)用于設置Fs模式,0單速模式,1雙倍速模式,一般設置為0;DACFsRatio(bit4:0)用于設置DAC的MCLK和FS的比率,我們設置00010,即256倍關系。
      15,寄存器R26(1Ah)和R27(1Bh),這兩個寄存器分別用于控制DAC左右聲道的音量衰減,LDACVOL(bit7:0)和RDACVOL(bit7:0)分別控制左聲道和右聲道DAC的衰減,0.5dB每步,0表示0dB衰減,192表示96dB衰減;通過這兩個寄存器可以完成輸出音量的調節。
      16,寄存器R29(1Dh),是DAC控制寄存器7,需要用到的位有:ZeroL(bit7)和ZeroR(bit6)分別控制左右聲道的全0輸出,類似靜音,1輸出0,0正常;一般設置為0。Mono(bit5)用于單聲道控制,0立體聲,1單聲道;一般設置為0。SE(bit4:2)用于設置3D音效,0~7表示3D效果的強弱,0表示關閉。
      17,寄存器39(27h)和42(2Ah),分別控制DAC左右通道的混音器,LD2LO(bit7)和RD2RO(bit7)分別控制左右DAC的混音器開關,0關閉,1開啟,需設置為1;LI2LO(bit6)和RI2RO(bit6)分別控制左右輸入通道的混音器開關,0關閉,1開啟,一般設置為1;LI2LOVOL(bit5:3)和RI2ROVOL(bit5:3)分別控制左右輸入通道的增益,0~7表示-6 ~ -15dB的增益調節范圍,默認設置為111,即-15dB。
      18,寄存器43(2Bh),是DAC控制寄存器21,這里我們只關心slrck(bit7)這個位,用于控制DACLRC和ADCLRC是否共用,我們設置為1,表示共用。
      以上,就是我們使用ES8388時所需要用到的一些寄存器,按照以上所述,對各個寄存器進行相應的配置,即可使用ES8388正常播放音樂了。關于ES8388更詳細的寄存器設置說明,我們這里就不再介紹了,請大家參考ES8388的數據手冊自行研究。

      41.1.3 I2S控制器介紹
      I2S(Inter-IC Sound,集成電路內置音頻總線)是一種同步串行通信協議,通常用于兩個數字音頻設備之間傳輸音頻數據。DNESP32S3內置兩個I2S接口(I2S0和I2S1),為多媒體應用,尤其是為數字音頻應用提供了靈活的數據通信接口。
      I2S標準總線定義了三種信號:串行時鐘信號BCK、字選擇信號WS和串行數據信號SD。一個基本的I2S數據總線有一個主機和一個從機。主機和從機的角色在通信過程中保持不變。DNESP32S3的I2S模塊包含獨立的發送單元和接收單元,能夠保證優良的通信性能。
      I2S有如下功能:
      主機模式:I2Sn作為主機,BCK/WS向外部輸出,向從機發送或從其接收數據。
      從機模式:I2Sn作為從機,BCK/WS從外部輸入,從主機接收或向其發送數據。
      全雙工:主機與從機之間的發送線和接收線各自獨立,發送數據和接收數據同時進行。
      半雙工:主機和從機只能有一方先發送數據,另一方接收數據。發送數據和接收數據不能同時進行。
      TDM RX模式:利用時分復用方式接收脈沖編碼調制(PCM)數據,并將其通過DMA存入儲存器的模式。信號線包括BCK、WS和DATA。可以接收最多16個通道的數據。通過用戶配置,可支持TDM Philips格式、TDM MSB對齊格式、TDM PCM格式等。
      PDM RX模式:接收脈沖密度調制(PDM)數據,并將其通過DMA存入儲存器的模式。信號線包括WS和DATA。通過用戶配置,可支持PDM標準格式等。
      TDM TX模式:通過DMA從儲存器中取得脈沖編碼調制(PCM)數據,并利用時分復用方式將其發送的模式。信號線包括BCK、WS和DATA,可以發送最多16個通道的數據。通過用戶配置,可支持TDM Philips格式、TDM MSB對齊格式、TDM PCM格式等。
      PDM TX模式:通過DMA從儲存器中取得脈沖密度調制(PDM)數據,并將其發送的模式。信號線包括WS和DATA。通過用戶配置,可支持PDM標準格式等。
      PCM-to-PDM TX模式(僅對I2S0有效):通過DMA從儲存器中取得脈沖編碼調制(PCM)數據,將其轉換為脈沖密度調制(PDM)數據,并將其發送的主機模式。信號線包括WS和DATA。通過用戶配置,可支持PDM標準格式等。
      PDM-to-PCM RX模式(僅對I2S0有效):接收脈沖密度調制(PDM)數據,將其轉換為脈沖編碼調制(PCM)數據,并將其通過DMA存入儲存器的主機模式或從機模式。信號線包括WS和DATA。通過用戶配置,可支持PDM標準格式等。
      更詳細的內容請大家參考《ESP32-S3技術參考手冊.pdf》第28章。

      41.2 硬件設計
      41.2.1例程功能
      本章實驗功能簡介:開機后,先初始化各外設,然后檢測字庫是否存在,如果檢測無問題,則開始循環播放SD卡MUSIC文件夾里面的歌曲(必須在SD卡根目錄建立一個MUSIC文件夾,并存放歌曲在里面),在SPILCD上顯示歌曲名字、播放時間、歌曲總時間、歌曲總數目、當前歌曲的編號等信息。KEY0用于選擇下一曲,KEY2用于選擇上一曲,KEY3用來控制暫停/繼續播放。LED閃爍,提示程序運行狀態。
      41.2.2硬件資源
      本實驗,大家需要準備1個SD卡(在里面新建一個MUSIC文件夾,并存放一些歌曲在MUSIC文件夾下)和一個耳機(非必備),分別插入SD卡接口和耳機接口,然后下載本實驗就可以通過耳機或板載喇叭來聽歌了。實驗用到的硬件資源如下:

      1. LED燈
        LED -IO0
        2.獨立按鍵
        KEY0(XL9555) - IO1_7
        KEY1(XL9555) - IO1_6
        KEY2(XL9555) - IO1_5
        KEY3(XL9555) - IO1_4
      2. XL9555
        IIC_SDA-IO41
        IIC_SCL-IO42
      3. SPILCD
        CS-IO21
        SCK-IO12
        SDA-IO11
        DC-IO40(在P5端口,使用跳線帽將IO_SET和LCD_DC相連)
        PWR- IO1_3(XL9555)
        RST- IO1_2(XL9555)
      4. SD
        CS-IO2
        SCK-IO12
        MOSI-IO11
        MISO-IO13
      5. ES8388音頻CODEC芯片(IIC端口0)
        IIC_SDA-IO41
        IIC_SCL-IO42
        I2S_BCK_IO-IO46
        I2S_WS_IO-IO9
        I2S_DO_IO-IO10
        I2S_DI_IO-IO14
        IS2_MCLK_IO-IO3

      41.2.3 原理圖
      DNESP32S3開發板板載了ES8388解碼芯片的驅動電路,原理圖如圖41.1.1所示:

      image009

      圖41.2.3.1 ES838原理圖
      圖中,PHONE接口可以用來插耳機,并連接了板載的喇叭SPEAKER(開發板正上方)。

      41.3 程序設計
      41.3.1 程序流程圖
      程序流程圖能幫助我們更好的理解一個工程的功能和實現的過程,對學習和設計工程有很好的主導作用。下面看看本實驗的程序流程圖:

      image012

      圖41.3.1.1音頻播放實驗程序流程圖
      41.3.2 I2S函數解析
      ESP-IDF提供了一套API來配置I2S。要使用此功能,需要導入必要的頭文件:

      #include"driver/i2s.h"
      #include"driver/i2s_std.h"
      #include"driver/i2s_pdm.h"
      

      接下來,作者將介紹一些常用的ESP32-S3中的I2S函數,這些函數的描述及其作用如下:
      1,設置I2S引腳
      該函數用給定的配置,來配置I2S總線,該函數原型如下所示:

      esp_err_ti2s_set_pin(i2s_port_t i2s_num, consti2s_pin_config_t *pin);
      
      

      該函數的形參描述如下表所示:

      QQ截圖20251009103451

      表41.3.2.1i2s_set_pin()函數形參描述
      該函數的返回值描述,如下表所示:

      QQ截圖20251009103500

      表41.3.2.2函數i2s_set_pin ()返回值描述
      該函數使用i2s_pin_config_t類型的結構體變量傳入,該結構體的定義如下所示:

      QQ截圖20251009103508

      表41.3.2.3i2s_pin_config_t結構體參數值描述
      完成上述結構體參數配置之后,可以將結構傳遞給 i2s_set_pin () 函數,用以實例化IIC并返回IIC句柄。
      2,安裝I2S驅動
      該函數安裝I2S驅動,該函數原型如下所示:

      esp_err_ti2s_driver_install(i2s_port_t i2s_num,
                                   consti2s_config_t *i2s_config,
                                   intqueue_size,
                                   void *i2s_queue);
      

      該函數的形參描述如下表所示:

      QQ截圖20251009103519

      表41.3.2.4i2s_driver_install()函數形參描述
      該函數的返回值描述,如下表所示:

      QQ截圖20251009103527

      表41.3.2.5函數i2s_driver_install ()返回值描述
      3,處理緩沖區
      該函數將TX DMA緩沖區的內容歸零,該函數原型如下所示:

      esp_err_ti2s_zero_dma_buffer(i2s_port_t i2s_num);
      
      

      該函數的形參描述如下表所示:

      QQ截圖20251009103536

      表41.3.2.6i2s_zero_dma_buffer ()函數形參描述
      該函數的返回值描述,如下表所示:

      QQ截圖20251009103546

      表41.3.2.7函數i2s_zero_dma_buffer()返回值描述

      41.3.3 音頻播放驅動解析
      在IDF版的30_music例程中,作者在30_music\components\BSP路徑下新增了一個I2S文件夾和一個ES8388文件夾,分別用于存放i2s.c、i2s.h和es8388.c以及es8388.h這四個文件。其中,i2s.h和es8388.h文件負責聲明I2S以及ES8388相關的函數和變量,而i2s.c和es8388.c文件則實現了I2S以及ES8388的驅動代碼。下面,我們將詳細解析這四個文件的實現內容。
      1,i2s驅動
      音樂文件我們要通過SD卡來傳給單片機,那我們自然要用到文件系統。LCD、按鍵交互這些我們也需要實現。
      由于播放功能涉及到多個外設的配合使用,用文件系統讀音頻文件,做播放控制等,所以我們把ES8388的硬件驅動放到components\BSP目錄下,播放功能作為APP放到main目錄下。
      這里我們只講解核心代碼,詳細的源碼請大家參考光盤本實驗對應源碼,I2S的驅動主要包括兩個文件:i2s.c和i2s.h。
      除去I2S的管腳,我們需要初始其它IO的模式,我們在頭文件sai.h中定義SAI的引腳,方便如果IO變更之后作修改:

      #defineI2S_NUM       (I2S_NUM_0)         /*I2S端口 */
      #defineI2S_BCK_IO    (GPIO_NUM_46)     /* 設置串行時鐘引腳,ES8388_SCLK */
      #defineI2S_WS_IO     (GPIO_NUM_9)          /* 設置左右聲道的時鐘引腳,ES8388_LRCK */
      #defineI2S_DO_IO     (GPIO_NUM_10)     /* ES8388_SDOUT */
      #define I2S_DI_IO    (GPIO_NUM_14)         /*ES8388_SDIN */
      #defineIS2_MCLK_IO   (GPIO_NUM_3)           /* ES8388_MCLK */
      #defineSAMPLE_RATE   (44100)               /* 采樣率 */
      

      接下來開始介紹i2s.c,主要是I2S的初始化代碼如下:

      /* I2S默認配置 */
      #defineI2S_CONFIG_DEFAULT() { \
          .mode = (i2s_mode_t)(I2S_MODE_MASTER |I2S_MODE_TX | I2S_MODE_RX),     \
          .sample_rate = SAMPLE_RATE,                                                   \
          .bits_per_sample =I2S_BITS_PER_SAMPLE_16BIT,                              \
          .channel_format =I2S_CHANNEL_FMT_RIGHT_LEFT,                              \
          .communication_format =I2S_COMM_FORMAT_STAND_I2S,                       \
          .intr_alloc_flags = 0,                                                        \
          .dma_buf_count = 8,                                                            \
          .dma_buf_len = 256,                                                            \
          .use_apll = false                                                              \
      }
      /**
      * @brief      初始化I2S
      * @param      無
      * @retval     ESP_OK:初始化成功;其他:失敗
      */
      esp_err_t i2s_init(void)
      {
          esp_err_t ret_val =ESP_OK;
          i2s_pin_config_t pin_config = {
              .bck_io_num= I2S_BCK_IO,
              .ws_io_num= I2S_WS_IO,
              .data_out_num= I2S_DO_IO,
              .data_in_num= I2S_DI_IO,
              .mck_io_num= IS2_MCLK_IO,
          };
         
          i2s_config_t i2s_config =I2S_CONFIG_DEFAULT();
          i2s_config.sample_rate= SAMPLE_RATE;
          i2s_config.bits_per_sample= I2S_BITS_PER_SAMPLE_16BIT;
          i2s_config.use_apll= true;
          ret_val |=i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL);
          ret_val |=i2s_set_pin(I2S_NUM, &pin_config);
          ret_val |=i2s_zero_dma_buffer(I2S_NUM);
          return ret_val;
      }
      /**
      * @brief      I2S TRX啟動
      * @param      無
      * @retval     無
      */
      voidi2s_trx_start(void)
      {
          i2s_start(I2S_NUM);
      }
      /**
      * @brief      I2S TRX停止
      * @param      無
      * @retval     無
      */
      voidi2s_trx_stop(void)
      {
          i2s_stop(I2S_NUM);
      }
      /**
      * @brief      I2S卸載
      * @param      無
      * @retval     無
      */
      voidi2s_deinit(void)
      {
          i2s_driver_uninstall(I2S_NUM);
      }
      /**
      * @brief      設置采樣率
      * @param      sampleRate  : 采樣率
      * @param      bits_sample :位寬
      * @retval     無
      */
      voidi2s_set_samplerate_bits_sample(intsamplerate,int bits_sample)
      {
          i2s_set_clk(I2S_NUM,samplerate,bits_sample,I2S_CHANNEL_STEREO);
      }
      /**
      * @brief      I2S傳輸數據
      * @param      buffer: 數據存儲區的首地址
      * @param      frame_size: 數據大小
      * @retval     無
      */
      size_ti2s_tx_write(uint8_t *buffer, uint32_tframe_size)
      {
          size_t bytes_written;
          i2s_write(I2S_NUM,buffer, frame_size, &bytes_written, 100);
          return bytes_written;
      }
      /**
      * @brief      I2S讀取數據
      * @param      buffer: 讀取數據存儲區的首地址
      * @param      frame_size: 讀取數據大小
      * @retval     無
      */
      size_ti2s_rx_read(uint8_t *buffer, uint32_tframe_size)
      {
          size_t bytes_written;
          i2s_read(I2S_NUM,buffer, frame_size, &bytes_written, 1000);
          return bytes_written;
      }
      

      函數i2s_init()完成初始化I2S,該初始化不需要像I2C以及IO擴展芯片那樣設置傳參,通過配置相關的結構體并安裝I2S的驅動和配置I2S引腳以及將TX DMA緩沖區的內容歸零。函數sai1_samplerate_set則是用前面介紹的查表法,根據采樣率來設置SAI的時鐘。函數i2s_trx_start()用于啟動I2S驅動,在調用了i2s_driver_install()之后不需要調用這個函數(它是自動啟動的),但是在調用了i2s_stop()之后調用該函數是必要的。而函數i2s_trx_stop()用于停止I2S驅動,在調用i2s_driver_uninstall()之前不需要調用i2s_stop()。i2s_set_samplerate_bits_sample()用于設置I2S RX和TX的時鐘和位寬度。函數i2s_tx_write()用于將數據寫入I2S DMA傳輸緩沖區。函數i2s_rx_read()從I2S DMA接收緩沖區讀取數據。以上是對I2S驅動文件下部分函數的功能概述,具體內容請參照該驅動文件。
      2,ES8388驅動
      ES8388主要用來將音頻信號轉換為數字信號或將數字信號轉換為音頻信號,接下來,我們開始介紹ES8388的幾個函數,代碼如下:

      /**
      * @brief      ES8388初始化
      * @param      無
      * @retval     0,初始化正常
      *              其他,錯誤代碼
      */
      uint8_tes8388_init(i2c_obj_t self)
      {
          esp_err_t ret_val =ESP_OK;
          if (self.init_flag== ESP_FAIL)
          {
              /* 初始化IIC */
              iic_init(I2C_NUM_0);
          }
          es8388_i2c_master = self;
          /* 軟復位ES8388*/
          ret_val |=es8388_write_reg(0, 0x80);
          ret_val |=es8388_write_reg(0, 0x00);
          /* 等待復位 */
          vTaskDelay(100);
          ret_val |=es8388_write_reg(0x01, 0x58);
          ret_val |=es8388_write_reg(0x01, 0x50);
          ret_val |=es8388_write_reg(0x02, 0xF3);
          ret_val |=es8388_write_reg(0x02, 0xF0);
          /* 麥克風偏置電源關閉 */
          ret_val |=es8388_write_reg(0x03, 0x09);
          /* 使能參考 500K驅動使能 */
          ret_val |=es8388_write_reg(0x00, 0x06);
          /* DAC電源管理,不打開任何通道 */
          ret_val |=es8388_write_reg(0x04, 0x00);
          /* MCLK不分頻 */
          ret_val |=es8388_write_reg(0x08, 0x00);
          /* DAC控制 DACLRC與ADCLRC相同 */
          ret_val |=es8388_write_reg(0x2B, 0x80);
          /* ADC L/R PGA增益配置為+24dB */
          ret_val |=es8388_write_reg(0x09, 0x88);
          /* ADC數據選擇為left data = left ADC, right data=left ADC音頻數據為16bit */
          ret_val |=es8388_write_reg(0x0C, 0x4C);
          /* ADC配置 MCLK/采樣率=256 */
          ret_val |=es8388_write_reg(0x0D, 0x02);
          /* ADC數字音量控制將信號衰減 L 設置為最小!!! */
          ret_val |=es8388_write_reg(0x10, 0x00);
          /* ADC數字音量控制將信號衰減 R 設置為最小!!! */
          ret_val |=es8388_write_reg(0x11, 0x00);
          /* DAC 音頻數據為16bit */
          ret_val |=es8388_write_reg(0x17, 0x18);
          /* DAC 配置 MCLK/采樣率=256 */
          ret_val |=es8388_write_reg(0x18, 0x02);
          /* DAC數字音量控制將信號衰減 L 設置為最小!!! */
          ret_val |=es8388_write_reg(0x1A, 0x00);
          /* DAC數字音量控制將信號衰減 R 設置為最小!!! */
          ret_val |=es8388_write_reg(0x1B, 0x00);
          /* L混頻器 */
          ret_val |=es8388_write_reg(0x27, 0xB8);
          /* R混頻器 */
          ret_val |=es8388_write_reg(0x2A, 0xB8);
          vTaskDelay(100);
          if (ret_val!= ESP_OK)
          {
              while(1)
              {
                  printf("ES8388初始化失敗!!!\r\n");
                  vTaskDelay(500);
              }
          }
          else
          {
              printf("ES8388初始化成功!!!\r\n");
          }
         
          return 0;
      }
      /**
      * @brief      IIC寫入函數
      * @param      slave_addr:ES8388地址
      * @param      reg_add:寄存器地址
      * @param      data:寫入的數據
      * @retval     無
      */
      esp_err_t es8388_write_reg(uint8_treg_addr, uint8_t data)
      {
          i2c_buf_t buf[2] = {
              {.len = 1, .buf = ?_addr},
              {.len = 1, .buf = &data},
          };
          i2c_transfer(&es8388_i2c_master,ES8388_ADDR >> 1, 2, buf,I2C_FLAG_STOP);
          return ESP_OK;
      }
      /**
      * @brief      讀取數據
      * @param      reg_add:寄存器地址
      * @param      p_data:讀取的數據
      * @retval     無
      */
      esp_err_tes8388_read_reg(uint8_t reg_addr, uint8_t *pdata)
      {
          i2c_buf_t buf[2] = {
              {.len = 1, .buf = ?_addr},
              {.len = 1, .buf = pdata},
          };
          i2c_transfer(&es8388_i2c_master,ES8388_ADDR >> 1, 2, buf,I2C_FLAG_WRITE|
                                                                               I2C_FLAG_READ |
                                                                               I2C_FLAG_STOP);
          return ESP_OK;
      }
      /**
      * @brief      設置ES8388工作模式
      * @param      fmt : 工作模式
      * @arg        0, 飛利浦標準I2S;
      * @arg        1, MSB(左對齊);
      * @arg        2, LSB(右對齊);
      * @arg        3, PCM/DSP
      * @param      len : 數據長度
      * @arg        0, 24bit
      * @arg        1, 20bit
      * @arg        2, 18bit
      * @arg        3, 16bit
      * @arg        4, 32bit
      * @retval     無
      */
      voides8388_sai_cfg(uint8_t fmt, uint8_t len)
      {
          fmt &= 0x03;
          len &= 0x07;    /* 限定范圍 */
          es8388_write_reg(23, (fmt << 1) | (len << 3));  /* R23,ES8388工作模式設置 */
      }
      /**
      * @brief      設置耳機音量
      * @param      volume : 音量大小(0 ~ 33)
      * @retval     無
      */
      voides8388_hpvol_set(uint8_t volume)
      {
          if (volume> 33)
          {
              volume = 33;
          }
         
          es8388_write_reg(0x2E,volume);
          es8388_write_reg(0x2F,volume);
      }
      /**
      * @brief      設置喇叭音量
      * @param      volume : 音量大小(0 ~ 33)
      * @retval     無
      */
      voides8388_spkvol_set(uint8_t volume)
      {
          if (volume> 33)
          {
              volume = 33;
          }
         
          es8388_write_reg(0x30,volume);
          es8388_write_reg(0x31,volume);
      }
      /**
      * @brief      設置3D環繞聲
      * @param      depth : 0 ~ 7(3D強度,0關閉,7最強)
      * @retval     無
      */
      voides8388_3d_set(uint8_t depth)
      {
          depth &= 0x7;       /* 限定范圍 */
          es8388_write_reg(0x1D, depth<< 2);    /* R7,3D環繞設置 */
      }
      /**
      * @brief      ES8388 DAC/ADC配置
      * @param      dacen : dac使能(1)/關閉(0)
      * @param      adcen : adc使能(1)/關閉(0)
      * @retval     無
      */
      voides8388_adda_cfg(uint8_t dacen, uint8_t adcen)
      {
          uint8_t tempreg = 0;
         
          tempreg |= ((!dacen) << 0);
          tempreg |= ((!adcen) << 1);
          tempreg |= ((!dacen) << 2);
          tempreg |= ((!adcen) << 3);
          es8388_write_reg(0x02,tempreg);
      }
      /**
      * @brief      ES8388 DAC輸出通道配置
      * @param      o1en : 通道1使能(1)/禁止(0)
      * @param      o2en : 通道2使能(1)/禁止(0)
      * @retval     無
      */
      voides8388_output_cfg(uint8_t o1en, uint8_t o2en)
      {
          uint8_t tempreg = 0;
          tempreg |= o1en * (3 << 4);
          tempreg |= o2en * (3 << 2);
          es8388_write_reg(0x04,tempreg);
      }
      /**
      * @brief      ES8388 MIC增益設置(MIC PGA增益)
      * @param      gain : 0~8, 對應0~24dB  3dB/Step
      * @retval     無
      */
      voides8388_mic_gain(uint8_t gain)
      {
          gain &= 0x0F;
          gain |= gain << 4;
          es8388_write_reg(0x09, gain);    /* R9,左右通道PGA增益設置 */
      }
      /**
      * @brief      ES8388 ALC設置
      * @param      sel
      * @arg        0,關閉ALC
      * @arg        1,右通道ALC
      * @arg        2,左通道ALC
      * @arg        3,立體聲ALC
      * @param      maxgain : 0~7,對應-6.5~+35.5dB
      * @param      minigain: 0~7,對應-12~+30dB6dB/STEP
      * @retval     無
      */
      voides8388_alc_ctrl(uint8_t sel, uint8_tmaxgain, uint8_t mingain)
      {
          uint8_t tempreg = 0;
          tempreg = sel << 6;
          tempreg |= (maxgain& 0x07) << 3;
          tempreg |=mingain & 0x07;
          es8388_write_reg(0x12,tempreg);    /* R18,ALC設置 */
      }
      /**
      * @brief      ES8388 ADC輸出通道配置
      * @param      in : 輸入通道
      * @arg        0, 通道1輸入
      * @arg        1, 通道2輸入
      * @retval     無
      */
      voides8388_input_cfg(uint8_t in)
      {
          es8388_write_reg(0x0A, (5 * in) << 4);    /* ADC1 輸入通道選擇L/R  INPUT1*/
      }
      

      以上代碼中,es8388_init函數用于初始化es8388,這里只是通用配置(ADC&DAC),初始化完成后,并不能正常播放音樂,還需要通過es8388_adda_cfg函數使能DAC,然后通過設置es8388_output_cfg選擇DAC輸出,通過es8388_sai_cfg配置ES8388工作模式,最后設置音量才可以接收I2S音頻數據,實現音樂播放。
      3,wavplay驅動
      wavpaly主要用于wav格式的音頻文件解碼,接下來看看wavplay.c里面的幾個函數,代碼如下:

      /**
      * @brief      WAV解析初始化
      * @param      fname : 文件路徑+文件名
      * @param      wavx  : 信息存放結構體指針
      * @retval     0,打開文件成功
      *             1,打開文件失敗
      *             2,非WAV文件
      *             3,DATA區域未找到
      */
      uint8_twav_decode_init(uint8_t *fname,__wavctrl *wavx)
      {
          FIL *ftemp;
          uint8_t *buf;
          uint32_t br = 0;
          uint8_t res = 0;
          ChunkRIFF *riff;
          ChunkFMT *fmt;
          ChunkFACT *fact;
          ChunkDATA *data;
         
          ftemp = (FIL*)mymalloc(sizeof(FIL));
          buf =mymalloc(512);
         
          if (ftemp && buf)                                        /* 內存申請成功 */
          {
              res =f_open(ftemp, (TCHAR*)fname,FA_READ);    /* 打開文件 */
              
              if (res == FR_OK)
              {
                  f_read(ftemp, buf, 512, (UINT *)&br);         /* 讀取512字節在數據 */
                  riff = (ChunkRIFF*)buf;                       /* 獲取RIFF塊 */
                  
      if (riff->Format == 0x45564157)                   /* 是WAV文件 */
                  {
                      fmt = (ChunkFMT*)(buf + 12);             /* 獲取FMT塊 */
                      /*讀取FACT塊*/
                      fact = (ChunkFACT*)(buf + 12 + 8 + fmt->ChunkSize);  
                     
                      if (fact->ChunkID== 0x74636166 || fact->ChunkID== 0x5453494C)
                      {
                          /* 具有fact/LIST塊的時候(未測試)*/
                          wavx->datastart= 12 + 8 + fmt->ChunkSize+8+fact->ChunkSize;
                      }
                      else
                      {
                          wavx->datastart= 12 + 8 + fmt->ChunkSize;
                      }
                     
                      data = (ChunkDATA*)(buf + wavx->datastart); /* 讀取DATA塊 */
                     
                      if (data->ChunkID== 0x61746164)               /* 解析成功! */
                      {
                          wavx->audioformat= fmt->AudioFormat;     /* 音頻格式 */
                          wavx->nchannels= fmt->NumOfChannels;     /* 通道數 */
                          wavx->samplerate= fmt->SampleRate;       /* 采樣率 */
                          wavx->bitrate= fmt->ByteRate * 8;         /* 得到位速 */
                          wavx->blockalign= fmt->BlockAlign;       /* 塊對齊 */
                          wavx->bps = fmt->BitsPerSample;/* 位數,16/24/32位 */
                          
                          wavx->datasize= data->ChunkSize;          /* 數據塊大小 */
                          wavx->datastart= wavx->datastart + 8;/* 數據流開始的地方. */
                          
                          printf("wavx->audioformat:%d\r\n", wavx->audioformat);
                          printf("wavx->nchannels:%d\r\n", wavx->nchannels);
                          printf("wavx->samplerate:%d\r\n", wavx->samplerate);
                          printf("wavx->bitrate:%d\r\n", wavx->bitrate);
                          printf("wavx->blockalign:%d\r\n", wavx->blockalign);
                          printf("wavx->bps:%d\r\n", wavx->bps);
                          printf("wavx->datasize:%d\r\n", wavx->datasize);
                          printf("wavx->datastart:%d\r\n", wavx->datastart);  
                      }
                      else
                      {
                          res = 3;   /* data區域未找到. */
                      }
                  }
                  else
                  {
                      res = 2;       /* 非wav文件 */
                  }
              }
              else
              {
                  res = 1;           /* 打開文件錯誤 */
              }
          }
         
          f_close(ftemp);
          free(ftemp);            /* 釋放內存 */
          free(buf);
         
          return 0;
      }
      /**
      * @brief      獲取當前播放時間
      * @param      fname : 文件指針
      * @param      wavx  : wavx播放控制器
      * @retval     無
      */
      voidwav_get_curtime(FIL *fx,__wavctrl *wavx)
      {
          long long fpos;
          wavx->totsec= wavx->datasize / (wavx->bitrate/ 8); /* 歌曲總長度(單位:秒) */
          fpos = fx->fptr-wavx->datastart;                 /* 得到當前文件播放到的地方 */
          wavx->cursec= fpos*wavx->totsec/ wavx->datasize;  /* 當前播放到第多少秒了? */
      }
      /**
      * @brief      播放某個wav文件
      * @param      fname : 文件路徑+文件名
      * @retval     KEY0_PRES,錯誤
      *             KEY1_PRES,打開文件失敗
      *             其他,非WAV文件
      */
      uint8_twav_play_song(uint8_t *fname)
      {
          uint8_t key = 0;
          uint8_t t = 0;
          uint8_t res;
          i2s_play_end =ESP_FAIL;
          i2s_play_next_prev =ESP_FAIL;
          g_audiodev.file = (FIL*)malloc(sizeof(FIL));
          g_audiodev.tbuf =malloc(WAV_TX_BUFSIZE);
         
          if (g_audiodev.file ||g_audiodev.tbuf)
          {
              /* 得到文件的信息 */
              res =wav_decode_init(fname, &wavctrl);
              /* 解析文件成功 */
              if (res == 0)
              {
                  if (wavctrl.bps == 16)
                  {
                      /* 飛利浦標準,16位數據長度 */
                      es8388_sai_cfg(0, 3);
                      i2s_set_samplerate_bits_sample(wavctrl.samplerate,
                                                    I2S_BITS_PER_SAMPLE_16BIT);
                  }
                  else if (wavctrl.bps == 24)
                  {
                      /* 飛利浦標準,24位數據長度 */
                      es8388_sai_cfg(0, 0);
                      i2s_set_samplerate_bits_sample(wavctrl.samplerate,
                                                    I2S_BITS_PER_SAMPLE_24BIT);
                   }
                  audio_stop();
                  if (MUSICTask_Handler== NULL)
                  {
                      taskENTER_CRITICAL(&my_spinlock);
                      /* 創建任務1,任務函數 */
                      xTaskCreatePinnedToCore((TaskFunction_t)music,
                     
                                              /* 任務名稱 */
                                              (const char*    )"music",
                                             
                                              /* 任務堆棧大小 */
                                              (uint16_t       )MUSIC_STK_SIZE,
                                             
                                              /* 傳入給任務函數的參數 */
                                              (void*          )NULL,
                                             
                                              /* 任務優先級 */
                                              (UBaseType_t    )MUSIC_PRIO,
                                             
                                              /* 任務句柄 */
                                              (TaskHandle_t*  )&MUSICTask_Handler,
                                             
                                              /* 該任務哪個內核運行 */
                                              (BaseType_t     ) 0);
                                             
                      taskEXIT_CRITICAL(&my_spinlock);
                  }
                  /* 打開文件 */
                  res =f_open(g_audiodev.file, (TCHAR*)fname,FA_READ);
                  if (res == 0)
                  {
                      /* 開始音頻播放 */
                      audio_start();
                      vTaskDelay(100);
                      audio_start();
                      vTaskDelay(100);
                      while (res == 0)
                      {
                          while (1)
                          {
                              if (i2s_play_end== ESP_OK)
                              {
                                  res =KEY0_PRES;
                                  break;
                              }
                              key =xl9555_key_scan(0);
                              
                              /* 暫停 */
                              if (key ==KEY3_PRES)
                              {
                                  if ((g_audiodev.status& 0x0F) == 0x03)
                                  {
                                      audio_stop();
                                      vTaskDelay(100);
                                  }
                                  else if ((g_audiodev.status& 0x0F) == 0x00)
                                  {
                                      audio_start();
                                      vTaskDelay(100);
                                  }
                              }
                              
                              /* 下一曲/上一曲 */
                              if (key ==KEY2_PRES || key == KEY0_PRES)
                              {
                                  i2s_play_next_prev =ESP_OK;
                                  vTaskDelay(100);
                                  res =KEY0_PRES;
                                  break;
                              }
                              
                              /* 暫停不刷新時間 */
                              if ((g_audiodev.status& 0x0F) == 0x03)
                              {
                                  /* 得到總時間和當前播放的時間 */
                                  wav_get_curtime(g_audiodev.file, &wavctrl);
                                  audio_msg_show(wavctrl.totsec,
                                              wavctrl.cursec,
                                              wavctrl.bitrate);
                              }
                              
                              t++;
                              if (t == 20)
                              {
                                  t = 0 ;
                                  LED_TOGGLE();
                              }
                              
                              if ((g_audiodev.status& 0x01) == 0)
                              {
                                  vTaskDelay(10);
                              }
                              else
                              {
                                  break;
                              }
                          }
                          /* 退出切換歌曲 */
                          if (key ==KEY2_PRES || key == KEY0_PRES)
                          {
                              res = key;
                              break;
                          }
                      }
                      audio_stop();
                  }
                  else
                  {
                      res = 0xFF;
                  }
              }
              else
              {
                  res = 0xFF;
              }
          }
          else
          {
              res = 0xFF;
          }
         
          /* 釋放內存 */
          free(g_audiodev.tbuf);
         
          /* 釋放內存 */
          free(g_audiodev.file);
         
          return res;
      }
      

      以上代碼中,wav_decode_init函數,用來對wav音頻文件進行解析,得到wav的詳細信息(音頻采樣率,位數,數據流起始位置等);wav_play_song函數,是播放WAV最終執行的函數,該函數解析完WAV文件后,設置ES8388和I2S的參數(采樣率,位數等),然后不斷填充數據,實現WAV播放,該函數中還進行了按鍵檢測,實現上下曲切換和暫停/播放等操作。
      4,audioplay驅動
      這部分我們需要根據ES8388推薦的初始化順序時行配置。我們需要借助SD卡和文件系統把我們需要播放的歌曲傳給ES8388播放。我們在User目錄下新建一個《APP》文件夾,同時在該目錄下新建audioplay.c和audioplay.h并加入到工程。
      首先判斷音樂文件類型,符合條件的再把相應的文件數據發送給ES8388,我們在FATFS的擴展文件中已經實現了判斷文件類型這個功能,在圖片顯示實驗也演示了這部分代碼的使用,我們把這個功能封裝成了audio_get_tnum()函數,這部分參考我們光盤源碼即可。接下來我們來分析一下audio play()和audio_play_song ()函數,實現播放歌曲的功能,代碼如下:

      /**
      * @brief      播放音樂
      * @param      無
      * @retval     無
      */
      voidaudio_play(void)
      {
          uint8_t res;
         
          /* 目錄 */
          FF_DIR wavdir;
         
          /* 文件信息 */
          FILINFO *wavfileinfo;
         
          /* 帶路徑的文件名 */
          uint8_t *pname;
         
          /* 音樂文件總數 */
          uint16_t totwavnum;
         
          /* 當前索引 */
          uint16_t curindex;
         
          /* 鍵值 */
          uint8_t key;
          uint32_t temp;
         
          /* 音樂offset索引表 */
          uint32_t *wavoffsettbl;
          /* 開啟DAC關閉ADC */
          es8388_adda_cfg(1, 0);
         
          /* DAC選擇通道1輸出 */
          es8388_output_cfg(1, 1);
          /* 打開音樂文件夾 */
          while (f_opendir(&wavdir, "0:/MUSIC"))
          {
              text_show_string(30, 190, 240, 16, "MUSIC文件夾錯誤!", 16, 0, BLUE);
              vTaskDelay(200);
              
              /* 清除顯示 */
              lcd_fill(30, 190, 240, 206, WHITE);
              vTaskDelay(200);
          }
          /* 得到總有效文件數 */
          totwavnum =audio_get_tnum((uint8_t *)"0:/MUSIC");
         
          /* 音樂文件總數為0 */
          while (totwavnum== NULL)
          {
              text_show_string(30, 190, 240, 16, "沒有音樂文件!", 16, 0, BLUE);
              vTaskDelay(200);
              
              /* 清除顯示 */
              lcd_fill(30, 190, 240, 146, WHITE);
              vTaskDelay(200);
          }
         
          /* 申請內存 */
          wavfileinfo = (FILINFO*)malloc(sizeof(FILINFO));
         
          /* 為帶路徑的文件名分配內存 */
          pname =malloc(255 * 2 + 1);
         
          /* 申請4*totwavnum個字節的內存,用于存放音樂文件offblock索引 */
          wavoffsettbl =malloc(4 * totwavnum);
         
          /* 內存分配出錯 */
          while (!wavfileinfo|| !pname || !wavoffsettbl)
          {
              text_show_string(30, 190, 240, 16, "內存分配失敗!", 16, 0, BLUE);
              vTaskDelay(200);
              
              /* 清除顯示 */
              lcd_fill(30, 190, 240, 146, WHITE);
              vTaskDelay(200);
          }
         
          /* 記錄索引,打開目錄 */
          res =f_opendir(&wavdir, "0:/MUSIC");
         
          if (res == FR_OK)
          {
              /* 當前索引為0 */
              curindex = 0;
              
              /* 全部查詢一遍 */
              while (1)
              {
                  /* 記錄當前index */
                  temp =wavdir.dptr;
                  /* 讀取目錄下的一個文件 */
                  res =f_readdir(&wavdir, wavfileinfo);
                  
                  if ((res != FR_OK) || (wavfileinfo->fname[0] == 0))
                  {
                      break;  /* 錯誤了/到末尾了,退出 */
                  }
                  res =exfuns_file_type(wavfileinfo->fname);
                  
                  /* 取高四位,看看是不是音樂文件 */
                  if ((res & 0xF0) == 0x40)
                  {
                      /* 記錄索引 */
                      wavoffsettbl[curindex] = temp;
                      curindex++;
                  }
              }
          }
         
          /* 從0開始顯示 */
          curindex = 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(30, 190,lcd_self.width - 1, 190 + 16, WHITE);
              audio_index_show(curindex+ 1, totwavnum);
              
              /* 顯示歌曲名字 */
              text_show_string(30, 190,lcd_self.width - 60, 16,
                              (char *)wavfileinfo->fname, 16, 0, BLUE);
              
              /* 播放這個音頻文件 */
              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;  /* 產生了錯誤 */
              }
          }
          /* 釋放內存 */
          free(wavfileinfo);
         
          /* 釋放內存 */
          free(pname);
         
          /* 釋放內存 */
          free(wavoffsettbl);
      }
      /**
      * @brief      播放某個音頻文件
      * @param      fname : 文件名
      * @retval     按鍵值
      * @arg        KEY0_PRES , 下一曲.
      * @arg        KEY2_PRES , 上一曲.
      * @arg        其他 , 錯誤
      */
      uint8_taudio_play_song(uint8_t *fname)
      {
          uint8_t res;  
         
          res =exfuns_file_type((char *)fname);
          switch (res)
          {
              case T_WAV:
                  res =wav_play_song(fname);
                  break;
              case T_MP3:
                  /* 自行實現 */
                  break;
              default:            /* 其他文件,自動跳轉到下一曲 */
                  printf("can'tplay:%s\r\n", fname);
                  res =KEY0_PRES;
                  break;
          }
          return res;
      }
      

      這里,audio_play函數在main函數里面被調用,該函數首先設置ES8388相關配置,然后查找SD卡里面的MUSIC文件夾,并統計該文件夾里面總共有多少音頻文件(統計包括:WAV/MP3/APE/FLAC等),然后,該函數調用audio_play_song函數,按順序播放這些音頻文件。
      在audio_play_song函數里面,通過判斷文件類型,調用不同的解碼函數,本章,支持WAV文件,通過wav_play_song函數實現WAV解碼。

      41.3.4 CMakeLists.txt文件
      打開本實驗BSP下的CMakeLists.txt文件,其內容如下所示:

      set(src_dirs
                  IIC
                  LCD
                  LED
                  SDIO
                  SPI
                  XL9555
                  ES8388
                  I2S)
      set(include_dirs
                  IIC
                  LCD
                  LED
                  SDIO
                  SPI
                  XL9555
                  ES8388
                  I2S)
      set(requires
                  driver
                  fatfs)
      idf_component_register(SRC_DIRS${src_dirs}
      INCLUDE_DIRS ${include_dirs}REQUIRES ${requires})
      component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)
      

      上述的紅色I2C、ES8388驅動需要由開發者自行添加,以確保音頻播放驅動能夠順利集成到構建系統中。這一步驟是必不可少的,它確保了音頻播放驅動的正確性和可用性,為后續的開發工作提供了堅實的基礎。
      打開本實驗main文件下的CMakeLists.txt文件,其內容如下所示:

      idf_component_register(
          SRC_DIRS
              "."
              "APP"
          INCLUDE_DIRS
              "."
              "APP")
      

      上述的紅色APP驅動需要由開發者自行添加,在此便不做贅述了。

      41.3.5 實驗應用代碼
      打開main/main.c文件,該文件定義了工程入口函數,名為app_main。該函數代碼如下。

      i2c_obj_ti2c0_master;
      /**
      * @brief      程序入口
      * @param      無
      * @retval     無
      */
      voidapp_main(void)
      {
          esp_err_t ret;
          uint8_t key = 0;
          /* 初始化NVS*/
          ret =nvs_flash_init();
      if (ret ==ESP_ERR_NVS_NO_FREE_PAGES ||
      ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
          {
              ESP_ERROR_CHECK(nvs_flash_erase());
              ret =nvs_flash_init();
          }
          /* 初始化LED*/
          led_init();
         
          /* 初始化IIC0*/
          i2c0_master =iic_init(I2C_NUM_0);
         
          /* 初始化SPI*/
          spi2_init();
         
          /* 初始化IO擴展芯片 */  
          xl9555_init(i2c0_master);
         
          /* 初始化LCD*/
          lcd_init();
         
          /* ES8388初始化 */
          es8388_init(i2c0_master);
         
          /* 開啟DAC關閉ADC */
          es8388_adda_cfg(1, 0);
          es8388_input_cfg(0);
         
          /* DAC選擇通道輸出 */
          es8388_output_cfg(1, 1);
         
          /* 設置耳機音量 */
          es8388_hpvol_set(20);
         
          /* 設置喇叭音量 */
          es8388_spkvol_set(20);
         
          /* 打開喇叭 */
          xl9555_pin_write(SPK_EN_IO,0);
         
          /* I2S初始化 */
          i2s_init();
         
          /* 檢測不到SD卡 */
          while (sd_spi_init())
          {
              lcd_show_string(30, 110, 200, 16, 16, "SDCard Error!", RED);
              vTaskDelay(500);
              lcd_show_string(30, 130, 200, 16, 16, "PleaseCheck! ", RED);
              vTaskDelay(500);
          }
         
          /* 檢查字庫 */
          while (fonts_init())
          {
              /* 清屏 */
              lcd_clear(WHITE);
              lcd_show_string(30, 30, 200, 16, 16, "ESP32-S3", RED);
              
              /* 更新字庫 */
              key =fonts_update_font(30, 50, 16, (uint8_t *)"0:", RED);
              /* 更新失敗 */
              while (key)
              {
                  lcd_show_string(30, 50, 200, 16, 16, "FontUpdate Failed!", RED);
                  vTaskDelay(200);
                  lcd_fill(20, 50, 200 + 20, 90 + 16, WHITE);
                  vTaskDelay(200);
              }
              lcd_show_string(30, 50, 200, 16, 16, "FontUpdate Success!   ", RED);
              vTaskDelay(1500);
              
              /* 清屏 */
              lcd_clear(WHITE);
          }
          /* 為fatfs相關變量申請內存 */
          ret =exfuns_init();
         
          /* 實驗信息顯示延時 */
          vTaskDelay(500);
          text_show_string(30, 50, 200, 16, "正點原子ESP32開發板",16,0, RED);
          text_show_string(30, 70, 200, 16, "音樂播放器 實驗", 16, 0, RED);
          text_show_string(30, 90, 200, 16, "正點原子@ALIENTEK", 16, 0, RED);
          text_show_string(30, 110, 200, 16, "KEY0:NEXT   KEY2:PREV", 16, 0, RED);
          text_show_string(30, 130, 200, 16, "KEY3:PAUSE/PLAY", 16, 0, RED);
          while (1)
          {
              /* 播放音樂 */
              audio_play();
          }
      }
      

      到這里本實驗的代碼基本就編寫完成了,我們準備好音樂文件放到SD卡根目錄下的《MUSIC》夾下測試本實驗的代碼。

      41.4 下載驗證
      在代碼編譯成功之后,我們下載代碼到開發板上,程序先執行字庫檢測,然后當檢測到SD卡根目錄的MUSIC文件夾有音頻文件(WAV格式音頻)的時候,就開始自動播放歌曲了,如圖41.4.1所示:

      image013

      圖41.4.1音樂播放中
      從上圖可以看出,總共1首歌曲,當前正在播放第1首歌曲,歌曲名、播放時間、總時長、碼率等也都有顯示。此時LED會隨著音樂的播放而閃爍。
      此時我們便可以聽到開發板板載喇叭播放出來的音樂了,也可以在開發板的PHONE端子插入耳機來聽歌。同時,我們可以通過按KEY0和KEY2來切換下一曲和上一曲,通過KEY_UP暫停和繼續播放。

      posted @ 2025-10-10 10:19  正點原子  閱讀(77)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 成全世界免费高清观看| 亚洲a∨国产av综合av下载| 色综合人人超人人超级国碰| 国产精品性视频一区二区| 香港经典a毛片免费观看播放| 国产成人久久精品二三区| 客服| 日韩视频一区二区三区视频| 国产乱码精品一品二品| A级毛片免费完整视频| 胶州市| 人人妻人人澡人人爽曰本| 久99久热免费视频播放| 亚洲日本欧洲二区精品 | 亚洲丰满熟女一区二区v| 午夜爽爽爽男女免费观看影院| 午夜天堂av天堂久久久| 蜜臀av日韩精品一区二区| 国产精品午夜无码AV天美传媒| 涪陵区| 熟女精品色一区二区三区| 国产日本一区二区三区久久| 成人3D动漫一区二区三区| 暖暖影院日本高清...免费| 999精品色在线播放| 午夜免费福利小电影| 日本肥老妇色xxxxx日本老妇 | 日本欧美一区二区三区在线播放| 丰满妇女强制高潮18xxxx| 国产AV巨作丝袜秘书| 久久av无码精品人妻出轨| 无码国产精品成人| 人妻丝袜无码专区视频网站| 999福利激情视频| 4399理论片午午伦夜理片| 亚洲精品一区三区三区在| 国产精品特级毛片一区二区三区| 亚洲国产成人久久精品不卡| 国产精品久久久久久久久电影网| 国产三级精品三级在线看| 色伦专区97中文字幕|