第38章 I2S—音頻播放與錄音輸入—零死角玩轉STM32-F429系列


 

第38章     I2S—音頻播放與錄音輸入

全套200集視頻教程和1000PDF教程請到秉火論壇下載:www.firebbs.cn

野火視頻教程優酷觀看網址:http://i.youku.com/firege

 

 

 

本章參考資料:《STM32F4xx 中文參考手冊》、《STM32F4xx規格書》、庫幫助文檔《stm32f4xx_dsp_stdperiph_lib_um.chm》及《I2S BUS》。

若對I2S通訊協議不了解,可先閱讀《I2S BUS》文檔的內容學習。

關於音頻編譯碼器WM8978,請參考其規格書《WM8978_v4.5》來了解。

38.1 I2S簡介

Inter-IC Sount Bus(I2S)是飛利浦半導體公司(現為恩智浦半導體公司)針對數字音頻設備之間的音頻數據傳輸而制定的一種總線標准。在飛利浦公司的I2S標准中,既規定了硬件接口規范,也規定了數字音頻數據的格式。

38.1.1 數字音頻技術

現實生活中的聲音是通過一定介質傳播的連續的波,它可以由周期和振幅兩個重要指標描述。正常人可以聽到的聲音頻率范圍為20Hz~20KHz。現實存在的聲音是模擬量,這對聲音保存和長距離傳輸造成很大的困難,一般的做法是把模擬量轉成對應的數字量保存,在需要還原聲音的地方再把數字量的轉成模擬量輸出,參考圖 381

381 音頻轉換過程

模擬量轉成數字量過程,一般可以分為三個過程,分別為采樣、量化、編碼,參考圖 382。用一個比源聲音頻率高的采樣信號去量化源聲音,記錄每個采樣點的值,最后如果把所有采樣點數值連接起來與源聲音曲線是互相吻合的,只是它不是連續的。在圖中兩條藍色虛線距離就是采樣信號的周期,即對應一個采樣頻率(FS),可以想象得到采樣頻率越高最后得到的結果就與源聲音越吻合,但此時采樣數據量越越大,一般使用44.1KHz采樣頻率即可得到高保真的聲音。每條藍色虛線長度決定着該時刻源聲音的量化值,該量化值有另外一個概念與之掛鈎,就是量化位數。量化位數表示每個采樣點用多少位表示數據范圍,常用有16bit24bit32bit,位數越高最后還原得到的音質越好,數據量也會越大。

382 聲音數字化過程

WM8978是一個低功耗、高質量的立體聲多媒體數字信號編譯碼器,集成DACADC,可以實現聲音信號量化成數字量輸出,也可以實現數字量音頻數據轉換為模擬量聲音驅動揚聲器。這樣使用WM8978芯片解決了聲音與數字量音頻數據轉換問題,並且通過配置WM8978芯片相關寄存器可以控制轉換過程的參數,比如采樣頻率,量化位數,增益、濾波等等。

WM8978芯片是一個音頻編譯碼器,但本身沒有保存音頻數據功能,它只能接收其它設備傳輸過來的音頻數據進行轉換輸出到揚聲器,或者把采樣到的音頻數據輸出到其它具有存儲功能的設備保存下來。該芯片與其他設備進行音頻數據傳輸接口就是I2S協議的音頻接口。

38.1.2 I2S總線接口

I2S總線接口有3個主要信號,但只能實現數據半雙工傳輸,后來為實現全雙工傳輸有些設備增加了擴展數據引腳。STM32f42x系列控制器支持擴展的I2S總線接口。

(1)    SD(Serial Data):串行數據線,用於發送或接收兩個時分復用的數據通道上的數據(僅半雙工模式),如果是全雙工模式,該信號僅用於發送數據。

(2)    WS(Word Select):字段選擇線,也稱幀時鍾(LRC)線,表明當前傳輸數據的聲道,不同標准有不同的定義。WS線的頻率等於采樣頻率(FS)。

(3)    CK(Serial Clock):串行時鍾線,也稱位時鍾(BCLK),數字音頻的每一位數據都對應有一個CK脈沖,它的頻率為:2*采樣頻率*量化位數,2代表左右兩個通道數據。

(4)    ext_SD(extend Serial Data):擴展串行數據線,用於全雙工傳輸的數據接收。

另外,有時為使系統間更好地同步,還要傳輸一個主時鍾(MCK)STM32F42x系列控制器固定輸出為256* FS

 

38.1.3 音頻數據傳輸協議標准

隨着技術的發展,在統一的I2S硬件接口下,出現了多種不同的數據格式,可分為左對齊(MSB)標准、右對齊(LSB)標准、I2S Philips標准。另外,STM32F42x系列控制器還支持PCM(脈沖編碼調)音頻傳輸協議。下面以STM32F42x系列控制器資源解釋這四個傳輸協議。

STM32f42x系列控制器I2S的數據寄存器只有16bit,並且左右聲道數據一般是緊鄰傳輸,為正確得到左右兩個聲道數據,需要軟件控制數據對應通道數據寫入或讀取。另外,音頻數據的量化位數可能不同,控制器支持16bit24bit32bit三種數據長度,因為數據寄存器是16bit的,所以對於24bit32bit數據長度需要發送兩個。為此,可以產生四種數據和幀格式組合:

    16位數據封裝在16位幀中

    16位數據封裝在32位幀中

    24位數據封裝在32位幀中

    32位數據封裝在32位幀中

當使用32位數據包中的16位數據時,前16(MSB)為有效位,16LSB被強制清零,無需任何軟件操作或DMA請求(只需一個讀/寫操作)。如果程序使用DMA傳輸(一般都會用),則24位和32位數據幀需要對數據寄存器執行兩次DMA操作。24位的數據幀,硬件會將8位非有效位擴展到帶有0位的32位。對於所有數據格式和通信標准而言,始終會先發送最高有效位(MSB優先)

1.    I2S Philips標准

使用WS信號來指示當前正在發送的數據所屬的通道,為0時表示左通道數據。該信號從當前通道數據的第一個位(MSB)之前的一個時鍾開始有效。發送方在時鍾信號(CK)的下降沿改變數據,接收方在上升沿讀取數據。WS信號也在SCK的下降沿變化。參考圖 383,為24bit數據封裝在32bit幀傳輸波形。正如之前所說,WS線頻率對於采樣頻率FS,一個WS線周期包括發送左聲道和右聲道數據,在圖中實際需要64CK周期來完成一次傳輸。

383 I2S Philips標准24bit傳輸

2.    左對齊標准

WS發生翻轉同時開始傳輸數據,參考圖 384,為24bit數據封裝在32bit幀傳輸波形。該標准較少使用。注意此時WS1時,傳輸的是左聲道數據,這剛好與I2S Philips標准相反。

384 左對齊標准24bit傳輸

3.    右對齊標准

與左對齊標准類似,參考圖 385,為24bit數據封裝在32bit幀傳輸波形。

385 右對齊標准24bit傳輸

4.    PCM標准

PCM即脈沖編碼調制,模擬語音信號經過采樣量化以及一定數據排列就是PCM了。WS不再作為聲道數據選擇。它有兩種模式,短幀模式和長幀模式,以WS信號高電平保持時間為判別依據,長幀模式保持13CK周期,短幀模式只保持1CK周期,可以通過相關寄存器位選擇。如果有多通道數據是在一個WS周期內傳輸完成的,傳完左聲道數據就緊跟發送右聲道數據。圖 386為單聲道數據16bit擴展到32bit數據幀發送波形。

386 PCM標准16bit傳輸

38.2 I2S功能框圖

STM32f42x系列控制器有兩個I2SI2S2I2S3,兩個的資源是相互獨立的,但分別與SPI2SPI3共用大部分資源。這樣I2S2SPI2只能選擇一個功能使用,I2S3SPI3相同道理。資源共用包括引腳共用和部分寄存器共用,當然也有部分是專用的。SPI已經在之前相關章節做了詳細講解,建議先看懂SPI相關內容再學習I2S

控制器的I2S支持兩種工作模式,主模式和從模式;主模式下使用自身時鍾發生器生成通信時鍾。I2S功能框圖參考圖 387

 

387 I2S功能框圖

1.    功能引腳

I2SSD映射到SPIMOSI引腳,ext_SD映射到SPIMISO引腳,WS映射到SPINSS引腳,CK映射到SPISCK引腳。MCKI2S專用引腳,用於主模式下輸出時鍾或在從模式下輸入時鍾。I2S時鍾發生器可以由控制器內部時鍾源分頻產生,亦可采用CKIN引腳輸入時鍾分頻得到,一般使用內部時鍾源即可。控制器I2S引腳分布參考表 381

381 STM32f42x系列控制器I2S引腳分布

引腳

I2S2

I2S3

SD

PC3/PB15/PI3

PC12/PD6/PB5

ext_SD

PC2/PB14/PI2

PC11/PB4

WS

PB12/PI0/PB9

PA4/PA15

CK

PB10/PB13/PI1/PD3

PC10/PB3

MCK

PC6

PC7

CKIN

PC9

其中,PI0PI1不能用於I2S的全雙工模式。

2.    數據寄存器

I2S有一個與SPI共用的SPI數據寄存器(SPI_DR),有效長度為16bit,用於I2S數據發送和接收,它實際由三個部分組成,一個移位寄存器、一個發送緩沖區和一個接收緩沖區,當處於發送模式時,向SPI_DR寫入數據先保存在發送緩沖區,總線自動把發送緩沖區內容轉入到移位寄存器中進行傳輸;在接收模式下,實際接收到的數據先填充移位寄存器,然后自動轉入接收緩沖區,軟件讀取SPI_DR時自動從接收緩沖區內讀取。I2S是掛載在APB1總線上的。

3.    邏輯控制

I2S的邏輯控制通過設置相關寄存器位實現,比如通過配置SPI_I2S配置寄存器(SPI_I2SCFGR)的相關位可以實現選擇I2SSPI模式切換、選擇I2S工作在主模式還是從模式並且選擇是發送還是接收、選擇I2S標准、傳輸數據長度等等。SPI控制寄存器2(SPI_CR2)可用於設置相關中斷和DMA請求使能,I2S5個中斷事件,分別為發送緩沖區為空、接收緩沖區非空、上溢錯誤、下溢錯誤和幀錯誤。SPI狀態寄存器(SPI_SR)用於指示當前I2S狀態。

4.    時鍾發生器

I2S比特率用來確定I2S數據線上的數據流和I2S時鍾信號頻率。I2S比特率=每個通道的位數×通道數×音頻采樣頻率。

388I2S時鍾發生器內部結構圖。I2SxCLK(x可選23)可以通過RCC_CFGR寄存器的I2SSRC位選擇使用PLLI2S時鍾作為I2S時鍾源或I2S_CKIN引腳輸入時鍾作為I2S時鍾源。一般選擇內部PLLI2S(通過R分頻系數)作為時鍾源。例程程序設置PLLI2S時鍾為258MHzR分頻系數為3,此時I2SxCLK時鍾為86MHz

388 I2S時鍾發生器內部結構

SPI_I2S預分頻器寄存器(SPI_I2SPR)MCKOE位用於設置MCK引腳時鍾輸出使能;ODD位設置預分頻器的奇數因子,實際分頻值=I2SDIV*2+ODDI2SDIV8位線性分頻器,不可設置為01

當使能MCK時鍾輸出,即MCKOE=1時,采樣頻率計算如下:

FS = I2SxCLK/[(16*2)*((2*I2SDIV)+ODD)*8)](通道幀寬度為16bit時)

FS = I2SxCLK/[(32*2)*((2*I2SDIV)+ODD)*4)](通道幀寬度為32bit時)

當禁止MCK時鍾輸出,即MCKOE=0時,采樣頻率計算如下:

FS = I2SxCLK/[(16*2)*((2*I2SDIV)+ODD))](通道幀寬度為16bit時)

FS = I2SxCLK/[(32*2)*((2*I2SDIV)+ODD))](通道幀寬度為32bit時)

38.3 WM8978音頻編譯碼器

WM8978是一個低功耗、高質量的立體聲多媒體數字信號編譯碼器。它主要應用於便攜式應用。它結合了立體聲差分麥克風的前置放大與揚聲器、耳機和差分、立體聲線輸出的驅動,減少了應用時必需的外部組件,比如不需要單獨的麥克風或者耳機的放大器。

高級的片上數字信號處理功能,包含一個5路均衡功能,一個用於ADC和麥克風或者線路輸入之間的混合信號的電平自動控制功能,一個純粹的錄音或者重放的數字限幅功能。另外在ADC的線路上提供了一個數字濾波的功能,可以更好的應用濾波,比如"減少風噪聲"。

WM8978可以被應用為一個主機或者一個從機。基於共同的參考時鍾頻率,比如 12MHz13MHz,內部的PLL可以為編譯碼器提供所有需要的音頻時鍾。與STM32控制器連接使用,STM32一般作為主機,WM8978作為從機。

389WM8978芯片內部結構示意圖,參考來自《WM8978_v4.5》。該圖給人的第一印象感覺就是很復雜,密密麻麻很多內容,特別有很多"開關"。實際上,每個開關對應着WM8978內部寄存器的一個位,通過控制寄存器的就可以控制開關的狀態。

389 WM8978內部結構

1.    輸入部分

WM8978結構圖的左邊部分是輸入部分,可用於模擬聲音輸入,即用於錄音輸入。有三個輸入接口,一個是由LINLIPRINRIP組合而成的偽差分立體聲麥克風輸入,一個是由L2R2組合的立體聲麥克風輸入,還有一個是由AUXLAUXR組合的線輸入或用來傳輸告警聲的輸入。

2.    輸出部分

WM8978結構圖的右邊部分是聲音放大輸出部分,LOUT1ROUT1用於耳機驅動,LOUT2ROUT2用於揚聲器驅動,OUT3OUT4也可以配置成立體聲線輸出,OUT4也可以用於提供一個左右聲道的單聲道混合。

3.    ADC和DAC

WM8978結構圖的中邊部分是芯片核心內容,處理聲音的ADDA轉換。ADC部分對聲音輸入進行處理,包括ADC濾波處理、音量控制、輸入限幅器/電平自動控制等等。DAC部分控制聲音輸出效果,包括DAC5路均衡器、DAC 3D放大、DAC輸出限幅以及音量控制等等處理。

4.    通信接口

WM8978有兩個通信接口,一個是數字音頻通信接口,另外一個是控制接口。音頻接口是采用I2S接口,支持左對齊、右對齊和I2S標准模式,以及DSP模式A和模擬B。控制接口用於控制器發送控制命令配置WM8978運行狀態,它提供2線或3線控制接口,對於STM32控制器,我們選擇2線接口方式,它實際就是I2C總線方式,其芯片地址固定為0011010。通過控制接口可以訪問WM8978內部寄存器,實現芯片工作環境配置,總共有58個寄存器,標示為R0R57,限於篇幅問題這里不再深入探究,每個寄存器意義參考《WM8978_v4.5》了解。

WM8978寄存器是16bit長度,高7([15:9]bit)用於標示寄存器地址,低9為有實際意義,比如對於圖 389中的某個開關。所以在控制器向芯片發送控制命令時,必須傳輸長度為16bit的指令,芯片會根據接收命令高7位值尋址。

5.    其他部分

WM8978作為主從機都必須對時鍾進行管理,由內部PLL單元控制。另外還有電源管理單元。

38.4 WAV格式文件

WAV是微軟公司開發的一種音頻格式文件,用於保存Windows平台的音頻信息資源,它符合資源互換文件格式(Resource Interchange File FormatRIFF)文件規范。標准格式化的WAV文件和CD格式一樣,也是44.1K的取樣頻率,16位量化數字,因此在聲音文件質量和CD相差無幾!WAVE是錄音時用的標准的WINDOWS文件格式,文件的擴展名為"WAV",數據本身的格式為PCM或壓縮型,屬於無損音樂格式的一種。

