字符设备驱动开发

驱动就是获取外设、传感器数据和控制外设。数据会提交给应用程序。

Linux 驱动编译既要编写一个驱动,还要编写一个简单的测试应用程序。

而单片机下驱动和应用都是放在一个文件里,也就是杂在一块。而 Linux 则是分开了。

一、字符设备驱动开发流程

        Linux 里一切皆文件,驱动设备表现就是一个/dev/下的文件,/dev/led。应用程序调用 open 函数 打开设备,比如 led。应用程序通过 write 函数向 /dev/led 写数据,比如写1打开,写0关闭。如果要关闭设备就是 close 函数。

        字符设备驱动的编写主要是驱动对应的 open、close、read。其实就是 file_operations 结构体的成员变量的实现。

     

二、驱动模块的加载与卸载

        Linux 驱动程序可以编译到 kernel 里,也就是 zImage。也可以编译成模块ko。测试的时候只需要加载ko即可。

1. 驱动编写

        编写驱动的注意事项

        编译驱动的时候需要用到 linux 内核源码!因此需要解压缩 Linux 内核源码,编译 Linux 内核源码。得到 zImage 和 dtb。需要使用编译后得到的 zImage 和 dtb 启动系统。这部分不懂的回去看 Linux 内核移植部分。

先编写一个简单的源码,用于测试驱动。

#include <linux/module.h>
static int __init chrdevbase_init(void)
{return 0;
}static void __exit chrdevbase_exit(void)
{}/*模块入口与出口
*/
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);

 Makefile 的编写

KERNELDIR := /home/prover/linux/linux_okCURRENT_PATH := $(shell pwd)obj-m := chrdevbase.obuild : kernel_moduleskernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modulesclean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

需要隐藏的文件

{"search.exclude": {"**/node_modules": true,"**/bower_components": true,"**/*.o":true,"**/*.su":true, "**/*.cmd":true,"Documentation":true,      },"files.exclude": {"**/.git": true,"**/.svn": true,"**/.hg": true,"**/CVS": true,"**/.DS_Store": true,  "**/*.o":true,"**/*.su":true, "**/*.cmd":true,"Documentation":true, }
}

指定内核源码路径 

{"configurations": [{"name": "Linux","includePath": ["${workspaceFolder}/**","/home/prover/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/include", "/home/prover/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/include", "/home/prover/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/include/generated/"],"defines": [],"compilerPath": "/usr/bin/clang","cStandard": "c11","cppStandard": "c++17","intelliSenseMode": "clang-x64"}],"version": 4
}

编译后,.ko就是我们需要的驱动文件了。 

 2. 驱动模块的加载和卸载

开发板上使用命令 modprobe

发现需要创建/lib/modules。

将 .ko文件和可执行文件 chrdevbase.o 拷贝到该目录下

对于一个新的模块使用modprobe,需要先使用depmod命令,否则报下面错误:

如果报下面错误,说明内核和你驱动不是同源的。

成功后,还有个 license 的警告。

在源码中添加 License,还可以再加个作者。当然我们还在函数中添加了printk语句,用于观察:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>static int __init chrdevbase_init(void)
{printk("chrdevbase_init\r\n");return 0;
}static void __exit chrdevbase_exit(void)
{printk("chrdevbase_exit\r\n");
}MODULE_LICENSE("GPL");
MODULE_AUTHOR("Prover");

make编译后,再拷贝到指定目录下。然后modprobe加载驱动,最后再rmmod卸载驱动。

3. 字符设备注册与注销

        对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模 块的时候也需要注销掉字符设备。

        函数原型为:

static inline int register_chrdev(unsigned int major, const char *name, 
const struct file_operations *fops) 
static inline void unregister_chrdev(unsigned int major, const char *name) 

register_chrdev 函数用于注册字符设备,此函数一共有三个参数,这三个参数的含义如下:

major:主设备号,Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两 部分,关于设备号后面会详细讲解。

name:设备名字,指向一串字符串。

fops:结构体 file_operations 类型指针,指向设备的操作函数集合变量。

unregister_chrdev 函数用户注销字符设备,此函数有两个参数,这两个参数含义如下:

major:要注销的设备对应的主设备号。 name:要注销的设备对应的设备名。

先查看下存在的设备号,我们觉得设置为200。

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>static struct file_operations test_fops;static int __init chrdevbase_init(void)
{//入口函数具体内容int retvalue = 0;//注册字符设备驱动retvalue = register_chrdev(200, "chrtest", &test_fops);if(retvalue < 0){//字符设备注册失败}//printk("chrdevbase_init\r\n");return 0;
}static void __exit chrdevbase_exit(void)
{unregister_chrdev(200, "chrtest");//printk("chrdevbase_exit\r\n");
}/*模块入口与出口
*/
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("Prover");

4. 实现设备的具体操作函数

file_operations 结构体就是设备的具体操作函数。

需要实现的基本功能:打开和关闭,读写。

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>//打开设备
static int chrtest_open(struct inode* inode, struct file* filp)
{//用户实现具体功能
}//从设备读取
static ssize_t chrtest_read(struct file* filp, char __user* buf, size_t cnt, loff_t* offt)
{//用户实现具体功能return 0;
}//向设备写数据
static ssize_t chrtest_write(struct file* filp,const char __user* buf,size_t cnt, loss_t *offt)
{//用户实现具体功能return 0;
}//关闭/释放设备
static int chrtest_release(struct inode *inode, struct file* filp)
{//用户实现具体功能return 0;
}static struct file_operations test_fops = {.owner   = THIS_MODULE,.open    = chrtest_open,.read    = chrtest_read,.write   = chrtest_write,.release = chrtest_release,
};static int __init chrdevbase_init(void)
{//入口函数具体内容int retvalue = 0;//注册字符设备驱动retvalue = register_chrdev(200, "chrtest", &test_fops);if(retvalue < 0){//字符设备注册失败}//printk("chrdevbase_init\r\n");return 0;
}static void __exit chrdevbase_exit(void)
{unregister_chrdev(200, "chrtest");//printk("chrdevbase_exit\r\n");
}/*模块入口与出口
*/
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("Prover");

三、Linux 设备号

为了方便管理,Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分 组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。

Linux 提供了 一个名为 dev_t 的数据类型表示设备号,dev_t 定义在文件 include/linux/types.h 里面,定义如下:

typedef __u32 __kernel_dev_t; 
typedef __kernel_dev_t dev_t; 

可以看出 dev_t 是__u32 类型的,而__u32 定义在文件 include/uapi/asm-generic/int-ll64.h 里 面,定义如下:

typedef unsigned int __u32; 

 dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型。其中高 12 位为主设备号,低 20 位为次设备号。

设备号操作函数

#define MINORBITS 20 
#define MINORMASK ((1U << MINORBITS) - 1) #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) 
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) 
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))

宏 MINORBITS 表示次设备号位数,一共是 20 位。  

宏 MINORMASK 表示次设备号掩码。

宏 MAJOR 用于从 dev_t 中获取主设备号,将 dev_t 右移 20 位即可。

宏 MINOR 用于从 dev_t 中获取次设备号,取 dev_t 的低 20 位的值即可。

宏 MKDEV 用于将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号。

前面自己分配的200这个设备号,其实算静态分配。当然也有提供动态分配设备号的方式,设备号的申请函数如下:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name) 

dev:保存申请到的设备号。

baseminor:次设备号起始地址,alloc_chrdev_region 可以申请一段连续的多个设备号,这 些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor 为起始地址地址开始递 增。一般 baseminor 为 0,也就是说次设备号从 0 开始。

count:要申请的设备号数量。

name:设备名字。

注销字符设备之后要释放掉设备号,设备号释放函数如下:

void unregister_chrdev_region(dev_t from, unsigned count) 

from:要释放的设备号。

count:表示从 from 开始,要释放的设备号数量。

四、字符设备驱动开发实验

1. 完善驱动程序

第二节将驱动的框架写好了,接下来要完善设备号等一系列东西。

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/types.h>#define CHRDEVBASE_MAJOR    200 //主设备号
#define CHRDEVBASE_NAME     "chrdevbase"    //设备名static char readbuf[100];
static char writebuf[100];
static char kerneldata[] = {"kernel data!"};/* 
* @description : 打开设备 
* @param – inode : 传递给驱动的 inode 
* @param - filp : 设备文件,file 结构体有个叫做 private_data 的成员变量 
* 一般在 open 的时候将 private_data 指向设备结构体。 
* @return : 0 成功;其他 失败 
*/ 
static int chrtest_open(struct inode* inode, struct file* filp)
{//用户实现具体功能return 0;
}/* 
* @description : 从设备读取数据 
* @param - filp : 要打开的设备文件(文件描述符) 
* @param - buf : 返回给用户空间的数据缓冲区 
* @param - cnt : 要读取的数据长度 
* @param - offt : 相对于文件首地址的偏移 
* @return : 读取的字节数,如果为负值,表示读取失败 
*/ 
static ssize_t chrtest_read(struct file* filp, char __user* buf, size_t cnt, loff_t* offt)
{//用户实现具体功能int retvalue = 0;//向用户空间发送数据memcpy(readbuf, kerneldata, sizeof(kerneldata));retvalue = copy_to_user(buf, readbuf, cnt);if(retvalue == 0){printk("kernel senddata ok!\r\n");}else{printk("kernel senddata failed!\r\n");}return 0;
}/* 
* @description : 向设备写数据 
* @param - filp : 设备文件,表示打开的文件描述符 
* @param - buf : 要写给设备写入的数据 
* @param - cnt : 要写入的数据长度 
* @param - offt : 相对于文件首地址的偏移 
* @return : 写入的字节数,如果为负值,表示写入失败 
*/ 
static ssize_t chrtest_write(struct file* filp,const char __user* buf,size_t cnt, loff_t *offt)
{//用户实现具体功能int retvalue = 0;//接收用户空间传递给内核的数据并且打印出来retvalue = copy_from_user(writebuf, buf, cnt);if(retvalue == 0){printk("kernel recevdata:%s\r\n",writebuf);}else{printk("kernel recevdata failed!\r\n");}return 0;
}/* 
* @description : 关闭/释放设备 
* @param - filp : 要关闭的设备文件(文件描述符) 
* @return : 0 成功;其他 失败 
*/
static int chrtest_release(struct inode *inode, struct file* filp)
{//用户实现具体功能return 0;
}/** 设备操作函数结构体 */
static struct file_operations chrdevbase_fops = {.owner   = THIS_MODULE,.open    = chrtest_open,.read    = chrtest_read,.write   = chrtest_write,.release = chrtest_release,
};/* 
* @description : 驱动入口函数 
* @param : 无 
* @return : 0 成功;其他 失败 
*/
static int __init chrdevbase_init(void)
{//入口函数具体内容int retvalue = 0;//注册字符设备驱动retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);if(retvalue < 0){//字符设备注册失败printk("chrdevbase driver register failed\r\n");}printk("chrdevbase_init()\r\n");return 0;
}/*
* @description : 驱动出口函数 
* @param : 无 
* @return : 无 
*/ 
static void __exit chrdevbase_exit(void)
{unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);printk("chrdevbase_exit()\r\n");
}/*模块入口与出口
*/
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);//LICENSE 和 作者信息
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Prover");

2. 编写测试APP

这部分,如果有 Linux C 编程的基础就更好了。调用一些 C 库文件操作基本函数。

chrdevbaseApp.c

#include "stdio.h" 
#include "unistd.h" 
#include "sys/types.h" 
#include "sys/stat.h" 
#include "fcntl.h" 
#include "stdlib.h" 
#include "string.h"/** 使用方法*./chrdevbaseApp /dev/chrdevbase <1>|<2>* argv[2] 1:读文件 * argv[2] 2:写文件 
*/static char usrdata[] = {"usr data!"}; /* 
* @description : main 主程序 
* @param - argc : argv 数组元素个数 
* @param - argv : 具体参数 
* @return : 0 成功;其他 失败 
*/ 
int main(int argc, char *argv[]) 
{ int fd, retvalue; char *filename; char readbuf[100], writebuf[100]; if(argc != 3){ printf("Error Usage!\r\n"); return -1; } filename = argv[1]; /* 打开驱动文件 */ fd = open(filename, O_RDWR); if(fd < 0){ printf("Can't open file %s\r\n", filename); return -1; } if(atoi(argv[2]) == 1){ /* 从驱动文件读取数据 */ retvalue = read(fd, readbuf, 50); if(retvalue < 0){ printf("read file %s failed!\r\n", filename); }else{ /* 读取成功,打印出读取成功的数据 */ printf("read data:%s\r\n",readbuf); } } if(atoi(argv[2]) == 2){ /* 向设备驱动写数据 */ memcpy(writebuf, usrdata, sizeof(usrdata)); retvalue = write(fd, writebuf, 50); if(retvalue < 0){ printf("write file %s failed!\r\n", filename); } } /* 关闭设备 */ retvalue = close(fd); if(retvalue < 0){ printf("Can't close file %s\r\n", filename); return -1; } return 0; 
} 

使用交叉编译器编译

arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp 

3. 加载驱动模块

将驱动文件和App文件放入根文件的lib/modules/4.1.15下

sudo cp chrdevbase.ko chrdevbaseApp /home/prover/linux/nfs/rootfs/lib/modules/4.1.15/ -f

 用modprobe驱动 .ko 后,查看设备号。

cat /proc/devices

当前系统存在 chrdevbase 这个设备,主设备号为 200,跟我们设置 的主设备号一致。

4. 创建设备节点文件

驱动加载成功需要在/dev 目录下创建一个与之对应的设备节点文件,应用程序就是通过操 作这个设备节点文件来完成对具体设备的操作。输入如下命令创建/dev/chrdevbase 这个设备节 点文件:

mknod /dev/chrdevbase c 200 0 

然后查看

5. 设备操作测试

./chrdevbaseApp /dev/chrdevbase 1 

第一行是 chrdevbase_read 函数 输出的信息。第二行则是APP中输出的接收到的数据:kernel data!

刚才的 1 是读文件操作,现在输入 2 来实现写文件操作:

./chrdevbaseApp /dev/chrdevbase 2 

既然读写都没问题,说明我们编写 的 chrdevbase 驱动是没有问题的。

6. 卸载驱动模块

不再使用某个设备的话,驱动卸载即可。

rmmod chrdevbase.ko

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

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

相关文章

参数映射服务完整解决方案

参数映射服务完整解决方案 1. 背景说明 在复杂的工作流程中&#xff0c;后续程序需要动态构造输入参数&#xff0c;这些参数源自多个前序程序的 JSON 数据输出。为了增强系统的灵活性和可扩展性&#xff0c;需要一个通用的参数映射服务来处理这种复杂的数据转换需求。 1.1 主…

SpringCloud - Nacos注册/配置中心

前言 该博客为Nacos学习笔记&#xff0c;主要目的是为了帮助后期快速复习使用 学习视频&#xff1a;7小快速通关SpringCloud 辅助文档&#xff1a;SpringCloud快速通关 一、简介 Nacos官网&#xff1a;https://nacos.io/docs/next/quickstart/quick-start/ Nacos /nɑ:kəʊ…

IDEA安装离线插件(目前提供了MavenHelper安装包)

目录 1、离线安装方式2、Maven Helper 1、离线安装方式 首先访问 IDEA插件网站 下载离线插件安装包&#xff0c;操作如下&#xff1a; 然后打开IDEA的Settings配置&#xff0c;点击Plugins&#xff0c;点击右侧设置按钮&#xff08;齿轮&#xff09;&#xff0c;选择Install P…

科技赋能数字内容体验的核心技术探索

内容概要 在数字化时代&#xff0c;科技的迅猛发展为我们的生活和工作带来了深刻的变革。数字内容体验已经成为人们获取信息和娱乐的重要途径&#xff0c;而这背后的技术支持则扮演着至关重要的角色。尤其是在人工智能、虚拟现实和区块链等新兴技术的推动下&#xff0c;数字内…

【LeetCode 刷题】贪心算法(2)-进阶

此博客为《代码随想录》贪心算法章节的学习笔记&#xff0c;主要内容为贪心算法进阶的相关题目解析。 文章目录 135. 分发糖果406. 根据身高重建队列134. 加油站968. 监控二叉树 135. 分发糖果 题目链接 class Solution:def candy(self, ratings: List[int]) -> int:n l…

工业相机,镜头的选型及实战

工业相机和镜头的选型是机器视觉系统中的关键步骤&#xff0c;选型不当可能导致成像质量差或系统性能不达标。&#xff08;用于个人的学习和记录&#xff09; 一、工业相机选型方法 确定分辨率 分辨率需求&#xff1a;根据被测物体的尺寸和检测精度要求计算所需分辨率。 公式…

探索robots.txt:网站管理者的搜索引擎指南

在数字时代&#xff0c;网站如同企业的在线名片&#xff0c;其内容和结构对搜索引擎的可见性至关重要。而在这背后&#xff0c;有一个默默工作的文件——robots.txt&#xff0c;它扮演着搜索引擎与网站之间沟通桥梁的角色。本文将深入探讨robots.txt的功能、编写方法及其在现代…

使用WebUI访问本地Deepseek(Ollama集成Open WebUI)

在《deepseek本地部署和使用&#xff08;Linux虚拟机&#xff09;》中&#xff0c;我们使用Ollama部署了Deepseek-r1&#xff0c;但是只能通过命令行方式交互&#xff0c;默认Ollama启动后&#xff0c;会启动一个监听到127.0.0.1&#xff0c;用以接收POST 请求&#xff0c;服务…

windows蓝牙驱动开发-蓝牙 LE 邻近感应配置文件

邻近感应检测是蓝牙低功耗 (LE) 的常见用途。 本部分提供了创建可用于开发 UWP 设备应用的邻近感应配置文件的设备实现的指南。 在开发此应用之前&#xff0c;应熟悉蓝牙 LE 函数和蓝牙 LE 邻近感应配置文件规范。 示例服务声明 蓝牙低功耗引入了一个新的物理层&#xff0c;…

模型 冗余系统(系统科学)

系列文章分享模型&#xff0c;了解更多&#x1f449; 模型_思维模型目录。为防故障、保运行的备份机制。 1 冗余系统的应用 1.1 冗余系统在企业管理中的应用-金融行业信息安全的二倍冗余技术 在金融行业&#xff0c;信息安全是保障业务连续性和客户资产安全的关键。随着数字化…

AI绘画社区:解锁艺术共创的无限可能(9/10)

AI 绘画&#xff1a;不只是技术&#xff0c;更是社交新潮流 在科技飞速发展的今天&#xff0c;AI 绘画早已不再仅仅是一项孤立的技术&#xff0c;它正以惊人的速度融入我们的社交生活&#xff0c;成为艺术爱好者们交流互动的全新方式&#xff0c;构建起一个充满活力与创意的社…

DeepSeek使用技巧大全(含本地部署教程)

在人工智能技术日新月异的今天&#xff0c;DeepSeek 作为一款极具创新性和实用性的 AI&#xff0c;在众多同类产品中崭露头角&#xff0c;凭借其卓越的性能和丰富的功能&#xff0c;吸引了大量用户的关注。 DeepSeek 是一款由国内顶尖团队研发的人工智能&#xff0c;它基于先进…

IDEA - 一个启动类多次启动方法

More Run/Debug -> Modify Run Configuration -> modify options -> Allow mutiple instances

Android Studio 配置 Gerrit Code Review

很多大厂&#xff08;华为、荣耀&#xff09;的大型项目都有gerrit代码审查流程&#xff0c;那么我们如何实现不手动敲命令行&#xff0c;就在Android Studio中像平常开发一样&#xff0c;只需要用鼠标点点点&#xff0c;就能将代码推送到gerrit审查仓呢&#xff0c;现在就来跟…

TypeScript 中的对象类型:深入理解接口和类型别名

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

【Java基础】序列化、反序列化和不可变类

Hi~&#xff01;这里是奋斗的明志&#xff0c;很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~~ &#x1f331;&#x1f331;个人主页&#xff1a;奋斗的明志 &#x1f331;&#x1f331;所属专栏&#xff1a;Java基础面经 &#x1f4da;本系列文章为个…

吴恩达深度学习——卷积神经网络的特殊应用

内容来自https://www.bilibili.com/video/BV1FT4y1E74V&#xff0c;仅为本人学习使用。 文章目录 人脸识别相关定义Similarity函数使用Siamese网络实现函数d使用Triplet损失学习参数 神经风格迁移深度卷积网络可视化神经风格迁移的代价函数内容损失函数风格损失函数 人脸识别 …

搭建linux qt5.6环境

文章目录 准备工作步骤测试 准备工作 1、linux虚拟机环境 2、linux qt安装包相关文件&#xff0c;本文采用压缩包方式 步骤 1&#xff09;启动虚拟机并登入 2&#xff09;打开linux终端命令行&#xff0c;ifconfig获取当前linux环境的ip 3&#xff09;使用WinSCP设置好ip、…

C++ 使用CURL开源库实现Http/Https的get/post请求进行字串和文件传输

CURL开源库介绍 CURL 是一个功能强大的开源库&#xff0c;用于在各种平台上进行网络数据传输。它支持众多的网络协议&#xff0c;像 HTTP、HTTPS、FTP、SMTP 等&#xff0c;能让开发者方便地在程序里实现与远程服务器的通信。 CURL 可以在 Windows、Linux、macOS 等多种操作系…

【产品小白】用户调研的需求是否都采纳?

在用户调研中&#xff0c;并非所有需求都应被直接采纳&#xff0c;而应通过系统分析转化为符合产品战略的有效决策。以下是关键思考框架&#xff1a; 1. 用户需求 ≠ 产品需求 矛盾性&#xff1a;用户个体需求可能相互冲突&#xff08;如A功能的去留&#xff09;&#xff0c;需…