Linux驱动开发之音频驱动与基础应用编程

目录

CODEC芯片

音频编码

I2S总线接口

数字音频接口(DAI)

设备树配置

ALSA

音频相关概念

应用程序编写

运行测试

CODEC芯片

音频若想被CPU"听到",就必须转换为CPU能够"听懂"的语言,即二进制数据的0和1。在信号处理领域,声音是模拟信号而二进制数据是数字信号,因此需要一个将模拟信号转化为数字信号的器件,而完成这个功能的就是ADC芯片。同理,CPU若想向外传达声音,就需要将数字信号转化为模拟信号,而完成这个功能的则是 DAC 芯片。将ADC和DAC芯片以及一系列其他单元叠加到一起就形成了专门用于音频处理的芯片,即音频编解码芯片(Audio CODEC),如ES8388,ES7202和rt5640等。不同CODEC芯片支持的录音/放音功能有所不同,其对应的引脚接口也会不同,但总体上CODEC对外接口可分为控制接口和数据接口两大部分,一般对应I2C和I2S。荣品RK3588开发板上使用的是rt5640,其原理图对应部分如下:

可以看出该CODEC通过AUDIO_SCL和AUDIO_SDA与CPU的I2C相连,通过I2C对其进行配置,并通过I2S接口进行数据传输。具有两路输入,两路扬声器输出以及一路耳机输出。

音频编码

音频编码一般采用脉冲编码调制(PCM)的方法,PCM是Pulse Code Modulation的缩写,该编码方法是对语音信号进行采样,然后对每个采样值进行量化编码,我们所熟知的CD音频即是使用PCM编码。使用PCM编码的音频文件在windows下的保存格式也是我们所熟知的WAV格式。

在PCM基础上发展起来的还有自适应差分脉冲编码调制ADPCM,其编码的方法是对输入样值进行自适应预测,然后对预测误差进行量化编码。除此之外,其他编码方式还有线性预测编码LPC,低延迟码激励线性预测编码LD-CELP等。而除了WAV格式,还有MP3,WMA和RAW等编码格式。

I2S总线接口

I2S(Inter-IC Sound)总线有时候也写作IIS,是飞利浦公司提出的一种用于数字音频设备之间进行音频数据传输的总线。和I2C、SPI这些常见的通信协议一样,I2S总线用于主控制器和音频CODEC芯片之间传输音频数据。因此,要想使用I2S协议,主控制器和音频CODEC 都得支持I2S协议。作为一种数字音频设备之间进行音频数据传输的总线标准,I2S一般有3~5根物理连接线:

  • SCK:串行时钟信号,为串行数据提供位时钟(BCLK),音频数据的每一位数据都对应一个SCK,而立体声都是双声道的,因此 SCK=2×采样率×采样位数。
  • WS:字段(声道)选择信号,也叫做LRCK,用于切换左右声道数据帧,WS为"1"表示正在传输左声道的数据,WS为"0"表示正在传输右声道的数据,其频率等于采样率。
  • SD:串行数据信号,也就是实际传输的音频数据,如果要同时实现放音和录音,那么就需要2根数据线IISDI和IISDO,分别用于录音和放音。
  • MCLK:为了使音频 CODEC 芯片与主控制器之间能够更好的同步而引入的信号,也叫做同步时钟或编码器时钟,一般是采样率的256倍或384倍。

数据的发送方和接收方需要有一个时钟信号来控制数据的传输,因此数据发送方(主设备)必须提供字段选择信号、时钟信号和数据信号。当有多个发送方和接收方,系统需引入控制模块,用于控制数字音频数据在不同设备之间的传输。传输模式示意图如下:

I2S总线协议也即音频数据时序图如下:

随着技术的发展,在统一的I2S接口下,出现了不同的数据格式,根据音频数据相对于LRCK 和SCK位置的不同,可分为LeftJustified(左对齐)和 RightJustified(右对齐)两种格式。

数字音频接口(DAI)

音频CODEC支持I2S协议,那么主控制器也必须支持I2S协议,而主控制器则是通过音频接口来连接CODEC,这个接口在RK平台上的外设则为数字音频接口(DAI)。RK平台有两种I2S控制器:I2S和I2S-TDM。I2S控制器支持I2S, PCM协议;I2S-TDM控制器支持 I2S,PCM, TDM 协议。除此之外,RK平台还具有PDM控制器,支持PDM协议的数字麦或者ADC;支持数字CODEC接口,可对接支持该协议的模拟CODEC组合成完整CODEC;支持语音活性检测 (Voice Activity Detection),VAD接收来自DAI的数据,处理统计分析,达到预设阈值时,触发中断,唤醒系统;还支持支持 SPDIF Transmitter 接口协议等。并且RK 平台支持任意 DAI 的组合使用,重组 DAI 生成 Combo DAI,如下图所示:

一般来说,RK发布的SDK中已经完成了DAI驱动的编写,开发者只需要根据应用场景配置属性启用相应功能即可。相关驱动文件基本都在目录kernel/sound/soc/rockchip下。而machine驱动部分主要涉及声卡的添加,其中Simple Card是ASoC通用的machine driver,可支持大部分标准声卡的添加。

设备树配置

荣品RK3588开发板上使用的是rt5640,其相关设备树配置如下:

cpp 复制代码
/ {
        rt5640-sound {
                compatible = "simple-audio-card";
                simple-audio-card,format = "i2s";
                simple-audio-card,name = "rockchip,rt5640-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 = <&i2s0_8ch>;
                };
                simple-audio-card,codec {
                        sound-dai = <&rt5640>;
                };
        };

       rk_headset: rk-headset {
        status = "okay";
        compatible = "rockchip_headset";
        headset_gpio = <&gpio1 RK_PC4 GPIO_ACTIVE_HIGH>;
        pinctrl-names = "default";
        pinctrl-0 = <&hp_det>;
    };
};

&i2s0_8ch {
    status = "okay";
};

&i2c7 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&i2c7m0_xfer>;

    rt5640: rt5640@1c {
        #sound-dai-cells = <0>;
        compatible = "realtek,rt5640";
        reg = <0x1c>;
        clocks = <&mclkout_i2s0>;
        clock-names = "mclk";
        realtek,in1-differential;
        pinctrl-names = "default";
        pinctrl-0 = <&i2s0_mclk>;
        io-channels = <&saradc 4>;
        hp-det-adc-value = <500>;
    
          spk-play-volume = <7>;       63-0 min-max
          hp-play-volume = <15>;       63-0 min-max
          capture-volume = <127>; //0-127 min-max
    };
};

&pinctrl {
        rt5640_pinctrl {
                hp_det:hp_det {
                        rockchip,pins = <1 RK_PC4 RK_FUNC_GPIO &pcfg_pull_none>;
                };
        };
};

结合原理图可知rt5640挂载在i2c7下,也即其codec节点添加在i2c7节点之下。驱动文件在目录kernel/sound/soc/codecs下,为rt5640.c,该驱动文件为厂家所编写,一般直接移植即可。在rt5640该codec节点添加完成后,需添加并使能sound节点,这里为sound-rt5640节点,其与Simple Card该通用machine driver相配对。最后根据硬件连接情况使能对应的i2s节点,这里为i2s0,与原理图一致。其余配置为开发板针对耳机的相关节点配置以及耳机插入检测所需的gpio资源配置等,在驱动中实现耳机插入检测后从扬声器切换为耳机的功能。最后通过图形化配置界面使能相关配置,如I2S,新添加的rt5640驱动等。

重新编译并烧写内核,使用cat /proc/asound/cards命令查看系统声卡,确认驱动是否移植成功。

ALSA

ALSA是Advanced Linux Sound Architecture(高级的 Linux 声音体系)的缩写,目前已经成为了linux下的主流音频体系架构,采用分离、分层思想设计,提供了音频和MIDI的支持,替代了原先旧版本中的OSS(开发声音系统)。在应用层,ALSA提供了一套标准的API,应用程序只需要调用这些API就可完成对底层音频硬件设备的控制,如播放、录音等,这一套 API称为alsa-lib,关系示意图如下。在Linux内核设备驱动层,基于ALSA音频驱动框架注册的sound设备会在/dev/snd 目录下生成相应的设备节点文件。

由于alsa-lib是ALSA提供的一套在Linux下的C语言函数库,因此需要将alsa-lib交叉编译移植到开发板上,这样基于alsa-lib编写的应用程序才能成功运行。除了移植alsa-lib库之外,通常还需要移植alsa-utils,alsa-utils包含了一些用于测试、配置声卡的命令和工具,譬如aplay、arecord、alsactl、alsaloop、alsamixer、amixer等。其中,aplay命令用于播放.wav格式的音频文件;arecord命令用于录音测试,其生成.wav格式的音频文件;alsaloop命令用于回环测试,可实现边录音边播放;alsamixer和amixer用于配置声卡的混音器,区别在于前者是图形化界面后者是命令行形式;alsactl则用来保存对声卡的配置。在ALSA官网有这些工具的详细介绍及大量资料参考。

