1前言
本文將基于STM32F4 Discovery板,從零開始設計并實現(xiàn)一個USB Audio的例子。
2設計構思所謂的USB AUDIO就是制作一個盒子,這個盒子可以通過USB連接到PC,PC端將其識別為Audio設備,然后在PC端播放音樂的時候,聲音可以通過盒子播放出來。
2.1從原理框圖開始圖1
如上圖所示,我們大概構思一下,為了實現(xiàn)USB%20AUDIO功能,我們使用一個MCU的USB外設連接PC端,整個流程是這樣:%20PC端播放音樂時,代表音樂的數(shù)據(jù)流從PC端通過USB傳輸?shù)組CU端,MCU端然后將其轉發(fā)給一個外部Codec,最后通過Codec上連接的揚聲器或耳機播放音樂。
2.2硬件支撐這里選擇ST官方的STM32F4-DISCOVERY板來實現(xiàn),之所以選擇這塊板子,就是因為其上有USB接口和Codec,正好符合我們設計的要求。
2.2.1%20USB接口如下圖為USB接口部分的電路:
圖2
這個一個將USB作為OTG的電路設計,在本設計中,我們只是將USB作為device來使用,因此,上圖我們關注下面部分就可以了。在本設計中,我們使用到全速USB,從上圖可以看出D+與D-引腳分別為PA12,PA11。
如下圖所示:
圖3
如上圖所示,這里的Codec為具體型號為CS43L22,MCU通過I2C接口(PB9,PB6)連接Codec,作為其控制接口,使用I2S(PC7,PC10,PC12,PA4)作為數(shù)據(jù)通道,此外,MCU使用PD4這個IO管腳控制Codec的reset。CS43L22的14,15腳連接到外面的耳機插孔,也就是說,我們可以通過插入耳機線的方式來收聽PC端播放的聲音。
2.3 軟件設計為了簡化開發(fā)流程,這里使用CubeMx自動生成代碼工具來生成初始化代碼,首先基于Cube庫架構以及USB協(xié)議棧的特點,我們得先設計一個合理的軟件框架。
圖4
如上圖,藍色表示的模塊為標準模塊,不需要我們?nèi)バ薷乃?,將由CubeMx自動生成,而綠色部分則可能涉及到需要修改,其中BSP部分是需要自己添加的代碼,其他的都是由CubeMx生成。
各個模塊的工作流程如下設計:
初始化流程: 由main開始,它首先對將使用到的外設I2C,I2S初始化,這最終將調(diào)到HAL MSP底層部分實現(xiàn)對具體IO管腳和外設的初始化。同時main使用usb description的數(shù)據(jù)通過調(diào)用USB棧初始化接口來完成對USB接口的初始化,這一步還涉及到USB的枚舉過程。
USB數(shù)據(jù)傳輸過程:PC端軟件在播放音樂后,通過USB通道向MCU傳輸音頻數(shù)據(jù),音頻數(shù)據(jù)到達MCU時,首先觸發(fā)USB中斷,然后進入到HAL driver層,在回調(diào)到 usb conf模塊,接著進入到usb core,usb core再轉給usb audio class,最后音頻數(shù)據(jù)到達usb audio interface模塊,到達這里,就只剩下對音頻數(shù)據(jù)進行處理了。Usb audio interface模塊是一個數(shù)據(jù)接收到數(shù)據(jù)處理的一個中間對接模塊。
USB音頻數(shù)據(jù)處理過程: usb audio interface 模塊將從USB stack底層傳上來的音頻數(shù)據(jù)轉發(fā)給Codec組件,最終通過Codec組件連接的耳機播放出來。
從以上的音頻數(shù)據(jù)流程來看,最主要的就是usbaudio interface模塊,它實現(xiàn)了從USB audio stack到codec驅動的對接。
接下來,我們來看看軟件層面上的實現(xiàn)。
還是老辦法,采用CubeMx這個工具來生成初始化代碼,這樣可以節(jié)省我們花費在基本外設上的調(diào)試初始參數(shù)時間。
3.1 創(chuàng)建CubeMx工程由于我們使用到的硬件平臺是STM32F4Discovery板,上面搭載的MCU型號是STM32F407VGT6,我們就以此型號創(chuàng)建一個名為Audio_Test的工程。
pinout:
外設有用到USB_OTG_FS(PA11,PA12device模式),I2C1(PB6,PB9),I2S3(PC12,PA4,PC10,PC7,半雙工主模式),此外Codec的reset使用PD4管腳控制,使用外部8M HSE。其pinout如下圖所示:
圖5
Clock configuration:
圖6
時鐘樹如上設置,主頻使用168M,I2S時鐘輸出初始化為96M。
Configuration:
HAL層:
Usb_FS:使用默認參數(shù)。
I2C:100K速率,7位地址寬度,使用默認參數(shù)。
I2S:主發(fā)模式,標準16位寬,默認音頻為48K,如下圖:
圖7
并為I2S發(fā)送添加DMA,半字位寬:
圖8
MiddleWares:
USB選擇Audiodevice class,其配置參數(shù)如下:
圖9
這里都是默認參數(shù)。
圖10
在描述符參數(shù)內(nèi)得為usb audio class修改兩個參數(shù):
PID得修改為0x5730(否則windows驅動會加載出錯)
序列號:序列號字符串內(nèi)不能包含字母,只能是數(shù)據(jù)(否則windowsaudio驅動在枚舉后也不會將音頻數(shù)據(jù)傳輸下來)。
最后修改工程設置,將堆大小設為4K,棧大小設為1K,如下圖:
圖11
如此就可以生成工程了,我們生成IAR工程。
3.2 生成的IAR工程介紹圖12
如上圖所示,生成的IAR工程,主要有User,Drivers,Middleware3個目錄。
User目錄下為用戶源碼文件,用戶的主要修改也將集中在此目錄下,在這里,我們的主要工作是集中在usbd_audio_if.c文件,它對應著之前軟件框圖中的usbaudio interface模塊,主要是實現(xiàn)USB audio協(xié)議棧與Codec的對接。其他源文件都保持不變就可以了。
Middlewares目錄對應著usb audio stack模塊,它由CubeMx自動生成,保持原樣就可以,不需要任何修改。
Drivers目錄對應著HAL層,它包含CMSIS,HAL驅動。
首先我們不做任何修改,先編譯一下工程,發(fā)現(xiàn)能順利編譯通過,并燒錄進STM32F4DISCOVERY板,運行后通過USB連接上電腦,發(fā)現(xiàn)在設備管理器中能正常識別到這個USB AUDIO設備,如下圖所示:
圖13
這說明,USB與PC端的連接是OK的,但不知道具體有沒有數(shù)據(jù)。我們使用USB分析儀TOTAL PHASE USB480這個設備對USB總線進行數(shù)據(jù)監(jiān)控,能夠正常采集USB枚舉過程和播放音樂的通信數(shù)據(jù),如下圖所示:
圖14
這表明,到目前為止,從PC端到USB端都是能正常工作的,從PC端發(fā)送過來的音頻數(shù)據(jù)已經(jīng)到達usb audio interface模塊,目前只不過還沒有對這些數(shù)據(jù)進行處理,顯然,接下來的工作,我們就需要將這些音頻數(shù)據(jù)通過codec驅動發(fā)送出去,最終到達外部組件CS32L22.
3.3.2 添加codec驅動和audio bsp模塊我們已經(jīng)知道,我們需要為audio添加BSP模塊,在這里,我們將BSP歸屬于drivers類,因此,在drivers目錄下添加BSP目錄,通過之前的軟件架構圖我們可以知道,BSP包含Codec驅動(CS43L22)和Audio bsp模塊,因此,我們在BSP目錄下有添加了Codec的驅動源碼cs43l22.c與bsp_audio.c,如下圖所示:
圖15
其中cs43l22.c為codec cs32l22的驅動,我們可以從ST的組件驅動中找到它,并copy過來直接使用,不需要修改任何代碼。而bsp_audio.c是我們自己寫的,它的任務是為usbd_audio_if.c與cs43l22.c提供服務,讓這兩個模塊勝利對接。
3.3.2.1 Codec與HAL的對接首先我們來看Codec驅動文件cs43l22.c源文件,這個文件需要使用這個外部需要提供的接口:
AUDIO_IO_Init()
AUDIO_IO_DeInit()
AUDIO_IO_Write()
AUDIO_IO_Read()
這個都是Codec的基本控制接口,是通過I2C來控制的。都是需要用戶在驅動外部來提供這些接口給到驅動,于是,我們在bsp_audio.c文件中來提供這個接口的實現(xiàn):
//---------------------forc43l22port--------------------------//
staticvoidI2Cx_Error(uint8_tAddr)
{
/*De-initializetheIOEcomunicationBUS*/
HAL_I2C_DeInit(&hi2c1);
/*Re-InitiaizetheIOEcomunicationBUS*/
//I2Cx_Init();
//MX_I2C1_Init();
}
staticvoidCODEC_Reset(void)
{
HAL_GPIO_WritePin(AUDIO_RESET_GPIO_Port,AUDIO_RESET_Pin,GPIO_PIN_RESET);
HAL_Delay(5);
HAL_GPIO_WritePin(AUDIO_RESET_GPIO_Port,AUDIO_RESET_Pin,GPIO_PIN_SET);
HAL_Delay(5);
}
voidAUDIO_IO_Init(void)
{
//I2Cx_Init();
}
voidAUDIO_IO_DeInit(void)
{
}
/**
*@briefWritesasingledata.
*@paramAddr:I2Caddress
*@paramReg:Regaddress
*@paramValue:Datatobewritten
*/
staticvoidI2Cx_Write(uint8_tAddr,uint8_tReg,uint8_tValue)
{
HAL_StatusTypeDefstatus=HAL_OK;
status=HAL_I2C_Mem_Write(&hi2c1,Addr,(uint16_t)Reg,I2C_MEMADD_SIZE_8BIT,&Value,1,I2C_TIMEOUT);
/*Checkthecommunicationstatus*/
if(status!=HAL_OK)
{
/*I2Cerroroccured*/
I2Cx_Error(Addr);
}
}
voidAUDIO_IO_Write(uint8_tAddr,uint8_tReg,uint8_tValue)
{
I2Cx_Write(Addr,Reg,Value);
}
/**
*@briefReadsasingledata.
*@paramAddr:I2Caddress
*@paramReg:Regaddress
* @retval Data to be read