Linux驱动开发初识

news2024/11/18 3:38:19

Linux驱动开发初识

文章目录

  • Linux驱动开发初识
    • 一、驱动的概念
      • 1.1 什么是驱动:
      • 1.2 驱动的分类:
    • 二、设备的概念
      • 2.1 主设备号&次设备号:
      • 2.2 设备号的作用:
    • 三、设备驱动整体调用过程
      • 3.1 上层用户操控设备的流程:
      • 3.2 Linux驱动的运行方式:
    • 四、基于框架编写驱动代码
      • 4.1 基本字符设备驱动框架:
      • 4.2 驱动代码的编译:
      • 4.3 驱动的加载&卸载:
      • 4.4 驱动的测试:
    • 五、树莓派IO口驱动的编写
      • 5.1 BCM2835芯片手册导读:
      • 5.2 Pin4引脚定位:
      • 5.3 根据驱动框架编写树莓派Pin4引脚驱动:
      • 5.4 编译测试Pin4引脚驱动:

一、驱动的概念

1.1 什么是驱动:

Linux内核驱动:是指一段代码,这段代码可以驱动底层硬件,即驱动就是对底层硬件设备的操作进行封装,并向上层提供函数接口。

1.2 驱动的分类:

Linux驱动分为三个基础大类:字符设备驱动块设备驱动网络设备驱动

  • 字符设备(Char Device):
  1. 字符设备是个能够像字节流(类似文件)一样被访问的设备
  2. 对字符设备发出读/写请求时,实际的硬件I/O操作一般紧接着发生
  3. 字符设备驱动程序通常至少要实现openclosereadwrite系统调用
  4. 比如我们常见的lcd、触摸屏、键盘、led、串口等等,他们一般对应具体的硬件都是进行出具的采集、处理、传输
  • 块设备(Block Device)
  1. 一个块设备驱动程序主要通过传输固定大小的数据(一般为512或1k)来访问设备
  2. 块设备通过buffer cache(内存缓冲区)访问,可以随机存取,即:任何块都可以读写,不必考虑它在设备的什么地方
  3. 块设备可以通过它们的设备特殊文件访问,但是更常见的是通过文件系统进行访问
  4. 只有一个块设备可以支持一个安装的文件系统
  5. 比如我们常见的电脑硬盘、SD卡、U盘、光盘等
  • 网络设备(Net Device)
  1. 任何网络事务都经过一个网络接口形成,即一个能够和其他主机交换数据的设备
  2. 访问网络接口的方法仍然是给它们分配一个唯一的名字(比如eth0),但这个名字在文件系统中不存在对应的节点
  3. 内核和网络设备驱动程序间的通信,完全不同于内核和字符以及块驱动程序之间的通信,内核调用一套和数据包传输相关的函(socket函数)而不是readwrite
  4. 比如我们常见的网卡设备、蓝牙设备

在这里插入图片描述

二、设备的概念

  • 在学习驱动和其开发之前,首先要知道所谓驱动,其对象就是设备

2.1 主设备号&次设备号:

在Linux中,各种设备都以文件的形式存在**/dev目录下**,称为设备文件最上层的应用程序可以打开,关闭,读写这些设备文件,从而完成对设备的操作

为了管理这些设备,系统为设备编了号,每个设备都拥有主设备号次设备号主设备号用于区分不同种类的设备,而次设备号用于区分同一类型的多个设备。(对于常用的设备如硬盘,Linux赋予的主设备号一般是3)

  • 在**/dev目录下输入ls -l**,就可以看到设备文件对应的主次设备号:

在这里插入图片描述

2.2 设备号的作用:

在了解了什么是主次设备号之后,就要了解设备号的用处:

  • 用户态中:当用户调用了如open, read, write等函数想要操作设备文件时,需要两个参数,第一个是文件名,第二个就是设备号
  • 内核态中:存在着一个驱动链表,用于管理所有设备的驱动,而驱动在链表中的位置就由设备号来检索

三、设备驱动整体调用过程

