Linux驱动开发|音频驱动

Posted 安迪西

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux驱动开发|音频驱动相关的知识,希望对你有一定的参考价值。

音频驱动

音频是常用到的功能,I.MX6ULL ALPHA开发板通过I.MX6ULL自带的SAI接口外接了一个 WM8960音频 DAC芯片,本文将介绍如何驱动 WM8960,并通过 WM8960芯片来完成音乐播放与录音

一、音频接口简介

1.1 音频编解码芯片

在信号处理领域,外界的声音是模拟信号,处理器能理解的是数字信号。因此处理器要听到外界的声音就涉及到模拟信号到数字信号的转换;同样处理器要向外界放出声音就涉及到数字信号到模拟信号的转换。模数和数模转换需要用到ADC/DAC芯片。

但是音频不是单单的数模或模数转换就可以了,还需要能够调节音效、对声音进行一些处理(需要 DSP 单元)、拥有统一的标准接口,方便开发等。将这些针对声音的各种要求全部叠加到 DAC 和 ADC 芯片上,就会得到一个专门用于音频的芯片,即音频编解码芯片,英文名字就是 Audio CODEC,CODEC 的本质是 ADC/DAC,因此采样率和采样位数就是衡量一款音频 CODEC 最重要的指标。

1.2 WM8960芯片介绍

WM8960 是一颗由 wolfson(欧胜)公司出品的低功耗、高质量音频编解码芯片。集成 D 类喇叭功放,每个通道可以驱动一个 1W 喇叭(8Ω )。内部集成 3 个立体声输入源,可以灵活配置,拥有一路完整的麦克风接口。 WM8960 内部 ADC 和 DAC 都为24 位。

WM8960 整体框图如下示:

– ①输入接口,立体声音频输入源,共提供了三路,为 LINPUTx/RINPUTx
– ②输出接口,输出给耳机或喇叭,SPK_LP/SPK_LN 用于连接左声道的喇叭;SPK_RP/SPK_RN 用于连接右声道的喇叭;HP_L/HP_R,用于连接耳机
– ③数字音频接口,用于和主控制器连接,共有 5 根线,支持 I2S 格式
– ④控制接口,是一个标准的 I2C 接口, 用于配置 WM8960

1.3 I2S总线接口

I2S (Inter-IC Sound) 总线是飞利浦公司提出的一种用于数字音频设备之间进行音频数据传输的总线。和 I2C、 SPI 这些常见的通信协议一样, I2S 总线用于主控制器和音频 CODEC 芯片之间传输音频数据。I2S 接口需要 3 根信号线(如果需要实现收和发,那么就要 4 根信号线,收和发分别使用一根信号线):

  • SCK: 串行时钟信号,也叫位时钟(BCLK),音频数据的每一位数据都对应一个 SCK,立体声都是双声道的,因此 SCK=2×采样率×采样位数

比如采样率为 44.1KHz、 16 位的立体声音频,那么 SCK=2× 44100× 16=1411200Hz=1.4112MHz

  • WS: 字段(声道)选择信号,也叫做帧时钟(LRCK),用于切换左右声道数据,为“1”表示正在传输左声道的数据,为“0”表示正在传输右声道的数据

WS 的频率等于采样率,比如采样率为 44.1KHz 的音频, WS=44.1KHz

  • SD: 串行数据信号,也就是实际的音频数据,若要同时实现放音和录音,就需
    要 2 根数据线

比如 WM8960 的 ADCDAT 和 DACDAT,就是分别用于录音和放音

不管音频数据是多少位的,数据的最高位都是最先传输的。数据的最高位总是出现在一帧开始后(LRCK变化)的第 2 个 SCK 脉冲处。另外,有时为了使音频 CODEC 芯片与主控制器之间能够更好的同步,会引入另外一个叫做 MCLK 的信号,也叫做主时钟或系统时钟,一般是采样率的 256 倍或 384 倍


随着技术的发展,在统一的 I2S 接口下,出现了不同的数据格式,根据 DATA 数据相对于 LRCK 和 SCLK 位置的不同,出现了 Left Justified (左对齐) 和 Right Justified (右对齐) 两种格式,时序图如下示:

1.4 SAI接口简介

I.MX6ULL 提供一个叫做 SAI 的外设,全称为 Synchronous Audio Interface,即同步音频接口。SAI 是一个全双工、支持帧同步的串行接口,支持 I2S、AC97、TDM 和音频 DSP,主要特性如下:

– 帧最大为 32 个字
– 字大小可选择 8bit 或 32bit
– 每个接收和发送通道拥有 32× 32bit 的 FIFO
– FIFO 错误以后支持平滑重启

I.MX6ULL 的 SAI 框图如下示,其中 SAI_TX/RX 开头的就是 SAI 外设提供给外部连接音频 CODEC 的信号线

二、音频驱动实验

正点原子 ALPHA 开发板音频原理图如下示:

上图中重点关注 SAI 和 I2C 这两个接口:

– SAI 接口共用到 6 根数据线,用于 I.MX6ULL 与 WM8960 之间的音频数据收发
– I2C接口连接到了 I.MX6ULL 的 I2C2 上,用于配置 WM8960

NXP 官方已经写好了 WM8960 驱动,因此直接配置内核使能 WM8960 驱动即可,按照如下所示步骤使能 WM8960 驱动

2.1 修改设备树
  • wm8960 i2c 接口设备树:在设备树中的“i2c2”节点下添加 wm8960 信息
