4. 字符设备驱动高级--- 下篇

news2025/1/14 1:25:23

文章目录

  • 一、字符设备驱动高级
    • 1.1 注册字符设备驱动新接口
      • 1.1.1 新接口与旧接口
      • 1.1.2 cdev介绍
      • 1.1.3 设备号
      • 1.1.4 编程实践
      • 1.1.5 alloc_chrdev_region自动分配设备号
      • 1.1.6 中途出错的倒影式错误处理方法
  • 二、字符设备驱动注册代码分析
    • 2.1 旧接口register_chrdev
    • 2.2 新接口register_chrdev_region & alloc_chrdev_region
    • 2.3 注销
  • 三、自动创建字符设备驱动的设备文件
    • 3.1 解决方案:udev(嵌入式中用的是mdev)
    • 3.2 内核驱动设备类相关函数
  • 四、设备类相关代码分析
    • 4.1 sysfs文件系统
  • 五、静态映射表建立过程分析
    • 5.1 建立映射表的三个关键部分
  • 六、动态映射结构体方式操作寄存器
  • 七、内核提供的读写寄存器接口

一、字符设备驱动高级

1.1 注册字符设备驱动新接口

1.1.1 新接口与旧接口

  • 旧接口:register_chrdev
  • 新接口:register_chrdev_region(注册设备号)/alloc_chrdev_region(分配设备号) + cdev

1.1.2 cdev介绍

(1) 结构体

struct cdev {
   struct kobject kobj;          // 内嵌的内核对象,每个 cdev 都是一个 kobject
   struct module *owner;       // 指向实现驱动的模块
   const struct file_operations *ops;   // 操纵这个字符设备文件的方法
   struct list_head list;       // 与 cdev 对应的字符设备文件的 inode->i_devices 的链表头,用来将已经向内核注册的所有字符设备形成链表
   dev_t dev;                   // 字符设备的设备号,由主设备号和次设备号构成
   unsigned int count;       // 隶属于同一主设备号的次设备号的个数.
};

(2) 模块加载函数通过 register_chrdev_region( ) 或 alloc_chrdev_region( )来静态或者动态获取设备号;
(3) 通过 cdev_init( ) 建立cdev与 file_operations之间的连接,通过 cdev_add( ) 向系统添加一个cdev以完成注册;
(4) 模块卸载函数通过cdev_del( )来注销cdev,通过 unregister_chrdev_region( )来释放设备号;

1.1.3 设备号

(1) 设备号 = 主设备号 + 次设备号
(2) dev_t类型
(3) MKDEV(MAJOR, MINOR);
说明: 获取设备在设备表中的位置。
MAJOR 主设备号
MINOR 次设备号

1.1.4 编程实践

(1) 使用register_chrdev_region + cdev_init + cdev_add进行字符设备驱动注册

全局变量:
	#define MYMAJOR		200
	#define MYCNT		1
	#define MYNAME		"testchar"
	
	static dev_t mydev;
	static struct cdev test_cdev;

注册驱动:
	// 新的接口注册字符设备驱动需要2步
	
	// 第1步:静态注册/分配主次设备号
	int retval;
	mydev = MKDEV(MYMAJOR, 0);
	retval = register_chrdev_region(mydev, MYCNT, MYNAME);
	if (retval)
	{
		printk(KERN_ERR "Unable to register minors for %s\n", MYNAME);
		return -EINVAL;
	}
	printk(KERN_INFO "register_chrdev_region success\n");
	
	// 第2步:注册字符设备驱动
	cdev_init(&test_cdev, &test_fops);
	retval = cdev_add(&test_cdev, mydev, MYCNT);
	if (retval) 
	{
		printk(KERN_ERR "Unable to cdev_add\n");
		return -EINVAL;
	}
	printk(KERN_INFO "cdev_add success\n");

注销驱动:
// 使用新的接口来注销字符设备驱动
	// 注销分2步:
	// 第一步真正注销字符设备驱动用cdev_del
	cdev_del(&test_cdev);
	// 第二步去注销申请的主次设备号
	unregister_chrdev_region(mydev, MYCNT);

1.1.5 alloc_chrdev_region自动分配设备号

