【嵌入式环境下linux内核及驱动学习笔记-(4-字符驱动实例)】

news2025/1/24 5:44:34

目录

  • 1、完整的字符设备驱动的模板
  • 2、相关函数
    • 2.1 container_of()
    • 2.2 kmalloc
    • 2.3 copy_to_user()
    • 2.4 copy_from_user()
    • 2.5 系统调用open()
    • 2.6 系统调用read
    • 2.7 系统调用write
    • 2.8 系统调用close
  • 3、实例:多个同类字符设备的驱动
    • 3.1 驱动代码
    • 3.2 测试

1、完整的字符设备驱动的模板

用一个内存块做为假设的设备。字符设备驱动实现这个内存块的读写等操作。应用程序能够通过对应的设备文件实现对这个设备(内存块)的读写等操作。

2、相关函数

2.1 container_of()

已知成员的地址,要获得所在结构体变量的地址,使用这个宏: container_of(局部成员实际地址,结构体类型名,成员在结构体中的名称)

宏的定义如下:
#include <linux/kernel.h>
#define container_of(ptr, type, member) ({
const typeof(((type *)0)->member) * __mptr = (ptr);
(type *)((char *)__mptr - offsetof(type, member)); })

解释:
第一句const typeof(((type *)0)->member) * __mptr = (ptr);算出了这个局部成员变量的地址,给了_mptr。
第二句offsetof(type,member)是计算出了member在type类型 中的偏移量(假设为s个byte)
(char *)__mptr是把这个地址强转成指向单个byte的地址,为了接下来计算地址量。
接下来((char *)__mptr - offsetof(type, member))就是当前的这个局部成员的地址往回减s,就得出了局部成员变量所在的结构体的起始地址

2.2 kmalloc

#include <linux/slab.h>
​ 函数原型:void *kmalloc(size_t size, gfp_t flags);

kmalloc() 申请的内存位于直接映射区域,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移,因为存在较简单的转换关系,所以对申请的内存大小有限制,不能超过128KB。

较常用的 flags(分配内存的方法):

  • GFP_ATOMIC —— 分配内存的过程是一个原子过程,分配内存的过程不会被(高优先级进程或中断)打断;(异常上下文内用这个标志)
  • GFP_KERNEL —— 正常分配内存;(会阻塞,所以用于任务上下文)
  • GFP_DMA —— 给 DMA 控制器分配内存,需要使用该标志(DMA要求分配虚拟地址和物理地址连续)。

对应的内存释放函数为:

void kfree(const void *objp);

void *kzalloc(size_t size, gfp_t flags) // kmalloc的近亲函数,分配并设0值

2.3 copy_to_user()

#include <asm/uaccess.h>
copy_to_user()函数用于内核空间向用户空间拷贝数据。它的原型如下:
c
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
参数解释:

  • to:指向用户空间缓冲区的指针。

  • from:指向内核空间源缓冲区的指针。

  • n:要拷贝的字节数。
    返回值:

  • 返回值0:表示成功拷贝n个字节数据到内核空间。

  • 返回值大于0小于等于n:表示拷贝失败,返回值为未成功拷贝的字节数
    这个函数主要在内核驱动中被使用,当驱动向用户空间传递数据时需要调用这个函数。例如一个读函数,它从设备读取数据到内核空间缓冲区,然后调用copy_to_user()将数据拷贝给用户提供的缓冲区。
    示例:
    c
    //内核驱动代码
    ssize_t driver_read(char __user *buf, size_t count)
    {
    char kernel_buf[100];
    unsigned long ret;

    //从设备读取数据到kernel_buf

    //将数据拷贝给用户空间buf
    ret = copy_to_user(buf, kernel_buf, count);
    if (ret > 0) {
    //拷贝失败
    return -EFAULT;
    }

    return count; //成功读取count个字节
    }
    这个函数需要注意几点:

  1. to指针指向的用户空间内存区域必须由用户进程分配和释放,内核不能直接访问用户空间内存。
  2. n表示内核空间要拷贝给用户空间的最大字节数。实际拷贝的字节数可能小于n,返回值表示实际拷贝的字节数。
  3. 如果返回值大于等于n,表示拷贝失败,需要返回错误码。
  4. 拷贝失败的原因可能是to指向的地址不合法,或者在拷贝过程中出现故障。
  5. from和to所指内存区域不能重叠。

2.4 copy_from_user()

#include <asm/uaccess.h>
copy_from_user()函数用于用户空间向内核空间拷贝数据。它的原型如下:
c
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
参数解释:

  • to:指向内核空间目标缓冲区的指针。

  • from:指向用户空间源缓冲区的指针。

  • n:要拷贝的字节数。
    返回值:

  • 返回值0:表示成功拷贝n个字节数据到内核空间。

  • 返回值大于0小于等于n:表示拷贝失败,返回值为未成功拷贝的字节数
    这个函数也主要在内核驱动中被使用,当驱动需要获取用户空间数据传递到内核时需要调用这个函数。例如一个写函数,它调用copy_from_user()从用户空间获取数据,然后将数据写入设备。
    示例:
    c
    //内核驱动代码
    ssize_t driver_write(const char __user *buf, size_t count)
    {
    char kernel_buf[100];
    unsigned long ret;

    //从用户空间拷贝数据到kernel_buf
    ret = copy_from_user(kernel_buf, buf, count);
    if (ret > 0) {
    //拷贝失败
    return -EFAULT;
    }

    //将kernel_buf中的数据写入设备

    return count; //成功写入count个字节
    }
    这个函数也需要注意几点:

  1. from指针指向的用户空间内存区域必须由用户进程分配和释放,内核不能直接访问用户空间内存。
  2. n表示要从用户空间拷贝到内核空间的最大字节数。实际拷贝的字节数可能小于n,返回值表示实际拷贝的字节数。
  3. 如果返回值大于等于n,表示拷贝失败,需要返回错误码。
  4. 拷贝失败的原因可能是from指向的地址不合法,或者在拷贝过程中出现故障。
  5. from和to所指内存区域不能重叠。

2.5 系统调用open()

  1. open函数打开的文件有两种类型:
  • 普通文件:文件路径名指向的是一个普通文件,打开后可以读写等操作。
  • 特殊文件:文件路径名指向的不是一个普通文件,比如终端设备/dev/tty,管道/dev/pipe等。打开特殊文件后对它的读写代表与该设备的读写通信。
  1. flags参数表示以什么模式打开文件:
  • O_RDONLY:只读方式打开,调用open的进程可以读取该文件,但不能写入。
  • O_WRONLY:只写方式打开,调用进程可以写入该文件,但不能读取。
  • O_RDWR:读写方式打开,调用进程可以同时读写该文件。
  • O_APPEND:以追加方式打开,每次写入会追加到文件末尾。
  • O_CREAT:如果该文件不存在,就创建该文件。
  • O_EXCL:与O_CREAT配合使用,如果文件已存在,则返回错误。
  • O_TRUNC:如果文件已存在,并且以只写或读写方式打开,则先将文件长度截断为0。

  • 这些flags可以使用|操作符组合使用。
  1. mode参数仅在O_CREAT被指定时有效,它表示创建文件的访问权限。一般用0777。
  2. 成功打开返回文件描述符(大于等于0的整数),失败返回-1。
  3. 打开的文件需要调用close关闭。
    示例代码:

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

int main() {
//只读方式打开文件
int fd1 = open(“file.txt”, O_RDONLY);

//只写方式打开文件,如果文件不存在则创建
int fd2 = open("file.txt", O_WRONLY | O_CREAT, 0777);

//读写方式打开文件,如果文件已存在则先清空内容
int fd3 = open("file.txt", O_RDWR | O_TRUNC);

//读写方式打开文件,如果文件不存在则创建,且只能由调用进程打开
int fd4 = open("file.txt", O_RDWR | O_CREAT | O_EXCL, 0777);

//.........

close(fd1);
close(fd2);
//......

}

2.6 系统调用read

read函数用于从已打开的文件描述符fd读取数据。它的原型如下:

ssize_t read(int fd, void *buf, size_t count);
参数解释:

  • fd:文件描述符,由open函数返回。
  • buf:指向用户缓冲区的指针,用于存放读取的数据。
  • count:要读取的最大数据量(以字节为单位)。
    返回值:
  • 成功读取的字节数。可能小于count,如果返回0,表示已读取到文件尾。
  • -1 表示读取失败。
    示例:
    c
    #include <unistd.h>

int main() {
int fd = open(“file.txt”, O_RDONLY);
char buf[100];
ssize_t count;

//读取100个字节到buf,并获取实际读取的字节数
count = read(fd, buf, 100);
//使用buf中的数据.........

//再次读取100个字节,如果文件结尾则返回0
count = read(fd, buf, 100);
//.......

close(fd);

}
read函数在读取文件时有几点需要注意:

  1. read读取的是文件内容,不是字符串,所以读取的数据不是以’\0’结束。
  2. 读取的数据量可能小于请求的count,这时只获取实际读取的字节数。需要重复调用read直到返回0,表示读取完毕。
  3. read读取文件时,文件读取指针会动。所以下次读取会从上次结束的地方开始。可以使用lseek函数改变读取指针的位置。
  4. 对于特殊文件(管道,终端等)的读取,read获取的数据来自该特殊文件对应设备的输入。
  5. 文件描述符fd需要是有效的,对已经close的文件调用read会导致错误。

2.7 系统调用write

write函数用于向已打开的文件描述符fd写入数据。它的原型如下:
c
ssize_t write(int fd, const void *buf, size_t count);
参数解释:

  • fd:文件描述符,由open函数返回。
  • buf:指向用户缓冲区的指针,该缓冲区包含要写入的数据。
  • count:要写入的最大数据量(以字节为单位)。
    返回值:
  • 成功写入的字节数。可能小于count。
  • -1 表示写入失败。
    示例:
    c
    #include <unistd.h>

int main() {
int fd = open(“file.txt”, O_WRONLY | O_CREAT, 0777);
char *buf = “Hello World!”;
ssize_t count;

//写入12个字节到文件
count = write(fd, buf, 12);

//再次写入,如果文件不够大可能不会写入12个字节
count = write(fd, buf, 12); 

close(fd);

}
write函数在写入文件时有几点需要注意:

  1. write写入的不是字符串,所以写入的数据不会自动追加’\0’结束符。
  2. 写入的数据量可能小于请求的count,这时只会写入实际的字节数。需要重复调用write直到写入全部数据。
  3. write写入文件时,文件写入指针会动。所以下次写入会从上次结束的地方开始。可以使用lseek函数改变写入指针的位置。
  4. 对于特殊文件(管道,终端等)的写入,write提供的数据会输出到该特殊文件对应设备的输出。
  5. 文件描述符fd需要是有效的,对已经close的文件调用write会导致错误。

2.8 系统调用close

close()函数用于关闭一个已打开的文件描述符。它的原型如下:
c
#include <unistd.h>
int close(int fd);
参数fd表示要关闭的文件描述符。
返回值:

  • 成功返回0
  • 失败返回-1,并设置errno
    这个函数通常用在文件操作完成后,关闭打开的文件。例如:
    c
    int fd = open(“file.txt”, O_RDONLY);
    // do something with fd
    close(fd);
    close()函数需要注意以下几点:
  1. fd必须是一个有效的文件描述符,否则close()将失败并设置errno为EBADF。
  2. 每个成功的open()或dup()调用都需要一个对应的close()来关闭文件。如果忘记关闭文件描述符,将导致资源泄露。
  3. close()成功调用后,fd不再引用 opened 文件的任何资源,并可在后续的open()调用中重新使用。
  4. 关闭 stdin、stdout 或 stderr 流的后果是未定义的。这三个流在程序开始时由内核自动打开,并在程序结束时自动关闭。
  5. close()可以在不关闭fd的情况下被重复调用,这种情况下将不进行任何操作并返回成功。
  6. 除非将fd显式声明为int型,否则调用close()时fd的类型需要与open()中的类型相同。
  7. 在进程终止时,所有未关闭的描述符都被自动关闭。但应该养成明确关闭不再需要的文件描述符的好习惯。
  8. close()可能会销毁文件打开时指定的任何记录锁。关闭文件描述符后,任何涉及该描述符的记录锁都变得无效。
  9. 如果文件是以O_APPEND模式打开的,close()将会丢弃追加模式,并将文件描述符的文件位置指针重置为文件开始。

3、实例:多个同类字符设备的驱动

本实例用于一次驱动同种类型的多个字符型设备(主设备号相同,次设备号不同)。以一个内存缓存虚拟一个字符型设备,驱动模块实现对内存缓存虚拟设备的读写操作。

3.1 驱动代码

/*************************************************************************
	> File Name: muti-oper-mem.c
    用一个内存池来虚拟设备,驱动模块完成对内存池的读与写。应用层程序通过系统调用open
    read write操作设备muti0,muti1,muti2。

    本例用于驱动同一类设备的不同子设备。主设备号相同,次设备号不同。
    与单一设备驱动不同之处在以下几点:
    1、mem设备要对应有多个
    2、cdev设备对象一一对应
    3、devno统一用alloc_chrdev_region()生成
    4、每一个cdev设备都要cdev_init和cdev_add
    4、每一个cdev都要cdev_del()
 ************************************************************************/

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <asm/uaccess.h>


/*1、定义重要的变量及结构体*/

#define  MEM_SIZE 500    //
#define  DEVICE_NUM 3

dev_t devno;
struct mem_dev_t{
    struct cdev  my_dev;  //cdev设备描述结构体变量
    char  mem[MEM_SIZE]; //内存池,当成虚拟设备

};

/*这个mem_dev_t指针类型,可以指向数组,在这里是指向三个元素的首元素的地址。*/
struct mem_dev_t *mem_dev;  


/*所有驱动函数声明*/
loff_t llseek (struct file *, loff_t, int);
ssize_t read (struct file *, char __user *, size_t, loff_t *);
ssize_t write (struct file *, const char __user *, size_t, loff_t *);
ssize_t aio_read (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t aio_write (struct kiocb *, const struct iovec *, unsigned long, loff_t);
int iterate (struct file *, struct dir_context *);
unsigned int poll (struct file *, struct poll_table_struct *);
long unlocked_ioctl (struct file *, unsigned int, unsigned long);
long compat_ioctl (struct file *, unsigned int, unsigned long);
int mmap (struct file *, struct vm_area_struct *);
int open (struct inode *, struct file *);
int flush (struct file *, fl_owner_t id);
int release (struct inode *, struct file *);
int fsync (struct file *, loff_t, loff_t, int datasync);
int aio_fsync (struct kiocb *, int datasync);
int fasync (int, struct file *, int);
int lock (struct file *, int, struct file_lock *);
ssize_t sendpage (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long get_unmapped_area(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int check_flags(int);
int flock (struct file *, int, struct file_lock *);
ssize_t splice_write(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t splice_read(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int setlease(struct file *, long, struct file_lock **);
long fallocate(struct file *file, int mode, loff_t offset,  loff_t len);
int show_fdinfo(struct seq_file *m, struct file *f);

//驱动操作函数结构体,成员函数为需要实现的设备操作函数指针
//简单版的模版里,只写了open与release两个操作函数。
struct file_operations fops={
    .open = open,
    .release = release,
    .read = read,
    .write = write,
};

/*3、初始化 cdev结构体的函数,并将cdev结构体与file_operations结构体关联起来*/
/*这样在内核中就有了设备描述的结构体cdev,以及设备操作函数的调用集合file_operations结构体*/
//static int cdev_setup(struct mem_dev_t *mem_dev  ){
static int cdev_setup(struct mem_dev_t *mem_dev , int num ){
    int unsucc = 0;
    dev_t sdevno = MKDEV(MAJOR(devno),num);
    cdev_init(&mem_dev->my_dev , &fops);

    mem_dev->my_dev.owner = THIS_MODULE;

    /*4、注册cdev结构体到内核链表中*/
    unsucc = cdev_add(&mem_dev->my_dev , sdevno , 1);
    if (unsucc){
        printk("cdev [%d] add failed \n",num);
        return -1;
    }
    printk("the major of this devno is %d\n",MAJOR(sdevno));
    printk("the minor of this devno is %d\n",MINOR(sdevno));


    return 0;

}

static int __init my_init(void){
    int major ;
    int i =0;
    int unsucc =0;
    //mem_dev = kzalloc(sizeof(struct mem_dev_t) , GFP_KERNEL);
    mem_dev = kzalloc(sizeof(struct mem_dev_t) * DEVICE_NUM , GFP_KERNEL);
    if (!mem_dev){
        printk(" allocating memory   failed");
        return  -1;
    }

    /*2、创建 devno */
    //unsucc = alloc_chrdev_region(&mem_dev->devno , 0 , 1 , "operate_memory");
    unsucc = alloc_chrdev_region(&devno , 0 , DEVICE_NUM , "operate_memory");
    if (unsucc){
        printk(" creating devno    failed\n");
        return -1;
    }else{

        major = MAJOR(devno);
        printk("make devno,major = %d  ; \n",major);
    }
    /*3、 分别初始化3个cdev结构体,分别关联cdev结构体与file_operations.*/
    /*4、注册cdev结构体到内核链表中*/
    for ( i = 0 ; i < DEVICE_NUM ; i++){
        if (cdev_setup(mem_dev+i , i ) == 0){
            printk("the driver operate_memory  initalization is complete\n");
               
        } else {
            printk("the driver operate_memory initalization failed\n");
            return -1;
        }
    }
    return 0;
}


static void  __exit my_exit(void)
{
    int i=0;
    for ( i = 0; i < DEVICE_NUM; i++){
        cdev_del(&((mem_dev+i)->my_dev));
    }
    unregister_chrdev_region(devno , DEVICE_NUM);
    printk("***************the driver operate_memory exit************\n");
}


/*5、驱动函数的实现*/
/*file_operations结构全成员函数.open的具体实现*/

int open(struct inode *pnode , struct file *pf){
    struct mem_dev_t *p = container_of(pnode->i_cdev, struct mem_dev_t , my_dev);
    pf->private_data = p;  //把全局变量指针放入到struct file结构体里
    printk("operate_memory is opened\n");
    return 0;


}
/*file_operations结构全成员函数.release的具体实现*/
int release(struct inode *pnode , struct file *pf){
    printk("operate_memory is closed \n");
    return 0;
}
    

/*file_operations结构全成员函数.read的具体实现*/
ssize_t read (struct file * pf, char __user * buf, size_t size , loff_t * ppos){
    struct mem_dev_t *pdev = pf->private_data;
    int count = 0;
    //判断偏移量的有效性
    if (*ppos >= MEM_SIZE){
        return 0;
    }
    //判断能够读到的字节数量
    if  (size > MEM_SIZE - *ppos){
        count = MEM_SIZE - *ppos;
    }else{
        count = size;
    }

    //copy_from_user返回值大于0失败
    if ( copy_to_user(buf , &pdev->mem[*ppos] , count )){
        return 0;
    }else{
        *ppos += count;
        return count;
    }
        
}

/*file_operations结构全成员函数.write的具体实现*/
ssize_t write (struct file * pf, const char __user *buf, size_t size , loff_t *ppos){
    struct mem_dev_t *pdev = pf->private_data;
    int count = 0;
    //判断偏移量的有效性
    if (*ppos >=MEM_SIZE ){
        return 0;
    }
    //判断能够写入的字节数量
    if (size > MEM_SIZE-*ppos){
        count = MEM_SIZE-*ppos;
    }else{
        count = size;
    }
    //copy_from_user返回值大于0失败
    if ( copy_from_user(&pdev->mem[*ppos] , buf , count)){
        return 0;
    }else{
        *ppos +=count;
        return count;
    }

}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("");

以上代码可以做为模版使用。针对一个或多个同类设备来开发类似驱动时,可以使用。

3.2 测试

对地以上这个驱动,不论要使用几个设备,都只需要加载一次该驱动即可。对应不同的设备,需要创建不同的多个设备文件。如本例是3个同类设备。因此就要手动创建3个设备文件(mknod),主设备号一致,次设备号从0开始到2。

使用时,可以用系统命令“> ” 、cat等操作设备文件,也可以写一段应用程序通过系统调用open() 、read()、write()等。具体见下方操作。
一、加载驱动

insmod /drv/muti-oper-memory.ko

二、手动生成设备文件

mknod /dev/muti0 c 251 0
mknod /dev/muti1 c 251 1
mknod /dev/muti2 c 251 2

三、测试

echo “hello /dev/muti0” > /dev/muti0
cat /dev/muti0
在这里插入图片描述

四、应用层测试程序用例

/*************************************************************************
	> File Name: muti_op_mem.c
    对应驱动的应用层测试程序。调用open read write 等 来测试驱动的运行
    分别打开三个驱动进行读写,测试是否正常 

    执行:./muei_op_mem.elf /dev/muti0 /dev/muti1 dev/muti2
************************************************************************/

#include<stdio.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>

#define DEVICE_NUM 3

int  main(int argc , char **argv){

    int fd[DEVICE_NUM] = {0};
    int size = 0;
    char buf[20] = {0};
    char * mesg[3] ={"this is test0\n" , "this is test1\n" , "this is test2\n"};
    int pos = 0;
    int i=0;

    if  (argc < DEVICE_NUM+1){
        printf("argument is  less!\n");
        return 0;
    }
    //向设备写入数据
    for (i = 0; i < DEVICE_NUM; i++){
        printf("open file %s\n",argv[i+1]);
        fd[i] = open(argv[i+1] , O_RDWR|O_APPEND  );
        if (fd[i] < 0){
            perror("open ");
        }

        size = write(fd[i] ,mesg[i] , strlen(mesg[i]));
        printf("write device[%d] size = %d\n",i,size);
        if (size == strlen(mesg[i])){
            printf("write to device[i] is sruccessul\n",i);
        }else{
            printf("write to device[i] is failed\n",i);
        }
    
        sleep(1);
        //printf("sleep is end \n");
        close(fd[i]);
    
        //从设备读出数据
        fd[i] = open(argv[i+1] , O_RDONLY);
        if (fd[i] < 0){
            perror("open");
        }

        size = read(fd[i] , buf , strlen(mesg[i]));
        if (size > 0){
            printf("read data of device[%d] is : %s\n" ,i, buf);
        }else{
            printf("read data of device[%d]  failed\n",i);
        }

        sleep(1);
        //printf("sleep is end \n");
        close(fd[i]);
    }





    return 0;

}

输出结果如下:

在这里插入图片描述

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

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

相关文章

【C语言】初阶指针(指针及其类型以及野指针)

简单不先于复杂&#xff0c;而是在复杂之后。 目录 1. 指针是什么&#xff1f; 2. 指针和指针类型 2.1 指针-整数 2.2 指针的解引用 3. 野指针 3.1 野指针成因 3.2 如何规避野指针 1. 指针是什么&#xff1f; 指针理解的两个要点&#xff1a; 1. 指针是内存中最小…

HCLE--虚拟机安装ntp等相关服务出现报错解决和通过SSH实现免密登录

.1 在服务端主机上安装ntp服务&#xff1a; yum install ntp 1.2 修改/etc/ntp.conf文件&#xff0c;将默认的ntp服务器地址替换为可用的ntp服务器地址。例如&#xff1a; server ntp1.aliyun.com iburst server ntp2.aliyun.com iburst server ntp3.aliyun.com iburst 1…

JSP-JDBC-设计一个简单的网上名片管理系统

需求&#xff1a; 1&#xff0e;设计一个简单的网上名片管理系统&#xff0c;实现名片的增、删、改、查等操作。该名片管理系统包括如下功能&#xff1a; &#xff08;1&#xff09;用户登录与注册 用户登录&#xff1a;在登录时&#xff0c;如果用户名和密码正确&#xff0…

举一反三学python(11)—excel实例

一、乘法表写入EXCEL 想必大家对九九乘法表的代码记忆尤新&#xff1a;for i in range(1,10): # 9行 for j in range(1,i1): # 列数是动态变化的&#xff0c;是1-9列 print(f{j}x{i}{j*i},end\t) # 格式化输出并按制表位…

8脚语音芯片有什么特点?

一、8脚语音芯片指的是什么&#xff1f; 8脚语音芯片&#xff0c;顾名思义就是拥有8个脚的一颗芯片&#xff0c;8脚其实是指8引脚&#xff0c;而引脚又被叫做管脚。引脚就是指从集成电路&#xff08;芯片&#xff09;内部电路引出与外围电路的接线&#xff0c;引脚构成了这块芯…

如何使用YOLOv5的pycocotools进行coco指标评估

使用YOLOv5进行coco指标评估 1. 安装pycocotools2.重新排序并命名3. 将txt标签转换为json格式格式2. 生成json3. 测试是否正确4. 运行val.py完整版本&#xff08;一次运行所有&#xff09; 1. 安装pycocotools pip install pycocotools -i https:pypi.douban.com/simple2.重新…

HTML5 <meter> 标签、HTML5 <mark> 标签

HTML5 <meter> 标签 实例 使用 meter 元素展示给定的数据范围&#xff1a; <meter value"2" min"0" max"10">2 out of 10</meter><br> <meter value"0.6">60%</meter>尝试一下 浏览器支持 Fir…

【Python知识】2个特别好用的python模块(请收藏!)

文章目录 前言一、介绍二、FuzzyWuzzy库介绍2.1 fuzz模块2.2 简单匹配&#xff08;Ratio&#xff09;2.3 非完全匹配&#xff08;Partial Ratio&#xff09;2.3 忽略顺序匹配&#xff08;Token Sort Ratio&#xff09;2.4 去重子集匹配&#xff08;Token Set Ratio&#xff09;…

matlab数据归一化与反归一化处理

假如数据实际取值范围为 X i ∈ [ − π π ] , i 1 , 2 , 3 X_i \in [-\pi \ \ \pi], i1,2,3 Xi​∈[−π π],i1,2,3&#xff0c;变量服从正态分布 示例如下&#xff1a; %% 数据归一化处理及其概率密度函数 clear clc Mu [0 0 0]; % 均值 Sigma [1 1 1]; % 标准差 C…

EA使用教程

文章目录 创建新工程属性设置导出图片到剪切板时序图中取消消息后面自动生成的括号在文本框中回车取消流程图的背景渐变导出更清晰图片 创建新工程 1. 点击 FILE -> New Project 开始创建新工程 2. 为新工程命名 3. 选择模型 以下为常用设计模型&#xff1a; Business …

Java并发工具合集JUC大爆发

1. CountDownLatch CountDownLatch是一个同步计数器&#xff0c;初始化的时候 传入需要计数的线程等待数&#xff0c;可以是需要等待执行完成的线程数&#xff0c;或者大于 &#xff0c;一般称为发令枪。\ ​ countdownlatch 是一个同步类工具&#xff0c;不涉及锁定&#xff0…

我实现了一个乞丐版的评论功能

文章目录 设计评论功能0 设计初衷1 前端组建设计**设计原则****设计代码**组件核心代码**调用组建并给出mock数据****效果** 2 后端数据库设计3 后端接口设计4 前后端联调5 后端评论保存接口设计6 前端评论填写流程设计7 联调8 验证码美化 设计评论功能 0 设计初衷 经过长达八…

三百左右的蓝牙耳机哪个音质好?三百左右音质最好的蓝牙耳机推荐

在外出携带的数码产品中&#xff0c;蓝牙耳机的出现频率居高不下&#xff0c;一部手机&#xff0c;一副耳机已经成为不少人外出的标配。蓝牙耳机无外乎是用来听的&#xff0c;下面&#xff0c;我来给大家推荐几款三百左右音质好的蓝牙耳机&#xff0c;一起来看看吧。 一、南卡…

LabVIEW-字符串与路径控件

在前面板中字符串与路径控件位于下图所示位置&#xff1a; 字符串输入和显示功能&#xff0c;是用户最常用的基本操作功能单击字符串控件&#xff0c;鼠标右键&#xff0c;选择“属性”可以对字符串控件的外观进行设置。显示样式有四种方式&#xff0c;即正常、反斜杠符号、密码…

家用洗地机好用吗?好用的洗地机分享

洗地机是一种高效、节能、环保的清洁设备&#xff0c;广泛应用于各种场所的地面清洁工作。它不仅可以快速清洁地面&#xff0c;还可以有效去除污渍、油渍等难以清洁的污染物&#xff0c;让地面恢复光洁如新的状态。同时&#xff0c;洗地机还可以减少清洁人员的劳动强度&#xf…

研读Rust圣经解析——Rust learn-10(泛型,trait,生命周期)

研读Rust圣经解析——Rust learn-10&#xff08;泛型&#xff0c;trait&#xff0c;生命周期&#xff09; 泛型应用泛型方法泛型结构体枚举泛型方法定义中的泛型 trait定义一个trait默认trait方法实现为结构体实现trait调用trait中实现的方法将trait作为参数trait bound多实现入…

2023年6月CDGP数据治理专家认证考试火热报名中

DAMA认证为数据管理专业人士提供职业目标晋升规划&#xff0c;彰显了职业发展里程碑及发展阶梯定义&#xff0c;帮助数据管理从业人士获得企业数字化转型战略下的必备职业能力&#xff0c;促进开展工作实践应用及实际问题解决&#xff0c;形成企业所需的新数字经济下的核心职业…

数据结构—单链表

目录 1.前言 2.了解单链表 3.单链表代码实现 3.1 单链表结构体实现 3.2 创建节点 3.3 打印单链表 3.4 尾插 3.5 头插 3. 6 头删 3.7 尾删 3.8 查找 3.9 插入 3.9.1 在pos位置之前插入 3.9.2 在pos位置之后插入&#xff08;主要使用这种功能&#xff09;---不需要找…

家用洗地机到底好不好用?家用洗地机分享

在当今社会&#xff0c;人们越来越关注卫生和清洁&#xff0c;这也促进了家庭和工作场所对清洁设备的需求。洗地机就是其中之一&#xff0c;它的高效和便捷性为我们提供了清洁和保洁的重要帮助。使用洗地机不仅能够卫生地保持地面清洁&#xff0c;而且可以节省时间和人力成本。…

拼多多的天天618,如何掀开电商营销的“皇帝新衣”?

电商价格战如火如荼&#xff0c;拼多多也在2023年4月正式启动“数码家电消费季”百亿补贴。 首季将在百亿补贴的基础上加码10亿&#xff0c;对手机、平板等各种数码家电&#xff0c;提供全品类补贴&#xff0c;苹果、华为、小米、美的等国内外各大品牌均会参与。拼多多相关负责…