Rockchip RK3399 - ASoC 声卡之Control设备&kcontrol
----------------------------------------------------------------------------------------------------------------------------
开发板 :NanoPC-T4开发板
eMMC :16GB
LPDDR3 :4GB
显示屏 :15.6英寸HDMI接口显示屏
u-boot :2023.04
linux :6.3
----------------------------------------------------------------------------------------------------------------------------
Control是音频驱动中用来表示用户可操作的音频参数或功能的抽象。它可以是音量控制、混音控制(Mixer)、开关控制(Mux)等。Control 提供了一个统一的接口,使用户能够通过音频设备驱动程序来管理和调整音频参数。
ALSA CORE已经实现了Control中间层,在include/sound/control.h中定义了所有的Control API.,如果你要为你的Codec实现自己的控件,请在代码中包含该头文件。
需要注意的是:这里说的Control设备中的Control表示的是控制的意思;而后文提到的controls/control/kcontrol表示的是控件的意思,其主要实现控制声卡的音量,混音等一系列控制,可以理解为switch,
一、Control设备
1.1 创建Control设备
Control设备和PCM设备一样,都属于声卡下的逻辑设备。用户空间的应用程序通过alsa-lib访问该Control设备,读取或控制控件的控制状态,从而达到控制音频Codec进行各种Mixer等控制操作。
Control设备的创建过程大体上和PCM设备的创建过程相同。对于创建 PCM设备只需要在驱动初始化时主动调用snd_pcm_new函数创建,而Control设备则用snd_ctl_create创建。不过由于 snd_card_create函数中已经会调用 snd_ctl_create函数创建Control设备,故我们无需显示地创建Control设备,只要建立声卡,Control设备则被自动地创建。
我们来看一下snd_ctl_create到底做了什么。函数定义在sound/core/control.c;
/* * create control core: * called from init.c */ int snd_ctl_create(struct snd_card *card) // 传入声卡设备 { static struct snd_device_ops ops = { // 声卡Control设备操作集 .dev_free = snd_ctl_dev_free, .dev_register = snd_ctl_dev_register, .dev_disconnect = snd_ctl_dev_disconnect, }; int err; if (snd_BUG_ON(!card)) return -ENXIO; if (snd_BUG_ON(card->number < 0 || card->number >= SNDRV_CARDS)) // 声卡设备编号无效 return -ENXIO; snd_device_initialize(&card->ctl_dev, card); // 初始化card->ctl_dev控制设备,设置class为sound_class,parent为card->card_dev dev_set_name(&card->ctl_dev, "controlC%d", card->number); // 为控制设备分配一个名词 controlC%d err = snd_device_new(card, SNDRV_DEV_CONTROL, card, &ops); // 创建一个新的snd_device实例,并将添加到声卡设备的devices链表中 if (err < 0) put_device(&card->ctl_dev); return err; }
这个函数主要只做了两件事情:
- 调用snd_device_initialize初始化声卡的控制设备,也就是card->ctrl_dev;这里设置控制设备的parent为声卡设备card->card_dev,class为sound_class;
- 调用snd_device_new为控制设备分配一个snd_device实例,并添加到声卡设备的逻辑设备链表devices中;
1.1.1 snd_device_initialize
snd_device_initialize函数定义在sound/core/init.c,用于初始化struct device 结构体的各种成员变量并分配合适的资源;
/** * snd_device_initialize - Initialize struct device for sound devices * @dev: device to initialize * @card: card to assign, optional */ void snd_device_initialize(struct device *dev, struct snd_card *card) { device_initialize(dev); if (card) dev->parent = &card->card_dev; dev->class = sound_class; dev->release = default_release; }
1.1.2 snd_ctl_dev_register
在注册声卡设备card时会遍历声卡设备的逻辑设备链表devices,并调用声卡逻辑设备操作集中的dev_register函数,对于Control设备也就是snd_ctl_dev_register函数;
我们最后来看一下Control设备的操作集:
static struct snd_device_ops ops = { .dev_free = snd_ctl_dev_free, .dev_register = snd_ctl_dev_register, .dev_disconnect = snd_ctl_dev_disconnect, };
这些回调函数都是定义在sound/core/control.c,以snd_ctl_dev_register函数为例,该回调函数建立了和用户空间应用程序(alsa-lib)通信所用的设备文件节点:/dev/snd/controlC%d;
/* * registration of the control device */ static int snd_ctl_dev_register(struct snd_device *device) { struct snd_card *card = device->device_data; return snd_register_device(SNDRV_DEVICE_TYPE_CONTROL, card, -1, // 注册Control设备 &snd_ctl_f_ops, card, &card->ctl_dev); }
dev_free、dev_disconnnet我们不是特别关心,忽略就即可:

/* * disconnection of the control device */ static int snd_ctl_dev_disconnect(struct snd_device *device) { struct snd_card *card = device->device_data; struct snd_ctl_file *ctl; read_lock(&card->ctl_files_rwlock); // 读写自旋锁 list_for_each_entry(ctl, &card->ctl_files, list) { // 遍历ctl_files链表 wake_up(&ctl->change_sleep); kill_fasync(&ctl->fasync, SIGIO, POLL_ERR); } read_unlock(&card->ctl_files_rwlock); // 释放锁 return snd_unregister_device(&card->ctl_dev); // 卸载声卡逻辑设备 } /* * free all controls */ static int snd_ctl_dev_free(struct snd_device *device) { struct snd_card *card = device->device_data; struct snd_kcontrol *control; down_write(&card->controls_rwsem); // 获取读写信号量 while (!list_empty(&card->controls)) { // 遍历控制链表 control = snd_kcontrol(card->controls.next); snd_ctl_remove(card, control); } up_write(&card->controls_rwsem); // 释放读写信号量 put_device(&card->ctl_dev); return 0; }
二、kcontrol
kcontrol其实是一种控件,其主要实现控制声卡的音量,混音等一系列控制,可以理解为switch。
kcontrol对应的数据结构是snd_kcontrol_new,这些snd_kcontrol_new结构会在声卡的初始化阶段,通过snd_soc_add_component_controls函数注册到系统中,用户空间就可以通过amixer或alsamixer等工具查看和设定这些控件的状态。
实际上除了snd_kcontrol_new结构外,还有一个snd_kcontrol结构,snd_kcontrol_new更像是kcontrol的模板,而snd_kcontrol才是真正的kcontrol。
控件的创建步骤如下:
(1)定义snd_kcontrol_new数组;
(2)通过snd_soc_add_component_controls根据snd_kcontrol_new数组创建并添加多个kcontrol到声卡card的controls链表;
2.1 数据结构
2.1.1 snd_kcontrol_new
struct snd_kcontrol_new定义在include/sound/control.h文件中:
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 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; };
其中:
- iface:控件的类型,alsa定义了几种类型(SNDDRV_CTL_ELEM_IFACE_XXX),常用的类型是 MIXER,当然也可以定义属于全局的 CARD 类型,也可以定义属于某类设备的类型,例如 HWDEP,PCMRAWMIDI,TIMER 等,这时需要在 device 和 subdevice 字段中指出声卡的设备逻辑编号;
- name :控件的名字,从ALSA 0.9.x开始,控件的名字是变得比较重要,因为控件的作用是按名字来归类的。ALSA 已经预定义了一些控件的名字,我们在后面的章节中会详细讨论;
- index:控件的在该声卡中的编号。如果声卡中有不止一个codec,每个codec中有相同名字的控件,这时我们可以通过index来区分这些控件。当index 为 0 时,则可以忽略这种区分策略;
- access:控件的访问类型。每一个 bit 代表一种访问类型,这些访问类型可以多个“或”运算组合在一起;
- private_value:根据不同的控件类型有不同的意义,比如对于普通的控件,private_value字段可以用来定义该控件所对应的寄存器的地址以及对应的控制位在寄存器中的位置信息;
- tlv:控件元数据;
- get:用于获取控件当前的状态值;
- put:用于设置控件的状态值;
2.1.2 关系图
为了更加清晰的了解struct snd_kcontrol_new、struct snd_kcontrol 等数据结构的关系,我们绘制了如下关系图:
2.1.3 控件的名字
控件的名字需要遵循一些标准,通常可以分成3部分来定义控件的名字:源–方向–功能。
- 源:可以理解为该控件的输入端,alsa已经预定义了一些常用的源,例如:Master,PCM,CD,Line、I2S、Headphone、Speaker、Mic等等;
- 方向:代表该控件的数据流向,例如:Playback,Capture,Bypass,Bypass Capture 等等,也可以不定义方向,这时表示该控件是双向的( playback和capture);
- 功能:根据控件的功能,可以是以下字符串:Switch,Volume,Route等等;
也有一些命名上的特例,比如Tone Control - Switch、Tone Control - Bass等,目前官方已经不推荐使用了。
更多内容可以参考官方指导手册:Standard ALSA Control Names。
2.1.3 访问标志
access字段是一个bitmask,它保存了该控件的访问类型。默认的访问类型是:SNDDRV_CTL_ELEM_ACCESS_READWRITE,表明该控件支持读和写操作。如果access字段没有定义(.access==0),此时也认为是 READWRITE 类型。
如果是一个只读控件,access应该设置为:SNDDRV_CTL_ELEM_ACCESS_READ,这时,我们不必定义put回调函数。类似地,如果是只写控件,access应该设置为:SNDDRV_CTL_ELEM_ACCESS_WRITE,这时,我们不必定义get回调函数。
如果控件的值会频繁地改变(例如:电平表),我们可以使用VOLATILE类型,这意味着该控件会在没有通知的情况下改变,应用程序应该定时地查询该控件的值。
2.1.5 回调函数
(1)info回调函数
info回调函数用于获取控件的详细信息。它的主要工作就是填充通过参数传入的snd_ctl_elem_info数据结构,以下例子是一个具有单个元素的boolean型控件的info回调:
/** * snd_ctl_boolean_mono_info - Helper function for a standard boolean info * callback with a mono channel * @kcontrol: the kcontrol instance * @uinfo: info to store * * This is a function that can be used as info callback for a standard * boolean control with a single mono channel. * * Return: Zero (always successful) */ int snd_ctl_boolean_mono_info(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_info *uinfo) { uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN; uinfo->count = 1; uinfo->value.integer.min = 0; uinfo->value.integer.max = 1; return 0; }
其中:
- type :指出该控件的值类型,值类型可以是BOOLEAN,INTEGER,ENUMERATED,BYTES,IEC958和INTEGER64之一;
- count:指出了该控件中包含有多少个元素单元,比如,立体声的音量控件左右两个声道的音量值,它的count字段等于2;
- value: 是一个联合体(union),value的内容和控件的类型有关;其中,boolean和integer类型是相同的;
ENUMERATED类型有些特殊。它的value需要设定一个字符串和字符串的索引,请看以下例子:
/** * snd_ctl_enum_info - fills the info structure for an enumerated control * @info: the structure to be filled * @channels: the number of the control's channels; often one * @items: the number of control values; also the size of @names * @names: an array containing the names of all control values * * Sets all required fields in @info to their appropriate values. * If the control's accessibility is not the default (readable and writable), * the caller has to fill @info->access. * * Return: Zero (always successful) */ int snd_ctl_enum_info(struct snd_ctl_elem_info *info, unsigned int channels, unsigned int items, const char *const names[]) { info->type = SNDRV_CTL_ELEM_TYPE_ENUMERATED; info->count = channels; info->value.enumerated.items = items; if (!items) return 0; if (info->value.enumerated.item >= items) info->value.enumerated.item = items - 1; WARN(strlen(names[info->value.enumerated.item]) >= sizeof(info->value.enumerated.name), "ALSA: too long item name '%s'\n", names[info->value.enumerated.item]); strscpy(info->value.enumerated.name, names[info->value.enumerated.item], sizeof(info->value.enumerated.name)); return 0; }
alsa已经为我们实现了一些通用的info回调函数,例如:snd_ctl_boolean_mono_info,snd_ctl_boolean_stereo_info、snd_ctl_enum_info等等。
(2)get回调函数
get回调函数用于读取控件的当前值,并返回给用户空间的应用程序;
static int snd_myctl_get(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol) { struct mychip *chip = snd_kcontrol_chip(kcontrol); ucontrol->value.integer.value[0] = get_some_value(chip); return 0; }
value字段的赋值依赖于控件的类型(如同info回调)。很多声卡的驱动利用它存储硬件寄存器的地址、bit-shift和bit-mask,这时private_value字段可以按以下例子进行设置:
private_value = reg | (shift << 16) | (mask << 24);
然后,get回调函数可以这样实现:
static int snd_sbmixer_get_single(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol) { int reg = kcontrol->private_value & 0xff; int shift = (kcontrol->private_value >> 16) & 0xff; int mask = (kcontrol->private_value >> 24) & 0xff; .... //根据以上的值读取相应寄存器的值并填入value中 }
如果控件的count字段大于1,表示控件有多个元素单元,get回调函数也应该为value填充多个数值。
(3)put回调函数
put回调函数用于把应用程序的控制值设置到控件中。
static int snd_myctl_put(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol) { struct mychip *chip = snd_kcontrol_chip(kcontrol); int changed = 0; if (chip->current_value != ucontrol->value.integer.value[0]) { change_current_value(chip, ucontrol->value.integer.value[0]); changed = 1; } return changed; }
如上述例子所示,当控件的值被改变时,put回调必须要返回1,如果值没有被改变,则返回0。如果发生了错误,则返回一个负数的错误号。
和get回调一样,当控件的count 大于1时,put回调也要处理多个控件中的元素值。
2.2 辅助宏定义
ASoc层已经为我们准备了大量的宏定义,用于定义常用的控件,这些宏定义位于include/sound/soc.h中。下面我们分别讨论一下如何用这些预设的宏定义来定义一些常用的控件。
2.2.1 简单的控件SOC_SINGLE
SOC_SINGLE应该算是最简单的控件了,这种控件只有一个控制量,比如一个开关,或者是一个数值变量(比如Codec中某个频率,FIFO大小等等)。我们看看这个宏是如何定义的:
#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, 0) }
宏定义的参数分别是:
- xname:该控件的名字;
- reg:该控件对应的寄存器的地址;
- shift:控制位在寄存器中的位移;
- max:控件可设置的最大值;
- invert:设定值是否逻辑取反;
这里又使用了一个宏来定义private_value字段:SOC_SINGLE_VALUE,我们看看它的定义:
#define SOC_SINGLE_VALUE(xreg, xshift, xmax, xinvert, xautodisable) \ SOC_DOUBLE_VALUE(xreg, xshift, xshift, xmax, xinvert, xautodisable) #define SOC_DOUBLE_VALUE(xreg, shift_left, shift_right, xmax, xinvert, xautodisable) \ ((unsigned long)&(struct soc_mixer_control) \ {.reg = xreg, .rreg = xreg, .shift = shift_left, \ .rshift = shift_right, .max = xmax, \ .invert = xinvert, .autodisable = xautodisable})
这里实际上是定义了一个soc_mixer_control结构,然后把该结构的地址赋值给了private_value字段,soc_mixer_control结构是这样的:
/* mixer control */ struct soc_mixer_control { int min, max, platform_max; int reg, rreg; unsigned int shift, rshift; unsigned int sign_bit; unsigned int invert:1; unsigned int autodisable:1; #ifdef CONFIG_SND_SOC_TOPOLOGY struct snd_soc_dobj dobj; #endif };
看来soc_mixer_control是控件特征的真正描述者,它确定了该控件对应寄存器的地址,位移值,最大值和是否逻辑取反等特性。
控件的put回调函数和get回调函数需要借助该结构来访问实际的寄存器。我们看看这get回调函数snd_soc_get_volsw的定义, 定义位于sound/soc/soc-ops.c;

/** * snd_soc_get_volsw - single mixer get callback * @kcontrol: mixer control * @ucontrol: control element information * * Callback to get the value of a single mixer control, or a double mixer * control that spans 2 registers. * * Returns 0 for success. */ int snd_soc_get_volsw(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol) { struct snd_soc_component *component = snd_kcontrol_chip(kcontrol); struct soc_mixer_control *mc = (struct soc_mixer_control *)kcontrol->private_value; unsigned int reg = mc->reg; unsigned int reg2 = mc->rreg; unsigned int shift = mc->shift; unsigned int rshift = mc->rshift; int max = mc->max; int min = mc->min; int sign_bit = mc->sign_bit; unsigned int mask = (1 << fls(max)) - 1; unsigned int invert = mc->invert; int val; int ret; if (sign_bit) mask = BIT(sign_bit + 1) - 1; ret = snd_soc_read_signed(component, reg, mask, shift, sign_bit, &val); if (ret) return ret; ucontrol->value.integer.value[0] = val - min; if (invert) ucontrol->value.integer.value[0] = max - ucontrol->value.integer.value[0]; if (snd_soc_volsw_is_stereo(mc)) { if (reg == reg2) ret = snd_soc_read_signed(component, reg, mask, rshift, sign_bit, &val); else ret = snd_soc_read_signed(component, reg2, mask, shift, sign_bit, &val); if (ret) return ret; ucontrol->value.integer.value[1] = val - min; if (invert) ucontrol->value.integer.value[1] = max - ucontrol->value.integer.value[1]; } return 0; }
上述代码一目了然,从private_value字段取出soc_mixer_control结构,利用该结构的信息,访问对应的寄存器,返回相应的值。
2.2.2 SOC_SINGLE_TLV
是SOC_SINGLE的一种扩展,主要用于定义那些有增益控制的控件,例如音量控制器,EQ均衡器等等。
#define SOC_SINGLE_TLV(xname, reg, shift, max, invert, tlv_array) \ { .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname, \ .access = SNDRV_CTL_ELEM_ACCESS_TLV_READ |\ SNDRV_CTL_ELEM_ACCESS_READWRITE,\ .tlv.p = (tlv_array), \ .info = snd_soc_info_volsw, .get = snd_soc_get_volsw,\ .put = snd_soc_put_volsw, \ .private_value = SOC_SINGLE_VALUE(reg, shift, max, invert, 0) }
从定义可以看出,用于设定寄存器信息的private_value字段的定义和SOC_SINGLE是一样的,甚至put、get回调函数也是使用同一套,唯一不同的是增加了一个tlv_array参数,并把它赋值给了tlv.p字段。用户空间可以通过对声卡的Control设备发起以下两种ioctl来访问tlv字段所指向的数组:
SNDRV_CTL_IOCTL_TLV_READ
SNDRV_CTL_IOCTL_TLV_WRITE
SNDRV_CTL_IOCTL_TLV_COMMAND
通常,tlv_array用来描述寄存器的设定值与它所代表的实际意义之间的映射关系,最常用的就是用于音量控件时,设定值与对应的dB值之间的映射关系,请看以下例子:
static const DECLARE_TLV_DB_SCALE(mixin_boost_tlv, 0, 900, 0); static const struct snd_kcontrol_new wm1811_snd_controls[] = { SOC_SINGLE_TLV("MIXINL IN1LP Boost Volume", WM8994_INPUT_MIXER_1, 7, 1, 0, mixin_boost_tlv), SOC_SINGLE_TLV("MIXINL IN1RP Boost Volume", WM8994_INPUT_MIXER_1, 8, 1, 0, mixin_boost_tlv), };
DECLARE_TLV_DB_SCALE用于定义一个dB值映射的tlv_array,上述的例子表明,该tlv的类型是SNDRV_CTL_TLVT_DB_SCALE,寄存器的最小值对应是0dB,寄存器每增加一个单位值,对应的dB数增加是9dB(0.01dB*900),而由接下来的两组SOC_SINGLE_TLV定义可以看出,我们定义了两个boost控件,寄存器的地址都是WM8994_INPUT_MIXER_1,控制位分别是第7bit和第8bit,最大值是1,所以,该控件只能设定两个数值0和1,对应的dB值就是0dB和9dB。
2.2.3 SOC_DOUBLE
与SOC_SINGLE相对应,区别是SOC_SINGLE只控制一个变量,而SOC_DOUBLE则可以同时在一个寄存器中控制两个相似的变量,最常用的就是用于一些立体声的控件,我们需要同时对左右声道进行控制,因为多了一个声道,参数也就相应地多了一个shift位移值;
#define SOC_DOUBLE(xname, reg, shift_left, shift_right, 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_DOUBLE_VALUE(reg, shift_left, shift_right, \ max, invert, 0) }
SOC_DOUBLE_R :与SOC_DOUBLE类似,对于左右声道的控制寄存器不一样的情况,使用SOC_DOUBLE_R来定义,参数中需要指定两个寄存器地址。
SOC_DOUBLE_TLV : 与SOC_SINGLE_TLV对应的立体声版本,通常用于立体声音量控件的定义。
SOC_DOUBLE_R_TLV: 左右声道有独立寄存器控制的SOC_DOUBLE_TLV版本。
2.2.4 Mixer控件
用于音频通道的路由控制,由多个输入和一个输出组成,多个输入可以自由地混合在一起,形成混合后的输出:如手机同时打电话,又播放音乐,需要将两路数据混合后再输出到Speaker,就需要用到Mixer;
对于Mixer控件,我们可以认为是多个简单控件的组合,通常,我们会为Mixer的每个输入端都单独定义一个简单控件来控制该路输入的开启和关闭,反应在代码上,就是定义一个soc_kcontrol_new数组:
static const struct snd_kcontrol_new wm8960_lin_boost[] = { SOC_SINGLE("LINPUT2 Switch", WM8960_LINPATH, 6, 1, 0), SOC_SINGLE("LINPUT3 Switch", WM8960_LINPATH, 7, 1, 0), SOC_SINGLE("LINPUT1 Switch", WM8960_LINPATH, 8, 1, 0), };
以上这个Mixer使用寄存器WM8960_LINPATH的第6,7、8位来分别控制3个输入端的开启和关闭。
2.2.5 Mux控件
与Mixer控件类似,也是多个输入端和一个输出端的组合控件,与Mixer控件不同的是,Mux控件的多个输入端同时只能有一个被选中。因此,Mux控件所对应的寄存器,通常可以设定一段连续的数值,每个不同的数值对应不同的输入端被打开,与上述的Mixer控件不同,ASoc用soc_enum结构来描述Mux控件的寄存器信息:
/* enumerated kcontrol */ struct soc_enum { int reg; unsigned char shift_l; unsigned char shift_r; unsigned int items; unsigned int mask; const char * const *texts; const unsigned int *values; unsigned int autodisable:1; #ifdef CONFIG_SND_SOC_TOPOLOGY struct snd_soc_dobj dobj; #endif };
其中:
- reg,reg2,shift_l,shift_r:两个寄存器地址和位移字段,用于描述左右声道的控制寄存器信息;
- texts:字符串数组指针用于描述每个输入端对应的名字;
- values:则指向一个数组,该数组定义了寄存器可以选择的值,每个值对应一个输入端,如果values是一组连续的值,通常我们可以忽略values参数。
下面我们先看看如何定义一个Mux控件:第一步,定义字符串和values数组,以下的例子因为values是连续的,所以不用定义:
static const char *drc_path_text[] = { "ADC", "DAC" };
第二步,利用ASoc提供的辅助宏定义soc_enum结构,用于描述寄存器:
static const struct soc_enum drc_path = SOC_ENUM_SINGLE(WM8993_DRC_CONTROL_1, 14, 2, drc_path_text);
亲爱的读者和支持者们,自动博客加入了打赏功能,陆陆续续收到了各位老铁的打赏。在此,我想由衷地感谢每一位对我们博客的支持和打赏。你们的慷慨与支持,是我们前行的动力与源泉。
日期 | 姓名 | 金额 |
---|---|---|
2023-09-06 | *源 | 19 |
2023-09-11 | *朝科 | 88 |
2023-09-21 | *号 | 5 |
2023-09-16 | *真 | 60 |
2023-10-26 | *通 | 9.9 |
2023-11-04 | *慎 | 0.66 |
2023-11-24 | *恩 | 0.01 |
2023-12-30 | I*B | 1 |
2024-01-28 | *兴 | 20 |
2024-02-01 | QYing | 20 |
2024-02-11 | *督 | 6 |
2024-02-18 | 一*x | 1 |
2024-02-20 | c*l | 18.88 |
2024-01-01 | *I | 5 |
2024-04-08 | *程 | 150 |
2024-04-18 | *超 | 20 |
2024-04-26 | .*V | 30 |
2024-05-08 | D*W | 5 |
2024-05-29 | *辉 | 20 |
2024-05-30 | *雄 | 10 |
2024-06-08 | *: | 10 |
2024-06-23 | 小狮子 | 666 |
2024-06-28 | *s | 6.66 |
2024-06-29 | *炼 | 1 |
2024-06-30 | *! | 1 |
2024-07-08 | *方 | 20 |
2024-07-18 | A*1 | 6.66 |
2024-07-31 | *北 | 12 |
2024-08-13 | *基 | 1 |
2024-08-23 | n*s | 2 |
2024-09-02 | *源 | 50 |
2024-09-04 | *J | 2 |
2024-09-06 | *强 | 8.8 |
2024-09-09 | *波 | 1 |
2024-09-10 | *口 | 1 |
2024-09-10 | *波 | 1 |
2024-09-12 | *波 | 10 |
2024-09-18 | *明 | 1.68 |
2024-09-26 | B*h | 10 |
2024-09-30 | 岁 | 10 |
2024-10-02 | M*i | 1 |
2024-10-14 | *朋 | 10 |
2024-10-22 | *海 | 10 |
2024-10-23 | *南 | 10 |
2024-10-26 | *节 | 6.66 |
2024-10-27 | *o | 5 |
2024-10-28 | W*F | 6.66 |
2024-10-29 | R*n | 6.66 |
2024-11-02 | *球 | 6 |
2024-11-021 | *鑫 | 6.66 |
2024-11-25 | *沙 | 5 |
2024-11-29 | C*n | 2.88 |

【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
2021-06-17 代码命名规范