音频基础到ALSA框架

1. 音频基础知识

1.1 PCM

1.1.1 音频采样

类似微积分的思想,音频数字化将连续的信号值,分解为一段一段的数据,只要足够小就能记录下连续的数据信息。如下图所示:将1秒的连续信号分为44.1K个离散的信号。



由于人耳能听到的频率范围是20Hz到20KHz , 根据采样定理,按照人耳能听到的最大频率的两倍进行采样,可以保证音频数字化后仍然有质量保障。

1.1.2 音频量化

上图将坐标轴沿着X轴进行离散化,但是却不能分辨Y轴对应的高度,量化就是使用一个数字去描述Y值。

CD是使用16bit 进行描述。总共65536个值。

1.1.3 音频编码

通常所说的音频的裸数据格式是脉冲编码调制数据(即PCM)。为了准确的描述PCM数据,通常需要记录采样过程中的三种数据,具体如下:

  • 量化格式 - Format(比如:16bit)

  • 采样率 - SampleRate(比如:将一秒的模拟信号分为44.1K个离散数据)

  • 声道数 - Channel

CD标准: 量化格式 16 bit, 采样率 44100, 声道数 2。

WAV格式就是采用 文件头 + PCM数据,来记录音频。

1.2 比特率

比特率用于描述音频单位时间(1s)内的比特(bit)数目。其计算公式如下:

KaTeX parse error: Can't use function '' in math mode at position 2: ̲ BitRate = Form...

对于CD音质,其比特率为: 44100×16×2=1411200bit/s=1378.125kbit/s44100 \times 16 \times 2 = 1411200 bit/s = 1378.125 kbit/s44100×16×2=1411200bit/s=1378.125kbit/s 。

一分钟的CD音质需要的存储空间大小为 1378.125 \\times 60 / 8 / 1024 = 10.09 MB

1.3 音频压缩

为什么需要音频压缩?

  1. 节省存储空间

  2. 提高传输效率

一分钟的音频数据大约占用了10MB, 这对于磁盘存储来说可以接受,但是对于网络传输无法接受。

1.3.1 有损压缩

有损压缩的原理:消除冗余信息。比如人耳能够听到20Hz至20KHz频率的范围,可以考虑过滤掉其他频率的音频信息。

1.3.2 频域遮蔽

具体如下图所示:



  • 静音门槛:这条曲线描述了不同频率下,至少多少dB的声音强度人耳能听到。低于曲线的数据将被删除。

  • 遮罩门槛: 是指在某个频域范围内,找到最大dB的音源,做一个钟形曲线。在曲线之上的才能被保留。

1.3.3 时域屏蔽

最大声音附近的声音,如果声音强度低于屏蔽线的声音将被屏蔽。

1.3.4 无损压缩

1.3.5 预测编码技术

预测编码利用数据本身的统计特性,根据已知的样本信息预测未知的样本值。

1.3.6 熵编码技术

熵编码是一种无损数据压缩方法,它利用数据的统计特性,将常见序列用较短的代码表示,而不常见的序列用较长的代码表示。

有点像哈夫曼算法

1.4 常见音频格式及其特点

音频格式 类型 特点 优点 缺点
WAV 无损 音质高,文件体积大 音质无损,兼容性极佳 文件体积大
FLAC 无损 无损压缩,文件较小 音质无损,体积适中,元数据支持 压缩率不如有损格式
ALAC 无损 苹果无损格式,文件较小 音质无损,体积适中,元数据支持 兼容性差,限于苹果设备
APE 无损 高压缩率,文件小 音质无损,体积小 解码慢,兼容性差
MP3 有损 文件小,音质较好 文件小,兼容性好 音质有损,低比特率下音质差
AAC 有损 音质优于MP3,文件小 音质好,体积小,支持多声道 兼容性不如MP3
WMA 有损 音质好,支持DRM 音质好,体积小,支持版权保护 兼容性差,限于Windows平台
OGG 有损 开放源代码,音质好 音质好,体积小,无专利限制 兼容性不如MP3

1.5 IIS协议

1.5.1 IIS是什么?