(1) register_chrdev_region是在事先知道要使用的主、次设备号时使用的;要先查看cat /proc/devices去查看没有使用的
(2) 更简便、更智能的方法是让内核给我们自动分配一个主设备号,使用alloc_chrdev_region就可以自动分配了。
(3) 自动分配的设备号,必须去知道它的主次设备号,否则后面没法去mknod创建他对应的设备文件。
(4) 使用MAJOR宏和MINOR宏从dev_t得到major和minor
(5) 反过来使用MKDEV宏从major和minor得到dev_t。
(6) 使用这些宏的代码具有可移植性

	#define MYCNT		1
	#define MYNAME		"testchar"
	static dev_t mydev;
	static struct cdev test_cdev;

	//自动分配主次设备号
	retval = alloc_chrdev_region(&mydev, 12, MYCNT, MYNAME);
	if (retval < 0) 
	{
		printk(KERN_ERR "Unable to alloc minors for %s\n", MYNAME);
		goto flag1;
	}
	printk(KERN_INFO "alloc_chrdev_region success\n");
	printk(KERN_INFO "major = %d, minor = %d.\n", MAJOR(mydev), MINOR(mydev));  //获取我们的主次设备号,用于创建设备文件

1.1.6 中途出错的倒影式错误处理方法

(1) 内核中很多函数中包含了很多个操作,这些操作每一步都有可能出错,而且出错后,后面的步骤就没有进行下去的必要性了。所以就有了倒影式处理错误的方法。

// 第1步:分配主次设备号
	retval = alloc_chrdev_region(&mydev, 12, MYCNT, MYNAME);
	if (retval < 0) 
	{
		printk(KERN_ERR "Unable to alloc minors for %s\n", MYNAME);
		goto flag1;
	}

// 第2步:注册字符设备驱动
	cdev_init(&test_cdev, &test_fops);
	retval = cdev_add(&test_cdev, mydev, MYCNT);
	if (retval) {
		printk(KERN_ERR "Unable to cdev_add\n");
		goto flag2;
	}

// 第3步:使用动态映射的方式来操作寄存器
	if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"))
		goto flag3;

// 如果第3步才出错跳转到这里来	
flag3:
	release_mem_region(GPJ0CON_PA, 4);
	
// 如果第2步才出错跳转到这里来
flag2:
	cdev_del(&test_cdev);

// 如果第1步才出错跳转到这里来
flag1:
	// 在这里把第1步做成功的东西给注销掉
	unregister_chrdev_region(mydev, MYCNT);
注:使用cdev_alloc,cdev_init的替代(重点)

在这里插入图片描述

二、字符设备驱动注册代码分析

2.1 旧接口register_chrdev

register_chrdev注册函数

=》 __register_chrdev -- 内核级函数
		==》 __register_chrdev_region -- 内核级函数
		==》cdev_alloc -- 让内核为这个结构体分配内存的。
			===》cdev_add -- 向内核里面添加一个驱动,注册驱动。

函数 __register_chrdev_region() 主要执行以下步骤:

  1. 分配一个新的 char_device_struct 结构,并用 0 填充。
  2. 如果申请的设备编号范围的主设备号为 0,那么表示设备驱动程序请求动态分配一个主设备号。动态分配主设备号的原则是从散列表的最后一个桶向前寻找,那个桶是空的,主设备号就是相应散列桶的序号。所以动态分配的主设备号总是小于 256,如果每个桶都有字符设备编号了,那动态分配就会失败。
  3. 根据参数设置 char_device_struct 结构中的初始设备号,范围大小及设备驱动名称。
  4. 计算出主设备号所对应的散列桶,为新的 char_device_struct 结构寻找正确的位置。同时,如果设备编号范围有重复的话,则出错返回。
  5. 将新的 char_device_struct 结构插入散列表中,并返回 char_device_struct 结构的地址。

2.2 新接口register_chrdev_region & alloc_chrdev_region

register_chrdev_region== 动态分配主次设备号
=》__register_chrdev_region

register_chrdev_region() 函数用于分配指定的设备编号范围。如果申请的设备编号范围跨越了主设备号,它会把分配范围内的编号按主设备号分割成较小的子范围,并在每个子范围上调用 __register_chrdev_region() 。如果其中有一次分配失败的话,那会把之前成功分配的都全部退回。

alloc_chrdev_region==让内核自动给我们分配设备号
=》__register_chrdev_region

alloc_chrdev_region() 函数用于动态申请设备编号范围,通过指针参数返回实际获得的起始设备编号。

2.3 注销

注销和注册分配字符设备编号范围类似,内核提供了两个注销字符设备编号范围的函数,分别是 unregister_chrdev_region() 和 unregister_chrdev() 。它们都调用__unregister_chrdev_region函数,这个就不分析了。

三、自动创建字符设备驱动的设备文件