38.4.1 RIFF文件規范

RIFF有不同數量的chunk(區塊)組成,每個chunk由"標識符"、"數據大小"和"數據"三個部分組成,"標識符"和"數據大小"都是占用4個字節空間。簡單RIFF格式文件結構參考圖 3810。最開始是ID為"RIFF"的chunkSize為"RIFF"chunk數據字節長度,所以總文件大小為Size+8。一般來說,chunk不允許內部再包含chunk,但有兩個例外,ID為"RIFF"和"LIST"的chunk卻是允許。對此"RIFF"在其"數據"首4個字節用來存放"格式標識碼(Form Type)","LIST"則對應"LIST Type"。

 

3810 RIFF文件格式結構

38.4.2 WAVE文件

WAVE文件是非常簡單的一種RIFF文件,其"格式標識碼"定義為WAVERIFF chunk包括兩個子chunkID分別為fmtdata,還有一個可選的fact chunkFmt chunk用於標示音頻數據的屬性,包括編碼方式、聲道數目、采樣頻率、每個采樣需要的bit數等等信息。fact chunk是一個可選chunk,一般當WAVE文件由某些軟件轉化而成就包含fact chunkdata chunk包含WAVE文件的數字化波形聲音數據。WAVE整體結構如表 382

382 WAVE文件結構

標識碼("RIFF")

數據大小

格式標識碼("WAVE")

"fmt"

"fmt"塊數據大小

"fmt"數據

"fact"(可選)

"fact"塊數據大小

"fact"數據

"data"

聲音數據大小

聲音數據

data chunkWAVE文件主體部分,包含聲音數據,一般有兩個編碼格式:PCMADPCMADPCM(自適應差分脈沖編碼調制)屬於有損壓縮,現在幾乎不用,絕大部分WAVE文件是PCM編碼。PCM編碼聲音數據可以說是在"數字音頻技術"介紹的源數據,主要參數是采樣頻率和量化位數。

383為量化位數為16bit時不同聲道數據在data chunk數據排列格式。

383 16bit聲音數據格式

單聲道

采樣一

采樣二

……

低字節

高字節

低字節

高字節

……

雙聲道

采樣一

……

左聲道

右聲道

……

低字節

高字節

低字節

高字節

……

38.4.3 WAVE文件實例分析

利用winhex工具軟件可以非常方便以十六進制查看文件,圖 3811為名為"張國榮-一盞小明燈.wav"文件使用winhex工具打開的部分界面截圖。這部分截圖是WAVE文件頭部分,聲音數據部分數據量非常大,有興趣可以使用winhex查看。

3811 WAV文件頭實例

下面對文件頭進行解讀,參考表 384

384 WAVE文件格式說明

  

偏移地址

字節數

數據類型

十六進制源碼

內容

文件頭

00H

4

char

52 49 46 46

"RIFF"標識符

04H

4

long int

F4 FE 83 01

文件長度:0x0183FEF4(注意順序)

08H

4

char

57 41 56 45

"WAVE"標識符

0CH

4

char

66 6D 74 20

"fmt ",最后一位為空格

10H

4

long int

10 00 00 00

fmt chunk大小:0x10

14H

2

int

01 00

編碼格式:0x01PCM

16H

2

int

02 00

聲道數目:0x01為單聲道,0x02為雙聲道

18H

4

int

44 AC 00 00

采樣頻率(每秒樣本數)0xAC44(44100)

1CH

4

long int

10 B1 02 00

每秒字節數:0x02B110,等於聲道數*采樣頻率*量化位數/8

20H

2

int

04 00

每個采樣點字節數:0x04,等於聲道數*量化位數/8

22H

2

int

10 00

量化位數:0x10

24H

4

char

64 61 74 61

"data"數據標識符

28H

4

long int

48 FE 83 01

聲音數據量:0x0183FE48

38.5 I2S初始化結構體詳解

標准庫函數對I2S外設建立了一個初始化結構體I2S_InitTypeDef。初始化結構體成員用於設置I2S工作環境參數,並由I2S相應初始化配置函數I2S_Init調用,這些設定參數將會設置I2S相應的寄存器,達到配置I2S工作環境的目的。

初始化結構體和初始化庫函數配合使用是標准庫精髓所在,理解了初始化結構體每個成員意義基本上就可以對該外設運用自如了。初始化結構體定義在stm32f4xx_spi.h文件中,初始化庫函數定義在stm32f4xx_spi.c文件中,編程時我們可以結合這兩個文件內注釋使用。

I2S初始化結構體用於配置I2S基本工作環境,比如I2S工作模式、通信標准選擇等等。它被I2S_Init函數調用。

代碼清單 381 I2S_InitTypeDef結構體

1 typedef struct {

2 uint16_t I2S_Mode; // I2S模式選擇

3 uint16_t I2S_Standard; // I2S標准選擇

4 uint16_t I2S_DataFormat; // 數據格式

5 uint16_t I2S_MCLKOutput; // 主時鍾輸出使能

6 uint32_t I2S_AudioFreq; // 采樣頻率

7 uint16_t I2S_CPOL; // 空閑電平選擇

8 } I2S_InitTypeDef;

(1)    I2S_Mode:I2S模式選擇,可選主機發送、主機接收、從機發送以及從機接收模式,它設定SPI_I2SCFGR寄存器I2SCFG位的值。一般設置STM32控制器為主機模式,當播放聲音時選擇發送模式;當錄制聲音時選擇接收模式。

(2)    I2S_Standard:通信標准格式選擇,可選I2S Philips標准、左對齊標准、右對齊標准、PCM短幀標准或PCM長幀標准,它設定SPI_I2SCFGR寄存器I2SSTD位和PCMSYNC位的值。一般設置為I2S Philips標准即可。

(3)    I2S_DataFormat:數據格式選擇,設定有效數據長度和幀長度,可選標准16bit格式、擴展16bit(32bit幀長度)格式、24bit格式和32bit格式,它設定SPI_I2SCFGR寄存器DATLEN位和CHLEN位的值。對應16bit數據長度可選16bit或32bit幀長度,其他都是32bit幀長度。

(4)    I2S_MCLKOutput:主時鍾輸出使能控制,可選使能輸出或禁止輸出,它設定SPI_I2SPR寄存器MCKOE位的值。為提高系統性能一般使能主時鍾輸出。

(5)    I2S_AudioFreq:采樣頻率設置,標准庫提供采樣采樣頻率選擇,分別為8kHz、11kHz、16kHz、22kHz、32kHz、44kHz、48kHz、96kHz、192kHz以及默認2Hz,它設定SPI_I2SPR寄存器的值。

(6)    I2S_CPOL:空閑狀態的CK線電平,可選高電平或低電平,它設定SPI_I2SCFGR寄存器CKPOL位的值。一般設置為電平即可。

38.6 錄音與回放實驗

WAV格式文件在現階段一般以無損音樂格式存在,音質可以達到CD格式標准。結合上一章SD卡操作內容,本實驗通過FatFS文件系統函數從SD卡讀取WAV格式文件數據,然后通過I2S接口將音頻數據發送到WM8978芯片,這樣在WM8978芯片的揚聲器接口即可輸出聲音,整個系統構成一個簡單的音頻播放器。反過來的,我們可以實現錄音功能,控制啟動WM8978芯片的麥克風輸入功能,音頻數據從WM8978芯片的I2S接口傳輸到STM32控制器存儲器中,利用SD卡文件讀寫函數,根據WAV格式文件的要求填充文件頭,然后就把WM8978傳輸過來的音頻數據寫入到WAV格式文件中,這樣就可以制成一個WAV格式文件,可以通過開發板回放也可以在電腦端回放。

38.6.1 硬件設計

開發板板載WM8978芯片,具體電路設計參考圖 3812WM8978STM32f42x有兩個連接接口,I2S音頻接口和兩線I2C控制接口,通過將WM8978芯片的MODE引腳拉低選擇兩線控制接口,符合I2C通信協議,這也導致WM8978是只寫的,所以在程序上需要做一些處理。WM8978輸入部分有兩種模式,一個是板載咪頭輸入,另外一個是通過3.5mm耳機插座引出。WM8978輸出部分通過3.5mm耳機插座引出,可直接接普通的耳機線或作為功放設備的輸入源。

3812 WM8978電路設計

38.6.2 軟件設計

這里只講解核心的部分代碼,有些變量的設置,頭文件的包含等沒有全部羅列出來,完整的代碼請參考本章配套的工程。

上一章我們已經介紹了基於SD卡的文件系統,認識讀寫SD卡內文件方法,前面已經介紹了WAV格式文件結構以及WM8978芯片相關內容,通過WM8978音頻接口傳輸過來的音頻數據可以直接作為WAV格式文件的音頻數據部分,大致過程就是程序控制WM8978啟動錄音功能,通過I2S音頻數據接口WM8978的錄音輸出傳輸到STM32控制器指定緩沖區內,然后利用FatFs的文件寫入函數把緩沖區數據寫入到WAV格式文件中,最終實現聲音錄制功能。同樣的道理,WAV格式文件中的音頻數據可以直接傳輸給WM8978芯片實現音樂播放,整個過程與聲音錄制工程相反。

STM32控制器與WM8978通信可分為兩部分驅動函數,一部分是I2C控制接口,另一部分是I2S音頻數據接口。

bsp_wm8978.cbsp_wm8978.h兩個是專門創建用來存放WM8978芯片驅動代碼。

1.    I2C控制接口

WM8978要正常工作並且實現符合我們的要求,我們必須對芯片相關寄存器進行必須要配置,STM32控制器通過I2C接口與WM8978芯片控制接口連接。I2C接口內容也已經在以前做了詳細介紹,這里主要講解WM8978的功能函數。

bsp_wm8978.c文件中的I2C_GPIO_Config函數、I2C_Mode_Configu函數以及wm8978_Init函數用於I2C通信接口GPIOI2C相關配置,屬於常規配置可以參考GPIOI2C章節理解,這里不再分析,代碼具體見本章配套程序工程文件。

輸入輸出選擇枚舉

代碼清單 382 輸入輸出選擇枚舉

1 /* WM8978 音頻輸入通道控制選項, 可以選擇多路,比如 MIC_LEFT_ON | LINE_ON */

2 typedef enum {

3 IN_PATH_OFF = 0x00, /* 無輸入 */

4 MIC_LEFT_ON = 0x01, /* LIN,LIP腳,MIC左聲道(接板載咪頭) */

5 MIC_RIGHT_ON = 0x02, /* RIN,RIP腳,MIC右聲道(接板載咪頭) */

6 LINE_ON = 0x04, /* L2,R2 立體聲輸入(接板載耳機插座) */

7 AUX_ON = 0x08, /* AUXL,AUXR 立體聲輸入(開發板沒用到) */

8 DAC_ON = 0x10, /* I2S數據DAC (CPU產生音頻信號) */

9 ADC_ON = 0x20 /* 輸入的音頻饋入WM8978內部ADC I2S錄音) */

10 } IN_PATH_E;

11

12 /* WM8978 音頻輸出通道控制選項, 可以選擇多路 */

13 typedef enum {

14 OUT_PATH_OFF = 0x00, /* 無輸出 */

15 EAR_LEFT_ON = 0x01, /* LOUT1 耳機左聲道(接板載耳機插座) */

16 EAR_RIGHT_ON = 0x02, /* ROUT1 耳機右聲道(接板載耳機插座) */

17 SPK_ON = 0x04, /* LOUT2ROUT2反相輸出單聲道(開發板沒用到)*/

18 OUT3_4_ON = 0x08, /* OUT3 OUT4 輸出單聲道音頻(開發板沒用到)*/

19 } OUT_PATH_E;

IN_PATH_EOUT_PATH_E枚舉了WM8978芯片可用的聲音輸入源和輸出端口,具體到開發板,如果進行錄用功能,設置輸入源為(MIC_RIGHT_ON|ADC_ON)(LINE_ON|ADC_ON),設置輸出端口為OUT_PATH_OFF(EAR_LEFT_ON | EAR_RIGHT_ON);對於音樂播放功能,設置輸入源為DAC_ON,設置輸出端口為(EAR_LEFT_ON | EAR_RIGHT_ON)

宏定義

代碼清單 383 宏定義

1 /* 定義最大音量 */

2 #define VOLUME_MAX 63 /* 最大音量 */

3 #define VOLUME_STEP 1 /* 音量調節步長 */

4

5 /* 定義最大MIC增益 */

6 #define GAIN_MAX 63 /* 最大增益 */

7 #define GAIN_STEP 1 /* 增益步長 */

8

9 /* STM32 I2C 快速模式 */

10 #define WM8978_I2C_Speed 400000

11 /* WM8978 I2C從機地址 */

12 #define WM8978_SLAVE_ADDRESS 0x34

13

14 /*I2C接口*/

15 #define WM8978_I2C I2C1

16 #define WM8978_I2C_CLK RCC_APB1Periph_I2C1

17

18 #define WM8978_I2C_SCL_PIN GPIO_Pin_6

19 #define WM8978_I2C_SCL_GPIO_PORT GPIOB

20 #define WM8978_I2C_SCL_GPIO_CLK RCC_AHB1Periph_GPIOB

21 #define WM8978_I2C_SCL_SOURCE GPIO_PinSource6

22 #define WM8978_I2C_SCL_AF GPIO_AF_I2C1

23

24 #define WM8978_I2C_SDA_PIN GPIO_Pin_7

25 #define WM8978_I2C_SDA_GPIO_PORT GPIOB

26 #define WM8978_I2C_SDA_GPIO_CLK RCC_AHB1Periph_GPIOB

27 #define WM8978_I2C_SDA_SOURCE GPIO_PinSource7

28 #define WM8978_I2C_SDA_AF GPIO_AF_I2C1

29

30 /*等待超時時間*/

31 #define WM8978_I2C_FLAG_TIMEOUT ((uint32_t)0x4000)

32 #define WM8978_I2C_LONG_TIMEOUT ((uint32_t)(10 * WM8978_I2C_FLAG_TIMEOUT))

WM8978聲音調節有一定的范圍限制,比如R52(LOUT1 Volume Control)LOUT1VOL[5:0]位用於設置LOUT1的音量大小,可賦值范圍為0~63WM8978包含可調節的輸入麥克風PGA增益,可對每個外部輸入端口可單獨設置增益大小,比如R45(Left Channel input PGA volume control)INPPGAVOL[5:0]用於設置左通道輸入增益音量,最大可設置值為63

WM8978控制接口被設置為I2C模式,其地址固定為0011010,為方便使用,直接定義為0x34

最后定義I2C通信超時等待時間。

WM8978寄存器寫入

代碼清單 384 WM8978寄存器寫入

1 static uint8_t WM8978_I2C_WriteRegister(uint8_t RegisterAddr,

2 uint16_t RegisterValue)