IIS 是飞利浦在1986年定义(1996年修订)的数字音频传输标准,用于数字音频数据在系统内器件之间传输,例如编解码器CODEC、DSP、数字输入/输出接口、ADC、DAC和数字滤波器等。

1.5.2 硬件结构

IIS是个相对来说简单的接口协议,没有地址和片选机制。

在总线上,只能同时存在一个主设备和发射设备;提供时钟的设备为主设备,可以是发射设备也可以是接收设备,或者是协调两者的其他控制设备。在高端应用场合中,CODEC经常作为主设备以便精确控制IIS的数据流。

IIS协议定义三根信号线:时钟信号SCK、数据信号SD和左右声道选择信号WS。

WS: 声道选择信号, WS=0表示选择左声道,WS=1表示选择右声道。

SCK: 模块内的同步信号, 从模式由外部提供, 主模式由内部产生。

SD: 串行数据, 以二进制补码的形式传输,优先传输最高位(MSB)。

1.5.3 操作模式

IIS的操作模式有3种: 标准模式,左对齐模式, 右对齐模式。

  1. 标准模式

    左右声道的数据均是在WS变化后的第二个SCK上升沿有效。

  2. 左对齐模式

    左对齐的格式的数据在WS边沿变化后的SCK的第一个上升沿有效。

  3. 右对齐模式

    此格式左右声道对应的WS与前面两种模式不一样,如上图所示,信号与WS有边沿对齐。

2. ALSA框架

2.1 什么是ALSA

ALSA是Advanced Linux Sound Architecture的缩写,即高级Linux声音架构。在Linux 2.6的内核版本后,ALSA目前已经成为了Linux的主流音频体系结构。

2.2 ALSA总体流程

复制代码
                                                  音频数据的硬件流图

Codec(音频编解码器)功能:

  • 对PCM数据进行D/A转换

  • 对输入数据进行A/D转换

  • 对音频信号做相应的处理, 例如音量控制,功率放大等

Audio Interface:

  • 用于传输音频信号,主流使用I2S接口。

Control Interface:

  • 用于配置Codec,主流使用I2C接口。
查看ALSA的设备文件
bash 复制代码
ls /dev/snd
  • controlC0,用于声卡的控制,例如音量,混音,麦克风的控制等等;
  • pcmC0D0c,用于录音的pcm设备0;
  • pcmC0D0p,用于播放的pcm设备0;

2.3 ALSA驱动

2.3.1 ALSA驱动主要的工作:
  1. 创建snd_card实例

  2. 填充snd_card实例中的内容

  3. 创建声卡中的逻辑设备,PCM、control等

  4. 注册声卡snd_card到系统中

    c 复制代码
    struct snd_card *snd_cards[SNDRV_CARDS]; /* sound/core/init.c */
2.3.2 PCM设备

PCM设备主要有两个功能:

  1. playback, 把用户程序中发送过来的PCM信号输出

  2. capture, 把输入的模拟信号转换为cpu能够处理的数字信号

2.3.3 ALSA驱动

首先是重要的结构体

  1. snd_card有一个双向链表,链表中挂接了各种逻辑设备
  1. snd_pcm设备, 一个pcm设备含有两个流(playback和capture),这两个流下又分别有一个或多个子流,子流含有具体的操作函数用于播放或者捕获声音。可通过函数snd_pcm_set_ops() 进行设置

    PCM设备注册流程

    PCM设备注册的大体流程:

    1. 创建声卡snd_card, 使用 snd_card_new() 进行创建。

    2. 创建PCM设备,使用 snd_pcm_new() 进行创建。它主要进行以下操作:

      • 创建 snd_pcm实例

      • 创建pcm的输入、输出流(对应capture 和 playback)

      • 创建pcm设备,并将其挂入snd_card的链表之中

    3. 使用 snd_pcm_set_ops() 设置子流的操作函数。

    4. 使用 snd_card_register() 注册声卡到系统中,回调函数snd_pcm_dev_register() 函数会将pcm设备注册到系统中。

    5. snd_pcm_dev_register()函数会调用snd_register_device()进行注册。

      snd_register_device()函数会找到一个没用的次设备号来与此设备绑定,同时将其注册到snd_minors中。同时指定操作函数ops。

    具体的流程如下所示:

如何通过ALSA驱动调用到对应的ops?

  1. alsa设备会统一一个入口函数snd_open()

  2. snd_open()函数会根据次设备号取到对应的设备操作函数。(前面注册的时候就根据次设备号存储进snd_minors[]数组中)

大致代码如下所示:

c 复制代码
static const struct file_operations snd_fops =
{
    .owner =    THIS_MODULE,
    .open =        snd_open,
    llseek =    noop_llseek,
};

static int __init alsa_sound_init(void)
{
    snd_major = major;
    snd_ecards_limit = cards_limit;

    //注册字符设备,对应的file_operations为snd_fops 
    if (register_chrdev(major, "alsa", &snd_fops)) {
        return -EIO;
    }
    ......
    return 0;
}
c 复制代码
static int snd_open(struct inode *inode, struct file *file)
{
    //得到次设备
    unsigned int minor = iminor(inode);
    //以次设备号为下标在snd_minors数组中找到对应的snd_minor 
    mptr = snd_minors[minor];
    ......

    //从snd_minor得到file_operations 
    new_fops = fops_get(mptr->f_ops);
    ......

    //替换原来的file_operations 
    replace_fops(file, new_fops);

    if (file->f_op->open)
        // snd_pcm_playback_open 或者 snd_pcm_capture_open
        err = file->f_op->open(inode, file); 
    return err;
}

// replace_fops宏定义会将 file 的操作函数替换为对应的设备,比如pcm
#define replace_fops(f, fops) \
    do {    \
        struct file *__file = (f); \
        fops_put(__file->f_op); \
        BUG_ON(!(__file->f_op = (fops))); \
    } while(0)

#define fops_put(fops) ({                        \
    const struct file_operations *_fops = (fops);            \
    if (_fops)                            \
        module_put((_fops)->owner);                \
})

应用层序open时,触发系统调用sys_open(), 然后内核查找inode,这个inode是在设备注册时创建和初始化的, 其中inode对应的文件操作函数已经被指定了。后续内核会申请file结构体,将ops操作函数赋值过去。

流程图如下:

snd_pcm_playback_open会根据次设备号从snd_minors数组中找到对应的pcm(保存在private_data中)实例

c 复制代码
open() -> snd_open() -> snd_pcm_playback_open() -> snd_pcm_open()
-> snd_pcm_open_file() -> snd_pcm_open_substream()
-> soc_pcm_open()

调用过程梳理:

  1. alsa会向系统注册一个字符设备,注册open函数为snd_open()

  2. 用户打开文件时(调用open函数),系统会调用snd_open()

  3. snd_open()函数根据次设备号,从snd_minors数组中找到对应的open函数并调用,这里的open函数一般是snd_pcm_*_open(snd_pcm_playback_open 或 snd_pcm_capture_open)

  4. snd_pcm_*_open会再次根据次设备号,从snd_minors数组中的private_data找到对应的pcm实例

  5. snd_pcm_open调用snd_pcm_open_file打开对应的pcm实例

  6. snd_pcm_open_file根据pcm实例,找到其对应的子流,然后调用snd_pcm_open_substream打开对应的子流

  7. 子流中的open函数是注册时注册进去的,在ASOC中是soc_pcm_open函数。

3. ASOC

3.1 ASOC是什么?

ASoC(ALSA System on Chip)是建立在标准 ALSA(Advanced Linux Sound Architecture)驱动层上的一套软件体系,专门用于支持嵌入式处理器和移动设备中的音频 Codec。它通过将音频硬件设备驱动划分为三个主要部分------Codec、Platform 和 Machine------来解决传统 ALSA 驱动中的一些局限性。

3.2 为什么有ASOC?

  1. Codec驱动与CPU的底层耦合过于紧密,这种不理想会导致代码的重复

  2. 音频事件没有标准的方法来通知用户,例如耳机、麦克风的插拔和检测,这些事件在移动设备中是非常普通的,而且通常都需要特定于机器的代码进行重新对音频路劲进行配置。

  3. 当进行播放或录音时,驱动会让整个codec处于上电状态,这对于PC没问题,但对于移动设备来说,这意味着浪费大量的电量。

3.3 ASOC流程图

  • codec、cpu对应的驱动被注册为平台驱动,当匹配上了平台设备时,执行相应的porbe函数,将其注册到component_list中

  • machine驱动与mechine设备匹配时,根据dai_link信息,从ASoC定义的三个全局的链表头变量:codec_list、dai_list、platform_list,匹配系统中的Codec、DAI、Platform。

    代码中首先在一个全局的compenent_list中进行查找匹配的compnent,再在component对应的dai_list查找dai。

    dai中含有一个snd_soc_dai_driver结构体,里面保存了驱动的系列函数

重要结构体的关系图如下:

3.4 关键改变

为了解除底层驱动的耦合,ASOC将音频设备驱动分为3个部分:Codec, Platform, Machine。

但是这3部分具体如何连接起来,如何运行?


连接

  1. 通过snd_soc_dai_link(它是snd_soc_card结构体的一个成员)结构,动态的配置3个不同设备之间的连接关系

  2. 在进行snd_soc_card声卡注册的时候,调用snd_soc_bind_card()函数

  3. snd_soc_bind_card()函数根据snd_soc_dai_link结构在全局链表头上寻找对应的实例进行连接

这里会有个顺序问题,是否有深意?看代码中会先找cpu_dai, 再找codec_dai。

  1. 连接的信息记录在snd_soc_pcm_runtime(简称rtd)结构体中

运行

  1. 当真正创建pcm设备的时候,pcm结构中的private_data会指向rtd。

  2. 同时pcm对应的子流中的private_data也会指向rtd。

  3. 子流对应的操作函数被统一设置为 soc_pcm_*() 系列函数。

  4. soc_pcm_*()会取出rtd,然后根据dai_link结构取出对应的操作函数,并执行。

3.5 打开一个pcm设备的大致调用栈

c 复制代码
pcm_open();
    pcm->fd = open("/dev/snd/pcmC0D0c");
        snd_pcm_capture_open();
            snd_pcm_open(SNDRV_PCM_STREAM_CAPTURE);
                snd_pcm_open_file();
                    snd_pcm_open_substream();
                        substream->ops->open();
                            soc_pcm_open();
                                cpu_dai->driver->ops->startup();
                                platform->driver->ops->open();
                                codec_dai->driver->ops->startup();
                                rtd->dai_link->ops->startup();

3.6 Kcontrol

  1. 什么是kcontrol?

    Kcontrol是一种控件,其主要实现控制声卡的音量,混音等一系列控制。

    snd_kcontrol_new 结构如下

    c 复制代码
    struct snd_kcontrol_new {  
            snd_ctl_elem_iface_t iface;     /* interface identifier */  
            unsigned int device;            /* device/client number */  
            unsigned int subdevice;         /* subdevice (substream) number */  
            const unsigned char *name;      /* ASCII name of item */  
            unsigned int index;             /* index of item */  
            unsigned int access;            /* access rights */  
            unsigned int count;             /* count of same elements */  
            snd_kcontrol_info_t *info;  
            snd_kcontrol_get_t *get;  
            snd_kcontrol_put_t *put;  
            union {  
                    snd_kcontrol_tlv_rw_t *c;  
                    const unsigned int *p;  
            } tlv;  
            unsigned long private_value;  
    };

    snd_kcontrol_new结构会在声卡的初始化阶段,通过snd_soc_add_codec_controls函数注册到系统中。

    两种注册方式:

    1. 在codec driver的probe函数中调用 snd_soc_add_codec_controls 注册kcontrol。

    2. 通过snd_soc_register_codec先添加到codec_dai的component链表中。

  2. 简单控件

    SOC_SINGLE是最简单的空间,这个控件只有一个控制量,比如开关,或则一个数值变量。

    SOC_SINGLE宏定义帮助填充snd_kcontrol_new结构体。

    其中的参数含义分别是:

    xname : 控件的名字

    reg : 该控件对应的寄存器地址

    shift : 控制位在寄存器中的位移

    max : 控件可以设置的最大值

    invert : 设定值是否逻辑反转

    c 复制代码
    #define SOC_SINGLE(xname, reg, shift, max, invert) \  
    {       .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname, \  
            .info = snd_soc_info_volsw, .get = snd_soc_get_volsw,\  
            .put = snd_soc_put_volsw, \  
            .private_value =  SOC_SINGLE_VALUE(reg, shift, max, invert) }  

    SOC_SINGLE_VALUE 宏定义的功能是创建一个soc_mixer_control 结构,然后将他的地址赋值给private_value字段。

    soc_mixer_control是控件的真正描述者,它确定了该控件对应寄存器的地址、位移值、最大值和是否逻辑取反等特性。

    put、get回调函数都需要借助这个结构。

    下面是一个get函数的例子:

    c 复制代码
    int snd_soc_get_volsw(struct snd_kcontrol *kcontrol,  
            struct snd_ctl_elem_value *ucontrol)  
    {  
        struct soc_mixer_control *mc =  
                (struct soc_mixer_control *)kcontrol->private_value;
        //   snd_kcontrol_chip 就是 kcontrol->private_data
        struct snd_soc_codec *codec = snd_kcontrol_chip(kcontrol);  
        unsigned int reg = mc->reg;  
        unsigned int reg2 = mc->rreg;  
        unsigned int shift = mc->shift;  
        int max = mc->max;  
        unsigned int mask = (1 << fls(max)) - 1;  
        unsigned int invert = mc->invert;  
    
        ucontrol->value.integer.value[0] =  
                (snd_soc_read(codec, reg) >> shift) & mask;  
        if (invert)  
                ucontrol->value.integer.value[0] =  
                        max - ucontrol->value.integer.value[0];  
    
        if (snd_soc_volsw_is_stereo(mc)) {  
                if (reg == reg2)  
                        ucontrol->value.integer.value[1] =  
                                (snd_soc_read(codec, reg) >> rshift) & mask;  
                else  
                        ucontrol->value.integer.value[1] =  
                                (snd_soc_read(codec, reg2) >> shift) & mask;  
                if (invert)  
                        ucontrol->value.integer.value[1] =  
                                max - ucontrol->value.integer.value[1];  
        }  
    
        return 0;  
    }  

    get函数会从kcontrol结构中取出soc_mixer_control结构体,读取其对应的寄存器以及偏移量,最后利用codec驱动实现对寄存器的读取。

  3. mixer控件

    Mixer控件用于音频通道的路由控制

多个输入一个输出,多个输入可以自由地混合在一起,形成混合后的输出

Mixer控件是多个简单控件的组合:

c 复制代码
static const struct snd_kcontrol_new left_speaker_mixer[] = {  
    SOC_SINGLE("Input Switch", WM8993_SPEAKER_MIXER, 7, 1, 0),  
    SOC_SINGLE("IN1LP Switch", WM8993_SPEAKER_MIXER, 5, 1, 0),  
    SOC_SINGLE("Output Switch", WM8993_SPEAKER_MIXER, 3, 1, 0),  
    SOC_SINGLE("DAC Switch", WM8993_SPEAKER_MIXER, 6, 1, 0),  
};  

以上这个mixer使用寄存器WM8993_SPEAKER_MIXER的第3,5,6,7位来分别控制4个输入端的开启和关闭。

  1. Mux控件
    Mux控件与Mixer控件类似,不同之处如下:

Mux控件也是多个输入对应一个输出,但是多个输入端只能有一个被选中。

前面的控件使用soc_mixer_control 结构体进行描述,Mux控件使用soc_enum结构来描述。

c 复制代码
/* enumerated kcontrol */  
struct soc_enum {  
    unsigned short reg;  
    unsigned short reg2;  
    unsigned char shift_l;  
    unsigned char shift_r;  
    unsigned int max;  
    unsigned int mask;  
    const char * const *texts;  
    const unsigned int *values;  
};  

其对应的回调函数也发生了变化:

c 复制代码
int snd_soc_get_enum_double(struct snd_kcontrol *kcontrol,  
        struct snd_ctl_elem_value *ucontrol)  
{  
    struct snd_soc_codec *codec = snd_kcontrol_chip(kcontrol);  
    struct soc_enum *e = (struct soc_enum *)kcontrol->private_value;  
    unsigned int val;  

    val = snd_soc_read(codec, e->reg);  
    ucontrol->value.enumerated.item[0]  
            = (val >> e->shift_l) & e->mask;  
    if (e->shift_l != e->shift_r)  
            ucontrol->value.enumerated.item[1] =  
                    (val >> e->shift_r) & e->mask;  

    return 0;  
}  

3.7 widget

kcontrol有以下几点不足:

  1. 只能描述自身,无法描述各个kcontrol之间的连接关系

  2. 没有相应的电源管理机制

  3. 没有相应的事件处理机制来响应播放、停止、上电、下电等音频事件

  4. 为了防止pop-pop声,需要用户程序关注各个kcontrol上电和下电的顺序

  5. 当一个音频路径不再有效时,不能自动关闭该路径上的所有的kcontrol

小技巧

宏定义中的根据参数数量动态选择宏

c 复制代码
#define SND_SOC_DAILINK_REG1(name)     SND_SOC_DAILINK_REG3(name##_cpus, name##_codecs, name##_platforms)
#define SND_SOC_DAILINK_REG2(cpu, codec) SND_SOC_DAILINK_REG3(cpu, codec, null_dailink_component)
#define SND_SOC_DAILINK_REG3(cpu, codec, platform)    \
    .cpus        = cpu,                \
    .num_cpus    = ARRAY_SIZE(cpu),        \
    .codecs        = codec,            \
    .num_codecs    = ARRAY_SIZE(codec),        \
    .platforms    = platform,            \
    .num_platforms    = ARRAY_SIZE(platform)

#define SND_SOC_DAILINK_REGx(_1, _2, _3, func, ...) func
#define SND_SOC_DAILINK_REG(...) \
    SND_SOC_DAILINK_REGx(__VA_ARGS__,        \
            SND_SOC_DAILINK_REG3,    \
            SND_SOC_DAILINK_REG2,    \
            SND_SOC_DAILINK_REG1)(__VA_ARGS__)

其中__VA_ARGS__ 是C99标准引入的一个特殊符号,用于在宏定义中表示可变参数。它允许宏接收任意数量的参数,并在宏展开时将这些参数传递给其他宏或函数。

  1. VA_ARGS 代表一个参数的时候:

    例如 SND_SOC_DAILINK_REG(name) 展开为

    c 复制代码
    SND_SOC_DAILINK_REGx(name,        \
            SND_SOC_DAILINK_REG3,    \
            SND_SOC_DAILINK_REG2,    \
            SND_SOC_DAILINK_REG1)(name)
    // 即展开为:
    SND_SOC_DAILINK_REG1(name)
    // 再次展开:
    SND_SOC_DAILINK_REG3(name##_cpus, name##_codecs, name##_platforms)
    // 再次展开:
     .cpus        = name_cpu,                \
     .num_cpus    = ARRAY_SIZE(name_cpu),        \
     .codecs        = name_codec,            \
     .num_codecs    = ARRAY_SIZE(name_codec),        \
     .platforms    = name_platform,            \
     .num_platforms    = ARRAY_SIZE(platform)
  2. VA_ARGS代表两个参数的时候:

    例如SND_SOC_DAILINK_REG(name1, name2)

    c 复制代码
    SND_SOC_DAILINK_REGx(name1,        \
            name2, \
            SND_SOC_DAILINK_REG3,    \
            SND_SOC_DAILINK_REG2,    \
            SND_SOC_DAILINK_REG1)(name1, name2)
    // 即展开为:
    SND_SOC_DAILINK_REG2(name1, name2)
    // 再次展开:
    SND_SOC_DAILINK_REG3(name1, name2, null_dailink_component)
    // 再次展开:
     .cpus        = name1_cpu,                \
     .num_cpus    = ARRAY_SIZE(name1_cpu),        \
     .codecs        = name2_codec,            \
     .num_codecs    = ARRAY_SIZE(name2_codec),        \
     .platforms    = null_dailink_component,            \
     .num_platforms    = ARRAY_SIZE(null_dailink_component)
  3. 三个参数时同理!


传递多个变量的宏

c 复制代码
#define DAILINK_COMP_ARRAY(param...)    param
// 例子 (定义snd_soc_dai_link_component数组使用到)
DAILINK_COMP_ARRAY({ "cpu_dai", "cpu" }, { "codec_dai", "codec" });
// 上述宏可以将多个参数打包到一个中

遍历链表的宏

c 复制代码
// list_for_each_entry 会遍历head指向的next链表, 将值保存在pos中
#define list_for_each_entry(pos, head, member)                  \
    for (pos = list_entry((head)->next, typeof(*pos), member);  \
         prefetch(pos->member.next), &pos->member != (head);    \
         pos = list_entry(pos->member.next, typeof(*pos), member))  

#define list_entry(ptr, type, member) \
    container_of(ptr, type, member)

// container_of 会将next指针的指向转换为成员member的指针,
// 然后减去这个成员的偏移,从而得到结构体的首地址
#define container_of(ptr, type, member) ({                      \
    const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
    (type *)( (char *)__mptr - offsetof(type,member) );})


// offset_of 获得type类型中,某个成员的偏移
#define offset_of(type, memb) \
    ((unsigned long)(&((type *)0)->memb))

遇到一些问题

操作系统系统调用的bug记录

unistd.h 文件: 添加系统调用时需要在改头文件增加系统调用号,同时加上使用__SYSCALL()宏,后续在sys.c中进行展开, 以此注册到系统之中。

写法1:

写法2:

sys.c 文件:该文件会展开该头文件 unistd.h 到系统调用的数组sys_call_table中。

但是写法1能够成功注册系统调用,写法2不能够成功注册系统调用。

原因:

会有一种场景,编译的时候,其他地方也用到了unistd.h这个头文件,但是如果其他文件先包含了unistd.h文件,这使得__NR_my_syscall_tick被定义了,sys.c对其进行展开的时候,会使得__SYSCALL()宏不被写入sys_call_table中,导致系统调用正确注册到系统当中。

如下是编译过程中其他文件对unistd.h的一个包含:

以后宏定义的时候,需要注意ifndef中的内容,特别是多个文件要使用时。

这里用到了下面的数组初始化方法:

数组可以使用下标加值的方法进行初始化,以及宏定义在数组内部使用宏定义也不会有语法错误。

以下是程序输出:


编译内核时的配置文件例子

  1. 新建一个文件夹,建立一个config配置项

    config TESTCONDEV

    bash 复制代码
    // 文件路径 drivers/test/Kconfig
    config TESTCONDEV
        tristate "a TESTCONDEV driver module"
        default m
        help 
            a TESTCONDEV driver
  2. 在上一级的Kconfig导入drivers/test/Kconfig

    bash 复制代码
    // 路径 driver/Kconfig
    source "driver/test_config/Kconfig"
相关推荐
爱跑马的程序员1 天前
UMS9620 展锐平台增加一个虚拟陀螺仪
驱动开发·安卓·传感器·展锐·虚拟陀螺·传感器驱动
被遗忘的旋律.1 天前
Linux驱动开发笔记(二十三)—— regmap
linux·驱动开发·笔记
Nautiluss1 天前
一起调试XVF3800麦克风阵列(九)
linux·人工智能·嵌入式硬件·音频·语音识别·dsp开发
比奇堡派星星1 天前
如何新加netlink
linux·驱动开发
电脑小管家1 天前
DirectX报错怎么办?快速修复游戏和软件崩溃问题
windows·驱动开发·microsoft·计算机外设·电脑
比奇堡派星星2 天前
Linux4.4使用AW9523
linux·开发语言·arm开发·驱动开发
比奇堡派星星2 天前
cmdline使用详解
linux·arm开发·驱动开发
shandianchengzi2 天前
【记录】AU|什么是泛音和音高,在频谱上如何体现?人类和乐器的区别明显吗?走近基本知识:从泛音列到人声奥秘的声学探索
音频·媒体·声音·au
AI时代原住民2 天前
SDD(Spec驱动开发)实战新范式:SDDAgent驱动SDD端到端开发流
驱动开发