/* 可参考绑定手册Documentation/devicetree/bindings/sound/wm8960.txt添加 */
codec: wm8960@1a 
	//全局搜索该属性就可找到WM8960的驱动文件sound/soc/coders/wm8960.c
	compatible = "wlf,wm8960";	
	reg = <0x1a>;  //设置WM8960的I2C地址
	clocks = <&clks IMX6UL_CLK_SAI2>;  //指定时钟源为SAI2
	clock-names = "mclk";	//指定时钟名字为mclk
	wlf,shared-lrclk;
;
  • I.MX6ULL SAI 音频接口设备树

imx6ull.dtsi 文件中会有关于 SAI 相关接口的描述,这部分是 NXP 原厂编写的,无需任何修改, SAI2 的设备子节点内容如下所示

sai2: sai@0202c000 
	compatible = "fsl,imx6ul-sai",
				 "fsl,imx6sx-sai";
	reg = <0x0202c000 0x4000>;
	interrupts = <GIC_SPI 98 IRQ_TYPE_LEVEL_HIGH>;
	clocks = <&clks IMX6UL_CLK_SAI2_IPG>,
			 <&clks IMX6UL_CLK_DUMMY>,
			 <&clks IMX6UL_CLK_SAI2>,
			 <&clks 0>, <&clks 0>;
	clock-names = "bus", "mclk0", "mclk1", "mclk2", "mclk3";
	dma-names = "rx", "tx";
	dmas = <&sdma 37 24 0>, <&sdma 38 24 0>;
	status = "disabled";
;

打开具体的设备树文件,向 sai2 节点里追加或修改相应属性值

&sai2 
	pinctrl-names = "default";
	pinctrl-0 = < &pinctrl_sai2
				  &pinctrl_sai2_hp_det_b >;
	assigned-clocks = < &clks IMX6UL_CLK_SAI2_SEL >,
					  < &clks IMX6UL_CLK_SAI2 >;
	assigned-clock-parents = < &clks IMX6UL_CLK_PLL4_AUDIO_DIV >;
	assigned-clock-rates = < 0 >, < 12288000 >;
	status = "okay";
;

SAI2 接口的 IO 设置: pinctrl_sai2 描述的是 SAI2 接口的 IO 配置;pinctrl_sai2_hp_det_b 描述的是耳机插入检测引脚

pinctrl_sai2: sai2grp 
	fsl,pins = <
		MX6UL_PAD_JTAG_TDI__SAI2_TX_BCLK 0x17088
		MX6UL_PAD_JTAG_TDO__SAI2_TX_SYNC 0x17088
		MX6UL_PAD_JTAG_TRST_B__SAI2_TX_DATA 0x11088
		MX6UL_PAD_JTAG_TCK__SAI2_RX_DATA 0x11088
		MX6UL_PAD_JTAG_TMS__SAI2_MCLK 0x17088
	>;
;

pinctrl_sai2_hp_det_b: sai2_hp_det_grp 
	fsl,pins = <
		MX6ULL_PAD_SNVS_TAMPER4__GPIO5_IO04 0x17059
	>;
;
  • I.MX6ULL sound 节点:根节点“/”下创建一个名为“sound”的子节点,NXP 官方已经针对 EVK 开发板编写了 sound 节点,可在此基础上进行修改
sound 
	//用于匹配驱动文件,搜索以下属性值就可以找到对应的
	//驱动文件:sound/soc/fsl/imx-wm8960.c
	compatible = "fsl,imx6ul-evk-wm8960",
				 "fsl,imx-audio-wm8960";
	model = "wm8960-audio";	//最终用户看到的此声卡名字
	cpu-dai = <&sai2>;	//DAI(Digital Audio Interface)句柄,即sai2这个节点
	audio-codec = <&codec>;	//音频解码芯片句柄,为“codec”这个节点
	asrc-controller = <&asrc>;	//asrc 控制器,异步采样频率转化器
	codec-master;
	gpr = <&gpr 4 0x100000 0x100000>;
	/*
	 * hp-det = <hp-det-pin hp-det-polarity>;
	 * hp-det-pin: JD1 JD2 or JD3
	 * hp-det-polarity = 0: hp detect high for headphone
	 * hp-det-polarity = 1: hp detect high for speaker
	 */
	//耳机插入检测引脚设置,第一个参数为检测引脚, 3表示JD3为检测引脚
	//第二个参数设置检测电平,设为0时,hp检测到高电平表示耳机插入;
	//设为1时,hp检测到高电平表示是喇叭,也就是耳机拔出了
	hp-det = <3 0>;	
	/* hp-det-gpios = <&gpio5 4 0>;
	   mic-det-gpios = <&gpio5 4 0>; */
	//音频器件一系列的连接设置,每个条目都是一对字符串
	//第一个字符串是连接的 sink,第二个是连接的 source(源)
	audio-routing =
		"Headphone Jack", "HP_L",
		"Headphone Jack", "HP_R",
		"Ext Spk", "SPK_LP",
		"Ext Spk", "SPK_LN",
		"Ext Spk", "SPK_RP",
		"Ext Spk", "SPK_RN",
		"LINPUT2", "Mic Jack",
		"LINPUT3", "Mic Jack",
		"RINPUT1", "Main MIC",
		"RINPUT2", "Main MIC",
		"Mic Jack", "MICB",
		"Main MIC", "MICB",
		"CPU-Playback", "ASRC-Playback",
		"Playback", "CPU-Playback",
		"ASRC-Capture", "CPU-Capture",
		"CPU-Capture", "Capture";
;
2.2 使能内核WM8960驱动

设备树配置完成以后就可以使能内核自带的 WM8960 驱动了,直接通过图形化界面配置即可,输入make menuconfig命令打开 linux 内核的图形化配置界面

  • 取消 ALSA 模拟 OSS API:按如下路径取消 ALSA 模拟 OSS API
