NXP i.MX8系列平台开发讲解 - 3.13 Linux 之Audio子系统(二)

专栏文章目录传送门:返回专栏目录


目录

1. Linux ALSA 内核框架

2. Linux ALSA 代码分析

2.1 声卡驱动初始化

2.2 声卡创建注册

2.3 PCM设备创建

3. ALSA ASoC

3.1 Machine

3.2 Platform

3.3 Codec


上一章节,对于Linux Audio子系统有了大概的了解,对音频的基础知识,Audio 子系统的介绍,ALSA的框架库相关知识。本章节将讲解ALSA驱动的实现原理,在应用上一些开发相关。

1. Linux ALSA 内核框架

ALSA作为音频子系统主要一个部分,从上一章节,了解主要有两个大部分ASoC Driver(纠正上一章节图片)和ALSA Soc

图1.1 Linux ALSA 内核框架

ASoc Driver:

包含三大Machine Driver、Platform Driver、Codec Driver

  • Machine Driver

    Machine驱动负责Platform和Codec之间的耦合以及部分和设备或板子特定的代码,Machine Driver 在 ASoC 框架中扮演了平台和 Codec 之间的沟通桥梁,使得硬件设备和音频处理能够协同工作,并在不同设备之间实现通用的音频处理流程,提高了驱动的可移植性和适应性。

    图1.2 Asoc内三大部分

    如图1.2 Machine就是充当对Platform与Codec的一个媒介,只要这两个连接起来,就会组成一个声卡设备。通过Machine 驱动开始,对于声卡的注册,绑定平台和Codec驱动等等;图中可以看出音频数据通道,可以从Codec搬运至Platform,也可以反过来,中间的那条线可以是I2S,PCM总线。

  • Platform Driver

    Platform 驱动程序类可以分为音频DMA驱动,SoC DAI 驱动程序和 DSP 驱动程序,比较常用的是前面两个DMA驱动,SoC DAI驱动。

    音频DMA : 主要负责管理数据在内存和音频硬件之间的传输的部分。它确保音频数据能够高效地从内存传输到音频硬件,以及从音频硬件传输回内存。

    Soc DAI:负责Soc的音频接口与数字音频接口的连接,确保数字音频从CPU dai流向硬件接口;

  • Codec Driver

    Codec Driver意思就是音频编解码器,可以用于去解码音频或者编码音频。对于解码来说,就是讲音频数据进行解码,在Codec后经过DAC转换成模拟信号就可以通过外部设备进行播放;对于编码,模拟信号输入后通过ADC转换得到数字音频,通过Codec进行编码,用于存储或者传输。

    Codec Driver功能还包括对于音频链路的控制,对音频信号做出相应的控制,比如放大音量等;

ALSA Core

ALSA 核心层,作为ALSA最重要部分,在 ALSA Core ,指的是 Advanced Linux Sound Architecture 核心模块,它包含多个子模块,包括 PCM、MIDI、Control 和 Sequence。这些子模块一起构成了 ALSA Core,提供了在 Linux 系统中进行音频数据处理、传输和控制的基础设施。

  • PCM: 通过 PCM 子模块,声音数据可以被捕获、处理和输出;

  • MIDI:通过 MIDI 子模块,音乐合成器可以接收和响应 MIDI 命令;

  • Control :通过 Control 子模块,用户可以调整音频设备的参数;

  • Sequence :通过 Sequence 子模块,可以管理和处理音频事件序列。这些功能共同构成了 ALSA 的核心能力,使得在 Linux 系统中实现高质量的音频处理成为可能

2. Linux ALSA 代码分析

ALSA 代码目录结构,内核版本5.4:

./sound/
├── ac97
├── ac97_bus.c
├── aoa
├── arm
├── atmel
├── core
├── drivers
├── firewire
├── hda
├── i2c
├── isa
├── Kconfig
├── last.c
├── Makefile
├── mips
├── oss
├── parisc
├── pci
├── pcmcia
├── ppc
├── sh
├── soc
├── sound_core.c
├── sparc
├── spi
├── synth
├── usb
├── x86
└── xen