3.1 上层用户操控设备的流程:

  1. C语言上层调用open函数。open(“/dev/pin4”,O_RDWR);调用/dev下的pin4以可读可写的方式打开。对于上层open调用到内核时会发生一次软中断中断号是0X80,从用户空间进入到内核空间

  2. open会调用到system_call(内核函数)system_call会根据/dev/pin4设备名,去找出需要的设备号。

  3. 再调到虚拟文件VFS ,调用VFS里的sys_opensys_open会找到在驱动链表里面,根据主设备号和次设备号找到引脚4里的open函数,引脚4里的open是对寄存器操作及对硬件的操作

在这里插入图片描述

3.2 Linux驱动的运行方式:

  1. 驱动编译进 Linux 内核中,当 Linux 内核启动的时就会自动运行驱动程序
  2. 驱动编译成模块(Linux 下模块扩展名为.ko),并在Linux 内核启动以后使用相应命令加载驱动模块

四、基于框架编写驱动代码

4.1 基本字符设备驱动框架:

#include <linux/fs.h>		            //file_operations声明
#include <linux/module.h>               //module_init  module_exit声明
#include <linux/init.h>                 //__init  __exit 宏定义声明
#include <linux/device.h>	            //class  devise声明
#include <linux/uaccess.h>              //copy_from_user 的头文件
#include <linux/types.h>                //设备号  dev_t 类型声明
#include <asm/io.h>                     //ioremap iounmap的头文件
 
 
static struct class *pin4_class;        //类对象
static struct device *pin4_class_dev;   //设备对象
 
static dev_t devno;                     //设备号
static int major =231;  		        //主设备号
static int minor =0;			        //次设备号
static char *module_name="pin4";        //模块名
 
//_open函数
static int pin4_open(struct inode *inode,struct file *file)
{
    printk("pin4_open\n");              //内核的打印函数和printf类似
     
    return 0;
}
 
//_write函数
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
    printk("pin4_write\n");             //内核的打印函数和printf类似
 
    return 0;
}
 
static struct file_operations pin4_fops = { //结构体的类型是“file_operations”,名字可以自定义
//该结构体的成员就包含实现open和write的驱动函数
//当上层用户想要open或者write这个设备时,就会最终跳转到这个驱动代码中实现的open和write操作函数
//此处只赋值了该结构体中的三个成员变量(在keil中是不能这样写的,linux中可以),这个结构体其实有很多成员,如果想要实现更多的驱动函数,可以把更多的该结构体成员赋值并在这段代码中重写
    .owner = THIS_MODULE,
    .open  = pin4_open,
    .write = pin4_write,
};
 
int __init pin4_drv_init(void)              //真实驱动入口
{
 
    int ret;
    devno = MKDEV(major,minor);             //创建设备号
    ret   = register_chrdev(major, module_name, &pin4_fops);  //注册驱动,告诉内核:把这个驱动加入到内核驱动的链表中
 
    //以下两句代码目的是“生成设备文件”,也可以通过“mknod”命令手动生成,但是一般不会这样做
    pin4_class=class_create(THIS_MODULE,"myfirstdemo"); //先创建‘类’
    pin4_class_dev =device_create(pin4_class,NULL,devno,NULL,module_name); //再创建‘设备’
 
    return 0;
}
 
void __exit pin4_drv_exit(void)
{
    device_destroy(pin4_class,devno); //先销毁‘设备’
    class_destroy(pin4_class); //在销毁‘类’
    unregister_chrdev(major, module_name);  //卸载驱动
}
 
module_init(pin4_drv_init);  //入口,内核加载驱动的时候,这个宏会被调用
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");    //linux内核遵循GPL协议

4.2 驱动代码的编译:

  • 进入Linux源码树目录下的驱动目录,因为驱动的是字符设备,所以进入的是驱动目录下的char目录。/home/shiyahao/SYSTEM/linux-rpi-4.19.y/drivers/char

在这里插入图片描述

  • 在这个路径下创建一个新的C文件:pin4driver.c,内容为我们刚刚的字符设备驱动:

在这里插入图片描述

在这里插入图片描述

  • 修改当前路径(字符设备驱动)下的Makefile,确保这个新的驱动会被编译到:

在这里插入图片描述

  • 回到linux内核源码的路径,运行以下指令尝试编译:
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules
  • 将编译好的驱动模块传到树莓派中:
scp ./drivers/char/pin4driver.ko pi@192.168.31.123:/home/pi

在这里插入图片描述

4.3 驱动的加载&卸载:

由于现在刚刚把驱动编译成了.ko的模块,所以需要运行以下指令来加载驱动模块:

sudo insmod pin4driver.ko		//加载驱动模块
sudo rmmod  pin4driver.ko 		//卸载驱动模块,此时驱动名字后不用加".ko"

运行成功后,就可以在**/dev**下看到生成的设备文件“pin4”了:

在这里插入图片描述

使用ls -l指令查看这个设备的主设备号&次设备号,和框架代码中的设置一样:

在这里插入图片描述

给pin驱动加权限:

sudo chmod 666 /dev/pin4

4.4 驱动的测试:

在树莓派下写一个测试驱动的C代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    int fd;

    fd = open("/dev/pin4", O_RDWR);                 //打开GPIO4口设备文件
    if(fd < 0){
        printf("open pin4 failed\n");
    }else{
        printf("open pin4 success\n");
    }

    write(fd, "1", 1);                              //输出高电平
    return 0;
}

执行测试程序后用dmesg 查看内核打印信息发现打印了驱动函数的信息:

在这里插入图片描述

可见内核也按照框架代码中的printk成功打印了信息!驱动测试成功!

同时,结果也再次印证了:当用户在最上层对 驱动文件 调用C库的open函数后,最后的结果还是调用最底层 驱动文件里实现的open驱动函数

五、树莓派IO口驱动的编写

​ 前面我们通过一个基本的字符设备驱动框架来测试了驱动的运行,但是在“pin4_open”和“pin4_write”这两个驱动函数的函数体里只写了一句内核打印的代码,作为一个真正的驱动文件这显然是不够的。

​ 同时,在之前就提到过,驱动位于内核态的最底层,其下方就直接是硬件,所以驱动函数的目标就是直接操控硬件,也就是直接操控寄存器。在我的pin4驱动函数中应该添加的也就是根据函数功能,操作寄存器从而实现I/O口操控的代码。

5.1 BCM2835芯片手册导读:

明确了目标后,就产生了这个问题:我怎么知道应该使用哪些寄存器,又应该怎么使用呢?

答案是:根据开发平台的芯片手册/电路图来找到具体的描述由于我是在树莓派3B上玩驱动的开发,所以我应该查阅这款树莓派的芯片,也就是BCM2835的芯片手册。

此处我只使用了芯片手册就定位了寄存器,而没有用电路图,原因是树莓派的这个芯片手册已经把用什么寄存器写的很清楚了

在BCM2835芯片手册的第六章描述了General Purpose I/O (GPIO)外设相关寄存器。这里驱动pin4引脚需要用到的寄存器有:

  1. GPIO Function Select Registers (GPFSELn) 功能选择寄存器

    在这里插入图片描述

    该寄存器共有五组,每个寄存器都有32位,以GPIO Alternate function select register 0为例,其中:
    29-0位 :每三位对于一个引脚,比如29-27对应的是GPIO Pin 9,26-24对应的是GPIO Pin 8,且这三位取不同的值代表该三位对应的引脚选择不同的功能。比如,当29-27位为000时表示GPIO Pin 9是输入功能,29-27位为001时表示GPIO Pin 9是输出的功能。

  2. GPIO Pin Output Set Registers (GPSETn) 置位寄存器

    在这里插入图片描述

    该寄存器共两组,每个寄存器都有32位,将寄存器某一位置1即将对应的引脚置1。

  3. GPIO Pin Output Clear Registers (GPCLRn) 清0寄存器

    与置位寄存器用法一至,将对应位数引脚置0。

  4. 所需寄存器的地址说明:

​ 在编写驱动程序时,IO空间的起始地址位0X3F000000,加上GPIO的偏移量0X200000,因此GPIO的物理地址是从0X3F200000开始的,而编程所需的地址是虚拟地址,需要通过MMU内存虚拟化管理将地址映射到虚拟地址上。

5.2 Pin4引脚定位:

Pin4引脚指的是BCM4号,对应WiringPi库第7号,物理引脚的7脚:

在这里插入图片描述

在这里插入图片描述

5.3 根据驱动框架编写树莓派Pin4引脚驱动:

#include <linux/fs.h>		 //file_operations声明
#include <linux/module.h>    //module_init  module_exit声明
#include <linux/init.h>      //__init  __exit 宏定义声明
#include <linux/device.h>    //class  devise声明
#include <linux/uaccess.h>   //copy_from_user 的头文件
#include <linux/types.h>     //设备号  dev_t 类型声明
#include <asm/io.h>          //ioremap iounmap的头文件


static struct class *pin4_class;  
static struct device *pin4_class_dev;

static dev_t devno;                //设备号
static int major =231;             //主设备号
static int minor =0;               //次设备号
static char *module_name="pin4";   //模块名


//首先定义所要用的寄存器,为了防止地址被编译器优化需要用到volatile关键字
volatile unsigned int *GPFSEL0 = NULL;
volatile unsigned int *GPSET0 = NULL;
volatile unsigned int *GPCLR0 = NULL;

static int pin4_open(struct inode *inode,struct file *file)
{
    printk("pin4_open\n");  //内核的打印函数和printf类似

    //配置引脚4的寄存器,将其配置为输出模式,即将GPFSEL0寄存器的第14-12位配置成001
    *GPFSEL0 &= 0XFFFF9FFF;  //将第14,13位置0
    *GPFSEL0 |= 0X00001000; //将第12位置1
    return 0;
}


static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
    int usercmd;
    printk("pin4_write\n");
    copy_from_user(&usercmd,buf,count);//获取应用层write函数写入的内容
    if(usercmd == 1){
        printk("set 1\n");
        *GPSET0 |=(0x1 << 4); //将Pin4引脚置1
    }else if (usercmd == 0)
    {
        printk("set 0\n");
        *GPCLR0 |=(0X1 << 4);//将Pin4引脚置0
    }else{
        printk("undo\n");
    }
    return 0;
}

static struct file_operations pin4_fops = {
    .owner = THIS_MODULE,
    .open  = pin4_open,//当应用层调用open函数时,内核会调用pin4_open.
    .write = pin4_write,//当应用层调用write函数时,内核会调用pin4_write.
};

int __init pin4_drv_init(void)  //真实的驱动入口
{

    int ret;
    devno = MKDEV(major,minor);  //创建设备号
    ret   = register_chrdev(major, module_name,&pin4_fops);  //注册驱动  告诉内核,把这个驱动加入到内核驱动的链表中

    pin4_class=class_create(THIS_MODULE,"myfirstdemo");//由代码在dev下自动生成设备
    pin4_class_dev =device_create(pin4_class,NULL,devno,NULL,module_name);  //创建设备文件

    GPFSEL0 = (volatile unsigned int *)ioremap(0X3f200000,4);//需要将物理地址映射位虚拟地址 ipremap第一个参数需要被映射的物理地址。第二个参数位映射的字节数
    GPSET0  = (volatile unsigned int *)ioremap(0X3f20001C,4);//通过芯片手册可以看到该寄存器在基础地址上偏移了1C
    GPCLR0  = (volatile unsigned int *)ioremap(0X3f200028,4);//通过芯片手册可以看到该寄存器在基础地址上偏移了28
    return 0;
}

void __exit pin4_drv_exit(void)
{

    iounmap(GPFSEL0);
    iounmap(GPSET0);
    iounmap(GPCLR0);
    device_destroy(pin4_class,devno);
    class_destroy(pin4_class);
    unregister_chrdev(major, module_name);  //卸载驱动
}

module_init(pin4_drv_init);  //入口
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");

然后在树莓派上编写测试代码:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char const *argv[])
{
        int fd,cmd;
        fd = open("/dev/pin4",O_RDWR);
        printf("input 0 ro 1 , 0 :Pin4 Set 0,1:Pin4 Set 1\n");
        scanf("%d",&cmd);

        printf("cmd = %d \n",cmd);
        write(fd,&cmd,1);
        return 0;
}

5.4 编译测试Pin4引脚驱动:

  • 将驱动代码编译后生成驱动模块放置在树莓派上进行测试:
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules

在这里插入图片描述

  • 将生成的驱动模块拷贝至树莓派:
scp ./drivers/char/pin4driver.ko pi@192.168.31.123:/home/pi
  • 在树莓派上安装驱动并给驱动权限:
sudo insmod pin4driver.ko
sudo chmod 666 /dev/pin4

运行测试程序:
在这里插入图片描述

在这里插入图片描述

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

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

相关文章

conda 虚拟环境安装GDAL

一. 背景 换了新电脑&#xff0c;要重新安装GDAL。从前是下了GDAL的.wheel文件用pip安装&#xff0c;但平时下轮子的网站现在都打不开&#xff0c;比如https://www.lfd.uci.edu/~gohlke/pythonlibs/#gdal&#xff0c;不晓得为什么。 后面看了这篇教程解决了问题&#xff08;h…

一文说透RTMP、RTSP、RTP、HLS、MPEG-DASH

实时视频传输协议 1. RTMP&#xff08;Real Time Messaging Protocol&#xff09; 简介&#xff1a;RTMP是由Adobe公司开发的实时消息传输协议&#xff0c;主要用于流媒体数据的传输。它基于TCP传输&#xff0c;具有低延迟、高可靠性的特点。特点&#xff1a;RTMP支持多种视频…

gtk安装和测试

linux的图形库有qt和gtk, qt功能强大&#xff0c;但是商用要付费。 gtk是个免费开源的&#xff0c;也不错。现在linux的类似windows办公环境基本是gtk编写的吧&#xff08;我猜&#xff09;。linux下的wps好像是用qt写的。 闲言少序&#xff0c;开始gtk. 先安装&#xff1a; …

北京未来感知科技一文概括(全):UWB定位技术

一、UWB定位技术带给人类社会的进步性#uwb定位# UWB&#xff08;超宽带&#xff09;定位技术作为一项前沿的无线通信技术&#xff0c;以其高精度、实时性、安全性和广泛的应用前景为人类社会的进步带来了显著的影响。随着技术的不断发展和创新应用的不断涌现&#xff0c;UWB定位…

双向链表的基本结构及功能实现

1.基本结构: 双向链表是一种链表数据结构&#xff0c;它由一系列节点组成&#xff0c;每个节点包含三个部分&#xff1a; (1).数据域&#xff1a;存储节点的数据 (2).前驱指针:指向前一个节点 (3).后驱指针:指向下一个节点 2.基本特性&#xff1a; 双向链接: 与单向链表…

全球化文创科技产业集团「得力集团」携手企企通,采购数字化项目一阶段上线!

近日&#xff0c;国内最大的办公与学习用品产业集团、多工作场景整体解决方案的领导者「得力集团有限公司」&#xff08;以下简称“得力集团”&#xff09;携手企企通打造的SRM项目一阶段成功上线。 基于该平台&#xff0c;得力集团在供应链采购业务方面实现全流程、全品类、全…

ELK-03-skywalking监控linux系统

文章目录 前言一、下载node_exporter二、启动node_exporter三、下载OpenTelemetry Collector四、启动OpenTelemetry Collector4.1 将配置文件下载到同级目录4.2 启动 五、查看总结 前言 skywalking安装完成后&#xff0c;开始我们的第一个监控-监控linux系统。 参考官方文档&a…

构造者模式多种实现方式

构造者模式 ​ 构造者模式建议将对象构造代码从产品类中抽取出来&#xff0c; 并将其放在一个名为构造者的独立对象中 ​ 构建者模式也是用来创建对象&#xff0c;但是相对于工厂模式来说&#xff0c;建造者模式适用于构建复杂对象&#xff0c;而工厂模式适用于创建对象的封装…

前端框架的对比和选择

在当今的前端开发领域&#xff0c;有多种流行的前端框架可供选择&#xff0c;如 Vue、React 和 Angular。以下是这些框架的对比以及 Vue 的优势&#xff1a; 一、React 特点&#xff1a; 声明式编程&#xff1a;使用 JSX 语法&#xff0c;使得组件的结构和行为更加清晰。虚拟…

力扣P1706全排列问题 很好的引入暴力 递归 回溯 dfs