-> Device Drivers
	-> Sound card support (SOUND [=y])
		-> Advanced Linux Sound Architecture (SND [=y])
			-> <> OSS Mixer API 				//不选择
			-> <> OSS PCM (digital audio) API 	//不选择
  • 使能 I.MX6ULL 的 WM8960 驱动:按如下路径使能 WM8960 驱动
-> Device Drivers
	-> Sound card support (SOUND [=y])
		-> Advanced Linux Sound Architecture (SND [=y])
			-> ALSA for SoC audio support (SND_SOC [=y])
				-> SoC Audio for Freescale CPUs
					-> <*> Asynchronous Sample Rate Converter (ASRC) module support //选中
					-> <*> SoC Audio support for i.MX boards with wm8960 //选中
2.3 重新编译内核和设备树

驱动使能以后重新编译 linux 内核,编译完成以后使用新的 zImage 和.dtb 文件启动,若设备树和驱动都使能的话系统启动过程中就会有如下图所示的 log 信息:

系统启动后会打印出 ALSA 设备列表,现在的音频 CODEC 驱动基本都是 ALSA 架构的,因此在 ALSA 设备列表中就会找到 “wm8960-audio” 这个声卡。进入系统以后查看一下/dev/snd 目录,有如下所示文件:

这些文件就是ALSA音频驱动框架对应的设备文件,其作用如下:

– controlC0:用于声卡控制, C0 表示声卡 0
– pcmC0D0c 和 pcmC0D1c: 用于录音的 pcm 设备,其中的“COD0”和“C0D1”分别表示声卡 0 中的设备 0 和设备 1,最后面的“c”是 capture 的缩写,表示录音
– pcmC0D0p 和 pcmC0D1p:用于播放的 pcm 设备,其中的“COD0”和“C0D1”分别表示声卡 0 中的设备 0 和设备 1,最后面的“p”是 playback 的缩写,表示放音
– timer: 定时器

2.4 alsa-lib 和 alsa-utils 移植

音频驱动使能以后还不能直接播放音乐或录音,还需要移植 alsa-lib 和 alsa-utils

  • alsa-lib 和 alsa-utils 源码下载
  • alsa-lib 移植:ALSA 相关库文件,应用程序通过调用库来对 ALSA 框架下的声卡进行操作

在Ubuntu和开发板中创建一个路径和名字完全一样的目录:/usr/share/arm-alsa,方便交叉编译时的绝对路径引用

mkdir /usr/share/arm-alsa

在 /home/andyxi/linux/tool 目录下创建“alsa-lib”目录,用来保存 alsa-lib 的编译结果

mkdir alsa-lib

将下载好的 alsa-lib 源码拷贝到 /home/andyxi/linux/tool 目录下解压,并进行配置编译安装

//解压 alsa-lib
tar -vxjf alsa-lib-1.2.2.tar.bz2 
//进入 alsa-lib 源码目录
cd alsa-lib-1.2.2/ 
//配置:--with-configdir用于设置 alsa-lib 编译出来的配置文件存放位置
./configure --host=arm-linux-gnueabihf 
--prefix=/home/andyxi/linux/tool/alsa-lib 
--with-configdir=/usr/share/arm-alsa
//编译
make
//安装
sudo make install

可能会出现提示 libatopology.la 编译失败,是因为 sudo 会切换到 root 用户下,但此时 root 用户下的环境变量中没有交叉编译器路径,因此会提示找不到“arm-linux-gnueabihf-gcc”

//解决方法就是先切换到 root 用户,重新执行一下/etc/profile文件
sudo -s 				//切换到 root 用户,但使用当前用户本身的环境
source /etc/profile 	//执行/etc/profile
make install 			//安装,此时已经工作在 root 下,因此不需要加“sudo”
su andyxi	 			//编译完成以后回原来的用户

编译完成以后前面创建的“alsa-lib”目录就会保存相应的编译结果,将lib目录下的所有文件拷贝到开发板根文件系的 /usr/lib 目录下;并将 /usr/share/arm-alsa 目录下的所有文件拷贝到开发板的/usr/share/arm-alsa 目录下