(1) 整体流程回顾
(2) 使用mknod创建设备文件的缺点
(3) 能否自动生成和删除设备文件

3.1 解决方案:udev(嵌入式中用的是mdev)

(1) 什么是udev?应用层的一个应用程序
(2) 内核驱动和应用层udev之间有一套信息传输机制(netlink协议)
(3) 应用层启用udev,内核驱动中使用相应接口
(4) 驱动注册和注销时信息会被传给udev,由udev在应用层进行设备文件的创建和删除
在这里插入图片描述

3.2 内核驱动设备类相关函数

  • class_create/class_destroy
  • device_create/device_destroy
#include <linux/device.h>  //相关的函数包含在这个头文件里面

static dev_t mydev;
static struct class *test_class;

	// 注册字符设备驱动完成后,添加设备类的操作,以让内核帮我们发信息
	// 给udev,让udev自动创建和删除设备文件
	test_class = class_create(THIS_MODULE, "aston_class");
	if (IS_ERR(test_class))
		return -EINVAL;
		
	// 最后1个参数字符串,就是我们将来要在/dev目录下创建的设备文件的名字
	// 所以我们这里要的文件名是/dev/test111
	device_create(test_class, NULL, mydev, NULL, "test111");

	//在注销设备驱动之前
	device_destroy(test_class, mydev);
	class_destroy(test_class);

在这里插入图片描述

四、设备类相关代码分析

4.1 sysfs文件系统

因为udev需要sysfs文件系统的支持(sysfs文件系统只在linux-2.6内核以上才有),所以它存在于Linux-2.6版本之后的内核。udev借助于netlink协议在内核驱动和应用层之间传递信息。当内核中的驱动完成注册和注销时,信息会被传送给应用层的udev,udev便会自动地完成设备文件的创建和删除。

内核中定义了struct class结构体,顾名思义,一个struct class结构体类型变量对应一个类, 内核同时提供了class_create(…)函数,可以用它来创建一个类,这个类存放于sysfs下面,一旦创建好了这个类,再调用 device_create(…)函数来在/dev目录下创建相应的设备节点。这样,加载模块的时候,用户空间中的udev会自动响应 device_create(…)函数,去/sysfs下寻找对应的类从而创建设备节点。
在这里插入图片描述
2、class_creat & device_createt
(1) class_creat 树形调用的主要的函数
class_create();
__class_create();
__class_register();
kset_register();
kobject_uevent();

(2) device_createt 树形调用的主要的函数

struct device *device_create(struct class *class, struct device *parent,
			     dev_t devt, void *drvdata, const char *fmt, ...) ------------------- 参数”...”就是可变参数, 更多的时候表示了次设备号。
{
.........
}

例如:此图中 tty 就表示主设备号, 后面的14… 15等表示次设备号
在这里插入图片描述

device_createt();
	device_create_vargs();
		kobject_set_name_vargs();
		device_register();
			device_add();
				kobject_add(); //真正的把设备添加进去了
					device_create_file();//下面这些函数都是操作 sysfs 的函数
					device_create_sys_dev_entry();
					devtmpfs_create_node();
					device_add_class_symlinks();
					device_add_attrs();
					device_pm_add();
					kobject_uevent();

device_create_file() 函数创建的就是 dev 目录(sysfs函数实现的),

int device_create_file(struct device *dev,
		       const struct device_attribute *attr)
{
	int error = 0;

	if (dev) {
		....
		error = sysfs_create_file(&dev->kobj, &attr->attr);
	}

	return error;
}
EXPORT_SYMBOL_GPL(device_create_file);

参数 uevent_attr

static struct device_attribute uevent_attr =
	__ATTR(uevent, S_IRUGO | S_IWUSR, show_uevent, store_uevent); -------- 给出的属性, 参数中 show_uevent, store_uevent 表示读和存储。

所以在执行cat dev 读取 dev 这个文件时, 内核调用的就是 show_uevent 函数。

五、静态映射表建立过程分析

5.1 建立映射表的三个关键部分

(1) 主映射表:映射表具体物理地址和虚拟地址的值相关的宏定义
在这里插入图片描述

(2) 映射表建立函数。该函数负责由(1)中的映射表来建立linux内核的页表映射关系。

