ALSA声卡驱动:

1.Linux ALSA声卡驱动之一:ALSA架构简介和ASOC架构简介

2.Linux ALSA声卡驱动之二:Platform

3. Linux ALSA声卡驱动之三:Platform之Cpu_dai

4. Linux ALSA声卡驱动之四:Codec 以及Codec_dai

5.Linux ALSA声卡驱动之五:Machine 以及ALSA声卡的注册

6.Linux ALSA声卡驱动之六:PCM的注册流程

7.Linux ALSA声卡驱动之七:录音(Capture) 调用流程

一.  Codec简介

在移动设备中,Codec的作用可以归结为4种,分别是:

  1. 对PCM等信号进行D/A转换,把数字的音频信号转换为模拟信号
  2. 对Mic、Linein或者其他输入源的模拟信号进行A/D转换,把模拟的声音信号转变CPU能够处理的数字信号
  3. 对音频通路进行控制,比如播放音乐,收听调频收音机,又或者接听电话时,音频信号在codec内的流通路线是不一样的
  4. 对音频信号做出相应的处理,例如音量控制,功率放大,EQ控制等

二、 Codec和codec_dai 的注册:snd_soc_register_codec

在第一章(Linux ALSA声卡驱动之一:ALSA架构简介和ASOC架构简介)以及介绍了platfrom可以细分platform和cpu_dai,codec可以细分codec和codec_dai ,platform和cpu_dai都有各自的驱动注册。由此猜想codec和codec_dai也是由各自的驱动注册吧,但实际却不是这样,mtk平台codec和codec_dai两部分代码都由一个驱动注册 mtk-soc-codec-63xx.c  snd_soc_register_codec,其实如果拆分出来也是可以。

2.1 snd_soc_register_codec函数时序图

从上述的时序图不难看出,通过snd_soc_register_codec , snd_soc_register_dais  , snd_soc_component_add_unlocked , list_add(&codec->list, &codec_list)这几个函数的转变,最终会把相应的component->list添加到component_list链表 ,codec->list添加到codec_list链表。下面我们看具体的代码细节

2.2 snd_soc_register_codec 函数相关结构体图

结合下图我们可以比较清晰的知道各个结构体之间的关系:

  1. 结构体snd_soc_dai   通过*driver 维护snd_soc_codec_driver  ,*driver就是mtk_6357_dai_codecs。snd_soc_dai的初始化也是通过mtk_6357_dai_codecs。 通过list_add(&dai->list, &component->dai_list),把自己添加到snd_soc_component中
  2. snd_soc_component 通过mtk_6357_dai_codecs初始化赋值,并且*dai_drv指向mtk_6357_dai_codecs  ,*driver维护snd_soc_component_driver结构体,dai_list链表头, 下面挂载snd_soc_dai:snd_soc_dai->list   list_add(&dai->list, &component->dai_list)。通过list_add(&component->list, &component_list) 把snd_soc_component添加到全局链表component_list中

2.3 snd_soc_register_codec函数实现细节

2.3.1 mtk_mt6357_codec_dev_probe函数

