• 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

      

  • 相关阅读:
    JavaScript对数组的处理(一)
    PHP数组排序函数array_multisort()函数详解(二)
    PHP数组排序函数array_multisort()函数详解(一)
    jquery html动态添加的元素绑定事件详解
    SpringBoot 通用Error设计
    SpringBoot Lombok
    VMware Big Data Extensions 安装步骤
    SpringBoot 通用Validator
    SpringBoot Mybatis keyProperty和useGeneratedKeys的作用
    Introduction to vSphere Integrated Containers
  • 原文地址:https://www.cnblogs.com/lihaiyan/p/4297769.html
Copyright © 2020-2023  润新知