在kernel/arch/arm/mach-s5pv210/mach-smdkc110.c中的smdkc110_map_io函数
smdkc110_map_io();
	s5p_init_io();
		iotable_init();  // 引出io 描述符的概念

  经过分析,真正的内核移植时给定的静态映射表在arch/arm/plat-s5p/cpu.c中的s5p_iodesc,本质是一个结构体数组,数组中每一个元素就是一个映射,这个映射描述了一段物理地址到虚拟地址之间的映射。这个结构体数组所记录的几个映射关系被iotable_init所使用,该函数负责将这个结构体数组格式的表建立成MMU所能识别的页表映射关系,这样在开机后可以直接使用相对应的虚拟地址来访问对应的物理地址。
注:该部分重点记录分析方法。
在这里插入图片描述
// 由上图可以看出,内存管理最小的表是4K。

(3) 开机时调用映射表建立函数

问题:开机时(kernel启动时)smdkc110_map_io怎么被调用的?
函数调用层级:
start_kernel();
	setup_arch();
		paging_init();
			devicemaps_init();
			
if (mdesc->map_io)
		mdesc->map_io();

六、动态映射结构体方式操作寄存器

知识回顾:之前的动态映射,每个寄存器地址是单独映射的,要进行多次。
(1) 仿效真实驱动中,用结构体封装的方式来进行单次多寄存器的地址映射。

实验代码:
typedef struct GPJ0REG
{
	volatile unsigned int gpj0con;
	volatile unsigned int gpj0dat;
} gpj0_reg_t;

#define GPJ0_REGBASE	0xe0200240    //物理地址
gpj0_reg_t *pGPJ0REG;

// 使用动态映射的方式来操作寄存器
	if (!request_mem_region(GPJ0_REGBASE, sizeof(gpj0_reg_t), "GPJ0REG"))
		return -EINVAL;
	pGPJ0REG = ioremap(GPJ0_REGBASE, sizeof(gpj0_reg_t));
	// 映射之后用指向结构体的指针来进行操作
	// 指针使用->结构体内元素的方式来操作各个寄存器
	pGPJ0REG->gpj0con = 0x11111111;
	pGPJ0REG->gpj0dat = ((0<<3) | (0<<4) | (0<<5));		// 亮
	
// 解除映射
	iounmap(pGPJ0REG);
	release_mem_region(GPJ0_REGBASE, sizeof(gpj0_reg_t));

七、内核提供的读写寄存器接口

  1. 内核提供的寄存器读写接口(在不同的架构下,可移植性高)
    (1) writel和readl,在3.字符设备驱动上篇已经提过了。
    (2) iowrite32和ioread32
  2. 代码实践
#define GPJ0CON		S5PV210_GPJ0CON
#define GPJ0DAT		S5PV210_GPJ0DAT

#define rGPJ0CON	*((volatile unsigned int *)GPJ0CON)
#define rGPJ0DAT	*((volatile unsigned int *)GPJ0DAT)

#define GPJ0CON_PA	0xe0200240
#define GPJ0DAT_PA 	0xe0200244

#define S5P_GPJ0REG(x)		(x)
#define S5P_GPJ0CON			S5P_GPJ0REG(0)
#define S5P_GPJ0DAT			S5P_GPJ0REG(4)

unsigned int *pGPJ0CON;
unsigned int *pGPJ0DAT;

static void __iomem *baseaddr;			// 寄存器的虚拟地址的基地址

// 使用动态映射的方式来操作寄存器
	if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"))
		return -EINVAL;
	if (!request_mem_region(GPJ0DAT_PA, 4, "GPJ0CON"))
		return -EINVAL;
	
	pGPJ0CON = ioremap(GPJ0CON_PA, 4);
	pGPJ0DAT = ioremap(GPJ0DAT_PA, 4);

/************/  原始的用解引用指针的方法  /***********/
	*pGPJ0CON = 0x11111111;         
	*pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));		// 亮

/***********/ 使用内部读写接口的方法  /***********/

 测试1:用2次ioremap得到的动态映射虚拟地址来操作,测试成功
writel(0x11111111, pGPJ0CON);
writel(((0<<3) | (0<<4) | (0<<5)), pGPJ0DAT);
	
测试2:用静态映射的虚拟地址来操作,测试成功
writel(0x11111111, GPJ0CON);
writel(((0<<3) | (0<<4) | (0<<5)), GPJ0DAT);
	
测试3:用1次ioremap映射多个寄存器得到虚拟地址,测试成功
if (!request_mem_region(GPJ0CON_PA, 8, "GPJ0BASE"))
		return -EINVAL;
baseaddr = ioremap(GPJ0CON_PA, 8);
	
