当前位置: 首页 > news >正文

嵌入式底层驱动需要知道的基本知识

先说结论,能,肯定能,必须能!

但是,问题重点在于坚持,程序员这一行 ,下班回家一般都要10点了,再刷两个小时枯燥的学习视频,我想大多数人是坚持不下来的。

但是,我要说但是,"linux驱动开发其实并不难,难的是市面上没有靠谱的书籍和教学视频",就好像是你向一个半瓶水的模拟电路工程师请教电路设计原理,张口就是经验值,问就是别人都是这么设计的,能用就行,他能给你讲明白才有鬼了。

之所以说linux驱动开发不难是因为内核中已经实现了一套非常简洁,通用的驱动框架,自打2.6版本以后就没怎么变过,足以说明该驱动框架的优秀。而目前市面上的书籍和教学视频根本没有足够重视讲解驱动框架的内容,只是硬扣单个驱动的细节。作为单片机工程师,你跟linux驱动工程师之间差的就只是一个驱动框架而已。

说了这么多,是时候上干货了。还是坚持我一贯的理念,学习任何新鲜东西都应该是由远及近,先整体掌握全局,再深入探究细节。原则上,你只要认真看完下面的内容,就差不多算是入门了。废话不多说,上菜。

前置知识:

1. linux驱动模块整体是以面向对象思想来设计的,驱动中的每个节点都描述成一个对象。

2. 对象通常采用结构体的形式描述,结构体中的变量表示对象属性,函数指针表示对象行为。

3. 对象之间的继承关系采用内嵌父类结构体对象的形式体现。

4. 驱动中的同类对象一般采用链表的形式串联在一起,链表使用内嵌struct list_head的形式表示。

5. 内核中大量使用container_of宏,实现通过已知结构体对象内部元素的地址获取整个结构体起始地址的功能。

例:继承实现方法

/* 父类 */
struct ANIMAL {
int age;
int weight;
};

/* 子类 */
struct DOG {
struct ANIMAL animal; /* 通过内嵌父类对象,来实现继承关系 */
int variety;
};

/* 通过dog对象中animal对象的地址获取dog对象的起始地址 */
struct DOG *dog = container_of(ptr_animal, struct DOG, animal);

例:链表的使用方法

struct xxx_dev {
int id;
int num;
struct list_head node; /* 通过在对象中嵌入struct list_head节点,来实现链表功能 */
};

/* 遍历链表的时候,已知node地址,借助container_of宏可以获取到外层对象xxx_dev的起始地址。*/
struct xxx_dev *dev = container_of(prt_node, struct xxx_dev, node);

核心驱动框架:

linux内核中对不同的组成部分高度抽象,采用 "总线-设备-驱动"模型来组织某一层驱动代码,多层之间可以叠加。模型结构如下,总线作为桥梁和纽带,连接设备和对应的驱动。

图片

(核心驱动框架)

设备:挂载在各个总线上的的硬件设备或者虚拟设备,比如挂在I2C总线上的温湿度传感器, 挂载在平台总线上I2C控制器等,都被抽象描述成设备对象。使用结构体struct device表示设备基类,用来描述硬件设备的各种参数,具体各类设备可以通过内嵌基类对象,实现继承和扩展。

驱动:记录硬件设备状态的变量和控制硬件工作的函数的集合,负责对硬件设备进行初始化,并向上层代码提供操作接口,比如SOC中各类总线控制器驱动,以及外挂的总线设备驱动等,使用结构体struct device_driver表示驱动基类,具体各类驱动可以通过内嵌基类对象,实现继承和扩展。

总线:表示各种物理或者虚拟总线。总线作为桥梁和纽带,用来连接设备和驱动,并提供驱动注册,设备发现,设备注册/卸载等功能。常见的总线例如:平台总线,I2C总线,SPI总线,USB总线等。其中平台总线是驱动工程师最长接触的总线类型,有些书籍把平台总线叫做虚拟总线,说是那些没有实际物理总线的都归类为平台总线,我认为这个说法不对。平台总线应该是指SOC中那些内部互联用的总线,比如ARM SOC中的AXI, AHB, APB总线等,这些总线上连接的大量的控制器,都可以通过地址映射直接访问,所以这些控制器一般被称为平台设备,连接的总线被称为平台总线。使用struct bus_type表示总线基类,具体各类总线可以通过内嵌基类,实现继承和扩展。