cd alsa-lib 	
sudo cp lib/* /home/zuozhongkai/linux/nfs/rootfs/lib/ -af
cd /usr/share/arm-alsa 
sudo cp * /home/andyxi/linux/nfs/rootfs/usr/share/arm-alsa/ -raf
  • alsa-utils 移植:ALSA 的一些小工具集合,可以通过这些小工具测试声卡

在 /home/andyxi/linux/tool 目录下创建“alsa-utils”目录,用来保存 alsa-utils 的编译结果

mkdir alsa-utils

将下载好的 alsa-utils 源码拷贝到 /home/andyxi/linux/tool 目录下解压,并进行配置编译安装(为了避免配置不成功,需切换到 root 用户)

tar -vxjf alsa-utils-1.2.2.tar.bz2	//解压
cd alsa-utils-1.2.2/	//进入 alsa-utils 源码目录
sudo -s 				//切换到 root 用户
source /etc/profile 	//执行/etc/profile
//配置
/configure --host=arm-linux-gnueabihf 
--prefix=/home/andyxi/linux/tool/alsa-utils 
--with-alsa-inc-prefix=/home/andyxi/linux/tool/alsa-lib/include/ 
--with-alsa-prefix=/home/andyxi/linux/tool/alsa-lib/lib/ 
--disable-alsamixer --disable-xmlto
make					//编译
make install			//安装,此时已经工作在 root 下,因此不需要加“sudo”
su andyxi	 			//编译完成以后回原来的用户

编译完成后就会在前面创建的“alsa-utils”目录下生成 bin、 sbin 和 share 三个文件夹,将其分别拷贝至开发板根目录下的/bin、 /sbin 和/usr/share/alsa 目录下

cd alsa-utils
sudo cp bin/* /home/andyxi/linux/nfs/rootfs/bin/ -rfa
sudo cp sbin/* /home/andyxi/linux/nfs/rootfs/sbin/ -rfa
sudo cp share/* /home/andyxi/linux/nfs/rootfs/usr/share/ -rfa

在开发板根文件系统中的/etc/profile 文件中加入如下内容,来指定 alsa 的配置文件

export ALSA_CONFIG_PATH=/usr/share/arm-alsa/alsa.conf

三、音频驱动测试

3.1 声卡设置与测试

amixer 使用方法

  • 查看帮助信息:声卡相关选项默认都是关闭的,因此在使用前要先设置声卡, alsa-utils 自带了 amixer 这个声卡设置工具。输入amixer --help命令即可查看 amixer的帮助信息

上图可以看出,amixer 命令分为前缀“s”和“c”各一组,这两组的基本功能都是一样的,只不过“s”开头的是 simple(简单)组,一般使用简化版

  • 查看设置项:使用amixer scontrolsamixer controls命令查看对应的设置项

  • 查看设置值:不同的设置项的设置值类型不同,例如使用amixer scontents命令查看scontents对应的设置值

上图可以看出,“Headphone”项目就是设置耳机音量的,范围 0-127,当前音量为 0。有些设置项是 bool 类型,只有 on 和 off 两种状态

  • 设置声卡:知道了设置项和设置值,就可以设置声卡了
amixer sset 设置项目 设置值
amixer cset 设置项目 设置值
  • 获取声卡设置值:读取当前声卡某项设置值使用如下命令
amixer sget 设置项目
amixer cget 设置项目
3.2 音乐播放测试
  • 使用 amixer 设置声卡:打开耳机和喇叭,并且设置喇叭和耳机音量
amixer sset Headphone 100,100
amixer sset Speaker 120,120
amixer sset 'Right Output Mixer PCM' on
amixer sset 'Left Output Mixer PCM' on
  • 使用 aplay 播放 WAV 格式音乐:aplay 是 alsa-utils 提供的,可播放 wav 格式的音乐;在开发板根文件系统下创建名为“music”的目录并存放 wav 格式的音频文件,然后输入如下命令播放:
aplay test.wav //播放歌曲
3.3 MIC 录音测试

ALPHA 开发板上有一个麦克风,可以用来完成录音测试

  • 使用 amixer 设置声卡:在开发板根文件系统的/music 目录下创建一个名为“mic_in_config.sh”的 shell 脚本,输入声卡的设置命令
### mic_in_config.sh 脚本内容
---------------------------------
#!/bin/sh

#设置捕获的音量
amixer cset name='Capture Volume' 90,90
#PCM
amixer sset 'PCM Playback' on
amixer sset 'Playback' 256
amixer sset 'Right Output Mixer PCM' on
amixer sset 'Left Output Mixer PCM' on
#ADC PCM
amixer sset 'ADC PCM' 200
#耳机/喇叭(扬声器)设置播放音量,直流/交流
#Turn on Headphone
amixer sset 'Headphone Playback ZC' on
#Set the volume of your headphones(98% volume, 127 is the MaxVolume)
amixer sset Headphone 125,125
#Turn on the speaker
amixer sset 'Speaker Playback ZC' on
#Set the volume of your Speaker(98% volume, 127 is the MaxVolume)
amixer sset Speaker 125,125
#Set the volume of your Speaker AC(80% volume, 100 is the MaxVolume)
amixer sset 'Speaker AC' 4
#Set the volume of your Speaker AC(80% volume, 5 is the MaxVolume)
amixer sset 'Speaker DC' 4
#音频输入,左声道管理
#Turn on Left Input Mixer Boost
amixer sset 'Left Input Mixer Boost' off
amixer sset 'Left Boost Mixer LINPUT1' off
amixer sset 'Left Input Boost Mixer LINPUT1' 0
amixer sset 'Left Boost Mixer LINPUT2' off
amixer sset 'Left Input Boost Mixer LINPUT2' 0
#Turn off Left Boost Mixer LINPUT3
amixer sset 'Left Boost Mixer LINPUT3' off
amixer sset 'Left Input Boost Mixer LINPUT3' 0
#音频输入,右声道管理,全部关闭
#Turn on Right Input Mixer Boost
amixer sset 'Right Input Mixer Boost' on
amixer sset 'Right Boost Mixer RINPUT1' off
amixer sset 'Right Input Boost Mixer RINPUT2' 0
amixer sset 'Right Boost Mixer RINPUT2' on
amixer sset 'Right Input Boost Mixer RINPUT2' 127
amixer sset 'Right Boost Mixer RINPUT3' off
amixer sset 'Right Input Boost Mixer RINPUT3' 0

给予 mic_in_config.sh 可执行权限并运行,命令如下:

chmod 777 mic_in_config.sh 		//给予可执行权限
./mic_in_config.sh 				//运行
  • 使用 arecord 录制音频:arecord 是 alsa-utils 提供的,输入如下命令录制音频
arecord -f cd -d 10 record.wav
//-f 设置录音质量
//cd 表示录音质量为 cd 级别
//-d 指定录音时间,单位是s
//音频名字为 record.wav
//录制完成后可使用aplay播放录制的record.wav音频
  • 单声道 MIC 录制立体声音频:上面 MIC 录出来的只有左声道有声音,配置 WM8960的右声道 ADC 直接使用左声道的数据,这样左右声道就共同使用一个 MIC,录出来的音频就是双声道的
//打开 linux 内核里面的 wm8960.c 这个文件,找到 wm8960_reg_defaults 数组
//此数组保存着 wm8960 的默认配置值,为<寄存器地址,值>的形式
//R23寄存器地址为0X17,将该寄存器的值改为0x01C4
static const struct reg_default wm8960_reg_defaults[] = 
	 0x0, 0x00a7 ,
	......
	 0x16, 0x00c3 ,
	/* 0x17, 0x01c0 ,*/
	 0x17, 0x01c4 ,
	 0x18, 0x0000 ,
	......
	 0x37, 0x00e9 ,