writel(0x11111111, baseaddr + S5P_GPJ0CON);
writel(((0<<3) | (0<<4) | (0<<5)), baseaddr + S5P_GPJ0DAT);
  1. 操作寄存器的大致流程
    总结:

注: 本文参考朱有鹏老师网上学员的学习记录博客,根据自己的理解,进行学习汇总,如有侵权,联系本人及时删除。

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

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

相关文章

Ceres-Solver 安装与卸载ubuntu20.04

卸载 sudo rm -rf /usr/local/lib/cmake/Ceres /usr/local/include/ceres /usr/local/lib/libceres.a 安装 sudo apt-get install libatlas-base-dev libsuitesparse-dev git clone https://github.com/ceres-solver/ceres-solver cd ceres-solver git checkout $(git descr…

若依学习——定时任务代码逻辑 详细梳理(springboot整合Quartz)

springboot整合Quartz关于若依定时任务的使用可以去看视频默认定时任务的使用关于springboot整合quartz的整合参考(150条消息) 定时任务框架Quartz-(一)Quartz入门与Demo搭建_quarzt_是Guava不是瓜娃的博客-CSDN博客(150条消息) SpringBoot整合Quartz_springboot quartz_桐花思…

ESP32设备驱动-MCP23017并行IO扩展驱动

MCP23017并行IO扩展驱动 1、MCP23017介绍 MCP23017是一个用于 I2C 总线应用的 16 位通用并行 I/O 端口扩展器。 16 位 I/O 端口在功能上由两个 8 位端口(PORTA 和 PORTB)组成。 MCP23017 可配置为在 8 位或 16 位模式下工作。 其引脚排列如下: MCP23017 在 3.3v 下工作正常…

UML 类图

车的类图结构为<>&#xff0c;表示车是一个抽象类&#xff1b; 它有两个继承类&#xff1a;小汽车和自行车&#xff1b;它们之间的关系为实现关系&#xff0c;使用带空心箭头的虚线表示&#xff1b; 小汽车为与SUV之间也是继承关系&#xff0c;它们之间的关系为泛化关系…

浅析Linux内核中进程完全公平CFS调度

一、前序 目前Linux支持三种进程调度策略&#xff0c;分别是SCHED_FIFO 、 SCHED_RR和SCHED_NORMAL&#xff1b;而Linux支持两种类型的进程&#xff0c;实时进程和普通进程。实时进程可以采用SCHED_FIFO 和SCHED_RR调度策略&#xff1b;普通进程则采用SCHED_NORMAL调度策略。从…

【架构师】跟我一起学架构——调用链

博客昵称&#xff1a;架构师Cool 最喜欢的座右铭&#xff1a;一以贯之的努力&#xff0c;不得懈怠的人生。 作者简介&#xff1a;一名Coder&#xff0c;软件设计师/鸿蒙高级工程师认证&#xff0c;在备战高级架构师/系统分析师&#xff0c;欢迎关注小弟&#xff01; 博主小留言…

C/C++:动态内存管理

目录 一. C/C内存分布 二. C/C动态内存管理 2.1 C语言动态内存管理 2.2 C动态内存管理 2.2.1 new/delete操作符 2.2.2 operator new与operator delete函数 2.3 new/delete的实现原理 2.4 定位new&#xff08;placement - new&#xff09; 2.5 new/delete和malloc/free的…

代码随想录算法训练营day49 | 动态规划 123.买卖股票的最佳时机III 188.买卖股票的最佳时机IV

day49123.买卖股票的最佳时机III1.确定dp数组以及下标的含义2.确定递推公式3.dp数组如何初始化4.确定遍历顺序5.举例推导dp数组188.买卖股票的最佳时机IV1.确定dp数组以及下标的含义2.确定递推公式4.dp数组如何初始化4.确定遍历顺序5.举例推导dp数组123.买卖股票的最佳时机III …

Zookeeper3.5.7版本——选举机制(非第一次启动)

目录一、ZooKeeper集群中哪些情况会进入Leader选举二、当一台机器进入Leader选举流程时&#xff0c;当前集群的两种状态2.1、集群中本来就已经存在一个Leader2.2、集群中确实不存在Leader三、Zookeeper中的一些概念了解3.1、SID3.2、ZXID3.3、Epoch一、ZooKeeper集群中哪些情况…

谈“对象“

面向对象一切皆对象&#xff0c;和java一样&#xff0c;各编程语言一样的思想规范类名首字母大写&#xff0c;和java一样创建的规范python3创建类的时候&#xff0c;可以不带括号&#xff0c;也可以带&#xff0c;也可以显示继承object&#xff0c;如果带个()空括号&#xff0c…