代码思路是受一个洛谷题解里面大佬的启发。应该算是一个dfs和回溯的入门题目&#xff0c;很好的入门题目了下面我会先给我原题解思路我想可以很快了解这个思路。下面是我自己根据力扣大佬写的。 我会进行详细讲解并配上图辅助理解大家请往下看 #include<iostream> #inc…

CentOS 安装 JAVA环境(JDK 1.8)

镜像选择 推荐国内镜像直接下载 清华镜像 https://mirrors.tuna.tsinghua.edu.cn/Adoptium 关于重命名 AdoptOpenJDK 镜像为 Adoptium 的通知 编程宝库 http://www.codebaoku.com/jdk/jdk-index.html 这个镜像站&#xff0c;包含Oracle JDK、OpenJDK、AdoptOpenJDK、阿里…

Lesson1 机器学习_基础介绍(环境配置、入门知识)

Anaconda说明 学习Python的时候&#xff0c;使用者需要耗费时间下载对应的库&#xff08;理解为Python的武器&#xff09;......一家数据公司想让Python的使用更加便利&#xff0c;于是anaconda诞生了anaconda相当于一辆运输车&#xff0c;不仅包含了Python&#xff0c;还附有P…

【Python】数据可视化之点线图

目录 散点图 气泡图 时序图 关系图 散点图 Scatterplot&#xff08;散点图&#xff09;是一种用于展示两个变量之间关系的图表类型。在散点图中&#xff0c;每个观测值&#xff08;或数据点&#xff09;都被表示为一个点&#xff0c;其中横轴&#xff08;X轴&#xff09;代…

MODELS 2024震撼续章:科技与可持续性的未来交响曲

MODELS 2024国际会议正如火如荼地进行着&#xff0c;每一天都充满了新的发现与启迪&#xff0c;每一场分享都是对技术前沿的一次深刻探索&#xff0c;更是对现实世界可持续性挑战的一次积极回应。现在让我们继续这场科技盛宴&#xff0c;看看小编为您精选几场的学术分享吧~ 会议…

【UE5】将2D切片图渲染为体积纹理,最终实现使用RT实时绘制体积纹理【第三篇-着色器光照】

在前两篇文章中&#xff0c;我们分别拆解描述了实现原理&#xff0c;并进行了基础的着色器制作。在这一篇文章中&#xff0c;我们将为它实现光照效果 简单的概述 当光线射入体积时&#xff0c;随着光线射入距离的增加&#xff0c;体积中的介质会对光线产生反射和吸收作用&…

springboot 接口接收及响应xml数据

1.实体类 import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement;XmlRootElement XmlAccessorType(XmlAccessType.FIELD) // …

gitlab添加CI自动测试

gitlab 配置 yml 语法 ** https://docs.gitlab.com/17.3/ee/ci/yaml/index.html ** 添加.gitlab-ci.yml文件并配置 &#xff1a; ## 定义几个阶段 stages: # List of stages for jobs, and their order of execution- build- test- deploy- ## 示例job &#xff0…

Linux 简易shell编写

shell shell是壳&#xff0c;外壳的意思&#xff0c;一般我们使用linux系统有用图形化界面的也有使用命令行界面的&#xff0c;这两个都是一种shell&#xff0c;以命令行为例&#xff1a; 如图这个就是我这里的命令行格式&#xff0c;在$符后面写的就是执行的指令&#xff0c;…

携手SelectDB,观测云实现性能与成本的双重飞跃

在刚刚落下帷幕的2024云栖大会上&#xff0c;观测云又一次迎来了全面革新。携手SelectDB&#xff0c;实现了技术的飞跃&#xff0c;这不仅彰显了观测云在监控观测领域的技术实力&#xff0c;也预示着我们可以为全球用户提供更加高效、稳定的数据监测与分析服务。这一技术升级&a…

同等学力英语用什么app背单词

同等学力申硕的意义和作用 授予同等学力人员硕士学位是国家为同等学力人员开辟的获得学位的渠道&#xff0c;对于在职人员业务素质的提高和干部队伍建设起到积极作用。它为那些没有传统学历背景但具有相应学术水平的人员提供了获取学位的机会&#xff0c;有助于提升他们的职业竞…