结合自己的专业知识,整合了其他人的文字。
参考学习 原文链接:https://blog.csdn.net/lihuan680680/article/details/122289214
原文链接:https://blog.csdn.net/QQ135102692/article/details/125348020
目录
1、常见音频知识
2、常见接口
3、stm32下,音频驱动,播放音乐方法
4、linux下,alsa架构分析
i2s,alsa驱动架构,其实和其他字符设备(i2c,input,lcd)非常像,我们需要关心的是codex设备驱动,注册过程,使用等。
音频驱动框架分为接口驱动(和soc相关,输出时序的),设备驱动(codex,具体的设备驱动),
alsa内核驱动(内核提供的一套注册接口,描述方法,将设备驱动,和接口驱动,联系起来)。
常见的音频接口有,I2S, PCM , AC97,dai。
常见的音频编码格式,aac,wav,pcm。
常见的音频文件格式,aac,wav,mp3。
1.PCM接口
最简单的音频接口是PCM(脉冲编码调制)接口,该接口由时钟脉冲(BCLK)、帧同步信号(FS)及接收数据(DR)和发送数据(DX)组成。在FS信号的上升沿数据传输从MSB开始,FS频率等于采样频率。FS信号之后开始数据字的传输,单个数据位按顺序进行传输,一个时钟周期传输一个数据字。PCM接口很容易实现,原则上能够支持任何数据方案和任何采样频率,但需要每个音频通道获得一个独立的数据队列。
2.IIS接口
IIS接口在20世纪80年代首先被PHILIPS用于消费音频产品,并在一个称为LRCLK(Left/Right CLOCK)的信号机制中经过转换,将两路音频变成单一的数据队列。当LRCLK为高时,左声道数据被传输;LRCLK为低时,右声道数据被传输。与CPM相比,IIS更适合于立体声系统,当然,IIS的变体也支持多通道的时分复用,因此可以支持多通道。
一、回想stm32下,音频驱动
播放wav格式音乐的过程。
stm32有一个音频dai(digital audio interface)接口,控制wm8960音频芯片的方法,硬件接口如下,需要关注的是dai,i2c接口。
单片机和linux的驱动方法。
单片机需要实现的三个驱动如上。
1、codex驱动
wm8978.c
#include "wm8978.h"
#include "myiic.h"
#include "delay.h"
//
//本程序只供学习使用,未经作者许可,不得用于其它任何用途
//ALIENTEK STM32F407开发板
//WM8978 驱动代码
//正点原子@ALIENTEK
//技术论坛:www.openedv.com
//创建日期:2014/5/24
//版本:V1.0
//版权所有,盗版必究。
//Copyright(C) 广州市星翼电子科技有限公司 2014-2024
//All rights reserved
//
//WM8978寄存器值缓存区(总共58个寄存器,0~57),占用116字节内存
//因为WM8978的IIC操作不支持读操作,所以在本地保存所有寄存器值
//写WM8978寄存器时,同步更新到本地寄存器值,读寄存器时,直接返回本地保存的寄存器值.
//注意:WM8978的寄存器值是9位的,所以要用u16来存储.
static u16 WM8978_REGVAL_TBL[58]=
{
0X0000,0X0000,0X0000,0X0000,0X0050,0X0000,0X0140,0X0000,
0X0000,0X0000,0X0000,0X00FF,0X00FF,0X0000,0X0100,0X00FF,
0X00FF,0X0000,0X012C,0X002C,0X002C,0X002C,0X002C,0X0000,
0X0032,0X0000,0X0000,0X0000,0X0000,0X0000,0X0000,0X0000,
0X0038,0X000B,0X0032,0X0000,0X0008,0X000C,0X0093,0X00E9,
0X0000,0X0000,0X0000,0X0000,0X0003,0X0010,0X0010,0X0100,
0X0100,0X0002,0X0001,0X0001,0X0039,0X0039,0X0039,0X0039,
0X0001,0X0001
};
//WM8978初始化
//返回值:0,初始化正常
// 其他,错误代码
u8 WM8978_Init(void)
{
u8 res;
GPIO_InitTypeDef GPIO_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB|RCC_AHB1Periph_GPIOC, ENABLE); //使能外设GPIOB,GPIOC时钟
//PB12/13 复用功能输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12 | GPIO_Pin_13;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;//复用功能
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100MHz
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉
GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化
//PC2/PC3/PC6复用功能输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_3|GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;//复用功能
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100MHz
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉
GPIO_Init(GPIOC, &GPIO_InitStructure);//初始化
GPIO_PinAFConfig(GPIOB,GPIO_PinSource12,GPIO_AF_SPI2); //PB12,AF5 I2S_LRCK
GPIO_PinAFConfig(GPIOB,GPIO_PinSource13,GPIO_AF_SPI2); //PB13,AF5 I2S_SCLK
GPIO_PinAFConfig(GPIOC,GPIO_PinSource3,GPIO_AF_SPI2); //PC3 ,AF5 I2S_DACDATA
GPIO_PinAFConfig(GPIOC,GPIO_PinSource6,GPIO_AF_SPI2); //PC6 ,AF5 I2S_MCK
GPIO_PinAFConfig(GPIOC,GPIO_PinSource2,GPIO_AF6_SPI2); //PC2 ,AF6 I2S_ADCDATA I2S2ext_SD是AF6!!!
IIC_Init();//初始化IIC接口
res=WM8978_Write_Reg(0,0); //软复位WM8978
if(res)return 1; //发送指令失败,WM8978异常
//以下为通用设置
WM8978_Write_Reg(1,0X1B); //R1,MICEN设置为1(MIC使能),BIASEN设置为1(模拟器工作),VMIDSEL[1:0]设置为:11(5K)
WM8978_Write_Reg(2,0X1B0); //R2,ROUT1,LOUT1输出使能(耳机可以工作),BOOSTENR,BOOSTENL使能
WM8978_Write_Reg(3,0X6C); //R3,LOUT2,ROUT2输出使能(喇叭工作),RMIX,LMIX使能
WM8978_Write_Reg(6,0); //R6,MCLK由外部提供
WM8978_Write_Reg(43,1<<4); //R43,INVROUT2反向,驱动喇叭
WM8978_Write_Reg(47,1<<8); //R47设置,PGABOOSTL,左通道MIC获得20倍增益
WM8978_Write_Reg(48,1<<8); //R48设置,PGABOOSTR,右通道MIC获得20倍增益
WM8978_Write_Reg(49,1<<1); //R49,TSDEN,开启过热保护
WM8978_Write_Reg(10,1<<3); //R10,SOFTMUTE关闭,128x采样,最佳SNR
WM8978_Write_Reg(14,1<<3); //R14,ADC 128x采样率
return 0;
}
//WM8978写寄存器
//reg:寄存器地址
//val:要写入寄存器的值
//返回值:0,成功;
// 其他,错误代码
u8 WM8978_Write_Reg(u8 reg,u16 val)
{
IIC_Start();
IIC_Send_Byte((WM8978_ADDR<<1)|0);//发送器件地址+写命令
if(IIC_Wait_Ack())return 1; //等待应答(成功?/失败?)
IIC_Send_Byte((reg<<1)|((val>>8)&0X01));//写寄存器地址+数据的最高位
if(IIC_Wait_Ack())return 2; //等待应答(成功?/失败?)
IIC_Send_Byte(val&0XFF); //发送数据
if(IIC_Wait_Ack())return 3; //等待应答(成功?/失败?)
IIC_Stop();
WM8978_REGVAL_TBL[reg]=val; //保存寄存器值到本地
return 0;
}
//WM8978读寄存器
//就是读取本地寄存器值缓冲区内的对应值
//reg:寄存器地址
//返回值:寄存器值
u16 WM8978_Read_Reg(u8 reg)
{
return WM8978_REGVAL_TBL[reg];
}
//WM8978 DAC/ADC配置
//adcen:adc使能(1)/关闭(0)
//dacen:dac使能(1)/关闭(0)
void WM8978_ADDA_Cfg(u8 dacen,u8 adcen)
{
u16 regval;
regval=WM8978_Read_Reg(3); //读取R3
if(dacen)regval|=3<<0; //R3最低2个位设置为1,开启DACR&DACL
else regval&=~(3<<0); //R3最低2个位清零,关闭DACR&DACL.
WM8978_Write_Reg(3,regval); //设置R3
regval=WM8978_Read_Reg(2); //读取R2
if(adcen)regval|=3<<0; //R2最低2个位设置为1,开启ADCR&ADCL
else regval&=~(3<<0); //R2最低2个位清零,关闭ADCR&ADCL.
WM8978_Write_Reg(2,regval); //设置R2
}
//WM8978 输入通道配置
//micen:MIC开启(1)/关闭(0)
//lineinen:Line In开启(1)/关闭(0)
//auxen:aux开启(1)/关闭(0)
void WM8978_Input_Cfg(u8 micen,u8 lineinen,u8 auxen)
{
u16 regval;
regval=WM8978_Read_Reg(2); //读取R2
if(micen)regval|=3<<2; //开启INPPGAENR,INPPGAENL(MIC的PGA放大)
else regval&=~(3<<2); //关闭INPPGAENR,INPPGAENL.
WM8978_Write_Reg(2,regval); //设置R2
regval=WM8978_Read_Reg(44); //读取R44
if(micen)regval|=3<<4|3<<0; //开启LIN2INPPGA,LIP2INPGA,RIN2INPPGA,RIP2INPGA.
else regval&=~(3<<4|3<<0); //关闭LIN2INPPGA,LIP2INPGA,RIN2INPPGA,RIP2INPGA.
WM8978_Write_Reg(44,regval);//设置R44
if(lineinen)WM8978_LINEIN_Gain(5);//LINE IN 0dB增益
else WM8978_LINEIN_Gain(0); //关闭LINE IN
if(auxen)WM8978_AUX_Gain(7);//AUX 6dB增益
else WM8978_AUX_Gain(0); //关闭AUX输入
}
//WM8978 输出配置
//dacen:DAC输出(放音)开启(1)/关闭(0)
//bpsen:Bypass输出(录音,包括MIC,LINE IN,AUX等)开启(1)/关闭(0)
void WM8978_Output_Cfg(u8 dacen,u8 bpsen)
{
u16 regval=0;
if(dacen)regval|=1<<0; //DAC输出使能
if(bpsen)
{
regval|=1<<1; //BYPASS使能
regval|=5<<2; //0dB增益
}
WM8978_Write_Reg(50,regval);//R50设置
WM8978_Write_Reg(51,regval);//R51设置
}
//WM8978 MIC增益设置(不包括BOOST的20dB,MIC-->ADC输入部分的增益)
//gain:0~63,对应-12dB~35.25dB,0.75dB/Step
void WM8978_MIC_Gain(u8 gain)
{
gain&=0X3F;
WM8978_Write_Reg(45,gain); //R45,左通道PGA设置
WM8978_Write_Reg(46,gain|1<<8); //R46,右通道PGA设置
}
//WM8978 L2/R2(也就是Line In)增益设置(L2/R2-->ADC输入部分的增益)
//gain:0~7,0表示通道禁止,1~7,对应-12dB~6dB,3dB/Step
void WM8978_LINEIN_Gain(u8 gain)
{
u16 regval;
gain&=0X07;
regval=WM8978_Read_Reg(47); //读取R47
regval&=~(7<<4); //清除原来的设置
WM8978_Write_Reg(47,regval|gain<<4);//设置R47
regval=WM8978_Read_Reg(48); //读取R48
regval&=~(7<<4); //清除原来的设置
WM8978_Write_Reg(48,regval|gain<<4);//设置R48
}
//WM8978 AUXR,AUXL(PWM音频部分)增益设置(AUXR/L-->ADC输入部分的增益)
//gain:0~7,0表示通道禁止,1~7,对应-12dB~6dB,3dB/Step
void WM8978_AUX_Gain(u8 gain)
{
u16 regval;
gain&=0X07;
regval=WM8978_Read_Reg(47); //读取R47
regval&=~(7<<0); //清除原来的设置
WM8978_Write_Reg(47,regval|gain<<0);//设置R47
regval=WM8978_Read_Reg(48); //读取R48
regval&=~(7<<0); //清除原来的设置
WM8978_Write_Reg(48,regval|gain<<0);//设置R48
}
//设置I2S工作模式
//fmt:0,LSB(右对齐);1,MSB(左对齐);2,飞利浦标准I2S;3,PCM/DSP;
//len:0,16位;1,20位;2,24位;3,32位;
void WM8978_I2S_Cfg(u8 fmt,u8 len)
{
fmt&=0X03;
len&=0X03;//限定范围
WM8978_Write_Reg(4,(fmt<<3)|(len<<5)); //R4,WM8978工作模式设置
}
//设置耳机左右声道音量
//voll:左声道音量(0~63)
//volr:右声道音量(0~63)
void WM8978_HPvol_Set(u8 voll,u8 volr)
{
voll&=0X3F;
volr&=0X3F;//限定范围
if(voll==0)voll|=1<<6;//音量为0时,直接mute
if(volr==0)volr|=1<<6;//音量为0时,直接mute
WM8978_Write_Reg(52,voll); //R52,耳机左声道音量设置
WM8978_Write_Reg(53,volr|(1<<8)); //R53,耳机右声道音量设置,同步更新(HPVU=1)
}
//设置喇叭音量
//voll:左声道音量(0~63)
void WM8978_SPKvol_Set(u8 volx)
{
volx&=0X3F;//限定范围
if(volx==0)volx|=1<<6;//音量为0时,直接mute
WM8978_Write_Reg(54,volx); //R54,喇叭左声道音量设置
WM8978_Write_Reg(55,volx|(1<<8)); //R55,喇叭右声道音量设置,同步更新(SPKVU=1)
}
//设置3D环绕声
//depth:0~15(3D强度,0最弱,15最强)
void WM8978_3D_Set(u8 depth)
{
depth&=0XF;//限定范围
WM8978_Write_Reg(41,depth); //R41,3D环绕设置
}
//设置EQ/3D作用方向
//dir:0,在ADC起作用
// 1,在DAC起作用(默认)
void WM8978_EQ_3D_Dir(u8 dir)
{
u16 regval;
regval=WM8978_Read_Reg(0X12);
if(dir)regval|=1<<8;
else regval&=~(1<<8);
WM8978_Write_Reg(18,regval);//R18,EQ1的第9位控制EQ/3D方向
}
//设置EQ1
//cfreq:截止频率,0~3,分别对应:80/105/135/175Hz
//gain:增益,0~24,对应-12~+12dB
void WM8978_EQ1_Set(u8 cfreq,u8 gain)
{
u16 regval;
cfreq&=0X3;//限定范围
if(gain>24)gain=24;
gain=24-gain;
regval=WM8978_Read_Reg(18);
regval&=0X100;
regval|=cfreq<<5; //设置截止频率
regval|=gain; //设置增益
WM8978_Write_Reg(18,regval);//R18,EQ1设置
}
//设置EQ2
//cfreq:中心频率,0~3,分别对应:230/300/385/500Hz
//gain:增益,0~24,对应-12~+12dB
void WM8978_EQ2_Set(u8 cfreq,u8 gain)
{
u16 regval=0;
cfreq&=0X3;//限定范围
if(gain>24)gain=24;
gain=24-gain;
regval|=cfreq<<5; //设置截止频率
regval|=gain; //设置增益
WM8978_Write_Reg(19,regval);//R19,EQ2设置
}
//设置EQ3
//cfreq:中心频率,0~3,分别对应:650/850/1100/1400Hz
//gain:增益,0~24,对应-12~+12dB
void WM8978_EQ3_Set(u8 cfreq,u8 gain)
{
u16 regval=0;
cfreq&=0X3;//限定范围
if(gain>24)gain=24;
gain=24-gain;
regval|=cfreq<<5; //设置截止频率
regval|=gain; //设置增益
WM8978_Write_Reg(20,regval);//R20,EQ3设置
}
//设置EQ4
//cfreq:中心频率,0~3,分别对应:1800/2400/3200/4100Hz
//gain:增益,0~24,对应-12~+12dB
void WM8978_EQ4_Set(u8 cfreq,u8 gain)
{
u16 regval=0;
cfreq&=0X3;//限定范围
if(gain>24)gain=24;
gain=24-gain;
regval|=cfreq<<5; //设置截止频率
regval|=gain; //设置增益
WM8978_Write_Reg(21,regval);//R21,EQ4设置
}
//设置EQ5
//cfreq:中心频率,0~3,分别对应:5300/6900/9000/11700Hz
//gain:增益,0~24,对应-12~+12dB
void WM8978_EQ5_Set(u8 cfreq,u8 gain)
{
u16 regval=0;
cfreq&=0X3;//限定范围
if(gain>24)gain=24;
gain=24-gain;
regval|=cfreq<<5; //设置截止频率
regval|=gain; //设置增益
WM8978_Write_Reg(22,regval);//R22,EQ5设置
}
2、i2s接口驱动
i2s.c
#include "i2s.h"
#include "usart.h"
//
//本程序只供学习使用,未经作者许可,不得用于其它任何用途
//ALIENTEK STM32F407开发板
//I2S 驱动代码
//正点原子@ALIENTEK
//技术论坛:www.openedv.com
//创建日期:2014/5/24
//版本:V1.0
//版权所有,盗版必究。
//Copyright(C) 广州市星翼电子科技有限公司 2014-2024
//All rights reserved
//
//I2S2初始化
//参数I2S_Standard: @ref SPI_I2S_Standard I2S标准,
//I2S_Standard_Phillips,飞利浦标准;
//I2S_Standard_MSB,MSB对齐标准(右对齐);
//I2S_Standard_LSB,LSB对齐标准(左对齐);
//I2S_Standard_PCMShort,I2S_Standard_PCMLong:PCM标准
//参数I2S_Mode: @ref SPI_I2S_Mode I2S_Mode_SlaveTx:从机发送;I2S_Mode_SlaveRx:从机接收;I2S_Mode_MasterTx:主机发送;I2S_Mode_MasterRx:主机接收;
//参数I2S_Clock_Polarity @ref SPI_I2S_Clock_Polarity: I2S_CPOL_Low,时钟低电平有效;I2S_CPOL_High,时钟高电平有效
//参数I2S_DataFormat: @ref SPI_I2S_Data_Format :数据长度,I2S_DataFormat_16b,16位标准;I2S_DataFormat_16bextended,16位扩展(frame=32bit);I2S_DataFormat_24b,24位;I2S_DataFormat_32b,32位.
void I2S2_Init(u16 I2S_Standard,u16 I2S_Mode,u16 I2S_Clock_Polarity,u16 I2S_DataFormat)
{
I2S_InitTypeDef I2S_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE);//使能SPI2时钟
RCC_APB1PeriphResetCmd(RCC_APB1Periph_SPI2,ENABLE); //复位SPI2
RCC_APB1PeriphResetCmd(RCC_APB1Periph_SPI2,DISABLE);//结束复位
I2S_InitStructure.I2S_Mode=I2S_Mode;//IIS模式
I2S_InitStructure.I2S_Standard=I2S_Standard;//IIS标准
I2S_InitStructure.I2S_DataFormat=I2S_DataFormat;//IIS数据长度
I2S_InitStructure.I2S_MCLKOutput=I2S_MCLKOutput_Disable;//主时钟输出禁止
I2S_InitStructure.I2S_AudioFreq=I2S_AudioFreq_Default;//IIS频率设置
I2S_InitStructure.I2S_CPOL=I2S_Clock_Polarity;//空闲状态时钟电平
I2S_Init(SPI2,&I2S_InitStructure);//初始化IIS
SPI_I2S_DMACmd(SPI2,SPI_I2S_DMAReq_Tx,ENABLE);//SPI2 TX DMA请求使能.
I2S_Cmd(SPI2,ENABLE);//SPI2 I2S EN使能.
}
//采样率计算公式:Fs=I2SxCLK/[256*(2*I2SDIV+ODD)]
//I2SxCLK=(HSE/pllm)*PLLI2SN/PLLI2SR
//一般HSE=8Mhz
//pllm:在Sys_Clock_Set设置的时候确定,一般是8
//PLLI2SN:一般是192~432
//PLLI2SR:2~7
//I2SDIV:2~255
//ODD:0/1
//I2S分频系数表@pllm=8,HSE=8Mhz,即vco输入频率为1Mhz
//表格式:采样率/10,PLLI2SN,PLLI2SR,I2SDIV,ODD
const u16 I2S_PSC_TBL[][5]=
{
{800 ,256,5,12,1}, //8Khz采样率
{1102,429,4,19,0}, //11.025Khz采样率
{1600,213,2,13,0}, //16Khz采样率
{2205,429,4, 9,1}, //22.05Khz采样率
{3200,213,2, 6,1}, //32Khz采样率
{4410,271,2, 6,0}, //44.1Khz采样率
{4800,258,3, 3,1}, //48Khz采样率
{8820,316,2, 3,1}, //88.2Khz采样率
{9600,344,2, 3,1}, //96Khz采样率
{17640,361,2,2,0}, //176.4Khz采样率
{19200,393,2,2,0}, //192Khz采样率
};
//设置IIS的采样率(@MCKEN)
//samplerate:采样率,单位:Hz
//返回值:0,设置成功;1,无法设置.
u8 I2S2_SampleRate_Set(u32 samplerate)
{
u8 i=0;
u32 tempreg=0;
samplerate/=10;//缩小10倍
for(i=0;i<(sizeof(I2S_PSC_TBL)/10);i++)//看看改采样率是否可以支持
{
if(samplerate==I2S_PSC_TBL[i][0])break;
}
RCC_PLLI2SCmd(DISABLE);//先关闭PLLI2S
if(i==(sizeof(I2S_PSC_TBL)/10))return 1;//搜遍了也找不到
RCC_PLLI2SConfig((u32)I2S_PSC_TBL[i][1],(u32)I2S_PSC_TBL[i][2]);//设置I2SxCLK的频率(x=2) 设置PLLI2SN PLLI2SR
RCC->CR|=1<<26; //开启I2S时钟
while((RCC->CR&1<<27)==0); //等待I2S时钟开启成功.
tempreg=I2S_PSC_TBL[i][3]<<0; //设置I2SDIV
tempreg|=I2S_PSC_TBL[i][4]<<8; //设置ODD位
tempreg|=1<<9; //使能MCKOE位,输出MCK
SPI2->I2SPR=tempreg; //设置I2SPR寄存器
return 0;
}
//I2S2 TX DMA配置
//设置为双缓冲模式,并开启DMA传输完成中断
//buf0:M0AR地址.
//buf1:M1AR地址.
//num:每次传输数据量
void I2S2_TX_DMA_Init(u8* buf0,u8 *buf1,u16 num)
{
NVIC_InitTypeDef NVIC_InitStructure;
DMA_InitTypeDef DMA_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1,ENABLE);//DMA1时钟使能
DMA_DeInit(DMA1_Stream4);
while (DMA_GetCmdStatus(DMA1_Stream4) != DISABLE){}//等待DMA1_Stream1可配置
/* 配置 DMA Stream */
DMA_InitStructure.DMA_Channel = DMA_Channel_0; //通道0 SPI2_TX通道
DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&SPI2->DR;//外设地址为:(u32)&SPI2->DR
DMA_InitStructure.DMA_Memory0BaseAddr = (u32)buf0;//DMA 存储器0地址
DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;//存储器到外设模式
DMA_InitStructure.DMA_BufferSize = num;//数据传输量
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设非增量模式
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//存储器增量模式
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;//外设数据长度:16位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;//存储器数据长度:16位
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;// 使用循环模式
DMA_InitStructure.DMA_Priority = DMA_Priority_High;//高优先级
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable; //不使用FIFO模式
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_1QuarterFull;
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;//外设突发单次传输
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;//存储器突发单次传输
DMA_Init(DMA1_Stream4, &DMA_InitStructure);//初始化DMA Stream
DMA_DoubleBufferModeConfig(DMA1_Stream4,(u32)buf1,DMA_Memory_0);//双缓冲模式配置
DMA_DoubleBufferModeCmd(DMA1_Stream4,ENABLE);//双缓冲模式开启
DMA_ITConfig(DMA1_Stream4,DMA_IT_TC,ENABLE);//开启传输完成中断
NVIC_InitStructure.NVIC_IRQChannel = DMA1_Stream4_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x00;//抢占优先级0
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x00;//子优先级0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//使能外部中断通道
NVIC_Init(&NVIC_InitStructure);//配置
}
//I2S DMA回调函数指针
void (*i2s_tx_callback)(void); //TX回调函数
//DMA1_Stream4中断服务函数
void DMA1_Stream4_IRQHandler(void)
{
if(DMA_GetITStatus(DMA1_Stream4,DMA_IT_TCIF4)==SET)DMA1_Stream4,传输完成标志
{
DMA_ClearITPendingBit(DMA1_Stream4,DMA_IT_TCIF4);
i2s_tx_callback(); //执行回调函数,读取数据等操作在这里面处理
}
}
//I2S开始播放
void I2S_Play_Start(void)
{
DMA_Cmd(DMA1_Stream4,ENABLE);//开启DMA TX传输,开始播放
}
//关闭I2S播放
void I2S_Play_Stop(void)
{
DMA_Cmd(DMA1_Stream4,DISABLE);//关闭DMA,结束播放
}
3、连结文件,将文件解码后,调用i2d提供的dma,读写数据,到i2s接口,驱动时序,模数转换
封装了一个wav.c文件,解码wav格式,然后dma,数据传输
#include "wavplay.h"
#include "audioplay.h"
#include "usart.h"
#include "delay.h"
#include "malloc.h"
#include "ff.h"
#include "i2s.h"
#include "wm8978.h"
#include "key.h"
#include "led.h"
//
//本程序只供学习使用,未经作者许可,不得用于其它任何用途
//ALIENTEK STM32F407开发板
//WAV 解码代码
//正点原子@ALIENTEK
//技术论坛:www.openedv.com
//创建日期:2014/6/29
//版本:V1.0
//版权所有,盗版必究。
//Copyright(C) 广州市星翼电子科技有限公司 2014-2024
//All rights reserved
//********************************************************************************
//V1.0 说明
//1,支持16位/24位WAV文件播放
//2,最高可以支持到192K/24bit的WAV格式.
//
__wavctrl wavctrl; //WAV控制结构体
vu8 wavtransferend=0; //i2s传输完成标志
vu8 wavwitchbuf=0; //i2sbufx指示标志
//WAV解析初始化
//fname:文件路径+文件名
//wavx:wav 信息存放结构体指针
//返回值:0,成功;1,打开文件失败;2,非WAV文件;3,DATA区域未找到.
u8 wav_decode_init(u8* fname,__wavctrl* wavx)
{
FIL*ftemp;
u8 *buf;
u32 br=0;
u8 res=0;
ChunkRIFF *riff;
ChunkFMT *fmt;
ChunkFACT *fact;
ChunkDATA *data;
ftemp=(FIL*)mymalloc(SRAMIN,sizeof(FIL));
buf=mymalloc(SRAMIN,512);
if(ftemp&&buf) //内存申请成功
{
res=f_open(ftemp,(TCHAR*)fname,FA_READ);//打开文件
if(res==FR_OK)
{
f_read(ftemp,buf,512,&br); //读取512字节在数据
riff=(ChunkRIFF *)buf; //获取RIFF块
if(riff->Format==0X45564157)//是WAV文件
{
fmt=(ChunkFMT *)(buf+12); //获取FMT块
fact=(ChunkFACT *)(buf+12+8+fmt->ChunkSize);//读取FACT块
if(fact->ChunkID==0X74636166||fact->ChunkID==0X5453494C)wavx->datastart=12+8+fmt->ChunkSize+8+fact->ChunkSize;//具有fact/LIST块的时候(未测试)
else wavx->datastart=12+8+fmt->ChunkSize;
data=(ChunkDATA *)(buf+wavx->datastart); //读取DATA块
if(data->ChunkID==0X61746164)//解析成功!
{
wavx->audioformat=fmt->AudioFormat; //音频格式
wavx->nchannels=fmt->NumOfChannels; //通道数
wavx->samplerate=fmt->SampleRate; //采样率
wavx->bitrate=fmt->ByteRate*8; //得到位速
wavx->blockalign=fmt->BlockAlign; //块对齐
wavx->bps=fmt->BitsPerSample; //位数,16/24/32位
wavx->datasize=data->ChunkSize; //数据块大小
wavx->datastart=wavx->datastart+8; //数据流开始的地方.
printf("wavx->audioformat:%d\r\n",wavx->audioformat);
printf("wavx->nchannels:%d\r\n",wavx->nchannels);
printf("wavx->samplerate:%d\r\n",wavx->samplerate);
printf("wavx->bitrate:%d\r\n",wavx->bitrate);
printf("wavx->blockalign:%d\r\n",wavx->blockalign);
printf("wavx->bps:%d\r\n",wavx->bps);
printf("wavx->datasize:%d\r\n",wavx->datasize);
printf("wavx->datastart:%d\r\n",wavx->datastart);
}else res=3;//data区域未找到.
}else res=2;//非wav文件
}else res=1;//打开文件错误
}
f_close(ftemp);
myfree(SRAMIN,ftemp);//释放内存
myfree(SRAMIN,buf);
return 0;
}
//填充buf
//buf:数据区
//size:填充数据量
//bits:位数(16/24)
//返回值:读到的数据个数
u32 wav_buffill(u8 *buf,u16 size,u8 bits)
{
u16 readlen=0;
u32 bread;
u16 i;
u8 *p;
if(bits==24)//24bit音频,需要处理一下
{
readlen=(size/4)*3; //此次要读取的字节数
f_read(audiodev.file,audiodev.tbuf,readlen,(UINT*)&bread); //读取数据
p=audiodev.tbuf;
for(i=0;i<size;)
{
buf[i++]=p[1];
buf[i]=p[2];
i+=2;
buf[i++]=p[0];
p+=3;
}
bread=(bread*4)/3; //填充后的大小.
}else
{
f_read(audiodev.file,buf,size,(UINT*)&bread);//16bit音频,直接读取数据
if(bread<size)//不够数据了,补充0
{
for(i=bread;i<size-bread;i++)buf[i]=0;
}
}
return bread;
}
//WAV播放时,I2S DMA传输回调函数
void wav_i2s_dma_tx_callback(void)
{
u16 i;
if(DMA1_Stream4->CR&(1<<19))
{
wavwitchbuf=0;
if((audiodev.status&0X01)==0)
{
for(i=0;i<WAV_I2S_TX_DMA_BUFSIZE;i++)//暂停
{
audiodev.i2sbuf1[i]=0;//填充0
}
}
}else
{
wavwitchbuf=1;
if((audiodev.status&0X01)==0)
{
for(i=0;i<WAV_I2S_TX_DMA_BUFSIZE;i++)//暂停
{
audiodev.i2sbuf2[i]=0;//填充0
}
}
}
wavtransferend=1;
}
//得到当前播放时间
//fx:文件指针
//wavx:wav播放控制器
void wav_get_curtime(FIL*fx,__wavctrl *wavx)
{
long long fpos;
wavx->totsec=wavx->datasize/(wavx->bitrate/8); //歌曲总长度(单位:秒)
fpos=fx->fptr-wavx->datastart; //得到当前文件播放到的地方
wavx->cursec=fpos*wavx->totsec/wavx->datasize; //当前播放到第多少秒了?
}
//播放某个WAV文件
//fname:wav文件路径.
//返回值:
//KEY0_PRES:下一曲
//KEY1_PRES:上一曲
//其他:错误
u8 wav_play_song(u8* fname)
{
u8 key;
u8 t=0;
u8 res;
u32 fillnum;
audiodev.file=(FIL*)mymalloc(SRAMIN,sizeof(FIL));
audiodev.i2sbuf1=mymalloc(SRAMIN,WAV_I2S_TX_DMA_BUFSIZE);
audiodev.i2sbuf2=mymalloc(SRAMIN,WAV_I2S_TX_DMA_BUFSIZE);
audiodev.tbuf=mymalloc(SRAMIN,WAV_I2S_TX_DMA_BUFSIZE);
if(audiodev.file&&audiodev.i2sbuf1&&audiodev.i2sbuf2&&audiodev.tbuf)
{
res=wav_decode_init(fname,&wavctrl);//得到文件的信息
if(res==0)//解析文件成功
{
if(wavctrl.bps==16)
{
WM8978_I2S_Cfg(2,0); //飞利浦标准,16位数据长度
I2S2_Init(I2S_Standard_Phillips,I2S_Mode_MasterTx,I2S_CPOL_Low,I2S_DataFormat_16bextended); //飞利浦标准,主机发送,时钟低电平有效,16位扩展帧长度
}else if(wavctrl.bps==24)
{
WM8978_I2S_Cfg(2,2); //飞利浦标准,24位数据长度
I2S2_Init(I2S_Standard_Phillips,I2S_Mode_MasterTx,I2S_CPOL_Low,I2S_DataFormat_24b); //飞利浦标准,主机发送,时钟低电平有效,24位扩展帧长度
}
I2S2_SampleRate_Set(wavctrl.samplerate);//设置采样率
I2S2_TX_DMA_Init(audiodev.i2sbuf1,audiodev.i2sbuf2,WAV_I2S_TX_DMA_BUFSIZE/2); //配置TX DMA
i2s_tx_callback=wav_i2s_dma_tx_callback; //回调函数指wav_i2s_dma_callback
audio_stop();
res=f_open(audiodev.file,(TCHAR*)fname,FA_READ); //打开文件
if(res==0)
{
f_lseek(audiodev.file, wavctrl.datastart); //跳过文件头
fillnum=wav_buffill(audiodev.i2sbuf1,WAV_I2S_TX_DMA_BUFSIZE,wavctrl.bps);
fillnum=wav_buffill(audiodev.i2sbuf2,WAV_I2S_TX_DMA_BUFSIZE,wavctrl.bps);
audio_start();
while(res==0)
{
while(wavtransferend==0);//等待wav传输完成;
wavtransferend=0;
if(fillnum!=WAV_I2S_TX_DMA_BUFSIZE)//播放结束?
{
res=KEY0_PRES;
break;
}
if(wavwitchbuf)fillnum=wav_buffill(audiodev.i2sbuf2,WAV_I2S_TX_DMA_BUFSIZE,wavctrl.bps);//填充buf2
else fillnum=wav_buffill(audiodev.i2sbuf1,WAV_I2S_TX_DMA_BUFSIZE,wavctrl.bps);//填充buf1
while(1)
{
key=KEY_Scan(0);
if(key==WKUP_PRES)//暂停
{
if(audiodev.status&0X01)audiodev.status&=~(1<<0);
else audiodev.status|=0X01;
}
if(key==KEY2_PRES||key==KEY0_PRES)//下一曲/上一曲
{
res=key;
break;
}
wav_get_curtime(audiodev.file,&wavctrl);//得到总时间和当前播放的时间
audio_msg_show(wavctrl.totsec,wavctrl.cursec,wavctrl.bitrate);
t++;
if(t==20)
{
t=0;
LED0=!LED0;
}
if((audiodev.status&0X01)==0)delay_ms(10);
else break;
}
}
audio_stop();
}else res=0XFF;
}else res=0XFF;
}else res=0XFF;
myfree(SRAMIN,audiodev.tbuf); //释放内存
myfree(SRAMIN,audiodev.i2sbuf1);//释放内存
myfree(SRAMIN,audiodev.i2sbuf2);//释放内存
myfree(SRAMIN,audiodev.file); //释放内存
return res;
}
4、写了个app,应用层,调用wav接口,播放文件。
#include "audioplay.h"
#include "ff.h"
#include "malloc.h"
#include "usart.h"
#include "wm8978.h"
#include "i2s.h"
#include "led.h"
#include "lcd.h"
#include "delay.h"
#include "key.h"
#include "exfuns.h"
#include "text.h"
#include "string.h"
//
//本程序只供学习使用,未经作者许可,不得用于其它任何用途
//ALIENTEK STM32F407开发板
//音乐播放器 应用代码
//正点原子@ALIENTEK
//技术论坛:www.openedv.com
//创建日期:2014/5/24
//版本:V1.0
//版权所有,盗版必究。
//Copyright(C) 广州市星翼电子科技有限公司 2014-2024
//All rights reserved
//
//音乐播放控制器
__audiodev audiodev;
//开始音频播放
void audio_start(void)
{
audiodev.status=3<<0;//开始播放+非暂停
I2S_Play_Start();
}
//关闭音频播放
void audio_stop(void)
{
audiodev.status=0;
I2S_Play_Stop();
}
//得到path路径下,目标文件的总个数
//path:路径
//返回值:总有效文件数
u16 audio_get_tnum(u8 *path)
{
u8 res;
u16 rval=0;
DIR tdir; //临时目录
FILINFO tfileinfo; //临时文件信息
u8 *fn;
res=f_opendir(&tdir,(const TCHAR*)path); //打开目录
tfileinfo.lfsize=_MAX_LFN*2+1; //长文件名最大长度
tfileinfo.lfname=mymalloc(SRAMIN,tfileinfo.lfsize); //为长文件缓存区分配内存
if(res==FR_OK&&tfileinfo.lfname!=NULL)
{
while(1)//查询总的有效文件数
{
res=f_readdir(&tdir,&tfileinfo); //读取目录下的一个文件
if(res!=FR_OK||tfileinfo.fname[0]==0)break; //错误了/到末尾了,退出
fn=(u8*)(*tfileinfo.lfname?tfileinfo.lfname:tfileinfo.fname);
res=f_typetell(fn);
if((res&0XF0)==0X40)//取高四位,看看是不是音乐文件
{
rval++;//有效文件数增加1
}
}
}
myfree(SRAMIN,tfileinfo.lfname);
return rval;
}
//显示曲目索引
//index:当前索引
//total:总文件数
void audio_index_show(u16 index,u16 total)
{
//显示当前曲目的索引,及总曲目数
LCD_ShowxNum(60+0,230,index,3,16,0X80); //索引
LCD_ShowChar(60+24,230,'/',16,0);
LCD_ShowxNum(60+32,230,total,3,16,0X80); //总曲目
}
//显示播放时间,比特率 信息
//totsec;音频文件总时间长度
//cursec:当前播放时间
//bitrate:比特率(位速)
void audio_msg_show(u32 totsec,u32 cursec,u32 bitrate)
{
static u16 playtime=0XFFFF;//播放时间标记
if(playtime!=cursec) //需要更新显示时间
{
playtime=cursec;
//显示播放时间
LCD_ShowxNum(60,210,playtime/60,2,16,0X80); //分钟
LCD_ShowChar(60+16,210,':',16,0);
LCD_ShowxNum(60+24,210,playtime%60,2,16,0X80); //秒钟
LCD_ShowChar(60+40,210,'/',16,0);
//显示总时间
LCD_ShowxNum(60+48,210,totsec/60,2,16,0X80); //分钟
LCD_ShowChar(60+64,210,':',16,0);
LCD_ShowxNum(60+72,210,totsec%60,2,16,0X80); //秒钟
//显示位率
LCD_ShowxNum(60+110,210,bitrate/1000,4,16,0X80);//显示位率
LCD_ShowString(60+110+32,210,200,16,16,"Kbps");
}
}
//播放音乐
void audio_play(void)
{
u8 res;
DIR wavdir; //目录
FILINFO wavfileinfo;//文件信息
u8 *fn; //长文件名
u8 *pname; //带路径的文件名
u16 totwavnum; //音乐文件总数
u16 curindex; //图片当前索引
u8 key; //键值
u16 temp;
u16 *wavindextbl; //音乐索引表
WM8978_ADDA_Cfg(1,0); //开启DAC
WM8978_Input_Cfg(0,0,0);//关闭输入通道
WM8978_Output_Cfg(1,0); //开启DAC输出
while(f_opendir(&wavdir,"0:/MUSIC"))//打开音乐文件夹
{
Show_Str(60,190,240,16,"MUSIC文件夹错误!",16,0);
delay_ms(200);
LCD_Fill(60,190,240,206,WHITE);//清除显示
delay_ms(200);
}
totwavnum=audio_get_tnum("0:/MUSIC"); //得到总有效文件数
while(totwavnum==NULL)//音乐文件总数为0
{
Show_Str(60,190,240,16,"没有音乐文件!",16,0);
delay_ms(200);
LCD_Fill(60,190,240,146,WHITE);//清除显示
delay_ms(200);
}
wavfileinfo.lfsize=_MAX_LFN*2+1; //长文件名最大长度
wavfileinfo.lfname=mymalloc(SRAMIN,wavfileinfo.lfsize); //为长文件缓存区分配内存
pname=mymalloc(SRAMIN,wavfileinfo.lfsize); //为带路径的文件名分配内存
wavindextbl=mymalloc(SRAMIN,2*totwavnum); //申请2*totwavnum个字节的内存,用于存放音乐文件索引
while(wavfileinfo.lfname==NULL||pname==NULL||wavindextbl==NULL)//内存分配出错
{
Show_Str(60,190,240,16,"内存分配失败!",16,0);
delay_ms(200);
LCD_Fill(60,190,240,146,WHITE);//清除显示
delay_ms(200);
}
//记录索引
res=f_opendir(&wavdir,"0:/MUSIC"); //打开目录
if(res==FR_OK)
{
curindex=0;//当前索引为0
while(1)//全部查询一遍
{
temp=wavdir.index; //记录当前index
res=f_readdir(&wavdir,&wavfileinfo); //读取目录下的一个文件
if(res!=FR_OK||wavfileinfo.fname[0]==0)break; //错误了/到末尾了,退出
fn=(u8*)(*wavfileinfo.lfname?wavfileinfo.lfname:wavfileinfo.fname);
res=f_typetell(fn);
if((res&0XF0)==0X40)//取高四位,看看是不是音乐文件
{
wavindextbl[curindex]=temp;//记录索引
curindex++;
}
}
}
curindex=0; //从0开始显示
res=f_opendir(&wavdir,(const TCHAR*)"0:/MUSIC"); //打开目录
while(res==FR_OK)//打开成功
{
dir_sdi(&wavdir,wavindextbl[curindex]); //改变当前目录索引
res=f_readdir(&wavdir,&wavfileinfo); //读取目录下的一个文件
if(res!=FR_OK||wavfileinfo.fname[0]==0)break; //错误了/到末尾了,退出
fn=(u8*)(*wavfileinfo.lfname?wavfileinfo.lfname:wavfileinfo.fname);
strcpy((char*)pname,"0:/MUSIC/"); //复制路径(目录)
strcat((char*)pname,(const char*)fn); //将文件名接在后面
LCD_Fill(60,190,240,190+16,WHITE); //清除之前的显示
Show_Str(60,190,240-60,16,fn,16,0); //显示歌曲名字
audio_index_show(curindex+1,totwavnum);
key=audio_play_song(pname); //播放这个音频文件
if(key==KEY2_PRES) //上一曲
{
if(curindex)curindex--;
else curindex=totwavnum-1;
}else if(key==KEY0_PRES)//下一曲
{
curindex++;
if(curindex>=totwavnum)curindex=0;//到末尾的时候,自动从头开始
}else break; //产生了错误
}
myfree(SRAMIN,wavfileinfo.lfname); //释放内存
myfree(SRAMIN,pname); //释放内存
myfree(SRAMIN,wavindextbl); //释放内存
}
//播放某个音频文件
u8 audio_play_song(u8* fname)
{
u8 res;
res=f_typetell(fname);
switch(res)
{
case T_WAV:
res=wav_play_song(fname);
break;
default://其他文件,自动跳转到下一曲
printf("can't play:%s\r\n",fname);
res=KEY0_PRES;
break;
}
return res;
}
ok有了上面的基础,回忆,再回到linux平台,看看有什么区别,发现,其实是非常像的。单片机和linux驱动开发,区别就在于,linux内核,提供了一套,专门管理文件的架构,一切皆是文件,所以,把驱动也看成一个文件,来给应用层操作,所以内核就,在这个基础上,提供一套注册到内核的接口,当做文件来管理了。
二、ALSA架构
ALSA(Advanced Linux Sound Architecture)即高级 Linux 声音架构,目前已经成为了linux的主流音频体系结构,ALSA在内核部分提供alsa-driver对音频驱动进行耦合和管理,在用户空间空间提供alsa-lib,应用开发人员可以使用alsa-lib接口控制声卡。
linux ASoC音频设备驱动
ASoC是ALSA在SoC方面的发展和演变,它的本质仍然属于ALSA,但是在ALSA架构基础上对CPU相关的代码和Codec相关的代码进行了分离,其原因是采用传统ALSA架构情况下,同一型号的Codec工作于不同的CPU时,需要不同的驱动,这是不符合代码重用的要求的。
ASoC主要由3部分组成:
(1)Codec驱动,这一部分只关系Codec本身,与CPU相关的特性不由此部分操作
(2)平台驱动,这一部分只关心CPU本身,不关系Codec,它主要处理了两个问题:DMA引擎和SoC解除的PCM、IIS或AC’97数字接口控制。
(3)板驱动,这一部分将平台驱动和Codec驱动绑定在一起,描述了板一级的硬件特征,
其中machine是连接codec driver及platform driver的桥梁
以上3部分中,1和2基本都可以仍然是通用的驱动了,即Codec驱动认为自己可以连接任意CPU,而CPU的IIS、PCM、或AC’97接口对应的平台驱动则认为自己可以连接符号其接口类型的Codec,只有3是不通用的,由特定的电路板上具体的CPU和Codec确定,比如codex设置48k的采样率,16位,那我的soc是不知道的,也是需要配置成48k采样率,16位,否则,采集的数据是不对。这样就减少了耦合度。就是设备驱动分离的思想。因此它很像一个插座,上面插着Codec和平台这两个插头。ASoC的用户空间编程方法与ALSA完全一致。
嵌入式移动设备的音频子系统目前主要是ALSA 驱动 asoc 框架,其中包括 codec driver、 soc的platform driver、 板载硬件machine driver 等。
1、codec driver,音频编解码,adc,dac 转换芯片驱动,只关心 codec 本身,只需调用字符设备的框架(i2c,i2s的接口),控制code设置参数,模数,数模的转换。
2、platform driver 主要是平台 soc相关的驱动,不同soc,cpu 提供dai( 如 i2s), dma 等部分,提供时序控制的驱动代码,设置寄存器。
3、machine driver 主要将 platform driver 和 codec driver 连接起来。
platform 和 dai接口driver 一般修改较少,这些都是芯片商写好的,我们作为使用者,只需关心codex, 主要修改machine 和 codec driver,这也将是我们后续分析的重点。
ALSA架构图如下:
一个音频文件的播放流程如下图,由dma将音频文件的内容从内存搬运到IIS控制器,后传送到codec音频解码芯片,将数字信号转换为模拟信号输出给耳机或者喇叭,如下图
上面这些图,看完后,音频驱动框架就非常清楚了。关于i2s的介绍,本篇不介绍,在第一篇有介绍。
他们的文件在linux内核的目录如下。
include/sound/driver.h
sound/core/*.c
3、alsa设备文件介绍
linux把注册的codex当做card,声卡的意思。设备节点目录:/dev/snd
ALSA系统包括包括驱动包alsa-driver、开发包alsa-libs、开发包插件alsa-libplugins、设置管理工具包alsa-utils、其他声音相关处理小程序包alsa-tools、特殊音频固件支持包alsa-firmware、OSS接口兼容模拟层工具alsa-oss供7个子项目,其中只有驱动包是必需的。
目前ALSA内核提供给用户空间的接口有:
(1)设备信息接口(/proc/asound)
(2)设备控制接口(/dev/snd/controlCX)
(3)混音器设备接口(/dev/snd/mixerCXDX)
(4)PCM设备接口(/dev/snd/pcmCXDX)
(5)原始MIDI(迷笛)设备接口(/dev/snd/midiCXDX)
(6)声音合成(synthesizer)设备接口(/dev/snd/seq)
(7)定时器接口(/dev/snd/timer)
这些接口被提供给alsa-lib使用,而不是给应用程序使用,应用程序最好使用alsa-lib,或者更高级的接口比如jack提供的接口。
controlC0:用于声卡的控制,例如通道选择,混音,麦克风的控制等;
midiC0D0:用于播放midi音频;
pcmC0D0c : 用于录音的pcm设备;
pcmC0D0p :用于播放的pcm设备;
seq :音序器;
timer :定时器;
card声卡和组件管理
card声卡和组件管理
/* 对于每个声卡,必须创建一个card实例,该函数用于创建card
参数idx为索引号
参数id为标识的字符串
参数module一般指向THIS_MODULE
参数extra_size是要分配的额外的数据的大小,分配的extra_size大小的内存将作为card->private_data*/
struct snd_card *snd_card_new(int idx, const char *id,struct module *module, int extra_size)
/* 注册声卡 */
int snd_card_register(struct snd_card *card)
/* 释放(注销)声卡 */
int snd_card_free(struct snd_card *card)
/* 创建一个ALSA设备部件
参数type为设备类型,查看宏SNDRV_DEV_XXX*/
int snd_device_new(struct snd_card *card, snd_device_type_t type,void *device_data, struct snd_device_ops *ops)
/*
释放声卡的设备
参数device_data指向要设备的私有数据
*/
int snd_device_free(struct snd_card *card, void *device_data)
PCM 设备接口
/* 创建PCM实例
参数card指向声卡
参数id是标识字符串
参数device为PCM设备引索(0表示第1个PCM设备)
参数playback_count为播放设备的子流数
参数capture_count为录音设备的子流数
参数指向构造的PCM实例*/
int snd_pcm_new(struct snd_card *card, const char *id, int device,int playback_count, int capture_count,struct snd_pcm ** rpcm)
/* 设置PCM操作函数
参数direction,查看宏SNDRV_PCM_STREAM_XXX*/
void snd_pcm_set_ops(struct snd_pcm *pcm, int direction, struct snd_pcm_ops *ops)
/* 分配DMA缓冲区,仅当DMA缓冲区已预分配的情况下才可调用该函数 */
int snd_pcm_lib_malloc_pages(struct snd_pcm_substream *substream, size_t size)
/* 释放由snd_pcm_lib_malloc_pages函数分配的一致性DMA缓冲区 */
int snd_pcm_lib_free_pages(struct snd_pcm_substream *substream)
/* 分配缓冲区的最简单的方法是调用该函数
type的取值可查看宏SNDRV_DMA_TYPE_* */
int snd_pcm_lib_preallocate_pages_for_all(struct snd_pcm *pcm,int type, void *data,size_t size, size_t max)
控制接口说明
/* 创建一个control实例----struct snd_kcontrol结构体
参数ncontrol为初始化记录
private_data为设置的私有数据 */
struct snd_kcontrol *snd_ctl_new1(const struct snd_kcontrol_new *ncontrol, void *private_data)
/* 为声卡添加一个控制实例 */
int snd_ctl_add(struct snd_card *card, struct snd_kcontrol *kcontrol)
/* 驱动程序可中断服务程序中调用该函数来改变或更新一个control*/
void snd_ctl_notify(struct snd_card *card, unsigned int mask,struct snd_ctl_elem_id *id)
基于ALSA音频框架的驱动程序设计
1:struct snd_card *snd_card_new(int idx, const char *id,struct module *module, int extra_size);/* 创建一个声卡 */
2:static struct snd_device_ops ops = {
.dev_free = xxx_free,
};
3:struct snd_kcontrol *snd_ctl_new1(const struct snd_kcontrol_new *ncontrol,void *private_data); /* 创建control实例 */
4:int snd_ctl_add(struct snd_card *card, struct snd_kcontrol *kcontrol); /* 为声卡添加控制实例 */
5:int snd_device_new(struct snd_card *card, SNDRV_DEV_CODEC,void *device_data, struct snd_device_ops *ops); /* 创建一个ALSA设备部件-----编解码设备 */
6:int snd_pcm_new(struct snd_card *card, const char *id, int device,int playback_count, int capture_count,struct snd_pcm ** rpcm) /* 创建PCM实例 */
8:int snd_pcm_lib_preallocate_pages_for_all(struct snd_pcm *pcm,int type, void *data,size_t size, size_t max) /* 分配缓冲区 */
9:void snd_pcm_set_ops(struct snd_pcm *pcm, SNDRV_PCM_STREAM_PLAYBACK, struct snd_pcm_ops *ops) /* 设置PCM操作函数----播放 */
10:void snd_pcm_set_ops(struct snd_pcm *pcm, SNDRV_PCM_STREAM_CAPTURE, struct snd_pcm_ops *ops) /* 设置PCM操作函数 ----录音 */
11:/*音频相关的初始化:音频流相关、引脚等相关的初始化*/
12:DMA相关的设置
13:int snd_card_register(struct snd_card *card); /* 注册声卡 */IRQ_AC97
.ASoC音频驱动重要结构体
1:ASoC Codec驱动:
struct snd_soc_dai /* 数字音频接口,描述了Codec DAI(数字音频接口---Digital Audio Interface)和PCM配置 */
struct snd_soc_codec /* 音频编解码器---IO操作、动态音频电源管理以及时钟、PLL等控制 */
struct snd_soc_dai_ops /* 数字音频接口DAI操作函数集,soc接口时序设相关 */
struct snd_soc_ops /* SoC操作函数----Codec音频操作 */
*2:ASoC平台驱动:
*在ASoC平台驱动部分,同样存在着Codec驱动中的snd_soc_dai、snd_soc_dai_ops、snd_soc_ops这三个结构体的实例用于描述
*DAI和DAI的操作,不过不同的是,在平台驱动中,它们只描述CPU相关的部分而不描述Codec。除此之外,在ASoC
*平台驱动中,必须实现完整的DMA驱动,即传统ALSA的and_pcm_ops结构体成员函数trigger()、pointer()等,因此ASoC平台
*驱动通常由DAI和DMA两部分组成。
struct snd_soc_device { /* SoC设备 */
struct snd_soc_dai_link { /* 绑定ASoC Codec驱动和CPU端的平台驱动数据结构 */
二、CPU_DAI驱动分析
这个是关于soc,cpu的硬件接口dai,寄存器操作,控制时序的驱动。
1、CPU_DAI相关的重要数据
struct snd_soc_dai {
const char *name; //描述dai的名称
int id;
struct device *dev;
/* driver ops */
struct snd_soc_dai_driver *driver; //指向snd_soc_dai_driver,这个结构里描述了cpu_dai的支持类型和各种对cpu_dai进行操作的api
/* DAI runtime info */
unsigned int capture_active:1; /* stream is in use */
unsigned int playback_active:1; /* stream is in use */
unsigned int symmetric_rates:1;
unsigned int symmetric_channels:1;
unsigned int symmetric_samplebits:1;
unsigned int active;
unsigned char probed:1;
struct snd_soc_dapm_widget *playback_widget;
struct snd_soc_dapm_widget *capture_widget;
/* DAI DMA data */
void *playback_dma_data; //cpu_dai播放时使用的dma
void *capture_dma_data; //cpu_dai录制时使用的dma
/* Symmetry data - only valid if symmetry is being enforced */
unsigned int rate;
unsigned int channels;
unsigned int sample_bits;
/* parent platform/codec */
struct snd_soc_codec *codec; //指向所关联的codec设备
struct snd_soc_component *component; //指向所关联的component
/* CODEC TDM slot masks and params (for fixup) */
unsigned int tx_mask;
unsigned int rx_mask;
struct list_head list;
};
struct snd_soc_dai_driver {
/* DAI description */
const char *name;
unsigned int id;
unsigned int base;
struct snd_soc_dobj dobj;
/* DAI driver callbacks */
int (*probe)(struct snd_soc_dai *dai);
int (*remove)(struct snd_soc_dai *dai);
int (*suspend)(struct snd_soc_dai *dai); //休眠接口
int (*resume)(struct snd_soc_dai *dai); //唤醒接口
/* compress dai */
int (*compress_new)(struct snd_soc_pcm_runtime *rtd, int num);
/* Optional Callback used at pcm creation*/
int (*pcm_new)(struct snd_soc_pcm_runtime *rtd,
struct snd_soc_dai *dai);
/* DAI is also used for the control bus */
bool bus_control;
/* ops */
const struct snd_soc_dai_ops *ops; //对cpu_dai进行操作的api接口集合
const struct snd_soc_cdai_ops *cops;
/* DAI capabilities */
struct snd_soc_pcm_stream capture; //描述播放模式dai支持的参数
struct snd_soc_pcm_stream playback; //描述录制模式dai支持的参数
unsigned int symmetric_rates:1;
unsigned int symmetric_channels:1;
unsigned int symmetric_samplebits:1;
/* probe ordering - for components with runtime dependencies */
int probe_order;
int remove_order;
};
struct snd_soc_dai_ops {
/*
* DAI clocking configuration, all optional.
* Called by soc_card drivers, normally in their hw_params.
*/
//设置时钟
int (*set_sysclk)(struct snd_soc_dai *dai,
int clk_id, unsigned int freq, int dir);
//设置锁相环
int (*set_pll)(struct snd_soc_dai *dai, int pll_id, int source,
unsigned int freq_in, unsigned int freq_out);
//设置时钟分频系数
int (*set_clkdiv)(struct snd_soc_dai *dai, int div_id, int div);
int (*set_bclk_ratio)(struct snd_soc_dai *dai, unsigned int ratio);
/*
* DAI format configuration
* Called by soc_card drivers, normally in their hw_params.
*/
int (*set_fmt)(struct snd_soc_dai *dai, unsigned int fmt);
int (*xlate_tdm_slot_mask)(unsigned int slots,
unsigned int *tx_mask, unsigned int *rx_mask);
int (*set_tdm_slot)(struct snd_soc_dai *dai,
unsigned int tx_mask, unsigned int rx_mask,
int slots, int slot_width);
int (*set_channel_map)(struct snd_soc_dai *dai,
unsigned int tx_num, unsigned int *tx_slot,
unsigned int rx_num, unsigned int *rx_slot);
int (*get_channel_map)(struct snd_soc_dai *dai,
unsigned int *tx_num, unsigned int *tx_slot,
unsigned int *rx_num, unsigned int *rx_slot);
int (*set_tristate)(struct snd_soc_dai *dai, int tristate);
int (*set_sdw_stream)(struct snd_soc_dai *dai,
void *stream, int direction);
/*
* DAI digital mute - optional.
* Called by soc-core to minimise any pops.
*/
int (*digital_mute)(struct snd_soc_dai *dai, int mute);
int (*mute_stream)(struct snd_soc_dai *dai, int mute, int stream);
/*
* ALSA PCM audio operations - all optional.
* Called by soc-core during audio PCM operations.
*/
int (*startup)(struct snd_pcm_substream *,
struct snd_soc_dai *);
void (*shutdown)(struct snd_pcm_substream *,
struct snd_soc_dai *);
int (*hw_params)(struct snd_pcm_substream *,
struct snd_pcm_hw_params *, struct snd_soc_dai *);
int (*hw_free)(struct snd_pcm_substream *,
struct snd_soc_dai *);
int (*prepare)(struct snd_pcm_substream *,
struct snd_soc_dai *);
/*
* NOTE: Commands passed to the trigger function are not necessarily
* compatible with the current state of the dai. For example this
* sequence of commands is possible: START STOP STOP.
* So do not unconditionally use refcounting functions in the trigger
* function, e.g. clk_enable/disable.
*/
int (*trigger)(struct snd_pcm_substream *, int,
struct snd_soc_dai *);
int (*bespoke_trigger)(struct snd_pcm_substream *, int,
struct snd_soc_dai *);
/*
* For hardware based FIFO caused delay reporting.
* Optional.
*/
snd_pcm_sframes_t (*delay)(struct snd_pcm_substream *,
struct snd_soc_dai *);
};
看下关于soc接口部分,dts配置这些参数是怎么设置的,单片机是写死在i2s接口文件里,linux写在dts,方便修改适配
i2s1_8ch: i2s@fe410000 {
compatible = "rockchip,rk3568-i2s-tdm";
reg = <0x0 0xfe410000 0x0 0x1000>;
interrupts = <GIC_SPI 53 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&cru MCLK_I2S1_8CH_TX>, <&cru MCLK_I2S1_8CH_RX>, <&cru HCLK_I2S1_8CH>;
clock-names = "mclk_tx", "mclk_rx", "hclk";
dmas = <&dmac1 2>, <&dmac1 3>;
dma-names = "tx", "rx";
resets = <&cru SRST_M_I2S1_8CH_TX>, <&cru SRST_M_I2S1_8CH_RX>;
reset-names = "tx-m", "rx-m";
rockchip,cru = <&cru>;
rockchip,grf = <&grf>;
#sound-dai-cells = <0>;
pinctrl-names = "default";
pinctrl-0 = <&i2s1m0_sclktx
&i2s1m0_sclkrx
&i2s1m0_lrcktx
&i2s1m0_lrckrx
&i2s1m0_sdi0
&i2s1m0_sdi1
&i2s1m0_sdi2
&i2s1m0_sdi3
&i2s1m0_sdo0
&i2s1m0_sdo1
&i2s1m0_sdo2
&i2s1m0_sdo3>;
status = "disabled";
};
3、rockchip_i2s_tdm_probe分析
cpu_dai驱动从dts获取到cpu_dai硬件信息后,进入 rockchip_i2s_tdm_probe函数,进行一系列cpu_dai的硬件初始化工作
static int rockchip_i2s_tdm_probe(struct platform_device *pdev)
{
...
//初始化snd_soc_dai_driver结构,dai的支持配置和各种用于控制dai的api接口
rockchip_i2s_tdm_dai_prepare(pdev, &soc_dai);
//从设备树获取bclk_fs值,没有默认64
i2s_tdm->bclk_fs = 64;
if (!of_property_read_u32(node, "rockchip,bclk-fs", &val)) {
if ((val >= 32) && (val % 2 == 0))
i2s_tdm->bclk_fs = val;
}
//从设备树中读取各种初始化cpu_dai相关的设置保存到i2s_tdm,例如I2S的时钟参数等
of_property_read_u32(node, ..., &val);
devm_reset_control_get(&pdev->dev, ...);
devm_clk_get(&pdev->dev, "hclk")
devm_clk_get(&pdev->dev, "mclk_tx");
devm_clk_get(&pdev->dev, "mclk_rx")
//获取cpu_dai的寄存器地址,并初始化cpu_dai
platform_get_resource(pdev, IORESOURCE_MEM, 0);
devm_ioremap_resource(&pdev->dev, res);
//初始化regmap
devm_regmap_init_mmio(&pdev->dev, regs, &rockchip_i2s_tdm_regmap_config);
//TX/RX的dma参数获取
i2s_tdm->playback_dma_data.addr = res->start + I2S_TXDR;
i2s_tdm->playback_dma_data.addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
i2s_tdm->playback_dma_data.maxburst = 8;
i2s_tdm->capture_dma_data.addr = res->start + I2S_RXDR;
i2s_tdm->capture_dma_data.addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
i2s_tdm->capture_dma_data.maxburst = 8;
//初始化cpu_dai的tx和rx通路
rockchip_i2s_tdm_tx_path_prepare(i2s_tdm, node);
rockchip_i2s_tdm_rx_path_prepare(i2s_tdm, node);
i2s_tdm->soc_data->init(&pdev->dev, res->start);
//注册component组件
devm_snd_soc_register_component(&pdev->dev,&rockchip_i2s_tdm_component,soc_dai, 1);
- >snd_soc_register_component(dev, cmpnt_drv, dai_drv, num_dai);
//获取dma
devm_snd_dmaengine_pcm_register(&pdev->dev, NULL, 0);
->snd_soc_add_platform(dev, &pcm->platform,&dmaengine_pcm_platform);
return 0;
}
snd_soc_register_component(dev, cmpnt_drv, dai_drv, num_dai)->
snd_soc_component_initialize(cmpnt, cmpnt_drv, dev); //初始化component
//注册了一个dai,并填充snd_soc_dai_driver数据
snd_soc_register_dais(cmpnt, dai_drv, num_dai, true);
//将component加入component_list链表
snd_soc_component_add(cmpnt);
该函数被传入两个关键参数struct snd_soc_component_driver cmpnt_drv、struct snd_soc_dai_driver dai_drv,定义如下
static const struct snd_soc_component_driver rockchip_i2s_component = {
.name = DRV_NAME,
};
struct snd_soc_dai_driver rockchip_i2s_tdm_dai = {
.probe = rockchip_i2s_tdm_dai_probe,
.playback = {
.stream_name = "Playback",
.channels_min = 2,
.channels_max = 16,
.rates = SNDRV_PCM_RATE_8000_192000,
.formats = (SNDRV_PCM_FMTBIT_S8 |
SNDRV_PCM_FMTBIT_S16_LE |
SNDRV_PCM_FMTBIT_S20_3LE |
SNDRV_PCM_FMTBIT_S24_LE |
SNDRV_PCM_FMTBIT_S32_LE),
},
.capture = {
.stream_name = "Capture",
.channels_min = 2,
.channels_max = 16,
.rates = SNDRV_PCM_RATE_8000_192000,
.formats = (SNDRV_PCM_FMTBIT_S8 |
SNDRV_PCM_FMTBIT_S16_LE |
SNDRV_PCM_FMTBIT_S20_3LE |
SNDRV_PCM_FMTBIT_S24_LE |
SNDRV_PCM_FMTBIT_S32_LE),
},
//对cpu_dai的api操作集合,如dai的时钟配置、格式配置等
.ops = &rockchip_i2s_tdm_dai_ops,
};
dmaengine_pcm_request_chan_of(pcm, dev, config);
//根据name去dts中找资源,申请对应的DMA通道
chan = dma_request_slave_channel_reason(dev, name);
return of_dma_request_slave_channel(dev->of_node, name);
//注册platform到ASoC Core
ret = snd_soc_add_platform(dev, &pcm->platform,&dmaengine_pcm_platform);
platform中需要完成音频数据管理和音频数据的dma搬运,其中就涉及到了dma相关的操作,snd_soc_add_platform的目的就是完成dma通道的申请,并将pcm和dma关联起来
四、CODEC驱动分析
1、CODEC相关的重要数据
static const struct regmap_config rk817_codec_regmap_config = {
.name = "rk817-codec",
.reg_bits = 8, //寄存器地址位数(必选项)
.val_bits = 8, //寄存器值位数(必选项)
.reg_stride = 1,
.max_register = 0x4f, //最大寄存器值
.cache_type = REGCACHE_FLAT,
.volatile_reg = rk817_volatile_register,
.writeable_reg = rk817_codec_register, //寄存器是否可写
.readable_reg = rk817_codec_register, //寄存器是否可写读
.reg_defaults = rk817_reg_defaults, //默认寄存器配置参数
.num_reg_defaults = ARRAY_SIZE(rk817_reg_defaults),
};
2、配置codex的DTS
rk809_codec: codec {
#sound-dai-cells = <0>;
compatible = "rockchip,rk809-codec", "rockchip,rk817-codec";
clocks = <&cru I2S1_MCLKOUT>;
clock-names = "mclk";
assigned-clocks = <&cru I2S1_MCLKOUT>, <&cru I2S1_MCLK_TX_IOE>;
assigned-clock-rates = <12288000>;
assigned-clock-parents = <&cru I2S1_MCLKOUT_TX>, <&cru I2S1_MCLKOUT_TX>;
pinctrl-names = "default","spk_gpio";
pinctrl-0 = <&i2s1m0_mclk>;
pinctrl-1 = <&spk_ctl_gpio>;
hp-volume = <20>;
spk-volume = <3>;
//mic-in-differential;
capture-volume = <0>;
io-channels = <&saradc 4>;
hp-det-adc-value = <1000>;
spk-ctl-gpios = <&gpio3 RK_PC5 GPIO_ACTIVE_HIGH>;
status = "okay";
};
3、rk817_platform_probe分析
codec_dai驱动从dts获取到硬件信息后,通过platform平台框架注册的,然后进入rk817_platform_probe探测设备,;匹配成功后,进行一系列codec_dai的硬件初始化工作
static int rk817_platform_probe(struct platform_device *pdev)
{
...
rk817_codec_data = devm_kzalloc(&pdev->dev,sizeof(struct rk817_codec_priv),
GFP_KERNEL); //给rk817_codec_data申请空间
platform_set_drvdata(pdev, rk817_codec_data); //rk817_codec_data绑定pdev
//从dts节点中获取adc通道、gpio节点、volume值等
rk817_codec_parse_dt_property(&pdev->dev, rk817_codec_data);
//regmap初始化
rk817_codec_data->regmap = devm_regmap_init_i2c(rk817->i2c,&rk817_codec_regmap_config);
//mclk获取
rk817_codec_data->mclk = devm_clk_get(&pdev->dev, "mclk");
//componen注册
devm_snd_soc_register_component(&pdev->dev, &soc_codec_dev_rk817,rk817_dai, ARRAY_SIZE(rk817_dai));
...
}
devm_snd_soc_register_component后续分析:
初始化snd_soc_component的实例,然后注册dai,最终将注册的dai放入component->dai_list中,然后将分配的component放入到component_list链表中,遍历全局的component链表可以找到cpu_dai,codec_dai,起到作用就是将他们两绑定起来。
devm_snd_soc_register_component->
snd_soc_register_component->
snd_soc_add_component->
snd_soc_component_initialize //component初始化
snd_soc_register_dais //dai注册
snd_soc_component_add //component加入全局链表
五、MACHINE(simple-card.c)驱动分析
1、MACHINE相关的重要数据
struct snd_soc_dai_link {
/* config - must be set by machine driver */
const char *name; /* Codec name */
const char *stream_name; /* Stream name */
/*
* You MAY specify the link's CPU-side device, either by device name,
* or by DT/OF node, but not both. If this information is omitted,
* the CPU-side DAI is matched using .cpu_dai_name only, which hence
* must be globally unique. These fields are currently typically used
* only for codec to codec links, or systems using device tree.
*/
const char *cpu_name;
struct device_node *cpu_of_node;
/*
* You MAY specify the DAI name of the CPU DAI. If this information is
* omitted, the CPU-side DAI is matched using .cpu_name/.cpu_of_node
* only, which only works well when that device exposes a single DAI.
*/
const char *cpu_dai_name;
/*
* You MUST specify the link's codec, either by device name, or by
* DT/OF node, but not both.
*/
const char *codec_name;
struct device_node *codec_of_node;
/* You MUST specify the DAI name within the codec */
const char *codec_dai_name;
struct snd_soc_dai_link_component *codecs;
unsigned int num_codecs;
/*
* You MAY specify the link's platform/PCM/DMA driver, either by
* device name, or by DT/OF node, but not both. Some forms of link
* do not need a platform.
*/
const char *platform_name;
struct device_node *platform_of_node;
int id; /* optional ID for machine driver link identification */
const struct snd_soc_pcm_stream *params;
unsigned int num_params;
unsigned int dai_fmt; /* format to set on init */
enum snd_soc_dpcm_trigger trigger[2]; /* trigger type for DPCM */
/* codec/machine specific init - e.g. add machine controls */
int (*init)(struct snd_soc_pcm_runtime *rtd);
/* optional hw_params re-writing for BE and FE sync */
int (*be_hw_params_fixup)(struct snd_soc_pcm_runtime *rtd,
struct snd_pcm_hw_params *params);
/* machine stream operations */
const struct snd_soc_ops *ops;
const struct snd_soc_compr_ops *compr_ops;
/* Mark this pcm with non atomic ops */
bool nonatomic;
/* For unidirectional dai links */
unsigned int playback_only:1;
unsigned int capture_only:1;
/* Keep DAI active over suspend */
unsigned int ignore_suspend:1;
/* Symmetry requirements */
unsigned int symmetric_rates:1;
unsigned int symmetric_channels:1;
unsigned int symmetric_samplebits:1;
/* Do not create a PCM for this DAI link (Backend link) */
unsigned int no_pcm:1;
/* This DAI link can route to other DAI links at runtime (Frontend)*/
unsigned int dynamic:1;
/* This DAI link can be reconfigured at runtime (Backend) */
unsigned int dynamic_be:1;
/*
* This DAI can support no host IO (no pcm data is
* copied to from host)
*/
unsigned int no_host_mode:2;
/* DPCM capture and Playback support */
unsigned int dpcm_capture:1;
unsigned int dpcm_playback:1;
/* DPCM used FE & BE merged format */
unsigned int dpcm_merged_format:1;
/* DPCM used FE & BE merged channel */
unsigned int dpcm_merged_chan:1;
/* DPCM used FE & BE merged rate */
unsigned int dpcm_merged_rate:1;
/* pmdown_time is ignored at stop */
unsigned int ignore_pmdown_time:1;
/* Do not create a PCM for this DAI link (Backend link) */
unsigned int ignore:1;
struct list_head list; /* DAI link list of the soc card */
struct snd_soc_dobj dobj; /* For topology */
/* this value determines what all ops can be started asynchronously */
enum snd_soc_async_ops async_ops;
};
struct snd_soc_card {
const char *name;
const char *long_name;
const char *driver_name;
char dmi_longname[80];
char topology_shortname[32];
struct device *dev;
struct snd_card *snd_card;
struct module *owner;
struct mutex mutex;
struct mutex dapm_mutex;
struct mutex dapm_power_mutex;
bool instantiated;
bool topology_shortname_created;
int (*probe)(struct snd_soc_card *card);
int (*late_probe)(struct snd_soc_card *card);
int (*remove)(struct snd_soc_card *card);
/* the pre and post PM functions are used to do any PM work before and
* after the codec and DAI's do any PM work. */
int (*suspend_pre)(struct snd_soc_card *card);
int (*suspend_post)(struct snd_soc_card *card);
int (*resume_pre)(struct snd_soc_card *card);
int (*resume_post)(struct snd_soc_card *card);
/* callbacks */
int (*set_bias_level)(struct snd_soc_card *,struct snd_soc_dapm_context *dapm,
enum snd_soc_bias_level level);
int (*set_bias_level_post)(struct snd_soc_card *,struct snd_soc_dapm_context *dapm,
enum snd_soc_bias_level level);
int (*add_dai_link)(struct snd_soc_card *,
struct snd_soc_dai_link *link);
void (*remove_dai_link)(struct snd_soc_card *,
struct snd_soc_dai_link *link);
long pmdown_time;
/* CPU <--> Codec DAI links */
struct snd_soc_dai_link *dai_link; /* predefined links only */
int num_links; /* predefined links only */
struct list_head dai_link_list; /* all links */
int num_dai_links;
struct list_head rtd_list;
int num_rtd;
/* optional codec specific configuration */
struct snd_soc_codec_conf *codec_conf;
int num_configs;
/*
* optional auxiliary devices such as amplifiers or codecs with DAI
* link unused
*/
struct snd_soc_aux_dev *aux_dev;
int num_aux_devs;
struct list_head aux_comp_list;
const struct snd_kcontrol_new *controls;
int num_controls;
/*
* Card-specific routes and widgets.
* Note: of_dapm_xxx for Device Tree; Otherwise for driver build-in.
*/
const struct snd_soc_dapm_widget *dapm_widgets;
int num_dapm_widgets;
const struct snd_soc_dapm_route *dapm_routes;
int num_dapm_routes;
const struct snd_soc_dapm_widget *of_dapm_widgets;
int num_of_dapm_widgets;
const struct snd_soc_dapm_route *of_dapm_routes;
int num_of_dapm_routes;
bool fully_routed;
struct work_struct deferred_resume_work;
/* lists of probed devices belonging to this card */
struct list_head component_dev_list;
struct list_head widgets;
struct list_head paths;
struct list_head dapm_list;
struct list_head dapm_dirty;
/* attached dynamic objects */
struct list_head dobj_list;
/* Generic DAPM context for the card */
struct snd_soc_dapm_context dapm;
struct snd_soc_dapm_stats dapm_stats;
struct snd_soc_dapm_update *update;
#ifdef CONFIG_DEBUG_FS
struct dentry *debugfs_card_root;
struct dentry *debugfs_pop_time;
#endif
u32 pop_time;
void *drvdata;
};
DTS
rk809_sound: rk809-sound {
status = "okay";
compatible = "simple-audio-card";
simple-audio-card,format = "i2s";
simple-audio-card,name = "rockchip,rk809-codec";
simple-audio-card,mclk-fs = <256>;
simple-audio-card,widgets =
"Microphone", "Mic Jack",
"Headphone", "Headphone Jack";
simple-audio-card,routing =
"Mic Jack", "MICBIAS1",
"IN1P", "Mic Jack",
"Headphone Jack", "HPOL",
"Headphone Jack", "HPOR";
simple-audio-card,cpu {
sound-dai = <&i2s1_8ch>;
};
simple-audio-card,codec {
sound-dai = <&rk809_codec>;
};
};
3、asoc_simple_card_probe分析
codec_dai驱动从dts获取到cpu_dai和codec_dai的连接关系和节点后,进入asoc_simple_card_probe,进行声卡的注册和初始化工作
static int asoc_simple_card_probe(struct platform_device *pdev)
{
struct simple_card_data *priv;
struct snd_soc_dai_link *dai_link;
struct simple_dai_props *dai_props;
struct device *dev = &pdev->dev;
struct device_node *np = dev->of_node;
struct snd_soc_card *card;
int num, ret;
...
//获取节点"simple-audio-card,dai-link"
//由于节点不存在 num = 1
if (np && of_get_child_by_name(np, PREFIX "dai-link"))
num = of_get_child_count(np);
}else{ num = 1; }
//申请 priv\dai_props\dai_link的内存空间
//priv->dai_props priv->dai_link
priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
dai_props = devm_kcalloc(dev, num, sizeof(*dai_props), GFP_KERNEL);
dai_link = devm_kcalloc(dev, num, sizeof(*dai_link), GFP_KERNEL);
//初始化snd_soc_card
card = simple_priv_to_card(priv); //card=&(priv->snd_card)
card->owner = THIS_MODULE;
card->dev = dev;
card->dai_link = priv->dai_link;
card->num_links = num;
card->probe = asoc_simple_soc_card_probe;
//如果simple-card 存在
//对priv->dai_link priv->snd_card 部分填充和初始化
if (np && of_device_is_available(np)){
asoc_simple_card_parse_of(priv); //详细分析在下
} else {...};
//实例化和填充card,绑定dailink,创建音频节点pcmC0D0P,并完成cpu_dai和codec的probe
ret=devm_snd_soc_register_card(dev, card)->
return snd_soc_register_card(card); //详细分析在下
return ret;
}
asoc_simple_card_parse_ of ( priv )分析如下:
static int asoc_simple_card_parse_of(struct simple_card_data *priv)
{
//获取节点"simple-audio-card,dai-link"
//不存在dai_link=NULL
struct device_node *dai_link;
dai_link = of_get_child_by_name(node, PREFIX "dai-link");
/*
从设备树解析声卡的widgets
card->of_dapm_widgets = widgets;
-----card->of_dapm_widgets[0].id = snd_soc_dapm_mic;
-----card->of_dapm_widgets[0].name = "Microphone";
-----card->of_dapm_widgets[0].event = "Mic Jack";
card->num_of_dapm_widgets = num_widgets;
-----num_of_dapm_widgets = 2;
*/
ret = asoc_simple_card_of_parse_widgets(card, PREFIX);
/*
从设备树解析声卡的route
card->num_of_dapm_routes = num_routes;
-----num_of_dapm_routes = 4;
card->of_dapm_routes = routes;
-----card->of_dapm_routes[0].sink = "Mic Jack";
-----card->of_dapm_routes[0].sourece = "MICBIAS1"
-----card->of_dapm_routes[1].sink = "IN1P";
-----card->of_dapm_routes[1].sourece = "Mic Jack"
*/
ret = asoc_simple_card_of_parse_routing(card, PREFIX, 1);
if (dai_link) {
...
}else{
/* 以下列出函数功能*/
/*1 card->dai_link->dai_fmt = i2s
simple-audio-card,format = "i2s"
*/
/*2 card->dai_link->cpu_of_node= "<&i2s1_8ch>"
single_cpu=1
*/
/*3 card->dai_link->codec_of_node = "<&rk809_codec>"
*/
/*4 require clk,codec_dai->clk = "clk of i2s"
cpu_dai->systemclk = clk_get_rate(clk);
cpu_dai->clk_direction = SND_SOC_CLOCK_IN;
*/
/*5 require clk,codec_dai->clk = "I2S_MCSL_OUT"
codec_dai->systemclk = clk_get_rate(clk);
codec_dai->clk_direction = SND_SOC_CLOCK_IN;
*/
/*6 dai_link->codec_dai_name = "tlv320aic31xx-hifi"
static struct snd_soc_dai_driver
aic31xx_dai_driver[] = {
{
.name = "tlv320aic31xx-hifi",
.playback = {
...
},
.capture = {
...
},
.ops = &aic31xx_dai_ops,
.symmetric_rates = 1,
}
};
cpu_dai_name 由于dai->driver的名字不存在
所以 dai_link->codec_dai_name = "rockchip-i2s-tdm"
struct snd_soc_component_driver
rockchip_i2s_tdm_component = {
.name = DRV_NAME,
};
*/
/*7 dai_link->name =
"rockchip-i2s-tdm-tlv320aic31xx-hifi"
dai_link->stream_name =
"rockchip-i2s-tdm-tlv320aic31xx-hifi"
*/
/*8 dai_link->cpu_name = NULL;
dai_link->codec_name = NULL;
*/
ret = asoc_simple_card_dai_link_of(node, priv, 0, true);
}
//card->name = "rockchip,tlv320aic31xx-codec";
//simple-audio-card,name = "rockchip,tlv320aic31xx-codec";
ret = asoc_simple_card_parse_card_name(card, PREFIX);
...
}
snd_soc_register_card的分析流程如下:
int snd_soc_register_card(struct snd_soc_card *card)
{
struct snd_soc_pcm_runtime *rtd;
//num_links == 1
for (i = 0; i < card->num_links; i++) {
struct snd_soc_dai_link *link = &card->dai_link[i];
//dai_link->codecs[0].name = dai_link->codec_name;
//dai_link->codecs[0].of_node = dai_link->codec_of_node;
//dai_link->codecs[0].dai_name = dai_link->codec_dai_name;
ret = soc_init_dai_link(card, link);
}
//card->dev->driver_data = card;
dev_set_drvdata(card->dev, card);
//card相关链表头的初始化
INIT_LIST_HEAD(&card->widgets);
INIT_LIST_HEAD(&card->paths);
INIT_LIST_HEAD(&card->dapm_list);
INIT_LIST_HEAD(&card->aux_comp_list);
INIT_LIST_HEAD(&card->component_dev_list);
INIT_LIST_HEAD(&card->dai_link_list);
INIT_LIST_HEAD(&card->rtd_list);
INIT_LIST_HEAD(&card->dapm_dirty);
INIT_LIST_HEAD(&card->dobj_list);
//card相关锁的初始化
mutex_init(&card->mutex);
mutex_init(&card->dapm_mutex);
mutex_init(&card->dapm_power_mutex);
//声卡的部分参数实例化填充以及声卡的注册
ret = snd_soc_instantiate_card(card); //详细分析在下
...
}
snd_soc_instantiate_card的分析流程如下:
static int snd_soc_instantiate_card(struct snd_soc_card *card)
{
struct snd_soc_pcm_runtime *rtd;
struct snd_soc_dai_link *dai_link;
//num_links==1
for (i = 0; i < card->num_links; i++) {
//详细分析在下
//找到cpu/codec的snd_soc_dai
ret = soc_bind_dai_link(card, &card->dai_link[i]);
}
//将card->list加入到card->dai_link_list
snd_soc_add_dai_link(card, card->dai_link+i);
//创建新的声卡
//sprintf(str, "card%i", card->number);
//entry = snd_info_create_subdir(card->module, str, NULL);
ret = snd_card_new(card->dev, SNDRV_DEFAULT_IDX1,
SNDRV_DEFAULT_STR1,card->owner, 0, &card->snd_card);
//添加widgets
if (card->of_dapm_widgets)
snd_soc_dapm_new_controls(&card->dapm,
card->of_dapm_widgets,card->num_of_dapm_widgets);
...
//执行probe函数asoc_simple_soc_card_probe
card->probe(card);
//执行cpu和codec的component->driver->probe函数
//component->driver->probe(component)
for(...)
ret = soc_probe_link_components(card, rtd, order);
//执行cpu和codec的dai->driver->probe函数 (一般未定义)
//执行dai_link->init asoc_simple_card_dai_init
//执行cpu和codec的dai->driver->ops->set_fmt
//创建pcm soc_new_pcm(rtd, num) 初始化rtd->ops
//rtd->pcm = pcm;
for(...)
ret = soc_probe_link_dais(card, rtd, order);
//添加route
if (card->of_dapm_routes)
snd_soc_dapm_add_routes(&card->dapm, card->of_dapm_routes,card->num_of_dapm_routes);
//声卡名称填充 cat /proc/asound/cards
snprintf(card->snd_card->shortname,
sizeof(card->snd_card->shortname),"%s", card->name);
snprintf(card->snd_card->driver,sizeof(card->snd_card->driver),
"%s", card->driver_name ? card->driver_name : card->name);
...
//注册声卡,注册回调函数,如上面提到的snd_pcm_dev_register函数
//该函数建立设备节点:/dev/snd/pcmCxxDxxp和 /dev/snd/pcmCxxDxxc
snd_card_register(card->snd_card);
}
static int soc_bind_dai_link的分析流程如下:
static int soc_bind_dai_link(struct snd_soc_card *card,struct snd_soc_dai_link *dai_link)
{
struct snd_soc_pcm_runtime *rtd;
//遍历card->rtd_list
//如果rtd->dai_link == dai_link,表明当前的dai_link已经绑定
if (soc_is_dai_link_bound(card, dai_link)) {
return 0;
}
//rtd申请和填充
//rtd->card = card;
//rtd->dai_link = dai_link;
/*rtd->codec_dais = kcalloc(dai_link->num_codecs,
sizeof(struct snd_soc_dai *),GFP_KERNEL);
*/
rtd = soc_new_pcm_runtime(card, dai_link);
//从component链表中找到已经注册的cpu snd_soc_dai,并填充rtd
//并将cpu dai的component链表加入rtd的component链表
rtd->cpu_dai = snd_soc_find_dai(&cpu_dai_component);
snd_soc_rtdcom_add(rtd, rtd->cpu_dai->component);
//rtd->num_codecs = dai_link->num_codecs =1;
//从component链表中找到已经注册的codec snd_soc_dai,并填充rtd
//并将codec dai的component链表加入rtd的component链表
rtd->num_codecs = dai_link->num_codecs
for (i = 0; i < rtd->num_codecs; i++) {
codec_dais[i] = snd_soc_find_dai(&codecs[i]);
snd_soc_rtdcom_add(rtd, codec_dais[i]->component);
}
//将该rtd的链表加入到card->rtd_list中
soc_add_pcm_runtime(card, rtd);
}
六、ALSA-Probe的流程
下图是板子内核启动到probe完成阶段的流程图,可以看到ALSA初始化音频的整个流程是怎么初始化声卡,和配置一些基本参数的
step 1:
cpu_dai_probe:获取设备树有关I2S的硬件描述信息,如时钟参数,dma通道参数,regmap_i2c初始化,然后 devm_snd_soc_register_component注册cpu_dai,调用 devm_snd_dmaengine_pcm_register注册dmaengine
codec_dai_probe:硬件资源的获取,regmap_i2c初始化,然后调用devm_snd_soc_register_component注册
asoc_simple_card_probe:dai_link获取,获取cpu_dai,codec_dai的信息,初始化函数接口,调用devm_snd_soc_register_card执行声卡的注册,注册过程中依次执行具体设备的probe,声卡的probe,cpu_dai probe ,codec_probe
step 2:
rk817_probe: snd_soc_add_component_controls,设置音频通路相关的寄存器
rockchip_i2s_tdm_dai_probe: snd_soc_add_dai_controls ,设置I2S相关的寄存器
asoc_simple_soc_card_probe: asoc_simple_card_init_hp ,asoc_simple_card_init_hp ?
asoc_simple_card_hw_params : rockchip_i2s_tdm_set_sysclk设置I2S的时钟参数,rk817_set_dai_sysclk设置codec一层具体的时钟参数
snd_soc_dai_set_fmt:rk817_set_dai_fmt 设置音频数据格式,是I2S还是PCM,CODEC是master还是slave
七、Tinyalsa pcm_open的调用流程
应用程序,调用pcm格式,操控声卡的一些操作,先看下pcm的fops,接口如下,定义在pcm_native.c
const struct file_operations snd_pcm_f_ops[2] = {
{
.owner = THIS_MODULE,
.write = snd_pcm_write,
.write_iter = snd_pcm_writev,
.open = snd_pcm_playback_open,
.release = snd_pcm_release,
.llseek = no_llseek,
.poll = snd_pcm_playback_poll,
.unlocked_ioctl = snd_pcm_playback_ioctl,
.compat_ioctl = snd_pcm_ioctl_compat,
.mmap = snd_pcm_mmap,
.fasync = snd_pcm_fasync,
.get_unmapped_area = snd_pcm_get_unmapped_area,
},
{
.owner = THIS_MODULE,
.read = snd_pcm_read,
.read_iter = snd_pcm_readv,
.open = snd_pcm_capture_open,
.release = snd_pcm_release,
.llseek = no_llseek,
.poll = snd_pcm_capture_poll,
.unlocked_ioctl = snd_pcm_capture_ioctl,
.compat_ioctl = snd_pcm_ioctl_compat,
.mmap = snd_pcm_mmap,
.fasync = snd_pcm_fasync,
.get_unmapped_area = snd_pcm_get_unmapped_area,
}
};
substream是pcm ops的下一层,绝大部分任务都是在substream中处理,substream的ops定义在soc-pcm.c,定义如下
int soc_new_pcm(struct snd_soc_pcm_runtime *rtd, int num)
{
...
/* ASoC PCM operations */
if (rtd->dai_link->dynamic) {
rtd->ops.open = dpcm_fe_dai_open;
rtd->ops.hw_params = dpcm_fe_dai_hw_params;
rtd->ops.prepare = dpcm_fe_dai_prepare;
rtd->ops.trigger = dpcm_fe_dai_trigger;
rtd->ops.hw_free = dpcm_fe_dai_hw_free;
rtd->ops.close = dpcm_fe_dai_close;
rtd->ops.pointer = soc_pcm_pointer;
rtd->ops.ioctl = soc_pcm_ioctl;
} else {
rtd->ops.open = soc_pcm_open;
rtd->ops.hw_params = soc_pcm_hw_params;
rtd->ops.prepare = soc_pcm_prepare;
rtd->ops.trigger = soc_pcm_trigger;
rtd->ops.hw_free = soc_pcm_hw_free;
rtd->ops.close = soc_pcm_close;
rtd->ops.pointer = soc_pcm_pointer;
rtd->ops.ioctl = soc_pcm_ioctl;
}
...
if (playback)
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &rtd->ops);
if (capture)
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &rtd->ops);
...
}
step1:设置时钟参数
step2:设置音频参数,根据上层设置的采样率,数据格式来配置codec寄存器,并开启时钟
八、Tinyalsa pcm_write的调用流程
下图使用tinyalsa测试,可以看到alsa-driver在pcm_write阶段做了哪些工作,流程是什么
原文链接:https://blog.csdn.net/QQ135102692/article/details/125348020
到此,linux提供的一天音频管理架构就分析完了。
这篇文章太长了,移植一个codex,我们怎么修改dts的,怎么用alsa-lib来采集声音的,下篇将。