ARM——驱动——inmod加载内核模块

news2024/9/25 9:38:24

在上一篇文章的代码上添加出错处理

#include <linux/init.h>       // 包含初始化宏和函数  
#include <linux/kernel.h>     // 包含内核函数和变量  
#include <linux/fs.h>         // 包含文件操作的结构和函数  
#include <linux/kdev_t.h>     // 旧的设备号定义,现在通常包含在<linux/kdev_t.h>或不需要直接包含  
#include <linux/cdev.h>       // 包含字符设备操作的结构和函数  
#include <linux/module.h>     // 包含模块操作的宏和函数  
  
// 定义主设备号和次设备号  
#define MAJOR_NUM 255  
#define MINOR_NUM 0  
// 定义设备名称  
#define DEV_NAME "demo"  
// 定义要注册的设备数量(这里为1,因为次设备号固定为0)  
#define DEV_NUM 1  
  
// 文件打开操作  
static int open (struct inode * inode, struct file * file)  
{  
    printk("demo open ...\n");  
    return 0; // 成功返回0  
}  
  
// 文件读取操作  
static ssize_t read (struct file * file, char __user * buf, size_t len, loff_t * offset)  
{  
    printk("demo read ...\n");  
    return 0; // 这里仅作为示例,实际应该返回读取的字节数  
}  
  
// 文件写入操作  
static ssize_t write (struct file * file, const char __user * buf, size_t len, loff_t * offset)  
{  
    printk("demo write ...\n");  
    return 0; // 这里仅作为示例,实际应该返回写入的字节数  
}  
  
// 文件关闭操作,注意这里使用了.release而不是.close,因为.close在Linux 2.6.33之后被弃用  
static int close (struct inode * inode, struct file * file)  
{  
    printk("demo close ...\n");  
    return 0; // 成功返回0  
}  
  
// 文件操作结构  
static struct file_operations fops =   
{  
    .owner = THIS_MODULE,  // 指向拥有该结构的模块  
    .open = open,          // 打开操作  
    .read = read,          // 读取操作  
    .write = write,        // 写入操作  
    .release = close       // 关闭操作(注意使用.release)  
};  
  
// 字符设备结构体  
static struct cdev cdev;  
// 设备号  
static dev_t dev;  
  
// 模块初始化函数  
static int __init demo_init(void)  
{  
    int ret = 0;  
    dev = MKDEV(MAJOR_NUM, MINOR_NUM); // 创建设备号  
  
    cdev_init(&cdev, &fops); // 初始化字符设备  
  
    // 添加字符设备到系统  
    ret = cdev_add(&cdev, dev, DEV_NUM);  
    if(ret < 0)  
        goto err_cdev_add;  
  
    // 注册设备号到系统,但这一步实际上是多余的,因为我们已经手动指定了主设备号  
    // 并且通常在调用cdev_add之前不需要调用register_chrdev_region  
    // 除非你不确定主设备号是否已经被占用,或者你想要动态分配一个主设备号  
    ret = register_chrdev_region(dev, DEV_NUM, DEV_NAME);  
    if(ret < 0)  
        goto err_register_chrdev_region;  
  
    printk("demo_init  ###################\n");  
  
    return ret; // 这里应该返回0表示成功,但由于之前的错误处理,这里实际上不会执行  
  
err_cdev_add:  
    cdev_del(&cdev); // 如果cdev_add失败,则删除字符设备  
    printk("demo cdev_add failed\n");  
    return ret; // 返回错误码  
  
err_register_chrdev_region:  
    unregister_chrdev_region(dev, DEV_NUM); // 如果register_chrdev_region失败,则注销设备号  
    cdev_del(&cdev); // 删除字符设备  
    printk("demo register_chrdev_region failed\n"); // 注意这里修正了打印信息  
    return ret; // 返回错误码  
}  
  
// 模块清理函数  
static void __exit demo_exit(void)  
{  
    unregister_chrdev_region(dev, DEV_NUM); // 注销设备号  
    cdev_del(&cdev); // 删除字符设备  
    printk("demo_exit  ################

 一、驱动模块的加载

静态 编译到内核
这种是必须要加载到内核中的驱动,或者特别特别常用的驱动,只要开机就启动。

动态 编译到模块中

开机以后自己安装的驱动 开机后加载,试一下功能,不行就卸载掉,省区每次都要开机的情况。

1、在menu中将选项选成M(M即编译成一个模块)

但如果想要编译成模块 ,
之前不能<*>即,原来的内核中就不能加载过,
如果加载过在使用模块加载,会导致重复加载。

所以:需要先不选中再把uImage复制到根文件系统中。

make出一个uImage

再拷贝到根目录文件中。

2、选中<M>后,make modules  对应模块选项配置位M

将这个文件考到根目录文件中(也可以直接开发板挂载)

3、加载驱动模块

(1)用命令

insmod demo.ko    //动态加载驱动模块

insmod命令用于将指定的模块文件(通常是.ko文件)加载到Linux内核中。这使得用户可以在不重启系统的情况下,向内核添加新的功能或驱动。

关于GPL

GPL许可证的主要目标是保证软件对用户是自由的,即用户可以自由地运行、复制、分发、研究、改变和改进软件。同时,它要求软件必须以源代码的形式发布,以便其他人可以在此基础上进行修改和再发布。

任何遵循GPL许可发布的软件,其源代码都是开放的,任何人在任何时间都可以获取、修改和重新发布,但必须保证修改后的软件也遵循GPL许可。

GPL许可证有两个版本:GPL v2和G

删除原先的mod再insmod即可

(2)查看已经动态加载的驱动模块 

lsmod 

(3)卸载驱动模块

rmmod

4、设置设备节点,运行程序

运行app程序即再/home/Linux/tftpboot中写一个app程序

运行成功。

二、写一个led驱动加载模块

(1)ioremap

void __iomem *ioremap(phys_addr_t offset, unsigned long size);

ioremap 函数是用来将物理内存地址映射到内核的虚拟地址空间上,使得内核可以像访问普通内存一样访问这部分物理内存。

用这个函数在    static int __init led_init(void)  将物理地址映射到虚拟地址上,使内核可以访问地址。

(2)iounmap解除映射函数

void iounmap(void __iomem *addr);

 

在Linux内核中,ioremap 函数是用来将物理内存地址映射到内核的虚拟地址空间上,使得内核可以像访问普通内存一样访问这部分物理内存。这在设备驱动开发中尤其常见,因为许多硬件设备都需要通过特定的物理地址来访问其寄存器或内存区域

在 static void __exit led_exit(void)中用这个函数来解除寄存器地址与内核的映射

(3)声明寄存器的地址中添加寄存器的地址

(5)在/drivers/char/Makefile 中加入  命令

(5)/drivers/char/Kconfig中增加选项

(6)进入开发板按照一中运行。

(7)源代码

驱动程序

led.c

#include<linux/init.h>
#include<linux/kernel.h>
#include<linux/fs.h>
#include<linux/kdev_t.h>
#include<linux/cdev.h>
#include<linux/module.h>
#include <linux/io.h>


#define MAJOR_NUM 256
#define MINOR_NUM 0
#define DEV_NAME "led"
#define DEV_NUM 1

#define GPBCON 0x56000010
#define GPBDAT 0x56000014

static volatile unsigned long * gpbcon;
static volatile unsigned long * gpbdat;

void init_led(void)						
{

	// 配置GPB5引脚功能为输出
	*gpbcon &= ~(3 << 10);
	*gpbcon |= (1 << 10);

	// 将GPB5引脚电平置高
	*gpbdat |= (1 << 5);
}

void led_on(void)
{
	// 将GPB5引脚电平置低
	*gpbdat &= ~(1 << 5);
printk("led_on!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n");

}

void led_off(void)
{
	// 将GPB5引脚电平置高
	*gpbdat |= (1 << 5);
printk("led_off!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n");
}


static int open(struct inode *inode,struct file *file)
{
	init_led();
	printk("led open..\n");
	return 0;
}

static ssize_t read (struct file * file, char __user * buf, size_t len, loff_t * offset)
{
	printk("led read ...\n");
	return 0;
}

static ssize_t write (struct file * file, const char __user * buf, size_t len, loff_t * offset)
{

	if(strcmp(buf,"led_on")==0)
	{
		led_on();
	}
	if(strcmp(buf,"led_off")==0)
	{
		led_off();
	}

	printk("led write ...\n");
	return 0;
}

static int close (struct inode * inode, struct file * file)
{
	printk("led close ...\n");
	return 0;
}

static struct file_operations fops=
{
	.owner = THIS_MODULE,
	.open  =  open,
	.read  =  read,
	.write =  write,
	.release = close
};

static struct cdev cdev;
static dev_t dev;

static int __init led_init(void)
{

	int ret = 0;
	dev=MKDEV(MAJOR_NUM,MINOR_NUM);

	cdev_init(&cdev,&fops);
	
	ret =cdev_add(&cdev,dev,DEV_NUM);
	if(ret<0)
	{
		goto err_cdev_add;
	}
	ret = register_chrdev_region(dev,DEV_NUM,DEV_NAME);
	if(ret<0)
	{
		goto err_register_chrdev_region;
	}
	printk("led_init ##########################\n");

	
	gpbcon=ioremap(GPBCON,sizeof(*gpbcon));
	gpbdat=ioremap(GPBDAT,sizeof(*gpbdat));
	
	return ret;

err_cdev_add:
	cdev_del(&cdev);
	printk("led cdev_add failed\n");
	return ret;
err_register_chrdev_region:
	unregister_chrdev_region(dev,DEV_NUM);
	cdev_del(&cdev);
	printk("led resgister_chrdev_region\n");
	return ret;
}


static void __exit led_exit(void)
{

	iounmap(gpbcon);
	iounmap(gpbdat);

	unregister_chrdev_region(dev,DEV_NUM);
	cdev_del(&cdev);
	printk("led_exit #########################\n");
}



module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");


led_app.c

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

int main(int argc, const char *argv[]) {
    int fd = open("/dev/led", O_RDWR);
    if (fd < 0) {
        perror("open led");
        return -1;
    }

	while(1)
	{
		write(fd,"led_on",strlen("led_on"));
			sleep(2);

		write(fd,"led_off",strlen("led_off"));
			sleep(2);


	}
    close(fd);
    return 0;
}

(7)其他注意事项!!!!

但这里不能用strcpy和memcpy,因为是从内村空间往内核空间拷贝函数

用一个函数   copy_from_user()

copy_from_user() 是 Linux 内核中用于从用户空间安全地复制数据到内核空间的一个函数。这个函数是内核提供的一种机制,用于在用户程序和内核之间传递数据,但确保了内核在访问用户空间内存时不会引发错误(如访问违规或段错误)。

函数原型

unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);

  • to:指向内核空间中的缓冲区的指针,数据将被复制到这个缓冲区。
  • from:指向用户空间中的数据的指针。注意,这个指针被标记为 __user,这是一个特殊的类型,用于指示该指针指向用户空间。
  • n:要复制的字节数。

这里的字节数不能乱写,所以定义一个len_cp看谁小就拷贝谁,保证谁的空间都不会超过

修正好的源代码如下

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/module.h>
#include <asm/io.h>
#include <asm/string.h>
#include <asm/uaccess.h>

#define MAJOR_NUM 253
#define MINOR_NUM 0
#define DEV_NAME "led"
#define DEV_NUM 1
#define GPBCON 0x56000010
#define GPBDAT 0x56000014

static volatile unsigned long * gpbcon;
static volatile unsigned long * gpbdat;

static void init_led(void)						
{
	// 配置GPB5引脚功能为输出
	*gpbcon &= ~(3 << 10);
	*gpbcon |= (1 << 10);

	// 将GPB5引脚电平置高
	*gpbdat |= (1 << 5);
}

static void led_on(void)
{
	// 将GPB5引脚电平置低
	*gpbdat &= ~(1 << 5);
}

static void led_off(void)
{
	// 将GPB5引脚电平置高
	*gpbdat |= (1 << 5);
}

static int open (struct inode * inode, struct file * file)
{
	init_led();
	printk("led open ...\n");
	return 0;
}

static ssize_t read (struct file * file, char __user * buf, size_t len, loff_t * offset)
{
	//copy_to_user(buf, data, len);
	printk("led read ...\n");
	return 0;
}

static ssize_t write (struct file * file, const char __user * buf, size_t len, loff_t * offset)
{
	unsigned char data[12] = {0};
	size_t len_cp = sizeof(data) < len ? sizeof(data) : len;
	copy_from_user(data, buf, len_cp);

	if(!strcmp(data, "ledon"))
		led_on();
	else if(!strcmp(data, "ledoff"))
		led_off();
	else
		 return -1;

	printk("led write ...\n");
	return len_cp;
}

static int close (struct inode * inode, struct file * file)
{
	printk("led close ...\n");
	return 0;
}

static struct file_operations fops = 
{
	.owner = THIS_MODULE,
	.open = open,
	.read = read,
	.write = write,
	.release = close
};
static struct cdev cdev;
static dev_t dev;

static int __init led_init(void)
{
	int ret = 0;
	dev = MKDEV(MAJOR_NUM, MINOR_NUM);

	cdev_init(&cdev, &fops);

	ret = cdev_add(&cdev, dev, DEV_NUM);
	if(ret < 0)
		goto err_cdev_add;

	ret = register_chrdev_region(dev, DEV_NUM, DEV_NAME);
	if(ret < 0)
		goto err_register_chrdev_region;

	gpbcon = ioremap(GPBCON, sizeof(*gpbcon));
	gpbdat = ioremap(GPBDAT, sizeof(*gpbdat));

	printk("led_init  ...\n");

	return ret;

err_cdev_add:
	cdev_del(&cdev);
	printk("led cdev_add failed\n");
	return ret;

err_register_chrdev_region:
	unregister_chrdev_region(dev, DEV_NUM);
	cdev_del(&cdev);
	printk("led register_chrdev_region\n");	
	return ret;
}

static void __exit led_exit(void)
{
	iounmap(gpbcon);
	iounmap(gpbdat);
	unregister_chrdev_region(dev, DEV_NUM);
	cdev_del(&cdev);
	printk("led_exit  ###############################\n");
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

三、自动创建设备节点

(1)关于设备

①字符设备(数据的访问是有顺序的)

定义
字符设备是指在I/O传输过程中以字符为单位进行传输的设备。这类设备在数据传输时,数据被视为连续的字符流,逐个字符地进行读写操作。

特点

  • 数据传输单位:以字符为单位,即字节流的形式进行数据传输。
  • 缓冲区:通常没有内部缓冲区,数据会立即传输到设备或从设备中读取,这意味着字符设备的读写操作是实时的。
  • 访问模式:通常是顺序访问,即按照数据的顺序进行读写操作,不支持随机访问。
  • 应用场景:常用于与用户进行交互的设备,如键盘、鼠标、串口等,以及某些需要按字符处理的设备。

在操作系统中的表示
在UNIX或Linux系统中,字符设备以特别文件方式在文件目录树中占据位置并拥有相应的结点。这些设备文件可以使用与普通文件相同的文件操作命令进行打开、关闭、读、写等操作。

②块设备(数据访问是随机的)

定义
块设备是I/O设备中的一类,它将信息存储在固定大小的块中,每个块都有自己的地址。这类设备允许在设备的任意位置读取一定长度的数据。

特点

  • 数据传输单位:以固定大小的块为单位进行数据传输,数据块的大小通常在512字节到32768字节之间。
  • 缓冲区:通常具有内部缓冲区,用于在访问硬件之前缓存数据,以提高数据传输效率。
  • 访问模式:支持随机访问,即可以按块为单位在设备的任意位置进行读写操作。
  • 应用场景:常用于需要大量数据存储和读写操作的场景,如硬盘、U盘、SD卡等。

在操作系统中的表示
在操作系统中,块设备通过块设备驱动程序来管理和控制其读写操作。这些驱动程序负责提供访问设备的接口和功能,并处理设备的读写请求。

③网络设备(以套接字形式存在,只有名字,会集成负载的网络协议)

定义
网络设备是用来将各类服务器、PC、应用终端等节点相互连接,构成信息通信网络的专用硬件设备。

类型
网络设备包括信息网络设备、通信网络设备、网络安全设备等,具体如交换机、路由器、防火墙、网桥、集线器、网关、VPN服务器、网络接口卡(NIC)、无线接入点(WAP)、调制解调器、5G基站、光端机、光纤收发器等。

作用
网络设备的主要作用是构建网络,实现不同节点之间的信息通信和数据传输。它们通过物理连接和逻辑协议,将各个独立的计算设备连接成一个整体的网络系统。

综上所述,字符设备、块设备和网络设备在数据传输方式、应用场景和操作系统中的作用等方面存在显著差异。每种设备都有其独特的特点和用途,共同构成了现代计算机系统和网络的基础。


为什么我们的驱动需要手动创建节点呢

下一篇文件介绍自动设置节点。欢迎批评指正

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

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

相关文章

记忆化搜索与状态压缩:优化递归与动态规划的利器

记忆化搜索是解决递归和动态规划问题的一种高效优化技术。它结合了递归的灵活性和动态规划的缓存思想&#xff0c;通过保存已经计算过的子问题结果&#xff0c;避免了重复计算&#xff0c;大幅提升了算法的效率。当问题状态复杂时&#xff0c;状态压缩技术可以进一步优化空间使…

密码生成器(HTML+CSS+JavaScript)

&#x1f30f;个人博客主页&#xff1a;心.c ​ 前言&#xff1a;前两天写了密码生成器&#xff0c;现在跟大家分享一下&#xff0c;大家如果想使用随便拿&#xff0c;如果哪里有问题还请大佬们给我指出&#xff0c;感谢支持 &#x1f525;&#x1f525;&#x1f525;专题文章&…

《断点回归的非参数估计及 Stata 实现》

目录 一、引言 二、文献综述 三、理论原理 四、实证模型 五、稳健性检验 六、程序代码及解释 七、代码运行结果及解释 一、引言 断点回归&#xff08;Regression Discontinuity&#xff0c;RD&#xff09;设计是一种准实验方法&#xff0c;用于评估政策或干预措施的因果…

鉴权Cookies、授权session、token

2 鉴权、授权 区别&#xff1a;一个存在浏览器&#xff0c;一个存在服务器&#xff0c;session存到服务端当中 问1&#xff1a;http协议是一个典型的无状态协议&#xff0c;无记忆&#xff0c;那第一次登录下次是不是还要登录一次&#xff1f; ANS&#xff1a;不需要 因为Co…

C++设计模式3:工厂模式

工厂模式都是在那种有着大量类的程序里面使用的&#xff0c;比如我突然要产生许多的类&#xff0c;这时候就可以用工厂模式&#xff0c;工厂模式有如下三种类型。 简单工厂 用户输入实例化的信息(比如产品名称)&#xff0c;向工厂申请对象&#xff0c;工厂返回相应的对象&…

npm install报错,解决记录:11个步骤诊断和解决问题

在处理npm install报错时&#xff0c;可以遵循以下步骤来诊断和解决问题&#xff1a; 查看错误信息&#xff1a; 错误信息通常会给出问题的线索&#xff0c;例如依赖包版本冲突、网络问题、权限问题等。 更新npm和Node.js&#xff1a; 首先尝试更新npm和Node.js到最新版本&…

地平线—征程2(Journey 2-J2)芯片详解(16)—DDR系统

写在前面 本系列文章主要讲解地平线征程2(Journey 2-J2)芯片的相关知识,希望能帮助更多的同学认识和了解征程2(Journey 2-J2)芯片。 若有相关问题,欢迎评论沟通,共同进步。(*^▽^*) 错过其他章节的同学可以电梯直达目录↓↓↓ 地平线—征程2(Journey 2-J2)芯片详解…

新开发体育直播平台的创业指南:降低赛事版权成本方法

在全球化浪潮下&#xff0c;体育赛事已成为连接世界各地观众的情感纽带&#xff0c;其巨大的影响力不仅激发了全球观众的热情&#xff0c;也催生了体育赛事直播行业的蓬勃发展。然而&#xff0c;高昂的版权费用如同一道难以逾越的门槛&#xff0c;让众多新进入者和小型体育直播…

【与C++的邂逅】--- 类与对象(下)

Welcome to 9ilks Code World (๑•́ ₃ •̀๑) 个人主页: 9ilk (๑•́ ₃ •̀๑) 文章专栏&#xff1a; 与C的邂逅 本节我们继续学习类与对象下&#xff0c;下面给出本节内容大纲。 &#x1f3e0; 再谈构造 &#x1f4cc; 构造函数体赋值 在创建对象时&…

【图形学】TA之路-基于Unity Shader编程之初体验

学习shader之前你必须知道的事情&#xff1a; Unity开发引擎、Direct3D、Shader他们之间的关系 Direct3D 是一个底层图形 API&#xff0c;它直接与 GPU &#xff08;显卡&#xff09;交互&#xff0c;提供了访问硬件加速功能的接口。Unity 开发引擎&#xff0c;它封装了很多底…

[Linux] 认识系统服务(daemon)

参考&#xff1a;《鸟哥的Linux私房菜》 一、什么是 daemon 与服务&#xff08;service&#xff09; 在英语中的daemon就有守护进程&#xff0c;后台程序的意思。简单来说就是一直在后台运行的进程&#xff0c;我们就称之为服务(service)&#xff0c;或者是守护进程(daemon)。…

Java爬虫图像处理:从获取到解析

在互联网时代&#xff0c;数据的价值日益凸显&#xff0c;而爬虫技术作为获取网络数据的重要手段&#xff0c;其应用范围越来越广泛。本文将探讨Java爬虫在图像处理方面的应用&#xff0c;包括如何从网络中获取图像数据&#xff0c;以及如何对这些数据进行解析和处理。 Java爬…

实现Kruskal算法连通游戏地图地牢

前置知识 c分享&#xff5c;并查集从入门到出门 - 力扣&#xff08;LeetCode&#xff09; 彻底搞懂克鲁斯卡尔&#xff08;Kruskal&#xff09;算法&#xff08;附C代码实现&#xff09; - QuanHa - 博客园 (cnblogs.com) 白色点矩形是地牢&#xff0c;其中白线是按照krsukal…

网络编程知识点总结

物理链路网络运输会话表示应用 物链网运会表应 实际的数据帧 TCP和UDP的异同&#xff08;笔试面试&#xff09; 主机&#xff1a;host 转换&#xff1a;to 网络&#xff1a;network uint32_t htonl(uint32_t hostlong); //将4字节无符号整数的主机字节序转换为网络字节序&a…

云计算实训32——roles基本用法、使用剧本安装nginx、使用roles实现lnmp

一、安装nginx并更改其端口 编辑hosts配置文件 [rootmo ~]# vim /etc/ansible/hosts 创建目录 [rootmo ~]# mkdir /etc/ansible/playbook 编辑配置文件 [rootmo ~]# vim /etc/ansible/playbook/nginx.yml 执行测试 [rootmo ~]# ansible-playbook /etc/ansible/playbook/n…

菜鸟的进击之.net6控制台应用程序接收参数

1、新建控制台应用程序&#xff0c;新建-添加项目-控制台应用 2、3、下一步&#xff0c;填写项目名称和代码存放的位置 3、下一步&#xff0c;框架选择.net 6 &#xff0c;点击创建 4、然后项目就创建完成啦&#xff0c; 5、在可以直接在Program.cs写方法一些简单的逻辑&#x…

Rustrover、IDEA 的 Rust 类型不显示(已解决)

关键词&#xff1a;rustrover 类型不显示&#xff0c;rustrover 不显示类型&#xff0c;IntelliJ IDEA Rust 类型不显示&#xff0c;IntelliJ IDEA Rust 不显示类型 若移动端访问不佳&#xff0c;请使用 –> Github版 背景 博主手欠&#xff0c;使用 IntelliJ IDEA 时&am…

四款流行英文翻译工具,助你轻松应对翻译难题

作为一名教培行业的工作人员&#xff0c;我经常需要处理大量的英文文件&#xff0c;从教材到学术论文&#xff0c;再到各种国际交流的资料。翻译工具成了我工作中不可或缺的帮手。今天&#xff0c;我就来跟大家聊聊我用过的几款翻译工具在翻译英文文件时的表现如何呢&#xff1…

超越IP-Adapter!阿里提出UniPortrait,可通过文本定制生成高保真的单人或多人图像。

阿里提出UniPortrait&#xff0c;能根据用户提供的文本描述&#xff0c;快速生成既忠实于原图又能灵活调整的个性化人像&#xff0c;用户甚至可以通过简单的句子来描述多个不同的人物&#xff0c;而不需要一一指定每个人的位置。这种设计大大简化了用户的操作&#xff0c;提升了…

手机游玩植物大战僵尸杂交版V2.3.7最新版教程(文章末尾免费直接下载链接)

手机游玩植物大战僵尸杂交版V2.3.7最新版教程 【V2.3.7全面升级】植物大战僵尸杂交版&#xff1a;跨平台终极安装指南 - 苹果、安卓、电脑、电视兼容&#xff0c;界面革新&#xff0c;16卡槽扩展&#xff0c;高分辨率支持&#xff0c;BUG修复&#xff0c;畅享游戏乐趣 前言 …