3 {

4 /* Start the config sequence */

5 I2C_GenerateSTART(WM8978_I2C, ENABLE);

6

7 /* Test on EV5 and clear it */

8 WM8978_I2CTimeout = WM8978_I2C_FLAG_TIMEOUT;

9 while (!I2C_CheckEvent(WM8978_I2C, I2C_EVENT_MASTER_MODE_SELECT)) {

10 if ((WM8978_I2CTimeout--) == 0)

11 return WM8978_I2C_TIMEOUT_UserCallback();

12 }

13

14 /* Transmit the slave address and enable writing operation */

15 I2C_Send7bitAddress(WM8978_I2C, WM8978_SLAVE_ADDRESS,

16 I2C_Direction_Transmitter);

17

18 /* Test on EV6 and clear it */

19 WM8978_I2CTimeout = WM8978_I2C_FLAG_TIMEOUT;

20 while (!I2C_CheckEvent(WM8978_I2C,

21 I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)) {

22 if ((WM8978_I2CTimeout--) == 0)

23 return WM8978_I2C_TIMEOUT_UserCallback();

24 }

25

26 /* Transmit the first address for write operation */

27 I2C_SendData(WM8978_I2C,

28 ((RegisterAddr << 1) & 0xFE) | ((RegisterValue >> 8) & 0x1));

29

30 /* Test on EV8 and clear it */

31 WM8978_I2CTimeout = WM8978_I2C_FLAG_TIMEOUT;

32 while (!I2C_CheckEvent(WM8978_I2C,

33 I2C_EVENT_MASTER_BYTE_TRANSMITTING)) {

34 if ((WM8978_I2CTimeout--) == 0)

35 return WM8978_I2C_TIMEOUT_UserCallback();

36 }

37

38 /* Prepare the register value to be sent */

39 I2C_SendData(WM8978_I2C, RegisterValue&0xff);

40

41 /*!< Wait till all data have been physically transferred on the bus */

42 WM8978_I2CTimeout = WM8978_I2C_LONG_TIMEOUT;

43 while (!I2C_GetFlagStatus(WM8978_I2C, I2C_FLAG_BTF)) {

44 if ((WM8978_I2CTimeout--) == 0)

45 WM8978_I2C_TIMEOUT_UserCallback();

46 }

47

48 /* End the configuration sequence */

49 I2C_GenerateSTOP(WM8978_I2C, ENABLE);

50

51 /* Return the verifying value: 0 (Passed) or 1 (Failed) */

52 return 1;

53 }

WM8978_I2C_WriteRegister用於向WM8978芯片寄存器寫入數值,達到配置芯片工作環境,函數有兩個形參,一個是寄存器地址,可設置范圍為0~57;另外一個是寄存器值,WM8978芯片寄存器總共有16bit,前7bit用於尋址,后9位才是數據,這里寄存器值形參使用uint16_t類型,只有低9位有效。

使用I2C通信,首先使用I2C_Send7bitAddress函數選定WM8978芯片,接下來需要兩次調用I2C_SendData函數發送兩次數據,因為I2C數據發送一次只能發送8bit數據,為此需要把RegisterValue變量的第9bit整合到RegisterAddr變量的第0位先發送,接下來再發送RegisterValue變量的低8bit數據。

函數中還添加了I2C通信超時等待功能,防止出錯時卡死。

WM8978寄存器讀取

WM8978芯片是從硬件上選擇I2C通信模式,該模式是只寫的,STM32控制器無法讀取WM8978寄存器內容,但程序有時需要用到寄存器內容,為此我們創建了一個存放WM8978所有寄存器值的數組,在系統復位是將數組內容設置為WM8978缺省值,然后在每次修改寄存器內容時同步更新該數組內容,這樣可以達到該數組與WM8978寄存器內容相等的效果,參考代碼清單 385

代碼清單 385 WM8978寄存器值緩沖區和讀取

1 /*

2 wm8978寄存器緩存

3 由於WM8978I2C兩線接口不支持讀取操作,因此寄存器值緩存在內存中,

4 當寫寄存器時同步更新緩存,讀寄存器時直接返回緩存中的值。

5 寄存器MAP WM8978(V4.5_2011).pdf 的第89頁,寄存器地址是7bit寄存器數據是9bit

6 */

7 static uint16_t wm8978_RegCash[] = {

8 0x000, 0x000, 0x000, 0x000, 0x050, 0x000, 0x140, 0x000,

9 0x000, 0x000, 0x000, 0x0FF, 0x0FF, 0x000, 0x100, 0x0FF,

10 0x0FF, 0x000, 0x12C, 0x02C, 0x02C, 0x02C, 0x02C, 0x000,

11 0x032, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000,

12 0x038, 0x00B, 0x032, 0x000, 0x008, 0x00C, 0x093, 0x0E9,

13 0x000, 0x000, 0x000, 0x000, 0x003, 0x010, 0x010, 0x100,

14 0x100, 0x002, 0x001, 0x001, 0x039, 0x039, 0x039, 0x039,

15 0x001, 0x001

16 };

17

18 /**

19 * @brief cash中讀回讀回wm8978寄存器

20 * @param _ucRegAddr 寄存器地址

21 * @retval 寄存器值

22 */

23 static uint16_t wm8978_ReadReg(uint8_t _ucRegAddr)

24 {

25 return wm8978_RegCash[_ucRegAddr];

26 }

27

28 /**

29 * @brief wm8978寄存器

30 * @param _ucRegAddr寄存器地址

31 * @param _usValue寄存器值

32 * @retval 0:寫入失敗

33 * 1:寫入成功

34 */

35 static uint8_t wm8978_WriteReg(uint8_t _ucRegAddr, uint16_t _usValue)

36 {

37 uint8_t res;

38 res=WM8978_I2C_WriteRegister(_ucRegAddr,_usValue);

39 wm8978_RegCash[_ucRegAddr] = _usValue;

40 return res;

41 }

wm8978_WriteReg實現向WM8978寄存器寫入數據並修改緩沖區內容。

輸出音量修改與讀取

代碼清單 386 音量修改與讀取

1 /**

2 * @brief 修改輸出通道1音量

3 * @param _ucVolume :音量值, 0-63

4 * @retval

5 */

6 void wm8978_SetOUT1Volume(uint8_t _ucVolume)

7 {

8 uint16_t regL;

9 uint16_t regR;

10

11 if (_ucVolume > VOLUME_MAX) {

12 _ucVolume = VOLUME_MAX;

13 }

14 regL = _ucVolume;

15 regR = _ucVolume;

16 /*

17 R52 LOUT1 Volume control

18 R53 ROUT1 Volume control

19 */

20 /* 先更新左聲道緩存值 */

21 wm8978_WriteReg(52, regL | 0x00);

22

23 /* 再同步更新左右聲道的音量 */

24 /* 0x180表示在音量為0時再更新,避免調節音量出現的"嘎噠" */

25 wm8978_WriteReg(53, regR | 0x100);

26 }

27

28 /**

29 * @brief 讀取輸出通道1音量

30 * @param

31 * @retval 當前音量值

32 */

33 uint8_t wm8978_ReadOUT1Volume(void)

34 {

35 return (uint8_t)(wm8978_ReadReg(52) & 0x3F );

36 }

37

38 /**

39 * @brief 輸出靜音.

40 * @param _ucMute:模式選擇

41 * @arg 1:靜音

42 * @arg 0:取消靜音

43 * @retval

44 */

45 void wm8978_OutMute(uint8_t _ucMute)

46 {

47 uint16_t usRegValue;

48 if (_ucMute == 1) { /* 靜音 */

49 usRegValue = wm8978_ReadReg(52); /* Left Mixer Control */

50 usRegValue |= (1u << 6);

51 wm8978_WriteReg(52, usRegValue);

52

53 usRegValue = wm8978_ReadReg(53); /* Left Mixer Control */

54 usRegValue |= (1u << 6);

55 wm8978_WriteReg(53, usRegValue);

56

57 usRegValue = wm8978_ReadReg(54); /* Right Mixer Control */

58 usRegValue |= (1u << 6);

59 wm8978_WriteReg(54, usRegValue);

60

61 usRegValue = wm8978_ReadReg(55); /* Right Mixer Control */

62 usRegValue |= (1u << 6);

63 wm8978_WriteReg(55, usRegValue);

64 } else { /* 取消靜音 */

65 usRegValue = wm8978_ReadReg(52);

66 usRegValue &= ~(1u << 6);

67 wm8978_WriteReg(52, usRegValue);

68

69 usRegValue = wm8978_ReadReg(53); /* Left Mixer Control */

70 usRegValue &= ~(1u << 6);

71 wm8978_WriteReg(53, usRegValue);

72

73 usRegValue = wm8978_ReadReg(54);

74 usRegValue &= ~(1u << 6);

75 wm8978_WriteReg(54, usRegValue);

76

77 usRegValue = wm8978_ReadReg(55); /* Left Mixer Control */

78 usRegValue &= ~(1u << 6);

79 wm8978_WriteReg(55, usRegValue);

80 }

81 }

wm8978_SetOUT1Volume函數用於修改OUT1通道的音量大小,有一個形參用於指示音量大小,要求范圍為0~63。這里同時更新OUT1的左右兩個聲道音量,WM8978芯片的R52R53分別用於設置OUT1的左聲道和右聲道音量,具體位段意義參考表 385wm8978_SetOUT1Volume函數會同時修改WM8978寄存器緩存區wm8978_RegCash數組內容。

385 OUT1音量控制寄存器

寄存器地址

默認值

描述

R52(LOUT1 Volume Control)

8

不鎖存

直到一個 1 寫入到 HPVU 才更 LOUT1 ROUT1 音量

7

0

耳機音量零交叉使能:0=僅僅在零交叉時改變增益;1=立即改變增益

6

0

左耳機輸出消聲:0=正常操作;1=消聲

5:0

11101

左耳機輸出驅動:

000000=-57dB

111001=0dB

111111=+6dB

R53(ROUT1 Volume Control)

8

不鎖存

直到一個 1 寫入到 HPVU 才更 LOUT1 ROUT1 音量

7

0

耳機音量零交叉使能:0=僅僅在零交叉時改變增益;1=立即改變增益

6

0

左耳機輸出消聲:0=正常操作;1=消聲

5:0

11101

右耳機輸出驅動:

000000=-57dB

111001=0dB

111111=+6dB

另外,wm8978_SetOUT2Volume用於設置OUT2的音量,程序結構與wm8978_SetOUT1Volume相同,只是對應修改R54R55

wm8978_ReadOUT1Volume函數用於讀取OUT1的音量,它實際就是讀取wm8978_RegCash數組對應元素內容。

wm8978_OutMute用於靜音控制,它有一個形參用於設置靜音效果,如果為1則為開啟靜音,如果為0則取消靜音。靜音控制是通過R52R53的第6位實現的,在進入靜音模式時需要先保存OUT1OUT2的音量大小,然后在退出靜音模式時就可以正確返回到靜音前OUT1OUT2的配置。

輸入增益調整

代碼清單 387 輸入增益調整

1 /**

2 * @brief 設置增益

3 * @param _ucGain :增益值, 0-63

4 * @retval

5 */

6 void wm8978_SetMicGain(uint8_t _ucGain)

7 {

8 if (_ucGain > GAIN_MAX) {

9 _ucGain = GAIN_MAX;

10 }

11

12 /* PGA 音量控制 R45 R46

13 Bit8 INPPGAUPDATE

14 Bit7 INPPGAZCL 過零再更改

15 Bit6 INPPGAMUTEL PGA靜音

16 Bit5:0 增益值,0100000dB

17 */

18 wm8978_WriteReg(45, _ucGain);

19 wm8978_WriteReg(46, _ucGain | (1 << 8));

20 }

21

22

23 /**

24 * @brief 設置Line輸入通道的增益

25 * @param _ucGain :音量值, 0-7. 7最大,0最小。可衰減可放大。

26 * @retval

27 */

28 void wm8978_SetLineGain(uint8_t _ucGain)

29 {

30 uint16_t usRegValue;

31

32 if (_ucGain > 7) {

33 _ucGain = 7;

34 }

35

36 /*

37 Mic 輸入信道的增益由 PGABOOSTL PGABOOSTR 控制

38 Aux 輸入信道的輸入增益由 AUXL2BOOSTVO[2:0] AUXR2BOOSTVO[2:0] 控制

39 Line 輸入信道的增益由 LIP2BOOSTVOL[2:0] RIP2BOOSTVOL[2:0] 控制

40 */

41 /* R47(左聲道),R48(右聲道), MIC 增益控制寄存器

42 R47 (R48定義與此相同)

43 B8 PGABOOSTL=1,0表示MIC信號直通無增益,1表示MIC信號+20dB增益(通過自舉電路)

44 B7 = 0保留

45 B6:4 L2_2BOOSTVOL=x0表示禁止,1-7表示增益-12dB ~ +6dB(可以衰減也可以放大)

46 B3 = 0保留

47 B2:0 AUXL2BOOSTVOL=x0表示禁止,1-7表示增益-12dB~+6dB(可以衰減也可以放大)

48 */

49

50 usRegValue = wm8978_ReadReg(47);

51 usRegValue &= 0x8F;/* Bit6:40 1000 1111*/

52 usRegValue |= (_ucGain << 4);

53 wm8978_WriteReg(47, usRegValue); /* 寫左聲道輸入增益控制寄存器 */

54

55 usRegValue = wm8978_ReadReg(48);

56 usRegValue &= 0x8F;/* Bit6:40 1000 1111*/

57 usRegValue |= (_ucGain << 4);

58 wm8978_WriteReg(48, usRegValue); /* 寫右聲道輸入增益控制寄存器 */

59 }

wm8978_SetMicGain用於設置麥克風輸入的增益,可以設置增強或減弱輸入效果,比如對於部分聲音源本身就是比較微弱,我們就可以設置放大該信號,從而得到合適的錄制效果,該函數主要通過設置R45R46實現,可設置的范圍為0~63,默認值為16,沒有增益效果。

wm8978_SetLineGain用於設置LINE輸入的增益,對應芯片的L2R2引腳組合的輸入,開發板使用耳機插座引出拓展。它通過設置R47R48寄存器實現,可設置范圍為0~7,默認值為0,沒有增益效果。

音頻接口標准選擇

代碼清單 388 wm8978_CfgAudioIF函數

1 /**

2 * @brief 配置WM8978的音頻接口(I2S)

3 * @param _usStandard : 接口標准,

4 I2S_Standard_Phillips, I2S_Standard_MSB I2S_Standard_LSB

5 * @param _ucWordLen : 字長,162432 (丟棄不常用的20bit格式)

6 * @retval

7 */

8 void wm8978_CfgAudioIF(uint16_t _usStandard, uint8_t _ucWordLen)

9 {

10 uint16_t usReg;

11

12 /* WM8978(V4.5_2011).pdf 73頁,寄存器列表 */

13

14 /* REG R4, 音頻接口控制寄存器

15 B8 BCP = X, BCLK極性,0表示正常,1表示反相

16 B7 LRCP = x, LRC時鍾極性,0表示正常,1表示反相

17 B6:5 WL = x字長,00=16bit01=20bit10=24bit11=32bit

18 (右對齊模式只能操作在最大24bit)

19 B4:3 FMT = x,音頻數據格式,00=右對齊,01=左對齊,10=I2S格式,11=PCM

20 B2 DACLRSWAP = x, 控制DAC數據出現在LRC時鍾的左邊還是右邊

21 B1 ADCLRSWAP = x,控制ADC數據出現在LRC時鍾的左邊還是右邊

22 B0 MONO = 00表示立體聲,1表示單聲道,僅左聲道有效

23 */

24 usReg = 0;

25 if (_usStandard == I2S_Standard_Phillips) { /* I2S飛利浦標准 */

26 usReg |= (2 << 3);

27 } else if (_usStandard == I2S_Standard_MSB) { /* MSB對齊標准(左對齊) */

28 usReg |= (1 << 3);

29 } else if (_usStandard == I2S_Standard_LSB) { /* LSB對齊標准(右對齊) */

30 usReg |= (0 << 3);

31 } else { /*PCM標准(16位通道幀上帶長或短幀同步或者16位數據幀擴展為32位通道幀) */

32 usReg |= (3 << 3);;

33 }

34

35 if (_ucWordLen == 24) {

36 usReg |= (2 << 5);

37 } else if (_ucWordLen == 32) {

38 usReg |= (3 << 5);

39 } else {

40 usReg |= (0 << 5); /* 16bit */

41 }

42 wm8978_WriteReg(4, usReg);

43

44 /*

45 R6,時鍾產生控制寄存器

46 MS = 0, WM8978被動時鍾,由MCU提供MCLK時鍾

47 */

48 wm8978_WriteReg(6, 0x000);

49 }