【网络】序列化和反序列化

&#x1f941;作者&#xff1a; 华丞臧. &#x1f4d5;​​​​专栏&#xff1a;【网络】 各位读者老爷如果觉得博主写的不错&#xff0c;请诸位多多支持(点赞收藏关注)。如果有错误的地方&#xff0c;欢迎在评论区指出。 推荐一款刷题网站 &#x1f449; LeetCode刷题网站 文章…

WSL2使用Nvidia-Docker实现深度学习环境自由部署

1. Win11 显卡驱动的安装 注意&#xff1a;WSL2中是不需要且不能安装任何显卡驱动的&#xff0c;它的显卡驱动完全依赖于 Win11 中的显卡驱动&#xff0c;因此我们只需要安装你显卡对应的 Win11 版本显卡驱动版本&#xff08;必须是 Win11 版本的驱动&#xff09;&#xff0c;…

Three.js高级应用--利用Three.js+WebGL实现fbx和obj格式模型的自定义加载

通过对webgl和three.js的不断学习与实践&#xff0c;在三维应用场景建设过程中&#xff0c;利用Three.js与webgl配合可以实现大部分三维场景的应用需求&#xff0c;这一篇主要讲述如何利用Three.js加载已有的模型&#xff0c;支持的三维模型格式有.fbx和.obj&#xff0c;同时.o…

English Learning - L2 第1次小组纠音 [ɑː] [ɔː] [uː] 2023.2.25 周六

English Learning - L2 第1次小组纠音 [ɑː] [ɔː] [uː] 2023.2.25 周六共性问题分析大后元音 [ɑː]大后元音 [ɔː]后元音 [uː]我的发音问题后元音 [uː]大后元音 [ɑː] 和 [ɔː]纠音过程第一次第二次第三次共性问题分析 大后元音 [ɑː] 嘴唇过于松散&#xff0c;没…

SpringMVC文件上传、下载、国际化配置

Java知识点总结&#xff1a;想看的可以从这里进入 目录3.6、文件上传、下载3.6.1、文件上传3.6.2、文件下载3.7、国际化配置3.6、文件上传、下载 3.6.1、文件上传 form 表单想要具有文件上传功能&#xff0c;其必须满足以下 3 个条件。 form 表单的 method 属性必须设置为 p…

Spring基础知识(Spring注解开发大全)

原本xml文件写法 文件头 文件信息 配置Bean 初步修改的xml文件写法 文件头 文件信息 <context:component-scan base-package"要扫描的包"/>注解开发Bean 第一步&#xff1a;写config文件 Configuration//代表xml文件的文件头 ComponentScan(“要扫描的包”…

大型JAVA版云HIS医院管理系统源码 Saas应用+前后端分离+B/S架构

SaaS运维平台多集团多医院入驻强大的电子病历完整文档 有源码&#xff0c;有演示&#xff01; 云HIS系统技术栈&#xff1a; 1、前端框架&#xff1a;AngularNginx 2、后台框架&#xff1a;JavaSpring&#xff0c;SpringBoot&#xff0c;SpringMVC&#xff0c;SpringSecurity&…

【2022.1.3】手脱压缩壳练习(含练习exe)

【2022.1.3】手脱压缩壳练习&#xff08;含练习exe&#xff09; 文章目录【2022.1.3】手脱压缩壳练习&#xff08;含练习exe&#xff09;0、简介1、单步跟踪法&#xff08;#&#xff09;方法介绍&#xff08;0&#xff09;练习exe下载&#xff08;1&#xff09;、查看源程序&am…

精确率与召回率,ROC曲线与PR曲线

精确率与召回率&#xff0c;ROC曲线与PR曲线 在机器学习的算法评估中&#xff0c;尤其是分类算法评估中&#xff0c;我们经常听到精确率(precision)与召回率(recall)&#xff0c;ROC曲线与PR曲线这些概念&#xff0c;那这些概念到底有什么用处呢&#xff1f; 首先&#xff0c…

Linux系统GPIO应用编程

目录应用层如何操控GPIOGPIO 应用编程之输出GPIO 应用编程之输入GPIO 应用编程之中断在开发板上测试GPIO 输出测试GPIO 输入测试GPIO 中断测试本章介绍应用层如何控制GPIO&#xff0c;譬如控制GPIO 输出高电平、或输出低电平。应用层如何操控GPIO 与LED 设备一样&#xff0c;G…