目录/文件名作用和内容
ac97AC'97 音频编解码器的驱动和支持
aoaApple Onboard Audio 驱动
arm针对 ARM 架构的声音驱动
atmel用于 Atmel 音频硬件的驱动和支持
core声音子系统的核心代码,提供基本的声音处理功能
drivers各种音频硬件设备的驱动程序存放位置,如 ALSA 的主要驱动程序
firewire用于 FireWire 音频设备的支持
hdaHigh Definition Audio 驱动
i2c用于 I2C 总线上音频设备的驱动和支持
isa适用于 ISA 总线上音频设备的支持
Kconfig声音子系统的配置文件
mips针对 MIPS 架构的声音驱动
ossOpen Sound System(OSS)的兼容性驱动
parisc适用于 PA-RISC 架构的声音驱动
pci用于 PCI 总线上音频设备的驱动和支持
pcmcia用于 PCMCIA 总线上音频设备的驱动和支持
ppc适用于 PowerPC 架构的声音驱动
sh针对 SuperH 架构的声音驱动
soc针对 SoC 架构的声音驱动,包括 ASoC 子系统
sparc适用于 SPARC 架构的声音驱动
spi用于 SPI 总线上音频设备的驱动和支持
synth音频合成器的驱动和支持
usb用于 USB 总线上音频设备的驱动和支持
x86针对 x86 架构的声音驱动
xen用于虚拟化环境中的声音支持

重要的目录在于Core文件夹,实现了声音子系统的核心代码。

2.1 声卡驱动初始化

入口从ALSA 初始化开始:

vim ./sound/core/sound.c

static int __init ALSA_sound_init(void)
{snd_major = major;snd_ecards_limit = cards_limit;//注册字符设备驱动,将主设备号和字符设备的操作函数关联起来//主设备号为 116 :#define CONFIG_SND_MAJOR        116     /* standard configuration */ if (register_chrdev(major, "ALSA", &snd_fops)) {pr_err("ALSA core: unable to register native major device number %d\n", major);return -EIO;}//创建snd_proc_root ,创建相关的文件系统/proc/sound if (snd_info_init() < 0) {unregister_chrdev(major, "ALSA");return -ENOMEM;}
#ifndef MODULEpr_info("Advanced Linux Sound Architecture Driver Initialized.\n");
#endifreturn 0;
}

ALSA_sound_init主要注册字符设备驱动,将相关信息写入,并且在Linux 系统上目录/proc创建一些虚拟文件

int __init snd_info_init(void)
{snd_proc_root = snd_info_create_entry("asound", NULL, THIS_MODULE);if (!snd_proc_root)return -ENOMEM;snd_proc_root->mode = S_IFDIR | 0555;snd_proc_root->p = proc_mkdir("asound", NULL);if (!snd_proc_root->p)goto error;
#ifdef CONFIG_SND_OSSEMULsnd_oss_root = create_subdir(THIS_MODULE, "oss");if (!snd_oss_root)goto error;
#endif
#if IS_ENABLED(CONFIG_SND_SEQUENCER)snd_seq_root = create_subdir(THIS_MODULE, "seq");if (!snd_seq_root)goto error;
#endifif (snd_info_version_init() < 0 ||snd_minor_info_init() < 0 ||snd_minor_info_oss_init() < 0 ||snd_card_info_init() < 0 ||snd_info_minor_register() < 0)goto error;return 0;error:snd_info_free_entry(snd_proc_root);return -ENOMEM;
}

对于字符设备驱动中有一个文件操作接口file_operations

static const struct file_operations snd_fops =
{.owner =        THIS_MODULE,.open =         snd_open,.llseek =       noop_llseek,
}

snd_open接口函数,接口通过传参的inode参数进行获取设备的次设备号,然后查找对应的snd_minior结构体,如果没有加载就去初始化它,然后进行操作函数替换为行的操作函数表,最后打开设备。snd_open函数这里不展现出来,重点查看下snd_minior结构体。

struct snd_minor {int type;                       /* SNDRV_DEVICE_TYPE_XXX */int card;                       /* card number */int device;                     /* device number */const struct file_operations *f_ops;    /* file operations */void *private_data;             /* private data for f_ops->open */struct device *dev;             /* device for sysfs */struct snd_card *card_ptr;      /* assigned card instance */ANDROID_KABI_RESERVE(1);
};

snd_minor结构体作用在于代表音频设备的次设备,每个此设备都有独特标识,声卡编号,设备编号,文件操作函数等等,这样对于一个设备包含多个音频设备就更加方便的管理。

创建设备类文件:

调用了 init_oss_soundcore() 函数来初始化 OSS(Open Sound System)相关的音频核心模块。然后,它创建了一个名为 "sound" 的设备类(sound_class),这个类将用于管理音频设备的设备节点。

static int __init init_soundcore(void)
{int rc;rc = init_oss_soundcore();if (rc)return rc;sound_class = class_create(THIS_MODULE, "sound");if (IS_ERR(sound_class)) {cleanup_oss_soundcore();return PTR_ERR(sound_class);}sound_class->devnode = sound_devnode;return 0;
}

查看设备下class 相关设备节点

这些设备节点包括 card0card1card2 等等,每个 cardX 目录下都包含了与该声卡相关的信息和控制接口。其中的 pcmCXDYZ 节点表示音频设备的不同 PCM 子设备,例如 pcmC0D0c 表示第一张声卡的第一个 PCM 播放设备。有了这些设备节点对于用户通过这些节点去获取音频的状态,配置参数,进行音频输入输出等操作,更重要的是统一了音频操作的方式。

2.2 声卡创建注册

声卡的创建主要通过./sound/core/init.c 中的snd_card_new接口函数,主要使用声卡的设备结构体snd_card

int snd_card_new(struct device *parent, int idx, const char *xid,struct module *module, int extra_size,struct snd_card **card_ret)
{struct snd_card *card;int err;if (snd_BUG_ON(!card_ret))return -EINVAL;*card_ret = NULL;if (extra_size < 0)extra_size = 0;card = kzalloc(sizeof(*card) + extra_size, GFP_KERNEL);if (!card)return -ENOMEM;if (extra_size > 0)card->private_data = (char *)card + sizeof(struct snd_card);if (xid)strlcpy(card->id, xid, sizeof(card->id));err = 0;mutex_lock(&snd_card_mutex);if (idx < 0) /* first check the matching module-name slot */idx = get_slot_from_bitmask(idx, module_slot_match, module);if (idx < 0) /* if not matched, assign an empty slot */idx = get_slot_from_bitmask(idx, check_empty_slot, module);if (idx < 0)err = -ENODEV;else if (idx < snd_ecards_limit) {if (test_bit(idx, snd_cards_lock))err = -EBUSY;   /* invalid */} else if (idx >= SNDRV_CARDS)err = -ENODEV;if (err < 0) {mutex_unlock(&snd_card_mutex);dev_err(parent, "cannot find the slot for index %d (range 0-%i), error: %d\n",idx, snd_ecards_limit - 1, err);kfree(card);return err;}set_bit(idx, snd_cards_lock);           /* lock it */if (idx >= snd_ecards_limit)snd_ecards_limit = idx + 1; /* increase the limit */mutex_unlock(&snd_card_mutex);card->dev = parent;card->number = idx;
#ifdef MODULEWARN_ON(!module);card->module = module;
#endifINIT_LIST_HEAD(&card->devices);init_rwsem(&card->controls_rwsem);rwlock_init(&card->ctl_files_rwlock);INIT_LIST_HEAD(&card->controls);INIT_LIST_HEAD(&card->ctl_files);spin_lock_init(&card->files_lock);INIT_LIST_HEAD(&card->files_list);mutex_init(&card->memory_mutex);
#ifdef CONFIG_PMinit_waitqueue_head(&card->power_sleep);
#endifinit_waitqueue_head(&card->remove_sleep);card->sync_irq = -1;device_initialize(&card->card_dev);card->card_dev.parent = parent;card->card_dev.class = sound_class;card->card_dev.release = release_card_device;card->card_dev.groups = card->dev_groups;card->dev_groups[0] = &card_dev_attr_group;err = kobject_set_name(&card->card_dev.kobj, "card%d", idx);if (err < 0)goto __error;snprintf(card->irq_descr, sizeof(card->irq_descr), "%s:%s",dev_driver_string(card->dev), dev_name(&card->card_dev));/* the control interface cannot be accessed from the user space until *//* snd_cards_bitmask and snd_cards are set with snd_card_register */err = snd_ctl_create(card);if (err < 0) {dev_err(parent, "unable to register control minors\n");goto __error;}err = snd_info_card_create(card);if (err < 0) {dev_err(parent, "unable to create card info\n");goto __error_ctl;et*card_ret = card;return 0;__error_ctl:snd_device_free_all(card);__error:put_device(&card->card_dev);return err;
}