驱动框架继承关系:

如前所述,Linux驱动框架中分别定义了"总线-设备-驱动"各对象的基类,其他各子类都是从基类继承而来,继承关系如下图:

图片

(继承关系)

用户接口:

所有的硬件都是为了实现某些具体功能而生的,驱动程序操作硬件设备就是为了给上层应用提供服务,但是linux内核为了安全,把运行空间分成了内核空间(kernel space)和用户空间(user space)两部分,其中内核代码运行在内核空间,应用程序运行在用户空间。应用程序通过系统调用接口,调用内核以及驱动提供的各种服务,示意图如下:

图片

(系统调用示意图)

但是硬件种类多种多样,对应的驱动数量也不胜枚举,而且还在不断的变化中,不可能为每种驱动都提供系统调用接口,好在多数设备的操作步骤都很类似,主要可以概括为:初始化,读,写,关闭等基本步骤。根据设备的功能属性和使用方式不同,内核中把设备大体分为:字符设备,块设备和网络设备三个大类。其中字符设备和块设备因为操作步骤跟文件操作很相似,所以复用了VFS(虚拟文件系统)提供的系统调用接口(open,release,read,write, ioctl等接口), 其在内核中分别使用 struct cdev和struct block_device表示, 在用户空间以特殊文件形式存在于/dev目录下,使用ls -ls /dev 可以查看各文件的属性,其中属性crw-rw-rw-以'c'打头的表示字符设备,属性brw-rw----以b打头的表示块设备。

cros@cros-pc:~$ ls -ls /dev/
total 0
0 crw------- 1 root root 5, 1 5月 28 00:04 console
0 crw-rw-rw- 1 root root 1, 7 5月 28 00:04 full
0 crw-rw---- 1 root kvm 10, 232 5月 28 00:04 kvm
0 brw-rw---- 1 root disk 8, 0 5月 28 00:04 sda
0 brw-rw---- 1 root disk 8, 1 5月 28 00:04 sda1
0 brw-rw---- 1 root disk 8, 2 5月 28 00:04 sda2

网络设备因为操作方式不同,无法复用VFS的系统接口,所以只能单独提供几个独享的系统调用接口,如下SYSCALL_DEFINEx宏的第一个参数就是系统调用的名字:

cros@cros-pc:~/home/cros/kernel$ grep -rn "SYSCALL_DEFINE*" net/socket.c
1213:SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
1254:SYSCALL_DEFINE4(socketpair, int, family, int, type, int, protocol,
1363:SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
1392:SYSCALL_DEFINE2(listen, int, fd, int, backlog)
1425:SYSCALL_DEFINE4(accept4, int, fd, struct sockaddr __user *, upeer_sockaddr,
1506:SYSCALL_DEFINE3(accept, int, fd, struct sockaddr __user *, upeer_sockaddr,
1524:SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,
1556:SYSCALL_DEFINE3(getsockname, int, fd, struct sockaddr __user *, usockaddr,
1587:SYSCALL_DEFINE3(getpeername, int, fd, struct sockaddr __user *, usockaddr,
1619:SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len,
1663:SYSCALL_DEFINE4(send, int, fd, void __user *, buff, size_t, len,
1675:SYSCALL_DEFINE6(recvfrom, int, fd, void __user *, ubuf, size_t, size,
1720:SYSCALL_DEFINE4(recv, int, fd, void __user *, ubuf, size_t, size,
1731:SYSCALL_DEFINE5(setsockopt, int, fd, int, level, int, optname,
1765:SYSCALL_DEFINE5(getsockopt, int, fd, int, level, int, optname,
1795:SYSCALL_DEFINE2(shutdown, int, fd, int, how)
1988:SYSCALL_DEFINE3(sendmsg, int, fd, struct user_msghdr __user *, msg, unsigned int, flags)
2057:SYSCALL_DEFINE4(sendmmsg, int, fd, struct mmsghdr __user *, mmsg,
2154:SYSCALL_DEFINE3(recvmsg, int, fd, struct user_msghdr __user *, msg,
2272:SYSCALL_DEFINE5(recvmmsg, int, fd, struct mmsghdr __user *, mmsg,
2317:SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)

另外,内核为了方便用户层操作设备,引入了sys文件系统,位于/sys目录下,其分别从总线,设备, 类等不同角度描述整个驱动框架,如下所示:

cros@cros-pc:~$ ls -ls /sys/
total 0
0 drwxr-xr-x 2 root root 0 6月 2 10:25 block
0 drwxr-xr-x 43 root root 0 6月 2 10:25 bus
0 drwxr-xr-x 68 root root 0 6月 2 10:25 class
0 drwxr-xr-x 4 root root 0 6月 2 10:25 dev
0 drwxr-xr-x 24 root root 0 5月 28 00:04 devices
0 drwxr-xr-x 6 root root 0 5月 28 00:04 firmware
0 drwxr-xr-x 10 root root 0 5月 28 00:04 fs
0 drwxr-xr-x 2 root root 0 6月 2 10:25 hypervisor
0 drwxr-xr-x 15 root root 0 5月 28 00:04 kernel
0 drwxr-xr-x 182 root root 0 6月 2 10:25 module
0 drwxr-xr-x 3 root root 0 6月 2 10:25 power

完整的用户接口如下图:

图片

(用户接口框架)

代码模板:

内核模块模板:

内核驱动模块基本通过如下模板,注册初始化函数和卸载函数,作为驱动代码的入口和出口。

/* 内核模块初始化函数 */
static int __init xxx_init(void)
{
}
/* 内核模块注销函数 */
static void __exit xxx_exit(void)
{
}
/* 注册初始化函数,使得自动或者手动安装驱动时,自动执行初始化函数 */
module_init(xxx_init);
/* 注册注销函数,使得自动或者手动安装驱动时,自动执行注销函数 */
module_exit(xxx_exit);

总线代码模板:

/* 总线类型结构体 */
struct bus_type platform_bus_type = {
.name = "platform",
.dev_groups = platform_dev_groups,
.match = platform_match,
.uevent = platform_uevent,
.pm = &platform_dev_pm_ops,
};

/* 内核模块初始化函数 */
int __init platform_bus_init(void)
{
int error;

/* 这里以平台总线为例,演示总线注册流程 */
error = bus_register(&platform_bus_type);
return error;
}

/* 注册初始化函数,使得自动或者手动安装驱动时,自动执行初始化函数 */
module_init(platform_bus_init);
/* 因为平台总线是内核中必不可少的基础总线,所以没有提供卸载函数 */

驱动代码模板:

/* 设备驱动结构体 */
static struct platform_driver at91_twi_driver = {
/* probe函数负责解析设备对象提供的参数,进行硬件初始化,并向上层提供操作接口 */
.probe = at91_twi_probe,
/* 设备卸载执行的操作 */
.remove = at91_twi_remove,
.id_table = at91_twi_devtypes
.driver = {
.name = "at91_i2c",
/* 用于跟设备匹配用的字段 */
.of_match_table = of_match_ptr(atmel_twi_dt_ids),
.pm = at91_twi_pm_ops,
},
};
/* 内核模块初始化函数 */
static int __init at91_twi_init(void)
{
/* 以平台设备驱动为例,演示驱动注册过程 */
return platform_driver_register(&at91_twi_driver);
}
/* 内核模块注销函数 */
static void __exit at91_twi_exit(void)
{
/* 以平台设备驱动为例,演示驱动卸载过程 */
platform_driver_unregister(&at91_twi_driver);
}
/* 注册初始化函数,使得自动或者手动安装驱动时,自动执行初始化函数 */
module_init(at91_twi_init);
/* 注册注销函数,使得自动或者手动安装驱动时,自动执行注销函数 */
module_exit(at91_twi_exit);

设备代码模板:

PS:新版本内核中因为引入了设备树,绝大多数设备都在设备树中描述了,内核初始化过程中会自动解析设备树,生成并注册设备,所以一下代码目前很少见了,此处只是为了解释原理和基本流程。

/* 平台设备结构体 */
static struct platform_device s3c24xx_uart_device0 = {
.id = 0,
};
static struct platform_device s3c24xx_uart_device1 = {
.id = 1,
};
static struct platform_device s3c24xx_uart_device2 = {
.id = 2,
};
static struct platform_device s3c24xx_uart_device3 = {
.id = 3,
};
struct platform_device *s3c24xx_uart_src[4] = {
&s3c24xx_uart_device0,
&s3c24xx_uart_device1,
&s3c24xx_uart_device2,
&s3c24xx_uart_device3,
};

/* 模块初始化函数 */
static int __init s3c_arch_init(void)
{
int ret;
/* 以平台设备为例,演示设备注册过程 */
ret = platform_add_devices(s3c24xx_uart_src, nr_uarts);
return ret;
}

/* 注册模块初始化函数,类似功能的宏还有很多,名字各不相同 */
arch_initcall(s3c_arch_init);

平台设备驱动框架:

平台设备驱动是开发人员接触最多,也是修改最多的一类驱动,因为其主要包括SOC内置的各种总线控制器,以及PWM,RTC,WDT等内置功能模块。基本都是跟芯片强相关的内容,所以每个SOC都需要单独开发对应驱动。

图片

(平台设备驱动举例)

总线设备驱动框架:

总线设备驱动相比于平台设备设备来说更复杂一些,一般包含两层驱动,底层是总线控制器驱动,上层是总线设备驱动。另外,因为总线控制器多种多样,为了统一上层的编程接口,驱动中会在中间增加core层,实现对总线控制器的抽象,并对上层提供统一的总线操作接口,类似于设计模式中的适配器模式。典型如I2C驱动框架中的struct i2c_adapter,以及SPI驱动框架中的struct spi_master。如下是I2C驱动框架,大家可以仔细品一下。

图片

(I2C设备驱动框架)

内核中还有很多支持热插拔的设备驱动,例如USB驱动,同一个USB接口,可能接了设备,也可能没有接设备,可能接了个U盘,也可能接了个鼠标。例如mmc驱动,mmc接口可能插了个MMC卡,也可能插了个SD卡,还可能插了个SDIO网卡。我们无法假设接口上到底接的是什么设备,但是我们可以通过电平信号判断是否接了设备。为了能够判断接口上接的是什么设备,以及设备具有怎样的参数,一般对应的协会都会指定一套完善的协议标准(例如USB协议,SD协议)。驱动代码中只要按照协议规定,跟设备进行通信,获取到对方提供的信息,然后根据协议进行解析,就可以获得所接硬件的详细信息。然后加载对应的驱动就可以正常使用硬件了。以下是mmc驱动框架,相比于I2C驱动框架,主要是多了协议解析部分,你再细品!

图片

(MMC驱动框架)

总结:

还是重点强调一点,学习新东西,一定是要由远及近,逐渐深入。先知道每个模块是干什么的,然后在学会怎么使用 ,最后才是深入去研究工作原理,以及如何修改。学习驱动开发更是这样,熟悉基本的驱动框架和各个模块的具体框架才是你第一步需要做的,剩下工作就是配置寄存器,初始化硬件设备了,这不就是单片机工程师现在正在做的事情吗?

相关文章:

嵌入式底层驱动需要知道的基本知识

先说结论,能,肯定能,必须能! 但是,问题重点在于坚持,程序员这一行 ,下班回家一般都要10点了,再刷两个小时枯燥的学习视频,我想大多数人是坚持不下来的。 但是&#xff…...

《软件开发的201个原则》阅读笔记 120-161条

目录 使用有效的测试完成度标准 原则122 达成有效的测试覆盖 原则123 不要在单元测试之前集成 原则 124 测量你的软件 原则125 分析错误的原因 对错不对人 原则127 好的管理比好的技术更重要 使用恰当的方法 原则 129 不要相信你读到的一切 原则130 理解客户的优先级 原…...

JVM——类加载与字节码技术—类文件结构

由源文件被编译成字节码文件,然后经过类加载器进行类加载,了解类加载的各个阶段,了解有哪些类加载器,加载到虚拟机中执行字节码指令,执行时使用解释器进行解释执行,解释时对热点代码进行运行期的编译处理。…...

C语言学习之main函数两个参数的应用

main函数的两个参数: int main(int argc, char const *argv[]) {/* code */return 0; }参数argc:表示在执行程序时,在终端所输入参数的个数,包括可执行文件的名称;参数argv:1.本质上是一个字符型指针数组;2.用于获取指…...

本地部署 Stable Diffusion(Windows 系统)

相对于使用整合包,手动在 Windows 系统下本地部署 Stable Diffusion Web UI(简称 SD-WebUI),更能让人了解一些事情的来龙去脉。 一、安装前置软件:Python 和 Git 1、安装 Python for windows。 下载地址 https://www.p…...

Java源码分析(二)Double

本篇是源码分析的第二篇,上篇我们一起分析了Integer类的源码,本篇一起学习下Double类的源码,看下其实现。 一、Double类图 首先,相比Integer,Double类的源码只有1000行代码。如下是Integer及其关联类/接口的类图&#…...

文件上传漏洞之条件竞争

这里拿upload-labs的第18关做演示 首先先看代码 $is_upload false; $msg null;if(isset($_POST[submit])){$ext_arr array(jpg,png,gif);$file_name $_FILES[upload_file][name];$temp_file $_FILES[upload_file][tmp_name];$file_ext substr($file_name,strrpos($file_…...

javacv基础04-图像色彩空间转换函数Imgproc.cvtColor()(彩图转灰度图示例)

opencv python 实现方式参考 opencv-19 图像色彩空间转换函数cv2.cvtColor() javacv 中的函数 Imgproc.cvtColor(image, grey, Imgproc.COLOR_BGR2GRAY); 参数说明: image: 原始图像新灰度图转换参数:多种转换方式参考上面链接地址内容 javacv 实现方式…...

Spring Boot进阶(60):5种判断线程池任务是否全部完成的方案 | 实用技巧分享!

1. 前言🔥 多线程编程在现代软件开发中非常常见且重要,而线程池是多线程编程的常用技术。在使用线程池时,通常需要判断线程池中的任务是否全部完成,以便决定程序继续执行的下一步操作。本文将介绍5种判断线程池任务是否全部完成的…...

Git相关介绍和操作

Git 是一个版本控制系统,它可以记录代码的变更历史,并允许多人协同开发。下面是 Git 的基本概念和使用方式: 仓库(Repository):Git 仓库用于存储代码的版本历史,包括代码变更、注释、作者、时间…...

IDEA配置热启动

1.背景 开发过程中,当写完一个功能我们需要运行应用程序测试,可能这个小功能中存在多个小bug,我们需要改正后重启服务器,这无形之中拖慢了开发的速度增加了开发时间,SpringBoot提供了spring-boot-devtools,…...

【附安装包】Fireworks CS6安装教程

软件下载 软件:Fireworks版本:CS6语言:简体中文大小:165.87M安装环境:Win11/Win10/Win8/Win7硬件要求:CPU2.0GHz 内存4G(或更高)下载通道①百度网盘丨下载链接:https://pan.baidu.c…...

深度学习-4-二维目标检测-YOLOv3理论模型

单阶段目标检测模型YOLOv3 R-CNN系列算法需要先产生候选区域,再对候选区域做分类和位置坐标的预测,这类算法被称为两阶段目标检测算法。近几年,很多研究人员相继提出一系列单阶段的检测算法,只需要一个网络即可同时产生候选区域并…...

通俗理解DDPM到Stable Diffusion原理

代码1:stabel diffusion 代码库代码2:diffusers 代码库论文:High-Resolution Image Synthesis with Latent Diffusion Models模型权重:runwayml/stable-diffusion-v1-5 文章目录 1. DDPM的通俗理解1.1 DDPM的目的1.2 扩散过程1.3 …...

如何基于自己训练的Yolov5权重,结合DeepSort实现目标跟踪

网上有很多相关不错的操作demo,但自己在训练过程仍然遇到不少疑惑。因此,我这总结一下操作过程中所解决的问题。 1、deepsort的训练集是否必须基于逐帧视频? 我经过尝试,发现非连续性的图像仍可以作为训练集。一个实例&#xff0…...

C#_委托详解

委托是什么? 字面理解:例如A要建一栋别墅,找到B建筑施工队,请B来建筑别墅。 委托类型规定方法的签名(方法类型):返回值类型、参数类型、个数、顺序。 委托变量可以用来存储方法的引用&#x…...

R包开发-2.2:在RStudio中使用Rcpp制作R-Package(更新于2023.8.23)

目录 4-添加C函数 5-编辑元数据 6-启用Roxygen,执行文档化。 7-单元测试 8-在自己的计算机上安装R包: 9-程序发布 参考: 为什么要写这篇文章的更新日期?因为R语言发展很快,很多函数或者方式,现在可以使…...

基于数据湖的多流拼接方案-HUDI实操篇

目录 一、前情提要 二、代码Demo (一)多写问题 (二)如果要两个流写一个表,这种情况怎么处理? (三)测试结果 三、后序 一、前情提要 基于数据湖对两条实时流进行拼接&#xff0…...

Spring MVC 四:Context层级

这一节我们来回答上篇文章中避而不谈的有关什么是RootApplicationContext的问题。 这就需要引入Spring MVC的有关Context Hierarchy的问题。Context Hierarchy意思就是Context层级,既然说到Context层级,说明在Spring MVC项目中,可能存在不止…...

【C++ 学习 ⑱】- 多态(上)

目录 一、多态的概念和虚函数 1.1 - 用基类指针指向派生类对象 1.2 - 虚函数和虚函数的重写 1.3 - 多态构成的条件 1.4 - 多态的应用场景 二、协变和如何析构派生类对象 2.1 - 协变 2.2 - 如何析构派生类对象 三、C11 的 override 和 final 关键字 一、多态的概念和虚…...

合宙Air724UG LuatOS-Air LVGL API控件--进度条 (Bar)

进度条 (Bar) Bar 是进度条,可以用来显示数值,加载进度。 示例代码 – 创建进度条 bar lvgl.bar_create(lvgl.scr_act(), nil) – 设置尺寸 lvgl.obj_set_size(bar, 200, 20); – 设置位置居中 lvgl.obj_align(bar, NULL, lvgl.ALIGN_CENTER, 0, 0) …...

图神经网络与分子表征:番外——基组选择

学过高斯软件的人都知道,我们在撰写输入文件 gjf 时需要准备输入【泛函】和【基组】这两个关键词。 【泛函】敲定计算方法,【基组】则类似格点积分中的密度,与计算精度密切相关。 部分研究人员借用高斯中的一系列基组去包装输入几何信息&am…...

rabbitmq笔记-rabbitmq客户端开发使用

连接RabbitMQ 1.创建ConnectionFactory,给定参数ip地址,端口号,用户名和密码等 2.创建ConnectionFactory,使用uri方式实现,创建channel。 注意: Connection可以用来创建多个channel实例,但c…...

13.Oracle中nvl()与nvl2()函数详解

Oracle中nvl()与nvl2()函数详解: 函数nvl(expression1,expression2)根据参数1是否为null返回参数1或参数2的值; 函数nvl2(expression1,expression2,expression3)根据参数1是否为null返回参数2或参数3的值 1.nvl:根据参数1是否为null返回参数…...

设置某行被选中并滚动到改行

<el-table :data"tableDamItem" ref"singleTable" stripe style"width: 100%" height"250" highlight-current-row v-on:row-click"handleTableRow"></el-table>/*** 设置表格行被选中,并滚动到该行* param po…...

React钩子函数之useRef的基本使用

React钩子函数中的useRef是一个非常有用的工具&#xff0c;它可以用来获取DOM元素或者保存一些变量。在这篇文章中&#xff0c;我们将会讨论useRef的基本使用。 首先&#xff0c;我们需要知道useRef是如何工作的。它返回一个可变的ref对象&#xff0c;这个对象可以在组件的整个…...

无风扇迷你电脑信息与购买指南

本文将解释什么是无风扇迷你电脑&#xff0c;以及计算产品组合中你可以购买的一些不同的无风扇迷你电脑的信息指南。 无风扇迷你电脑是一种小型工业计算机&#xff0c;旨在处理复杂的工业工作负载。迷你电脑是通过散热器被动冷却可在各种类型的易失性环境中部署。无风扇微型计…...

比特币是怎么回事?

比特币是怎么回事&#xff1f; 一句话描述就是&#xff0c;初始化几个比特币&#xff0c;申请成为矿工组织&#xff0c;发生交易时抢单记账成功可以比特币奖励&#xff0c;随着比特币数量的增加&#xff0c;奖励越来越少。怎么记账成功呢&#xff0c;通过交易信息幸运数字哈希…...

vue3+ts+uniapp小程序端自定义日期选择器基于内置组件picker-view + 扩展组件 Popup 实现自定义日期选择及其他选择

vue3ts 基于内置组件picker-view 扩展组件 Popup 实现自定义日期选择及其他选择 vue3tsuniapp小程序端自定义日期选择器 1.先上效果图2.代码展示2.1 组件2.2 公共方法处理日期2.3 使用组件 3.注意事项3.1refSelectDialog3.1 backgroundColor"#fff" 圆角问题 自我记…...

Java进阶篇--泛型

前言 Java 泛型&#xff08;generics&#xff09;是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制&#xff0c;该机制允许程序员在编译时检测到非法的类型。它允许在定义类、接口和方法时使用类型参数。这种技术使得在编译期间可以使用任何类型&#xff0c;而…...