wm8978_CfgAudioIF函數用於設置WM8978芯片的音頻接口標准,它有兩個形參,第一個是標准選擇,可選I2S Philips標准(I2S_Standard_Phillips)、左對齊標准(I2S_Standard_MSB)以及右對齊標准(I2S_Standard_LSB);另外一個形參是字長設置,可選16bit24bit以及32bit,較常用16bit。它函數通過控制WM8978芯片R4實現,最后還通過通用時鍾控制寄存器R6設置芯片的I2S工作在從模式,時鍾線為輸入時鍾。

輸入輸出通道設置

代碼清單 389 wm8978_CfgAudioPath函數

1 void wm8978_CfgAudioPath(uint16_t _InPath, uint16_t _OutPath)

2 {

3 uint16_t usReg;

4 if ((_InPath == IN_PATH_OFF) && (_OutPath == OUT_PATH_OFF)) {

5 wm8978_PowerDown();

6 return;

7 }

8

9 /*

10 R1 寄存器 Power manage 1

11 Bit8 BUFDCOPEN, Output stage 1.5xAVDD/2 driver enable

12 Bit7 OUT4MIXEN, OUT4 mixer enable

13 Bit6 OUT3MIXEN, OUT3 mixer enable

14 Bit5 PLLEN .不用

15 Bit4 MICBEN ,Microphone Bias Enable (MIC偏置電路使能)

16 Bit3 BIASEN ,Analogue amplifier bias control必須設置為1模擬放大器才工作

17 Bit2 BUFIOEN , Unused input/output tie off buffer enable

18 Bit1:0 VMIDSEL, 必須設置為非00值模擬放大器才工作

19 */

20 usReg = (1 << 3) | (3 << 0);

21 if (_OutPath & OUT3_4_ON) { /* OUT3OUT4使能輸出 */

22 usReg |= ((1 << 7) | (1 << 6));

23 }

24 if ((_InPath & MIC_LEFT_ON) || (_InPath & MIC_RIGHT_ON)) {

25 usReg |= (1 << 4);

26 }

27 wm8978_WriteReg(1, usReg); /* 寫寄存器 */

28

29 /**********************************************/

30 /* 此處省略部分代碼,具體參考工程文件 */

31 /**********************************************/

32

33 /* R10 寄存器 DAC Control

34 B8 0

35 B7 0

36 B6 SOFTMUTE, Softmute enable:

37 B5 0

38 B4 0

39 B3 DACOSR128, DAC oversampling rate: 0=64x (lowest power)

40 1=128x (best performance)

41 B2 AMUTE, Automute enable

42 B1 DACPOLR, Right DAC output polarity

43 B0 DACPOLL, Left DAC output polarity:

44 */

45 if (_InPath & DAC_ON) {

46 wm8978_WriteReg(10, 0);

47 }

48 }

wm8978_CfgAudioPath函數用於配置聲音輸入輸出通道,有兩個形參,第一個形參用於設置輸入源,可以使用IN_PATH_E枚舉類型成員的一個或多個或運算結果;第二個形參用於設置輸出通道,可以使用OUT_PATH_E枚舉類型成員的一個或多個或運算結果。具體到開發板,如果進行錄用功能,設置輸入源為(MIC_RIGHT_ON|ADC_ON)(LINE_ON|ADC_ON),設置輸出端口為OUT_PATH_OFF(EAR_LEFT_ON | EAR_RIGHT_ON);對於音樂播放功能,設置輸入源為DAC_ON,設置輸出端口為(EAR_LEFT_ON | EAR_RIGHT_ON)

wm8978_CfgAudioPath函數首先判斷輸入參數合法性,如果輸入出錯直接調用函數wm8978_PowerDown進入低功耗模式,並退出。

接下來使用wm8978_WriteReg配置相關寄存器值。大致可分三個部分,第一部分是電源管理部分,主要涉及到R1R2R3三個寄存器,使用輸入輸出通道之前必須開啟相關電源。第二部分是輸入通道選擇及相關配置,配置R44控制選擇輸入通道,R14設置輸入的高通濾波器功能,R27R28R29R30設置輸入的可調陷波濾波器功能,R32R33R34控制輸入限幅器/電平自動控制(ALC)R35設置ALC噪聲門限,R47R48設置通道增益參數,R15R16設置ADC數字音量,R43設置AUXR功能。第三部分是輸出通道選擇及相關配置,控制R49選擇輸出通道,R50R51設置左右通道混合輸出效果,R56設置OUT3混合輸出效果,R57設置OUT4混合輸出效果,R11R12設置左右DAC數字音量,R10設置DAC參數。

軟件復位

代碼清單 3810 wm8978_Reset函數

1 uint8_t wm8978_Reset(void)

2 {

3 /* wm8978寄存器缺省值 */

4 const uint16_t reg_default[] = {

5 0x000, 0x000, 0x000, 0x000, 0x050, 0x000, 0x140, 0x000,

6 0x000, 0x000, 0x000, 0x0FF, 0x0FF, 0x000, 0x100, 0x0FF,

7 0x0FF, 0x000, 0x12C, 0x02C, 0x02C, 0x02C, 0x02C, 0x000,

8 0x032, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000,

9 0x038, 0x00B, 0x032, 0x000, 0x008, 0x00C, 0x093, 0x0E9,

10 0x000, 0x000, 0x000, 0x000, 0x003, 0x010, 0x010, 0x100,

11 0x100, 0x002, 0x001, 0x001, 0x039, 0x039, 0x039, 0x039,

12 0x001, 0x001

13 };

14 uint8_t res;

15 uint8_t i;

16

17 res=wm8978_WriteReg(0x00, 0);

18

19 for (i = 0; i < sizeof(reg_default) / 2; i++) {

20 wm8978_RegCash[i] = reg_default[i];

21 }

22 return res;

23 }

wm8978_Reset函數用於軟件復位WM8978芯片,通過寫入R0完成,使其寄存器復位到缺省狀態,同時會更新寄存器緩沖區數組wm8978_RegCash恢復到缺省狀態。

2.    I2S控制接口

WM8978集成I2S音頻接口,用於與外部設備進行數字音頻數據傳輸,芯片I2S接口屬性通過wm8978_CfgAudioIF函數配置。STM32控制器與WM8978進行音頻數據傳輸,一般設置STM32控制器為主機模式,WM8978作為從設備。

I2S_GPIO_Config函數用於初始化I2S相關GPIO,具體參考工程文件。

I2S工作模式配置

代碼清單 3811 I2Sx_Mode_Config函數

1 void I2Sx_Mode_Config(const uint16_t _usStandard,const uint16_t _usWordLen,

2 const uint32_t _usAudioFreq )

3 {

4 I2S_InitTypeDef I2S_InitStructure;

5 uint32_t n = 0;

6 FlagStatus status = RESET;

7 /**

8 * For I2S mode, make sure that either:

9 * - I2S PLL is configured using the functions RCC_I2SCLKConfig

10 * (RCC_I2S2CLKSource_PLLI2S),

11 * RCC_PLLI2SCmd(ENABLE) and RCC_GetFlagStatus(RCC_FLAG_PLLI2SRDY).

12 */

13 RCC_I2SCLKConfig(RCC_I2S2CLKSource_PLLI2S);

14 RCC_PLLI2SCmd(ENABLE);

15 for (n = 0; n < 500; n++) {

16 status = RCC_GetFlagStatus(RCC_FLAG_PLLI2SRDY);

17 if (status == 1)break;

18 }

19 /* 打開 I2S2 APB1 時鍾 */

20 RCC_APB1PeriphClockCmd(WM8978_CLK, ENABLE);

21

22 /* 復位 SPI2 外設到缺省狀態 */

23 SPI_I2S_DeInit(WM8978_I2Sx_SPI);

24

25 /* I2S2 外設配置 */

26 /* 配置I2S工作模式 */

27 I2S_InitStructure.I2S_Mode = I2S_Mode_MasterTx;

28 /* 接口標准 */

29 I2S_InitStructure.I2S_Standard = _usStandard;

30 /* 數據格式,16bit */

31 I2S_InitStructure.I2S_DataFormat = _usWordLen;

32 /* 主時鍾模式 */

33 I2S_InitStructure.I2S_MCLKOutput = I2S_MCLKOutput_Enable;

34 /* 音頻采樣頻率 */

35 I2S_InitStructure.I2S_AudioFreq = _usAudioFreq;

36 I2S_InitStructure.I2S_CPOL = I2S_CPOL_Low;

37 I2S_Init(WM8978_I2Sx_SPI, &I2S_InitStructure);

38

39 /* 使能 SPI2/I2S2 外設 */

40 I2S_Cmd(WM8978_I2Sx_SPI, ENABLE);

41 }

I2Sx_Mode_Config函數用於配置STM32控制器的I2S接口工作模式,它有三個形參,第一個為指定I2S接口標准,一般設置為I2S Philips標准,第二個為字長設置,一般設置為16bit,第三個為采樣頻率,一般設置為44KHz既可得到高音質效果。

首先是時鍾配置,使用RCC_I2SCLKConfig函數選擇I2S時鍾源,一般選擇內部PLLI2S時鍾,RCC_PLLI2SCmd函數使能PLLI2S時鍾,並等待時鍾正常后開啟I2S外設時鍾。

接下來通過給I2S_InitTypeDef結構體類型變量賦值設置I2S工作模式,並由I2S_Init函數完成I2S基本工作環境配置。

最后,I2S_Cmd函數用於使能I2S

I2S數據發送(DMA傳輸)

代碼清單 3812 I2Sx_TX_DMA_Init函數

1 void I2Sx_TX_DMA_Init(const uint16_t *buffer0,const uint16_t *buffer1,

2 const uint32_t num)

3 {

4 NVIC_InitTypeDef NVIC_InitStructure;

5 DMA_InitTypeDef DMA_InitStructure;

6

7 RCC_AHB1PeriphClockCmd(I2Sx_DMA_CLK,ENABLE);//DMA1時鍾使能

8

9 DMA_DeInit(I2Sx_TX_DMA_STREAM);

10 //等待DMA1_Stream4可配置

11 while (DMA_GetCmdStatus(I2Sx_TX_DMA_STREAM) != DISABLE) {}

12 //清空DMA1_Stream4上所有中斷標志

13 DMA_ClearITPendingBit(I2Sx_TX_DMA_STREAM,

14 DMA_IT_FEIF4|DMA_IT_DMEIF4|DMA_IT_TEIF4|DMA_IT_HTIF4|DMA_IT_TCIF4);

15

16 /* 配置 DMA Stream */

17 //通道0 SPIx_TX通道

18 DMA_InitStructure.DMA_Channel = I2Sx_TX_DMA_CHANNEL;

19 //外設地址為:(u32)&SPI2->DR

20 DMA_InitStructure.DMA_PeripheralBaseAddr =

21 (uint32_t)&WM8978_I2Sx_SPI->DR;

22 //DMA 存儲器0地址

23 DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)buffer0;

24 //存儲器到外設模式

25 DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;

26 //數據傳輸量

27 DMA_InitStructure.DMA_BufferSize = num;

28 //外設非增量模式

29 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;

30 //存儲器增量模式

31 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;

32 //外設數據長度:16

33 DMA_InitStructure.DMA_PeripheralDataSize =

34 DMA_PeripheralDataSize_HalfWord;

35 //存儲器數據長度:16

36 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;

37 // 使用循環模式

38 DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;

39 //高優先級

40 DMA_InitStructure.DMA_Priority = DMA_Priority_High;

41 //不使用FIFO模式

42 DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;

43 DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_1QuarterFull;

44 //外設突發單次傳輸

45 DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;

46 //存儲器突發單次傳輸

47 DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;

48 DMA_Init(I2Sx_TX_DMA_STREAM, &DMA_InitStructure);//初始化DMA Stream

49

50 //雙緩沖模式配置

51 DMA_DoubleBufferModeConfig(I2Sx_TX_DMA_STREAM,(uint32_t)buffer0,

52 DMA_Memory_0);

53 DMA_DoubleBufferModeConfig(I2Sx_TX_DMA_STREAM,(uint32_t)buffer1,

54 DMA_Memory_1);

55 //雙緩沖模式開啟

56 DMA_DoubleBufferModeCmd(I2Sx_TX_DMA_STREAM,ENABLE);

57

58 //開啟傳輸完成中斷

59 DMA_ITConfig(I2Sx_TX_DMA_STREAM,DMA_IT_TC,ENABLE);

60 //SPI2 TX DMA請求使能.

61 SPI_I2S_DMACmd(WM8978_I2Sx_SPI,SPI_I2S_DMAReq_Tx,ENABLE);

62

63 NVIC_InitStructure.NVIC_IRQChannel = I2Sx_TX_DMA_STREAM_IRQn;

64 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;

65 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;

66 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;

67 NVIC_Init(&NVIC_InitStructure);

68 }

I2Sx_TX_DMA_Init函數用於初始化I2S數據發送DMA請求工作環境,並啟動DMA傳輸。它有三個形參,第一個為緩沖區1地址,第二個為緩沖區2地址,第三為緩沖區大小。這里使用DMA的雙緩沖區模式,就是開辟兩個緩沖區空間,當第一個緩沖區用於DMA傳輸時(不占用CPU)CPU可以往第二個緩沖區填充數據,等到第一個緩沖區DMA傳輸完成后切換第二個緩沖區用於DMA傳輸,CPU往第一個緩沖區填充數據,如此不斷循環切換,可以達到DMA數據傳輸不間斷效果,具體可參考DMA章節。這里為保證播放流暢性使用了DMA雙緩沖區模式。

I2Sx_TX_DMA_Init函數首先是使能I2S發送DMA流時鍾,並復位DMA流配置和相關中斷標志位。

通過對DMA_InitTypeDef結構體類型的變量賦值配置DMA流工作環境並通過DMA_Init完成配置。

DMA_DoubleBufferModeConfig函數用於指定DMA雙緩沖區模式下緩沖區地址。通過DMA_DoubleBufferModeCmd函數可以開啟DMA雙緩沖區模式。這里使能DMA傳輸完成中,用於指示其中一個緩沖區傳輸完成,需要切換緩沖區,可以開始往緩沖區填充數據。

SPI_I2S_DMACmd用於使能I2S發送DMA傳輸請求。

最后配置DMA傳輸完成中斷的優先級。

DMA數據發送傳輸完成中斷服務函數

代碼清單 3813 DMA數據發送傳輸完成中斷服務函數

1 //I2S DMA 回調函數

2 void (*I2S_DMA_TX_Callback)(void);

3

4 void I2Sx_TX_DMA_STREAM_IRQFUN(void)

5 {

6 //DMA傳輸完成標志

7 if (DMA_GetITStatus(I2Sx_TX_DMA_STREAM,I2Sx_TX_DMA_IT_TCIF)==SET) {

8 //DMA傳輸完成標准

9 DMA_ClearITPendingBit(I2Sx_TX_DMA_STREAM,I2Sx_TX_DMA_IT_TCIF);

10 //執行回調函數,讀取數據等操作在這里面處理

11 I2S_DMA_TX_Callback();

12 }

13 }

首先聲明一個函數指針,即I2S_DMA_TX_Callback是一個函數指針,一般是先定義一個函數,再把函數名稱賦值給I2S_DMA_TX_Callback

I2Sx_TX_DMA_STREAM_IRQFUN函數是I2SDMA傳輸中斷服務函數,在判斷是DMA傳輸完成中斷后執行I2S_DMA_TX_Callback函數指針對應函數內容。

啟動和停止播放控制

代碼清單 3814 啟動和停止播放控制

1 void I2S_Play_Start(void)

2 {

3 DMA_Cmd(I2Sx_TX_DMA_STREAM,ENABLE);//開啟DMA TX傳輸,開始播放

4 }

5

6

7 void I2S_Play_Stop(void)