根据代码来看,主要做的流程就是:

  • 分配内存:根据声卡对象的大小进行分配。

  • 设置声卡的参数:设置声卡的ID,名称以及其他参数。

  • 创建声卡接口:通过snd_ctl_create函数创建声卡的控制接口,使其可以被用户空间访问。

  • 创建声卡信息接口:通过snd_info_card_create函数创建声卡的信息接口,用于提供声卡的相关信息。

创建声卡接口函数最终通过函数snd_card_register()该函数将声卡的设备信息,控制注册,放入到声卡组中形成一个list.

2.3 PCM设备创建

3. ALSA ASoC

从ALSA 内核框架查看到三个部分,这里将从代码简要分析下工作过程。

3.1 Machine

Machine主要负责Platform和Codec之间的耦合,更加使用更多的设备,起到一个连接作用。具体代码的实现:

vim sound/soc/soc-core.c

/* ASoC platform driver */
static struct platform_driver soc_driver = {.driver         = {.name           = "soc-audio",.pm             = &snd_soc_pm_ops,},.probe          = soc_probe,
};

这里看到platform_driver中name是soc-audio, 与平台中的platform_device一样将触发soc_probe调用:

/* probes a new socdev */
static int soc_probe(struct platform_device *pdev)
{struct snd_soc_card *card = platform_get_drvdata(pdev);/** no card, so machine driver should be registering card* we should not be here in that case so ret error*/if (!card)return -EINVAL;dev_warn(&pdev->dev,"ASoC: machine %s should use snd_soc_register_card()\n",card->name);/* Bodge while we unpick instantiation */card->dev = &pdev->dev;return devm_snd_soc_register_card(&pdev->dev, card);
}

主要soc_probe调用了devm_snd_soc_register_card,在这个函数中将做了很多工作,查看里面最关键部分代码

int snd_soc_register_card(struct snd_soc_card *card)
{if (!card->name || !card->dev)return -EINVAL;dev_set_drvdata(card->dev, card);INIT_LIST_HEAD(&card->widgets);INIT_LIST_HEAD(&card->paths);INIT_LIST_HEAD(&card->dapm_list);INIT_LIST_HEAD(&card->aux_comp_list);INIT_LIST_HEAD(&card->component_dev_list);INIT_LIST_HEAD(&card->list);INIT_LIST_HEAD(&card->rtd_list);INIT_LIST_HEAD(&card->dapm_dirty);INIT_LIST_HEAD(&card->dobj_list);card->instantiated = 0;mutex_init(&card->mutex);mutex_init(&card->dapm_mutex);mutex_init(&card->pcm_mutex);spin_lock_init(&card->dpcm_lock);return snd_soc_bind_card(card);
}

在函数snd_soc_bind_cardsoc_probe_link_dais调用调用了codec,dai和platform驱动的probe函数。

3.2 Platform

Platform 部分主要完成音频数据的管理,通过CPU的柱子接口DAI传输到Codec。在代码里面Platform改成了component说法,所以接口有所变化:

定义一个接口体snd_soc_component_driver 这个结构体内容比较多,在注册一个Platform时候需要定义一个这样的结构实例。

主要是注册了一个dai,还有一个DMA.

3.3 Codec

Codec 部分完成音频信号的转换,对音频进行控制;主要涉及的相关结构体

snd_soc_component snd_soc_component_driver snd_soc_dai_driver snd_soc_dai_driver snd_soc_dai

这些结构体都是创建后都是需要让Machine能够使用该Codec,所以对于不同Codec都需要遵从这些结构体的定义,去填充,将具体的操作函数以及数据进行赋值。注册也是在Codec代码中开始注册,注册后Machine就可以去使用。

以i.MX8MQ 中的wm8524音频芯片为例子:

wm8524采用I2C总线控制,

wm8524驱动代码:./sound/soc/codecs/wm8524.c

static int wm8524_codec_probe(struct platform_device *pdev)
{struct wm8524_priv *wm8524;int ret;wm8524 = devm_kzalloc(&pdev->dev, sizeof(struct wm8524_priv),GFP_KERNEL);if (wm8524 == NULL)return -ENOMEM;platform_set_drvdata(pdev, wm8524);wm8524->mute = devm_gpiod_get(&pdev->dev, "wlf,mute", GPIOD_OUT_LOW);if (IS_ERR(wm8524->mute)) {ret = PTR_ERR(wm8524->mute);if (ret != -EPROBE_DEFER)dev_err(&pdev->dev, "Failed to get mute line: %d\n", ret);return ret;}ret = devm_snd_soc_register_component(&pdev->dev,&soc_component_dev_wm8524, &wm8524_dai, 1);if (ret < 0)dev_err(&pdev->dev, "Failed to register component: %d\n", ret);return ret;
}

