※已刊登在“無線電”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…