8 {

9 DMA_Cmd(I2Sx_TX_DMA_STREAM,DISABLE);//關閉DMA TX傳輸,結束播放

10 }

11

I2S_Play_Start用於開始播放,I2S_Play_Stop用於停止播放,實際是通過控制DMA傳輸使能來實現。

I2S擴展功能模式配置

代碼清單 3815 I2Sxext_Mode_Config函數

1 void I2Sxext_Mode_Config(const uint16_t _usStandard,

2 const uint16_t _usWordLen,const uint32_t _usAudioFreq)

3 {

4 I2S_InitTypeDef I2Sext_InitStructure;

5

6 /* I2S2 外設配置 */

7 /* 配置I2S工作模式 */

8 I2Sext_InitStructure.I2S_Mode = I2S_Mode_MasterTx;

9 /* 接口標准 */

10 I2Sext_InitStructure.I2S_Standard = _usStandard;

11 /* 數據格式,16bit */

12 I2Sext_InitStructure.I2S_DataFormat = _usWordLen;

13 /* 主時鍾模式 */

14 I2Sext_InitStructure.I2S_MCLKOutput = I2S_MCLKOutput_Enable;

15 /* 音頻采樣頻率 */

16 I2Sext_InitStructure.I2S_AudioFreq = _usAudioFreq;

17 I2Sext_InitStructure.I2S_CPOL = I2S_CPOL_Low;

18

19 I2S_FullDuplexConfig(WM8978_I2Sx_ext, &I2Sext_InitStructure);

20

21 /* 使能 SPI2/I2S2 外設 */

22 I2S_Cmd(WM8978_I2Sx_ext, ENABLE);

23 }

I2Sxext_Mode_Config函數用於配置I2S全雙工模式,使用擴展I2S功能方便數據處理,這對實現錄音功能有很大的幫助。它有三個形參,第一個為指定I2S接口標准,一般設置為I2S Philips標准,第二個為字長設置,一般設置為16bit,第三個為采樣頻率,一般設置為44KHz既可得到高音質效果。

I2Sxext_Mode_Config函數沒有配置I2S相關時鍾,一般在I2Sx_Mode_Config函數完成時鍾配置,所以一般先運行I2Sx_Mode_Config函數才運行I2Sxext_Mode_Config函數。

I2Sxext_Mode_Config函數先給I2S_InitTypeDef結構體類型的變量賦值配置參數,然后調用I2S_FullDuplexConfig函數配置I2S全雙工模式。最后使用I2S_Cmd使能擴展I2S

I2S擴展數據接收(DMA傳輸)

代碼清單 3816 I2Sxext_RX_DMA_Init函數

1 void I2Sxext_RX_DMA_Init(const uint16_t *buffer0,const uint16_t *buffer1,

2 const uint32_t num)

3 {

4 NVIC_InitTypeDef NVIC_InitStructure;

5 DMA_InitTypeDef DMA_InitStructure;

6

7 RCC_AHB1PeriphClockCmd(I2Sx_DMA_CLK,ENABLE);

8

9 DMA_DeInit(I2Sxext_RX_DMA_STREAM);

10 while (DMA_GetCmdStatus(I2Sxext_RX_DMA_STREAM) != DISABLE) {}

11

12 DMA_ClearITPendingBit(I2Sxext_RX_DMA_STREAM,

13 DMA_IT_FEIF3|DMA_IT_DMEIF3|DMA_IT_TEIF3|DMA_IT_HTIF3|DMA_IT_TCIF3);

14

15 /* 配置 DMA Stream */

16 DMA_InitStructure.DMA_Channel = I2Sxext_RX_DMA_CHANNEL;

17 DMA_InitStructure.DMA_PeripheralBaseAddr =

18 (uint32_t)&WM8978_I2Sx_ext->DR;

19 DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)buffer0;

20 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;

21 DMA_InitStructure.DMA_BufferSize = num;

22 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;

23 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;

24 DMA_InitStructure.DMA_PeripheralDataSize =

25 DMA_PeripheralDataSize_HalfWord;

26 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;

27 DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;

28 DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;

29 DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;

30 DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_1QuarterFull;

31 DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;

32 DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;

33 DMA_Init(I2Sxext_RX_DMA_STREAM, &DMA_InitStructure);

34

35 DMA_DoubleBufferModeConfig(I2Sxext_RX_DMA_STREAM,

36 (uint32_t)buffer0,DMA_Memory_0);

37 DMA_DoubleBufferModeConfig(I2Sxext_RX_DMA_STREAM,

38 (uint32_t)buffer1,DMA_Memory_1);

39

40 DMA_DoubleBufferModeCmd(I2Sxext_RX_DMA_STREAM,ENABLE);

41

42 DMA_ITConfig(I2Sxext_RX_DMA_STREAM,DMA_IT_TC,ENABLE);

43

44 SPI_I2S_DMACmd(WM8978_I2Sx_ext,SPI_I2S_DMAReq_Rx,ENABLE);

45

46 NVIC_InitStructure.NVIC_IRQChannel = I2Sxext_RX_DMA_STREAM_IRQn;

47 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;

48 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;

49 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;

50 NVIC_Init(&NVIC_InitStructure);

51 }

I2Sxext_RX_DMA_Init函數配置擴展I2S的數據接收功能,使用DMA傳輸方式接收數據,程序結構與I2Sx_TX_DMA_Init函數一致,只是DMA傳輸方向不同,I2Sxext_RX_DMA_Init函數是從外設到存儲器傳輸,I2Sx_TX_DMA_Init函數是存儲器到外設傳輸。

I2Sxext_RX_DMA_Init函數也是使用DMA的雙緩沖區模式傳輸數據。最后使能了DMA傳輸完成中斷,並使能DMA數據接收請求。

DMA數據接收傳輸完成中斷服務函數

代碼清單 3817 DMA數據接收傳輸完成中斷服務函數

1 //I2S DMA RX回調函數

2 void (*I2S_DMA_RX_Callback)(void);

3

4 void I2Sxext_RX_DMA_STREAM_IRQFUN(void)

5 {

6 if(DMA_GetITStatus(I2Sxext_RX_DMA_STREAM,I2Sxext_RX_DMA_IT_TCIF)==SET) {

7 DMA_ClearITPendingBit(I2Sxext_RX_DMA_STREAM,I2Sxext_RX_DMA_IT_TCIF);

8 I2S_DMA_RX_Callback(); //執行回調函數,讀取數據等操作在這里面處理

9 }

10 }

DMA數據發送傳輸完成中斷服務函數類似,I2Sxext_RX_DMA_STREAM_IRQFUN函數在判斷得到是數據接收傳輸完成后執行I2S_DMA_RX_Callback函數,I2S_DMA_RX_Callback實際也是一個函數指針。

啟動和停止錄音

代碼清單 3818 啟動和停止錄音

1 void I2Sxext_Recorde_Start(void)

2 {

3 DMA_Cmd(I2Sxext_RX_DMA_STREAM,ENABLE);

4 }

5

6 void I2Sxext_Recorde_Stop(void)

7 {

8 DMA_Cmd(I2Sxext_RX_DMA_STREAM,DISABLE);

9 }

10

I2Sxext_Recorde_Start函數用於啟動錄音,I2Sxext_Recorde_Stop函數用於停止錄音,實際是通過控制DMA傳輸使能來實現。

至此,關於WM8978芯片的驅動程序已經介紹完整了,該部分程序都是在bsp_wm8978.c文件中的,接下來我們就可以使用這些驅動程序實現錄音和回放功能了。

3.    錄音和回放功能

錄音和回放功能是在WM8978驅動函數基礎上搭建而成的,實現代碼存放在Recorder.cRecorder.h文件中。啟動錄音功能后會在SD卡內創建一個WAV格式文件,把音頻數據保存在該文件中,錄音結束后既可得到一個完整的WAV格式文件。回放功能用於播放錄音文件,實際上回放功能的實現函數也是適用於播放其他WAV格式文件的。

枚舉和結構體類型定義

代碼清單 3819 枚舉和結構體類型定義

1 /* 錄音機狀態 */

2 enum {

3 STA_IDLE = 0, /* 待機狀態 */

4 STA_RECORDING, /* 錄音狀態 */

5 STA_PLAYING, /* 放音狀態 */

6 STA_ERR, /* error */

7 };

8

9 typedef struct {

10 uint8_t ucInput; /* 輸入源:0:MIC, 1:線輸入 */

11 uint8_t ucFmtIdx; /* 音頻格式:標准、位長、采樣頻率 */

12 uint8_t ucVolume; /* 當前放音音量 */

13 uint8_t ucGain; /* 當前增益 */

14 uint8_t ucStatus; /* 錄音機狀態,0表示待機,1表示錄音中,2表示播放中 */

15 } REC_TYPE;

16

17 /* WAV文件頭格式 */

18 typedef __packed struct {

19 uint32_t riff; /* = "RIFF" 0x46464952*/

20 uint32_t size_8; /* 從下個地址開始到文件尾的總字節數 */

21 uint32_t wave; /* = "WAVE" 0x45564157*/

22

23 uint32_t fmt; /* = "fmt " 0x20746d66*/

24 uint32_t fmtSize; /* 下一個結構體的大小(一般為16) */

25 uint16_t wFormatTag; /* 編碼方式,一般為1 */

26 uint16_t wChannels; /* 通道數,單聲道為1,立體聲為2 */

27 uint32_t dwSamplesPerSec; /* 采樣率 */

28 uint32_t dwAvgBytesPerSec; /* 每秒字節數(= 采樣率 × 每個采樣點字節數) */

29 uint16_t wBlockAlign; /* 每個采樣點字節數(=量化比特數/8*通道數) */

30 uint16_t wBitsPerSample; /* 量化比特數(每個采樣需要的bit) */

31

32 uint32_t data; /* = "data" 0x61746164*/

33 uint32_t datasize; /* 純數據長度 */

34 } WavHead;

首先,定義一個枚舉類型羅列錄音和回放功能的狀態,錄音和回放功能是不能同時使用的,使用枚舉類型區分非常有效。

REC_TYPE結構體類型定義了錄音和回放功能相關可控參數,包括WM8978聲音源輸入端,可選板載咪頭或板載耳機插座的LINE線輸入;音頻格式選擇,一般選擇I2S Philips標准、16bit字長、44KHz采樣頻率;音頻輸出耳機音量控制;錄音時聲音增益;當前狀態。

WavHead結構體類型定義了WAV格式文件頭,具體參考"WAV格式文件",這里沒有用到fact chunk。需要注意的是這里使用__packed關鍵字,它表示結構字節對齊。

啟動播放WAV格式音頻文件

代碼清單 3820 StartPlay函數

1 static void StartPlay(const char *filename)

2 {

3 printf("當前播放文件 -> %s\n",filename);

4

5 result=f_open(&file,filename,FA_READ);

6 if (result!=FR_OK) {

7 printf("打開音頻文件失敗!!!->%d\r\n",result);

8 result = f_close (&file);

9 Recorder.ucStatus = STA_ERR;

10 return;

11 }

12 //讀取WAV文件頭

13 result = f_read(&file,&rec_wav,sizeof(rec_wav),&bw);

14 //先讀取音頻數據到緩沖區

15 result = f_read(&file,(uint16_t *)buffer0,RECBUFFER_SIZE*2,&bw);

16 result = f_read(&file,(uint16_t *)buffer1,RECBUFFER_SIZE*2,&bw);

17

18 Delay_ms(10); /* 延遲一段時間,等待I2S中斷結束 */

19 I2S_Stop(); /* 停止I2S錄音和放音 */

20 wm8978_Reset(); /* 復位WM8978到復位狀態 */

21

22 Recorder.ucStatus = STA_PLAYING; /* 放音狀態 */

23

24 /* 配置WM8978芯片,輸入為DAC,輸出為耳機 */

25 wm8978_CfgAudioPath(DAC_ON, EAR_LEFT_ON | EAR_RIGHT_ON);

26 /* 調節音量,左右相同音量 */

27 wm8978_SetOUT1Volume(Recorder.ucVolume);

28 /* 配置WM8978音頻接口為飛利浦標准I2S接口,16bit */

29 wm8978_CfgAudioIF(I2S_Standard_Phillips, 16);

30

31 I2Sx_Mode_Config(g_FmtList[Recorder.ucFmtIdx][0],

32 g_FmtList[Recorder.ucFmtIdx][1],g_FmtList[Recorder.ucFmtIdx][2]);

33 I2Sxext_Mode_Config(g_FmtList[Recorder.ucFmtIdx][0],

34 g_FmtList[Recorder.ucFmtIdx][1],g_FmtList[Recorder.ucFmtIdx][2]);

35

36 I2Sxext_RX_DMA_Init(&recplaybuf[0],&recplaybuf[1],1);

37 DMA_ITConfig(I2Sxext_RX_DMA_STREAM,DMA_IT_TC,DISABLE);//開啟傳輸完成中斷

38 I2Sxext_Recorde_Stop();

39

40 I2Sx_TX_DMA_Init(buffer0,buffer1,RECBUFFER_SIZE);

41 I2S_Play_Start();

42 }

StartPlay函數用於啟動播放WAV格式音頻文件,它有一個形參,用於指示待播放文件名稱。函數首先檢查待播放文件是否可以正常打開,如果打開失敗則退出播放。如果可以正常打開文件則先讀取WAV格式文件頭,保存在WavHead結構體類型變量rec_wav中,同時先讀取音頻數據填充到兩個緩沖區中,這兩個緩沖區buffer0buffer1是定義的全局數組變量,用於DMA雙緩沖區模式。

接下來,配置WM8978的工作環境,首先停止I2S並復位WM8978芯片。這里是播放音頻功能,所以設置WM8978的輸入是DAC,播放I2S接口接收到的音頻數據,輸出設置為耳機輸出。STM32控制器的I2S接口和WM8978I2S接口都設置為I2S Philips標志、字長為16bit

然后,調用I2Sx_TX_DMA_Init配置I2SDMA發送請求,並調用I2S_Play_Start函數使能DMA數據傳輸。

啟動錄音功能

代碼清單 3821 StartRecord函數

1 static void StartRecord(const char *filename)

2 {

3 printf("當前錄音文件 -> %s\n",filename);

4 result=f_open(&file,filename,FA_CREATE_ALWAYS|FA_WRITE);

5 if (result!=FR_OK) {

6 printf("Open wavfile fail!!!->%d\r\n",result);

7 result = f_close (&file);

8 Recorder.ucStatus = STA_ERR;

9 return;

10 }

11

12 // 寫入WAV文件頭,這里必須寫入寫入后文件指針自動偏移到sizeof(rec_wav)位置,

13 // 接下來寫入音頻數據才符合格式要求。

14 result=f_write(&file,(const void *)&rec_wav,sizeof(rec_wav),&bw);

15

16 Delay_ms(10); /* 延遲一段時間,等待I2S中斷結束 */

17 I2S_Stop(); /* 停止I2S錄音和放音 */

18 wm8978_Reset(); /* 復位WM8978到復位狀態 */

19

20 Recorder.ucStatus = STA_RECORDING; /* 錄音狀態 */

21

22 /* 調節放音音量,左右相同音量 */

23 wm8978_SetOUT1Volume(Recorder.ucVolume);

24

25 if (Recorder.ucInput == 1) { /* 線輸入 */

26 /* 配置WM8978芯片,輸入為線輸入,輸出為耳機 */

27 wm8978_CfgAudioPath(LINE_ON | ADC_ON, EAR_LEFT_ON | EAR_RIGHT_ON);

28 wm8978_SetLineGain(Recorder.ucGain);

29 } else { /* MIC輸入 */

30 /* 配置WM8978芯片,輸入為Mic,輸出為耳機 */

31 wm8978_CfgAudioPath(MIC_RIGHT_ON|ADC_ON, EAR_LEFT_ON|EAR_RIGHT_ON);

32 wm8978_SetMicGain(Recorder.ucGain);

33 }

34

35 /* 配置WM8978音頻接口為飛利浦標准I2S接口,16bit */

36 wm8978_CfgAudioIF(I2S_Standard_Phillips, 16);

37

38 I2Sx_Mode_Config(g_FmtList[Recorder.ucFmtIdx][0],

39 g_FmtList[Recorder.ucFmtIdx][1],g_FmtList[Recorder.ucFmtIdx][2]);

40 I2Sxext_Mode_Config(g_FmtList[Recorder.ucFmtIdx][0],

41 g_FmtList[Recorder.ucFmtIdx][1],g_FmtList[Recorder.ucFmtIdx][2]);

42

43 I2Sx_TX_DMA_Init(&recplaybuf[0],&recplaybuf[1],1);

44 DMA_ITConfig(I2Sx_TX_DMA_STREAM,DMA_IT_TC,DISABLE);//開啟傳輸完成中斷

45

46 I2S_DMA_RX_Callback=Recorder_I2S_DMA_RX_Callback;

47 I2Sxext_RX_DMA_Init(buffer0,buffer1,RECBUFFER_SIZE);

48

49 I2S_Play_Start();

50 I2Sxext_Recorde_Start();

51 }

