STM32MP157驱动开发——Linux块设备驱动

news/2025/1/12 3:41:09/

STM32MP157驱动开发——Linux块设备驱动

  • 一、简介
  • 二、驱动开发
    • 1.使用请求队列的方式
    • 2.测试①
    • 3.不使用请求队列的方式
    • 4.测试②


参考文章:【正点原子】I.MX6U嵌入式Linux驱动开发——Linux 块设备驱动

一、简介

  之前学习的都是关于字符设备的驱动,包括 platform 子系统、I2C总线等,本质上都是对字符设备驱动的一层封装。这节就学习第二种驱动模式——块设备驱动。块设备驱动要远比字符设备驱动复杂得多,不同类型的存储设备又对应不同的驱动子系统。这一节使用开发板板载 RAM 模拟一个块设备,学习块设备驱动框架的使用。
  块设备是针对存储设备的,比如 SD 卡、EMMC、NAND Flash、Nor Flash、SPI Flash、机械硬盘、固态硬盘等。块设备与字符设备的区别如下:

1.块设备只能以块为单位进行读写访问,块是 linux 虚拟文件系统(VFS)基本的数据传输单位。而字符设备是以字节为单位进行数据传输的,不需要缓冲。
2.块设备在结构上是可以进行随机访问的,对于这些设备的读写都是按块进行的,块设备使用缓冲区来暂时存放数据,等到条件成熟以后再一次性将缓冲区中的数据写入块设备中。而字符设备是顺序的数据流设备,字符设备是按照字节进行读写访问的。字符设备不需要缓冲区,对于字符设备的访问都是实时的,而且也不需要按照固定的块大小进行访问。

块设备驱动框架的常用属性及使用流程就参考原子哥教程中的讲解,这里就不多赘述。

二、驱动开发

本节尝试使用开发板上的 RAM 模拟一段块设备,也就是 ramdisk,然后编写块设备驱动。

1.使用请求队列的方式

#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>
#include <linux/sched.h>
#include <linux/kernel.h>	
#include <linux/slab.h>		
#include <linux/fs.h>		
#include <linux/errno.h>	
#include <linux/types.h>	
#include <linux/fcntl.h>	
#include <linux/hdreg.h>
#include <linux/kdev_t.h>
#include <linux/vmalloc.h>
#include <linux/genhd.h>
#include <linux/blk-mq.h>
#include <linux/buffer_head.h>	
#include <linux/bio.h>#define RAMDISK_SIZE	(2 * 1024 * 1024) 	/* 容量大小为2MB */
#define RAMDISK_NAME	"ramdisk"			/* 名字 */
#define RADMISK_MINOR	3					/* 表示有三个磁盘分区!不是次设备号为3!*//* ramdisk设备结构体 */
struct ramdisk_dev{int major;						/* 主设备号 					  */unsigned char *ramdiskbuf;		/* ramdisk内存空间,用于模拟块设备 */struct gendisk *gendisk; 		/* gendisk 						  */struct request_queue *queue;	/* 请求队列 					  */struct blk_mq_tag_set tag_set; 	/* blk_mq_tag_set 				  */spinlock_t lock;				/* 自旋锁 						  */
};struct ramdisk_dev *ramdisk = NULL;		/* ramdisk设备指针 *//** @description	: 处理传输过程* @param-req 	: 请求* @return 		: 0,成功;其它表示失败*/
static int ramdisk_transfer(struct request *req)
{	unsigned long start = blk_rq_pos(req) << 9;  	/* blk_rq_pos获取到的是扇区地址,左移9位转换为字节地址 */unsigned long len  = blk_rq_cur_bytes(req);		/* 大小   *//* bio中的数据缓冲区* 读:从磁盘读取到的数据存放到buffer中* 写:buffer保存这要写入磁盘的数据*/void *buffer = bio_data(req->bio);		if(rq_data_dir(req) == READ) 		/* 读数据 */	memcpy(buffer, ramdisk->ramdiskbuf + start, len);else if(rq_data_dir(req) == WRITE) 	/* 写数据 */memcpy(ramdisk->ramdiskbuf + start, buffer, len);return 0;}/** @description	: 开始处理传输数据的队列* @hctx 		: 硬件相关的队列结构体* @bd			: 数据相关的结构体* @return 		: 0,成功;其它值为失败*/
static blk_status_t _queue_rq(struct blk_mq_hw_ctx *hctx, const struct blk_mq_queue_data* bd)
{struct request *req = bd->rq; /* 通过bd获取到request队列*/struct ramdisk_dev *dev = req->rq_disk->private_data;int ret;blk_mq_start_request(req);    /* 开启处理队列 */spin_lock(&dev->lock);		  ret = ramdisk_transfer(req);  /* 处理数据 */blk_mq_end_request(req, ret); /* 结束处理队列 */spin_unlock(&dev->lock);return BLK_STS_OK;}
/** 队列操作函数*/
static struct blk_mq_ops mq_ops = {.queue_rq = _queue_rq,
};/** @description		: 打开块设备* @param - dev 	: 块设备* @param - mode 	: 打开模式* @return 			: 0 成功;其他 失败*/
int ramdisk_open(struct block_device *dev, fmode_t mode)
{printk("ramdisk open\r\n");return 0;
}/** @description		: 释放块设备* @param - disk 	: gendisk* @param - mode 	: 模式* @return 			: 0 成功;其他 失败*/
void ramdisk_release(struct gendisk *disk, fmode_t mode)
{printk("ramdisk release\r\n");
}/** @description		: 获取磁盘信息* @param - dev 	: 块设备* @param - geo 	: 模式* @return 			: 0 成功;其他 失败*/
int ramdisk_getgeo(struct block_device *dev, struct hd_geometry *geo)
{/* 这是相对于机械硬盘的概念 */geo->heads = 2;			/* 磁头 */geo->cylinders = 32;	/* 柱面 */geo->sectors = RAMDISK_SIZE / (2 * 32 *512); /* 一个磁道上的扇区数量 */return 0;
}/* * 块设备操作函数 */
static struct block_device_operations ramdisk_fops =
{.owner	 = THIS_MODULE,.open	 = ramdisk_open,.release = ramdisk_release,.getgeo  = ramdisk_getgeo,
};/** @description	: 初始化队列相关操作* @set		 	: blk_mq_tag_set对象* @return 		: request_queue的地址*/
static struct request_queue * create_req_queue(struct blk_mq_tag_set *set)
{struct request_queue *q;#if 0/**这里是使用了blk_mq_init_sq_queue 函数*进行初始化的。*/q = blk_mq_init_sq_queue(set, &mq_ops, 2, BLK_MQ_F_SHOULD_MERGE);#elseint ret;memset(set, 0, sizeof(*set));set->ops = &mq_ops;		//操作函数set->nr_hw_queues = 2;	//硬件队列set->queue_depth = 2;	//队列深度set->numa_node = NUMA_NO_NODE;//numa节点set->flags =  BLK_MQ_F_SHOULD_MERGE; //标记在bio下发时需要合并ret = blk_mq_alloc_tag_set(set); //使用函数进行再次初始化if (ret) {printk(KERN_WARNING "sblkdev: unable to allocate tag set\n");return ERR_PTR(ret);}q = blk_mq_init_queue(set); //分配请求队列if(IS_ERR(q)) {blk_mq_free_tag_set(set);return q;}
#endifreturn q;
}/** @description	: 创建块设备,为应用层提供接口。* @set		 	: ramdisk_dev对象* @return 		: 0,表示成功;其它值为失败*/
static int create_req_gendisk(struct ramdisk_dev *set)
{struct ramdisk_dev *dev = set;/* 1、分配并初始化 gendisk */dev->gendisk = alloc_disk(RADMISK_MINOR);if(dev == NULL)return -ENOMEM;/* 2、添加(注册)disk */dev->gendisk->major = ramdisk->major; /* 主设备号 */dev->gendisk->first_minor = 0;		  /* 起始次设备号 */dev->gendisk->fops = &ramdisk_fops;	  /* 操作函数 */dev->gendisk->private_data = set;	  /* 私有数据 */dev->gendisk->queue = dev->queue;	  /* 请求队列 */sprintf(dev->gendisk->disk_name, RAMDISK_NAME); /* 名字 */set_capacity(dev->gendisk, RAMDISK_SIZE/512);	/* 设备容量(单位为扇区)*/add_disk(dev->gendisk);return 0;
}/** @description	: 驱动出口函数* @param 		: 无* @return 		: 无*/
static int __init ramdisk_init(void)
{int ret = 0;struct ramdisk_dev * dev;printk("ramdisk init\r\n");/* 1、申请内存 */dev = kzalloc(sizeof(*dev), GFP_KERNEL);if(dev == NULL) {return -ENOMEM;}dev->ramdiskbuf = kmalloc(RAMDISK_SIZE, GFP_KERNEL);if(dev->ramdiskbuf == NULL) {printk(KERN_WARNING "dev->ramdiskbuf: vmalloc failure.\n");return -ENOMEM;}ramdisk = dev;/* 2、初始化自旋锁 */spin_lock_init(&dev->lock);/* 3、注册块设备 */dev->major = register_blkdev(0, RAMDISK_NAME); /* 由系统自动分配主设备号 */if(dev->major < 0) {goto register_blkdev_fail;}/* 4、创建多队列 */dev->queue = create_req_queue(&dev->tag_set);if(dev->queue == NULL) {goto create_queue_fail;}/* 5、创建块设备 */ret = create_req_gendisk(dev);if(ret < 0)goto create_gendisk_fail;return 0;create_gendisk_fail:blk_cleanup_queue(dev->queue);blk_mq_free_tag_set(&dev->tag_set);
create_queue_fail:unregister_blkdev(dev->major, RAMDISK_NAME);
register_blkdev_fail:kfree(dev->ramdiskbuf);kfree(dev);return -ENOMEM;
}/** @description	: 驱动出口函数* @param 		: 无* @return 		: 无*/
static void __exit ramdisk_exit(void)
{printk("ramdisk exit\r\n");/* 释放gendisk */del_gendisk(ramdisk->gendisk);put_disk(ramdisk->gendisk);/* 清除请求队列 */blk_cleanup_queue(ramdisk->queue);/* 释放blk_mq_tag_set */blk_mq_free_tag_set(&ramdisk->tag_set);/* 注销块设备 */unregister_blkdev(ramdisk->major, RAMDISK_NAME);/* 释放内存 */kfree(ramdisk->ramdiskbuf);kfree(ramdisk);
}module_init(ramdisk_init);
module_exit(ramdisk_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");

①首先是宏定义部分,RAMDISK_SIZE 就是模拟块设备的大小,这里设置为 2MB。RAMDISK_NAME 为本实验名字,RADMISK_MINOR 是本实验此设备号数量。注意:非次设备号。设备号数量决定了本块设备的磁盘分区数量。
②通过 ramdisk 的设备结构体定义一个全局变量 ramdisk_dev 类型的指针。
③先看一下驱动模块的加载和卸载,即ramdisk_init()和ramdisk_exit()。

  • 在ramdisk_init()中先为自定义的ramdisk结构体申请空间,使用kmalloc申请2MB大小空间。然后初始化一个自旋锁,用于在队列操作的时候做保护。接着使用 register_blkdev 函数向内核注册一个块设备,返回值就是注册成功的块设备主设备号。这里让内核自动分配一个主设备号,因此 register_blkdev 函数的第一个参数为 0。create_req_queue 函数用于创建一个多队列,这一部分主要用于操作块设备。create_req_gendisk 函数用于创建一个块设备和提供一些接口给应用层调用。
  • ramdisk_exit 函数在卸载块设备驱动模块的时候使用,需要将前面注册的对象进行卸载、实例化的对象要进行释放。

④create_req_queue 函数用于创建一个多队列,首先设置多队列的重要参数,比如一些操作函数、队列深度、硬件队列个数和标志位等。然后设置 blk_mq_tag_set 的 ops 成员变量,这就是块设备的队列操作集,由开发人员实现。再使用 blk_mq_alloc_tag_set 函数进行再次初始化 blk_mq_tag_set 对象,最后根据此对象分配请求队列。
也可以使用 blk_mq_init_sq_queue 函数一步到位,第一个参数为 blk_mq_tag_set 对象、第二个参数为操作函数集合、第三个参数为硬件队列个数,第四个参数为标志位。
⑤使用 create_req_gendisk 函数进行初始化块设备。先使用 alloc_disk 分配一个 gendisk,然后初始化申请到的 gendisk 对象,重点是设置 geddisk 的 fops 成员变量。再使用 set_capacity 函数设置本块设备容量大小。注意:这里的大小是扇区数,不是字节数,一个扇区是 512 字节。gendisk 初始化完成以后就可以使用 add_disk 函数将 gendisk 添加到内核中,也就是向内核添加一个磁盘设备。
⑥gendisk 的 fops 操作集。就是块设备的操作集 block_device_operations,本节仅实现了 open、release 和 getgeo,其中 open 和 release 函数都是空函数,重点是 getgeo 函数。此函数用来获取磁盘信息,保存在参数 geo 中。
⑦blk_mq_tag_set 的 ops 操作集,也就是请求处理函数集合。使用 blk_mq_start_request 函数开启多队列处理,blk_mq_end_request 函数去结束多队列处理。ramdisk_transfer 数据处理函数,使用 ramdisk_transfer 数据处理函数,使用 bio_data 函数获取请求中的 bio 保存的数据。rq_data_dio 函数判断当前是读还是写,如果是写的话就将 bio 中的数据拷贝到 ramdisk 指定地址(扇区),如果是读的话就从 ramdisk 中的指定打字(扇区)读取数据放到 bio 中。

总结:主要是两个重要的结构体:blk_mq_tag_set 和 gendisk。可以把 blk_mq_tag_set 看作真正的 IO 读写操作(ops 操作集就是 IO 操作),有了底层操作之后,还需要 gendisk 结构体为上层提供接口调用(fops 就是实现上层调用的操作)。

2.测试①

驱动编写完成后,就可以编译出.ko文件进行挂载测试。
另外,还需要在 buildroot 中的 busybox 使能 mkfs.vfat 命令。在 buildroot 源码目录下,使用sudo make busybox-menuconfig命令打开 busybox 配置界面,选中以下选项:
在这里插入图片描述
然后使用以下命令,编译出新的根文件系统:

sudo make busybox	#编译新的busybox
sudo make 			#打包出新的buildroot

然后就可以使用新的根文件系统进行启动。
在这里插入图片描述
驱动挂载成功后,可以使用fdisk -l命令查看磁盘信息。其中就包括 2MB 的ramdisk设备。
然后使用以下命令进行格式化,然后挂载,就可以操作这块磁盘了。

mkfs.vfat /dev/ramdisk	#格式化磁盘为 vfat 格式
mkdir /mnt/ram_disk -P  #创建 ramdisk 挂载目录
mount /dev/ramdisk /mnt/ram_disk #挂载 ramdisk

3.不使用请求队列的方式

请求队列会用到 I/O 调度器,适合机械硬盘这种存储设备。对于 EMMC、SD、ramdisk 这样没有机械结构的存储设备,可以直接访问任意一个扇区,因此可以不需要 I/O 调度器,也就不需要请求队列了。
参考 linux 内核的 drivers/block/zram/zram_drv.c,把 blk_mq_tag_set 相关的都删除掉,然后修改 create_req_queue 函数即可,在此函数里使用 create_req_queue 函数设置“制造请求”函数。

#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>
#include <linux/sched.h>
#include <linux/kernel.h>	
#include <linux/slab.h>		
#include <linux/fs.h>		
#include <linux/errno.h>	
#include <linux/types.h>	
#include <linux/fcntl.h>	
#include <linux/hdreg.h>
#include <linux/kdev_t.h>
#include <linux/vmalloc.h>
#include <linux/genhd.h>
#include <linux/blk-mq.h>
#include <linux/buffer_head.h>	
#include <linux/bio.h>#define RAMDISK_SIZE	(2 * 1024 * 1024) 	/* 容量大小为2MB */
#define RAMDISK_NAME	"ramdisk"			/* 名字 */
#define RADMISK_MINOR	3					/* 表示有三个磁盘分区!不是次设备号为3!*//* ramdisk设备结构体 */
struct ramdisk_dev{int major;						/* 主设备号 					  */unsigned char *ramdiskbuf;		/* ramdisk内存空间,用于模拟块设备 */struct gendisk *gendisk; 		/* gendisk 						  */struct request_queue *queue;	/* 请求队列 					  */spinlock_t lock;				/* 自旋锁 						  */
};struct ramdisk_dev *ramdisk = NULL;		/* ramdisk设备指针 *//** @description		: 打开块设备* @param - dev 	: 块设备* @param - mode 	: 打开模式* @return 			: 0 成功;其他 失败*/
int ramdisk_open(struct block_device *dev, fmode_t mode)
{printk("ramdisk open\r\n");return 0;
}/** @description		: 释放块设备* @param - disk 	: gendisk* @param - mode 	: 模式* @return 			: 0 成功;其他 失败*/
void ramdisk_release(struct gendisk *disk, fmode_t mode)
{printk("ramdisk release\r\n");
}/** @description		: 获取磁盘信息* @param - dev 	: 块设备* @param - geo 	: 模式* @return 			: 0 成功;其他 失败*/
int ramdisk_getgeo(struct block_device *dev, struct hd_geometry *geo)
{/* 这是相对于机械硬盘的概念 */geo->heads = 2;			/* 磁头 */geo->cylinders = 32;	/* 柱面 */geo->sectors = RAMDISK_SIZE / (2 * 32 *512); /* 一个磁道上的扇区数量 */return 0;
}/* * 块设备操作函数 */
static struct block_device_operations ramdisk_fops =
{.owner	 = THIS_MODULE,.open	 = ramdisk_open,.release = ramdisk_release,.getgeo  = ramdisk_getgeo,
};/** @description	: “制造请求”函数* @param-q 	: 请求队列* @return 		: 无*/
static blk_qc_t ramdisk_make_request_fn(struct request_queue *q, struct bio *bio)
{int offset;struct bio_vec bvec;struct bvec_iter iter;unsigned long len = 0;struct ramdisk_dev *dev = q->queuedata;offset = (bio->bi_iter.bi_sector) << 9;	/* 获取要操作的设备的偏移地址 */spin_lock(&dev->lock);	/* 处理bio中的每个段 */bio_for_each_segment(bvec, bio, iter){char *ptr = page_address(bvec.bv_page) + bvec.bv_offset;len = bvec.bv_len;if(bio_data_dir(bio) == READ)	/* 读数据 */memcpy(ptr, dev->ramdiskbuf + offset, len);else if(bio_data_dir(bio) == WRITE)	/* 写数据 */memcpy(dev->ramdiskbuf + offset, ptr, len);offset += len;}spin_unlock(&dev->lock);bio_endio(bio);return BLK_QC_T_NONE;
}/** @description	: 初始化队列相关操作* @set		 	: blk_mq_tag_set对象* @return 		: request_queue的地址*/
static struct request_queue * create_req_queue(struct ramdisk_dev *set)
{struct request_queue *q;q = blk_alloc_queue(GFP_KERNEL);blk_queue_make_request(q, ramdisk_make_request_fn);q->queuedata = set;return q;
}/** @description	: 创建块设备,为应用层提供接口。* @set		 	: ramdisk_dev对象* @return 		: 0,表示成功;其它值为失败*/
static int create_req_gendisk(struct ramdisk_dev *set)
{struct ramdisk_dev *dev = set;/* 1、分配并初始化 gendisk */dev->gendisk = alloc_disk(RADMISK_MINOR);if(dev == NULL)return -ENOMEM;/* 2、添加(注册)disk */dev->gendisk->major = ramdisk->major; /* 主设备号 */dev->gendisk->first_minor = 0;		  /* 起始次设备号 */dev->gendisk->fops = &ramdisk_fops;	  /* 操作函数 */dev->gendisk->private_data = set;	  /* 私有数据 */dev->gendisk->queue = dev->queue;	  /* 请求队列 */sprintf(dev->gendisk->disk_name, RAMDISK_NAME); /* 名字 */set_capacity(dev->gendisk, RAMDISK_SIZE/512);	/* 设备容量(单位为扇区)*/add_disk(dev->gendisk);return 0;
}/** @description	: 驱动出口函数* @param 		: 无* @return 		: 无*/
static int __init ramdisk_init(void)
{int ret = 0;struct ramdisk_dev * dev;printk("ramdisk init\r\n");/* 1、申请内存 */dev = kzalloc(sizeof(*dev), GFP_KERNEL);if(dev == NULL) {return -ENOMEM;}dev->ramdiskbuf = kmalloc(RAMDISK_SIZE, GFP_KERNEL);if(dev->ramdiskbuf == NULL) {printk(KERN_WARNING "dev->ramdiskbuf: vmalloc failure.\n");return -ENOMEM;}ramdisk = dev;/* 2、初始化自旋锁 */spin_lock_init(&dev->lock);/* 3、注册块设备 */dev->major = register_blkdev(0, RAMDISK_NAME); /* 由系统自动分配主设备号 */if(dev->major < 0) {goto register_blkdev_fail;}/* 4、创建多队列 */dev->queue = create_req_queue(dev);if(dev->queue == NULL) {goto create_queue_fail;}/* 5、创建块设备 */ret = create_req_gendisk(dev);if(ret < 0)goto create_gendisk_fail;return 0;create_gendisk_fail:blk_cleanup_queue(dev->queue);
create_queue_fail:unregister_blkdev(dev->major, RAMDISK_NAME);
register_blkdev_fail:kfree(dev->ramdiskbuf);kfree(dev);return -ENOMEM;
}/** @description	: 驱动出口函数* @param 		: 无* @return 		: 无*/
static void __exit ramdisk_exit(void)
{printk("ramdisk exit\r\n");/* 释放gendisk */del_gendisk(ramdisk->gendisk);put_disk(ramdisk->gendisk);/* 清除请求队列 */blk_cleanup_queue(ramdisk->queue);/* 注销块设备 */unregister_blkdev(ramdisk->major, RAMDISK_NAME);/* 释放内存 */kfree(ramdisk->ramdiskbuf);kfree(ramdisk);
}module_init(ramdisk_init);
module_exit(ramdisk_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");

重点为 create_req_queue 函数,使用 blk_alloc_queue 和 blk_queue_make_request 这两个函数取代了上一种方式中 blk_mq_tag_set 结构体相关的操作。blk_alloc_queue 函数用来申请一个请求队列,blk_queue_make_request 函数设置“制造请求”函数,这里设置的制造请求函数为 ramdisk_make_request_fn,由开发人员实现。
ramdisk_make_request_fn 函数第一个参数依旧是请求队列,但是实际上这个请求队列不包含真正的请求,所有的处理内容都在第二个 bio 参数里面,所以 ramdisk_make_request_fn 函数里面是全部是对 bio 的操作。

4.测试②

非请求队列方式的驱动测试与上面的测试①没有什么区别,这里就不再赘述。


http://www.ppmy.cn/news/905122.html

相关文章

Shiro反序列化漏洞(CVE-2016-4437)+docker靶场+工具利用

一、Shiro反序列化漏洞-CVE-2016-4437原理 将java对象转换为字节序列&#xff08;json/xml&#xff09;的过程叫序列化&#xff0c;将字节序列&#xff08;json/xml&#xff09;恢复为java对象的过程称为反序列化。 Shiro框架提供了“记住我”的功能&#xff0c;用户登陆成功…

习题1.25

对吗?实践出真知,运行看看。代码如下。 (defn square [x] (* x x))(defn fast-expt[b n](println "call iter" n)(cond (= 1 n) b(= 2 n) (square b)(even? n) (square (fast-expt b (/ n 2))):else (* b (fast-expt b (- n 1)))))(defn expmod [base exp m](mod…

python贴吧顶贴_贴吧回复app-贴吧回复(贴吧顶贴神器手机版)v3.2.5-西西软件下载...

贴吧回复(贴吧顶贴神器手机版)&#xff0c;贴吧回复app是一款手机贴吧自动回复软件&#xff0c;可以设置自动回复内容以及间隔时间&#xff0c;可以设置想要回复的贴吧&#xff0c;让你成为贴吧第一回复达人。贴吧回复app推荐使用专用回帖文字诸如顶帖&#xff0c;DD&#xff0…

贴一张标签让你的手机支持NFC

NFC技术很不错&#xff0c;熟悉的人也不少&#xff0c;市场上具备NFC功能的手机却只有寥寥数款。巴克莱银行最近引进了一种名为PayTag的标签&#xff0c;贴着这个标签的任何设备都能实现NFC付款。 在此之前&#xff0c;巴克莱银行推出的借记卡和信用卡已经支持近距离无线支付。…

自制贴纸图案大全图片_贴纸的制作方法

本发明涉及装饰贴纸领域&#xff0c;尤其涉及一种贴纸。 背景技术&#xff1a; 贴纸作为包装用品中的一种&#xff0c;以其方便的印刷方式和粘贴方式受到商家的喜爱。但是在粘贴过程中&#xff0c;需要将贴纸从撕离部上分离出来。为了放置贴纸和底层单独的剥离&#xff0c;现在…

贴海报

题目描述 Bytetown城市要进行市长竞选,所有的选民可以畅所欲言地对竞选市长的候选人发表言论。为了统一管理,城市委员会为选民准备了一个张贴海报的electoral墙。 张贴规则如下: electoral墙是一个长度为N个单位的长方形,每个单位记为一个格子; 所有张贴的海报的高度必…

第十三章:Large Kernel Matters——大核心很重要,通过全局卷积网络改进语义分割

0.摘要 最近的网络架构设计趋势之一[30,31,14]是在整个网络中使用堆叠的小滤波器&#xff08;例如1x1或3x3&#xff09;&#xff0c;因为在相同的计算复杂度下&#xff0c;堆叠的小滤波器比一个大核心更高效。然而&#xff0c;在语义分割领域&#xff0c;我们需要进行密集的像素…

CSS3——手机端百度贴吧

做一个 &#xff1a; 前面有一个小logo&#xff0c;后后面有一个小三角。 <!DOCTYPE html> <html><head><meta charset"utf-8" /><title></title><style type"text/css">*{margin: 0;padding: 0;}div{width: …