音频相关概念

基于alsa-lib的应用编程中会涉及一系列音频相关概念,如样本长度,声道数和采样率等。

  • 样本(sample)是记录音频数据最基本的单元,样本长度就是采样位数,也称为位深度(Bit Depth、Sample Size、Sample Width)。指计算机在采集和播放声音文件时,所使用数字声音信号的二进制位数,或者说每个采样样本所包含的位数(计算机对每个通道采样量化时数字比特位数),通常有8bit、16bit、24bit等。
  • 声道数(channel)分为单声道(Mono)和双声道/立体声(Stereo)。1 表示单声道、2 表示立体声。
  • 帧(frame)表示一个声音单元,其长度为样本长度与声道数的乘积,一段音频数据就是由苦干帧组成的。把所有声道中的数据加在一起叫做一帧,对于单声道:一帧 = 样本长度 * 1;双声道:一帧 = 样本长度 * 2。譬如对于样本长度为 16bit 的双声道来说,一帧的大小等于:16 * 2 / 8 = 4 个字节。
  • 采样率(sample rate)指每秒钟采样次数,该次数是针对帧而言的。常见的采样率有:8KHz,44.1KHz等。
  • 交错模式(interleaved)是一种音频数据的记录方式,分为交错模式和非交错模式。在交错模式下,数据以连续桢的形式存放,即首先记录完桢 1 的左声道样本和右声道样本(假设为立体声格式),再记录桢 2 的左声道样本和右声道样本。而在非交错模式下,首先记录的是一个周期内所有桢的左声道样本,再记录右声道样本,数据是以连续通道的方式存储。多数情况下都是使用交错模式。
  • 周期(period)是音频设备处理(读、写)数据的单位,每一次读或写一个周期的数据,一个周期包含若干个帧;譬如周期的大小为 1024 帧,则表示音频设备进行一次读或写操作的数据量大小为 1024 帧,假设一帧为 4 个字节,那么也就是 1024*4=4096 个字节数据。一个周期其实就是两次硬件中断之间的帧数,音频设备每处理(读或写)完一个周期的数据就会产生一个中断,所以两个中断之间相差一个周期。
  • 数据缓冲区(buffer),一个缓冲区包含若干个周期,所以buffer是由若干个周期所组成的一块空间。

它们之间的关系如下:

音频设备底层驱动程序使用DMA来搬运数据,若buffer中有4个period,每当DMA搬运完一个period的数据就会触发一次中断,因此搬运整个buffer中的数据将产生4次中断。为了延迟问题,ALSA把缓存区拆分成多个周期,以周期为传输单元进行传输数据。所以,周期不宜设置过大,周期过大会导致延迟过高;但周期也不能太小,周期太小会导致频繁触发中断,这样会使得CPU被频繁中断而无法执行其它的任务,使得效率降低。

应用程序编写

基于alsa-lib编写简单的音频播放程序,通过WAV音频文件相关数据结构实现对音频文件的信息解析并进行打印显示。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <alsa/asoundlib.h>

#define PCM_PLAYBACK_DEV "hw:0,0"

//WAV 音频文件解析相关数据结构申明

typedef struct WAV_RIFF {
 char ChunkID[4]; /* "RIFF" */
 u_int32_t ChunkSize; /* 从下一个地址开始到文件末尾的总字节数 */
 char Format[4]; /* "WAVE" */
} __attribute__ ((packed)) RIFF_t;

typedef struct WAV_FMT {
 char Subchunk1ID[4]; /* "fmt " */
 u_int32_t Subchunk1Size; /* 16 for PCM */
 u_int16_t AudioFormat; /* PCM = 1*/
 u_int16_t NumChannels; /* Mono = 1, Stereo = 2, etc. */
 u_int32_t SampleRate; /* 8000, 44100, etc. */
 u_int32_t ByteRate; /* = SampleRate * NumChannels * BitsPerSample/8 */
 u_int16_t BlockAlign; /* = NumChannels * BitsPerSample/8 */
 u_int16_t BitsPerSample; /* 8bits, 16bits, etc. */
} __attribute__ ((packed)) FMT_t;

