1)实验平台:正点原子阿尔法Linux开发板
2)平台购买地址:https://item.taobao.com/item.htm?id=603672744434
2)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-300792-1-1.html
3)对正点原子Linux感兴趣的同学可以加群讨论:935446741
4)关注正点原子公众号,获取最新资料更新

第四十四章 设备树下的LED驱动实验

上一章我们详细的讲解了设备树语法以及在驱动开发中常用的OF函数,本章我们就开始第一个基于设备树的Linux驱动实验。本章在第四十二章实验的基础上完成,只是将其驱动开发改为设备树形式而已。

44.1 设备树LED驱动原理
在《第四十二章 新字符设备驱动实验》中,我们直接在驱动文件newchrled.c中定义有关寄存器物理地址,然后使用io_remap函数进行内存映射,得到对应的虚拟地址,最后操作寄存器对应的虚拟地址完成对GPIO的初始化。本章我们在第四十二章实验基础上完成,本章我们使用设备树来向Linux内核传递相关的寄存器物理地址,Linux驱动文件使用上一章讲解的OF函数从设备树中获取所需的属性值,然后使用获取到的属性值来初始化相关的IO。本章实验还是比较简单的,本章实验重点内容如下:
①、在imx6ull-alientek-emmc.dts文件中创建相应的设备节点。
②、编写驱动程序(在第四十二章实验基础上完成),获取设备树中的相关属性值。
③、使用获取到的有关属性值来初始化LED所使用的GPIO。
44.2 硬件原理图分析
本章实验硬件原理图参考8.3小节即可。
44.3 实验程序编写
本实验对应的例程路径为:开发板光盘-> 2、Linux驱动例程-> 4_dtsled。
本章实验在四十二章实验的基础上完成,重点是将驱动改为基于设备树的.
44.3.1 修改设备树文件
在根节点“/”下创建一个名为“alphaled”的子节点,打开imx6ull-alientek-emmc.dts文件,在根节点“/”最后面输入如下所示内容:
示例代码44.3.1.1 alphaled节点

1 alphaled {2       #address-cells = <1>;
3       #size-cells = <1>;
4       compatible = "atkalpha-led";
5       status = "okay";
6       reg = < 0X020C406C 0X04         /* CCM_CCGR1_BASE               */
7                       0X020E0068 0X04         /* SW_MUX_GPIO1_IO03_BASE       */
8                   0X020E02F4 0X04         /* SW_PAD_GPIO1_IO03_BASE       */
9                   0X0209C000 0X04         /* GPIO1_DR_BASE                */
10                  0X0209C004 0X04 >;   /* GPIO1_GDIR_BASE              */
11 };
第2、3行,属性#address-cells和#size-cells都为1,表示reg属性中起始地址占用一个字长(cell),地址长度也占用一个字长(cell)。
第4行,属性compatbile设置alphaled节点兼容性为“atkalpha-led”。
第5行,属性status设置状态为“okay”。
第6~10行,reg属性,非常重要!reg属性设置了驱动里面所要使用的寄存器物理地址,比如第6行的“0X020C406C 0X04”表示I.MX6ULL的CCM_CCGR1寄存器,其中寄存器首地址为0X020C406C,长度为4个字节。
设备树修改完成以后输入如下命令重新编译一下imx6ull-alientek-emmc.dts:

make dtbs
编译完成以后得到imx6ull-alientek-emmc.dtb,使用新的imx6ull-alientek-emmc.dtb启动Linux内核。Linux启动成功以后进入到/proc/device-tree/目录中查看是否有“alphaled”这个节点,结果如图44.3.1.1所示:

图44.3.1.1 alphaled节点
如果没有“alphaled”节点的话请重点查看下面两点:
①、检查设备树修改是否成功,也就是alphaled节点是否为根节点“/”的子节点。
②、检查是否使用新的设备树启动的Linux内核。
可以进入到图44.3.1中的alphaled目录中,查看一下都有哪些属性文件,结果如图44.3.1.2所示:

