zoukankan      html  css  js  c++  java
  • 20150222 IO端口映射和IO内存映射(详解S3C24XX_GPIO驱动)

    20150222 IO端口映射和IO内存映射(详解S3C24XX_GPIO驱动)

    2015-02-22 李海沿

    刚刚我们实现了linux系统内存的分配,读写,释放功能,下面,我们一鼓作气将IO端口映射及IO内存映射搞定,加油!


    (一)地址的概念

    1)物理地址:CPU地址总线传来的地址,由硬件电路控制其具体含义。物理地址中很大一部分是留给内存条中的内存的,但也常被映射到其他存储器上(如显存、BIOS等)。在程序指令中的虚拟地址经过段映射和页面映射后,就生成了物理地址,这个物理地址被放到CPU的地址线上。
            物理地址空间,一部分给物理RAM(内存)用,一部分给总线用,这是由硬件设计来决定的,因此在32 bits地址线的x86处理器中,物理地址空间是232次方,即4GB,但物理RAM一般不能上到4GB,因为还有一部分要给总线用(总线上还挂着别的许多设备)。在PC机中,一般是把低端物理地址给RAM用,高端物理地址给总线用。
    2)总线地址:总线的地址线或在地址周期上产生的信号。外设使用的是总线地址,CPU使用的是物理地址。
            物理地址与总线地址之间的关系由系统的设计决定的。在x86平台上,物理地址就是总线地址,这是因为它们共享相同的地址空间——这句话有点难理解,详见下面的"独立编址"。在其他平台上,可能需要转换/映射。比如:CPU需要访问物理地址是0xfa000的单元,那么在x86平台上,会产生一个PCI总线上对0xfa000地址的访问。因为物理地址和总线地址相同,所以凭眼睛看是不能确定这个地址是用在哪儿的,它或者在内存中,或者是某个卡上的存储单元,甚至可能这个地址上没有对应的存储器。

    3)虚拟地址:现代操作系统普遍采用虚拟内存管理(Virtual Memory Management)机制,这需要MMUMemory Management Unit)的支持。MMU通常是CPU的一部分,如果处理器没有MMU,或者有MMU但没有启用,CPU执行单元发出的内存地址将直接传到芯片引脚上,被内存芯片(物理内存)接收,这称为物理地址(Physical Address),如果处理器启用了MMUCPU执行单元发出的内存地址将被MMU截获,从CPUMMU的地址称为虚拟地址(Virtual Address),而MMU将这个地址翻译成另一个地址发到CPU芯片的外部地址引脚上,也就是将虚拟地址映射成物理地址。
            Linux中,进程的4GB(虚拟)内存分为用户空间、内核空间。用户空间分布为0~3GB(即PAGE_OFFSET,在0X86中它等于0xC0000000),剩下的1G为内核空间。程序员只能使用虚拟地址。系统中每个进程有各自的私有用户空间(03G),这个空间对系统中的其他进程是不可见的。
            CPU发出取指令请求时的地址是当前上下文的虚拟地址,MMU再从页表中找到这个虚拟地址的物理地址,完成取指。同样读取数据的也是虚拟地址,比如mov ax, var. 编译时var就是一个虚拟地址,也是通过MMU从也表中来找到物理地址,再产生总线时序,完成取数据的。


    (二)编址方式
    1)外设都是通过读写设备上的寄存器来进行的,外设寄存器也称为"I/O端口",而IO端口有两种编址方式:独立编址和统一编制。

            统一编址:外设接口中的IO寄存器(即IO端口)与主存单元一样看待,每个端口占用一个存储单元的地址,将主存的一部分划出来用作IO地址空间,如,在 PDP-11中,把最高的4K主存作为IO设备寄存器地址。端口占用了存储器的地址空间,使存储量容量减小。
            统一编址也称为"I/O内存"方式,外设寄存器位于"内存空间"(很多外设有自己的内存、缓冲区,外设的寄存器和内存统称"I/O空间")。
            如,SamsungS3C2440,是32ARM处理器,它的4GB地址空间被外设、RAM等瓜分:
    0x8000 1000    LED 8*8点阵的地址
    0x4800 0000 ~ 0x6000 0000  SFR(特殊暂存器)地址空间
    0x3800 1002   键盘地址
    0x3000 0000 ~ 0x3400 0000  SDRAM空间 
    0x2000 0020 ~ 0x2000 002e  IDE
    0x1900 0300   CS8900

            独立编址(单独编址):IO地址与存储地址分开独立编址,I/0端口地址不占用存储空间的地址范围,这样,在系统中就存在了另一种与存储地址无关的IO址,CPU也必须具有专用与输入输出操作的IO指令(INOUT等)和控制逻辑。独立编址下,地址总线上过来一个地址,设备不知道是给IO端口的、还是给存储器的,于是处理器通过MEMR/MEMWIOR/IOW两组控制信号来实现对I/O端口和存储器的不同寻址。如,intel 80x86就采用单独编址,CPU内存和I/O是一起编址的,就是说内存一部分的地址和I/O地址是重叠的。
            独立编址也称为"I/O端口"方式,外设寄存器位于"I/O(地址)空间"
            对于x86架构来说,通过IN/OUT指令访问。PC架构一共有655368bitI/O端口,组成64KI/O地址空间,编号从 0~0xFFFF,有16位,80x86用低16位地址线A0-A15来寻址。连续两个8bit的端口可以组成一个16bit的端口,连续4个组成一个 32bit的端口。I/O地址空间和CPU的物理地址空间是两个不同的概念,例如I/O地址空间为64K,一个32bitCPU物理地址空间是4G如,在Intel 8086+Redhat9.0 下用"more /proc/ioports"可看到:

    0000-001f : dma1
    0020-003f : pic1
    0040-005f : timer
    0060-006f : keyboard
    0070-007f : rtc
    0080-008f : dma page reg
    00a0-00bf : pic2
    00c0-00df : dma2
    00f0-00ff : fpu
    0170-0177 : ide1
    ……

    不过Intel x86平台普通使用了名为内存映射(MMIO)的技术,该技术是PCI规范的一部分,IO设备端口被映射到内存空间,映射后,CPU访问IO端口就如同访问内存一样。看Intel TA 719文档给出的x86/x64系统典型内存地址分配表:
    系统资源  占用

    BIOS 1M

    本地APIC 4K

    芯片组保留 2M

    IO APIC 4K

    PCI设备 256M

    PCI Express设备 256M

    PCI设备(可选) 256M

    显示帧缓存 16M

    TSEG 1M

    对于某一既定的系统,它要么是独立编址、要么是统一编址,具体采用哪一种则取决于CPU的体系结构。 如,PowerPC、m68k等采用统一编址,而X86等则采用独立编址,存在IO空间的概念。目前,大多数嵌入式微控制器如ARM、PowerPC等并 不提供I/O空间,仅有内存空间,可直接用地址、指针访问。但对于Linux内核而言,它可能用于不同的CPU,所以它必须都要考虑这两种方式,于是它采 用一种新的方法,将基于I/O映射方式的或内存映射方式的I/O端口通称为"I/O区域"(I/O region),不论你采用哪种方式,都要先申请IO区域:request_resource(),结束时释放 它:release_resource()。
     

    2)对外设的访问

    1、访问I/O内存的流程是:request_mem_region() -> ioremap() -> ioread8()/iowrite8() -> iounmap() ->release_mem_region() 
            前面说过,IO内存是统一编址下的概念,对于统一编址,IO地址空间是物理主存的一部分,对于编程而言,我们只能操作虚拟内存,所以,访问的第一步就是要把设备所处的物理地址映射到虚拟地址,Linux2.6下用ioremap():
            void *ioremap(unsigned long offset, unsigned long size);
    然后,我们可以直接通过指针来访问这些地址,但是也可以用Linux内核的一组函数来读写:
    ioread8(), iowrite16(), ioread8_rep(), iowrite8_rep()......

    2、访问I/O端口
            访问IO端口有2种途径:I/O映射方式(I/Omapped)、内存映射方式(Memorymapped)。前一种途径不映射到内存空间,直接使用 intb()/outb()之类的函数来读写IO端口;后一种MMIO是先把IO端口映射到IO内存("内存空间"),再使用访问IO内存的函数来访问 IO端口。
            void ioport_map(unsigned long port, unsigned int count);
    通过这个函数,可以把port开始的count个连续的IO端口映射为一段"内存空间",然后就可以在其返回的地址是像访问IO内存一样访问这些IO端口。

    Linux
    下的IO端口和IO内存
    CPU对外设端口物理地址的编址方式有两种:一种是IO映射方式,另一种是内存映射方式。 
     Linux将基于IO映射方式的和内存映射方式的IO端口统称为IO区域(IO region)。
      IO region仍然是一种IO资源,因此它仍然可以用resource结构类型来描述。

      Linux管理IO region

      1) request_region()

      把一个给定区间的IO端口分配给一个IO设备。

      2) check_region()

      检查一个给定区间的IO端口是否空闲,或者其中一些是否已经分配给某个IO设备。

      3) release_region()

      释放以前分配给一个IO设备的给定区间的IO端口。

      Linux中可以通过以下辅助函数来访问IO端口:

      inb(),inw(),inl(),outb(),outw(),outl()

      "b""w""l"分别代表8位,16位,32位。

     IO内存资源的访问

      1) request_mem_region()

      请求分配指定的IO内存资源。

      2) check_mem_region()

      检查指定的IO内存资源是否已被占用。

      3) release_mem_region()

      释放指定的IO内存资源。

      其中传给函数的start address参数是内存区的物理地址(以上函数参数表已省略)。

      驱动开发人员可以将内存映射方式的IO端口和外设内存统一看作是IO内存资源。

      ioremap()用来将IO资源的物理地址映射到内核虚地址空间(3GB - 4GB)中,参数addr是指向内核虚地址的指针。

      Linux中可以通过以下辅助函数来访问IO内存资源:

      readb(),readw(),readl(),writeb(),writew(),writel()

      Linuxkernel/resource.c文件中定义了全局变量ioport_resourceiomem_resource,来分别描述基于IO映射方式的整个IO端口空间和基于内存映射方式的IO内存资源空间(包括IO端口和外设内存)。

    1)关于IO与内存空间:
        X86处理器中存在着I/O空间的概念,I/O空间是相对于内存空间而言的,它通过特定的指令inout来访问。端口号标识了外设的寄存器地址。Intel语法的inout指令格式为:
        IN 累加器, {端口号│DX}
        OUT {端口号│DX},累加器
        目前,大多数嵌入式微控制器如ARMPowerPC等中并不提供I/O空间,而仅存在内存空间。内存空间可以直接通过地址、指针来访问,程序和程序运行中使用的变量和其他数据都存在于内存空间中。 
        即便是在X86处理器中,虽然提供了I/O空间,如果由我们自己设计电路板,外设仍然可以只挂接在内存空间。此时,CPU可以像访问一个内存单元那样访问外设I/O端口,而不需要设立专门的I/O指令。因此,内存空间是必须的,而I/O空间是可选的。

    2inboutb

    Linux设备驱动中,宜使用Linux内核提供的函数来访问定位于I/O空间的端口,这些函数包括:
    · 读写字节端口(8位宽)
    unsigned inb(unsigned port); 
    void outb(unsigned char byte, unsigned port); 
    · 读写字端口(16位宽)
    unsigned inw(unsigned port); 
    void outw(unsigned short word, unsigned port); 
    · 读写长字端口(32位宽)
    unsigned inl(unsigned port); 
    void outl(unsigned longword, unsigned port); 
    · 读写一串字节
    void insb(unsigned port, void *addr, unsigned long count); 
    void outsb(unsigned port, void *addr, unsigned long count);
    · insb()从端口port开始读count个字节端口,并将读取结果写入addr指向的内存;outsb()addr指向的内存的count个字节连续地写入port开始的端口。
    · 读写一串字
    void insw(unsigned port, void *addr, unsigned long count); 
    void outsw(unsigned port, void *addr, unsigned long count); 
    · 读写一串长字
    void insl(unsigned port, void *addr, unsigned long count); 
    void outsl(unsigned port, void *addr, unsigned long count); 
    上述各函数中I/O端口号port的类型高度依赖于具体的硬件平台,因此,只是写出了unsigned

     

     

    以上知识点摘自: http://www.embeddedlinux.org.cn/html/yingjianqudong/201304/20-2556.html

    这儿我不献丑了,因为上面的已经写的很好了。

     

     

    (四)Linux下的IO端口和IO内存

    由于我本人对IMX257的IO内存分配不太了解,曾经询问过很多周立功公司那些所谓的技术人员,每个人回答的答案都是,"你好,我们提供的资料就只是官网上的光盘资料了哦",每次碰到这种话,我真的很无语,我只能说,好吧,只能说是我智商太低了,那些资料太高深了。

    好了,废话也不多说了,这次,我就针对S3C24XX的gpio驱动程序来讲解io端口的分配。

     

    1. 定义一些寄存器(数据寄存器,配置寄存器)

    上图中,gpio_va就是我们GPIO的基址,是用来确定GPFCON配置寄存器和GPFDAT数据寄存器的指针地址的

    ,其初始化我们在init函数中会讲解。

     

    2.在init函数中确定gpio基址

    前面我们寄存器的正常作用的前提就是gpio_va这个基址,所以我们在init函数中分配IO端口

    如图所示,将gpio的地址0x56000000这个io端口分配为gpio_va,这样也就确定了,前面的配置寄存器和数据寄存器。

     

    3.在open函数中配置GPIO

    和单片机程序一样,使用GPIO都要先配置GPIO,原理一样:

    给GPIO引脚赋初值,以后对GPIO引脚的操作,直接用这个寄存器赋值就可以了

     

    4.在exit函数中释放IO端口

    使用完GPIO引脚后,要将我们申请的IO端口释放掉。

     

    总结一下,很简单,申请,配置,释放

    申请IO端口 à 配置GPIO端口为输出(入) à 给数据寄存器赋值(读取) à 释放IO端口

      附上驱动程序:

      1 #include <linux/module.h>
      2 #include <linux/kernel.h>
      3 #include <linux/fs.h>
      4 #include <linux/init.h>
      5 #include <linux/delay.h>
      6 #include <asm/uaccess.h>
      7 #include <asm/irq.h>
      8 #include <asm/io.h>
      9 #include <asm/arch/regs-gpio.h>
     10 #include <asm/hardware.h>
     11 
     12 #define DEVICE_NAME     "leds"  
     13 /* 加载模式后,执行”cat /proc/devices”命令看到的设备名称 */
     14 #define LED_MAJOR       231     /* 主设备号 */
     15 
     16 static struct class *leds_class;
     17 static struct class_device    *leds_class_devs[4];
     18 
     19 /* bit0<=>D10, 0:亮, 1:灭 
     20  *  bit1<=>D11, 0:亮, 1:灭 
     21  *  bit2<=>D12, 0:亮, 1:灭 
     22  */ 
     23 static char leds_status = 0x0;  
     24 static DECLARE_MUTEX(leds_lock); // 定义赋值
     25 
     26 //static int minor;
     27 static unsigned long gpio_va;  //gpio基址
     28 #define GPIO_OFT(x) ((x) - 0x56000000)
     29 //GPIO 配置寄存器
     30 #define GPFCON  (*(volatile unsigned long *)(gpio_va + GPIO_OFT(0x56000050)))
     31 //GPIO 数据寄存器
     32 #define GPFDAT  (*(volatile unsigned long *)(gpio_va + GPIO_OFT(0x56000054)))
     33 /* 应用程序对设备文件/dev/leds执行open(...)时,
     34  * 就会调用s3c24xx_leds_open函数
     35  */
     36 static int s3c24xx_leds_open(struct inode *inode, struct file *file)
     37 {
     38     int minor = MINOR(inode->i_rdev); //MINOR(inode->i_cdev);
     39     switch(minor)
     40     {
     41         case 0: /* /dev/leds */
     42         {
     43             // 配置3引脚为输出
     44             //s3c2410_gpio_cfgpin(S3C2410_GPF4, S3C2410_GPF4_OUTP);
     45             GPFCON &= ~(0x3<<(4*2));
     46             GPFCON |= (1<<(4*2));
     47             
     48             //s3c2410_gpio_cfgpin(S3C2410_GPF5, S3C2410_GPF5_OUTP);
     49             GPFCON &= ~(0x3<<(5*2));
     50             GPFCON |= (1<<(5*2));
     51 
     52             //s3c2410_gpio_cfgpin(S3C2410_GPF6, S3C2410_GPF6_OUTP);
     53             GPFCON &= ~(0x3<<(6*2));
     54             GPFCON |= (1<<(6*2));
     55 
     56             // 都输出0
     57             //s3c2410_gpio_setpin(S3C2410_GPF4, 0);
     58             GPFDAT &= ~(1<<4);
     59             
     60             //s3c2410_gpio_setpin(S3C2410_GPF5, 0);
     61             GPFDAT &= ~(1<<5);
     62             //s3c2410_gpio_setpin(S3C2410_GPF6, 0);
     63             GPFDAT &= ~(1<<6);
     64 
     65             down(&leds_lock);
     66             leds_status = 0x0;
     67             up(&leds_lock);
     68                 
     69             break;
     70         }
     71 
     72         case 1: /* /dev/led1 */
     73         {
     74             s3c2410_gpio_cfgpin(S3C2410_GPF4, S3C2410_GPF4_OUTP);
     75             s3c2410_gpio_setpin(S3C2410_GPF4, 0);
     76             
     77             down(&leds_lock);
     78             leds_status &= ~(1<<0);
     79             up(&leds_lock);
     80             
     81             break;
     82         }
     83 
     84         case 2: /* /dev/led2 */
     85         {
     86             s3c2410_gpio_cfgpin(S3C2410_GPF5, S3C2410_GPF5_OUTP);
     87             s3c2410_gpio_setpin(S3C2410_GPF5, 0);
     88             leds_status &= ~(1<<1);
     89             break;
     90         }
     91 
     92         case 3: /* /dev/led3 */
     93         {
     94             s3c2410_gpio_cfgpin(S3C2410_GPF6, S3C2410_GPF6_OUTP);
     95             s3c2410_gpio_setpin(S3C2410_GPF6, 0);
     96 
     97             down(&leds_lock);
     98             leds_status &= ~(1<<2);
     99             up(&leds_lock);
    100             
    101             break;
    102         }
    103     }    
    104     return 0;
    105 }
    106 static int s3c24xx_leds_read(struct file *filp, char __user *buff, 
    107                                          size_t count, loff_t *offp)
    108 {
    109     int minor = MINOR(filp->f_dentry->d_inode->i_rdev);
    110     char val;
    111 
    112     switch (minor)
    113     {
    114         case 0: /* /dev/leds */
    115         {
    116             copy_to_user(buff, (const void *)&leds_status, 1);
    117             break;
    118         }
    119         case 1: /* /dev/led1 */
    120         {
    121             down(&leds_lock);
    122             val = leds_status & 0x1;
    123             up(&leds_lock);
    124             copy_to_user(buff, (const void *)&val, 1);
    125             break;
    126         }
    127         case 2: /* /dev/led2 */
    128         {
    129             down(&leds_lock);
    130             val = (leds_status>>1) & 0x1;
    131             up(&leds_lock);
    132             copy_to_user(buff, (const void *)&val, 1);
    133             break;
    134         }
    135         case 3: /* /dev/led3 */
    136         {
    137             down(&leds_lock);
    138             val = (leds_status>>2) & 0x1;
    139             up(&leds_lock);
    140             copy_to_user(buff, (const void *)&val, 1);
    141             break;
    142         }
    143     }
    144     return 1;
    145 }
    146 static ssize_t s3c24xx_leds_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
    147 {
    148     //int minor = MINOR(inode->i_rdev); //MINOR(inode->i_cdev);
    149     int minor = MINOR(file->f_dentry->d_inode->i_rdev);
    150     char val;
    151     copy_from_user(&val, buf, 1);
    152     switch (minor)
    153     {
    154         case 0: /* /dev/leds */
    155         {            
    156             s3c2410_gpio_setpin(S3C2410_GPF4, (val & 0x1));
    157             s3c2410_gpio_setpin(S3C2410_GPF5, (val & 0x1));
    158             s3c2410_gpio_setpin(S3C2410_GPF6, (val & 0x1));
    159             down(&leds_lock);
    160             leds_status = val;
    161             up(&leds_lock);
    162             break;
    163         }
    164         case 1: /* /dev/led1 */
    165         {
    166             s3c2410_gpio_setpin(S3C2410_GPF4, val);
    167             if (val == 0)
    168             {
    169                 down(&leds_lock);
    170                 leds_status &= ~(1<<0);
    171                 up(&leds_lock);
    172             }
    173             else
    174             {
    175                 down(&leds_lock);
    176                 leds_status |= (1<<0);                
    177                 up(&leds_lock);
    178             }
    179             break;
    180         }
    181         case 2: /* /dev/led2 */
    182         {
    183             s3c2410_gpio_setpin(S3C2410_GPF5, val);
    184             if (val == 0)
    185             {
    186                 down(&leds_lock);
    187                 leds_status &= ~(1<<1);
    188                 up(&leds_lock);
    189             }
    190             else
    191             {
    192                 down(&leds_lock);
    193                 leds_status |= (1<<1);                
    194                 up(&leds_lock);
    195             }
    196             break;
    197         }
    198         case 3: /* /dev/led3 */
    199         {
    200             s3c2410_gpio_setpin(S3C2410_GPF6, val);
    201             if (val == 0)
    202             {
    203                 down(&leds_lock);
    204                 leds_status &= ~(1<<2);
    205                 up(&leds_lock);
    206             }
    207             else
    208             {
    209                 down(&leds_lock);
    210                 leds_status |= (1<<2);                
    211                 up(&leds_lock);
    212             }
    213             break;
    214         }
    215     }
    216     return 1;
    217 }
    218 /* 这个结构是字符设备驱动程序的核心
    219  * 当应用程序操作设备文件时所调用的open、read、write等函数,
    220  * 最终会调用这个结构中指定的对应函数
    221  */
    222 static struct file_operations s3c24xx_leds_fops = {
    223     .owner  =   THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
    224     .open   =   s3c24xx_leds_open,     
    225     .read    =    s3c24xx_leds_read,       
    226     .write    =    s3c24xx_leds_write,       
    227 };
    228 
    229 /*
    230  * 执行insmod命令时就会调用这个函数 
    231  */
    232 static int __init s3c24xx_leds_init(void)
    233 {
    234     int ret;
    235     int minor = 0;
    236 
    237     gpio_va = ioremap(0x56000000, 0x100000);
    238     if (!gpio_va) {
    239         return -EIO;
    240     }
    241     /* 注册字符设备
    242      * 参数为主设备号、设备名字、file_operations结构;
    243      * 这样,主设备号就和具体的file_operations结构联系起来了,
    244      * 操作主设备为LED_MAJOR的设备文件时,就会调用s3c24xx_leds_fops中的相关成员函数
    245      * LED_MAJOR可以设为0,表示由内核自动分配主设备号
    246      */
    247     ret = register_chrdev(LED_MAJOR, DEVICE_NAME, &s3c24xx_leds_fops);
    248     if (ret < 0) {
    249       printk(DEVICE_NAME " can't register major number
    ");
    250       return ret;
    251     }
    252     leds_class = class_create(THIS_MODULE, "leds");
    253     if (IS_ERR(leds_class))
    254         return PTR_ERR(leds_class);
    255     
    256     leds_class_devs[0] = class_device_create(leds_class, NULL, MKDEV(LED_MAJOR, 0), NULL, "leds");
    257     
    258     for (minor = 1; minor < 4; minor++)
    259     {
    260         leds_class_devs[minor] = class_device_create(leds_class, NULL, MKDEV(LED_MAJOR, minor), NULL, "led%d", minor);
    261         if (unlikely(IS_ERR(leds_class_devs[minor])))
    262             return PTR_ERR(leds_class_devs[minor]);
    263     }
    264         
    265     printk(DEVICE_NAME " initialized
    ");
    266     return 0;
    267 }
    268 
    269 /*
    270  * 执行rmmod命令时就会调用这个函数 
    271  */
    272 static void __exit s3c24xx_leds_exit(void)
    273 {
    274     int minor;
    275     /* 卸载驱动程序 */
    276     unregister_chrdev(LED_MAJOR, DEVICE_NAME);
    277 
    278     for (minor = 0; minor < 4; minor++)
    279     {
    280         class_device_unregister(leds_class_devs[minor]);
    281     }
    282     class_destroy(leds_class);
    283     //释放IO端口
    284     iounmap(gpio_va);
    285 }
    286 
    287 /* 这两行指定驱动程序的初始化函数和卸载函数 */
    288 module_init(s3c24xx_leds_init);
    289 module_exit(s3c24xx_leds_exit);
    290 
    291 /* 描述驱动程序的一些信息,不是必须的 */
    292 MODULE_LICENSE("GPL");
    View Code

      

  • 相关阅读:
    SharePoint 2013 中的SQL Server 安全
    SharePoint 2013 的HTML5特性之响应式布局
    SharePoint 2013 一些小技巧
    SharePoint 2013 排错之"Code blocks are not allowed in this file"
    SharePoint 2013 创建搜索中心及搜索设置
    SharePoint 2013 使用PowerShell创建State Service
    SharePoint 2013 内容部署功能简介
    SharePoint 使用PowerShell恢复误删的网站集
    SharePoint 自定义WebPart之间的连接
    linux之misc及使用misc创建字符设备
  • 原文地址:https://www.cnblogs.com/lihaiyan/p/4297769.html
Copyright © 2011-2022 走看看