static FMT_t wav_fmt;

typedef struct WAV_DATA {
 char Subchunk2ID[4]; /* "data" */
 u_int32_t Subchunk2Size; /* data size */
} __attribute__ ((packed)) DATA_t;

static snd_pcm_t *pcm = NULL; //pcm 句柄
static unsigned int buf_bytes; //应用程序缓冲区的大小(字节为单位)
static void *buf = NULL; //指向应用程序缓冲区的指针
static int fd = -1; //指向 WAV 音频文件的文件描述符
static snd_pcm_uframes_t period_size = 1024; //周期大小(单位: 帧)
static unsigned int periods = 16; //周期数(设备驱动层 buffer 的大小)

static int snd_pcm_init(void)
{
    snd_pcm_hw_params_t *hwparams = NULL;
    int ret;
    /* 打开 PCM 设备 */
    ret = snd_pcm_open(&pcm, PCM_PLAYBACK_DEV, SND_PCM_STREAM_PLAYBACK, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_open error: %s: %s\n",
        PCM_PLAYBACK_DEV, snd_strerror(ret));
        return -1;
    }
    /* 申请hwparams */
    snd_pcm_hw_params_malloc(&hwparams);
    /* 初始化hwparams */
    ret = snd_pcm_hw_params_any(pcm, hwparams);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_any error: %s\n", snd_strerror(ret));
        goto err2;
    }
    /* 设交错模式 */
    ret = snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_access error: %s\n", snd_strerror(ret));
        goto err2;
    }
    /* 设置数据格式: 有符号 16 位、小端模式 */
    ret = snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_format error: %s\n", snd_strerror(ret));
        goto err2;
    }
    /* 设置采样率 */
    ret = snd_pcm_hw_params_set_rate(pcm, hwparams, wav_fmt.SampleRate, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_rate error: %s\n", snd_strerror(ret));
        goto err2;
    }
    /* 设置声道数: 双声道 */
    ret = snd_pcm_hw_params_set_channels(pcm, hwparams, wav_fmt.NumChannels);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_channels error: %s\n", snd_strerror(ret));
        goto err2;
    }
    /* 设置周期大小 */
    ret = snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_period_size error: %s\n", snd_strerror(ret));
        goto err2;
    }
    /* 设置周期数(驱动层 buffer 的大小) */
    ret = snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_periods error: %s\n", snd_strerror(ret));
        goto err2;
    }
    /* 使配置生效 */
    ret = snd_pcm_hw_params(pcm, hwparams);
    snd_pcm_hw_params_free(hwparams); //释放 hwparams 对象占用的内存
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params error: %s\n", snd_strerror(ret));
        goto err1;
    }
    buf_bytes = period_size * wav_fmt.BlockAlign; //变量赋值,一个周期的字节大小
    return 0;
err2:
        snd_pcm_hw_params_free(hwparams); //释放内存
err1:
        snd_pcm_close(pcm); //关闭 pcm 设备
        return -1;
}
static int open_wav_file(const char *file)
{
    RIFF_t wav_riff;
    DATA_t wav_data;
    int ret;
    fd = open(file, O_RDONLY);
    if (0 > fd) {
        fprintf(stderr, "open error: %s: %s\n", file, strerror(errno));
        return -1;
    }
    /* 读取 RIFF chunk */
    ret = read(fd, &wav_riff, sizeof(RIFF_t));
    if (sizeof(RIFF_t) != ret) {
    if (0 > ret)
        perror("read error");
    else
        fprintf(stderr, "check error: %s\n", file);
    close(fd);
    return -1;
    }
    if (strncmp("RIFF", wav_riff.ChunkID, 4) ||//校验
    strncmp("WAVE", wav_riff.Format, 4)) {
        fprintf(stderr, "check error: %s\n", file);
        close(fd);
        return -1;
    }
    /* 读取 sub-chunk-fmt */
    ret = read(fd, &wav_fmt, sizeof(FMT_t));
    if (sizeof(FMT_t) != ret) {
    if (0 > ret)
        perror("read error");
    else
        fprintf(stderr, "check error: %s\n", file);
    close(fd);
    return -1;
    }
    if (strncmp("fmt ", wav_fmt.Subchunk1ID, 4)) {//校验
        fprintf(stderr, "check error: %s\n", file);
        close(fd);
        return -1;
    }
    /* 打印音频文件的信息 */
    printf("<<<<音频文件格式信息>>>>\n\n");
    printf(" file name: %s\n", file);
    printf(" Subchunk1Size: %u\n", wav_fmt.Subchunk1Size);
    printf(" AudioFormat: %u\n", wav_fmt.AudioFormat);
    printf(" NumChannels: %u\n", wav_fmt.NumChannels);
    printf(" SampleRate: %u\n", wav_fmt.SampleRate);
    printf(" ByteRate: %u\n", wav_fmt.ByteRate);
    printf(" BlockAlign: %u\n", wav_fmt.BlockAlign);
    printf(" BitsPerSample: %u\n\n", wav_fmt.BitsPerSample);
    /* sub-chunk-data */
    if (0 > lseek(fd, sizeof(RIFF_t) + 8 + wav_fmt.Subchunk1Size,SEEK_SET)) {
        perror("lseek error");
        close(fd);
        return -1;
    }
    while(sizeof(DATA_t) == read(fd, &wav_data, sizeof(DATA_t))) {
        /* 找到 sub-chunk-data */
        if (!strncmp("data", wav_data.Subchunk2ID, 4))//校验
            return 0;
        if (0 > lseek(fd, wav_data.Subchunk2Size, SEEK_CUR)) {
            perror("lseek error");
            close(fd);
            return -1;
        }
    }
    fprintf(stderr, "check error: %s\n", file);
    return -1;
}

