• ARM Linux驱动篇 学习温度传感器ds18b20的驱动编写过程


    ARM Linux驱动篇 学习温度传感器ds18b20的驱动编写过程

    原文地址:http://www.cnblogs.com/NickQ/p/9026545.html

    一、开发板与ds18b20的入门

    ds18B20是常用的数字温度传感器,具有体积小,硬件开销低,抗干扰能力强,精度高的特点。但楼主在使用过程中发现,ds18b20测量的温度还是需要进行一定的软件校准的。后面我们会谈论到。

    除了上面提到的,ds18b20还有很多可圈可点的有点。下面说楼主所关注到的几个。

    1. 单总线协议,称为总线,必然可以挂载很多设备,但却只占用一个IO口。这对于缺乏IO资源的设备来说,就像是救命稻草。
    2. 可以由用户自己权衡测量精度和测量时间。根据Datasheet,18B20控制寄存器有两位是用来控制精度和测量时间的。如下图。
      下图表明:用户可以在精度9bit-12bit中,自由切换,这也对应着93.75ms,187.5ms,375ms,750ms四个最大测量时间。也就是说9bit精度意味着,最大测量时间最有93.75ms(对于缓慢变化的温度来说,这已经很快了),但只可以精确到0.25摄氏度。12bit精度意味着,虽然最大测量时间有750ms,但精度却能达到0.0625(750ms对于温度测量不能算很慢,但换来的这个精度却是不低的)。
      测量精度与测量时间
    3. 可以使用寄生电源供电。这也是这个芯片突出的地方。这意味着可以不连接电源线,也为PCB布板,多设备走线省去了很多方便。

    二、开发板的硬件电路和寄存器

    楼主这里使用的是飞凌2440开发板,做学习之用。
    电路连接图如下
    电路连接图
    这个板子上是接了电源和外部上拉。事实上这个电源可以由寄生电源,即由信号线DQ上的外部上拉提供。
    图中,也可以看出信号线DQ是连接在了GPG0口。

    寄存器说明图
    寄存器物理地址:
    reg_addr
    寄存器配置说明
    GPGCON-GPG0
    reg2
    GPGDAT AND GPGUP
    reg3

    三、 驱动实现

    #include <linux/module.h>   /* Every Linux kernel module must include this head */
    #include <linux/init.h>     /* Every Linux kernel module must include this head */
    #include <linux/kernel.h>   /* printk() */
    #include <linux/fs.h>       /* struct fops */
    #include <linux/errno.h>    /* error codes */
    #include <linux/cdev.h>     /* cdev_alloc()  */
    #include <asm/io.h>         /* ioremap()  */
    #include <linux/ioport.h>   /* request_mem_region() */
    #include <linux/delay.h>     
    #include <linux/moduleparam.h>      
    #include <linux/types.h>     
    #include <mach/regs-gpio.h>     
    #include <mach/hardware.h>     
    #include <asm/uaccess.h>     
    #include <linux/gpio.h>     
    #include <linux/device.h>    
    
    //定义驱动模块信息
    //模块作者和描述
    #define DRV_AUTHOR                "Nick <nickxq@qq.com>"
    #define DRV_DESC                  "S3C24XX 18B20 driver"
    
    //模块名
    #define DEV_NAME                  "s3c18b20"
    
    //模块版本信息(只作用于安装和卸载的打印信息中)
    #define DRV_MAJOR_VER             1
    #define DRV_MINOR_VER             0
    #define DRV_REVER_VER             0
    
    
    //定义GPG口的寄存器地址,注意偏移地址在程序中的使用
    #define S3C_GPG_BASE              0x56000060           //寄存器物理地址基地址
    #define GPGCON_OFFSET             0
    #define GPGDAT_OFFSET             4
    #define GPGUP_OFFSET              8
    #define S3C_GPG_LEN               0x10   /* 0x56000060~0x56000070  */ //此处定义的大小包括了四个寄存器地址空间,即4*4字节(包括保留的寄存器地址)
    
    //定义ds18b20 DQ线对应的端口的GPIO编号。例如:GPG0 GPIO编号为0
    #define GPIO_NUM_18B20            0      //18B20 PORT is PG0 
    
    //定义函数操作的参数
    //GPIO_Mode 
    #define GPIO_MODE_INPUT           0x00
    #define GPIO_MODE_OUTPUT          0x01
    #define GPIO_MODE_EINT            0x10
    
    //GPIO_STATUS
    #define GPIO_STATUS_LOW           0
    #define GPIO_STATUS_HIGH          1
    
    //GPIO_PULLUP
    #define GPIO_PULLUP_ENABLE        0
    #define GPIO_PULLUP_DISABLE       1
    
    #define DISABLE                   0
    #define ENABLE                    1
    
    //定义函数宏
    #define s3c_gpio_read(reg)      __raw_readl((reg)+s3c_gpg_membase)
    #define s3c_gpio_write(val,reg) __raw_writel((val),(reg)+s3c_gpg_membase)
    
    //设置GPIO模式 参数:操作寄存器的偏移地址、GPIO编号、设置的状态(取值应为上述的宏)
    #define s3c_18b20_gpio_mode(gpio_mode)       s3c2440_gpio_cfgpin_mode(GPGCON_OFFSET,GPIO_NUM_18B20, gpio_mode)
    #define s3c_18b20_gpio_setsta(gpio_status)   s3c2440_gpio_cfgpin_status(GPGDAT_OFFSET,GPIO_NUM_18B20, gpio_status)
    #define s3c_18b20_gpio_getsta()              s3c2440_gpio_getpin_status(GPGDAT_OFFSET,GPIO_NUM_18B20)
    #define s3c_18b20_gpio_pullup(gpio_pullup)   s3c2440_gpio_cfgpin_pullup(GPGUP_OFFSET,GPIO_NUM_18B20, gpio_pullup)
    
    //全局变量的定义
    //设备数量、主设备好、次设备号(此处主设备号可由静态给定,只要不为0即可。若主设备号为0,则动态申请)
    int dev_count = 1;
    int dev_major = 0;
    int dev_minor = 0;
    
    int debug = DISABLE;
    
    //定义存储 映射的虚拟地址空间地址的起始地址 变量
    static void __iomem   *s3c_gpg_membase;
    static struct cdev    *s3c_18b20_cdev;
    
    //设置GPIO模式,此函数由上述的带参数宏调用
    static int s3c2440_gpio_cfgpin_mode(unsigned long gpio_addr,unsigned char gpio_num, unsigned char gpio_mode)
    {
    	volatile unsigned long  gpg_con;
    	
    	if((GPIO_MODE_INPUT != gpio_mode) && (GPIO_MODE_OUTPUT != gpio_mode) && (GPIO_MODE_EINT != gpio_mode))
    	{
    	    return -1;
    	}
    	
    	/* Set GPxCON register, set correspond GPIO port as input or output mode  */
    	gpg_con =  s3c_gpio_read(gpio_addr);  //此处的gpio_addr是偏移地址,调用此宏后会根据s3c_gpg_membase转换为绝对虚拟地址
    	gpg_con &= ~(0x3<<(2*gpio_num));      /* Clear the currespond GPIO configure register */
    	gpg_con |= gpio_mode<<(2*gpio_num);   /* Set the currespond GPIO as output mode */
    
    	//带参数宏,实现将gpgdat写入gpio_addr。
    	s3c_gpio_write(gpg_con,gpio_addr);    //此处的gpio_addr是偏移地址,调用此宏后会根据s3c_gpg_membase转换为绝对虚拟地址
    	
    	return 0;
    }
    
    //设置GPIO引脚电平状态,此函数由上述的带参数宏调用
    static int s3c2440_gpio_cfgpin_status(unsigned long gpio_addr,unsigned char gpio_num, unsigned char gpio_status)
    {
    	volatile unsigned long  gpg_dat;
    	
    	if((GPIO_STATUS_LOW != gpio_status) && (GPIO_STATUS_HIGH != gpio_status))
    	{
    		return -1;
    	}
    
    	/* Set GPxDAT register, set correspond GPIO port power level as high level or low level */
    	gpg_dat = s3c_gpio_read(gpio_addr); //此处的gpio_addr是偏移地址,调用此宏后会根据s3c_gpg_membase转换为绝对虚拟地址
    	
    	if(GPIO_STATUS_LOW == gpio_status)
    	{
    	    gpg_dat &= ~(0x1<<gpio_num); /* This port set to low level */
    	}
    	else
    	{
    	    gpg_dat |= (0x1<<gpio_num); /* This port set to high level*/
    	}
    	
        //带参数宏,实现将gpgdat写入gpio_addr。
    	s3c_gpio_write(gpg_dat,gpio_addr);  //此处的gpio_addr是偏移地址,调用此宏后会根据s3c_gpg_membase转换为绝对虚拟地址
    	
    	return 0;
    }
    
    //设置GPIO上拉状态,此函数由上述的带参数宏调用
    static int s3c2440_gpio_cfgpin_pullup(unsigned long gpio_addr,unsigned char gpio_num, unsigned char gpio_pullup)
    {
    	volatile unsigned long  gpg_up;
    	
    	if((GPIO_PULLUP_ENABLE != gpio_pullup) && (GPIO_PULLUP_DISABLE != gpio_pullup))
    	{
    		return -1;
    	}
    	
    	/* Set GPxUP register, set correspond GPIO port pull up resister as enable or disable  */
    	gpg_up = s3c_gpio_read(gpio_addr);
    	
    	if(GPIO_PULLUP_ENABLE == gpio_pullup)
    	{
            gpg_up &= ~(0x1<<gpio_num); /* Enable pull up resister */
    	}
    	else
    	{
            gpg_up |= (0x1<<gpio_num);  /* Disable pull up resister */
    	}
    	
    	s3c_gpio_write(gpg_up,gpio_addr);
    	
    	return 0;
    }
    
    //读取GPIO引脚电平状态,此函数由上述的带参数宏调用
    static int s3c2440_gpio_getpin_status(unsigned long gpio_addr,unsigned char gpio_num)
    {
    	volatile unsigned long  gpg_dat;
    	
    	/* Get GPxDAT register, get correspond GPIO port power level as high level or low level */
    	gpg_dat = s3c_gpio_read(gpio_addr);
    	
    	gpg_dat &= (0x1<<gpio_num);  
    	
    	if(gpg_dat)
    	{
            return GPIO_STATUS_HIGH;
    	}
    	else
    	{
    	    return GPIO_STATUS_LOW;
    	}
    }
    
    //向内核申请4*4个字节的虚拟地址空间,并与寄存器物理地址绑定映射
    static int s3c_18b20_addr_init(void)
    {
    	
        if(!request_mem_region(S3C_GPG_BASE, S3C_GPG_LEN, "s3c2440 18b20"))
        {
            return -EBUSY;
        }
    
        if( !(s3c_gpg_membase=ioremap(S3C_GPG_BASE, S3C_GPG_LEN)) )
        {
            release_mem_region(S3C_GPG_BASE, S3C_GPG_LEN);
            return -ENOMEM;
        }
    
        return 0;
    }
    
    //释放虚拟地址,解除地址映射
    static void s3c_18b20_addr_release(void)
    {
        release_mem_region(S3C_GPG_BASE, S3C_GPG_LEN);
    	
        iounmap(s3c_gpg_membase);
    }
    
    //写ds18b20复位时序
    static int s3c_ds18b20_clk_reset(void)    
    {    
        int retval = 0;    
    
        s3c_18b20_gpio_mode(GPIO_MODE_OUTPUT);    
        s3c_18b20_gpio_pullup(GPIO_PULLUP_ENABLE);    
    
        s3c_18b20_gpio_setsta(GPIO_STATUS_HIGH);    
        udelay(2);    
        s3c_18b20_gpio_setsta(GPIO_STATUS_LOW); // 拉低ds18b20总线,复位ds18b20     
        udelay(500);                // 保持复位电平500us     
    
        s3c_18b20_gpio_setsta(GPIO_STATUS_HIGH); // 释放ds18b20总线     
        udelay(60);    
    
        // 若复位成功,ds18b20发出存在脉冲(低电平,持续60~240us)     
        s3c_18b20_gpio_mode(GPIO_MODE_INPUT);     
        retval = s3c_18b20_gpio_getsta();
    
        udelay(500);    
        s3c_18b20_gpio_mode(GPIO_MODE_OUTPUT);     
        s3c_18b20_gpio_pullup(GPIO_PULLUP_ENABLE);     
        s3c_18b20_gpio_setsta(GPIO_STATUS_HIGH); // 释放总线     
    
        return retval;    
    }  
    
    //ds18b20写数据时序
    static void s3c_ds18b20_clk_write_byte(unsigned char data)    
    {    
        int i = 0,flag = 0;    
    
        s3c_18b20_gpio_mode(GPIO_MODE_OUTPUT);    
        s3c_18b20_gpio_pullup(GPIO_PULLUP_DISABLE);    
    
        for (i = 0; i < 8; i++)    
        {    
            // 总线从高拉至低电平时,就产生写时隙     
            s3c_18b20_gpio_setsta(GPIO_STATUS_HIGH);     
            udelay(2);    
            s3c_18b20_gpio_setsta(GPIO_STATUS_LOW);   
    		flag = data & 0x01;
            if(flag)		
    		{
    			s3c_18b20_gpio_setsta(GPIO_STATUS_HIGH); 
    		}
            else
    		{
    			s3c_18b20_gpio_setsta(GPIO_STATUS_LOW); 
    		}  
            udelay(60);    
            data >>= 1;    
        }    
        s3c_18b20_gpio_setsta(GPIO_STATUS_HIGH); // 重新释放ds18b20总线     
    }   
    
    //ds18b20读数据时序
    static unsigned char s3c_ds18b20_clk_read_byte(void)    
    {    
        int i;    
        unsigned char data = 0;    
    
        for (i = 0; i < 8; i++)    
        {    
            // 总线从高拉至低,只需维持低电平17ts,再把总线拉高,就产生读时隙     
            s3c_18b20_gpio_mode(GPIO_MODE_OUTPUT);   
            s3c_18b20_gpio_pullup(GPIO_PULLUP_ENABLE);   
            s3c_18b20_gpio_setsta(GPIO_STATUS_HIGH);  
            udelay(2);    
            s3c_18b20_gpio_setsta(GPIO_STATUS_LOW);   
            udelay(2);    
            s3c_18b20_gpio_setsta(GPIO_STATUS_HIGH);   
            udelay(8);    
            data >>= 1;    
            s3c_18b20_gpio_mode(GPIO_MODE_INPUT);    
            if (s3c_18b20_gpio_getsta())    
                data |= 0x80;    
            udelay(50);    
        }    
        s3c_18b20_gpio_mode(GPIO_MODE_OUTPUT);   
        s3c_18b20_gpio_pullup(GPIO_PULLUP_ENABLE);   
        s3c_18b20_gpio_setsta(GPIO_STATUS_HIGH); // 释放ds18b20总线     
        return data;    
    }  
    
    
    //内核的调用接口
    static int s3c_18b20_open(struct inode *inode, struct file *file)
    {
        int flag = 0;    
    
        printk(KERN_ERR "open start
    ");
    
        flag = s3c_ds18b20_clk_reset();    
        
        printk(KERN_ERR "ds18b20 reset is %d
    ",flag);
        
        if (flag & 0x01)    
        {    
            printk(KERN_WARNING "open ds18b20 failed
    ");    
            return -1;    
        }    
        printk(KERN_NOTICE "open ds18b20 successful
    ");    
        return 0;    
    }
    
    //内核的调用接口
    static int s3c_18b20_release(struct inode *inode, struct file *file)
    {
        printk(KERN_DEBUG "/dev/s3c_18b20%d closed.
    ", iminor(inode));
    
        return 0;
    }
    
    //内核的调用接口
    static ssize_t s3c_18b20_read(struct file *filp, char __user * buf, size_t count, loff_t * f_pos) 
    {
        int flag;    
        unsigned long err;    
        unsigned char result[2] = { 0x00, 0x00 };    
    	
        flag = s3c_ds18b20_clk_reset(); 
    	
        if (flag & 0x01)    
        {    
            printk(KERN_WARNING "ds18b20 init failed
    ");    
            return -1;    
        }    
    
        s3c_ds18b20_clk_write_byte(0xcc);    
        s3c_ds18b20_clk_write_byte(0x44);    
    
        flag = s3c_ds18b20_clk_reset();    
        if (flag & 0x01)    
            return -1;    
    
        s3c_ds18b20_clk_write_byte(0xcc);    
        s3c_ds18b20_clk_write_byte(0xbe);    
    
        result[0] = s3c_ds18b20_clk_read_byte();    // 温度低八位     
        result[1] = s3c_ds18b20_clk_read_byte();    // 温度高八位     
    
        err = copy_to_user(buf, &result, sizeof(result));    
        return err ? -EFAULT : min(sizeof(result), count);    
    }    
    
    //定义的ds18b20文件操作的数据结构
    static struct file_operations s3c_18b20_fops = 
    {
        .owner   = THIS_MODULE,
        .open    = s3c_18b20_open,
        .read    = s3c_18b20_read,
        .release = s3c_18b20_release,
    };
    
    //模块安装调用的初始化
    static int __init s3c_18b20_init(void)
    {
        int                    result;
        dev_t                  devno;
    
    	//申请并映射虚拟地址
        if( 0 != s3c_18b20_addr_init() )
        {
            printk(KERN_ERR "s3c2440 18B20 addr initialize failure.
    ");
            return -ENODEV;
        }
    
    	//为设备注册设备号。如果dev_major不为0,则动态申请主设备号。否者使用dev_major为主设备号
        if (0 != dev_major) /*  Static */
        {
            devno = MKDEV(dev_major, dev_minor);
            result = register_chrdev_region(devno, dev_count, DEV_NAME);
        }
        else
        {
            result = alloc_chrdev_region(&devno, dev_minor, dev_count, DEV_NAME);
            dev_major = MAJOR(devno);
        }
    
        /*  Alloc for device major failure */
        if (result < 0)
        {
            printk(KERN_ERR "S3C %s driver can't use major %d
    ", DEV_NAME, dev_major);
            return -ENODEV;
        } 
    	
        printk(KERN_DEBUG "S3C %s driver use major %d
    ", DEV_NAME, dev_major);
    
    	//为s3c_18b20_cdev数据结构申请空间
        if(NULL == (s3c_18b20_cdev=cdev_alloc()) )
        {
            printk(KERN_ERR "S3C %s driver can't alloc for the cdev.
    ", DEV_NAME);
            unregister_chrdev_region(devno, dev_count);
            return -ENOMEM;
        }
        
    	//绑定字符设备数据结构
        s3c_18b20_cdev->owner = THIS_MODULE;
        cdev_init(s3c_18b20_cdev, &s3c_18b20_fops);
    
    	//注册cdev到内核
        result = cdev_add(s3c_18b20_cdev, devno, dev_count);
    	
        if (0 != result)
        {   
            printk(KERN_INFO "S3C %s driver can't reigster cdev: result=%d
    ", DEV_NAME, result); 
            goto ERROR;
        }
    
                
        printk(KERN_ERR "S3C %s driver[major=%d] version %d.%d.%d installed successfully!
    ", 
                DEV_NAME, dev_major, DRV_MAJOR_VER, DRV_MINOR_VER,DRV_REVER_VER);
        return 0;
    
    
    ERROR:
        printk(KERN_ERR "S3C %s driver installed failure.
    ", DEV_NAME);
        cdev_del(s3c_18b20_cdev);
        unregister_chrdev_region(devno, dev_count);
        return result;
    }
    
    static void __exit s3c_18b20_exit(void)
    {
        dev_t devno = MKDEV(dev_major, dev_minor);
    
        s3c_18b20_addr_release();
    
        cdev_del(s3c_18b20_cdev);
        unregister_chrdev_region(devno, dev_count);
    
        printk(KERN_ERR "S3C %s driver version %d.%d.%d removed!
    ", 
                DEV_NAME, DRV_MAJOR_VER, DRV_MINOR_VER,DRV_REVER_VER);
    
        return ;
    }
    
    /* These two functions defined in <linux/init.h> */
    module_init(s3c_18b20_init);
    module_exit(s3c_18b20_exit);
    
    module_param(debug, int, S_IRUGO);
    module_param(dev_major, int, S_IRUGO);
    
    MODULE_AUTHOR(DRV_AUTHOR);
    MODULE_DESCRIPTION(DRV_DESC);
    MODULE_LICENSE("GPL");
    
    

    四、编译的Makefile

    LINUX_SRC = ${shell pwd}/../kernel/linux-3.0
    CROSS_COMPILE=/opt/xtools/arm920t/bin/arm-linux-
    INST_PATH=${shell pwd}/
    PWD := $(shell pwd)
    EXTRA_CFLAGS+=-DMODULE
    obj-m += kernel_18b20.o
    
    modules:
            @make -C $(LINUX_SRC) M=$(PWD) modules
            @make clear
    
    uninstall:
            rm -f ${INST_PATH}/*.ko
    
    install: uninstall
            cp -af *.ko ${INST_PATH}
    
    clear:
            @rm -f *.o *.cmd *.mod.c
            @rm -rf *~ core .depend .tmp_versions Module.symvers modules.order -f
            @rm -f .*ko.cmd .*.o.cmd .*.o.d
    
    clean: clear
            @rm -f *.ko
    

    解释说明:
    LINUX_SRC 指定开发板已编译过得内核路径
    CROSS_COMPILE 指定交叉编译器
    INST_PATH 安装路径
    obj-m += kernel_18b20.o 编译成模块,输出文件名为kernel_18b20.o

    编译运行。
    修改驱动文件名为kernel_18b20.c
    使用make编译后,将kernel_18b20.ko传输至开发板。
    使用insmod安装。可以使用dmesg查看安装打印信息,也可以使用lsmod查看设备信息

    [root@NickQ_fl2440 driver]# insmod kernel_18b20.ko 
    [root@NickQ_fl2440 driver]# dmesg
    S3C s3c18b20 driver use major 253
    S3C s3c18b20 driver[major=253] version 1.0.0 installed successfully!
    [root@NickQ_fl2440 driver]# lsmod
    kernel_18b20 3250 0 - Live 0xbf000000
    

    然后查看主设备号cat /proc/devices,使用moknod创建设备节点

    [root@NickQ_fl2440 driver]# cat /proc/devices | grep s3c18b20
    253 s3c18b20
    [root@NickQ_fl2440 driver]# mknod -m 755 /dev/s3c18b20 c 253 0
    [root@NickQ_fl2440 driver]# ls /dev/s3c18b20 
    /dev/s3c18b20
    

    mknod 用法

    [root@NickQ_fl2440 driver]# mknod --help
    BusyBox v1.27.1 (2017-11-20 21:14:15 CST) multi-call binary.
    
    Usage: mknod [-m MODE] NAME TYPE MAJOR MINOR
    
    Create a special file (block, character, or pipe)
    
            -m MODE Creation mode (default a=rw)
    TYPE:
            b       Block device
            c or u  Character device
            p       Named pipe (MAJOR and MINOR are ignored)
    

    五、编写测试程序

    [nick@XQLY driver]$ vim ~/s3c2440/linux/drivers/test_18b20.c 
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <linux/ioctl.h>
    
    
    int main()
    {
        int fd;
        unsigned char result[2];
        unsigned char integer_value = 0;
        float decimal_value = 0.0;
        float temperature   = 0.0;
    
        fd = open("/dev/s3c18b20", 0);
    
        if(fd < 0)
        {
            perror("open device failed
    ");
            exit(1);
        }
        else
            printf("Open success!
    ");
    
        while(1)
        {
            read(fd, &result, sizeof(result));
    
            integer_value = ((result[0] & 0xf0) >> 4) | ((result[1] & 0x08) << 4);
    
            decimal_value = (result[0] & 0x0f) * 0.0625;
    
            temperature = (float)integer_value + decimal_value;
    
            printf("Current Temperature:%6.4f
    ", temperature);
    
            sleep(1);
        }
    }
       
    

    解析温度的说明:
    温度格式

    我们读取出来的数据放置在result[2]里。
    result[0]对应从LS Byte里读回来的值
    result[1]对应从MS Byte里读回来的值

    所以integer_value = ((result[0] & 0xf0) >> 4) | ((result[1] & 0x08) << 4);是将LS的高四位和MS的低四位取出(其中有一位符号位S),并合成一个数,即为整数部分。decimal_value = (result[0] & 0x0f) * 0.0625;是提取小数部分

    六、测试现象

    [root@NickQ_fl2440 driver]# ./start_s3c18b20 
    Open success!
    Current Temperature:26.5625
    Current Temperature:26.6250
    Current Temperature:26.5625
    Current Temperature:26.6250
    Current Temperature:26.6250
    ^C
    
  • 相关阅读:
    cocoapods使用遇到的一些问题
    so
    UITextField的各种属性方法介绍
    <<第1章 初识JAVA>>
    《第16章 复习》
    《第17章 图书管理系统》
    《第15章 字符串》
    《第13章 猜拳游戏》
    《第14章 带参数的方法》
    《第12章 类的无参方法》
  • 原文地址:https://www.cnblogs.com/NickQ/p/9026545.html
Copyright © 2020-2023  润新知