基于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加密协议:用于进行数据加密,这个是完全私有的加密协议。

开始工作前我们需要进行一些前期准备,如下图:

基于STM32和W5500实现AirPlay音频播放_第1张图片

                                                                             图1 硬件框图及接线

iPhone用来播放音乐,并通过Airplay协议发送音频流。W5500EVB是WIZnet的W5500开发板,其中的W5500除了包含以太网的MAC和PHY外,还内置了硬件的TCP/IP协议栈,是目前比较常用的以太网方案。我们使用W5500EVB作为服务器接收并解码音频数据,开发板的操作可以参考http://www.w5500.com中的例程。PCM5102A音频模块可以将解码后的音频数据进行播放。经过分析后我们要实现AirPlay音频播放主要是实现以下三个方面:

  1. iPhone在网络中发现Airplay设备(W5500EVB)并建立连接;
  2. W5500EVB接收并解码音频数据;
  3. 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服务的相关信息,如下图:

基于STM32和W5500实现AirPlay音频播放_第2张图片

                                                                                    图2 RAOP服务报文

其中:
Service:此字段是服务名称,格式:MAC地址@设备名._raop._tcp.local

Protocol:服务的类别:_AirPlay是视频服务(未用到),_raop是音频服务。

Name:数据传输的协议,可以通过TCP或者UDP传输。

Port:声明了RTSP命令交互的端口号为5005,客户端可以通过此端口号与服务端建立连接。

这部分如果没有错误的话,应该能在iPhone的控制中心的音乐播放页面看到我们用W5500EVB虚拟出来的AirPlay设备,就是下图中“wiznet”。

基于STM32和W5500实现AirPlay音频播放_第3张图片

                                                                                     图3  iPhone发现设备

iPhone成功发现AirPlay设备后就可以连接设备,此时我们点击列表中显示的设备,连接成功后对应设备的后面会显示对勾,如下图所示:

基于STM32和W5500实现AirPlay音频播放_第4张图片

                                                                                      图4  iPhone连接设备

话虽简短,但实现起来可一点也不容易。AirPlay的握手使用的是RTSP(Real-Time Stream Protocol)协议,RTSP是一个基于文本的多媒体播放控制协议。上文介绍的iPhone发现设备的过程中指定了RTSP是通过TCP进行通信且端口号为5005,所以我们要创建一个端口号为5005的TCP服务器来接收数据包,并通过下图中的几个步骤完成握手:Apple-Challen

基于STM32和W5500实现AirPlay音频播放_第5张图片

                                                                                     图5  AirPlay握手通讯步骤

对RTSP数据包的解析是通过rtsp_parase_request()函数进行的如下方代码20行所示。

1 void do_tcp_server(SOCKET suint16 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(buffer0sizeof(buffer));

16             querry_flag=1;

17             recv(sbufferlen);

18             memset(send_buffer0sizeof(send_buffer));

19             /*解析RTSP数据包并拼接响应数据*/

20             rtsp_parase_request((char*)buffer(char*)send_bufferslen);

21             /*发送响应数据包*/

22             if (0==send(ssend_bufferstrlen(send_buffer))) {

23                 send(ssend_bufferstrlen(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(sSn_MR_TCPlocalportSn_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(response0x001024);

10     /*获取Apple-Challenge参数*/

11     mid(rcv_buffer"Apple-Challenge: ""\r\n"CHALLENGE);

12     /*获取加密Apple-Response*/

13     rsakey_sign(rsakey response sizeof(response) CHALLENGEipaddr 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 SETUPRECORDPAUSE FLUSH TEARDOWN OPTIONSSET_PARAMETER\r\n\r\n"CHALLENGEresponse);

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设备。

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(&alacfmtpstr);

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(packetbuf0sizeof(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功能时会用到。

3、音频数据的播放

     音频播放采用的是PCM5102A的DAC模块,该模块是通过I2S接口进行通信,由于STM32的I2S2的针脚与SPI2复用,而SPI2已经用来与W5500进行通信,所以我们只能选择I2S3接口, W5500EVB与PCM5102A模块连接示意图如下所示:

基于STM32和W5500实现AirPlay音频播放_第6张图片

                                                                                       图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_DisableENABLE);

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* buf0u16 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(SPI3SPI_I2S_DMAReq_TxENABLE);

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(sbufferlenip&port);

11             /*解码收到的音频数据*/

12             outputlen=decode_audio_data(buffer len 1);

13             /*配置DMA*/

14             I2S2_TX_DMA_Init((uint8*)audiobufoutputlen/2);

15         }

16         break;

17     case SOCK_CLOSED:

18         socket(s Sn_MR_UDP556410);

19         break;

20     }

21 }

将编译好的程序下载到W5500EVB,将耳机插入PCM5102A模块,然后用iPhone手机搜索并连接W5500EVB设备,点击播放音乐就可以用耳机或者音响听音乐了,玄学部分直接略过,老音箱终于有枯木逢春的感觉了。由于时间匆忙,本文的项目中虽然实现了通过AirPlay播放音乐,但还有很大的优化空间,例如对各个音乐播放器的兼容性问题,QQ音乐、网易音乐等实现都不太一样(本文中用的是网易云音乐);音乐播放过程中的音量设置问题;音乐播放过程中的噪音问题等等。而且由于手上只有带STM32F103的W5500EVB开发板,STM32F103芯片在运行加密解密时会比较慢,RAM空间也比较小,偶尔出现的卡顿就是由于这部分原因造成的。而相对来说,可能使用WiFi会更适合本方案,我也会继续优化此项目以实现更稳定的播放效果。

 

你可能感兴趣的:(全硬件TCP/IP协议栈,产品应用,WIZnet新闻,WIZnet产品应用小例,TCP/IP芯片,W5500,STM32,AirPlay)