stm32PC指针中,怎么让PC0到PC4的IO口输出电平从0000变为0001,再变为0010,依次加1,直到所有电平变为1111

第38章 &&&&I2S&音频播放与录音输入
全套集视频教程和页教程请到秉火论坛下载:
野火视频教程优酷观看网址:
本章参考资料:《中文参考手册》、《规格书》、库帮助文档《》及《》。
若对通讯协议不了解,可先阅读《》文档的内容学习。
关于音频编译码器,请参考其规格书《》来了解。
38.1 I2S简介
是飞利浦半导体公司现为恩智浦半导体公司针对数字音频设备之间的音频数据传输而制定的一种总线标准。在飞利浦公司的标准中,既规定了硬件接口规范,也规定了数字音频数据的格式。
38.1.1 数字音频技术
现实生活中的声音是通过一定介质传播的连续的波,它可以由周期和振幅两个重要指标描述。正常人可以听到的声音频率范围为。现实存在的声音是模拟量,这对声音保存和长距离传输造成很大的困难,一般的做法是把模拟量转成对应的数字量保存,在需要还原声音的地方再把数字量的转成模拟量输出,参考图。
图 381 音频转换过程
模拟量转成数字量过程,一般可以分为三个过程,分别为采样、量化、编码,参考图。用一个比源声音频率高的采样信号去量化源声音,记录每个采样点的值,最后如果把所有采样点数值连接起来与源声音曲线是互相吻合的,只是它不是连续的。在图中两条蓝色虚线距离就是采样信号的周期,即对应一个采样频率,可以想象得到采样频率越高最后得到的结果就与源声音越吻合,但此时采样数据量越越大,一般使用采样频率即可得到高保真的声音。每条蓝色虚线长度决定着该时刻源声音的量化值,该量化值有另外一个概念与之挂钩,就是量化位数。量化位数表示每个采样点用多少位表示数据范围,常用有、或,位数越高最后还原得到的音质越好,数据量也会越大。
图 382 声音数字化过程
是一个低功耗、高质量的立体声多媒体数字信号编译码器,集成和,可以实现声音信号量化成数字量输出,也可以实现数字量音频数据转换为模拟量声音驱动扬声器。这样使用芯片解决了声音与数字量音频数据转换问题,并且通过配置芯片相关寄存器可以控制转换过程的参数,比如采样频率,量化位数,增益、滤波等等。
芯片是一个音频编译码器,但本身没有保存音频数据功能,它只能接收其它设备传输过来的音频数据进行转换输出到扬声器,或者把采样到的音频数据输出到其它具有存储功能的设备保存下来。该芯片与其他设备进行音频数据传输接口就是协议的音频接口。
38.1.2 I2S总线接口
总线接口有个主要信号,但只能实现数据半双工传输,后来为实现全双工传输有些设备增加了扩展数据引脚。系列控制器支持扩展的总线接口。
(1)&&&&SD(Serial Data):串行数据线,用于发送或接收两个时分复用的数据通道上的数据(仅半双工模式),如果是全双工模式,该信号仅用于发送数据。
(2)&&&&WS(Word Select):字段选择线,也称帧时钟(LRC)线,表明当前传输数据的声道,不同标准有不同的定义。WS线的频率等于采样频率(FS)。
(3)&&&&CK(Serial Clock):串行时钟线,也称位时钟(BCLK),数字音频的每一位数据都对应有一个CK脉冲,它的频率为:2*采样频率*量化位数,2代表左右两个通道数据。
(4)&&&&ext_SD(extend Serial Data):扩展串行数据线,用于全双工传输的数据接收。
另外,有时为使系统间更好地同步,还要传输一个主时钟,系列控制器固定输出为。
38.1.3 音频数据传输协议标准
随着技术的发展,在统一的硬件接口下,出现了多种不同的数据格式,可分为左对齐标准、右对齐标准、标准。另外,系列控制器还支持脉冲编码调音频传输协议。下面以系列控制器资源解释这四个传输协议。
系列控制器的数据寄存器只有,并且左右声道数据一般是紧邻传输,为正确得到左右两个声道数据,需要软件控制数据对应通道数据写入或读取。另外,音频数据的量化位数可能不同,控制器支持、和三种数据长度,因为数据寄存器是的,所以对于和数据长度需要发送两个。为此,可以产生四种数据和帧格式组合:
?&&&&将位数据封装在位帧中
?&&&&将位数据封装在位帧中
?&&&&将位数据封装在位帧中
?&&&&将位数据封装在位帧中
当使用32位数据包中的16位数据时,前16位(MSB)为有效位,16位LSB被强制清零,无需任何软件操作或DMA请求(只需一个读/写操作)。如果程序使用DMA传输(一般都会用),则24位和32位数据帧需要对数据寄存器执行两次DMA操作。24位的数据帧,硬件会将8位非有效位扩展到带有0位的32位。对于所有数据格式和通信标准而言,始终会先发送最高有效位MSB优先。
1.&&&&I2S Philips标准
使用信号来指示当前正在发送的数据所属的通道,为时表示左通道数据。该信号从当前通道数据的第一个位之前的一个时钟开始有效。发送方在时钟信号的下降沿改变数据,接收方在上升沿读取数据。信号也在的下降沿变化。参考图,为数据封装在帧传输波形。正如之前所说,线频率对于采样频率,一个线周期包括发送左声道和右声道数据,在图中实际需要个周期来完成一次传输。
图 383 I2S Philips标准24bit传输
2.&&&&左对齐标准
在发生翻转同时开始传输数据,参考图,为数据封装在帧传输波形。该标准较少使用。注意此时为时,传输的是左声道数据,这刚好与标准相反。
图 384 左对齐标准24bit传输
3.&&&&右对齐标准
与左对齐标准类似,参考图,为数据封装在帧传输波形。
图 385 右对齐标准24bit传输
4.&&&&PCM标准
即脉冲编码调制,模拟语音信号经过采样量化以及一定数据排列就是了。不再作为声道数据选择。它有两种模式,短帧模式和长帧模式,以信号高电平保持时间为判别依据,长帧模式保持个周期,短帧模式只保持个周期,可以通过相关寄存器位选择。如果有多通道数据是在一个周期内传输完成的,传完左声道数据就紧跟发送右声道数据。图为单声道数据扩展到数据帧发送波形。
图 386 PCM标准16bit传输
38.2 I2S功能框图
系列控制器有两个,和,两个的资源是相互独立的,但分别与和共用大部分资源。这样和只能选择一个功能使用,和相同道理。资源共用包括引脚共用和部分寄存器共用,当然也有部分是专用的。已经在之前相关章节做了详细讲解,建议先看懂相关内容再学习。
控制器的支持两种工作模式,主模式和从模式;主模式下使用自身时钟发生器生成通信时钟。功能框图参考图。
图 387 I2S功能框图
1.&&&&功能引脚
的映射到的引脚,映射到的引脚,映射到的引脚,映射到的引脚。是专用引脚,用于主模式下输出时钟或在从模式下输入时钟。时钟发生器可以由控制器内部时钟源分频产生,亦可采用引脚输入时钟分频得到,一般使用内部时钟源即可。控制器引脚分布参考表。
表 381 STM32f42x系列控制器I2S引脚分布
其中,和不能用于的全双工模式。
2.&&&&数据寄存器
有一个与共用的数据寄存器,有效长度为,用于数据发送和接收,它实际由三个部分组成,一个移位寄存器、一个发送缓冲区和一个接收缓冲区,当处于发送模式时,向写入数据先保存在发送缓冲区,总线自动把发送缓冲区内容转入到移位寄存器中进行传输;在接收模式下,实际接收到的数据先填充移位寄存器,然后自动转入接收缓冲区,软件读取时自动从接收缓冲区内读取。是挂载在总线上的。
3.&&&&逻辑控制
的逻辑控制通过设置相关寄存器位实现,比如通过配置配置寄存器的相关位可以实现选择和模式切换、选择工作在主模式还是从模式并且选择是发送还是接收、选择标准、传输数据长度等等。控制寄存器可用于设置相关中断和请求使能,有个中断事件,分别为发送缓冲区为空、接收缓冲区非空、上溢错误、下溢错误和帧错误。状态寄存器用于指示当前状态。
4.&&&&时钟发生器
I2S比特率用来确定I2S数据线上的数据流和I2S时钟信号频率。I2S比特率=每个通道的位数&通道数&音频采样频率。
图为时钟发生器内部结构图。可选或可以通过寄存器的位选择使用时钟作为时钟源或引脚输入时钟作为时钟源。一般选择内部通过分频系数作为时钟源。例程程序设置时钟为,分频系数为,此时时钟为。
图 388 I2S时钟发生器内部结构
预分频器寄存器的位用于设置引脚时钟输出使能;位设置预分频器的奇数因子,实际分频值;为位线性分频器,不可设置为或。
当使能时钟输出,即时,采样频率计算如下:
(通道帧宽度为时)
(通道帧宽度为时)
当禁止时钟输出,即时,采样频率计算如下:
(通道帧宽度为时)
(通道帧宽度为时)
38.3 WM8978音频编译码器
是一个低功耗、高质量的立体声多媒体数字信号编译码器。它主要应用于便携式应用。它结合了立体声差分麦克风的前置放大与扬声器、耳机和差分、立体声线输出的驱动,减少了应用时必需的外部组件,比如不需要单独的麦克风或者耳机的放大器。
高级的片上数字信号处理功能,包含一个路均衡功能,一个用于和麦克风或者线路输入之间的混合信号的电平自动控制功能,一个纯粹的录音或者重放的数字限幅功能。另外在的线路上提供了一个数字滤波的功能,可以更好的应用滤波,比如"减少风噪声"。
可以被应用为一个主机或者一个从机。基于共同的参考时钟频率,比如和,内部的可以为编译码器提供所有需要的音频时钟。与控制器连接使用,一般作为主机,作为从机。
图为芯片内部结构示意图,参考来自《》。该图给人的第一印象感觉就是很复杂,密密麻麻很多内容,特别有很多"开关"。实际上,每个开关对应着内部寄存器的一个位,通过控制寄存器的就可以控制开关的状态。
图 389 WM8978内部结构
1.&&&&输入部分
结构图的左边部分是输入部分,可用于模拟声音输入,即用于录音输入。有三个输入接口,一个是由和、和组合而成的伪差分立体声麦克风输入,一个是由和组合的立体声麦克风输入,还有一个是由和组合的线输入或用来传输告警声的输入。
2.&&&&输出部分
结构图的右边部分是声音放大输出部分,和用于耳机驱动,和用于扬声器驱动,和也可以配置成立体声线输出,也可以用于提供一个左右声道的单声道混合。
3.&&&&ADC和DAC
结构图的中边部分是芯片核心内容,处理声音的和转换。部分对声音输入进行处理,包括滤波处理、音量控制、输入限幅器电平自动控制等等。部分控制声音输出效果,包括路均衡器、放大、输出限幅以及音量控制等等处理。
4.&&&&通信接口
有两个通信接口,一个是数字音频通信接口,另外一个是控制接口。音频接口是采用接口,支持左对齐、右对齐和标准模式,以及模式和模拟。控制接口用于控制器发送控制命令配置运行状态,它提供线或线控制接口,对于控制器,我们选择线接口方式,它实际就是总线方式,其芯片地址固定为。通过控制接口可以访问内部寄存器,实现芯片工作环境配置,总共有个寄存器,标示为至,限于篇幅问题这里不再深入探究,每个寄存器意义参考《》了解。
寄存器是长度,高位用于标示寄存器地址,低为有实际意义,比如对于图中的某个开关。所以在控制器向芯片发送控制命令时,必须传输长度为的指令,芯片会根据接收命令高位值寻址。
5.&&&&其他部分
作为主从机都必须对时钟进行管理,由内部单元控制。另外还有电源管理单元。
38.4 WAV格式文件
是微软公司开发的一种音频格式文件,用于保存平台的音频信息资源,它符合资源互换文件格式,文件规范。标准格式化的文件和格式一样,也是的取样频率,位量化数字,因此在声音文件质量和相差无几!是录音时用的标准的文件格式,文件的扩展名为"",数据本身的格式为或压缩型,属于无损音乐格式的一种。
38.4.1 RIFF文件规范
有不同数量的区块组成,每个由"标识符"、"数据大小"和"数据"三个部分组成,"标识符"和"数据大小"都是占用个字节空间。简单格式文件结构参考图。最开始是为""的,为""数据字节长度,所以总文件大小为。一般来说,不允许内部再包含,但有两个例外,为""和""的却是允许。对此""在其"数据"首个字节用来存放"格式标识码",""则对应""。
图 3810 RIFF文件格式结构
38.4.2 WAVE文件
文件是非常简单的一种文件,其"格式标识码"定义为。包括两个子,分别为和,还有一个可选的。用于标示音频数据的属性,包括编码方式、声道数目、采样频率、每个采样需要的数等等信息。是一个可选,一般当文件由某些软件转化而成就包含。包含文件的数字化波形声音数据。整体结构如表。
表 382 WAVE文件结构
格式标识码""
""块数据大小
""块数据大小
声音数据大小
是文件主体部分,包含声音数据,一般有两个编码格式:和,自适应差分脉冲编码调制属于有损压缩,现在几乎不用,绝大部分文件是编码。编码声音数据可以说是在"数字音频技术"介绍的源数据,主要参数是采样频率和量化位数。
表为量化位数为时不同声道数据在数据排列格式。
表 383 16bit声音数据格式
38.4.3 WAVE文件实例分析
利用工具软件可以非常方便以十六进制查看文件,图为名为"张国荣一盏小明灯"文件使用工具打开的部分界面截图。这部分截图是文件头部分,声音数据部分数据量非常大,有兴趣可以使用查看。
图 3811 WAV文件头实例
下面对文件头进行解读,参考表。
表 384 WAVE文件格式说明
十六进制源码
52 49 46 46
"RIFF"标识符
F4 FE 83 01
文件长度:0x0183FEF4(注意顺序)
57 41 56 45
"WAVE"标识符
66 6D 74 20
"fmt ",最后一位为空格
10 00 00 00
fmt chunk大小:0x10
编码格式:0x01为PCM。
声道数目:0x01为单声道,0x02为双声道
44 AC 00 00
采样频率(每秒样本数):0xAC44(44100)
10 B1 02 00
每秒字节数:0x02B110,等于声道数*采样频率*量化位数/8
每个采样点字节数:0x04,等于声道数*量化位数/8
量化位数:0x10
64 61 74 61
"data"数据标识符
48 FE 83 01
声音数据量:0x0183FE48
38.5 I2S初始化结构体详解
标准库函数对外设建立了一个初始化结构体。初始化结构体成员用于设置工作环境参数,并由相应初始化配置函数调用,这些设定参数将会设置相应的寄存器,达到配置工作环境的目的。
初始化结构体和初始化库函数配合使用是标准库精髓所在,理解了初始化结构体每个成员意义基本上就可以对该外设运用自如了。初始化结构体定义在文件中,初始化库函数定义在文件中,编程时我们可以结合这两个文件内注释使用。
初始化结构体用于配置基本工作环境,比如工作模式、通信标准选择等等。它被函数调用。
代码清单 381 I2S_InitTypeDef结构体
1 typedef struct {
uint16_t I2S_M // I2S模式选择
uint16_t I2S_S // I2S标准选择
uint16_t I2S_DataF // 数据格式
uint16_t I2S_MCLKO // 主时钟输出使能
uint32_t I2S_AudioF // 采样频率
uint16_t I2S_CPOL; // 空闲电平选择
8 } I2S_InitTypeD
(1)&&&&I2S_Mode:I2S模式选择,可选主机发送、主机接收、从机发送以及从机接收模式,它设定SPI_I2SCFGR寄存器I2SCFG位的值。一般设置STM32控制器为主机模式,当播放声音时选择发送模式;当录制声音时选择接收模式。
(2)&&&&I2S_Standard:通信标准格式选择,可选I2S Philips标准、左对齐标准、右对齐标准、PCM短帧标准或PCM长帧标准,它设定SPI_I2SCFGR寄存器I2SSTD位和PCMSYNC位的值。一般设置为I2S Philips标准即可。
(3)&&&&I2S_DataFormat:数据格式选择,设定有效数据长度和帧长度,可选标准16bit格式、扩展16bit(32bit帧长度)格式、24bit格式和32bit格式,它设定SPI_I2SCFGR寄存器DATLEN位和CHLEN位的值。对应16bit数据长度可选16bit或32bit帧长度,其他都是32bit帧长度。
(4)&&&&I2S_MCLKOutput:主时钟输出使能控制,可选使能输出或禁止输出,它设定SPI_I2SPR寄存器MCKOE位的值。为提高系统性能一般使能主时钟输出。
(5)&&&&I2S_AudioFreq:采样频率设置,标准库提供采样采样频率选择,分别为8kHz、11kHz、16kHz、22kHz、32kHz、44kHz、48kHz、96kHz、192kHz以及默认2Hz,它设定SPI_I2SPR寄存器的值。
(6)&&&&I2S_CPOL:空闲状态的CK线电平,可选高电平或低电平,它设定SPI_I2SCFGR寄存器CKPOL位的值。一般设置为电平即可。
38.6 录音与回放实验
格式文件在现阶段一般以无损音乐格式存在,音质可以达到格式标准。结合上一章卡操作内容,本实验通过文件系统函数从卡读取格式文件数据,然后通过接口将音频数据发送到芯片,这样在芯片的扬声器接口即可输出声音,整个系统构成一个简单的音频播放器。反过来的,我们可以实现录音功能,控制启动芯片的麦克风输入功能,音频数据从芯片的接口传输到控制器存储器中,利用卡文件读写函数,根据格式文件的要求填充文件头,然后就把传输过来的音频数据写入到格式文件中,这样就可以制成一个格式文件,可以通过开发板回放也可以在电脑端回放。
38.6.1 硬件设计
开发板板载芯片,具体电路设计参考图。与有两个连接接口,音频接口和两线控制接口,通过将芯片的引脚拉低选择两线控制接口,符合通信协议,这也导致是只写的,所以在程序上需要做一些处理。输入部分有两种模式,一个是板载咪头输入,另外一个是通过耳机插座引出。输出部分通过耳机插座引出,可直接接普通的耳机线或作为功放设备的输入源。
图 3812 WM8978电路设计
38.6.2 软件设计
这里只讲解核心的部分代码,有些变量的设置,头文件的包含等没有全部罗列出来,完整的代码请参考本章配套的工程。
上一章我们已经介绍了基于卡的文件系统,认识读写卡内文件方法,前面已经介绍了格式文件结构以及芯片相关内容,通过音频接口传输过来的音频数据可以直接作为格式文件的音频数据部分,大致过程就是程序控制启动录音功能,通过音频数据接口的录音输出传输到控制器指定缓冲区内,然后利用的文件写入函数把缓冲区数据写入到格式文件中,最终实现声音录制功能。同样的道理,格式文件中的音频数据可以直接传输给芯片实现音乐播放,整个过程与声音录制工程相反。
控制器与通信可分为两部分驱动函数,一部分是控制接口,另一部分是音频数据接口。
和两个是专门创建用来存放芯片驱动代码。
1.&&&&I2C控制接口
要正常工作并且实现符合我们的要求,我们必须对芯片相关寄存器进行必须要配置,控制器通过接口与芯片控制接口连接。接口内容也已经在以前做了详细介绍,这里主要讲解的功能函数。
文件中的函数、函数以及函数用于通信接口和相关配置,属于常规配置可以参考和章节理解,这里不再分析,代码具体见本章配套程序工程文件。
输入输出选择枚举
代码清单 382 输入输出选择枚举
1 /* WM8978 音频输入通道控制选项, 可以选择多路,比如 MIC_LEFT_ON | LINE_ON */
2 typedef enum {
IN_PATH_OFF = 0x00, /* 无输入 */
MIC_LEFT_ON = 0x01, /* LIN,LIP脚,MIC左声道(接板载咪头) */
MIC_RIGHT_ON = 0x02, /* RIN,RIP脚,MIC右声道(接板载咪头) */
LINE_ON = 0x04, /* L2,R2 立体声输入(接板载耳机插座) */
AUX_ON = 0x08, /* AUXL,AUXR 立体声输入(开发板没用到) */
DAC_ON = 0x10, /* I2S数据DAC (CPU产生音频信号) */
ADC_ON = 0x20 /* 输入的音频馈入WM8978内部ADC (I2S录音) */
10 } IN_PATH_E;
12 /* WM8978 音频输出通道控制选项, 可以选择多路 */
13 typedef enum {
OUT_PATH_OFF = 0x00, /* 无输出 */
EAR_LEFT_ON = 0x01, /* LOUT1 耳机左声道(接板载耳机插座) */
EAR_RIGHT_ON = 0x02, /* ROUT1 耳机右声道(接板载耳机插座) */
SPK_ON = 0x04, /* LOUT2和ROUT2反相输出单声道(开发板没用到)*/
OUT3_4_ON = 0x08, /* OUT3 和 OUT4 输出单声道音频(开发板没用到)*/
19 } OUT_PATH_E;
和枚举了芯片可用的声音输入源和输出端口,具体到开发板,如果进行录用功能,设置输入源为或,设置输出端口为或;对于音乐播放功能,设置输入源为,设置输出端口为。
代码清单 383 宏定义
1 /* 定义最大音量 */
2 #define VOLUME_MAX 63 /* 最大音量 */
3 #define VOLUME_STEP 1 /* 音量调节步长 */
5 /* 定义最大MIC增益 */
6 #define GAIN_MAX 63 /* 最大增益 */
7 #define GAIN_STEP 1 /* 增益步长 */
9 /* STM32 I2C 快速模式 */
10 #define WM8978_I2C_Speed 400000
11 /* WM8978 I2C从机地址 */
12 #define WM8978_SLAVE_ADDRESS 0x34
14 /*I2C接口*/
15 #define WM8978_I2C I2C1
16 #define WM8978_I2C_CLK RCC_APB1Periph_I2C1
18 #define WM8978_I2C_SCL_PIN GPIO_Pin_6
19 #define WM8978_I2C_SCL_GPIO_PORT GPIOB
20 #define WM8978_I2C_SCL_GPIO_CLK RCC_AHB1Periph_GPIOB
21 #define WM8978_I2C_SCL_SOURCE GPIO_PinSource6
22 #define WM8978_I2C_SCL_AF GPIO_AF_I2C1
24 #define WM8978_I2C_SDA_PIN GPIO_Pin_7
25 #define WM8978_I2C_SDA_GPIO_PORT GPIOB
26 #define WM8978_I2C_SDA_GPIO_CLK RCC_AHB1Periph_GPIOB
27 #define WM8978_I2C_SDA_SOURCE GPIO_PinSource7
28 #define WM8978_I2C_SDA_AF GPIO_AF_I2C1
30 /*等待超时时间*/
31 #define WM8978_I2C_FLAG_TIMEOUT ((uint32_t)0x4000)
32 #define WM8978_I2C_LONG_TIMEOUT ((uint32_t)(10 * WM8978_I2C_FLAG_TIMEOUT))
声音调节有一定的范围限制,比如的位用于设置的音量大小,可赋值范围为。包含可调节的输入麦克风增益,可对每个外部输入端口可单独设置增益大小,比如的位用于设置左通道输入增益音量,最大可设置值为。
控制接口被设置为模式,其地址固定为,为方便使用,直接定义为。
最后定义通信超时等待时间。
WM8978寄存器写入
代码清单 384 WM8978寄存器写入
1 static uint8_t WM8978_I2C_WriteRegister(uint8_t RegisterAddr,
uint16_t RegisterValue)
/* Start the config sequence */
I2C_GenerateSTART(WM8978_I2C, ENABLE);
/* Test on EV5 and clear it */
WM8978_I2CTimeout = WM8978_I2C_FLAG_TIMEOUT;
while (!I2C_CheckEvent(WM8978_I2C, I2C_EVENT_MASTER_MODE_SELECT)) {
if ((WM8978_I2CTimeout--) == 0)
return WM8978_I2C_TIMEOUT_UserCallback();
/* Transmit the slave address and enable writing operation */
I2C_Send7bitAddress(WM8978_I2C, WM8978_SLAVE_ADDRESS,
I2C_Direction_Transmitter);
/* Test on EV6 and clear it */
WM8978_I2CTimeout = WM8978_I2C_FLAG_TIMEOUT;
while (!I2C_CheckEvent(WM8978_I2C,
I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)) {
if ((WM8978_I2CTimeout--) == 0)
return WM8978_I2C_TIMEOUT_UserCallback();
/* Transmit the first address for write operation */
I2C_SendData(WM8978_I2C,
((RegisterAddr && 1) & 0xFE) | ((RegisterValue && 8) & 0x1));
/* Test on EV8 and clear it */
WM8978_I2CTimeout = WM8978_I2C_FLAG_TIMEOUT;
while (!I2C_CheckEvent(WM8978_I2C,
I2C_EVENT_MASTER_BYTE_TRANSMITTING)) {
if ((WM8978_I2CTimeout--) == 0)
return WM8978_I2C_TIMEOUT_UserCallback();
/* Prepare the register value to be sent */
I2C_SendData(WM8978_I2C, RegisterValue&0xff);
/*!& Wait till all data have been physically transferred on the bus */
WM8978_I2CTimeout = WM8978_I2C_LONG_TIMEOUT;
while (!I2C_GetFlagStatus(WM8978_I2C, I2C_FLAG_BTF)) {
if ((WM8978_I2CTimeout--) == 0)
WM8978_I2C_TIMEOUT_UserCallback();
/* End the configuration sequence */
I2C_GenerateSTOP(WM8978_I2C, ENABLE);
/* Return the verifying value: 0 (Passed) or 1 (Failed) */
用于向芯片寄存器写入数值,达到配置芯片工作环境,函数有两个形参,一个是寄存器地址,可设置范围为;另外一个是寄存器值,芯片寄存器总共有,前用于寻址,后位才是数据,这里寄存器值形参使用类型,只有低位有效。
使用通信,首先使用函数选定芯片,接下来需要两次调用函数发送两次数据,因为数据发送一次只能发送数据,为此需要把变量的第整合到变量的第位先发送,接下来再发送变量的低数据。
函数中还添加了通信超时等待功能,防止出错时卡死。
WM8978寄存器读取
芯片是从硬件上选择通信模式,该模式是只写的,控制器无法读取寄存器内容,但程序有时需要用到寄存器内容,为此我们创建了一个存放所有寄存器值的数组,在系统复位是将数组内容设置为缺省值,然后在每次修改寄存器内容时同步更新该数组内容,这样可以达到该数组与寄存器内容相等的效果,参考代码清单。
代码清单 385 WM8978寄存器值缓冲区和读取
wm8978寄存器缓存
3 由于WM8978的I2C两线接口不支持读取操作,因此寄存器值缓存在内存中,
4 当写寄存器时同步更新缓存,读寄存器时直接返回缓存中的值。
5 寄存器MAP 在WM_2011).pdf 的第89页,寄存器地址是7bit,寄存器数据是9bit
7 static uint16_t wm8978_RegCash[] = {
0x000, 0x000, 0x000, 0x000, 0x050, 0x000, 0x140, 0x000,
0x000, 0x000, 0x000, 0x0FF, 0x0FF, 0x000, 0x100, 0x0FF,
0x0FF, 0x000, 0x12C, 0x02C, 0x02C, 0x02C, 0x02C, 0x000,
0x032, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000,
0x038, 0x00B, 0x032, 0x000, 0x008, 0x00C, 0x093, 0x0E9,
0x000, 0x000, 0x000, 0x000, 0x003, 0x010, 0x010, 0x100,
0x100, 0x002, 0x001, 0x001, 0x039, 0x039, 0x039, 0x039,
0x001, 0x001
* @brief 从cash中读回读回wm8978寄存器
* @param _ucRegAddr :寄存器地址
* @retval 寄存器值
23 static uint16_t wm8978_ReadReg(uint8_t _ucRegAddr)
return wm8978_RegCash[_ucRegAddr];
* @brief 写wm8978寄存器
* @param _ucRegAddr:寄存器地址
* @param _usValue:寄存器值
* @retval 0:写入失败
* 1:写入成功
35 static uint8_t wm8978_WriteReg(uint8_t _ucRegAddr, uint16_t _usValue)
res=WM8978_I2C_WriteRegister(_ucRegAddr,_usValue);
wm8978_RegCash[_ucRegAddr] = _usV
实现向寄存器写入数据并修改缓冲区内容。
输出音量修改与读取
代码清单 386 音量修改与读取
* @brief 修改输出通道1音量
* @param _ucVolume :音量值, 0-63
* @retval 无
6 void wm8978_SetOUT1Volume(uint8_t _ucVolume)
uint16_t regL;
uint16_t regR;
if (_ucVolume & VOLUME_MAX) {
_ucVolume = VOLUME_MAX;
regL = _ucV
regR = _ucV
R52 LOUT1 Volume control
R53 ROUT1 Volume control
/* 先更新左声道缓存值 */
wm8978_WriteReg(52, regL | 0x00);
/* 再同步更新左右声道的音量 */
/* 0x180表示在音量为0时再更新,避免调节音量出现的"嘎哒"声 */
wm8978_WriteReg(53, regR | 0x100);
* @brief 读取输出通道1音量
* @param 无
* @retval 当前音量值
33 uint8_t wm8978_ReadOUT1Volume(void)
return (uint8_t)(wm8978_ReadReg(52) & 0x3F );
* @brief 输出静音.
* @param _ucMute:模式选择
* @arg 1:静音
* @arg 0:取消静音
* @retval 无
45 void wm8978_OutMute(uint8_t _ucMute)
uint16_t usRegV
if (_ucMute == 1) { /* 静音 */
usRegValue = wm8978_ReadReg(52); /* Left Mixer Control */
usRegValue |= (1u && 6);
wm8978_WriteReg(52, usRegValue);
usRegValue = wm8978_ReadReg(53); /* Left Mixer Control */
usRegValue |= (1u && 6);
wm8978_WriteReg(53, usRegValue);
usRegValue = wm8978_ReadReg(54); /* Right Mixer Control */
usRegValue |= (1u && 6);
wm8978_WriteReg(54, usRegValue);
usRegValue = wm8978_ReadReg(55); /* Right Mixer Control */
usRegValue |= (1u && 6);
wm8978_WriteReg(55, usRegValue);
} else { /* 取消静音 */
usRegValue = wm8978_ReadReg(52);
usRegValue &= ~(1u && 6);
wm8978_WriteReg(52, usRegValue);
usRegValue = wm8978_ReadReg(53); /* Left Mixer Control */
usRegValue &= ~(1u && 6);
wm8978_WriteReg(53, usRegValue);
usRegValue = wm8978_ReadReg(54);
usRegValue &= ~(1u && 6);
wm8978_WriteReg(54, usRegValue);
usRegValue = wm8978_ReadReg(55); /* Left Mixer Control */
usRegValue &= ~(1u && 6);
wm8978_WriteReg(55, usRegValue);
函数用于修改通道的音量大小,有一个形参用于指示音量大小,要求范围为。这里同时更新的左右两个声道音量,芯片的和分别用于设置的左声道和右声道音量,具体位段意义参考表。函数会同时修改寄存器缓存区数组内容。
表 385 OUT1音量控制寄存器
寄存器地址
直到一个写入到才更新和音量
耳机音量零交叉使能:仅仅在零交叉时改变增益;立即改变增益
左耳机输出消声:正常操作;消声
左耳机输出驱动:
直到一个写入到才更新和音量
耳机音量零交叉使能:仅仅在零交叉时改变增益;立即改变增益
左耳机输出消声:正常操作;消声
右耳机输出驱动:
另外,用于设置的音量,程序结构与相同,只是对应修改和。
函数用于读取的音量,它实际就是读取数组对应元素内容。
用于静音控制,它有一个形参用于设置静音效果,如果为则为开启静音,如果为则取消静音。静音控制是通过和的第位实现的,在进入静音模式时需要先保存和的音量大小,然后在退出静音模式时就可以正确返回到静音前和的配置。
输入增益调整
代码清单 387 输入增益调整
* @brief 设置增益
* @param _ucGain :增益值, 0-63
* @retval 无
6 void wm8978_SetMicGain(uint8_t _ucGain)
if (_ucGain & GAIN_MAX) {
_ucGain = GAIN_MAX;
/* PGA 音量控制 R45, R46
Bit8 INPPGAUPDATE
Bit7 INPPGAZCL 过零再更改
Bit6 INPPGAMUTEL PGA静音
Bit5:0 增益值,010000是0dB
wm8978_WriteReg(45, _ucGain);
wm8978_WriteReg(46, _ucGain | (1 && 8));
* @brief 设置Line输入通道的增益
* @param _ucGain :音量值, 0-7. 7最大,0最小。可衰减可放大。
* @retval 无
28 void wm8978_SetLineGain(uint8_t _ucGain)
uint16_t usRegV
if (_ucGain & 7) {
_ucGain = 7;
Mic 输入信道的增益由 PGABOOSTL 和 PGABOOSTR 控制
Aux 输入信道的输入增益由 AUXL2BOOSTVO[2:0] 和 AUXR2BOOSTVO[2:0] 控制
Line 输入信道的增益由 LIP2BOOSTVOL[2:0] 和 RIP2BOOSTVOL[2:0] 控制
/* R47(左声道),R48(右声道), MIC 增益控制寄存器
R47 (R48定义与此相同)
B8 PGABOOSTL=1,0表示MIC信号直通无增益,1表示MIC信号+20dB增益(通过自举电路)
B7 = 0,保留
B6:4 L2_2BOOSTVOL=x,0表示禁止,1-7表示增益-12dB ~ +6dB(可以衰减也可以放大)
B3 = 0,保留
B2:0 AUXL2BOOSTVOL=x,0表示禁止,1-7表示增益-12dB~+6dB(可以衰减也可以放大)
usRegValue = wm8978_ReadReg(47);
usRegValue &= 0x8F;/* 将Bit6:4清0 */
usRegValue |= (_ucGain && 4);
wm8978_WriteReg(47, usRegValue); /* 写左声道输入增益控制寄存器 */
usRegValue = wm8978_ReadReg(48);
usRegValue &= 0x8F;/* 将Bit6:4清0 */
usRegValue |= (_ucGain && 4);
wm8978_WriteReg(48, usRegValue); /* 写右声道输入增益控制寄存器 */
用于设置麦克风输入的增益,可以设置增强或减弱输入效果,比如对于部分声音源本身就是比较微弱,我们就可以设置放大该信号,从而得到合适的录制效果,该函数主要通过设置和实现,可设置的范围为,默认值为,没有增益效果。
用于设置输入的增益,对应芯片的和引脚组合的输入,开发板使用耳机插座引出拓展。它通过设置和寄存器实现,可设置范围为,默认值为,没有增益效果。
音频接口标准选择
代码清单 388 wm8978_CfgAudioIF函数
* @brief 配置WM8978的音频接口(I2S)
* @param _usStandard : 接口标准,
I2S_Standard_Phillips, I2S_Standard_MSB 或 I2S_Standard_LSB
* @param _ucWordLen : 字长,16、24、32 (丢弃不常用的20bit格式)
* @retval 无
8 void wm8978_CfgAudioIF(uint16_t _usStandard, uint8_t _ucWordLen)
uint16_t usR
/* WM_2011).pdf 73页,寄存器列表 */
/* REG R4, 音频接口控制寄存器
B8 BCP = X, BCLK极性,0表示正常,1表示反相
B7 LRCP = x, LRC时钟极性,0表示正常,1表示反相
B6:5 WL = x,字长,00=16bit,01=20bit,10=24bit,11=32bit
18 (右对齐模式只能操作在最大24bit)
B4:3 FMT = x,音频数据格式,00=右对齐,01=左对齐,10=I2S格式,11=PCM
B2 DACLRSWAP = x, 控制DAC数据出现在LRC时钟的左边还是右边
B1 ADCLRSWAP = x,控制ADC数据出现在LRC时钟的左边还是右边
B0 MONO = 0,0表示立体声,1表示单声道,仅左声道有效
usReg = 0;
if (_usStandard == I2S_Standard_Phillips) { /* I2S飞利浦标准 */
usReg |= (2 && 3);
} else if (_usStandard == I2S_Standard_MSB) { /* MSB对齐标准(左对齐) */
usReg |= (1 && 3);
} else if (_usStandard == I2S_Standard_LSB) { /* LSB对齐标准(右对齐) */
usReg |= (0 && 3);
} else { /*PCM标准(16位通道帧上带长或短帧同步或者16位数据帧扩展为32位通道帧) */
usReg |= (3 && 3);;
if (_ucWordLen == 24) {
usReg |= (2 && 5);
} else if (_ucWordLen == 32) {
usReg |= (3 && 5);
usReg |= (0 && 5); /* 16bit */
wm8978_WriteReg(4, usReg);
R6,时钟产生控制寄存器
MS = 0, WM8978被动时钟,由MCU提供MCLK时钟
wm8978_WriteReg(6, 0x000);
函数用于设置芯片的音频接口标准,它有两个形参,第一个是标准选择,可选标准、左对齐标准以及右对齐标准;另外一个形参是字长设置,可选、以及,较常用。它函数通过控制芯片实现,最后还通过通用时钟控制寄存器设置芯片的工作在从模式,时钟线为输入时钟。
输入输出通道设置
代码清单 389 wm8978_CfgAudioPath函数
1 void wm8978_CfgAudioPath(uint16_t _InPath, uint16_t _OutPath)
uint16_t usR
if ((_InPath == IN_PATH_OFF) && (_OutPath == OUT_PATH_OFF)) {
wm8978_PowerDown();
R1 寄存器 Power manage 1
Bit8 BUFDCOPEN, Output stage 1.5xAVDD/2 driver enable
Bit7 OUT4MIXEN, OUT4 mixer enable
Bit6 OUT3MIXEN, OUT3 mixer enable
Bit5 PLLEN .不用
Bit4 MICBEN ,Microphone Bias Enable (MIC偏置电路使能)
Bit3 BIASEN ,Analogue amplifier bias control必须设置为1模拟放大器才工作
Bit2 BUFIOEN , Unused input/output tie off buffer enable
Bit1:0 VMIDSEL, 必须设置为非00值模拟放大器才工作
usReg = (1 && 3) | (3 && 0);
if (_OutPath & OUT3_4_ON) { /* OUT3和OUT4使能输出 */
usReg |= ((1 && 7) | (1 && 6));
if ((_InPath & MIC_LEFT_ON) || (_InPath & MIC_RIGHT_ON)) {
usReg |= (1 && 4);
wm8978_WriteReg(1, usReg); /* 写寄存器 */
/**********************************************/
/* 此处省略部分代码,具体参考工程文件 */
/**********************************************/
/* R10 寄存器 DAC Control
B6 SOFTMUTE, Softmute enable:
B3 DACOSR128, DAC oversampling rate: 0=64x (lowest power)
1=128x (best performance)
B2 AMUTE, Automute enable
B1 DACPOLR, Right DAC output polarity
B0 DACPOLL, Left DAC output polarity:
if (_InPath & DAC_ON) {
wm8978_WriteReg(10, 0);
函数用于配置声音输入输出通道,有两个形参,第一个形参用于设置输入源,可以使用枚举类型成员的一个或多个或运算结果;第二个形参用于设置输出通道,可以使用枚举类型成员的一个或多个或运算结果。具体到开发板,如果进行录用功能,设置输入源为或,设置输出端口为或;对于音乐播放功能,设置输入源为,设置输出端口为。
函数首先判断输入参数合法性,如果输入出错直接调用函数进入低功耗模式,并退出。
接下来使用配置相关寄存器值。大致可分三个部分,第一部分是电源管理部分,主要涉及到、和三个寄存器,使用输入输出通道之前必须开启相关电源。第二部分是输入通道选择及相关配置,配置控制选择输入通道,设置输入的高通滤波器功能,、、和设置输入的可调陷波滤波器功能,、和控制输入限幅器电平自动控制,设置噪声门限,和设置通道增益参数,和设置数字音量,设置功能。第三部分是输出通道选择及相关配置,控制选择输出通道,和设置左右通道混合输出效果,设置混合输出效果,设置混合输出效果,和设置左右数字音量,设置参数。
代码清单 3810 wm8978_Reset函数
1 uint8_t wm8978_Reset(void)
/* wm8978寄存器缺省值 */
const uint16_t reg_default[] = {
0x000, 0x000, 0x000, 0x000, 0x050, 0x000, 0x140, 0x000,
0x000, 0x000, 0x000, 0x0FF, 0x0FF, 0x000, 0x100, 0x0FF,
0x0FF, 0x000, 0x12C, 0x02C, 0x02C, 0x02C, 0x02C, 0x000,
0x032, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000,
0x038, 0x00B, 0x032, 0x000, 0x008, 0x00C, 0x093, 0x0E9,
0x000, 0x000, 0x000, 0x000, 0x003, 0x010, 0x010, 0x100,
0x100, 0x002, 0x001, 0x001, 0x039, 0x039, 0x039, 0x039,
0x001, 0x001
res=wm8978_WriteReg(0x00, 0);
for (i = 0; i & sizeof(reg_default) / 2; i++) {
wm8978_RegCash[i] = reg_default[i];
函数用于软件复位芯片,通过写入完成,使其寄存器复位到缺省状态,同时会更新寄存器缓冲区数组恢复到缺省状态。
2.&&&&I2S控制接口
集成音频接口,用于与外部设备进行数字音频数据传输,芯片接口属性通过函数配置。控制器与进行音频数据传输,一般设置控制器为主机模式,作为从设备。
函数用于初始化相关,具体参考工程文件。
I2S工作模式配置
代码清单 3811 I2Sx_Mode_Config函数
1 void I2Sx_Mode_Config(const uint16_t _usStandard,const uint16_t _usWordLen,
const uint32_t _usAudioFreq )
I2S_InitTypeDef I2S_InitS
uint32_t n = 0;
FlagStatus status = RESET;
* For I2S mode, make sure that either:
* - I2S PLL is configured using the functions RCC_I2SCLKConfig
* (RCC_I2S2CLKSource_PLLI2S),
* RCC_PLLI2SCmd(ENABLE) and RCC_GetFlagStatus(RCC_FLAG_PLLI2SRDY).
RCC_I2SCLKConfig(RCC_I2S2CLKSource_PLLI2S);
RCC_PLLI2SCmd(ENABLE);
for (n = 0; n & 500; n++) {
status = RCC_GetFlagStatus(RCC_FLAG_PLLI2SRDY);
if (status == 1)break;
/* 打开 I2S2 APB1 时钟 */
RCC_APB1PeriphClockCmd(WM8978_CLK, ENABLE);
/* 复位 SPI2 外设到缺省状态 */
SPI_I2S_DeInit(WM8978_I2Sx_SPI);
/* I2S2 外设配置 */
/* 配置I2S工作模式 */
I2S_InitStructure.I2S_Mode = I2S_Mode_MasterTx;
/* 接口标准 */
I2S_InitStructure.I2S_Standard = _usS
/* 数据格式,16bit */
I2S_InitStructure.I2S_DataFormat = _usWordL
/* 主时钟模式 */
I2S_InitStructure.I2S_MCLKOutput = I2S_MCLKOutput_E
/* 音频采样频率 */
I2S_InitStructure.I2S_AudioFreq = _usAudioF
I2S_InitStructure.I2S_CPOL = I2S_CPOL_L
I2S_Init(WM8978_I2Sx_SPI, &I2S_InitStructure);
/* 使能 SPI2/I2S2 外设 */
I2S_Cmd(WM8978_I2Sx_SPI, ENABLE);
函数用于配置控制器的接口工作模式,它有三个形参,第一个为指定接口标准,一般设置为标准,第二个为字长设置,一般设置为,第三个为采样频率,一般设置为既可得到高音质效果。
首先是时钟配置,使用函数选择时钟源,一般选择内部时钟,函数使能时钟,并等待时钟正常后开启外设时钟。
接下来通过给结构体类型变量赋值设置工作模式,并由函数完成基本工作环境配置。
最后,函数用于使能。
I2S数据发送(DMA传输)
代码清单 3812 I2Sx_TX_DMA_Init函数
1 void I2Sx_TX_DMA_Init(const uint16_t *buffer0,const uint16_t *buffer1,
const uint32_t num)
NVIC_InitTypeDef NVIC_InitS
DMA_InitTypeDef DMA_InitS
RCC_AHB1PeriphClockCmd(I2Sx_DMA_CLK,ENABLE);//DMA1时钟使能
DMA_DeInit(I2Sx_TX_DMA_STREAM);
//等待DMA1_Stream4可配置
while (DMA_GetCmdStatus(I2Sx_TX_DMA_STREAM) != DISABLE) {}
//清空DMA1_Stream4上所有中断标志
DMA_ClearITPendingBit(I2Sx_TX_DMA_STREAM,
DMA_IT_FEIF4|DMA_IT_DMEIF4|DMA_IT_TEIF4|DMA_IT_HTIF4|DMA_IT_TCIF4);
/* 配置 DMA Stream */
//通道0 SPIx_TX通道
DMA_InitStructure.DMA_Channel = I2Sx_TX_DMA_CHANNEL;
//外设地址为:(u32)&SPI2-&DR
DMA_InitStructure.DMA_PeripheralBaseAddr =
(uint32_t)&WM8978_I2Sx_SPI-&DR;
//DMA 存储器0地址
DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)buffer0;
//存储器到外设模式
DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToP
//数据传输量
DMA_InitStructure.DMA_BufferSize =
//外设非增量模式
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_D
//存储器增量模式
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_E
//外设数据长度:16位
DMA_InitStructure.DMA_PeripheralDataSize =
DMA_PeripheralDataSize_HalfW
//存储器数据长度:16位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfW
// 使用循环模式
DMA_InitStructure.DMA_Mode = DMA_Mode_C
//高优先级
DMA_InitStructure.DMA_Priority = DMA_Priority_H
//不使用FIFO模式
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_D
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_1QuarterF
//外设突发单次传输
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_S
//存储器突发单次传输
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_S
DMA_Init(I2Sx_TX_DMA_STREAM, &DMA_InitStructure);//初始化DMA Stream
//双缓冲模式配置
DMA_DoubleBufferModeConfig(I2Sx_TX_DMA_STREAM,(uint32_t)buffer0,
DMA_Memory_0);
DMA_DoubleBufferModeConfig(I2Sx_TX_DMA_STREAM,(uint32_t)buffer1,
DMA_Memory_1);
//双缓冲模式开启
DMA_DoubleBufferModeCmd(I2Sx_TX_DMA_STREAM,ENABLE);
//开启传输完成中断
DMA_ITConfig(I2Sx_TX_DMA_STREAM,DMA_IT_TC,ENABLE);
//SPI2 TX DMA请求使能.
SPI_I2S_DMACmd(WM8978_I2Sx_SPI,SPI_I2S_DMAReq_Tx,ENABLE);
NVIC_InitStructure.NVIC_IRQChannel = I2Sx_TX_DMA_STREAM_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
函数用于初始化数据发送请求工作环境,并启动传输。它有三个形参,第一个为缓冲区地址,第二个为缓冲区地址,第三为缓冲区大小。这里使用的双缓冲区模式,就是开辟两个缓冲区空间,当第一个缓冲区用于传输时不占用,可以往第二个缓冲区填充数据,等到第一个缓冲区传输完成后切换第二个缓冲区用于传输,往第一个缓冲区填充数据,如此不断循环切换,可以达到数据传输不间断效果,具体可参考章节。这里为保证播放流畅性使用了双缓冲区模式。
函数首先是使能发送流时钟,并复位流配置和相关中断标志位。
通过对结构体类型的变量赋值配置流工作环境并通过完成配置。
函数用于指定双缓冲区模式下缓冲区地址。通过函数可以开启双缓冲区模式。这里使能传输完成中,用于指示其中一个缓冲区传输完成,需要切换缓冲区,可以开始往缓冲区填充数据。
用于使能发送传输请求。
最后配置传输完成中断的优先级。
DMA数据发送传输完成中断服务函数
代码清单 3813 DMA数据发送传输完成中断服务函数
1 //I2S DMA 回调函数
2 void (*I2S_DMA_TX_Callback)(void);
4 void I2Sx_TX_DMA_STREAM_IRQFUN(void)
//DMA传输完成标志
if (DMA_GetITStatus(I2Sx_TX_DMA_STREAM,I2Sx_TX_DMA_IT_TCIF)==SET) {
//清DMA传输完成标准
DMA_ClearITPendingBit(I2Sx_TX_DMA_STREAM,I2Sx_TX_DMA_IT_TCIF);
//执行回调函数,读取数据等操作在这里面处理
I2S_DMA_TX_Callback();
首先声明一个函数指针,即是一个函数指针,一般是先定义一个函数,再把函数名称赋值给。
函数是的传输中断服务函数,在判断是传输完成中断后执行函数指针对应函数内容。
启动和停止播放控制
代码清单 3814 启动和停止播放控制
1 void I2S_Play_Start(void)
DMA_Cmd(I2Sx_TX_DMA_STREAM,ENABLE);//开启DMA TX传输,开始播放
7 void I2S_Play_Stop(void)
DMA_Cmd(I2Sx_TX_DMA_STREAM,DISABLE);//关闭DMA TX传输,结束播放
用于开始播放,用于停止播放,实际是通过控制传输使能来实现。
I2S扩展功能模式配置
代码清单 3815 I2Sxext_Mode_Config函数
1 void I2Sxext_Mode_Config(const uint16_t _usStandard,
const uint16_t _usWordLen,const uint32_t _usAudioFreq)
I2S_InitTypeDef I2Sext_InitS
/* I2S2 外设配置 */
/* 配置I2S工作模式 */
I2Sext_InitStructure.I2S_Mode = I2S_Mode_MasterTx;
/* 接口标准 */
I2Sext_InitStructure.I2S_Standard = _usS
/* 数据格式,16bit */
I2Sext_InitStructure.I2S_DataFormat = _usWordL
/* 主时钟模式 */
I2Sext_InitStructure.I2S_MCLKOutput = I2S_MCLKOutput_E
/* 音频采样频率 */
I2Sext_InitStructure.I2S_AudioFreq = _usAudioF
I2Sext_InitStructure.I2S_CPOL = I2S_CPOL_L
I2S_FullDuplexConfig(WM8978_I2Sx_ext, &I2Sext_InitStructure);
/* 使能 SPI2/I2S2 外设 */
I2S_Cmd(WM8978_I2Sx_ext, ENABLE);
函数用于配置全双工模式,使用扩展功能方便数据处理,这对实现录音功能有很大的帮助。它有三个形参,第一个为指定接口标准,一般设置为标准,第二个为字长设置,一般设置为,第三个为采样频率,一般设置为既可得到高音质效果。
函数没有配置相关时钟,一般在函数完成时钟配置,所以一般先运行函数才运行函数。
函数先给结构体类型的变量赋值配置参数,然后调用函数配置全双工模式。最后使用使能扩展。
I2S扩展数据接收(DMA传输)
代码清单 3816 I2Sxext_RX_DMA_Init函数
1 void I2Sxext_RX_DMA_Init(const uint16_t *buffer0,const uint16_t *buffer1,
const uint32_t num)
NVIC_InitTypeDef NVIC_InitS
DMA_InitTypeDef DMA_InitS
RCC_AHB1PeriphClockCmd(I2Sx_DMA_CLK,ENABLE);
DMA_DeInit(I2Sxext_RX_DMA_STREAM);
while (DMA_GetCmdStatus(I2Sxext_RX_DMA_STREAM) != DISABLE) {}
DMA_ClearITPendingBit(I2Sxext_RX_DMA_STREAM,
DMA_IT_FEIF3|DMA_IT_DMEIF3|DMA_IT_TEIF3|DMA_IT_HTIF3|DMA_IT_TCIF3);
/* 配置 DMA Stream */
DMA_InitStructure.DMA_Channel = I2Sxext_RX_DMA_CHANNEL;
DMA_InitStructure.DMA_PeripheralBaseAddr =
(uint32_t)&WM8978_I2Sx_ext-&DR;
DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)buffer0;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToM
DMA_InitStructure.DMA_BufferSize =
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_D
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_E
DMA_InitStructure.DMA_PeripheralDataSize =
DMA_PeripheralDataSize_HalfW
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfW
DMA_InitStructure.DMA_Mode = DMA_Mode_C
DMA_InitStructure.DMA_Priority = DMA_Priority_M
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_D
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_1QuarterF
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_S
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_S
DMA_Init(I2Sxext_RX_DMA_STREAM, &DMA_InitStructure);
DMA_DoubleBufferModeConfig(I2Sxext_RX_DMA_STREAM,
(uint32_t)buffer0,DMA_Memory_0);
DMA_DoubleBufferModeConfig(I2Sxext_RX_DMA_STREAM,
(uint32_t)buffer1,DMA_Memory_1);
DMA_DoubleBufferModeCmd(I2Sxext_RX_DMA_STREAM,ENABLE);
DMA_ITConfig(I2Sxext_RX_DMA_STREAM,DMA_IT_TC,ENABLE);
SPI_I2S_DMACmd(WM8978_I2Sx_ext,SPI_I2S_DMAReq_Rx,ENABLE);
NVIC_InitStructure.NVIC_IRQChannel = I2Sxext_RX_DMA_STREAM_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
函数配置扩展的数据接收功能,使用传输方式接收数据,程序结构与函数一致,只是传输方向不同,函数是从外设到存储器传输,函数是存储器到外设传输。
函数也是使用的双缓冲区模式传输数据。最后使能了传输完成中断,并使能数据接收请求。
DMA数据接收传输完成中断服务函数
代码清单 3817 DMA数据接收传输完成中断服务函数
1 //I2S DMA RX回调函数
2 void (*I2S_DMA_RX_Callback)(void);
4 void I2Sxext_RX_DMA_STREAM_IRQFUN(void)
if(DMA_GetITStatus(I2Sxext_RX_DMA_STREAM,I2Sxext_RX_DMA_IT_TCIF)==SET) {
DMA_ClearITPendingBit(I2Sxext_RX_DMA_STREAM,I2Sxext_RX_DMA_IT_TCIF);
I2S_DMA_RX_Callback(); //执行回调函数,读取数据等操作在这里面处理
与数据发送传输完成中断服务函数类似,函数在判断得到是数据接收传输完成后执行函数,实际也是一个函数指针。
启动和停止录音
代码清单 3818 启动和停止录音
1 void I2Sxext_Recorde_Start(void)
DMA_Cmd(I2Sxext_RX_DMA_STREAM,ENABLE);
6 void I2Sxext_Recorde_Stop(void)
DMA_Cmd(I2Sxext_RX_DMA_STREAM,DISABLE);
函数用于启动录音,函数用于停止录音,实际是通过控制传输使能来实现。
至此,关于芯片的驱动程序已经介绍完整了,该部分程序都是在文件中的,接下来我们就可以使用这些驱动程序实现录音和回放功能了。
3.&&&&录音和回放功能
录音和回放功能是在驱动函数基础上搭建而成的,实现代码存放在和文件中。启动录音功能后会在卡内创建一个格式文件,把音频数据保存在该文件中,录音结束后既可得到一个完整的格式文件。回放功能用于播放录音文件,实际上回放功能的实现函数也是适用于播放其他格式文件的。
枚举和结构体类型定义
代码清单 3819 枚举和结构体类型定义
1 /* 录音机状态 */
STA_IDLE = 0, /* 待机状态 */
STA_RECORDING, /* 录音状态 */
STA_PLAYING, /* 放音状态 */
STA_ERR, /* error */
9 typedef struct {
uint8_t ucI /* 输入源:0:MIC, 1:线输入 */
uint8_t ucFmtI /* 音频格式:标准、位长、采样频率 */
uint8_t ucV /* 当前放音音量 */
uint8_t ucG /* 当前增益 */
uint8_t ucS /* 录音机状态,0表示待机,1表示录音中,2表示播放中 */
15 } REC_TYPE;
17 /* WAV文件头格式 */
18 typedef __packed struct {
uint32_t /* = "RIFF" 0x*/
uint32_t size_8; /* 从下个地址开始到文件尾的总字节数 */
uint32_t /* = "WAVE" 0x*/
uint32_t /* = "fmt " 0x20746d66*/
uint32_t fmtS /* 下一个结构体的大小(一般为16) */
uint16_t wFormatT /* 编码方式,一般为1 */
uint16_t wC /* 通道数,单声道为1,立体声为2 */
uint32_t dwSamplesPerS /* 采样率 */
uint32_t dwAvgBytesPerS /* 每秒字节数(= 采样率 & 每个采样点字节数) */
uint16_t wBlockA /* 每个采样点字节数(=量化比特数/8*通道数) */
uint16_t wBitsPerS /* 量化比特数(每个采样需要的bit数) */
uint32_t /* = "data" 0x*/
uint32_t /* 纯数据长度 */
首先,定义一个枚举类型罗列录音和回放功能的状态,录音和回放功能是不能同时使用的,使用枚举类型区分非常有效。
结构体类型定义了录音和回放功能相关可控参数,包括声音源输入端,可选板载咪头或板载耳机插座的线输入;音频格式选择,一般选择标准、字长、采样频率;音频输出耳机音量控制;录音时声音增益;当前状态。
结构体类型定义了格式文件头,具体参考"格式文件",这里没有用到。需要注意的是这里使用关键字,它表示结构字节对齐。
启动播放WAV格式音频文件
代码清单 3820 StartPlay函数
1 static void StartPlay(const char *filename)
printf("当前播放文件 -& %s\n",filename);
result=f_open(&file,filename,FA_READ);
if (result!=FR_OK) {
printf("打开音频文件失败!!!-&%d\r\n",result);
result = f_close (&file);
Recorder.ucStatus = STA_ERR;
//读取WAV文件头
result = f_read(&file,&rec_wav,sizeof(rec_wav),&bw);
//先读取音频数据到缓冲区
result = f_read(&file,(uint16_t *)buffer0,RECBUFFER_SIZE*2,&bw);
result = f_read(&file,(uint16_t *)buffer1,RECBUFFER_SIZE*2,&bw);
Delay_ms(10); /* 延迟一段时间,等待I2S中断结束 */
I2S_Stop(); /* 停止I2S录音和放音 */
wm8978_Reset(); /* 复位WM8978到复位状态 */
Recorder.ucStatus = STA_PLAYING; /* 放音状态 */
/* 配置WM8978芯片,输入为DAC,输出为耳机 */
wm8978_CfgAudioPath(DAC_ON, EAR_LEFT_ON | EAR_RIGHT_ON);
/* 调节音量,左右相同音量 */
wm8978_SetOUT1Volume(Recorder.ucVolume);
/* 配置WM8978音频接口为飞利浦标准I2S接口,16bit */
wm8978_CfgAudioIF(I2S_Standard_Phillips, 16);
I2Sx_Mode_Config(g_FmtList[Recorder.ucFmtIdx][0],
g_FmtList[Recorder.ucFmtIdx][1],g_FmtList[Recorder.ucFmtIdx][2]);
I2Sxext_Mode_Config(g_FmtList[Recorder.ucFmtIdx][0],
g_FmtList[Recorder.ucFmtIdx][1],g_FmtList[Recorder.ucFmtIdx][2]);
I2Sxext_RX_DMA_Init(&recplaybuf[0],&recplaybuf[1],1);
DMA_ITConfig(I2Sxext_RX_DMA_STREAM,DMA_IT_TC,DISABLE);//开启传输完成中断
I2Sxext_Recorde_Stop();
I2Sx_TX_DMA_Init(buffer0,buffer1,RECBUFFER_SIZE);
I2S_Play_Start();
函数用于启动播放格式音频文件,它有一个形参,用于指示待播放文件名称。函数首先检查待播放文件是否可以正常打开,如果打开失败则退出播放。如果可以正常打开文件则先读取格式文件头,保存在结构体类型变量中,同时先读取音频数据填充到两个缓冲区中,这两个缓冲区和是定义的全局数组变量,用于双缓冲区模式。
接下来,配置的工作环境,首先停止并复位芯片。这里是播放音频功能,所以设置的输入是,播放接口接收到的音频数据,输出设置为耳机输出。控制器的接口和的接口都设置为标志、字长为。
然后,调用配置的发送请求,并调用函数使能数据传输。
启动录音功能
代码清单 3821 StartRecord函数
1 static void StartRecord(const char *filename)
printf("当前录音文件 -& %s\n",filename);
result=f_open(&file,filename,FA_CREATE_ALWAYS|FA_WRITE);
if (result!=FR_OK) {
printf("Open wavfile fail!!!-&%d\r\n",result);
result = f_close (&file);
Recorder.ucStatus = STA_ERR;
// 写入WAV文件头,这里必须写入写入后文件指针自动偏移到sizeof(rec_wav)位置,
// 接下来写入音频数据才符合格式要求。
result=f_write(&file,(const void *)&rec_wav,sizeof(rec_wav),&bw);
Delay_ms(10); /* 延迟一段时间,等待I2S中断结束 */
I2S_Stop(); /* 停止I2S录音和放音 */
wm8978_Reset(); /* 复位WM8978到复位状态 */
Recorder.ucStatus = STA_RECORDING; /* 录音状态 */
/* 调节放音音量,左右相同音量 */
wm8978_SetOUT1Volume(Recorder.ucVolume);
if (Recorder.ucInput == 1) { /* 线输入 */
/* 配置WM8978芯片,输入为线输入,输出为耳机 */
wm8978_CfgAudioPath(LINE_ON | ADC_ON, EAR_LEFT_ON | EAR_RIGHT_ON);
wm8978_SetLineGain(Recorder.ucGain);
} else { /* MIC输入 */
/* 配置WM8978芯片,输入为Mic,输出为耳机 */
wm8978_CfgAudioPath(MIC_RIGHT_ON|ADC_ON, EAR_LEFT_ON|EAR_RIGHT_ON);
wm8978_SetMicGain(Recorder.ucGain);
/* 配置WM8978音频接口为飞利浦标准I2S接口,16bit */
wm8978_CfgAudioIF(I2S_Standard_Phillips, 16);
I2Sx_Mode_Config(g_FmtList[Recorder.ucFmtIdx][0],
g_FmtList[Recorder.ucFmtIdx][1],g_FmtList[Recorder.ucFmtIdx][2]);
I2Sxext_Mode_Config(g_FmtList[Recorder.ucFmtIdx][0],
g_FmtList[Recorder.ucFmtIdx][1],g_FmtList[Recorder.ucFmtIdx][2]);
I2Sx_TX_DMA_Init(&recplaybuf[0],&recplaybuf[1],1);
DMA_ITConfig(I2Sx_TX_DMA_STREAM,DMA_IT_TC,DISABLE);//开启传输完成中断
I2S_DMA_RX_Callback=Recorder_I2S_DMA_RX_C
I2Sxext_RX_DMA_Init(buffer0,buffer1,RECBUFFER_SIZE);
I2S_Play_Start();
I2Sxext_Recorde_Start();
函数在结构上与函数类似,它实现启动录音功能,它有一个形参,指示保存录音数据的文件名称。函数会首先创建录音文件,因为用到标志位,函数会总是创建新文件,如果已存在该文件则会覆盖原先的文件内容。
这些必须先写入格式文件头数据,这样当前文件指针自动移动到文件头的下一个字节,即是存放音频数据的起始位置。
开发板支持线输入和板载咪头输入,程序默认使用咪头输入,在录音同时使能耳机输出,这样录音时在耳机接口是有相同的声音输出的。
录音功能需要使能扩展。
录音和回放功能选择
代码清单 3822 RecorderDemo函数
1 void RecorderDemo(void)
uint8_t ucR /* 通过串口打印相关信息标志 */
Recorder.ucStatus=STA_IDLE; /* 开始设置为空闲状态 */
Recorder.ucInput=0; /* 缺省MIC输入 */
Recorder.ucFmtIdx=3; /* 缺省飞利浦I2S标准,16bit数据长度,44K采样率 */
Recorder.ucVolume=35; /* 缺省耳机音量 */
if (Recorder.ucInput==0) { //MIC
Recorder.ucGain=50; /* 缺省MIC增益 */
rec_wav.wChannels=1; /* 缺省MIC单通道 */
} else { //LINE
Recorder.ucGain=6; /* 缺省线路输入增益 */
rec_wav.wChannels=2; /* 缺省线路输入双声道 */
rec_wav.riff=0x; /* "RIFF"; RIFF 标志 */
rec_wav.size_8=0; /* 文件长度,未确定 */
rec_wav.wave=0x; /* "WAVE"; WAVE 标志 */
rec_wav.fmt=0x20746d66; /* "fmt "; fmt 标志,最后一位为空 */
rec_wav.fmtSize=16; /* sizeof(PCMWAVEFORMAT) */
rec_wav.wFormatTag=1; /* 1 表示为PCM 形式的声音数据 */
/* 每样本的数据位数,表示每个声道中各个样本的数据位数。 */
rec_wav.wBitsPerSample=16;
/* 采样频率(每秒样本数) */
rec_wav.dwSamplesPerSec=g_FmtList[Recorder.ucFmtIdx][2];
/* 每秒数据量;其值为通道数&每秒数据位数&每样本的数据位数/ 8。 */
rec_wav.dwAvgBytesPerSec=
rec_wav.wChannels*rec_wav.dwSamplesPerSec*rec_wav.wBitsPerSample/8;
/* 数据块的调整数(按字节算的),其值为通道数&每样本的数据位值/8。 */
rec_wav.wBlockAlign=rec_wav.wChannels*rec_wav.wBitsPerSample/8;
rec_wav.data=0x; /* "data"; 数据标记符 */
rec_wav.datasize=0; /* 语音数据大小目前未确定*/
/* 如果路径不存在,创建文件夹 */
result = f_opendir(&dir,RECORDERDIR);
while (result != FR_OK) {
f_mkdir(RECORDERDIR);
result = f_opendir(&dir,RECORDERDIR);
/* 初始化并配置I2S */
I2S_Stop();
I2S_GPIO_Config();
I2Sx_Mode_Config(g_FmtList[Recorder.ucFmtIdx][0],
g_FmtList[Recorder.ucFmtIdx][1],g_FmtList[Recorder.ucFmtIdx][2]);
I2Sxext_Mode_Config(g_FmtList[Recorder.ucFmtIdx][0],
g_FmtList[Recorder.ucFmtIdx][1],g_FmtList[Recorder.ucFmtIdx][2]);
I2S_DMA_TX_Callback=MusicPlayer_I2S_DMA_TX_C
I2S_Play_Stop();
I2S_DMA_RX_Callback=Recorder_I2S_DMA_RX_C
I2Sxext_Recorde_Stop();
ucRefresh = 1;
bufflag=0;
/* 进入主程序循环体 */
while (1) {
/* 如果使能串口打印标志则打印相关信息 */
if (ucRefresh == 1) {
DispStatus(); /* 显示当前状态,频率,音量等 */
ucRefresh = 0;
if (Recorder.ucStatus == STA_IDLE) {
/* KEY2开始录音 */
if (Key_Scan(KEY2_GPIO_PORT,KEY2_PIN)==KEY_ON) {
/* 寻找合适文件名 */
for (i=1; i&0 ++i) {
sprintf(recfilename,"0:/recorder/rec%03d.wav",i);
result=f_open(&file,(const TCHAR *)recfilename,FA_READ);
if (result==FR_NO_FILE)break;
f_close(&file);
if (i==0xff) {
Recorder.ucStatus =STA_ERR;
/* 开始录音 */
StartRecord(recfilename);
ucRefresh = 1;
/* TouchPAD开始回放录音 */
if (TPAD_Scan(0)) {
/* 开始回放 */
StartPlay(recfilename);
ucRefresh = 1;
/* KEY1停止录音或回放 */
if (Key_Scan(KEY1_GPIO_PORT,KEY1_PIN)==KEY_ON) {
/* 对于录音,需要把WAV文件内容填充完整 */
if (Recorder.ucStatus == STA_RECORDING) {
I2Sxext_Recorde_Stop();
I2S_Play_Stop();
rec_wav.size_8=wavsize+36;
rec_wav.datasize=
result=f_lseek(&file,0);
result=f_write(&file,(const void *)&rec_wav,
sizeof(rec_wav),&bw);
result=f_close(&file);
printf("录音结束\r\n");
ucRefresh = 1;
Recorder.ucStatus = STA_IDLE; /* 待机状态 */
I2S_Stop(); /* 停止I2S录音和放音 */
wm8978_Reset(); /* 复位WM8978到复位状态 */
/* DMA传输完成 */
if (Isread==1) {
switch (Recorder.ucStatus) {
case STA_RECORDING: // 录音功能,写入数据到文件
if (bufflag==0)
result=f_write(&file,buffer0,RECBUFFER_SIZE*2,(UINT*)&bw);
result=f_write(&file,buffer1,RECBUFFER_SIZE*2,(UINT*)&bw);
wavsize+=RECBUFFER_SIZE*2;
case STA_PLAYING: // 回放功能,读取数据到播放缓冲区
if (bufflag==0)
result = f_read(&file,buffer0,RECBUFFER_SIZE*2,&bw);
result = f_read(&file,buffer1,RECBUFFER_SIZE*2,&bw);
/* 播放完成或读取出错停止工作 */
if ((result!=FR_OK)||(file.fptr==file.fsize)) {
printf("播放完或者读取出错退出...\r\n");
I2S_Play_Stop();
file.fptr=0;
f_close(&file);
Recorder.ucStatus = STA_IDLE; /* 待机状态 */
I2S_Stop(); /* 停止I2S录音和放音 */
wm8978_Reset(); /* 复位WM8978到复位状态 */
函数实现录音和回放功能。是一个结构体类型变量,指示录音和回放功能相关参数,这里通过赋值缺省选择板载咪头输入、使用标准、字长、采样频率,并设置了音量和增益,对于输入增益范围为。是结构体类型变量,用于设置格式文件头,很多成员赋值为缺省值即可,成员和变量表示数据大小,因为录音时间长度直接影响这两个变量大小,现在并无法确定它们大小,需要在停止录音后才可计算得到它们的值。
接下来是使用的功能函数和组合判断卡内是否有名为""的文件夹,如果没有改文件夹就创建它,因为我们打算把录音文件存放在该文件夹内。
接下来就是调用相关函数完成工作环境配置。
是我们定义的一个函数的函数名,把他赋值给函数指针,这样可以实现在执行数据发送完成中断服务函数时执行函数。情况与之类似。开始时停止录音和回放功能。
变量作为通过串口打印相关操作和状态信息到串口调试助手"刷新"标志。变量用于指示当前空闲缓冲区,工程定义了两个缓冲区和用于双缓冲区模式,对于录音功能,为表示当前使用填充,已经填充完整,为表示当前使用填充,已经填充完整;对于回放功能,为表示用于当前播放,已经播放完成需要读取新数据填充,为表示用于当前播放,已经播放完成需要读取新数据填充。变量用于指示传输完成状态,为时表示传输完成,只有在传输完成中断服务函数中才会被置。
接下来就是无限循环函数了,先判断变量状态,如果为就执行函数,该函数只是使用函数打印相关信息。开发板集成有两个独立按键和,还有一个电容按键,程序设置按下按键开始录音功能,触摸电容按键开始回放功能,按下按键用于停止录音和回放功能,这里按键和电容按键是互斥的。
在空闲状态下,允许按下按键启动录音,录音功能是通过调用函数启动的,该函数需要一个参数指示录音文件名称,使用在执行函数之前会循环使用函数获取卡内不存在的文件,防止覆盖已存在文件。
在空闲状态下,允许触摸电容按键启动回放功能,它直接使用函数启动上一个录音文件播放。
不在空闲状态下,按下按键可以停在录音或回放功能,回放功能只需要停在的数据发送接口并复位即可,录音功能需要停在扩展的数据接收功能,并且需要填充完整格式文件头并写入到录音文件中。
无限循环还要不懂检测变量的状态,当它在传输完成中断服务函数被置时说明双缓冲区状态发生改变,对于录音功能,需要把以填充满的缓冲区数据通过写入到文件中;对于回放功能,需要利用函数读取新数据填充到已播缓冲区中,如果遇到读取出错或文件已经播放完全需要停止的传输并复位。
DMA传输完成中断回调函数
代码清单 3823 DMA传输完成中断回调函数
1 /* DMA发送完成中断回调函数 */
2 /* 缓冲区内容已经播放完成,需要切换缓冲区,进行新缓冲区内容播放
3 同时读取WAV文件数据填充到已播缓冲区 */
4 void MusicPlayer_I2S_DMA_TX_Callback(void)
if (Recorder.ucStatus == STA_PLAYING) {
if (I2Sx_TX_DMA_STREAM-&CR&(1&&19)) { //当前使用Memory1数据
bufflag=0; //可以将数据读取到缓冲区0
} else { //当前使用Memory0数据
bufflag=1; //可以将数据读取到缓冲区1
Isread=1; // DMA传输完成标志
16 /* DMA接收完成中断回调函数 */
17 /* 录音数据已经填充满了一个缓冲区,需要切换缓冲区,
18 同时可以把已满的缓冲区内容写入到文件中 */
19 void Recorder_I2S_DMA_RX_Callback(void)
if (Recorder.ucStatus == STA_RECORDING) {
if (I2Sxext_RX_DMA_STREAM-&CR&(1&&19)) { //当前使用Memory1数据
bufflag=0;
} else { //当前使用Memory0数据
bufflag=1;
Isread=1; // DMA传输完成标志
这两个函数用于在传输完成后切换缓冲区。数据流配置寄存器的位用于指示当前目标缓冲区,如果为,当前目标缓冲区为存储器;如果为,则为存储器。
代码清单 3824 main函数
1 int main(void)
/* NVIC中断优先级组选择 */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
/* 关闭BL_8782wifi使能 */
BL8782_PDN_INIT();
/* 初始化按键 */
Key_GPIO_Config();
/* 初始化调试串口,一般为串口1 */
Debug_USART_Config();
/* 挂载SD卡文件系统 */
result = f_mount(&fs,"0:",1); //挂载文件系统
if (result!=FR_OK) {
printf("\n SD卡文件系统挂载失败\n");
while (1);
/* 初始化系统滴答定时器 */
SysTick_Init();
printf("WM8978录音和回放功能\n");
/* 初始化电容按键 */
TPAD_Init();
/* 检测WM8978芯片,此函数会自动配置CPU的GPIO */
if (wm8978_Init()==0) {
printf("检测不到WM8978芯片!!!\n");
while (1); /* 停机 */
printf("初始化WM8978成功\n");
/* 录音与回放功能 */
RecorderDemo();
函数主要完成各个外设的初始化,包括独立按键初始化、电容按键初始化、调试串口初始化、卡文件系统挂载还有系统定时器初始化。
初始化接口用于控制芯片,并复位芯片,如果初始化成功则运行函数进行录音和回放功能测试。
38.6.3 下载验证
把卡插入到开发板右侧的卡槽内,使用线连接开发板上的""接口到电脑,将耳机插入到开发板上边沿左侧的耳机插座,电脑端配置好串口调试助手参数。编译实验程序并下载到开发板上,程序运行后在串口调试助手可接收到开发板发过来的提示信息,按下开发板左下边沿的按键,开始执行录音功能测试,不断对着咪头说话,就可以把声音录制下来,按下按键可以停止录音。然后触摸电容按键就可以在耳机接口听到之前录音内容了,按下按键可停止播放。录音完成后也可以在电脑端打开卡,找到其中的录音文件,可在电脑端音频播放器播放录音文件。
38.7 MP3播放器
格式音乐文件普遍存在我们生活中,实际上本身是一种音频编码方式,全称为。音频文件是标准中的声音部分,根据压缩质量和编码复杂程度划分为三层,即、、,且分别对应、、这三种声音文件,其中压缩率可达到至,可以大大减少文件占用存储空间大小。音频编码的层次越高,编码器越复杂,压缩率也越高。是利用人耳对高频声音信号不敏感的特性,将时域波形信号转换成频域信号,并划分成多个频段,对不同的频段使用不同的压缩率,对高频加大压缩比(甚至忽略信号)对低频信号使用小压缩比,保证信号不失真。这样一来就相当于抛弃人耳基本听不到的高频声音,只保留能听到的低频部分,这样可得到很高的压缩率。
38.7.1 MP3文件结构
文件大致分为个部分:(),音频数据,。是文件中附加关于该文件的歌手、标题、专辑名称、年代、风格等等信息,有两个版本和。固定存放在文件末尾,固定长度为字节,以三个字符开头,后面跟上歌曲信息。因为可存储信息量有限,有些文件添加了,是可选的,如果存在那它必然存在在文件起始位置,它实际是的补充。
1.&&&&ID3V2
以灵活管理方法文件附件信息,比可以存储更多的信息,同时也比在结构上复杂得多。常用是版本,一个文件最多就一个标签。标签由一个标签头和若干个标签帧或一个扩展标签头组成。关于曲目的信息如标题、作者等都存放在不同的标签帧中,扩展标签头和标签帧并不是必要的,但每个标签至少要有一个标签帧。标签头和标签帧一起顺序存放在文件的首部。
标签头结构如表。
表 386 标签头
版本号,就记录为
副版本号,一般记录为
标志字节,一般不用设置
标签大小,整个所占空间字节的大小
其中标签大小计算如下:
每个标签帧都有一个个字节的帧头和至少一个字节的不定长度内容,在文件中是连续存放的。标签帧头由三个部分组成,即、和。是用四个字符表示帧内容含义,比如为标题、为作者、为专辑、为音轨、为年代等等信息。用四个字节组成数表示帧大小。是标签帧的标志位,一般为即可。
2.&&&&ID3V1
是早期的版本,可以存放的信息量有限,但编程比简单很多,即使到现在使用还是很多。是固定存放在文件末尾的字节,组成结构见表。
表 387 ID3V1结构
标识符:""
其中,歌名是固定分配为个字节,如果歌名太短则以填充完整,太长则被截断,其他信息类似情况存储。音乐类别总共有种,每一种对应一个数组,比如对应""、对应""、对应""等等。
3.&&&&MP3数据帧
音频数据是文件的主体部分,它由一系列数据帧组成,每个包含一段音频的压缩数据,通过解码库解码即可得到对应音频数据,就可以通过发送到芯片播放音乐,按顺序解码所有帧就可以得到整个文件的音轨。
每个由两部分组成,帧头和数据实体,长度可能不同,由位率决定。帧头记录了数据帧的位率、采样率、版本等等信息,总共有个字节,见表。
表 388 数据帧头结构
同步信息:每位固定为:。
版本:;未定义;;。
层:未定义;;;。
校验:校验;不校验。
位率:参考表。
采样频率:
对于:;;;未定义。
对于:;;;未定义。
对于:;;;未定义。
帧长调整:用来调整文件头长度,无需调整;调整。
声道:立体声;;双声道;单声道。
扩充模式:当声道模式为时才使用。
版权:文件是否合法,不合法;合法。
原版标志:非原版;原版。
强调方式:用于声音经降噪压缩后再补偿的分类,几乎不用。
位率位在不同版本和层都有不同的定义,具体参考表,单位为。其中对应,对应和;对应,对应,对应,表示位率可变,表示该定义不合法。
表 389 位率选择
例如,当,,时,描述为位率为。
数据帧长度取决于位率和采样频率,具体计算如下:
对于标准,时:
对于或标准,时:
如果有校验,则存放在在帧头后两个字节。接下来就是帧主数据。一般来说一个文件每个帧的是固定不变的,所以每个帧都有相同的长度,称为,还有小部分文件的是可变,称之为,它是公司推出的算法。
保存的是经过压缩算法压缩后得到的数据,为得到大的压缩率会损失部分源声音,属于失真压缩。
38.7.2 MP3解码库
文件是经过压缩算法压缩而存在的,为得到信号,需要对文件进行解码,解码过程大致为:比特流分析、霍夫曼编码、逆量化处理、立体声处理、频谱重排列、抗锯齿处理、变换、子带合成、输出。整个过程涉及很多算法计算,要自己编程实现不是一件现实的事情,还好有很多公司经过长期努力实现了解码库编程。
现在合适在小型嵌入式控制器移植运行的有两个版本的开源解码库,分别为解码库和解码库,是一个高精度音频解码库,支持、以及标准,它可以提供输出,完全是定点计算,更多信息可参考网站:。
解码库支持浮点和定点计算实现,将该算法移植到控制器运行使用定点计算实现,它支持、以及标准的解码。解码库支持可变位速率、恒定位速率,以及立体声和单声道音频格式。更多信息可参考网站:。
因为解码库需要占用的资源比解码库更少,特别是空间的使用,这对控制器来说是比较重要的,所以在实验工程中我们选择解码库实现文件解码。这两个解码库都是一帧为解码单位的,一次解码一帧,这在应用解码库时是需要着重注意的。
解码库涉及算法计算,整个界面过程复杂,有兴趣可以深入探究,这里我们着重讲解移植和使用方法。
网站有提供解码库代码,经过整理,移植解码库需要用到的的文件如图。有优化解码速度,部分解码过程使用汇编实现。
图 3813 Helix解码库文件结构
38.7.3 Helix解码库移植
在"录音与回放实验"已经实现了驱动代码,现在我们可以移植解码库工程中,实现文件解码,将解码输出的数据通过接口发送到芯片实现音乐播放。
我们在"录音与回放实验"工程文件基础上移植解码,首先将需要用到的文件添加到工程中,如图。文件夹下文件是解码库源码,工程移植中是不需要修改文件夹下代码的,我们只需直接调用相关解码函数即可。建议自己移植时直接使用例程中文件夹内文件。我们是在文件中调用解码库相关函数实现文件解码的,该文件是我们自己创建的。
图 3814 添加Helix解码库文件到工程
接下来还需要在工程选项中添加解码库的文件夹路径,编译器可以寻找到相关头文件,见图。
图 3815 添加Helix解码库文件夹路径
38.7.4 MP3播放器功能实现
"录音与回放实验"中的回放功能实际上就是从卡内读取格式文件数据,然后提取里边音频数据通过传输到芯片内实现声音播放。播放器的功能也是类似的,只不过现在音频数据提取方法不同,需要先经过解码库解码后才可得到"可直接"播放的音频数据。由此可以看到,播放器只是添加了解码库实现代码,在硬件设计上并没有任何改变,即这里直接使用"录音与回放实验"中硬件设计即可。
实验工程代码中创建和两个文件存放播放器实现代码。解码库是用来解码数据帧,一次解码一帧,它是不能用来检索和标签的,如果需要获取歌名、作者等信息需要自己编程实现。解码过程可能用到的解码库函数有:
?&&&&:初始化解码器函数
?&&&&:关闭解码器函数
?&&&&:寻找帧同步函数
?&&&&:解码帧函数
?&&&&:获取帧信息函数
函数初始化解码器,它会申请分配一个存储空间用于存放解码器状态的一个数据结构并将其初始化,该数据结构由结构体定义,它封装了解码器内部运算数据信息。函数会返回指向该数据结构的指针。
函数用于关闭解码器,释放由函数申请的存储空间,所以一个函数都需要有一个函数与之对应。它有一个形参,一般由函数的返回指针赋值。
函数用于寻址数据帧同步信息,实际上就是寻址数据帧开始的都为""的同步信息。它有两个形参,第一个为源数据缓冲区指针,第二个为缓冲区大小,它会返回一个类型变量,用于指示同步字较缓冲区起始地址的偏移量,如果在缓冲区中找不到同步字,则直接返回。
函数用于解码数据帧,它有五个形参,第一个为解码器数据结构指针,一般由函数返回值赋值;第二个参数为指向解码源数据缓冲区开始地址的一个指针,注意这里是地址的指针,即是指针的指针;第三个参数是一个指向存放解码源数据缓冲区有效数据量的变量指针;第四个参数是解码后输出数据的指针,一般由我们定义的缓冲区地址赋值,对于双声道输出数据缓冲区以顺序排列;第五个参数是数据格式选择,一般设置为表示标准的格式。函数还有一个返回值,用于返回解码错误,返回说明解码正常。
函数用于获取数据帧信息,它有两个形参,第一个为解码器数据结构指针,一般由函数返回值赋值;第二个参数为数据帧信息结构体指针,该结构体定义见代码清单。
代码清单 3825 MP3数据帧信息结构体
1 typedef struct _MP3FrameInfo {
int //位率
int nC //声道数
int //采样率
int bitsPerS //采样位数
int outputS //PCM数据数
int //层级
int //版本
9 } MP3FrameI
该结构体成员包括了该数据帧的位率、声道、采样频率等等信息,它实际上是从数据帧的帧头信息中提取的。
MP3播放器实现
代码清单 3826 mp3PlayerDemo函数
1 void mp3PlayerDemo(const char *mp3file)
uint8_t *read_ptr=
uint32_t frames=0;
int err=0, i=0, outputSamps=0;
int read_offset = 0; /* 读偏移指针 */
int bytes_left = 0; /* 剩余字节数 */
mp3player.ucFreq=I2S_AudioFreq_D
mp3player.ucStatus=STA_IDLE;
mp3player.ucVolume=40;
result=f_open(&file,mp3file,FA_READ);
if (result!=FR_OK) {
printf("Open mp3file :%s fail!!!-&%d\r\n",mp3file,result);
result = f_close (&file);
return; /* 停止播放 */
printf("当前播放文件 -& %s\n",mp3file);
//初始化MP3解码器
Mp3Decoder = MP3InitDecoder();
if (Mp3Decoder==0) {
printf("初始化helix解码库设备\n");
return; /* 停止播放 */
printf("初始化中...\n");
Delay_ms(10); /* 延迟一段时间,等待I2S中断结束 */
wm8978_Reset(); /* 复位WM8978到复位状态 */
/* 配置WM8978芯片,输入为DAC,输出为耳机 */
wm8978_CfgAudioPath(DAC_ON, EAR_LEFT_ON | EAR_RIGHT_ON);
/* 调节音量,左右相同音量 */
wm8978_SetOUT1Volume(mp3player.ucVolume);
/* 配置WM8978音频接口为飞利浦标准I2S接口,16bit */
wm8978_CfgAudioIF(I2S_Standard_Phillips, 16);
/* 初始化并配置I2S */
I2S_Stop();
I2S_GPIO_Config();
I2Sx_Mode_Config(I2S_Standard_Phillips,I2S_DataFormat_16b,
mp3player.ucFreq);
I2S_DMA_TX_Callback=MP3Player_I2S_DMA_TX_C
I2Sx_TX_DMA_Init((uint16_t *)outbuffer[0],(uint16_t *)outbuffer[1],
MP3BUFFER_SIZE);
bufflag=0;
mp3player.ucStatus = STA_PLAYING; /* 放音状态 */
result=f_read(&file,inputbuf,INPUTBUF_SIZE,&bw);
if (result!=FR_OK) {
printf("读取%s失败 -& %d\r\n",mp3file,result);
MP3FreeDecoder(Mp3Decoder);
bytes_left=
/* 进入主程序循环体 */
while (mp3player.ucStatus == STA_PLAYING) {
//寻找帧同步,返回第一个同步字的位置
read_offset = MP3FindSyncWord(read_ptr, bytes_left);
//没有找到同步字
if (read_offset & 0) {
result=f_read(&file,inputbuf,INPUTBUF_SIZE,&bw);
if (result!=FR_OK) {
printf("读取%s失败 -& %d\r\n",mp3file,result);
bytes_left=
read_ptr += read_ //偏移至同步字的位置
bytes_left -= read_ //同步字之后的数据大小
if (bytes_left & 1024) { //补充数据
/* 注意这个地方因为采用的是DMA读取,所以一定要4字节对齐 */
i=(uint32_t)(bytes_left)&3; //判断多余的字节
if (i) i=4-i; //需要补充的字节
memcpy(inputbuf+i, read_ptr, bytes_left); //从对齐位置开始复制
read_ptr = inputbuf+i; //指向数据对齐位置
//补充数据
result=f_read(&file,inputbuf+bytes_left+i,
INPUTBUF_SIZE-bytes_left-i,&bw);
bytes_left += //有效数据流大小
//开始解码参数:mp3解码结构体、输入流指针、输入流大小、输出流指针、数据格式
err = MP3Decode(Mp3Decoder, &read_ptr, &bytes_left,
outbuffer[bufflag], 0);
//错误处理
if (err != ERR_MP3_NONE) {
switch (err) {
case ERR_MP3_INDATA_UNDERFLOW:
printf("ERR_MP3_INDATA_UNDERFLOW\r\n");
result = f_read(&file, inputbuf, INPUTBUF_SIZE, &bw);
read_ptr =
bytes_left =
case ERR_MP3_MAINDATA_UNDERFLOW:
/* do nothing - next call to decode will provide more mainData */
printf("ERR_MP3_MAINDATA_UNDERFLOW\r\n");
printf("UNKNOWN ERROR:%d\r\n", err);
// 跳过此帧
if (bytes_left & 0) {
bytes_left --;
read_ptr ++;
} else { //解码无错误,准备把数据输出到PCM
MP3GetLastFrameInfo(Mp3Decoder, &Mp3FrameInfo); //获取解码信息
/* 输出到DAC */
outputSamps = Mp3FrameInfo.outputS //PCM数据个数
if (outputSamps & 0) {
if (Mp3FrameInfo.nChans == 1) { //单声道
//单声道数据需要复制一份到另一个声道
for (i = outputSamps - 1; i &= 0; i--) {
outbuffer[bufflag][i * 2] = outbuffer[bufflag][i];
outbuffer[bufflag][i * 2 + 1] = outbuffer[bufflag][i];
outputSamps *= 2;
}//if (Mp3FrameInfo.nChans == 1) //单声道
}//if (outputSamps & 0)
/* 根据解码信息设置采样率 */
if (Mp3FrameInfo.samprate != mp3player.ucFreq) { //采样率
mp3player.ucFreq = Mp3FrameInfo.
printf(" \r\n Bitrate %dKbps",Mp3FrameInfo.bitrate/1000);
printf(" \r\n Samprate %dHz", mp3player.ucFreq);
printf(" \r\n BitsPerSample %db", Mp3FrameInfo.bitsPerSample);
printf(" \r\n nChans %d", Mp3FrameInfo.nChans);
printf(" \r\n Layer %d", Mp3FrameInfo.layer);
printf(" \r\n Version %d", Mp3FrameInfo.version);
printf(" \r\n OutputSamps %d", Mp3FrameInfo.outputSamps);
printf("\r\n");
//I2S_AudioFreq_Default = 2,正常的帧,每次都要改速率
if (mp3player.ucFreq &= I2S_AudioFreq_Default) {
//根据采样率修改I2S速率
I2Sx_Mode_Config(I2S_Standard_Phillips,I2S_DataFormat_16b,
mp3player.ucFreq);
I2Sx_TX_DMA_Init((uint16_t *)outbuffer[0],
(uint16_t *)outbuffer[1],outputSamps);
I2S_Play_Start();
}//else 解码正常
if (file.fptr==file.fsize) { //mp3文件读取完成,退出
printf("END\r\n");
while (Isread==0) {
I2S_Stop();
mp3player.ucStatus=STA_IDLE;
MP3FreeDecoder(Mp3Decoder);
f_close(&file);
函数是播放器的实现函数,篇幅很长,需要我们仔细分析。它有一个形参,用于指定待播放的文件,需要用文件的绝对路径加全名称赋值。
是定义的一个指针变量,它用于指示解码器源数据地址,把它初始化为用来存放解码器源数据缓冲区数组的首地址。和主要用于函数,用来指示帧同步相对解码器源数据缓冲区首地址的偏移量,用于指示解码器源数据缓冲区有效数据量。
是一个结构体类型变量,指示音量、状态和采样频率信息。
函数用于打开文件,如果文件打开失败则直接退出播放。函数用于初始化解码器,分配解码器必须内存空间,如果初始化解码器失败直接退出播放。
接下来配置芯片功能,使能耳机输出,设置音量,使用标准和数据长度。还要设置外设工作环境,同样是标准和数据长度,采样频率先使用,在运行函数获取数据帧的采样频率后需要再次修改。是一个函数名,该函数实现双缓冲区标志位切换以及指示传输完成,它在的发送传输完成中断服务函数中调用。用于初始化的发送请求,使用双缓冲区模式。用于指示当前传输的缓冲区号,用于指示传输完成。
函数从卡读取文件数据,存放在缓冲区中,变量保存实际读取到的数据的字节数。如果读取数据失败则运行函数关闭解码器后退出播放器。
接下来是循环解码帧数据并}

我要回帖

更多关于 stm32IO口输入模式 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信