关键在于devm_snd_soc_register_component这个函数,里面将会调用snd_soc_register_dais注册多个DAI设备;

static int snd_soc_register_dais(struct snd_soc_component *component,struct snd_soc_dai_driver *dai_drv,size_t count)
  • dev: 传递设备节点,通常是相关的平台设备节点。

  • dai_drvs: 指向一个snd_soc_dai_driver数组,数组中的每个元素表示一个DAI设备的驱动信息。

  • count: 指定dai_drvs数组中的元素个数。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/17646.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LabVIEW通过以太网控制PLC程序开发

在使用LabVIEW通过以太网控制PLC程序开发时&#xff0c;需要综合考虑硬件、软件和通信协议的协调工作。以下是详细步骤、注意事项、重点和难点分析&#xff0c;以及几种实现方式及其特点的概述。 实现步骤 确定硬件和软件环境&#xff1a; 确定PLC型号和品牌&#xff08;如西门…

WebGL技术在工程控制中的应用

WebGL技术在工程控制中的应用非常广泛&#xff0c;它利用了浏览器内的强大图形处理能力&#xff0c;可以为工程领域提供实时的三维可视化和交互功能。以下是WebGL在工程控制中的几个主要应用领域及其具体应用。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&…

【Qt 学习笔记】Qt窗口 | Qt窗口介绍 | QMainwindow类及各组件介绍

博客主页&#xff1a;Duck Bro 博客主页系列专栏&#xff1a;Qt 专栏关注博主&#xff0c;后期持续更新系列文章如果有错误感谢请大家批评指出&#xff0c;及时修改感谢大家点赞&#x1f44d;收藏⭐评论✍ Qt窗口 | Qt窗口介绍 | QMainwindow类及各组件介绍 文章编号&#xff…

力扣63 不同路径Ⅱ Java版本

文章目录 题目描述代码 题目描述 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角&#xff08;在下图中标记为 “Finish”&#xff09;。 现在考虑网格…

粤嵌—2024/5/28—最大正方形(✔)

代码实现&#xff1a; 方法一&#xff1a;模拟——超时 int maximalSquare(char **matrix, int matrixSize, int *matrixColSize) {int maxSide 0;if (matrix NULL || matrixColSize NULL || matrixSize < 0 || matrixColSize[0] < 0) {return 0;}for (int i 0; i &l…

【每日一坑】KiCAD 覆铜区域约束

【每日一坑】 1.螺丝孔周围不想要要铜皮&#xff1b; 2、首先在CTRLshiftK;画一个区域&#xff0c;比如铺一个GND; 3、选择CUTOUT; 4、画线&#xff0c;画好闭合图形&#xff1b;如下图 5、就是这样了&#xff0c;就是还没有画圆或者异形的&#xff1b;

linux安装Java报错cannot execute binary file: Exec format error

uname -a 查看自己jdk版本 下载JDK路径Java Archive Downloads - Java SE 17 下载对应的版本jdk jdk-17.0.10_linux-aarch64_bin.tar.gz jdk-17.0.10_linux-x64_bin.tar.gz

吴恩达深度学习笔记:超 参 数 调 试 、 Batch 正 则 化 和 程 序 框 架(Hyperparameter tuning)3.8-3.9

目录 第二门课: 改善深层神经网络&#xff1a;超参数调试、正 则 化 以 及 优 化 (Improving Deep Neural Networks:Hyperparameter tuning, Regularization and Optimization)第三周&#xff1a; 超 参 数 调 试 、 Batch 正 则 化 和 程 序 框 架&#xff08;Hyperparameter …

【Python】 删除列表元素的简单方法

基本原理 在Python中&#xff0c;列表&#xff08;list&#xff09;是一种非常灵活的数据结构&#xff0c;它允许我们存储一系列的元素。有时&#xff0c;我们需要根据元素的值来删除列表中的特定元素。虽然Python没有内置的函数直接通过值来删除列表中的元素&#xff0c;但我…