StartRecord函數在結構上與StartPlay函數類似,它實現啟動錄音功能,它有一個形參,指示保存錄音數據的文件名稱。StartRecord函數會首先創建錄音文件,因為用到FA_CREATE_ALWAYS標志位,f_open函數會總是創建新文件,如果已存在該文件則會覆蓋原先的文件內容。

這些必須先寫入WAV格式文件頭數據,這樣當前文件指針自動移動到文件頭的下一個字節,即是存放音頻數據的起始位置。

開發板支持LINE線輸入和板載咪頭輸入,程序默認使用咪頭輸入,在錄音同時使能耳機輸出,這樣錄音時在耳機接口是有相同的聲音輸出的。

錄音功能需要使能擴展I2S

錄音和回放功能選擇

代碼清單 3822 RecorderDemo函數

1 void RecorderDemo(void)

2 {

3 uint8_t i;

4 uint8_t ucRefresh; /* 通過串口打印相關信息標志 */

5 DIR dir;

6

7 Recorder.ucStatus=STA_IDLE; /* 開始設置為空閑狀態 */

8 Recorder.ucInput=0; /* 缺省MIC輸入 */

9 Recorder.ucFmtIdx=3; /* 缺省飛利浦I2S標准,16bit數據長度,44K采樣率 */

10 Recorder.ucVolume=35; /* 缺省耳機音量 */

11 if (Recorder.ucInput==0) { //MIC

12 Recorder.ucGain=50; /* 缺省MIC增益 */

13 rec_wav.wChannels=1; /* 缺省MIC單通道 */

14 } else { //LINE

15 Recorder.ucGain=6; /* 缺省線路輸入增益 */

16 rec_wav.wChannels=2; /* 缺省線路輸入雙聲道 */

17 }

18

19 rec_wav.riff=0x46464952; /* "RIFF"; RIFF 標志 */

20 rec_wav.size_8=0; /* 文件長度,未確定 */

21 rec_wav.wave=0x45564157; /* "WAVE"; WAVE 標志 */

22

23 rec_wav.fmt=0x20746d66; /* "fmt "; fmt 標志,最后一位為空 */

24 rec_wav.fmtSize=16; /* sizeof(PCMWAVEFORMAT) */

25 rec_wav.wFormatTag=1; /* 1 表示為PCM 形式的聲音數據 */

26 /* 每樣本的數據位數,表示每個聲道中各個樣本的數據位數。 */

27 rec_wav.wBitsPerSample=16;

28 /* 采樣頻率(每秒樣本數) */

29 rec_wav.dwSamplesPerSec=g_FmtList[Recorder.ucFmtIdx][2];

30 /* 每秒數據量;其值為通道數×每秒數據位數×每樣本的數據位數/ 8 */

31 rec_wav.dwAvgBytesPerSec=

32 rec_wav.wChannels*rec_wav.dwSamplesPerSec*rec_wav.wBitsPerSample/8;

33 /* 數據塊的調整數(按字節算的),其值為通道數×每樣本的數據位值/8 */

34 rec_wav.wBlockAlign=rec_wav.wChannels*rec_wav.wBitsPerSample/8;

35

36 rec_wav.data=0x61746164; /* "data"; 數據標記符 */

37 rec_wav.datasize=0; /* 語音數據大小目前未確定*/

38

39 /* 如果路徑不存在,創建文件夾 */

40 result = f_opendir(&dir,RECORDERDIR);

41 while (result != FR_OK) {

42 f_mkdir(RECORDERDIR);

43 result = f_opendir(&dir,RECORDERDIR);

44 }

45

46 /* 初始化並配置I2S */

47 I2S_Stop();

48 I2S_GPIO_Config();

49 I2Sx_Mode_Config(g_FmtList[Recorder.ucFmtIdx][0],

50 g_FmtList[Recorder.ucFmtIdx][1],g_FmtList[Recorder.ucFmtIdx][2]);

51 I2Sxext_Mode_Config(g_FmtList[Recorder.ucFmtIdx][0],

52 g_FmtList[Recorder.ucFmtIdx][1],g_FmtList[Recorder.ucFmtIdx][2]);

53

54 I2S_DMA_TX_Callback=MusicPlayer_I2S_DMA_TX_Callback;

55 I2S_Play_Stop();

56

57 I2S_DMA_RX_Callback=Recorder_I2S_DMA_RX_Callback;

58 I2Sxext_Recorde_Stop();

59

60 ucRefresh = 1;

61 bufflag=0;

62 Isread=0;

63 /* 進入主程序循環體 */

64 while (1) {

65 /* 如果使能串口打印標志則打印相關信息 */

66 if (ucRefresh == 1) {

67 DispStatus(); /* 顯示當前狀態,頻率,音量等 */

68 ucRefresh = 0;

69 }

70 if (Recorder.ucStatus == STA_IDLE) {

71 /* KEY2開始錄音 */

72 if (Key_Scan(KEY2_GPIO_PORT,KEY2_PIN)==KEY_ON) {

73 /* 尋找合適文件名 */

74 for (i=1; i<0xff; ++i) {

75 sprintf(recfilename,"0:/recorder/rec%03d.wav",i);

76 result=f_open(&file,(const TCHAR *)recfilename,FA_READ);

77 if (result==FR_NO_FILE)break;

78 }

79 f_close(&file);

80

81 if (i==0xff) {

82 Recorder.ucStatus =STA_ERR;

83 continue;

84 }

85 /* 開始錄音 */

86 StartRecord(recfilename);

87 ucRefresh = 1;

88 }

89 /* TouchPAD開始回放錄音 */

90 if (TPAD_Scan(0)) {

91 /* 開始回放 */

92 StartPlay(recfilename);

93 ucRefresh = 1;

94 }

95 } else {

96 /* KEY1停止錄音或回放 */

97 if (Key_Scan(KEY1_GPIO_PORT,KEY1_PIN)==KEY_ON) {

98 /* 對於錄音,需要把WAV文件內容填充完整 */

99 if (Recorder.ucStatus == STA_RECORDING) {

100 I2Sxext_Recorde_Stop();

101 I2S_Play_Stop();

102 rec_wav.size_8=wavsize+36;

103 rec_wav.datasize=wavsize;

104 result=f_lseek(&file,0);

105 result=f_write(&file,(const void *)&rec_wav,

106 sizeof(rec_wav),&bw);

107 result=f_close(&file);

108 printf("錄音結束\r\n");

109 }

110 ucRefresh = 1;

111 Recorder.ucStatus = STA_IDLE; /* 待機狀態 */

112 I2S_Stop(); /* 停止I2S錄音和放音 */

113 wm8978_Reset(); /* 復位WM8978到復位狀態 */

114 }

115 }

116 /* DMA傳輸完成 */

117 if (Isread==1) {

118 Isread=0;

119 switch (Recorder.ucStatus) {

120 case STA_RECORDING: // 錄音功能,寫入數據到文件

121 if (bufflag==0)

122 result=f_write(&file,buffer0,RECBUFFER_SIZE*2,(UINT*)&bw);

123 else

124 result=f_write(&file,buffer1,RECBUFFER_SIZE*2,(UINT*)&bw);

125 wavsize+=RECBUFFER_SIZE*2;

126 break;

127 case STA_PLAYING: // 回放功能,讀取數據到播放緩沖區

128 if (bufflag==0)

129 result = f_read(&file,buffer0,RECBUFFER_SIZE*2,&bw);

130 else

131 result = f_read(&file,buffer1,RECBUFFER_SIZE*2,&bw);

132 /* 播放完成或讀取出錯停止工作 */

133 if ((result!=FR_OK)||(file.fptr==file.fsize)) {

134 printf("播放完或者讀取出錯退出...\r\n");

135 I2S_Play_Stop();

136 file.fptr=0;

137 f_close(&file);

138 Recorder.ucStatus = STA_IDLE; /* 待機狀態 */

139 I2S_Stop(); /* 停止I2S錄音和放音 */

140 wm8978_Reset(); /* 復位WM8978到復位狀態 */

141 }

142 break;

143 }

144 }

145 }

146 }

RecorderDemo函數實現錄音和回放功能。Recorder是一個REC_TYPE結構體類型變量,指示錄音和回放功能相關參數,這里通過賦值缺省選擇板載咪頭輸入、使用I2S Philips標准、16bit字長、44KHz采樣頻率,並設置了音量和增益,對於LINE輸入增益范圍為0~7rec_wavWavHead結構體類型變量,用於設置WAV格式文件頭,很多成員賦值為缺省值即可,成員size_8datasize變量表示數據大小,因為錄音時間長度直接影響這兩個變量大小,現在並無法確定它們大小,需要在停止錄音后才可計算得到它們的值。

接下來是使用FatFs的功能函數f_opendirf_mkdir組合判斷SD卡內是否有名為"recorder"的文件夾,如果沒有改文件夾就創建它,因為我們打算把錄音文件存放在該文件夾內。

接下來就是調用I2S相關函數完成I2S工作環境配置。

MusicPlayer_I2S_DMA_TX_Callback是我們定義的一個函數的函數名,把他賦值給函數指針I2S_DMA_TX_Callback,這樣可以實現在執行DMA數據發送完成中斷服務函數時執行MusicPlayer_I2S_DMA_TX_Callback函數。Recorder_I2S_DMA_RX_Callback情況與之類似。開始時停止錄音和回放功能。

ucRefresh變量作為通過串口打印相關操作和狀態信息到串口調試助手"刷新"標志。bufflag變量用於指示當前空閑緩沖區,工程定義了兩個緩沖區buffer0buffer1用於DMA雙緩沖區模式,對於錄音功能,buffag0表示當前DMA使用buffer1填充,buffer0已經填充完整,為1表示當前DMA使用buffer0填充,buffer1已經填充完整;對於回放功能,buffag0表示buffer1用於當前播放,buffer0已經播放完成需要讀取新數據填充,為1表示buffer0用於當前播放,buffer1已經播放完成需要讀取新數據填充。Isread變量用於指示DMA傳輸完成狀態,為1時表示DMA傳輸完成,只有在DMA傳輸完成中斷服務函數中才會被置1

接下來就是無限循環函數了,先判斷ucRefresh變量狀態,如果為1就執行DispStatus函數,該函數只是使用printf函數打印相關信息。開發板集成有兩個獨立按鍵K1K2,還有一個電容按鍵,程序設置按下K2按鍵開始錄音功能,觸摸電容按鍵開始回放功能,按下K1按鍵用於停止錄音和回放功能,這里K2按鍵和電容按鍵是互斥的。

在空閑狀態下,允許按下K2按鍵啟動錄音,錄音功能是通過調用StartRecord函數啟動的,該函數需要一個參數指示錄音文件名稱,使用在執行StartRecord函數之前會循環使用f_open函數獲取SD卡內不存在的文件,防止覆蓋已存在文件。

在空閑狀態下,允許觸摸電容按鍵啟動回放功能,它直接使用StartPlay函數啟動上一個錄音文件播放。

不在空閑狀態下,按下K1按鍵可以停在錄音或回放功能,回放功能只需要停在I2SDMA數據發送接口並復位WM8978即可,錄音功能需要停在擴展I2SDMA數據接收功能,並且需要填充完整WAV格式文件頭並寫入到錄音文件中。

無限循環還要不懂檢測Isread變量的狀態,當它在DMA傳輸完成中斷服務函數被置1時說明雙緩沖區狀態發生改變,對於錄音功能,需要把以填充滿的緩沖區數據通過f_write寫入到文件中;對於回放功能,需要利用f_read函數讀取新數據填充到已播緩沖區中,如果遇到讀取出錯或文件已經播放完全需要停止I2SDMA傳輸並復位WM8978

DMA傳輸完成中斷回調函數

代碼清單 3823 DMA傳輸完成中斷回調函數

1 /* DMA發送完成中斷回調函數 */

2 /* 緩沖區內容已經播放完成,需要切換緩沖區,進行新緩沖區內容播放

3 同時讀取WAV文件數據填充到已播緩沖區 */

4 void MusicPlayer_I2S_DMA_TX_Callback(void)

5 {

6 if (Recorder.ucStatus == STA_PLAYING) {

7 if (I2Sx_TX_DMA_STREAM->CR&(1<<19)) { //當前使用Memory1數據

8 bufflag=0; //可以將數據讀取到緩沖區0

9 } else { //當前使用Memory0數據

10 bufflag=1; //可以將數據讀取到緩沖區1

11 }

12 Isread=1; // DMA傳輸完成標志

13 }

14 }

15

16 /* DMA接收完成中斷回調函數 */

17 /* 錄音數據已經填充滿了一個緩沖區,需要切換緩沖區,

18 同時可以把已滿的緩沖區內容寫入到文件中 */

19 void Recorder_I2S_DMA_RX_Callback(void)

20 {

21 if (Recorder.ucStatus == STA_RECORDING) {

22 if (I2Sxext_RX_DMA_STREAM->CR&(1<<19)) { //當前使用Memory1數據

23 bufflag=0;

24 } else { //當前使用Memory0數據

25 bufflag=1;

26 }

27 Isread=1; // DMA傳輸完成標志

28 }

29 }

這兩個函數用於在DMA傳輸完成后切換緩沖區。DMA數據流x配置寄存器的CT位用於指示當前目標緩沖區,如果為1,當前目標緩沖區為存儲器1;如果為0,則為存儲器0

主函數

代碼清單 3824 main函數

1 int main(void)

2 {

3 FRESULT result;

4

5 /* NVIC中斷優先級組選擇 */

6 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

7

8 /* 關閉BL_8782wifi使能 */

9 BL8782_PDN_INIT();

10

11 /* 初始化按鍵 */

12 Key_GPIO_Config();

13

14 /* 初始化調試串口,一般為串口1 */

15 Debug_USART_Config();

16

17 /* 掛載SD卡文件系統 */

18 result = f_mount(&fs,"0:",1); //掛載文件系統

19 if (result!=FR_OK) {

20 printf("\n SD卡文件系統掛載失敗\n");

21 while (1);

22 }

23

24 /* 初始化系統滴答定時器 */

25 SysTick_Init();

26 printf("WM8978錄音和回放功能\n");

27

28 /* 初始化電容按鍵 */

29 TPAD_Init();

30

31 /* 檢測WM8978芯片,此函數會自動配置CPUGPIO */

32 if (wm8978_Init()==0) {

33 printf("檢測不到WM8978芯片!!!\n");

34 while (1); /* 停機 */

35 }

36 printf("初始化WM8978成功\n");

37

38 /* 錄音與回放功能 */

39 RecorderDemo();

40 }