;
//修改完成后重新编译内核,使用新的内核启动开发板,重新测试MIC录音
3.4 LINE IN 录音测试

ALPHA 开发板的 LINE IN 接了双声道,所以不需要共用左声道数据,不需要更改R23寄存器的默认值。 LINE IN 即线路输入

  • 使用 amixer 设置声卡:/music 下创建名为“line_in_config.sh”的 shell 脚本,输入声卡的设置命令
#!/bin/sh
#设置捕获的音量
amixer cset name='Capture Volume' 100,100
#PCM
amixer sset 'PCM Playback' on
amixer sset 'Playback' 256
amixer sset 'Right Output Mixer PCM' on
amixer sset 'Left Output Mixer PCM' on
#ADC PCM
amixer sset 'ADC PCM' 200
#录音前应该设置耳机或者扬声器的音量为 0(下面并没有设置)防止干扰
#耳机/喇叭(扬声器)设置播放音量,直流/交流
#Turn on Headphone
amixer sset 'Headphone Playback ZC' on
#Set the volume of your headphones(98% volume, 127 is the MaxVolume)
amixer sset Headphone 125,125
#Turn on the speaker
amixer sset 'Speaker Playback ZC' on
#Set the volume of your Speaker(98% volume, 127 is the MaxVolume)
amixer sset Speaker 125,125
#Set the volume of your Speaker AC(80% volume, 100 is the MaxVolume)
amixer sset 'Speaker AC' 4
#Set the volume of your Speaker AC(80% volume, 5 is the MaxVolume)
amixer sset 'Speaker DC' 4
#音频输入,左声道管理
#Turn off Left Input Mixer Boost
amixer sset 'Left Input Mixer Boost' on
#关闭其他通道输入
amixer sset 'Left Boost Mixer LINPUT1' off
amixer sset 'Left Input Boost Mixer LINPUT1' 0
#关闭麦克风左声道输入
amixer sset 'Left Boost Mixer LINPUT2' on
amixer sset 'Left Input Boost Mixer LINPUT2' 127
#Line_in 右声道输入关闭
amixer sset 'Left Boost Mixer LINPUT3' off
amixer sset 'Left Input Boost Mixer LINPUT3' 0
#音频输入,右声道管理
#Turn on Right Input Mixer Boost
amixer sset 'Right Input Mixer Boost' on
amixer sset 'Right Boost Mixer RINPUT1' off
amixer sset 'Right Input Boost Mixer RINPUT1' 0
amixer sset 'Right Boost Mixer RINPUT2' off
amixer sset 'Right Input Boost Mixer RINPUT2' 0
#要想设置成音频输入, 请打开 RINPUT3,看原理图可知
#其他的声道通过上面的配置可关闭,这样是为了避免干扰,需要的时候就打开
#RINPUT3 打开(关键点)
amixer sset 'Right Boost Mixer RINPUT3' on
amixer sset 'Right Input Boost Mixer RINPUT3' 127

给予 line_in_config.sh 可执行权限并运行,命令如下:

chmod 777 line_in_config.sh 		//给予可执行权限
./line_in_config.sh 				//运行
  • 使用 arecord 录制音频:arecord 是 alsa-utils 提供的,输入如下命令录制音频
arecord -f cd -d 10 record.wav
//-f 设置录音质量
//cd 表示录音质量为 cd 级别
//-d 指定录音时间,单位是s
//音频名字为 record.wav
//录制完成后可使用aplay播放录制的record.wav音频
//LINE IN接了左右双声道,因此录制出来的音频是立体声的
3.5 开机自动配置声卡

开发板重启后声卡的所有设置都会消失,必须重新设置声卡。下面介绍如何保存声卡的设置

  • 使用 alsactl 保存声卡设置:声卡设置的保存通过 alsactl 来完成,是 alsa-utils 提供的工具

alsactl 默认将声卡配置文件保存在 /var/lib/alsa 目录下,因此在开发板根文件系统下创建 /var/lib/alsa 目录

mkdir /var/lib/alsa -p

使用 amixer 设置声卡后,输入如下命令保存声卡设置

alsactl -f var/lib/alsa/asound.state store 	//保存声卡设置
//-f 指定声卡配置文件
//store 表示保存

保存成功后就会生成/var/lib/alsa/asound.state 文件,里面是关于声卡的各种设置信息

如果要使用 asound.state 中的配置信息来配置声卡,执行如下命令:

alsactl -f var/lib/alsa/asound.state restore

打开/etc/init.d/rcS 文件,在最后面追加如下内容

if [ -f "/var/lib/alsa/asound.state" ]; then
		echo "ALSA: Restoring mixer setting......"
		/sbin/alsactl -f var/lib/alsa/asound.state restore &
fi

设置完成后重启开发板,就会自动设置声卡,会输入如下图示内容

最后使用 aplay 播放音乐测试声卡开机自动配置是否正确

Linux驱动开发-编写VS1053芯片音频驱动

1. 前言

VS1053是一款硬件编解码的音频芯片,提供SPI接口和IIS接口两种通信协议,这篇文章是介绍在Linux下如果模拟SPI时序来操作VS1053完成录音、播放音频歌曲功能。但是没有注册标准的音频驱动,没有对接音频框架,只是在驱动层完成VS1053的直接控制,本篇的重点主要是介绍如何初始化开发板的GPIO口,使用Linux的延时函数,模拟SPI时序,代码写了两种版本,一种是直接通过ioremap直接映射GPIO口地址,完成配置,一种是直接调用官方内核提供的库函数接口,完成GPIO口初始化,控制。