01 一文理解,Prometheus详细介绍

01 一文理解&#xff0c;Prometheus详细介绍 介绍 大家好&#xff0c;我是秋意零。 Prometheus 是一个开源的系统监控和报警工具包&#xff0c;最初由SoundCloud开发&#xff0c;并在2012年作为开源项目发布。Prometheus 目前由Cloud Native Computing Foundation&#xff08…

Qt发生“无法消除重载函数歧义的问题”

当使用Qt5的信号与槽语法时&#xff0c;如果一个槽或信号被重载了&#xff08;即同一个函数名有不同的参数列表&#xff09;&#xff0c;编译器可能会因为不清楚应该连接哪个函数版本而报错。 为了解决这个问题&#xff0c;可以通过提供静态转型 (static_cast) 或者提供函数指针…

常见的螺纹防松措施有哪些?——SunTorque智能扭矩系统

智能扭矩系统-智能拧紧系统-扭矩自动控制系统-SunTorque 螺纹连接作为机械工程中常见的连接方式&#xff0c;其稳定性和可靠性对于整个机械系统的正常运行至关重要。然而&#xff0c;由于振动、冲击、温度变化等因素的影响&#xff0c;螺纹连接往往会出现松动现象&#xff0c;…

【Mac】Ulysses for Mac(优秀的markdown写作软件) v34.3中文版安装教程

软件介绍 哪款markdown写作软件最好用&#xff1f;小编推荐您使用尤利西斯&#xff1a;Ulysses mac版&#xff01;这是mac上一款优秀的markdown写作工具。Ulysses mac版具备全新的Soulmen写作坏境&#xff0c;采用了革命性的功能增强&#xff0c;结合了最好的部分最小标记&…

DES加密算法笔记

【DES加密算法&#xff5c;密码学&#xff5c;信息安全】https://www.bilibili.com/video/BV1KQ4y127AT?vd_source7ad69e0c2be65c96d9584e19b0202113 根据此视频学习 DES是对称密码中的分组加密算法 (分组加密对应流加密算法) 流加密算法就是一个字节一个字节加密 分组加…

Day38 代码随想录打卡|二叉树篇---二叉树的最大深度

题目&#xff08;leecode T104&#xff09;&#xff1a; 给定一个二叉树 root &#xff0c;返回其最大深度。 二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。 递归法&#xff1a; 递归法首先将根节点作为参数传入&#xff0c;随后每一轮传入当前节点的…

20道经典自动化测试面试题

概述 觉得自动化测试很难&#xff1f; 是的&#xff0c;它确实不简单。但是学会它&#xff0c;工资高啊&#xff01; 担心面试的时候被问到自动化测试&#xff1f; 嗯&#xff0c;你担心的没错&#xff01;确实会被经常问到&#xff01; 现在应聘软件测试工程师的岗位&…

Apache-Doris单机部署

参考&#xff1a; 快速体验 Apache Doris - Apache Doris 1、Apache Doris是一款 基于MPP架构的高性能、实时的分析型数据库&#xff0c;以高效、简单、统一的特点被人们所熟知&#xff0c;仅需 亚秒级响应时间即可返回海量数据下的查询结果&#xff0c;不仅可以支持高并发的点…

C#开发上位机应用:基础与实践

C#是一种流行的面向对象编程语言&#xff0c;常用于Windows应用程序的开发。上位机应用是一种用于监控和控制设备或系统的应用程序&#xff0c;通常与下位机&#xff08;如传感器、执行器等&#xff09;进行通信。在本文中&#xff0c;我们将介绍C#开发上位机应用的基础知识和实…

适用于Android的最佳数据恢复软件

如果您的 Android 设备崩溃&#xff0c;您需要找到一种方法来取回您的数据。幸运的是&#xff0c;有许多数据恢复程序可以帮助您恢复丢失的文件。有些是免费的&#xff0c;而另一些则需要付费。这是适用于Android设备的最佳数据恢复软件列表。 什么是数据恢复软件&#xff1f; …

设计模式8——原型模式

写文章的初心主要是用来帮助自己快速的回忆这个模式该怎么用&#xff0c;主要是下面的UML图可以起到大作用&#xff0c;在你学习过一遍以后可能会遗忘&#xff0c;忘记了不要紧&#xff0c;只要看一眼UML图就能想起来了。同时也请大家多多指教。 原型模式&#xff08;Prototyp…