int main(int argc, char *argv[])
{
    int ret;
    if (2 != argc) {
        fprintf(stderr, "Usage: %s <audio_file>\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    /* 打开 WAV 音频文件 */
    if (open_wav_file(argv[1]))
        exit(EXIT_FAILURE);
    /* 初始化 PCM Playback 设备 */
    if (snd_pcm_init())
        goto err1;
    /* 申请读缓冲区 */
    buf = malloc(buf_bytes);
    if (NULL == buf) {
        perror("malloc error");
        goto err2;
    }
    /* 播放 */
    for ( ; ; ) {
        memset(buf, 0x00, buf_bytes); 
        ret = read(fd, buf, buf_bytes); 
        if (0 >= ret) 
            goto err3;
        ret = snd_pcm_writei(pcm, buf, period_size);
        if (0 > ret) {
            fprintf(stderr, "snd_pcm_writei error: %s\n", snd_strerror(ret));
            goto err3;
        }
        else if (ret < period_size) {
            if (0 > lseek(fd, (ret-period_size) * wav_fmt.BlockAlign, SEEK_CUR)) {
                perror("lseek error");
                goto err3;
            }
        }
    }
err3:
    free(buf); 
err2:
    snd_pcm_close(pcm); 
err1:
    close(fd); 
    exit(EXIT_FAILURE);
}

运行测试

这里采用龙芯2K0300久久派开发板进行测试,同时也是验证只要开发板移植了alsa-lib就能运行基于alsa-lib所编写的音频应用程序。交叉编译得到pcm_playback,拷入开发板运行播放音频文件,插入耳机能听到声音,并将程序运行的打印信息与文件信息作对比。

总结:本篇详细介绍了音频驱动相关概念,RK平台音频相关配置以及ALSA编程相关概念等,最后基于alsa-lib编写了简单的音频播放程序并在开发板上进行了验证。

相关推荐
cen难取名2 小时前
驱动开发WDK(1)-环境准备和初始程序。详细教程
驱动开发·microsoft
fai厅的秃头姐!2 小时前
C语言03
c语言·数据结构·算法
life_time_2 小时前
C语言(22)
c语言·开发语言
lllsure3 小时前
Linux 实用指令
linux·物联网
努力的小T3 小时前
使用 Docker 部署 Apache Spark 集群教程
linux·运维·服务器·docker·容器·spark·云计算
Nerd Nirvana4 小时前
OpenSSL crt & key (生成一套用于TLS双向认证的证书密钥)
linux·ssl·shell·认证·加密·tls·oepnssl
Macdo_cn4 小时前
Infuse Pro for Mac v8.1 全能视频播放器 支持M、Intel芯片
macos·音视频
letisgo54 小时前
记录一次部署PC端网址全过程
linux·阿里云·服务器运维
怪怪王5 小时前
【GPU驱动】OpenGLES图形管线渲染机制
驱动开发·gpu·opengl
猫猫的小茶馆5 小时前
【网络编程】UDP协议
linux·服务器·网络·网络协议·ubuntu·udp