main函數主要完成各個外設的初始化,包括獨立按鍵初始化、電容按鍵初始化、調試串口初始化、SD卡文件系統掛載還有系統定時器初始化。

wm8978_Init初始化I2C接口用於控制WM8978芯片,並復位WM8978芯片,如果初始化成功則運行RecorderDemo函數進行錄音和回放功能測試。

38.6.3 下載驗證

Micro SD卡插入到開發板右側的卡槽內,使用USB線連接開發板上的"USB TO UART"接口到電腦,將耳機插入到開發板上邊沿左側的耳機插座,電腦端配置好串口調試助手參數。編譯實驗程序並下載到開發板上,程序運行后在串口調試助手可接收到開發板發過來的提示信息,按下開發板左下邊沿的K2按鍵,開始執行錄音功能測試,不斷對着咪頭說話,就可以把聲音錄制下來,按下K1按鍵可以停止錄音。然后觸摸電容按鍵就可以在耳機接口聽到之前錄音內容了,按下K1按鍵可停止播放。錄音完成后也可以在電腦端打開SD卡,找到其中的錄音文件,可在電腦端音頻播放器播放錄音文件。

38.7 MP3播放器

MP3格式音樂文件普遍存在我們生活中,實際上MP3本身是一種音頻編碼方式,全稱為Moving Picture Experts Group Audio Layer III(MPEG Audio Layer 3)MPEG音頻文件是MPEG標准中的聲音部分,根據壓縮質量和編碼復雜程度划分為三層,即Layer-1Layer2Layer3,且分別對應MP1MP2MP3這三種聲音文件,其中MP3壓縮率可達到10:112:1,可以大大減少文件占用存儲空間大小。MPEG音頻編碼的層次越高,編碼器越復雜,壓縮率也越高。MP3是利用人耳對高頻聲音信號不敏感的特性,將時域波形信號轉換成頻域信號,並划分成多個頻段,對不同的頻段使用不同的壓縮率,對高頻加大壓縮比(甚至忽略信號)對低頻信號使用小壓縮比,保證信號不失真。這樣一來就相當於拋棄人耳基本聽不到的高頻聲音,只保留能聽到的低頻部分,這樣可得到很高的壓縮率。

38.7.1 MP3文件結構

MP3文件大致分為3個部分:TAG_V2ID3V2),音頻數據,TAG_V1(ID3V1)ID3MP3文件中附加關於該MP3文件的歌手、標題、專輯名稱、年代、風格等等信息,有兩個版本ID3V1ID3V2ID3V1固定存放在MP3文件末尾,固定長度為128字節,以TAG三個字符開頭,后面跟上歌曲信息。因為ID3V1可存儲信息量有限,有些MP3文件添加了ID3V2ID3V2是可選的,如果存在ID3V2那它必然存在在MP3文件起始位置,它實際是ID3V1的補充。

1.    ID3V2

ID3V2以靈活管理方法MP3文件附件信息,比ID3V1可以存儲更多的信息,同時也比ID3V1在結構上復雜得多。常用是ID3V2.3版本,一個MP3文件最多就一個ID3V2.3標簽。ID3V2.3標簽由一個標簽頭和若干個標簽幀或一個擴展標簽頭組成。關於曲目的信息如標題、作者等都存放在不同的標簽幀中,擴展標簽頭和標簽幀並不是必要的,但每個標簽至少要有一個標簽幀。標簽頭和標簽幀一起順序存放在MP3文件的首部。

標簽頭結構如表 386

386 標簽頭

地址

標識

字節

描述

00H

Header

3

字符串"ID3"

03H

Ver

1

版本號,ID3V2.3就記錄為3

04H

Revision

1

副版本號,一般記錄為0

05H

Flag

1

標志字節,一般不用設置

06H

Size

4

標簽大小,整個ID3V2所占空間字節的大小

其中標簽大小計算如下:

Total_size=(Size[0]&0x7F)*0x200000+(Size[1]&0x7F)*0x400

+(Size[2]&0x7F)*0x80+(Size[3]&0x7F)

每個標簽幀都有一個10個字節的幀頭和至少一個字節的不定長度內容,在文件中是連續存放的。標簽幀頭由三個部分組成,即Frame[4]Size[4]Flags[2]Frame是用四個字符表示幀內容含義,比如TIT2為標題、TPE1為作者、TALB為專輯、TRCK為音軌、TYER為年代等等信息。Size用四個字節組成32bit數表示幀大小。Flags是標簽幀的標志位,一般為0即可。

2.    ID3V1

ID3V1是早期的版本,可以存放的信息量有限,但編程比ID3V2簡單很多,即使到現在使用還是很多。ID3V1是固定存放在MP3文件末尾的128字節,組成結構見表 387

387 ID3V1結構

字節

字長

描述

1-3

3

標識符:"TAG"

4-33

30

歌名

34-63

30

作者

64-93

30

專輯名

94-97

4

年份

98-127

30

附注

128

1

MP3音樂類別

其中,歌名是固定分配為30個字節,如果歌名太短則以0填充完整,太長則被截斷,其他信息類似情況存儲。MP3音樂類別總共有147種,每一種對應一個數組,比如0對應"Blues"、1對應"Classic Rock"、2對應"Country"等等。

3.    MP3數據幀

音頻數據是MP3文件的主體部分,它由一系列數據幀(Frame)組成,每個Frame包含一段音頻的壓縮數據,通過解碼庫解碼即可得到對應PCM音頻數據,就可以通過I2S發送到WM8978芯片播放音樂,按順序解碼所有幀就可以得到整個MP3文件的音軌。

每個Frame由兩部分組成,幀頭和數據實體,Frame長度可能不同,由位率決定。幀頭記錄了MP3數據幀的位率、采樣率、版本等等信息,總共有4個字節,見表 388

388 數據幀頭結構

名稱

位長

描述

Sync

12字節

11

同步信息:每位固定為:1

Version

2

版本:00-MPEG2.501-未定義;10-MPEG211-MPEG1

Layer

2

層:00-未定義;01-Layer310-Layer211-Layer1

Error Protection

1

CRC校驗:0-校驗;1-不校驗。

Bitrate incdex

3字節

4

位率:參考表 389

Sampling frequency

2

采樣頻率:

對於MPEG-100-44.1kHz01-48kHz10-32kHz11-未定義。

對於MPEG-200-22.05kHz01-24kHz10-16kHz11-未定義。

對於MPEG-2.500-11.025kHz01-12kHz10-8kHz11-未定義。

Padding

1

幀長調整:用來調整文件頭長度,0-無需調整;1-調整。

Private

1

保留字。

Mode

4字節

2

聲道:00-立體聲Stereo01-Joint Stereo10-雙聲道;11-單聲道。

Mode extension

2

擴充模式:當聲道模式為01時才使用。

Copyright

1

版權:文件是否合法,0-不合法;1-合法。

Original

1

原版標志:0-非原版;1-原版。

Emphasis

2

強調方式:用於聲音經降噪壓縮后再補償的分類,幾乎不用。

位率位在不同版本和層都有不同的定義,具體參考表 389,單位為kbps。其中V1對應MPEG-1V2對應MPEG-2MPEG-2.5L1對應Layer1L2對應Layer2L3對應Layer3free表示位率可變,bad表示該定義不合法。

389 位率選擇

bits

V1,L1

V1,L2

V1,L3

V2,L1

V2,L2

V2,L3

0000

free

free

free

free

free

free

0001

32

32

32

32(32)

32(8)

8(8)

0010

64

48

40

64(48)

48(16)

16(16)

0011

96

56

48

96(56)

56(24)

24(24)

0100

128

64

56

128(64)

64(32)

32(32)

0101

160

80

64

160(80)

80(40)

64(40)

0110

192

96

80

192(96)

96(48)

80(48)

0111

224

112

96

224(112)

112(56)

56(56)

1000

256

128

112

256(128)

128(64)

64(64)

1001

288

160

128

288(144)

160(80)

128(80)

1010

320

192

160

320(160)

192(96)

160(96)

1011

352

224

192

352(176)

224(112)

112(112)

1100

384

256

224

384(192)

256(128)

128(128)

1101

416

320

256

416(224)

320(144)

256(144)

1110

448

384

320

448(256)

384(160)

320(160)

1111

bad

bad

bad

bad

bad

bad

例如,當Version=11Layer=01Bitrate_incdex=0101時,描述為MPEG-1 Layer3(MP3)位率為64kbps

數據幀長度取決於位率(Bitrate)和采樣頻率(Sampling_freq),具體計算如下:

對於MPEG-1標准,Layer1時:

Size=(48000*Bitrate)/Sampling_freq + Padding

Layer2Layer3時:

Size=(144000*Bitrate)/Sampling_freq + Padding

對於MPEG-2MPEG-2.5標准,Layer1時:

Size=(24000*Bitrate)/Sampling_freq + Padding

Layer2Layer3時:

Size=(72000*Bitrate)/Sampling_freq + Padding

如果有CRC校驗,則存放在在幀頭后兩個字節。接下來就是幀主數據(MAIN_DATA)。一般來說一個MP3文件每個幀的Bitrate是固定不變的,所以每個幀都有相同的長度,稱為CBR,還有小部分MP3文件的Bitrate是可變,稱之為VBR,它是XING公司推出的算法。

MAIN_DATA保存的是經過壓縮算法壓縮后得到的數據,為得到大的壓縮率會損失部分源聲音,屬於失真壓縮。

38.7.2 MP3解碼庫

MP3文件是經過壓縮算法壓縮而存在的,為得到PCM信號,需要對MP3文件進行解碼,解碼過程大致為:比特流分析、霍夫曼編碼、逆量化處理、立體聲處理、頻譜重排列、抗鋸齒處理、IMDCT變換、子帶合成、PCM輸出。整個過程涉及很多算法計算,要自己編程實現不是一件現實的事情,還好有很多公司經過長期努力實現了解碼庫編程。

現在合適在小型嵌入式控制器移植運行的有兩個版本的開源MP3解碼庫,分別為Libmad解碼庫和Helix解碼庫,Libmad是一個高精度MPEG音頻解碼庫,支持MPEG-1MPEG-2以及MPEG-2.5標准,它可以提供24bitPCM輸出,完全是定點計算,更多信息可參考網站:http://www.underbit.com/

Helix解碼庫支持浮點和定點計算實現,將該算法移植到STM32控制器運行使用定點計算實現,它支持MPEG-1MPEG-2以及MPEG-2.5標准的Layer3解碼。Helix解碼庫支持可變位速率、恆定位速率,以及立體聲和單聲道音頻格式。更多信息可參考網站:https://datatype.helixcommunity.org/Mp3dec

因為Helix解碼庫需要占用的資源比Libmad解碼庫更少,特別是RAM空間的使用,這對STM32控制器來說是比較重要的,所以在實驗工程中我們選擇Helix解碼庫實現MP3文件解碼。這兩個解碼庫都是一幀為解碼單位的,一次解碼一幀,這在應用解碼庫時是需要着重注意的。

Helix解碼庫涉及算法計算,整個界面過程復雜,有興趣可以深入探究,這里我們着重講解Helix移植和使用方法。

Helix網站有提供解碼庫代碼,經過整理,移植Helix解碼庫需要用到的的文件如圖 3813。有優化解碼速度,部分解碼過程使用匯編實現。

3813 Helix解碼庫文件結構

38.7.3 Helix解碼庫移植

在"錄音與回放實驗"已經實現了WM8978驅動代碼,現在我們可以移植Helix解碼庫工程中,實現MP3文件解碼,將解碼輸出的PCM數據通過I2S接口發送到WM8978芯片實現音樂播放。

我們在"錄音與回放實驗"工程文件基礎上移植Helix解碼,首先將需要用到的文件添加到工程中,如圖 3814MP3文件夾下文件是Helix解碼庫源碼,工程移植中是不需要修改文件夾下代碼的,我們只需直接調用相關解碼函數即可。建議自己移植時直接使用例程中mp3文件夾內文件。我們是在mp3Player.c文件中調用Helix解碼庫相關函數實現MP3文件解碼的,該文件是我們自己創建的。

3814 添加Helix解碼庫文件到工程

接下來還需要在工程選項中添加Helix解碼庫的文件夾路徑,編譯器可以尋找到相關頭文件,見圖 3815

3815 添加Helix解碼庫文件夾路徑

38.7.4 MP3播放器功能實現

"錄音與回放實驗"中的回放功能實際上就是從SD卡內讀取WAV格式文件數據,然后提取里邊音頻數據通過I2S傳輸到WM8978芯片內實現聲音播放。MP3播放器的功能也是類似的,只不過現在音頻數據提取方法不同,MP3需要先經過解碼庫解碼后才可得到"可直接"播放的音頻數據。由此可以看到,MP3播放器只是添加了MP3解碼庫實現代碼,在硬件設計上並沒有任何改變,即這里直接使用"錄音與回放實驗"中硬件設計即可。

實驗工程代碼中創建mp3Player.cmp3Player.h兩個文件存放MP3播放器實現代碼。Helix解碼庫是用來解碼MP3數據幀,一次解碼一幀,它是不能用來檢索ID3V1ID3V2標簽的,如果需要獲取歌名、作者等信息需要自己編程實現。解碼過程可能用到的Helix解碼庫函數有:

    MP3InitDecoder:初始化解碼器函數

    MP3FreeDecoder:關閉解碼器函數

    MP3FindSyncWord:尋找幀同步函數

    MP3Decode:解碼MP3幀函數

    MP3GetLastFrameInfo:獲取幀信息函數

MP3InitDecoder函數初始化解碼器,它會申請分配一個存儲空間用於存放解碼器狀態的一個數據結構並將其初始化,該數據結構由MP3DecInfo結構體定義,它封裝了解碼器內部運算數據信息。MP3InitDecoder函數會返回指向該數據結構的指針。

MP3FreeDecoder函數用於關閉解碼器,釋放由MP3InitDecoder函數申請的存儲空間,所以一個MP3InitDecoder函數都需要有一個MP3FreeDecoder函數與之對應。它有一個形參,一般由MP3InitDecoder函數的返回指針賦值。

MP3FindSyncWord函數用於尋址數據幀同步信息,實際上就是尋址數據幀開始的11bit都為"1"的同步信息。它有兩個形參,第一個為源數據緩沖區指針,第二個為緩沖區大小,它會返回一個int類型變量,用於指示同步字較緩沖區起始地址的偏移量,如果在緩沖區中找不到同步字,則直接返回-1

MP3Decode函數用於解碼數據幀,它有五個形參,第一個為解碼器數據結構指針,一般由MP3InitDecoder函數返回值賦值;第二個參數為指向解碼源數據緩沖區開始地址的一個指針,注意這里是地址的指針,即是指針的指針;第三個參數是一個指向存放解碼源數據緩沖區有效數據量的變量指針;第四個參數是解碼后輸出PCM數據的指針,一般由我們定義的緩沖區地址賦值,對於雙聲道輸出數據緩沖區以LRLRLR…順序排列;第五個參數是數據格式選擇,一般設置為0表示標准的MPEG格式。函數還有一個返回值,用於返回解碼錯誤,返回ERR_MP3_NONE說明解碼正常。

MP3GetLastFrameInfo函數用於獲取數據幀信息,它有兩個形參,第一個為解碼器數據結構指針,一般由MP3InitDecoder函數返回值賦值;第二個參數為數據幀信息結構體指針,該結構體定義見代碼清單 3825

代碼清單 3825 MP3數據幀信息結構體

1 typedef struct _MP3FrameInfo {

2 int bitrate; //

3 int nChans; //聲道數

4 int samprate; //采樣率

5 int bitsPerSample; //采樣位數

6 int outputSamps; //PCM數據數

7 int layer; //層級

8 int version; //版本

9 } MP3FrameInfo;