当前采用的开发板是友善之臂的Tiny4412,芯片是三星的EXYNOS4412,这款芯片出来有很长一段时间了,之前用在三星的S系列手机上的,最高主频是1.5GZ,稳定推荐主频是1.4GHZ,内核是三星提供的demon,友善之臂在基础上完成了移植适配,也就是现在拿到的Tiny4412开发板内核,Linux 版本是3.5,不支持设备树。

2. VS1053硬件介绍

VS1053这款编码解码芯片在单片机里用的较多,性价比很高,因为支持SPI接口,所以单片机操作起来也比较容易,编码解码都是芯片内部完成,不消耗CPU资源,芯片的电压支持是3.3V。

可以使用VS1053设计MP3播放器,比如:用在跑步机上听歌,用在便携式音箱里放歌,做复读机、录音笔 等等。

解码的音频格式支持: MP3、OGG、WMA、WAV、MIDI、AAC、FLAC(需要加载 patch)

编码的音频格式支持: WAV(PCM/IMA ADPCM)、OGG(需要加载 patch)

VS1053使用的12.288M 的晶振, 在12.288MHz时钟下,最高到48000HZ的所有采样率都可以正常使用。

当前我采用的VS1053是正点原子设计的完整模块,方便杜邦线与开发板进行测试。

模块引出的接口功能: 这是SPI接口引脚

下面是SPI接口硬件的功能描述:

SPI读时序:

SPI写时序:

VS1053模块与单片机之间的连线图:

3. 驱动代码

3.1 驱动端代码

#include <linux/init.h>
#include <linux/module.h>
#include <linux/ioctl.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/err.h>
#include <linux/list.h>
#include <linux/errno.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/compat.h>
#include <linux/spi/spi.h>
#include <linux/spi/spidev.h>
#include <asm/uaccess.h>
#include <linux/gpio.h>
#include <mach/gpio.h>
#include <plat/gpio-cfg.h>
#include <linux/delay.h>
#include "mp3_data.h"
#include <linux/miscdevice.h>   /*杂项字符设备头文件*/

#define VS_WRITE_COMMAND 	0x02  //写命令
#define VS_READ_COMMAND 	0x03  //读命令

//VS10XX寄存器定义
#define SPI_MODE        	0x00   
#define SPI_STATUS      	0x01   
#define SPI_BASS        	0x02   
#define SPI_CLOCKF      	0x03   
#define SPI_DECODE_TIME 	0x04   
#define SPI_AUDATA      	0x05   
#define SPI_WRAM        	0x06   
#define SPI_WRAMADDR    	0x07   
#define SPI_HDAT0       	0x08   
#define SPI_HDAT1       	0x09 
  
#define SPI_AIADDR      	0x0a   
#define SPI_VOL         	0x0b   
#define SPI_AICTRL0     	0x0c   
#define SPI_AICTRL1     	0x0d   
#define SPI_AICTRL2     	0x0e   
#define SPI_AICTRL3     	0x0f   
#define SM_DIFF         	0x01   
#define SM_JUMP         	0x02   
#define SM_RESET        	0x04   
#define SM_OUTOFWAV     	0x08   
#define SM_PDOWN        	0x10   
#define SM_TESTS        	0x20   
#define SM_STREAM       	0x40   
#define SM_PLUSV        	0x80   
#define SM_DACT         	0x100   
#define SM_SDIORD       	0x200   
#define SM_SDISHARE     	0x400   
#define SM_SDINEW       	0x800   
#define SM_ADPCM        	0x1000   
#define SM_ADPCM_HP     	0x2000 		 

#define I2S_CONFIG			 0XC040
#define GPIO_DDR			 0XC017
#define GPIO_IDATA			 0XC018
#define GPIO_ODATA			 0XC019

/*
Tiny4412与VS1053硬件连接:
	VCC--3V~5V
	GND--0V
	SCK---SCLK:GPB_0
	SI---MOSI:GPB_3
	SO---MISO:GPB_2
	XCS--CS  :GPB_1
	DREQ-----:GPB_5
	XDCS-----:GPB_4
	RST------:GPB_6
*/
void VS1053_Init(void);
u16  VS1053_ReadReg(u8 address);				//读寄存器
u16  VS1053_ReadRAM(u16 addr);	    	  		//读RAM
void VS1053_WriteRAM(u16 addr,u16 val);			//写RAM
void VS1053_WriteData(u8 data);				  	//写数据
void VS1053_WriteCmd(u8 address,u16 data);		//写命令
u8   VS1053_Reset(void);			    		//硬复位
void VS1053_SoftReset(void);           			//软复位
u8 	 VS1053_SPI_ReadWriteByte(u8 data);     	//SPI接口,读写一个字节 
void VS1053_SoftReset(void);					//初始化VS1053	 
u8 	 VS1053_SendMusicData(u8* buf);				//向VS10XX发送32字节 
void VS1053_SetVol(u8 volx);				  	//设置主音量   


/*
函数功能:移植接口--SPI时序读写一个字节
函数参数:data:要写入的数据
返 回 值:读到的数据
*/
u8 VS1053_SPI_ReadWriteByte(u8 tx_data)
			  	 
   u8 rx_data=0;				 
   u8 i;
   for(i=0;i<8;i++)
	
		gpio_set_value(EXYNOS4_GPB(0), 0);  
		if(tx_data&0x80)gpio_set_value(EXYNOS4_GPB(3), 1);
		else gpio_set_value(EXYNOS4_GPB(3), 0);
		tx_data<<=1;	
		gpio_set_value(EXYNOS4_GPB(0), 1);
		rx_data<<=1;
		if(gpio_get_value(EXYNOS4_GPB(2)))rx_data|=0x01;
	
	return rx_data; 



