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音频播放主要是实现以下三个方面:
接下来我们将分别实现这三个步骤:
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服务器来接收数据包,并通过下图中的几个步骤完成握手:Apple-Challen
图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相关数据,并进行解密代码如下:
1if(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-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设备。
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
2 datalen, int use_seqnum)
3 {
4 unsigned short seqnum;
5 raop_buffer_entry_t entry;
6 int encryptedlen;
7 AES_CTX aes_ctx;
8 int outputlen;
9 /* Check packet data length is valid */
10 if (datalen < 12 || datalen > 1472) {
11 return -1;
12 }
13 /* Get correct seqnum for the packet */
14 if (use_seqnum) {
15 seqnum = (data[2] << 8) | data[3];
16 }
17 /* Update the raop_buffer entry header */
18 entry.flags = data[0];
19 entry.type = data[1];
20 entry.seqnum = seqnum;
21 entry.timestamp = (data[4] << 24) | (data[5] << 16) |
22 (data[6] << 8) | data[7];
23 entry.ssrc = (data[8] << 24) | (data[9] << 16) |
24 (data[10] << 8) | data[11];
25 entry.available = 1;
26 /* Decrypt audio data */
27 encryptedlen = (datalen-12)/16*16;
28 AES_set_key(&aes_ctx, aeskey, aesiv, AES_MODE_128);
29 AES_convert_key(&aes_ctx);
30 memset(packetbuf,0,sizeof(data));
31 AES_cbc_decrypt(&aes_ctx, &data[12], (uint8*)packetbuf,
32 encryptedlen);
33 memcpy(packetbuf+encryptedlen, &data[12+encryptedlen],
34 datalen-12-encryptedlen);
35 /* Decode ALAC audio data */
36 outputlen = audio_buffer_size;
37 alac_decode_frame(&alac, (uint8*)packetbuf ,audiobuf,
38 &outputlen);
39 entry.audio_buffer_len = outputlen;
40 return outputlen;
41 }
在程序中W5500EVB通过UDP端口每收到数据包先会判断数据包的长度是否小于12因为RTP的包头为12个字节,小于12字节就会直接丢弃掉,大于12字节且小于1472(UDP包的最大长度)就会通过31行AES_cbc_decrypt()函数的对数据解密然后把解密后的数据通过alac_decode_frame()函数转换为PCM5102A模块可播放的数据并将数据存储在audiobuf中等待发送给音频模块,返回可播放数据长度outputlen,该值在我们初始化I2S的DMA功能时会用到。
音频播放采用的是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
6 /*Init GPIO*/
7 RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI3, ENABLE);
8 /*SPI*/
9 RCC_APB2PeriphClockCmd(
10 RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB|RCC_APB2Periph_
11 GPIOC|RCC_APB2Periph_AFIO, ENABLE);
12 GPIO_PinRemapConfig(GPIO_Remap_SWJ_Disable,ENABLE);
13 /*GPIO_Pin7 --> I2S_MCK*/
14 GPIO_InitStruct.GPIO_Pin = GPIO_Pin_7;
15 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
16 GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
17 GPIO_Init(GPIOC, &GPIO_InitStruct);
18 /*GPIO_Pin_15 -->I2S3_WS*/
19 GPIO_InitStruct.GPIO_Pin = GPIO_Pin_15;
20 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
21 GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
22 GPIO_Init(GPIOA, &GPIO_InitStruct);
23 /*GPIO_Pin_3 -->I2S3_CK
24 GPIO_Pin_5 -->I2S3_SD
25 */
26 GPIO_InitStruct.GPIO_Pin = GPIO_Pin_3|GPIO_Pin_5;
27 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
28 GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
29 GPIO_Init(GPIOB, &GPIO_InitStruct);
30 /*Init IIS*/
31 SPI_I2S_DeInit(SPI3);
32 I2S_InitStructure.I2S_Mode = I2S_Mode_MasterTx;
33 I2S_InitStructure.I2S_Standard = I2S_Standard_Phillips;
34 I2S_InitStructure.I2S_DataFormat = I2S_DataFormat_16b;
35 I2S_InitStructure.I2S_MCLKOutput=I2S_MCLKOutput_Disable;
36 I2S_InitStructure.I2S_AudioFreq = I2S_AudioFreq_44k;
37 /*I2S clock steady state is low level */
38 I2S_InitStructure.I2S_CPOL = I2S_CPOL_Low;
39 I2S_Init(SPI3, &I2S_InitStructure);
40 I2S_Cmd(SPI3, ENABLE);
41 }
代码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会更适合本方案,我也会继续优化此项目以实现更稳定的播放效果。