※已刊登在“無線電”1月刊上 基於STM32和W5500實現AirPlay音頻播放
作者:常席正,魏文龍
AirPlay是蘋果公司推出的一套無線音視頻解決方案,我們手裡的iPhone、iPad甚至是Apple Watch等設備還有電腦上的iTunes都支持AirPlay。使用AirPlay可以方便的使移動設備的音頻流,視頻流可以投射到音箱和顯示設備上,而無需藍牙設備的配對過程。但是支持AirPlay功能的音響設備普遍都比較昂貴,而且家裡的3.5毫米的插口的老音箱也沒有利用起來,本着“喜新不厭舊,改造舊物發揮餘熱”的精神,我開始了新一輪的折騰。
我的想法是用嵌入式方案STM32+W5500的方式實現AirPlay協議,並使用I2S接口接PCM5102A音頻模塊來實現音頻播放。於是馬上上網查資料,發現成熟的方案還不太多,現有的方案都是在linux或者windows上運行的,精挑細選之後選擇了https://github.com/juhovh/shAirPlay這個AirPlay開源項目作為參考,主要是該代碼是用C語言實現移植到stm32比較方便。
在開始之前我們有必要先了解一下AirPlay, AirPlay是蘋果公司收購airtunes後,在airtunes協議的基礎上增加了視頻,照片的傳輸,從而變為完整的AirPlay協議。AirPlay可以將iPhone 、iPad、iPod touch 等iOS 設備上的包括圖片、音頻、視頻及鏡像傳輸到支持AirPlay協議的設備中播放,實現隨時隨地的無線流媒體傳輸。在我們的這個項目中,我們只需要實現AirPlay協議中的音頻流部分。AirPlay的實現過程中包含多個子協議,其中有的協議是完全標準的,有一部分協議蘋果公司進行了一些修改,有的則是完全私有的。
- Multicast DNS:用於發布服務,啟動後,在iOS的控制中心菜單中就能看到支持AirPlay的設備列表;
- HTTP / RTSP / RTP:用於流媒體服務,傳輸音視頻數據,進行播放控制等;
- NTP:網絡時間協議,用於時間同步;
- FAirPlay DRM加密協議:用於進行數據加密,這個是完全私有的加密協議。
開始工作前我們需要進行一些前期準備,如下圖:
圖1 硬件框圖及接線
iPhone用來播放音樂,並通過Airplay協議發送音頻流。W5500EVB是WIZnet的W5500開發板,其中的W5500除了包含以太網的MAC和PHY外,還內置了硬件的TCP/IP協議棧,是目前比較常用的以太網方案。我們使用W5500EVB作為服務器接收並解碼音頻數據,開發板的操作可以參考http://www.w5500.com中的例程。PCM5102A音頻模塊可以將解碼後的音頻數據進行播放。經過分析後我們要實現AirPlay音頻播放主要是實現以下三個方面:
- iPhone在網絡中發現Airplay設備(W5500EVB)並建立連接;
- W5500EVB接收並解碼音頻數據;
- W5500EVB通過I2S接口將音頻傳送到PCM5102A音頻模塊;
接下來我們將分別實現這三個步驟:
1、發現Airplay設備並建立連接
AirPlay發現設備是基於mDNS協議(Multicast DNS)實現,iPhone與W5500EVB需要連入同一網絡且W5500EVB要加入組播組224.0.0.251:5353才可以接收mDNS報文。W5500EVB收到iPhone發出的Querry查詢報文後回復Response報文,報文的內容可以參考文檔《Unofficial AirPlay Protocol Specification》(http://nto.github.io/AirPlay.html),下方為mDNS設備發現和設備註冊代碼:
1 uint8 mdns_query(uint8 s, uint8 * name,uint8* rname)
2 {
3 uint8 ip[4];
4 uint16 len, port;
5 switch (getSn_SR(s)) {
6 case SOCK_CLOSED:/*打開SOCKET並加入組播組224.0.0.251*/
7 setDIPR(s,DIP);/* 設置目標IP 224.0.0.251*/
8 setDHAR(s,DHAR);/*設置目標MAC 01:00:5e:00:00:FB */
9 setDPORT(s,DPORT);/*設置目標端口5353*/
10 socket(s, Sn_MR_UDP, 5353,Sn_MR_MULTI);/*打開SOCKET並加入組播組*/
11 break;
12 case SOCK_UDP:
13 if ((len = getSn_RX_RSR(s)) > 0) {
14 if (len > MAX_DNS_BUF_SIZE) {
15 len = MAX_DNS_BUF_SIZE;
16 }
17 len = recvfrom(s, BUFPUB, len, ip, &port);
18 /*檢查收到報文的flag確定報文是否為查詢報文*/
19 if ((BUFPUB[2]&0x80)==0) {
20 len = mdns_makeresponse(0,name,rname,BUFPUB,MAX_DNS_BUF_SIZE);
21 sendto(s, BUFPUB, len, DIP,DPORT);
22 }
23 }
24 break;
25 }
26 return DNS_RET_PROGRESS;
27 }
mdns_query()函數中狀態機的case SOCK_CLOSED部分用來初始化網絡並加入mDNS組播。當我們點擊iOS播放頁面的AirPlay圖標的時候,會自動發送服務請求。case SOCK_UDP部分第19行函數用來判斷是否iPhone發過來的mDNS服務請求。代碼20行的mdns_makeresponse()函數用來拼接響應報文,該響應報文中包含設備信息以及將要提供的RAOP服務類型,RAOP服務是AirPlay的音頻流協議,RAOP從本質上來說是實時流協議,只不過增加了身份驗證請求-應答的步驟,RAOP服務用兩個信道實現流媒體:一個是用實時流協議的控制信道;另一個是數據信道用來發送數據。通過21行的sendto ()函數可以將響應報文發送到組播組,iPhone就可以接收到這個響應數據包。通過抓包工具抓取響應報文我們可以看到RAOP服務的相關信息,如下圖:
圖2 RAOP服務報文
其中:
Service:此字段是服務名稱,格式:MAC地址@設備名._raop._tcp.local;
Protocol:服務的類別:_AirPlay是視頻服務(未用到),_raop是音頻服務。
Name:數據傳輸的協議,可以通過TCP或者UDP傳輸。
Port:聲明了RTSP命令交互的端口號為5005,客戶端可以通過此端口號與服務端建立連接。
這部分如果沒有錯誤的話,應該能在iPhone的控制中心的音樂播放頁面看到我們用W5500EVB虛擬出來的AirPlay設備,就是下圖中“wiznet”。
圖3 iPhone發現設備
iPhone成功發現AirPlay設備後就可以連接設備,此時我們點擊列表中顯示的設備,連接成功後對應設備的後面會顯示對勾,如下圖所示:
圖4 iPhone連接設備
話雖簡短,但實現起來可一點也不容易。AirPlay的握手使用的是RTSP(Real-Time Stream Protocol)協議,RTSP是一個基於文本的多媒體播放控制協議。上文介紹的iPhone發現設備的過程中指定了RTSP是通過TCP進行通信且端口號為5005,所以我們要創建一個端口號為5005的TCP服務器來接收數據包,並通過下圖中的幾個步驟完成握手:
圖5 AirPlay握手通訊步驟
對RTSP數據包的解析是通過rtsp_parase_request()函數進行的如下方代碼20行所示。
1 void do_tcp_server(SOCKET s,uint16 localport)
2 {
3 uint16 len;
4 uint8 send_buffer[1024];
5 switch (getSn_SR(s)) {
6 case SOCK_INIT:
7 listen(s);
8 break;
9 case SOCK_ESTABLISHED:
10 if (getSn_IR(s) & Sn_IR_CON) {
11 setSn_IR(s, Sn_IR_CON);
12 }
13 len=getSn_RX_RSR(s);
14 if (len>0) {
15 memset(buffer,0,sizeof(buffer));
16 querry_flag=1;
17 recv(s,buffer,len);
18 memset(send_buffer,0,sizeof(send_buffer));
19 /*解析RTSP數據包並拼接響應數據*/
20 rtsp_parase_request((char*)buffer,(char*)send_buffer,s,len);
21 /*發送響應數據包*/
22 if (0==send(s,send_buffer,strlen(send_buffer))) {
23 send(s,send_buffer,strlen(send_buffer));
24 }
25 }
27 break;
28 case SOCK_CLOSE_WAIT:
29 disconnect(s);
30 querry_flag=0;
31 break;
32 case SOCK_CLOSED:
33 querry_flag=0;
34 socket(s,Sn_MR_TCP,localport,Sn_MR_ND);
35 break;
36 }
37 }
由於蘋果的AirPlay協議為了防止其他未經蘋果允許的設備的接入,對傳輸的數據用非對稱性RSA加密算法進行加密,非對稱性的意思就是加密和解密用的不是同一份密鑰,RSA加密算法的密鑰分為公鑰和私鑰,兩者內容不同,用途也不同。公鑰用於加密,一般交給客戶端使用;私鑰用於解密,一般由服務器管理。iPhone中存有公鑰用來對iPhone輸出的數據流進行加密,接收端設備利用私鑰對接收的數據(音頻)流進行解密。W5500EVB是作為服務器接收數據所以我們只需要知道私鑰就可以解析數據,我們可以直接百度網上已有大神破譯出的私鑰。RSA加密算法的實現可以參考開源項目https://github.com/juhovh/shAirPlay工程中的RSA加密解密相關函數。
iPhone會先發送OPTIONS請求來確定AirPlay設備(W5500EVB)支持的方法,W5500EVB回復支持的全部方法包含ANNOUNCE,SETUP,RECORD,PAUSE,FLUSH,TEARDOWN,OPTIONS,GET_PARAMETER,SET_PARAMETER等,方法具體含義可參考RTSP協議相關文檔。下面分別是由iPhone發出的OPTIONS請求報文和AirPlay設備回復的OPTIONS響應報文
iPhone OPTIONS 請求報文:
OPTIONS * RTSP/1.0
CSeq: 0
DACP-ID: 4CB06073C86450D8
Active-Remote: 2937221397
User-Agent: AirPlay/373.9.1
AirPlay設備OPTIONS響應報文:
RTSP/1.0 200 OK
CSeq: 0
Apple-Jack-Status: connected; type=analog
Public:ANNOUNCE,SETUP,RECORD,PAUSE,FLUSH,TEARDOWN,OPTIONS,GET_PARAMETER,SET_PARAMETER
iPhone收到W5500EVB的響應報文後,會向W5500EVB發送包含Apple-Challenge的OPTIONS數據包,Apple-Challenge後的參數是隨機生成一個字符串且經過了RSA算法加密,W5500EVB要將Apple-Challenge中的參數先進行base64解碼,解碼後的數據尾部添加W5500EVB的IP地址和MAC地址然後通過RSA私鑰加密後用base64編碼,W5500EVB將加密處理後的數據作為Apple-Response的參數發送給iPhone,iPhone對該數據進行驗證,數據正確則進行下一步,數據不正確則斷開連接。下圖為包含Apple-Challenge的OPTIONS 數據包:
OPTIONS * RTSP/1.0
Apple-Challenge: UJPWMzMloBFr98cQQHX3OQ==
CSeq: 2
DACP-ID: 4CB06073C86450D8
Active-Remote: 2937221397
User-Agent: AirPlay/373.9.1
接收到OPTIONS數據包後,截取Apple-Challenge相關數據,並進行解密代碼如下:
1 if(strstr(rcv_buffer,”Apple-Challenge:”)!=NULL)
2 {
3 rsakey_t *rsakey;
4 rsakey = rsakey_init_pem(pemstr);
5 if (!rsakey) {
6 printf(“Initializing RSA failed\n”);
7 return;
8 }
9 memset(response,0x00,1024);
10 /*獲取Apple-Challenge參數*/
11 mid(rcv_buffer,”Apple-Challenge: “,”\r\n”,CHALLENGE);
12 /*獲取加密Apple-Response*/
13 rsakey_sign(rsakey, response, sizeof(response), CHALLENGE,ipaddr, sizeof(ipaddr), hwaddr, sizeof(hwaddr));
14 mid(rcv_buffer,”CSeq: “,”\r\n”,CHALLENGE);
15 sprintf(send_buffer,”RTSP/1.0 200 OK\r\nCSeq: %s\r\nApple-Jack-Status:connected; type=analog\r\nApple-Response: %s\r\nPublic: ANNOUNCE, SETUP,RECORD,PAUSE, FLUSH, TEARDOWN, OPTIONS,SET_PARAMETER\r\n\r\n”,CHALLENGE,response);
16 }
通過11行處的mid()函數來獲取Apple-Challenge後的參數然後14行處的rsakey_sign()函數對獲取數據進行加密解密,15行處完成對RTSP響應報文的拼接。拼接生成的報文如下圖所示:
RTSP/1.0 200 OK
CSeq: 2
Apple-Jack-Status: connected; type=analog
Apple-Apple-Jack-Response:Dw5Jrbs1mhjks3YErCo1tSOUV8/G8pOOShS3dUocjWzDGQR6DfqiSEovks+G4nHmCw9BccjlpVHzzRUINYZenWhUy8zlGsVGNwuO4okfi86PjGp5VAS6RPeYbW/CpAPgrzpDsVCblSGt8kQbn+sWuku9WMfa4gYU82DgfmL3laphZlidEIZd8D6FwzAth4pbRdtL3N8GuM2kWGRSpT6FL4VGk326a58g0kUNqNDxHp0fTa4ijk8VORzkyKO9ByFeysmZqGDBurLuSvDoAs0c1zR9aHAIXfJkWd0Ii3WviC2F0+vEODcRgOh7gOvy/i5+OOTiUfvHiDFIqlhVCRnZ2g
Public:ANNOUNCE,SETUP,RECORD,PAUSE,FLUSH,TEARDOWN,OPTIONS,SET_PARAMETER
iPhone收到AirPlay設備的response後,如果驗證Apple-Response數據正確,成功完整設備間的握手,下一步就是傳輸音頻數據給AirPlay設備。
2、音頻數據接收與解碼
iPhone與AirPlay設備連接成功後,就開始通過UDP協議發送音頻數據但是iPhone通過AirPlay傳輸的音數據都是加密過的,對於接收端來說,需要正確解密後才能對音視頻數據進行處理。音頻數據採用AES CBC128算法進行加密,該算法解密時需輸入參數rsaaeskey、aeskiv,這兩個參數通過解析iPhone基於RTSP協議所發送的ANNOUNCE請求報文來獲取, ANNOUNCE在傳輸的時候遵循了SDP協議。SDP協議用來描述媒體信息,下圖是ANNOUNCE請求報文
ANNOUNCE rtsp://192.168.1.150/1561243076001349804 RTSP/1.0
Content-Length: 652
Content-Type: application/sdp
CSeq: 3
DACP-ID: 4CB06073C86450D8
Active-Remote: 2937221397
User-Agent: AirPlay/373.9.1
v=0
o=AirTunes 1561243076001349804 0 IN IP4 192.168.1.100
s=AirTunes
i=Wenlong… iPhone
c=IN IP4 192.168.1.100
t=0 0
m=audio 0 RTP/AVP 96
a=rtpmap:96 AppleLossless
a=fmtp:96 352 0 16 40 10 14 2 255 0 0 44100
a=rsaaeskey:bx0eKFGbphzETu16PLtXyP8s2CDKHpjIclJCmChdw6b12YSEvzDR3jlQwTWQdRRRrr99cek6JzdE0pgv0TzAF++FK8g63la8H9ioEcLFq84zWT/7atIlPNFC7RELlQG5ff/yTXHJ7LkzxQF12DvzQzIPd8GMx5ik/rxnLObZ+GQAbB2xtW/By2JT5gapEMBsx8+t+0sZXNwA3GXrjcjF+h6+oAD37A3U04rR/iK+Pvzglvy/13ZOrXL1VJpTkE1O+TIflAzfl0BkBbtfd3lX/+Te+Og8+gXXe516Dg4/v1Veddj4HQYZ/vrxE/qYFGDZIFZUdmpBtmtVMqAYwt1n5w==
a=aesiv:UohAefAQLdnT4BIBimuhfg==
a=min-latency:11025
a=max-latency:88200
W5500EVB解析收到ANNOUNCE請求包獲取rsaaeskey,aesiv並解碼。
1 void raop_announce(char *recv_buffer)
2 {
3 mid(recv_buffer,”Active-Remote: “,”\r\n”,remotestr);
4 mid(recv_buffer,”rtpmap:”,”\r\n”,rtpmapstr );
5 mid(recv_buffer,”fmtp:”,”\r\n”,fmtpstr);
6 mid(recv_buffer,”rsaaeskey:”,”\r\n”,rsaaeskeystr);
7 mid(recv_buffer,”aesiv:”,”\r\n”,aesivstr);
8 /*解碼aeskey*/
9 rsakey_decrypt(rsakey, aeskey, sizeof(aeskey), rsaaeskeystr);
10 /*解碼aesiv*/
11 rsakey_decode(rsakey, aesiv, sizeof(aesiv), aesivstr);
12 /*init alac*/
13 raop_buffer_init(&alac,fmtpstr);
14 return;
15 }
iPhone會繼續向W5500EVB發送SETUP數據包,數據包中包含timing_port 與control_port。timing_port 用來傳輸 AirPlay 的時間同步包,同時也可以主動向iPhone請求當前的時間戳來校準流的時間戳。control_port是用來發送resendTransmit Request的端口,也就是當接收端發現收到的音樂流數據包中有丟失幀的時候,可以通過 control port 發送 resendTransmit 的 request 給iPhone,iPhone收到後會將幀在 response 中補發回來。下面的分別是iPhone發送的SETUP報文及W5500EVB回復的SETUP響應報文。
SETUP rtsp://192.168.1.150/1561243076001349804 RTSP/1.0
Transport: RTP/AVP/UDP;unicast;mode=record;timing_port=55703;control_port=56616
CSeq: 4
DACP-ID: 4CB06073C86450D8
Active-Remote: 2937221397
User-Agent: AirPlay/373.9.1
W5500EVB回復的響應報文中的server_port, server port用來傳輸音頻流數據包。
RTSP/1.0 200 OK
CSeq: 4
Apple-Jack-Status: connected; type=analog
Transport: RTP/AVP/UDP;unicast;mode=record;timing_port=56461;events;control_port=51196;server_port=55641
Session:DEADBEEF
SETUP數據包確定音頻流傳輸方式與傳輸端口號後,iPhone就開始發送音頻數據到W5500EVB指定的server_port 55641端口,W5500EVB接收音頻數據,通過解密過程後,我們會得到AAC編碼的音頻數據,播放器播放AAC數據還需要對其進行解碼,話不多說,直接通過部分代碼來說明音頻解密過程。
1 int decode_audio_data(unsigned char *data, unsigned short datalen, int use_seqnum)
2{
3 unsigned short seqnum;
4 raop_buffer_entry_t entry;
5 int encryptedlen;
6 AES_CTX aes_ctx;
7 int outputlen;
8 /* Check packet data length is valid */
9 if (datalen < 12 || datalen > 1472) {
10 return -1;
11 }
12 /* Get correct seqnum for the packet */
13 if (use_seqnum) {
14 seqnum = (data[2] << 8) | data[3];
15 }
16 /* Update the raop_buffer entry header */
17 entry.flags = data[0];
18 entry.type = data[1];
19 entry.seqnum = seqnum;
20 entry.timestamp = (data[4] << 24) | (data[5] << 16) | (data[6] << 8) | data[7];
21 entry.ssrc = (data[8] << 24) | (data[9] << 16) | (data[10] << 8) | data[11];
22 entry.available = 1;
23 /* Decrypt audio data */
24 encryptedlen = (datalen-12)/16*16;
25 AES_set_key(&aes_ctx, aeskey, aesiv, AES_MODE_128);
26 AES_convert_key(&aes_ctx);
27 memset(packetbuf,0,sizeof(data));
28 AES_cbc_decrypt(&aes_ctx, &data[12], (uint8*)packetbuf,encryptedlen);
29 memcpy(packetbuf+encryptedlen, &data[12+encryptedlen],datalen-12-encryptedlen);
30 /* Decode ALAC audio data */
31 outputlen = audio_buffer_size;
32 alac_decode_frame(&alac, (uint8*)packetbuf ,audiobuf,&outputlen);
33 entry.audio_buffer_len = outputlen;
34 return outputlen;
35 }
在程序中W5500EVB通過UDP端口每收到數據包先會判斷數據包的長度是否小於12因為RTP的包頭為12個字節,小於12字節就會直接丟棄掉,大於12字節且小於1472(UDP包的最大長度)就會通過31行AES_cbc_decrypt()函數的對數據解密然後把解密後的數據通過alac_decode_frame()函數轉換為PCM5102A模塊可播放的數據並將數據存儲在audiobuf中等待發送給音頻模塊,返回可播放數據長度outputlen,該值在我們初始化I2S的DMA功能時會用到。
3、音頻數據的播放
音頻播放採用的是PCM5102A的DAC模塊,該模塊是通過I2S接口進行通信,由於STM32的I2S2的針腳與SPI2復用,而SPI2已經用來與W5500進行通信,所以我們只能選擇I2S3接口, W5500EVB與PCM5102A模塊連接示意圖如下所示:
圖6 模塊硬件連接圖
程序上直接將解碼後的數據發送到PCM5102A模塊即可。為了能與PCM512A模塊正常通信要初始化W5500EVB的I2S3接口,需要注意的是I2S3接口的時鐘腳PB3,該引腳默認為JTAG的JTDO腳,初始化時需要禁止JTAG以使PB3能夠作為I2S3的時鐘腳,初始化代碼如下所示:
1 void I2S_Config(void)
2 {
3 I2S_InitTypeDef I2S_InitStructure;
4 GPIO_InitTypeDef GPIO_InitStruct;
5 /*Init GPIO*/
6 RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI3, ENABLE);
7 /*SPI*/
8 RCC_APB2PeriphClockCmd(
RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB|RCC_APB2Periph_
GPIOC|RCC_APB2Periph_AFIO, ENABLE);
9 GPIO_PinRemapConfig(GPIO_Remap_SWJ_Disable,ENABLE);
10 /*GPIO_Pin7 –> I2S_MCK*/
11 GPIO_InitStruct.GPIO_Pin = GPIO_Pin_7;
12 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
13 GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
14 GPIO_Init(GPIOC, &GPIO_InitStruct);
15 /*GPIO_Pin_15 –>I2S3_WS*/
16 GPIO_InitStruct.GPIO_Pin = GPIO_Pin_15;
17 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
18 GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
19 GPIO_Init(GPIOA, &GPIO_InitStruct);
20 /*GPIO_Pin_3 –>I2S3_CK
21 GPIO_Pin_5 –>I2S3_SD*/
22 GPIO_InitStruct.GPIO_Pin = GPIO_Pin_3|GPIO_Pin_5;
23 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
24 GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
25 GPIO_Init(GPIOB, &GPIO_InitStruct);
26 /*Init IIS*/
27 SPI_I2S_DeInit(SPI3);
28 I2S_InitStructure.I2S_Mode = I2S_Mode_MasterTx;
29 I2S_InitStructure.I2S_Standard = I2S_Standard_Phillips;
30 I2S_InitStructure.I2S_DataFormat = I2S_DataFormat_16b;
31 I2S_InitStructure.I2S_MCLKOutput=I2S_MCLKOutput_Disable;
32 I2S_InitStructure.I2S_AudioFreq = I2S_AudioFreq_44k;
33 /*I2S clock steady state is low level */
34 I2S_InitStructure.I2S_CPOL = I2S_CPOL_Low;
35 I2S_Init(SPI3, &I2S_InitStructure);
36 I2S_Cmd(SPI3, ENABLE);
37 }
代碼12行處通過調用GPIO_PinRemapConfig()函數禁用JTAG, 32行處模式配置為主設備發送I2S_Mode_MasterTx,通信標準設置為I2S_Standard_Phillips,數據格式為標準16位格式I2S_DataFormat_16b,採樣頻率設置為44kHz I2S_AudioFreq_44k, I2S時鐘線空閑狀態的為低電平。
為了提高數據的傳輸速度與效率,要打開IIS的DMA發送功能,每次發送SPI_I2S_DMAReq_Tx 請求後會將指定的buf0內的數據發送到SPI3的DR數據寄存器。該函數是buf0即為存儲音頻數據的audiobuf,需要特別注意的是:我們的數據是按照16bit傳送的,而audiobuf內的數據為uint8型,所以 num值為audiobuf內的有效數據長度/2。
1 void I2S2_TX_DMA_Init(u8* buf0,u16 num)
2 {
3 NVIC_InitTypeDef NVIC_InitStructure;
4 DMA_InitTypeDef DMA_InitStructure;
5 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA2, ENABLE);
6 DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)(&SPI3->DR);
7 DMA_InitStructure.DMA_MemoryBaseAddr = (u32)buf0;
8 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
9 DMA_InitStructure.DMA_BufferSize = num;
10 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
11 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
12 DMA_InitStructure.DMA_PeripheralDataSize=DMA_PeripheralDataSize_HalfWord;
13 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
14 DMA_InitStructure.DMA_Mode = DMA_Mode_Circular ;
15 DMA_InitStructure.DMA_Priority = DMA_Priority_High;
16 DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
17 DMA_Init(DMA2_Channel2, &DMA_InitStructure);
18 DMA_Cmd(DMA2_Channel2, ENABLE);
19 SPI_I2S_DMACmd(SPI3,SPI_I2S_DMAReq_Tx,ENABLE);
20 }
音頻流的處理過程為通過UDP接收音頻數據包,然後對收到的數據包進行解碼,並將解碼後的數據存儲到audiobuf,通過I2S3的DMA功能將數據發送到PCM5102A模塊,代碼如下所示:
1 void do_raop(uint8 s)
2 {
3 int outputlen;
4 uint8 ip[4];
5 uint16 len, port;
6 switch (getSn_SR(s)) {
7 case SOCK_UDP:
8 if ((len = getSn_RX_RSR(s)) > 0) {
9 /*接收音頻數據*/
10 recvfrom(s,buffer,len,ip,&port);
11 /*解碼收到的音頻數據*/
12 outputlen=decode_audio_data(buffer, len ,1);
13 /*配置DMA*/
14 I2S2_TX_DMA_Init((uint8*)audiobuf,outputlen/2);
15 }
16 break;
17 case SOCK_CLOSED:
18 socket(s, Sn_MR_UDP,55641,0);
19 break;
20 }
21 }
將編譯好的程序下載到W5500EVB,將耳機插入PCM5102A模塊,然後用iPhone手機搜索並連接W5500EVB設備,點擊播放音樂就可以用耳機或者音響聽音樂了,玄學部分直接略過,老音箱終於有枯木逢春的感覺了。由於時間匆忙,本文的項目中雖然實現了通過AirPlay播放音樂,但還有很大的優化空間,例如對各個音樂播放器的兼容性問題,QQ音樂、網易音樂等實現都不太一樣(本文中用的是網易雲音樂);音樂播放過程中的音量設置問題;音樂播放過程中的噪音問題等等。而且由於手上只有帶STM32F103的W5500EVB開發板,STM32F103芯片在運行加密解密時會比較慢,RAM空間也比較小,偶爾出現的卡頓就是由於這部分原因造成的。而相對來說,可能使用WiFi會更適合本方案,我也會繼續優化此項目以實現更穩定的播放效果。