/*
函数功能:软复位VS10XX
*/
void VS1053_SoftReset(void)
	 
	u8 retry=0;  				   
	while(gpio_get_value(EXYNOS4_GPB(5))==0); //等待软件复位结束	   
	VS1053_SPI_ReadWriteByte(0Xff);			  //启动传输
	retry=0;
	while(VS1053_ReadReg(SPI_MODE)!=0x0800)	  // 软件复位,新模式  
	
		VS1053_WriteCmd(SPI_MODE,0x0804);	  // 软件复位,新模式	    
		msleep(2);//等待至少1.35ms 
		if(retry++>100)break; 	  
		
	while(gpio_get_value(EXYNOS4_GPB(5))==0); //等待软件复位结束	 
	retry=0;
	while(VS1053_ReadReg(SPI_CLOCKF)!=0X9800) //设置VS10XX的时钟,3倍频 ,1.5xADD 
	
		VS1053_WriteCmd(SPI_CLOCKF,0X9800);	  //设置VS10XX的时钟,3倍频 ,1.5xADD
		if(retry++>100)break; 	    
		 
	msleep(20);



/*
函数 功 能:硬复位MP3
函数返回值:1:复位失败;0:复位成功	
*/
u8 VS1053_Reset(void)

	u8 retry=0;
	gpio_set_value(EXYNOS4_GPB(6), 0);
	msleep(20);
	gpio_set_value(EXYNOS4_GPB(4), 1);//取消数据传输
	gpio_set_value(EXYNOS4_GPB(1), 1); //取消数据传输
	gpio_set_value(EXYNOS4_GPB(6), 1);	   
	while(gpio_get_value(EXYNOS4_GPB(5))==0&&retry<200)//等待DREQ为高
	
		retry++;
		udelay(50);
	;
	msleep(20);	
	if(retry>=200)return 1;
	else return 0;	    		 



/*
函数功能:向VS10XX写命令
函数参数:
				address:命令地址
				data   :命令数据
*/
void VS1053_WriteCmd(u8 address,u16 data)
  
	while(gpio_get_value(EXYNOS4_GPB(5))==0);	//等待空闲		   	   
	gpio_set_value(EXYNOS4_GPB(4), 1); 	 
	gpio_set_value(EXYNOS4_GPB(1), 0); 	 
	VS1053_SPI_ReadWriteByte(VS_WRITE_COMMAND);//发送VS10XX的写命令
	VS1053_SPI_ReadWriteByte(address); 			//地址
	VS1053_SPI_ReadWriteByte(data>>8); 			//发送高八位
	VS1053_SPI_ReadWriteByte(data);	 			//第八位
	gpio_set_value(EXYNOS4_GPB(1), 1);            
 


/*
函数参数:向VS1053写数据
函数参数:data:要写入的数据
*/
void VS1053_WriteData(u8 data)

	gpio_set_value(EXYNOS4_GPB(4), 0);   
	VS1053_SPI_ReadWriteByte(data);
	gpio_set_value(EXYNOS4_GPB(4), 1);      



/*
函数功能:读VS1053的寄存器 
函数参数:address:寄存器地址
返回值:读到的值
*/
u16 VS1053_ReadReg(u8 address)
 
	u16 temp=0;   	
    while(gpio_get_value(EXYNOS4_GPB(5))==0);//非等待空闲状态   	
	gpio_set_value(EXYNOS4_GPB(4), 1);       
	gpio_set_value(EXYNOS4_GPB(1), 0);        
	VS1053_SPI_ReadWriteByte(VS_READ_COMMAND);//发送VS10XX的读命令
	VS1053_SPI_ReadWriteByte(address);       	//地址
	temp=VS1053_SPI_ReadWriteByte(0xff); 		  //读取高字节
	temp=temp<<8;
	temp+=VS1053_SPI_ReadWriteByte(0xff); 		//读取低字节
	gpio_set_value(EXYNOS4_GPB(1), 1);      
   return temp; 
  


/*
函数功能:读取VS1053的RAM
函数参数:addr:RAM地址
返 回 值:读到的值
*/
u16 VS1053_ReadRAM(u16 addr) 
 
	u16 res;			   	  
 	VS1053_WriteCmd(SPI_WRAMADDR, addr); 
	res=VS1053_ReadReg(SPI_WRAM);  
 	return res;
 


/*
函数功能:写VS1053的RAM
函数参数:
		addr:RAM地址
		val:要写入的值 
*/
void VS1053_WriteRAM(u16 addr,u16 val) 
  		   	  
 	VS1053_WriteCmd(SPI_WRAMADDR,addr);	//写RAM地址 
	while(gpio_get_value(EXYNOS4_GPB(5))==0); 							//等待空闲	   
	VS1053_WriteCmd(SPI_WRAM,val); 			//写RAM值 
 


/*
函数参数:发送一次音频数据,固定为32字节
返 回 值:0,发送成功
				  1,本次数据未成功发送   
*/ 
u8 VS1053_SendMusicData(u8* buf)

	u8 n;
	if(gpio_get_value(EXYNOS4_GPB(5))!=0)  //送数据给VS10XX
				   	 
		gpio_set_value(EXYNOS4_GPB(4), 0);  
		for(n=0;n<32;n++)
		
			VS1053_SPI_ReadWriteByte(buf[n]);	 			
		
		gpio_set_value(EXYNOS4_GPB(4), 1);     				   
	else return 1;
	return 0;//成功发送了