static int mtk_mt6357_codec_dev_probe(struct platform_device *pdev)
{pdev->dev.coherent_dma_mask = DMA_BIT_MASK(64);if (pdev->dev.dma_mask == NULL)pdev->dev.dma_mask = &pdev->dev.coherent_dma_mask;if (pdev->dev.of_node) {dev_set_name(&pdev->dev, "%s", MT_SOC_CODEC_NAME);/* check if use hp depop flow *///读取use_hp_depop_flow属性值of_property_read_u32(pdev->dev.of_node,"use_hp_depop_flow",&mUseHpDepopFlow);pr_debug("%s(), use_hp_depop_flow = %d\n",__func__, mUseHpDepopFlow);} else {pr_debug("%s(), pdev->dev.of_node = NULL!!!\n", __func__);}pr_debug("%s: dev name %s\n", __func__, dev_name(&pdev->dev));return snd_soc_register_codec(&pdev->dev,&soc_mtk_codec, mtk_6357_dai_codecs,ARRAY_SIZE(mtk_6357_dai_codecs));
}
  • soc_mtk_codec  这个结构体我们暂时不分析,其中不难看出,主要是对probe  remove  read write 函数的驱动层实现
static struct snd_soc_codec_driver soc_mtk_codec = {.probe = mt6357_codec_probe,.remove = mt6357_codec_remove,.read = mt6357_read,.write = mt6357_write,
};
  • mtk_6357_dai_codecs 
static struct snd_soc_dai_driver mtk_6357_dai_codecs[] = {{.name = MT_SOC_CODEC_TXDAI_NAME,.ops = &mt6323_aif1_dai_ops,.playback = {.stream_name = MT_SOC_DL1_STREAM_NAME,.channels_min = 1,.channels_max = 2,.rates = SNDRV_PCM_RATE_8000_192000,.formats = SND_SOC_ADV_MT_FMTS,},},{.name = MT_SOC_CODEC_RXDAI_NAME,.ops = &mt6323_aif1_dai_ops,.capture = {.stream_name = MT_SOC_UL1_STREAM_NAME,.channels_min = 1,.channels_max = 2,.rates = SOC_HIGH_USE_RATE,.formats = SND_SOC_ADV_MT_FMTS,},},{.name = MT_SOC_CODEC_TDMRX_DAI_NAME,.ops = &mt6323_aif1_dai_ops,.capture = {.stream_name = MT_SOC_TDM_CAPTURE_STREAM_NAME,.channels_min = 2,.channels_max = 2,.rates = SNDRV_PCM_RATE_8000_192000,.formats = (SNDRV_PCM_FMTBIT_U8 | SNDRV_PCM_FMTBIT_S8 |SNDRV_PCM_FMTBIT_U16_LE | SNDRV_PCM_FMTBIT_S16_LE |SNDRV_PCM_FMTBIT_U16_BE | SNDRV_PCM_FMTBIT_S16_BE |SNDRV_PCM_FMTBIT_U24_LE | SNDRV_PCM_FMTBIT_S24_LE |SNDRV_PCM_FMTBIT_U24_BE | SNDRV_PCM_FMTBIT_S24_BE |SNDRV_PCM_FMTBIT_U24_3LE | SNDRV_PCM_FMTBIT_S24_3LE |SNDRV_PCM_FMTBIT_U24_3BE | SNDRV_PCM_FMTBIT_S24_3BE |SNDRV_PCM_FMTBIT_U32_LE | SNDRV_PCM_FMTBIT_S32_LE |SNDRV_PCM_FMTBIT_U32_BE | SNDRV_PCM_FMTBIT_S32_BE),},},{.name = MT_SOC_CODEC_I2S0TXDAI_NAME,.ops = &mt6323_aif1_dai_ops,.playback = {.stream_name = MT_SOC_I2SDL1_STREAM_NAME,.channels_min = 1,.channels_max = 2,.rate_min = 8000,.rate_max = 192000,.rates = SNDRV_PCM_RATE_8000_192000,.formats = SND_SOC_ADV_MT_FMTS,},},
........

如上代码也就是对snd_soc_dai_driver结构体赋值,取得mtk_6357_dai_codecs数组,这个跟上一章节(Linux ALSA声卡驱动之三:Platform之Cpu_dai),mtk_dai_stub_dai 数组获取一样。下面是snd_soc_dai_driver 和snd_soc_pcm_stream  两个结构体的构成。

  • snd_soc_dai_driver
struct snd_soc_dai_driver {/* DAI description */const char *name;unsigned int id;unsigned int base;/* 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);/* DAI is also used for the control bus */bool bus_control;/* ops */const struct snd_soc_dai_ops *ops;//一个struct snd_soc_dai_ops类型的结构, 该dai的操作函数集/* DAI capabilities */struct snd_soc_pcm_stream capture;struct snd_soc_pcm_stream playback;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;
}
  • snd_soc_pcm_stream

struct snd_soc_pcm_stream {const char *stream_name;u64 formats; //这个格式定义在pcm.h SNDRV_PCM_FMTBITxxxx     /* SNDRV_PCM_FMTBIT_* */unsigned int rates;  //采样率 我们常说16k 48k  /* SNDRV_PCM_RATE_* */unsigned int rate_min;    //最小采样率 /* min rate */unsigned int rate_max;    //最大采样率 /* max rate */unsigned int channels_min;    //最小通道数,单声道1/* min channels */unsigned int channels_max; //最大通道数  立体声道2/* max channels */unsigned int sig_bits;  //位数 8  还是16    /* number of bits of content */}

2.3.1 snd_soc_register_codec

  1. codec_drv的值赋值到codec->component
  2. codec_drv和dai_drv都挂载到 codec->component->dai_list,期间还会生出snd_soc_dai也会挂载到 codec->component
  3. codec->list 挂载到全局变量codec_list
int snd_soc_register_codec(struct device *dev,const struct snd_soc_codec_driver *codec_drv,struct snd_soc_dai_driver *dai_drv,int num_dai)
{struct snd_soc_dapm_context *dapm;struct snd_soc_codec *codec;struct snd_soc_dai *dai;int ret, i;dev_dbg(dev, "codec register %s\n", dev_name(dev));codec = kzalloc(sizeof(struct snd_soc_codec), GFP_KERNEL);if (codec == NULL)return -ENOMEM;codec->component.codec = codec;ret = snd_soc_component_initialize(&codec->component,&codec_drv->component_driver, dev);if (ret)goto err_free;// //codec_drv的值赋值到codec->componentif (codec_drv->controls) {codec->component.controls = codec_drv->controls;codec->component.num_controls = codec_drv->num_controls;}if (codec_drv->dapm_widgets) {codec->component.dapm_widgets = codec_drv->dapm_widgets;codec->component.num_dapm_widgets = codec_drv->num_dapm_widgets;}if (codec_drv->dapm_routes) {codec->component.dapm_routes = codec_drv->dapm_routes;codec->component.num_dapm_routes = codec_drv->num_dapm_routes;}if (codec_drv->probe)codec->component.probe = snd_soc_codec_drv_probe;if (codec_drv->remove)codec->component.remove = snd_soc_codec_drv_remove;if (codec_drv->write)codec->component.write = snd_soc_codec_drv_write;if (codec_drv->read)codec->component.read = snd_soc_codec_drv_read;codec->component.ignore_pmdown_time = codec_drv->ignore_pmdown_time;dapm = snd_soc_codec_get_dapm(codec);dapm->idle_bias_off = codec_drv->idle_bias_off;dapm->suspend_bias_off = codec_drv->suspend_bias_off;if (codec_drv->seq_notifier)dapm->seq_notifier = codec_drv->seq_notifier;if (codec_drv->set_bias_level)dapm->set_bias_level = snd_soc_codec_set_bias_level;codec->dev = dev;codec->driver = codec_drv;codec->component.val_bytes = codec_drv->reg_word_size;#ifdef CONFIG_DEBUG_FScodec->component.init_debugfs = soc_init_codec_debugfs;codec->component.debugfs_prefix = "codec";
#endifif (codec_drv->get_regmap)codec->component.regmap = codec_drv->get_regmap(dev);for (i = 0; i < num_dai; i++) {fixup_codec_formats(&dai_drv[i].playback);fixup_codec_formats(&dai_drv[i].capture);}
//依据dai_drv 创建snd_soc_dai  并且添加到 codec->component->dai_listret = snd_soc_register_dais(&codec->component, dai_drv, num_dai, false);if (ret < 0) {dev_err(dev, "ASoC: Failed to register DAIs: %d\n", ret);goto err_cleanup;}//从component.dai_list中取出dai,并且codec赋值到dai->codec , list_for_each_entry(dai, &codec->component.dai_list, list)dai->codec = codec;mutex_lock(&client_mutex);//codec->component->list添加到component_list ,至此cpu dai 的component 和codec dai  的component都添加到component_listsnd_soc_component_add_unlocked(&codec->component);//codec->list添加到codec_list中list_add(&codec->list, &codec_list);mutex_unlock(&client_mutex);dev_dbg(codec->dev, "ASoC: Registered codec '%s'\n",codec->component.name);return 0;err_cleanup:snd_soc_component_cleanup(&codec->component);
err_free:kfree(codec);return ret;
}

2.3.2 snd_soc_register_dais

  1. 初始化snd_soc_component,并赋值。
  2. dai_drv for循环逐一取出snd_soc_dai_driver 创建新的snd_soc_dai 并且挂在component->dai_list
static int snd_soc_register_dais(struct snd_soc_component *component,struct snd_soc_dai_driver *dai_drv, size_t count,bool legacy_dai_naming)
{struct device *dev = component->dev;struct snd_soc_dai *dai;unsigned int i;int ret;dev_dbg(dev, "ASoC: dai register %s #%Zu\n", dev_name(dev), count);//snd_soc_dai_driver 添加到snd_soc_componentcomponent->dai_drv = dai_drv;component->num_dai = count;for (i = 0; i < count; i++) {dai = kzalloc(sizeof(struct snd_soc_dai), GFP_KERNEL);if (dai == NULL) {ret = -ENOMEM;goto err;}/** Back in the old days when we still had component-less DAIs,* instead of having a static name, component-less DAIs would* inherit the name of the parent device so it is possible to* register multiple instances of the DAI. We still need to keep* the same naming style even though those DAIs are not* component-less anymore.*///snd_soc_dai_driver name 赋值到snd_soc_dai name 并且赋值 idif (count == 1 && legacy_dai_naming &&(dai_drv[i].id == 0 || dai_drv[i].name == NULL)) {dai->name = fmt_single_name(dev, &dai->id);} else {dai->name = fmt_multiple_name(dev, &dai_drv[i]);if (dai_drv[i].id)dai->id = dai_drv[i].id;elsedai->id = i;}if (dai->name == NULL) {kfree(dai);ret = -ENOMEM;goto err;}// 创建新的snd_soc_dai 并且挂在component->dai_listdai->component = component;dai->dev = dev;dai->driver = &dai_drv[i];//ops 由dai_drv赋值的,  可能是cpu dai 就是:mtk_dai_stub_dai ,codec dai:mtk_6357_dai_codecsif (!dai->driver->ops)dai->driver->ops = &null_dai_ops;list_add(&dai->list, &component->dai_list);dev_dbg(dev, "ASoC: Registered DAI '%s'\n", dai->name);}return 0;err:snd_soc_unregister_dais(component);return ret;
}

2.3.3 struct snd_soc_dai    由核心层内部创建和维护, 用于代表一个dai. 可以是cpu_dai或codec_dai

struct snd_soc_dai {const char *name;//该dai的名称, ASoC核心层靠name来区分不同的dai.int id;//应该是核心层自动分配的一个id号struct device *dev;/* driver ops */struct snd_soc_dai_driver *driver;//指向下属的snd_soc_dai_driver, 该结构体一般由底层驱动实现/* 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;void *capture_dma_data;/* 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;struct snd_soc_component *component;/* CODEC TDM slot masks and params (for fixup) */unsigned int tx_mask;unsigned int rx_mask;struct list_head list;//用于把自己挂载到component->dai_list下.   list_add(&dai->list, &component->dai_list)
};

2.3.4 struct snd_soc_component   当底层驱动注册platform、codec+codec dai、cpu dai时, 核心层都会创建一个对应的snd_soc_component,并且会挂到component_list 链表

struct snd_soc_component {const char *name;  //这个跟device_driver->name 和snd_soc_component_driver->id有关,int id;const char *name_prefix;struct device *dev;struct snd_soc_card *card;unsigned int active;unsigned int ignore_pmdown_time:1; /* pmdown_time is ignored at stop */unsigned int registered_as_component:1;//在snd_soc_register_component时修改为1struct list_head list;//用于把自己挂载到全局链表component_list下 ,component_list 实在soc-core 中保持的全局变量struct snd_soc_dai_driver *dai_drv;//dai  有可能是cpu dai 也有可能是 codec dai  是一个数组int num_dai;//dai 的数量const struct snd_soc_component_driver *driver;//指向下属的snd_soc_component_driver, 该结构体一般由底层平台驱动实现struct list_head dai_list;//链表头, 挂接snd_soc_dai->list   list_add(&dai->list, &component->dai_list)int (*read)(struct snd_soc_component *, unsigned int, unsigned int *);int (*write)(struct snd_soc_component *, unsigned int, unsigned int);struct regmap *regmap;int val_bytes;struct mutex io_mutex;/* attached dynamic objects */struct list_head dobj_list;#ifdef CONFIG_DEBUG_FSstruct dentry *debugfs_root;
#endif/** DO NOT use any of the fields below in drivers, they are temporary and* are going to be removed again soon. If you use them in driver code the* driver will be marked as BROKEN when these fields are removed.*//* Don't use these, use snd_soc_component_get_dapm() */struct snd_soc_dapm_context dapm;const struct snd_kcontrol_new *controls;unsigned int num_controls;const struct snd_soc_dapm_widget *dapm_widgets;unsigned int num_dapm_widgets;const struct snd_soc_dapm_route *dapm_routes;unsigned int num_dapm_routes;struct snd_soc_codec *codec;int (*probe)(struct snd_soc_component *);void (*remove)(struct snd_soc_component *);#ifdef CONFIG_DEBUG_FSvoid (*init_debugfs)(struct snd_soc_component *component);const char *debugfs_prefix;
#endif
};

2.3.5 struct snd_soc_component_driver  底层驱动需要填充该结构体, 然后向ASoC核心层注册

struct snd_soc_component_driver {
//ASoC核心层用名字区分不同的snd_soc_component_driver.
//注意这个name与snd_soc_component->name不是同一个. 这里的name由驱动编写者填入, 而component->name由系统自动生成.const char *name;/* Default control and setup, added after probe() is run */const struct snd_kcontrol_new *controls;unsigned int num_controls;//dapm_widgets、dapm_routes : 与dapm相关, dapm其实是对kcontrol做了一层封装const struct snd_soc_dapm_widget *dapm_widgets;unsigned int num_dapm_widgets;const struct snd_soc_dapm_route *dapm_routes;unsigned int num_dapm_routes;int (*probe)(struct snd_soc_component *);void (*remove)(struct snd_soc_component *);/* DT */int (*of_xlate_dai_name)(struct snd_soc_component *component,struct of_phandle_args *args,const char **dai_name);void (*seq_notifier)(struct snd_soc_component *, enum snd_soc_dapm_type,int subseq);int (*stream_event)(struct snd_soc_component *, int event);/* probe ordering - for components with runtime dependencies */int probe_order;int remove_order;
};

2.3.6 struct snd_soc_codec

struct snd_soc_codec {struct device *dev;const struct snd_soc_codec_driver *driver;struct list_head list;struct list_head card_list;/* runtime */unsigned int cache_bypass:1; /* Suppress access to the cache */unsigned int suspended:1; /* Codec is in suspend PM state */unsigned int cache_init:1; /* codec cache has been initialized *//* codec IO */void *control_data; /* codec control (i2c/3wire) data */hw_write_t hw_write;void *reg_cache;/* component */struct snd_soc_component component;#ifdef CONFIG_DEBUG_FSstruct dentry *debugfs_reg;
#endif
};

总结:

梳理完,你会发现,其实跟前面的platform  cpu_dai一样都是初始化结构体snd_soc_component 和snd_soc_codec  并且把这两个结构体挂到component_list 和code_list中。以供soc_bind_dai_link绑定cpu_dai 、codec_dai 、codec、platform使用。

Linux ALSA声卡驱动之四:Codec 以及Codec_dai相关推荐

  1. Linux ALSA声卡驱动之四:machine和dai_link的作用和实现

    一.模块化管理 alsa音频驱动模块化管理,是linux驱动比较典型的代码架构,app调用snd_pcm_open.snd_pcm_writei.snd_pcm_readi等接口到alsa_lib后, ...

  2. Linux ALSA声卡驱动之四:Control设备的创建

    声明:本博内容均由http://blog.csdn.net/droidphone原创,转载请注明出处,谢谢! Control接口 Control接口主要让用户空间的应用程序(alsa-lib)可以访问 ...

  3. Linux ALSA声卡驱动之二:Platform

    ALSA声卡驱动: 1.Linux ALSA声卡驱动之一:ALSA架构简介和ASOC架构简介 2.Linux ALSA声卡驱动之二:Platform 3. Linux ALSA声卡驱动之三:Platf ...

  4. Linux ALSA声卡驱动之五:Machine 以及ALSA声卡的注册

    ALSA声卡驱动: 1.Linux ALSA声卡驱动之一:ALSA架构简介和ASOC架构简介 2.Linux ALSA声卡驱动之二:Platform 3. Linux ALSA声卡驱动之三:Platf ...

  5. Linux ALSA声卡驱动之三:Platform之Cpu_dai

    ALSA声卡驱动: 1.Linux ALSA声卡驱动之一:ALSA架构简介和ASOC架构简介 2.Linux ALSA声卡驱动之二:Platform 3. Linux ALSA声卡驱动之三:Platf ...

  6. Linux ALSA声卡驱动之七:录音(Capture) 调用流程

    ALSA声卡驱动: 1.Linux ALSA声卡驱动之一:ALSA架构简介和ASOC架构简介 2.Linux ALSA声卡驱动之二:Platform 3. Linux ALSA声卡驱动之三:Platf ...

  7. Linux ALSA声卡驱动之一:ALSA架构简介和ASOC架构简介

    ALSA声卡驱动: 1.Linux ALSA声卡驱动之一:ALSA架构简介和ASOC架构简介 2.Linux ALSA声卡驱动之二:Platform 3. Linux ALSA声卡驱动之三:Platf ...

  8. Linux ALSA声卡驱动之八:ASoC架构中的Platform

    1.  Platform驱动在ASoC中的作用 前面几章内容已经说过,ASoC被分为Machine,Platform和Codec三大部件,Platform驱动的主要作用是完成音频数据的管理,最终通过C ...

  9. linux alsa声卡驱动原理分析- 设备打开过程和数据流程,linux alsa声卡驱动原理分析解析- 设备打开过程跟数据流程资料.ppt...

    linux alsa声卡驱动原理分析解析- 设备打开过程跟数据流程资料 Linux ALSA声卡驱动原理分析 -设备打开过程和数据流程;目 录;目 录;一.导 读;目 录;二.ALSA架构简介;二. ...

最新文章

  1. Linux如何搜索文件的方法
  2. [JAVA]寻找满足和的最短子序列(Minimum Size Subarray Sum)
  3. 鸿蒙系统画饼,任正非说在三年内华为鸿蒙系统即可媲美苹果!真的不是“画饼”?...
  4. (七)Maven使用的最佳实践
  5. MySQL存储树形数据优化技笔记
  6. html5支持多线程,html5 多线程
  7. 以太坊代码标准是什么_以太坊:什么是ERC20标准?
  8. java hssfcell 单元格样式_Java使用poi进行对Excel的操作
  9. 基于孪生卷积网络(Siamese CNN)和短时约束度量联合学习的tracklet association方法
  10. 使用jQuery.form插件,实现完美的表单异步提交
  11. Zookeeper 集群的安装与部署
  12. excel教程自学网_5个口碑爆棚的自学网站,不花一分钱直接看教程
  13. 大数据行业调研报告(最新版)
  14. python pywifi 破解wifi密码
  15. Android画布放大缩小,android画板---涂鸦,缩放,旋转,贴纸实现
  16. java 控制台类_Java Console类(控制台)
  17. Kali与编程:Winserver2019上搭建wds网络部署服务器
  18. html怎么用空格占位符,HTML空格占位符
  19. Android常用热门开源库汇总(持续更新)
  20. Distilled Person Re-identification: Towards a More Scalable System

热门文章

  1. osgEarth .earth 文件详情
  2. html5绘制图形幸运大转盘,微信小程序利用canvas 绘制幸运大转盘功能
  3. webpack配置缓存
  4. 聊聊Java中的TLAB
  5. UDP的主要特点、首部格式及功能
  6. Android常用五大平台上架详解
  7. python 不能被2,3整除的数字
  8. 最新『资源分享』IT视频教程
  9. 苹果手机怎么截图,小白点截图方法
  10. 基于FPGA的MPPT系统开发