图44.3.1.2 alphaled节点文件
大家可以查看一下compatible、status等属性值是否和我们设置的一致。
44.3.2 LED灯驱动程序编写
设备树准备好以后就可以编写驱动程序了,本章实验在第四十二章实验驱动文件newchrled.c的基础上修改而来。新建名为“4_dtsled”文件夹,然后在4_dtsled文件夹里面创建vscode工程,工作区命名为“dtsled”。工程创建好以后新建dtsled.c文件,在dtsled.c里面输入如下内容:
示例代码44.3.2.1 dtsled.c文件内容

1   #include <linux/types.h>
2   #include <linux/kernel.h>
3   #include <linux/delay.h>
4   #include <linux/ide.h>
5   #include <linux/init.h>
6   #include <linux/module.h>
7   #include <linux/errno.h>
8   #include <linux/gpio.h>
9   #include <linux/cdev.h>
10  #include <linux/device.h>
11  #include <linux/of.h>
12  #include <linux/of_address.h>
13  #include <asm/mach/map.h>
14  #include <asm/uaccess.h>
15  #include <asm/io.h>
16  /***************************************************************
17  Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
18  文件名     : dtsled.c
19  作者          : 左忠凯
20  版本          : V1.0
21  描述          : LED驱动文件。
22  其他          : 无
23  论坛          : www.openedv.com
24  日志          : 初版V1.0 2019/7/9 左忠凯创建
25  ***************************************************************/
26  #define DTSLED_CNT      1               /* 设备号个数    */
27  #define DTSLED_NAME         "dtsled"  /* 名字       */
28  #define LEDOFF              0               /* 关灯       */
29  #define LEDON               1               /* 开灯       */
30
31  /* 映射后的寄存器虚拟地址指针 */
32  static void __iomem *IMX6U_CCM_CCGR1;
33  static void __iomem *SW_MUX_GPIO1_IO03;
34  static void __iomem *SW_PAD_GPIO1_IO03;
35  static void __iomem *GPIO1_DR;
36  static void __iomem *GPIO1_GDIR;
37
38  /* dtsled设备结构体 */
39  struct dtsled_dev{40      dev_t devid;                /* 设备号      */
41      struct cdev cdev;           /* cdev         */
42      struct class *class;    /* 类            */
43      struct device *device;      /* 设备           */
44      int major;                  /* 主设备号     */
45      int minor;                  /* 次设备号     */
46      struct device_node  *nd; /* 设备节点    */
47  };
48
49  struct dtsled_dev dtsled;   /* led设备 */
50
51  /*
52   * @description    : LED打开/关闭
53   * @param - sta    : LEDON(0) 打开LED,LEDOFF(1) 关闭LED
54   * @return         : 无
55   */
56  void led_switch(u8 sta)
57  {58      u32 val = 0;
59      if(sta == LEDON) {60          val = readl(GPIO1_DR);
61          val &= ~(1 << 3);
62          writel(val, GPIO1_DR);
63      }else if(sta == LEDOFF) {64          val = readl(GPIO1_DR);
65          val|= (1 << 3);
66          writel(val, GPIO1_DR);
67      }
68  }
69
70  /*
71   * @description    : 打开设备
72   * @param – inode  : 传递给驱动的inode
73   * @param – filp   : 设备文件,file结构体有个叫做private_data的成员变量
74   *                    一般在open的时候将private_data指向设备结构体。
75   * @return         : 0 成功;其他 失败
76   */
77  static int led_open(struct inode *inode, struct file *filp)
78  {79      filp->private_data = &dtsled; /* 设置私有数据 */
80      return 0;
81  }
82
83  /*
84   * @description    : 从设备读取数据
85   * @param – filp   : 要打开的设备文件(文件描述符)
86   * @param - buf    : 返回给用户空间的数据缓冲区
87   * @param - cnt    : 要读取的数据长度
88   * @param – offt   : 相对于文件首地址的偏移
89   * @return         : 读取的字节数,如果为负值,表示读取失败
90   */
91  static ssize_t led_read(struct file *filp, char __user *buf,
size_t cnt, loff_t *offt)
92  {93      return 0;
94  }
95
96  /*
97   * @description    : 向设备写数据
98   * @param - filp   : 设备文件,表示打开的文件描述符
99   * @param - buf    : 要写给设备写入的数据
100  * @param - cnt    : 要写入的数据长度
101  * @param – offt   : 相对于文件首地址的偏移
102  * @return         : 写入的字节数,如果为负值,表示写入失败
103  */
104 static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
105 {106     int retvalue;
107     unsigned char databuf[1];
108     unsigned char ledstat;
109
110     retvalue = copy_from_user(databuf, buf, cnt);
111     if(retvalue < 0) {112         printk("kernel write failed!\r\n");
113         return -EFAULT;
114     }
115
116     ledstat = databuf[0];          /* 获取状态值    */
117
118     if(ledstat == LEDON) {
119         led_switch(LEDON);          /* 打开LED灯   */
120     } else if(ledstat == LEDOFF) {121         led_switch(LEDOFF);         /* 关闭LED灯   */
122     }
123     return 0;
124 }
125
126 /*
127  * @description    : 关闭/释放设备
128  * @param – filp   : 要关闭的设备文件(文件描述符)
129  * @return         : 0 成功;其他 失败
130  */
131 static int led_release(struct inode *inode, struct file *filp)
132 {133     return 0;
134 }
135
136 /* 设备操作函数 */
137 static struct file_operations dtsled_fops = {138     .owner = THIS_MODULE,
139     .open = led_open,
140     .read = led_read,
141     .write = led_write,
142     .release =  led_release,
143 };
144
145 /*
146  * @description    : 驱动入口函数
147  * @param          : 无
148  * @return         : 无
149  */
150 static int __init led_init(void)
151 {152     u32 val = 0;
153     int ret;
154     u32 regdata[14];
155     const char *str;
156     struct property *proper;
157
158     /* 获取设备树中的属性数据 */
159     /* 1、获取设备节点:alphaled */
160     dtsled.nd = of_find_node_by_path("/alphaled");
161     if(dtsled.nd == NULL) {162         printk("alphaled node can not found!\r\n");
163         return -EINVAL;
164     } else {165         printk("alphaled node has been found!\r\n");
166     }
167
168     /* 2、获取compatible属性内容 */
169     proper = of_find_property(dtsled.nd, "compatible", NULL);
170     if(proper == NULL) {171         printk("compatible property find failed\r\n");
172     } else {173         printk("compatible = %s\r\n", (char*)proper->value);
174     }
175
176     /* 3、获取status属性内容 */
177     ret = of_property_read_string(dtsled.nd, "status", &str);
178     if(ret < 0){179         printk("status read failed!\r\n");
180     } else {181         printk("status = %s\r\n",str);
182     }
183
184     /* 4、获取reg属性内容 */
185     ret = of_property_read_u32_array(dtsled.nd, "reg", regdata, 10);
186     if(ret < 0) {187         printk("reg property read failed!\r\n");
188     } else {189         u8 i = 0;
190         printk("reg data:\r\n");
191         for(i = 0; i < 10; i++)
192             printk("%#X ", regdata[i]);
193         printk("\r\n");
194     }
195
196     /* 初始化LED */
197 #if 0
198     /* 1、寄存器地址映射 */
199     IMX6U_CCM_CCGR1 = ioremap(regdata[0], regdata[1]);
200     SW_MUX_GPIO1_IO03 = ioremap(regdata[2], regdata[3]);
201     SW_PAD_GPIO1_IO03 = ioremap(regdata[4], regdata[5]);
202     GPIO1_DR = ioremap(regdata[6], regdata[7]);
203     GPIO1_GDIR = ioremap(regdata[8], regdata[9]);
204 #else
205     IMX6U_CCM_CCGR1 = of_iomap(dtsled.nd, 0);
206     SW_MUX_GPIO1_IO03 = of_iomap(dtsled.nd, 1);
207     SW_PAD_GPIO1_IO03 = of_iomap(dtsled.nd, 2);
208     GPIO1_DR = of_iomap(dtsled.nd, 3);
209     GPIO1_GDIR = of_iomap(dtsled.nd, 4);
210 #endif
211
212     /* 2、使能GPIO1时钟 */
213     val = readl(IMX6U_CCM_CCGR1);
214     val &= ~(3 << 26);  /* 清楚以前的设置 */
215     val |= (3 << 26);   /* 设置新值 */
216     writel(val, IMX6U_CCM_CCGR1);
217
218     /* 3、设置GPIO1_IO03的复用功能,将其复用为
219      *    GPIO1_IO03,最后设置IO属性。
220      */
221     writel(5, SW_MUX_GPIO1_IO03);
222
223     /* 寄存器SW_PAD_GPIO1_IO03设置IO属性 */
224     writel(0x10B0, SW_PAD_GPIO1_IO03);
225
226     /* 4、设置GPIO1_IO03为输出功能 */
227     val = readl(GPIO1_GDIR);
228     val &= ~(1 << 3);   /* 清除以前的设置 */
229     val |= (1 << 3);    /* 设置为输出 */
230     writel(val, GPIO1_GDIR);
231
232     /* 5、默认关闭LED */
233     val = readl(GPIO1_DR);
234     val |= (1 << 3);
235     writel(val, GPIO1_DR);
236
237     /* 注册字符设备驱动 */
238     /* 1、创建设备号 */
239     if (dtsled.major) {             /*  定义了设备号 */
240         dtsled.devid = MKDEV(dtsled.major, 0);
241         register_chrdev_region(dtsled.devid, DTSLED_CNT,            DTSLED_NAME);
242     } else {                        /* 没有定义设备号 */
243         alloc_chrdev_region(&dtsled.devid, 0, DTSLED_CNT,  DTSLED_NAME); /* 申请设备号 */
244         dtsled.major = MAJOR(dtsled.devid); /* 获取分配号的主设备号 */
245         dtsled.minor = MINOR(dtsled.devid); /* 获取分配号的次设备号 */
246     }
247     printk("dtsled major=%d,minor=%d\r\n",dtsled.major, dtsled.minor);
248
249     /* 2、初始化cdev */
250     dtsled.cdev.owner = THIS_MODULE;
251     cdev_init(&dtsled.cdev, &dtsled_fops);
252
253     /* 3、添加一个cdev */
254     cdev_add(&dtsled.cdev, dtsled.devid, DTSLED_CNT);
255
256     /* 4、创建类 */
257     dtsled.class = class_create(THIS_MODULE, DTSLED_NAME);
258     if (IS_ERR(dtsled.class)) {259         return PTR_ERR(dtsled.class);
260     }
261
262     /* 5、创建设备 */
263     dtsled.device = device_create(dtsled.class, NULL, dtsled.devid,   NULL, DTSLED_NAME);
264     if (IS_ERR(dtsled.device)) {265         return PTR_ERR(dtsled.device);
266     }
267
268     return 0;
269 }
270
271 /*
272  * @description    : 驱动出口函数
273  * @param          : 无
274  * @return         : 无
275  */
276 static void __exit led_exit(void)
277 {278     /* 取消映射 */
279     iounmap(IMX6U_CCM_CCGR1);
280     iounmap(SW_MUX_GPIO1_IO03);
281     iounmap(SW_PAD_GPIO1_IO03);
282     iounmap(GPIO1_DR);
283     iounmap(GPIO1_GDIR);
284
285     /* 注销字符设备驱动 */
286     cdev_del(&dtsled.cdev);/* 删除cdev */
287     unregister_chrdev_region(dtsled.devid, DTSLED_CNT);/*注销设备号*/
288
289     device_destroy(dtsled.class, dtsled.devid);
290     class_destroy(dtsled.class);
291 }
292
293 module_init(led_init);
294 module_exit(led_exit);
295 MODULE_LICENSE("GPL");
296 MODULE_AUTHOR("zuozhongkai");

dtsled.c文件中的内容和第四十二章的newchrled.c文件中的内容基本一样,只是dtsled.c中包含了处理设备树的代码,我们重点来看一下这部分代码。
第46行,在设备结构体dtsled_dev中添加了成员变量nd,nd是device_node结构体类型指针,表示设备节点。如果我们要读取设备树某个节点的属性值,首先要先得到这个节点,一般在设备结构体中添加device_node指针变量来存放这个节点。
第160~166行,通过of_find_node_by_path函数得到alphaled节点,后续其他的OF函数要使用device_node。
第169~174行,通过of_find_property函数获取alphaled节点的compatible属性,返回值为property结构体类型指针变量,property的成员变量value表示属性值。
第177~182行,通过of_property_read_string函数获取alphaled节点的status属性值。
第185~194行,通过of_property_read_u32_array函数获取alphaled节点的reg属性所有值,并且将获取到的值都存放到regdata数组中。第192行将获取到的reg属性值依次输出到终端上。
第199~203行,使用“古老”的ioremap函数完成内存映射,将获取到的regdata数组中的寄存器物理地址转换为虚拟地址。
第205~209行,使用of_iomap函数一次性完成读取reg属性以及内存映射,of_iomap函数是设备树推荐使用的OF函数。
44.3.3 编写测试APP
本章直接使用第四十二章的测试APP,将上一章的ledApp.c文件复制到本章实验工程下即可。
44.4 运行测试
44.4.1 编译驱动程序和测试APP
1、编译驱动程序
编写Makefile文件,本章实验的Makefile文件和第四十章实验基本一样,只是将obj-m变量的值改为dtsled.o,Makefile内容如下所示:
示例代码44.4.1.1 Makefile文件
1 KERNELDIR := /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek

4 obj-m := dtsled.o

11 clean:
12 $(MAKE) -C (KERNELDIR)M=(KERNELDIR) M=(KERNELDIR)M=(CURRENT_PATH) clean
第4行,设置obj-m变量的值为dtsled.o。
输入如下命令编译出驱动模块文件:
make -j32
编译成功以后就会生成一个名为“dtsled.ko”的驱动模块文件。
2、编译测试APP
输入如下命令编译测试ledApp.c这个测试程序:
arm-linux-gnueabihf-gcc ledApp.c -o ledApp
编译成功以后就会生成ledApp这个应用程序。
44.4.2 运行测试
将上一小节编译出来的dtsled.ko和ledApp这两个文件拷贝到rootfs/lib/modules/4.1.15目录中,重启开发板,进入到目录lib/modules/4.1.15中,输入如下命令加载dtsled.ko驱动模块:
depmod //第一次加载驱动的时候需要运行此命令
modprobe dtsled.ko //加载驱动
驱动加载成功以后会在终端中输出一些信息,如图44.4.2.1所示:

图44.4.2.1 驱动加载成功以后输出的信息
从图44.4.2.1可以看出,alpahled这个节点找到了,并且compatible属性值为“atkalpha-led”,status属性值为“okay”,reg属性的值为“0X20C406C 0X4 0X20E0068 0X4 0X20E02F4 0X4 0X209C000 0X4 0X209C004 0X4”,这些都和我们设置的设备树一致。
驱动加载成功以后就可以使用ledApp软件来测试驱动是否工作正常,输入如下命令打开LED灯:
./ledApp /dev/dtsled 1 //打开LED灯
输入上述命令以后观察I.MX6U-ALPHA开发板上的红色LED灯是否点亮,如果点亮的话说明驱动工作正常。在输入如下命令关闭LED灯:
./ledApp /dev/dtsled 0 //关闭LED灯
输入上述命令以后观察I.MX6U-ALPHA开发板上的红色LED灯是否熄灭。如果要卸载驱动的话输入如下命令即可:
rmmod dtsled.ko

【正点原子Linux连载】第四十四章 设备树下的LED驱动实验 -摘自【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.0相关推荐

  1. linux 正点原子ov5640_【正点原子FPGA连载】第二十六章基于OV5640的二值化实验-摘自【正点原子】领航者 ZYNQ 之嵌入式开发指南 (amobbs.com 阿莫电子论坛)...

    本帖最后由 正点原子 于 2020-10-26 16:21 编辑 QQ群头像.png (1.78 KB) 2020-10-24 10:50 上传5)关注正点原子公众号,获取最新资料 100846rel ...

  2. 【正点原子MP157连载】第十五章 窗口门狗(WWDG)实验-摘自【正点原子】STM32MP1 M4裸机CubeIDE开发指南

    1)实验平台:正点原子STM32MP157开发板 2)购买链接:https://item.taobao.com/item.htm?&id=629270721801 3)全套实验源码+手册+视频 ...

  3. 【正点原子MP157连载】第二十四章 设备树下的LED驱动实验-摘自【正点原子】STM32MP1嵌入式Linux驱动开发指南V1.7

    1)实验平台:正点原子STM32MP157开发板 2)购买链接:https://item.taobao.com/item.htm?&id=629270721801 3)全套实验源码+手册+视频 ...

  4. 【正点原子MP157连载】第三十五章 设备树下的platform驱动编写-摘自【正点原子】STM32MP1嵌入式Linux驱动开发指南V1.7

    1)实验平台:正点原子STM32MP157开发板 2)购买链接:https://item.taobao.com/item.htm?&id=629270721801 3)全套实验源码+手册+视频 ...

  5. 韦东山 IMX6ULL和正点原子_「正点原子Linux连载」第四十四章设备树下的LED驱动实验...

    1)实验平台:正点原子Linux开发板 2)摘自<正点原子I.MX6U嵌入式Linux驱动开发指南> 关注官方微信号公众号,获取更多资料:正点原子 上一章我们详细的讲解了设备树语法以及在驱 ...

  6. 【正点原子FPGA连载】第三十一章RTC实时时钟数码管显示实验 -摘自【正点原子】新起点之FPGA开发指南_V2.1

    1)实验平台:正点原子新起点V2开发板 2)平台购买地址:https://detail.tmall.com/item.htm?id=609758951113 2)全套实验源码+手册+视频下载地址:ht ...

  7. linux键盘设置的文件在哪个文件夹,「正点原子Linux连载」第十五章按键输入试验...

    原标题:「正点原子Linux连载」第十五章按键输入试验 第十五章按键输入试验 前面几章试验都是讲解如何使用I.MX6U的GPIO输出控制功能,I.MX6U的IO不仅能作为输出,而且也可以作为输入.I. ...

  8. linux uart寄存器 代替 printk,Linux驱动学习之设备树(设备树下的LED驱动实验),...

    Linux驱动学习之设备树(设备树下的LED驱动实验), 概念 Linux内核从3.x开始引入设备树的概念,用于实现驱动代码与设备信息相分离.相当于从驱动代码分离出来的配置文件,比如串口的波特率通过设 ...

  9. 【正点原子Linux连载】第四十五章 pinctrl和gpio子系统实验 -摘自【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.0

    1)实验平台:正点原子阿尔法Linux开发板 2)平台购买地址:https://item.taobao.com/item.htm?id=603672744434 2)全套实验源码+手册+视频下载地址: ...

最新文章

  1. MySQL面试题 | 附答案解析(十五)
  2. 如何拼通网络ip地址_如何解决IP地址冲突
  3. Mysql分析-profile详解
  4. Oracle中比较日期大小
  5. 二叉树的深度优先和广度优先遍历
  6. 阿里巴巴HBase高可用8年填坑实录
  7. QQ浏览器如何修改截屏快捷键?QQ浏览器修改截屏快捷键的方法
  8. linux 针对目录空间配额,linux磁盘配额quota
  9. Hemberg-lab单细胞转录组数据分析(六)
  10. linux压缩命令(二)bzip2总结
  11. c语言求行列式的值原理,新手作品:行列式计算C语言版
  12. 五秒原则,做一件事之前数 5 秒,1,2,3,4,5 立马去做。比如睡觉:数五秒,立马放下手机,闭眼。...
  13. 使用X-shell管理员root连接ubuntu17.10服务器拒绝密码的一个失误!
  14. python 大智慧股池_大智慧的一般设置及股票池的安装步骤、使用方法
  15. 《统计学》第八版贾俊平第七章知识点总结及课后习题答案
  16. 福州化工实验室建设注意隐患分析
  17. 【信息系统项目管理师】第十六章 变更管理思维导图
  18. (HTTP 503) (Request-ID: req-4f56de6f-d29c-4c44-aed3-e6ef8253384a)
  19. 提高Java反射速度的方法以及对setAccessable的误解
  20. 基于 8051单片机的线跟随小车

热门文章

  1. Vue实现京东登陆页面(仅实现部分功能)
  2. hdmi网线延长器_HDMI单网线延长器的制作方法
  3. Mac上使用Emacs
  4. 1. Emacs使用本地elpa镜像
  5. 数据结构与算法学习(第一天)
  6. Java 图片加水印
  7. 20. Linux提权:从入门到放弃
  8. Linux5.9下DHCP服务器的配搭建
  9. Verilog实现快递柜
  10. 概率 插空法和捆绑法