/*
函数功能:设定VS1053播放的音量
函数参数:volx:音量大小(0~254)
*/
void VS1053_SetVol(u8 volx)

    u16 volt=0; 			      //暂存音量值
    volt=254-volx;			      //取反一下,得到最大值,表示最大的表示 
	volt<<=8;
    volt+=254-volx;				  //得到音量设置后大小
    VS1053_WriteCmd(SPI_VOL,volt);//设音量 



/*
函数功能:VS1053初始化
Tiny4412硬件连接:
	VCC--3V~5V
	GND--0V
	SCK---SCLK:GPB_0
	SI---MOSI:GPB_3
	SO---MISO:GPB_2
	XCS--CS  :GPB_1
	DREQ-----:GPB_5
	XDCS-----:GPB_4
	RST------:GPB_6
*/
void VS1053SpiInit(void)

	/*1. 注册GPIO*/
	gpio_request(EXYNOS4_GPB(0), "VS1053_CLK-SCLK");
	gpio_request(EXYNOS4_GPB(1), "VS1053_CS");
	gpio_request(EXYNOS4_GPB(2), "VS1053_MISO");
	gpio_request(EXYNOS4_GPB(3), "VS1053_MOSI");
	gpio_request(EXYNOS4_GPB(4), "VS1053_XDCS");
	gpio_request(EXYNOS4_GPB(5), "gpio_get_value(EXYNOS4_GPB(5))");
	gpio_request(EXYNOS4_GPB(6), "VS1053_RST");
	
	/*2. 配置GPIO口模式*/
	s3c_gpio_cfgpin(EXYNOS4_GPB(0), S3C_GPIO_OUTPUT);  //时钟
	s3c_gpio_cfgpin(EXYNOS4_GPB(1), S3C_GPIO_OUTPUT);  //片选
	s3c_gpio_cfgpin(EXYNOS4_GPB(2), S3C_GPIO_INPUT);   //输入模式
	s3c_gpio_cfgpin(EXYNOS4_GPB(3), S3C_GPIO_OUTPUT);  //输出模式
	s3c_gpio_cfgpin(EXYNOS4_GPB(4), S3C_GPIO_OUTPUT);  //输出模式
	s3c_gpio_cfgpin(EXYNOS4_GPB(5), S3C_GPIO_INPUT);   //输入模式
	s3c_gpio_cfgpin(EXYNOS4_GPB(6), S3C_GPIO_OUTPUT);  //输出模式
	
	/*3. 上拉GPIO口*/
	gpio_set_value(EXYNOS4_GPB(0), 1);
	gpio_set_value(EXYNOS4_GPB(1), 1);
	gpio_set_value(EXYNOS4_GPB(3), 1);
	gpio_set_value(EXYNOS4_GPB(4), 1);
	gpio_set_value(EXYNOS4_GPB(6), 1);





/*****************************************************************************************************/
static int tiny4412_open(struct inode *my_inode, struct file *my_file)

	printk("VS1053 open函数调用成功!\\r\\n");
	return 0;



static int tiny4412_release(struct inode *my_inode, struct file *my_file)

	printk("VS1053 release函数调用成功!\\r\\n");
	return 0;



static u8 Music_buff[32];
static ssize_t tiny4412_write(struct file *my_file, const char __user *buf, size_t len, loff_t *loff)

	if(0!=copy_from_user(Music_buff,buf,len))printk("拷贝错误!\\r\\n");  //每次接收32个字节数据			 
	while(VS1053_SendMusicData(Music_buff));    //给VS10XX发送音频数据
	return len;


#define VS1053_INIT_SET 188
static long tiny4412_unlocked_ioctl(struct file *my_file, unsigned int cmd, unsigned long data)

	switch(cmd)
	
		case VS1053_INIT_SET:
			VS1053_Reset();      								//硬复位MP3
			VS1053_SoftReset(); 								//软复位VS10XX
			VS1053_SetVol(250);								    //设置音量	
			printk("VS1053设置成功!\\r\\n");
			break;
	
	return 0;



/*文件操作集合*/
static struct file_operations tiny4412_fops=

	.open=tiny4412_open,
	.write=tiny4412_write,
	.release=tiny4412_release,
	.unlocked_ioctl=tiny4412_unlocked_ioctl
;


/*
核心结构体
*/
static struct miscdevice tiny4412_misc=

	.minor=MISC_DYNAMIC_MINOR,  /*自动分配次设备号*/
	.name="tiny4412_vs1053",       /*设备文件,指定/dev/生成的文件名称*/
	.fops=&tiny4412_fops
;


static int __init VS1053_init(void)

	VS1053SpiInit(); //初始化GPIO口
	/*杂项设备注册*/
    misc_register(&tiny4412_misc);
	return 0;



static void __exit VS1053_exit(void)

	/*释放GPIO口*/
	gpio_free(EXYNOS4_GPB(0));
	gpio_free(EXYNOS4_GPB(1));
	gpio_free(EXYNOS4_GPB(2));
	gpio_free(EXYNOS4_GPB(3));
	gpio_free(EXYNOS4_GPB(4));
	gpio_free(EXYNOS4_GPB(5));
	gpio_free(EXYNOS4_GPB(6));
	/*杂项设备注销*/
	misc_deregister(&tiny4412_misc);
	
	printk("VS1053 driver exit ok!\\n");


module_exit(VS1053_exit);
module_init(VS1053_init)linux下ALSA音频驱动软件开发

如何在Linux下开发摄像头驱动

Linux驱动开发-编写NEC红外线协议解码驱动

Linux内核开发与Linux驱动开发有啥关系?

什么是linux下的C语言驱动开发?

嵌入式分类