Linux第67步_linux字符设备驱动_注册和注销

1、字符设备注册与注销的函数原型”

/*字符设备注册的函数原型*/

static inline int register_chrdev(unsigned int major,\

                                  const char *name,  \

                                  const struct file_operations *fops)

/*

major:主设备号,Limnux下每个设备都有一个设备号,设备号分为主设备号和次设备号两部分。

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

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

*/

/*字符设备注销的函数原型*/

static inline void unregister_chrdev(unsigned int major,\

                                     const char *name)

/*

major:主设备号,Limnux下每个设备都有一个设备号,设备号分为主设备号和次设备号两部分。

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

*/

2、Linux设备号

1)、使用设备号的原因:为了方便管理,Linux 中每个设备都有一个设备号。

2)、设备号的组成

Linux设备号是由主设备号和次设备号两部分组成;

主设备号表示某一个具体的驱动;

次设备号表示使用这个驱动的各个设备。

Linux使用“dev_t的数据类型”表示设备号;

dev_t的数据类型”定义在文件“include/linux/types.h”里面,定义如下:

typedef __u32 __kernel_dev_t;

//为“__u32”起个别名叫“__kernel_dev_t”

typedef __kernel_dev_t dev_t;

//为“__kernel_dev_t”起个别名叫“dev_t”

由此可见,dev_t是 umsigned int类型,是一个32位的数据类型。这个32位的数据就是Linux设备号,它是由“主设备号”和“次设备号”两部分构成,其中高12位为“主设备号”,低20位为“次设备号”。因此,Linux系统中主设备号范围为0~4095,所以,大家在选择主设备号的时候一定不要超过这个范围

在文件“include/linux/kdev_t.h”中提供了几个关于设备号的操作函数(本质是宏),如下所示:

#define MINORBITS 20         //定义“次设备号”占据低20位

#define MINORMASK ((1U << MINORBITS) - 1) //定义“次设备号”的掩码值

#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))

//输入参数dev为“Linux设备号”

//将dev右移20位得到“主设备号”

#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))

//输入参数dev为“Linux设备号”

//将dev与0xFFFFF相与后得到“次设备号”

#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))

//输入参数ma为“主设备号”

//输入参数mi为“次设备号”

//将ma左移20位,再与mi相或,就得到“Linux设备号”

设备号的申请函数

int alloc_chrdev_region(dev_t *dev,\

                        unsigned baseminor,\

                        unsigned count,\

                        const char *name)

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

//baseminor:次设备号的起始地址

//alloc_chrdev_region可以申请一段连续的多个“设备号”,这些“设备号”的“主设备号”是一样的,但是“次设备号”不同。“次设备号”以baseminor为起始值开始递增。通常baseminor的值为0。

//count:要申请的设备号数量;

// name:表示“设备名字”

设备号的释放函数

void unregister_chrdev_region(dev_t from, unsigned count)

// from:要释放的设备号;

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

3、字符设备加载,注册,注销和卸载的一般模板

#define xxx_MAJOR   200

//定义主设备号

//静态分配设备号:在串口输入“cat/proc/devices”查询当前已用的主设备号

//然后使用一个“没有被使用的设备号”作为该设备的的主设备号

#define xxx_NAME   "xxxName"  //定义设备的名字。

const struct file_operations xxx_fops = {};

//声明一个file_operations结构变量

/*驱动入口函数 */

static int  __init xxx_init(void)

{

    int ret;

    ret = register_chrdev(xxx_MAJOR, xxx_NAME, &xxx_fops);

//注册字符设备驱动

//xxx_MAJOR为主设备号,采用宏xxx_NAME定义设备名字

//xxx_fops是设备的操作函数集合,它是file_operations结构变量

    if (ret < 0) {

       pr_err("xxx_init is failed!!!\r\n");

       return ret;

    }

    else pr_err("xxx_init is ok!!!\r\n");

   return 0;

}

/*驱动出口函数 */

static void __exit xxx_exit(void)

{

   /*出口函数具体内容 */

    unregister_chrdev(xxx_MAJOR, xxx_NAME);

    //注销字符设备驱动

//xxx_MAJOR为主设备号,采用宏xxx_NAME定义设备名字

}

module_init(xxx_init); //声明xxx_init()为驱动入口函数

module_exit(xxx_exit); //声明xxx_exit()为驱动出口函数

4、查看linux-5.4.31”中的设备注册和注销

打开虚拟机上“VSCode”,点击“文件”,点击“打开文件夹”,点击“zgq”,点击“linux”,点击“atk-mp1”,点击“linux”,点击“my_linux”,点击“linux-5.4.31”,见下图:

点击“确定

点击“查看”,点击“搜索”,输入“register_chrdev

假如我们点击的是“rtlx-cmp.c”,见下图:

rtlx-cmp.c”程序如下:

#include <linux/device.h>

#include <linux/fs.h>

#include <linux/err.h>

#include <linux/wait.h>

#include <linux/sched.h>

#include <linux/smp.h>

#include <asm/mips_mt.h>

#include <asm/vpe.h>

#include <asm/rtlx.h>

static int major;//major:设备号

/*从字面意思看,是一个中断*/

static void rtlx_interrupt(void)

{

    int i;

    struct rtlx_info *info;

    struct rtlx_info **p = vpe_get_shared(aprp_cpu_index());

    if (p == NULL || *p == NULL)

       return;

    info = *p;

    if (info->ap_int_pending == 1 && smp_processor_id() == 0) {

       for (i = 0; i < RTLX_CHANNELS; i++) {

           wake_up(&channel_wqs[i].lx_queue);

           wake_up(&channel_wqs[i].rt_queue);

       }

       info->ap_int_pending = 0;

    }

}

/*从字面看是讲中断堆栈指针*/

void _interrupt_sp(void)

{

    smp_send_reschedule(aprp_cpu_index());

}

/*入口函数初始化*/

int __init rtlx_module_init(void)

{

    struct device *dev;

    int i, err;

    if (!cpu_has_mipsmt) {

       pr_warn("VPE loader: not a MIPS MT capable processor\n");

       return -ENODEV;

    }

    if (num_possible_cpus() - aprp_cpu_index() < 1) {

       pr_warn("No TCs reserved for AP/SP, not initializing RTLX.\n"

           "Pass maxcpus=<n> argument as kernel argument\n");

       return -ENODEV;

    }

    major = register_chrdev(0, RTLX_MODULE_NAME, &rtlx_fops);

//注册字符设备驱动

   //0为主设备号,采用宏RTLX_MODULE_NAME定义设备名字

   //rtlx_fops是设备的操作函数集合,它是file_operations结构变量

    if (major < 0) {//读取主设备号小于0,则打印错误信息

       pr_err("rtlx_module_init: unable to register device\n");

       return major;

    }

    /* initialise the wait queues */

    for (i = 0; i < RTLX_CHANNELS; i++) {

       init_waitqueue_head(&channel_wqs[i].rt_queue);

       //初始化等待队列头

       init_waitqueue_head(&channel_wqs[i].lx_queue);

       //初始化等待队列头

       atomic_set(&channel_wqs[i].in_open, 0);//状态重置

       mutex_init(&channel_wqs[i].mutex);//初始化互斥体

       dev = device_create(mt_class, NULL, MKDEV(major, i), NULL,

                  "%s%d", RTLX_MODULE_NAME, i);

//创建设备, major为主设备号,i为次设备号

//参数mt_class表示该设备位于mt_class类下面

// parent为NULL表示没有父设备

//将major左移20位,再与i相或,就得到“Linux设备号”

//drvdata为NULL表示没有使用设备文件

//采用RTLX_MODULE_NAME宏定义指向字符串表示设备名

       if (IS_ERR(dev)) {

           while (i--)

              device_destroy(mt_class, MKDEV(major, i));

              //删除创建的设备

              //参数mt_class是设备所处的类,参数i是设备号

           err = PTR_ERR(dev);

           goto out_chrdev;

       }

    }

    /* set up notifiers */

    rtlx_notify.start = rtlx_starting;

    rtlx_notify.stop = rtlx_stopping;

    vpe_notify(aprp_cpu_index(), &rtlx_notify);

    if (cpu_has_vint) {

       aprp_hook = rtlx_interrupt;

    } else {

       pr_err("APRP RTLX init on non-vectored-interrupt processor\n");

       err = -ENODEV;

       goto out_class;

    }

    return 0;

out_class:

    for (i = 0; i < RTLX_CHANNELS; i++)

       device_destroy(mt_class, MKDEV(major, i));

       //删除创建的设备

       //参数mt_class是要删除的设备所处的类,参数i是要删除的设备号

out_chrdev:

    unregister_chrdev(major, RTLX_MODULE_NAME);

   //注销字符设备驱动

   //major为主设备号,采用宏RTLX_MODULE_NAME定义设备

    return err;

}

/*出口函数初始化*/

void __exit rtlx_module_exit(void)

{

    int i;

    for (i = 0; i < RTLX_CHANNELS; i++)

       device_destroy(mt_class, MKDEV(major, i));

     //删除创建的设备

     //参数mt_class是要删除的设备所处的类,参数i是要删除的设备号

    unregister_chrdev(major, RTLX_MODULE_NAME);

   //注销字符设备驱动

    //major为主设备号,采用宏RTLX_MODULE_NAME定义设备名字

    aprp_hook = NULL;

}

5、编写“字符设备驱”动注册与注销的程序

1)、创建“/home/zgq/linux/Linux_Drivers/01_MyCharDevice/”目录

输入“cd /home/zgq/linux/Linux_Drivers/

切换到“/home/zgq/linux/Linux_Drivers/”目录

输入“ls”,查询“/home/zgq/linux/Linux_Drivers/”目录下的文件和文件夹

输入“mkdir 01_MyCharDevice

创建“/home/zgq/linux/Linux_Drivers/01_MyCharDevice/”目录

输入“ls”,查询“/home/zgq/linux/Linux_Drivers/”目录下的文件和文件夹

输入“cd 01_MyCharDevice/

切换到“/home/zgq/linux/Linux_Drivers/01_MyCharDevice/”目录

输入“pwd”获取绝对路径

输入“cd /home/zgq/linux/Linux_Drivers/00_My_TestDriver/

切换到“/home/zgq/linux/Linux_Drivers/00_My_TestDriver/”目录

输入“ls”,查询“/home/zgq/linux/Linux_Drivers/00_My_TestDriver/”目录下的文件和文件夹

输入“cp Makefile /home/zgq/linux/Linux_Drivers/01_MyCharDevice回车

拷贝“Makefile

2)、修改Makefile文件

Makefile文件内容如下:

KERNELDIR := /home/zgq/linux/atk-mp1/linux/my_linux/linux-5.4.31

#使用“:=”将其后面的字符串赋值给KERNELDIR

CURRENT_PATH := $(shell pwd)

#采用“shell pwd”获取当前打开的路径

#使用“$(变量名)”引用“变量的值”

obj-m := MyCharDevice.o

#生成“obj-m”需要依赖“MyCharDevice.o”

build: kernel_modules

#生成“build”需要依赖“kernel_modules”

        @echo $(KERNELDIR)

#输出KERNELDIR的值为“/home/zgq/linux/atk-mp1/linux/linux-5.4.31”

        @echo $(CURRENT_PATH)

#输出CURRENT_PATH的值为/home/zgq/linux/Linux_Drivers/00_My_TestDriver”

        @echo $(MAKE)

#输出MAKE的值为make

kernel_modules:

        $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules

#后面的"modules"表示编译成模块

#“KERNELDIR”上面定义为“/home/zgq/linux/atk-mp1/linux/my_linux/linux-5.4.31”,即“指定的工作目录”

#“CURRENT_PATH”上面定义为“当前的工作目录”

#“-C $(KERNELDIR) M=$(CURRENT_PATH) ”表示将“当前的工作目录”切换到“指定的目录”中

#即切换到“/home/zgq/linux/atk-mp1/linux/my_linux/linux-5.4.31”。

#M表示模块源码目录

#在“make和modules”之间加入“M=$(CURRENT_PATH)”,表示切换到由“CURRENT_PATH”指定的目录中读取源码,同时将其编>译为.ko 文件

clean:

        $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

#“KERNELDIR”上面定义为“/home/zgq/linux/atk-mp1/linux/my_linux/linux-5.4.31”,即“指定的工作目录”

#“CURRENT_PATH”上面定义为“当前的工作目录”

3)、使用VSCode创建“MyCharDevice.c文件

添加内容如下:

#include <linux/init.h>       //必须要包含的头文件

#include <linux/module.h>     //必须要包含的头文件

#include <linux/string.h>     //下面要用到字符串,显然也要包含

#include <linux/kernel.h>     //必须要包含的头文件

#include <linux/device.h>     //必须要包含的头文件

#include <linux/fs.h>         //使能结构体"file_operations"

#define MyCharDevice_MAJOR 200

//定义主设备号

//可以通过串口输入“cat /proc/devices”查询当前已用的主设备号

#define MyCharDevice_NAME "MyCharDeviceName"

//使用MyCharDevice_NAME指向一串字符串表示设备的名字。

const struct file_operations MyCharDevice_fops = {};

//声明file_operations结构变量MyCharDevice_fops

//它是指向设备的操作函数集合变量

/*入口函数初始化*/

static int __init MyCharDevice_init(void)

{

    int ret = 0;

    printk("MyCharDevice_init\r\n");

    ret = register_chrdev(MyCharDevice_MAJOR, MyCharDevice_NAME, &MyCharDevice_fops);

    //返回的ret=0表示字符设备驱动注册成功

    //主设备号为MyCharDevice_MAJOR

    //设备名字为RTLX_MODULE_NAME宏定义

    //MyCharDevice_fops是设备的操作函数集合

    if (ret < 0) {

       pr_err("MyCharDevice_init is failed!!!\r\n");

       return ret;

    }

    else printk("ret=%d\r\n",ret);

    return ret;

}

/*出口函数初始化*/

static void __exit MyCharDevice_exit(void)

{

    printk("MyCharDevice_exit\r\n");

    unregister_chrdev(MyCharDevice_MAJOR, MyCharDevice_NAME);

    //主设备号为MyCharDevice_MAJOR的值

    //设备名字为MyCharDevice_NAME宏定义的字符串“MyCharDeviceName”

}

module_init(MyCharDevice_init);

/*将MyCharDevice_init()指定为入口函数*/

module_exit(MyCharDevice_exit);

/*将MyCharDevice_exit()指定为出口函数*/

MODULE_AUTHOR("Zhanggong");//添加作者名字

MODULE_DESCRIPTION("This is test module!");//模块介绍

MODULE_LICENSE("GPL");//LICENSE采用“GPL协议”

MODULE_INFO(intree,"Y");

//去除显示“loading out-of-tree module taints kernel.”

4)、编译

输入“cd /home/zgq/linux/Linux_Drivers/01_MyCharDevice/

输入“ls

输入“sudo cp  MyCharDevice.ko  /home/zgq/linux/nfs/rootfs/lib/modules/5.4.31

输入密码“123456回车

输入“cd /home/zgq/linux/nfs/rootfs/lib/modules/5.4.31

输入“ls

5)、测试

启动开发板,从网络下载程序

输入“root

输入“cd /lib/modules/5.4.31/

在nfs挂载中,切换到“/lib/modules/5.4.31/”目录,

注意:“lib/modules/5.4.31/在虚拟机中是位于“/home/zgq/linux/nfs/rootfs/”目录下,但在开发板中,却是位于根目录中

输入“ls

输入“depmod”,驱动在第一次执行时,需要运行“depmod”

输入“modprobe MyCharDevice.ko”,加载“MyCharDevice.ko”模块

输入“lsmod”查看有哪些驱动在工作

输入“rmmod MyCharDevice.ko”,卸载“MyCharDevice.ko”模块

注意:输入“rmmod MyCharDevice”也可以卸载“MyCharDevice.ko”模块

输入“lsmod”查看有哪些驱动在工作

输入“modprobe MyCharDevice.ko”,加载“MyCharDevice.ko”模块

输入“cat /proc/devices回车”查询设备号

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

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

相关文章

HTTP详解(HTTP的特点,状态码,工作原理,GET和POST的区别,如何解决无状态通信)!!!

文章目录 一、HTTP协议简介二、HTTP的主要特点三、HTTP之URL四、Request和Respons五、HTTP的状态码六、HTTP工作原理七、GET和POST请求的区别八、解决HTTP无状态通信——Cookie和Session 一、HTTP协议简介 HTTP协议是Hyper Text Transfer Protocol&#xff08;超文本传输协议&…

基础!!!吴恩达deeplearning.ai:卷积层

以下内容有任何不理解可以翻看我之前的博客哦&#xff1a;吴恩达deeplearning.ai专栏 文章目录 回顾——密集层 Dense Layer卷积层 Convolutional Neural Network定义优势具体说明心电图卷积层搭建 到目前为止&#xff0c;你使用的所有神经网络层都是密集层类型&#xff0c;这…

软件测试有哪些常用的测试方法?

软件测试是软件开发过程中重要组成部分&#xff0c;是用来确认一个程序的质量或者性能是否符合开发之前提出的一些要求。软件测试的目的有两方面&#xff0c;一方面是确认软件的质量&#xff0c;另一方面是提供信息&#xff0c;例如&#xff0c;给开发人员或者程序经理反馈意见…

基于vue-office实现docx、xlsx、pdf文件的在线预览

概述 在做项目的时候会遇到docx、xlsx、pdf等文件的在线预览需求&#xff0c;实现此需求可以有多种解决方式&#xff0c;本文基于vue-office实现纯前端的文件预览。 效果 如下图&#xff0c;分别为docx、xlsx、pdf三种类型的文件在线加载后的效果。你也可以访问官方预览网址…

当Web3叙事寒冬到来,游戏是否是冬日里的“一把火”?

出品&#xff5c;欧科云链研究院 作者&#xff5c;Jason Jiang 以太坊创始人Vitalik在2019年曾说&#xff1a;金融与游戏会是区块链最先落地的场景。 在DeFi金融创新驱动上个周期后&#xff0c;沉寂近两年的Web3游戏板块&#xff0c;如今似乎也在复苏。无论是频繁获得融资&a…

pandas/geopandas 笔记:逐record的轨迹dataFrame转成逐traj_id的轨迹dataFrame

我们现在有这样的一个dataframe&#xff0c;名字为dart 我们需要这样一个DataFrame&#xff0c;每一行有两列&#xff0c;一列是new_installation_id&#xff0c;表示这个轨迹的id&#xff1b;另一列就是这个new_installation_id的轨迹 dart_new dart[[new_installation_id]]…

如何使用Docker部署IT-Tools并结合内网穿透实现公网访问本地工具箱服务

作为程序员&#xff0c;在日常工作中&#xff0c;需要借助一些工具来提高我们工作效率&#xff0c;IT-Tools是为开发人员度身打造的一套便捷在线工具。它提供全面功能&#xff0c;使开发者能以更高效方式完成任务。经由IT-Tools&#xff0c;开发人员能轻松应对各类技术挑战&…

基于java SSM springboot动物检疫信息管理系统设计和实现

基于java SSM springboot动物检疫信息管理系统设计和实现 博主介绍&#xff1a;多年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 央顺技术团队 Java毕设项目精品实战案例《500套》 欢迎点赞 收藏 ⭐留言 文末…

代码遗产:探索祖传代码的历史、挑战与现代融合艺术

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua&#xff0c;在这里我会分享我的知识和经验。&#x…

NC65 rest接口 开发 NC65接口开发

一、在对应模块META-INF下编写 xxx.rest 文件,也要放在Home里对应的目录下。 二、开发接口&#xff0c;继承extends AbstractUAPRestResource&#xff0c;&#xff08;有的项目会继承别的方法如&#xff1a;AbstractNCCRestResource&#xff0c;MTFRestResource&#xff1b;有…

adb下载安装及使用教程

adb下载安装及使用教程 一、ADB的介绍1.ADB是什么&#xff1f;2.内容简介3.ADB常用命令1. ADB查看设备2. ADB安装软件3. ADB卸载软件4. ADB登录设备shell5. ADB从电脑上发送文件到设备6. ADB从设备上下载文件到电脑7. ADB显示帮助信息 4.为什么要用ADB 二、ADB的下载1.Windows版…

Vue <component/> 特殊的内置组件使用

在 Vue 中&#xff0c; 是一个特殊的内置组件&#xff0c;它可以根据提供的数据动态地渲染不同的子组件。这个功能非常有用&#xff0c;因为它可以让你在不同的情况下动态地切换和渲染不同的组件。 将is的值绑定为一个变量&#xff0c;is就变成了v-bind:is“变量名”&#xff…

SpringBoot 3 新特性

目录 1. GraalVM1.1 生成本地可执行应用1.2 生成docker镜像 2. 支持虚拟线程2.1 不开启虚拟线程时压测2.2 开启虚拟线程时压测 3. HTTP Interface 1. GraalVM 使用GraalVM将SpringBoot应用程序编译成本地可执行的镜像文件&#xff0c;可以显著提升启动速度、峰值性能以及减少内…

C++ 网络编程学习三

C 网络编程学习三 用智能指针延长session的生命周期处理粘包问题 用智能指针延长session的生命周期 问题&#xff1a; 客户端断开后&#xff1a;会触发服务器对应session的写或读事件&#xff0c;由于是异步编程&#xff0c;需要在回调中对读写事件进行处理。客户端断开&#…

上拉电阻与下拉电阻、电容的作用

上拉电阻与下拉电阻 在单片机电路中&#xff0c;上拉电阻和下拉电阻都是常见的电路元件&#xff0c;它们在数字电路设计中扮演着重要的角色。它们的作用如下&#xff1a; 1. **上拉电阻**&#xff1a; - **作用**&#xff1a;当一个引脚没有外部信号时&#xff0c;上拉电阻…

【数学】【深度优先搜索】【图论】【欧拉环路】753. 破解保险箱

作者推荐 动态规划的时间复杂度优化 本文涉及知识点 数学 深度优先搜索 图论 欧拉环路 LeetCode753. 破解保险箱 有一个需要密码才能打开的保险箱。密码是 n 位数, 密码的每一位都是范围 [0, k - 1] 中的一个数字。 保险箱有一种特殊的密码校验方法&#xff0c;你可以随意…

从C到C++

二、从C到C 本章介绍一些C拓展的非面向对象功能。 引用&#xff08;掌握&#xff09; 1.1 概念 引用从一定程度上讲是一个指针的平替&#xff0c;几乎被所有面向对象编程语言所使用。引用相当于对某一个目标变量起”别名“。 操作引用与操作原变量完全一样。 #include <iost…

12 单口RAM IP核

RAM IP 核简介 RAM 是随机存取存储器&#xff08;Random Access Memory&#xff09;的简称&#xff0c;它是一种易失性存储器&#xff0c;RAM 工作时可以随时从任意一个合法地址写入或读取数据。 Vivado 软件自带的 Block Memory Generator IP 核&#xff08;缩写为 BMG&#…

linux centos7.9改dns和ip

vi /etc/sysconfig/network-scripts/ifcfg-ens32 &#xff1a;wq后 重启网络服务 systemctl restart network —————————————————————————— 篇外话题 软件下载 xshell可以从腾讯软件中心下载

哈登谈失利:这是我们所有人的责任 不能在主场领先18分然后输球

nba直播吧地址&#xff1a;www.gznuqsxy.cn 2月29日讯 今天结束的一场NBA常规赛&#xff0c;快船在主场112比116被湖人逆转。赛后哈登接受了媒体的采访。 哈登谈到了这场失利&#xff1a;“这是我们所有人的责任。这是一场令人失望的失利。你不能在主场领先18分然后让出一场胜…