該結構體成員包括了該數據幀的位率、聲道、采樣頻率等等信息,它實際上是從數據幀的幀頭信息中提取的。

MP3播放器實現

代碼清單 3826 mp3PlayerDemo函數

1 void mp3PlayerDemo(const char *mp3file)

2 {

3 uint8_t *read_ptr=inputbuf;

4 uint32_t frames=0;

5 int err=0, i=0, outputSamps=0;

6 int read_offset = 0; /* 讀偏移指針 */

7 int bytes_left = 0; /* 剩余字節數 */

8

9 mp3player.ucFreq=I2S_AudioFreq_Default;

10 mp3player.ucStatus=STA_IDLE;

11 mp3player.ucVolume=40;

12

13 result=f_open(&file,mp3file,FA_READ);

14 if (result!=FR_OK) {

15 printf("Open mp3file :%s fail!!!->%d\r\n",mp3file,result);

16 result = f_close (&file);

17 return; /* 停止播放 */

18 }

19 printf("當前播放文件 -> %s\n",mp3file);

20

21 //初始化MP3解碼器

22 Mp3Decoder = MP3InitDecoder();

23 if (Mp3Decoder==0) {

24 printf("初始化helix解碼庫設備\n");

25 return; /* 停止播放 */

26 }

27 printf("初始化中...\n");

28

29 Delay_ms(10); /* 延遲一段時間,等待I2S中斷結束 */

30 wm8978_Reset(); /* 復位WM8978到復位狀態 */

31

32 /* 配置WM8978芯片,輸入為DAC,輸出為耳機 */

33 wm8978_CfgAudioPath(DAC_ON, EAR_LEFT_ON | EAR_RIGHT_ON);

34

35 /* 調節音量,左右相同音量 */

36 wm8978_SetOUT1Volume(mp3player.ucVolume);

37

38 /* 配置WM8978音頻接口為飛利浦標准I2S接口,16bit */

39 wm8978_CfgAudioIF(I2S_Standard_Phillips, 16);

40

41 /* 初始化並配置I2S */

42 I2S_Stop();

43 I2S_GPIO_Config();

44 I2Sx_Mode_Config(I2S_Standard_Phillips,I2S_DataFormat_16b,

45 mp3player.ucFreq);

46 I2S_DMA_TX_Callback=MP3Player_I2S_DMA_TX_Callback;

47 I2Sx_TX_DMA_Init((uint16_t *)outbuffer[0],(uint16_t *)outbuffer[1],

48 MP3BUFFER_SIZE);

49

50 bufflag=0;

51 Isread=0;

52

53 mp3player.ucStatus = STA_PLAYING; /* 放音狀態 */

54 result=f_read(&file,inputbuf,INPUTBUF_SIZE,&bw);

55 if (result!=FR_OK) {

56 printf("讀取%s失敗 -> %d\r\n",mp3file,result);

57 MP3FreeDecoder(Mp3Decoder);

58 return;

59 }

60 read_ptr=inputbuf;

61 bytes_left=bw;

62 /* 進入主程序循環體 */

63 while (mp3player.ucStatus == STA_PLAYING) {

64 //尋找幀同步,返回第一個同步字的位置

65 read_offset = MP3FindSyncWord(read_ptr, bytes_left);

66 //沒有找到同步字

67 if (read_offset < 0) {

68 result=f_read(&file,inputbuf,INPUTBUF_SIZE,&bw);

69 if (result!=FR_OK) {

70 printf("讀取%s失敗 -> %d\r\n",mp3file,result);

71 break;

72 }

73 read_ptr=inputbuf;

74 bytes_left=bw;

75 continue;

76 }

77

78 read_ptr += read_offset; //偏移至同步字的位置

79 bytes_left -= read_offset; //同步字之后的數據大小

80 if (bytes_left < 1024) { //補充數據

81 /* 注意這個地方因為采用的是DMA讀取,所以一定要4字節對齊 */

82 i=(uint32_t)(bytes_left)&3; //判斷多余的字節

83 if (i) i=4-i; //需要補充的字節

84 memcpy(inputbuf+i, read_ptr, bytes_left); //從對齊位置開始復制

85 read_ptr = inputbuf+i; //指向數據對齊位置

86 //補充數據

87 result=f_read(&file,inputbuf+bytes_left+i,

88 INPUTBUF_SIZE-bytes_left-i,&bw);

89 bytes_left += bw; //有效數據流大小

90 }

91 //開始解碼參數:mp3解碼結構體、輸入流指針、輸入流大小、輸出流指針、數據格式

92 err = MP3Decode(Mp3Decoder, &read_ptr, &bytes_left,

93 outbuffer[bufflag], 0);

94 frames++;

95 //錯誤處理

96 if (err != ERR_MP3_NONE) {

97 switch (err) {

98 case ERR_MP3_INDATA_UNDERFLOW:

99 printf("ERR_MP3_INDATA_UNDERFLOW\r\n");

100 result = f_read(&file, inputbuf, INPUTBUF_SIZE, &bw);

101 read_ptr = inputbuf;

102 bytes_left = bw;

103 break;

104 case ERR_MP3_MAINDATA_UNDERFLOW:

105 /* do nothing - next call to decode will provide more mainData */

106 printf("ERR_MP3_MAINDATA_UNDERFLOW\r\n");

107 break;

108 default:

109 printf("UNKNOWN ERROR:%d\r\n", err);

110 // 跳過此幀

111 if (bytes_left > 0) {

112 bytes_left --;

113 read_ptr ++;

114 }

115 break;

116 }

117 Isread=1;

118 } else { //解碼無錯誤,准備把數據輸出到PCM

119 MP3GetLastFrameInfo(Mp3Decoder, &Mp3FrameInfo); //獲取解碼信息

120 /* 輸出到DAC */

121 outputSamps = Mp3FrameInfo.outputSamps; //PCM數據個數

122 if (outputSamps > 0) {

123 if (Mp3FrameInfo.nChans == 1) { //單聲道

124 //單聲道數據需要復制一份到另一個聲道

125 for (i = outputSamps - 1; i >= 0; i--) {

126 outbuffer[bufflag][i * 2] = outbuffer[bufflag][i];

127 outbuffer[bufflag][i * 2 + 1] = outbuffer[bufflag][i];

128 }

129 outputSamps *= 2;

130 }//if (Mp3FrameInfo.nChans == 1) //單聲道

131 }//if (outputSamps > 0)

132

133 /* 根據解碼信息設置采樣率 */

134 if (Mp3FrameInfo.samprate != mp3player.ucFreq) { //采樣率

135 mp3player.ucFreq = Mp3FrameInfo.samprate;

136

137 printf(" \r\n Bitrate %dKbps",Mp3FrameInfo.bitrate/1000);

138 printf(" \r\n Samprate %dHz", mp3player.ucFreq);

139 printf(" \r\n BitsPerSample %db", Mp3FrameInfo.bitsPerSample);

140 printf(" \r\n nChans %d", Mp3FrameInfo.nChans);

141 printf(" \r\n Layer %d", Mp3FrameInfo.layer);

142 printf(" \r\n Version %d", Mp3FrameInfo.version);

143 printf(" \r\n OutputSamps %d", Mp3FrameInfo.outputSamps);

144 printf("\r\n");

145 //I2S_AudioFreq_Default = 2,正常的幀,每次都要改速率

146 if (mp3player.ucFreq >= I2S_AudioFreq_Default) {

147 //根據采樣率修改I2S速率

148 I2Sx_Mode_Config(I2S_Standard_Phillips,I2S_DataFormat_16b,

149 mp3player.ucFreq);

150 I2Sx_TX_DMA_Init((uint16_t *)outbuffer[0],

151 (uint16_t *)outbuffer[1],outputSamps);

152 }

153 I2S_Play_Start();

154 }

155 }//else 解碼正常

156

157 if (file.fptr==file.fsize) { //mp3文件讀取完成,退出

158 printf("END\r\n");

159 break;

160 }

161

162 while (Isread==0) {

163 }

164 Isread=0;

165 }

166 I2S_Stop();

167 mp3player.ucStatus=STA_IDLE;

168 MP3FreeDecoder(Mp3Decoder);

169 f_close(&file);

170 }

mp3PlayerDemo函數是MP3播放器的實現函數,篇幅很長,需要我們仔細分析。它有一個形參,用於指定待播放的MP3文件,需要用MP3文件的絕對路徑加全名稱賦值。

read_ptr是定義的一個指針變量,它用於指示解碼器源數據地址,把它初始化為用來存放解碼器源數據緩沖區(inputbuf數組)的首地址。read_offsetbytes_left主要用於MP3FindSyncWord函數,read_offset用來指示幀同步相對解碼器源數據緩沖區首地址的偏移量,bytes_left用於指示解碼器源數據緩沖區有效數據量。

mp3player是一個MP3_TYPE結構體類型變量,指示音量、狀態和采樣頻率信息。

f_open函數用於打開文件,如果文件打開失敗則直接退出播放。MP3InitDecoder函數用於初始化Helix解碼器,分配解碼器必須內存空間,如果初始化解碼器失敗直接退出播放。

接下來配置WM8978芯片功能,使能耳機輸出,設置音量,使用I2S Philips標准和16bit數據長度。還要設置I2S外設工作環境,同樣是I2S Philips標准和16bit數據長度,采樣頻率先使用I2S_AudioFreq_Default,在運行MP3GetLastFrameInfo函數獲取數據幀的采樣頻率后需要再次修改。MP3Player_I2S_DMA_TX_Callback是一個函數名,該函數實現DMA雙緩沖區標志位切換以及指示DMA傳輸完成,它在I2SDMA發送傳輸完成中斷服務函數中調用。I2Sx_TX_DMA_Init用於初始化I2SDMA發送請求,使用雙緩沖區模式。bufflag用於指示當前DMA傳輸的緩沖區號,Isread用於指示DMA傳輸完成。

f_read函數從SD卡讀取MP3文件數據,存放在inputbuf緩沖區中,bw變量保存實際讀取到的數據的字節數。如果讀取數據失敗則運行MP3FreeDecoder函數關閉解碼器后退出播放器。

接下來是循環解碼幀數據並播放。MP3FindSyncWord用於選擇幀同步信息,如果在源數據緩沖區中找不到同步信息,read_offset值為-1,需要讀取新的MP3文件數據,重新尋找幀同步信息。一般MP3起始部分是ID3V2信息,所以可能需要循環幾次才能尋找到幀同步信息。如果找到幀同步信息說明找到數據幀,接下來數據就是數據幀的幀頭以及MAIN_DATA

有時找到了幀同步信息,但可能源數據緩沖區並沒有包括整幀數據,這時需要把從幀同步信息開始的源數據,復制到源數據緩沖區起始地址上,再使用f_read函數讀取新數據填充滿整個源數據緩沖區,保證源數據緩沖區保存有整幀源數據。

MP3Decode函數開始對源數據緩沖區中幀數據進行解碼,通過函數返回值可判斷得到解碼狀態,如果發生解碼錯誤則執行對應的代碼。

在解碼無錯誤時,就可以使用MP3GetLastFrameInfo函數獲取幀信息,如果有數據輸出並且是單聲道需要把數據復制成雙聲道數據格式。如果采樣頻率與上一次有所不同則執行通過串口打印幀信息到串口調試助手,可能還需要調整I2S的工作環境,還調用I2S_Play_Start啟動播放。因為mp3player.ucFreq缺省值為I2S_AudioFreq_Default,一般都不會與Mp3FrameInfo.samprate相等,所以會至少進入if語句內,即會執行I2S_Play_Start函數。

如果文件讀取已經都了文件末尾就退出循環,MP3文件已經播放完整。循環中還需要等待DMA數據傳輸完成才進行下一幀的解碼操作。

mp3PlayerDemo函數最后停止I2S,關閉解碼器和MP3文件。

DMA發送完成中斷回調函數

代碼清單 3827 MP3Player_I2S_DMA_TX_Callback函數

1 void MP3Player_I2S_DMA_TX_Callback(void)

2 {

3 if (I2Sx_TX_DMA_STREAM->CR&(1<<19)) { //當前使用Memory1數據

4 bufflag=0; //可以將數據讀取到緩沖區0

5 } else { //當前使用Memory0數據

6 bufflag=1; //可以將數據讀取到緩沖區1

7 }

8 Isread=1; // DMA傳輸完成標志

9 }

MP3Player_I2S_DMA_TX_Callback函數用於在DMA發送完成后切換緩沖區。DMA數據流x配置寄存器的CT位用於指示當前目標緩沖區,如果為1,當前目標緩沖區為存儲器1;如果為0,則為存儲器0

主函數

代碼清單 3828 main函數

1 int main(void)

2 {

3 FRESULT result;

4

5 /* NVIC中斷優先級組選擇 */

6 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

7

8 /* 關閉BL_8782wifi使能 */

9 BL8782_PDN_INIT();

10

11 /* 初始化調試串口,一般為串口1 */

12 Debug_USART_Config();

13

14 /* 掛載SD卡文件系統 */

15 result = f_mount(&fs,"0:",1); //掛載文件系統

16 if (result!=FR_OK) {

17 printf("\n SD卡文件系統掛載失敗\n");

18 while (1);

19 }

20

21 /* 初始化系統滴答定時器 */

22 SysTick_Init();

23 printf("MP3播放器\n");

24

25 /* 檢測WM8978芯片,此函數會自動配置CPUGPIO */

26 if (wm8978_Init()==0) {

27 printf("檢測不到WM8978芯片!!!\n");

28 while (1); /* 停機 */

29 }

30 printf("初始化WM8978成功\n");

31

32 while (1) {

33 mp3PlayerDemo("0:/mp3/張國榮-玻璃之情.mp3");

34 mp3PlayerDemo("0:/mp3/張國榮-全賴有你.mp3");

35 }

36 }

main函數主要完成各個外設的初始化,包括初始化禁用WIFI模塊、調試串口初始化、SD卡文件系統掛載還有系統滴答定時器初始化。

wm8978_Init初始化I2C接口用於控制WM8978芯片,並復位WM8978芯片,如果初始化成功則進入無限循環,執行MP3播放器實現函數mp3PlayerDemo,它有一個形參,用於指定播放文件。

另外,為使程序正常運行還需要適當增加控制器的棧空間,見代碼清單 3829Helix解碼過程需要用到較多局部變量,需要調整棧空間,防止棧空間溢出。

代碼清單 3829 棧空間大小調整

1 Stack_Size EQU 0x00001000

2

3 AREA STACK, NOINIT, READWRITE, ALIGN=3

4 Stack_Mem SPACE Stack_Size

38.7.5 下載驗證

將工程文件夾中的"音頻文件放在SD卡根目錄下"文件夾的內容拷貝到Micro SD卡根目錄中,把Micro SD卡插入到開發板右側的卡槽內,使用USB線連接開發板上的"USB TO UART"接口到電腦,電腦端配置好串口調試助手參數,在開發板的上邊沿的耳機插座(左邊那個)插入耳機。編譯實驗程序並下載到開發板上,程序運行后在串口調試助手可接收到開發板發過來的提示信息,如果沒有提示錯誤信息則直接在耳機可聽到音樂,播放完后自動切換下一首,如此循環。

實驗主要展示MP3解碼庫移植過程和實現簡單MP3文件播放,跟實際意義上的MP3播放器在功能上還有待完善,比如快進快退功能、聲音調節、音效調節等等。

38.8 每課一問

4.    SPI和I2S共用很多資源,比較兩值異同點。

5.    播放WAV和MP3格式文件時,如何獲取歌曲總時間和正在播放進度時間。

6.    簡述播放WAV和MP3格式文件時,實現快進快退功能方法和有哪些要點需要注意。

7.    在MP3播放器實驗例程基礎上,實現每過1秒通過串口打印一次當前播放時間和歌曲總時間,同時實現按鍵控制音量大小調節。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM