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 音频压缩
为什么需要音频压缩?
-
节省存储空间
-
提高传输效率
一分钟的音频数据大约占用了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种: 标准模式,左对齐模式, 右对齐模式。
-
标准模式

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

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

此格式左右声道对应的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驱动主要的工作:
-
创建snd_card实例
-
填充snd_card实例中的内容
-
创建声卡中的逻辑设备,PCM、control等
-
注册声卡snd_card到系统中
cstruct snd_card *snd_cards[SNDRV_CARDS]; /* sound/core/init.c */
2.3.2 PCM设备
PCM设备主要有两个功能:
-
playback, 把用户程序中发送过来的PCM信号输出
-
capture, 把输入的模拟信号转换为cpu能够处理的数字信号
2.3.3 ALSA驱动
首先是重要的结构体
- snd_card有一个双向链表,链表中挂接了各种逻辑设备

-
snd_pcm设备, 一个pcm设备含有两个流(playback和capture),这两个流下又分别有一个或多个子流,子流含有具体的操作函数用于播放或者捕获声音。可通过函数snd_pcm_set_ops() 进行设置
PCM设备注册流程
PCM设备注册的大体流程:
-
创建声卡snd_card, 使用 snd_card_new() 进行创建。
-
创建PCM设备,使用 snd_pcm_new() 进行创建。它主要进行以下操作:
-
创建 snd_pcm实例
-
创建pcm的输入、输出流(对应capture 和 playback)
-
创建pcm设备,并将其挂入snd_card的链表之中

-
-
使用 snd_pcm_set_ops() 设置子流的操作函数。
-
使用 snd_card_register() 注册声卡到系统中,回调函数snd_pcm_dev_register() 函数会将pcm设备注册到系统中。
-
snd_pcm_dev_register()函数会调用snd_register_device()进行注册。
snd_register_device()函数会找到一个没用的次设备号来与此设备绑定,同时将其注册到snd_minors中。同时指定操作函数ops。

具体的流程如下所示:

-
如何通过ALSA驱动调用到对应的ops?
-
alsa设备会统一一个入口函数snd_open()
-
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中)实例
copen() -> snd_open() -> snd_pcm_playback_open() -> snd_pcm_open() -> snd_pcm_open_file() -> snd_pcm_open_substream() -> soc_pcm_open()
调用过程梳理:
-
alsa会向系统注册一个字符设备,注册open函数为snd_open()
-
用户打开文件时(调用open函数),系统会调用snd_open()
-
snd_open()函数根据次设备号,从snd_minors数组中找到对应的open函数并调用,这里的open函数一般是snd_pcm_*_open(snd_pcm_playback_open 或 snd_pcm_capture_open)
-
snd_pcm_*_open会再次根据次设备号,从snd_minors数组中的private_data找到对应的pcm实例
-
snd_pcm_open调用snd_pcm_open_file打开对应的pcm实例
-
snd_pcm_open_file根据pcm实例,找到其对应的子流,然后调用snd_pcm_open_substream打开对应的子流
-
子流中的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?
-
Codec驱动与CPU的底层耦合过于紧密,这种不理想会导致代码的重复
-
音频事件没有标准的方法来通知用户,例如耳机、麦克风的插拔和检测,这些事件在移动设备中是非常普通的,而且通常都需要特定于机器的代码进行重新对音频路劲进行配置。
-
当进行播放或录音时,驱动会让整个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部分具体如何连接起来,如何运行?
连接
-
通过snd_soc_dai_link(它是snd_soc_card结构体的一个成员)结构,动态的配置3个不同设备之间的连接关系
-
在进行snd_soc_card声卡注册的时候,调用snd_soc_bind_card()函数
-
snd_soc_bind_card()函数根据snd_soc_dai_link结构在全局链表头上寻找对应的实例进行连接
这里会有个顺序问题,是否有深意?看代码中会先找cpu_dai, 再找codec_dai。
- 连接的信息记录在snd_soc_pcm_runtime(简称rtd)结构体中
运行
-
当真正创建pcm设备的时候,pcm结构中的private_data会指向rtd。
-
同时pcm对应的子流中的private_data也会指向rtd。
-
子流对应的操作函数被统一设置为 soc_pcm_*() 系列函数。
-
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
-
什么是kcontrol?
Kcontrol是一种控件,其主要实现控制声卡的音量,混音等一系列控制。
snd_kcontrol_new 结构如下
cstruct 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函数注册到系统中。
两种注册方式:
-
在codec driver的probe函数中调用 snd_soc_add_codec_controls 注册kcontrol。
-
通过snd_soc_register_codec先添加到codec_dai的component链表中。
-
-
简单控件
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函数的例子:
cint 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驱动实现对寄存器的读取。
-
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个输入端的开启和关闭。
- 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有以下几点不足:
-
只能描述自身,无法描述各个kcontrol之间的连接关系
-
没有相应的电源管理机制
-
没有相应的事件处理机制来响应播放、停止、上电、下电等音频事件
-
为了防止pop-pop声,需要用户程序关注各个kcontrol上电和下电的顺序
-
当一个音频路径不再有效时,不能自动关闭该路径上的所有的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标准引入的一个特殊符号,用于在宏定义中表示可变参数。它允许宏接收任意数量的参数,并在宏展开时将这些参数传递给其他宏或函数。
-
当VA_ARGS 代表一个参数的时候:
例如 SND_SOC_DAILINK_REG(name) 展开为
cSND_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) -
当VA_ARGS代表两个参数的时候:
例如SND_SOC_DAILINK_REG(name1, name2)
cSND_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) -
三个参数时同理!
传递多个变量的宏
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中的内容,特别是多个文件要使用时。
这里用到了下面的数组初始化方法:
数组可以使用下标加值的方法进行初始化,以及宏定义在数组内部使用宏定义也不会有语法错误。
以下是程序输出:
编译内核时的配置文件例子
-
新建一个文件夹,建立一个config配置项
config TESTCONDEV
bash// 文件路径 drivers/test/Kconfig config TESTCONDEV tristate "a TESTCONDEV driver module" default m help a TESTCONDEV driver -
在上一级的Kconfig导入drivers/test/Kconfig
bash// 路径 driver/Kconfig source "driver/test_config/Kconfig"