驱动入门笔记

基于正点原子IMX6UL开发板的笔记

40 字符设备驱动开发
c_cpp_properties.json
{ 
    "configurations": [ 
        { 
            "name": "Linux", 
            "includePath": [ 
                "${workspaceFolder}/**", 
                "/home/yinliang/linux/kernel/nxp_linux/include",
                "/home/yinliang/linux/kernel/nxp_linux/arch/arm/include",
                "/home/yinliang/linux/kernel/nxp_linux/arch/arm/include/generated"
                ], 
            "defines": [], 
            "compilerPath": "/usr/bin/gcc", 
            "cStandard": "c11", 
            "cppStandard": "c++17", 
            "intelliSenseMode": "clang-x64" 
        } 
    ], 
    "version": 4 
} 
chrdevbase.c
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
/***************************************************************  
文件名  : chrdevbase.c 
作者    : junluoyu 
版本    : V1.0 
描述    : chrdevbase驱动文件。 
其他    : 无 
日志    : 初版 V1.0 2022/11/28 
***************************************************************/

#define CHRDEVBASE_MAJOR  200             /* 主设备号 */
#define CHRDEVBASE_NAME  "chrdevbase"     /* 设备名   */

static char readbuf[100];                 /* 读缓存   */
static char writebuf[100];                /* 写缓存   */ 
static char kerneldata[] = {"kernel data!"};

/*
 * @description    : 打开设备
 * @param - inode  :传递给驱动的inode 
 * @param - filp   :设备文件,file结构体有给叫做private_data的成员变量
 *                   ,一般再open的时候将private_data指向设备的结构体 
 * @return         :0成功;其他失败
 */
 static int chrdevbase_open(struct inode *inode, struct file *filp)
 {
    printk("chrdevbase open!\r\n");
    return 0;
 }

 /*
 * @description    : 从设备读取数据
 * @param - buf    :返回给用户空间的数据缓冲区
 * @param - filp   :要打开的设备文件(文件描述符)
 * @param - cnt    :要读取的数据长度
 * @param - offt   :相对于文件首地址的偏移
 * @return         :读取的字节数,负值表示读取失败
 */
 static ssize_t chrdevbase_read(struct file *filp, char __user *buf,
                                size_t cnt, loff_t *offt)
 {
    int retvalue = 0;

    /*向用户空间发送数据 */
    memcpy(readbuf, kerneldata, sizeof(kerneldata));
    retvalue = copy_to_user(buf, readbuf, cnt);
    if(retvalue == 0) {
        printk("kernel senddata ok!\r\n");
    }else{
        printk("kernel senddata failed!\r\n");
    }

    //printk("chrdevbase read!\r\n");
    return 0;
 }

/*
 * @description    : 从设备写数据
 * @param - buf    :要写给设备写入的数据
 * @param - filp   :设备文件,表示打开的文件描述符
 * @param - cnt    :要写入的数据长度
 * @param - offt   :相对于文件首地址的偏移
 * @return         :写入的字节数,负值表示读取失败
 */
 static ssize_t chrdevbase_write(struct file *filp, const char __user *buf,
                                size_t cnt, loff_t *offt)
 {
    int retvalue = 0;

    /*接收用户空间传递给内核的数据并打印出来*/
    retvalue = copy_from_user(writebuf, buf, cnt);
    if(retvalue == 0) {
        printk("kernel recvdata: %s\r\n", writebuf);
    }else{
        printk("kernel recvdata failed!\r\n");
    }

    //printk("chrdevbase write!\r\n");
    return 0;
 }

 /*
 * @description    : 关闭、释放设备
 * @param - filp   :要关闭的设备文件
 * @return         :0成功;其他 失败
 */
 static int chrdevbase_release(struct inode *inode, struct file *filp)
 {
    // printk("chrdevbase release! \r\n");
    return 0;
 }

 /*
 * 设备操作结构体
 */
static struct file_operations chrdevbase_fops = {
    .owner = THIS_MODULE,
    .open  = chrdevbase_open,
    .read  = chrdevbase_read,
    .write = chrdevbase_write,
    .release = chrdevbase_release,
};

 /*
 * @description    : 驱动入口函数
 * @param - filp   :无
 * @return         :0成功; 其他 失败
 */
 static int __init chrdevbase_init(void)
 {
    int retvalue = 0;

    /* 注册字符设备驱动 */
    retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME,
                                &chrdevbase_fops);

    if (retvalue < 0)
    {
        printk("chrdevbase driver register failed\r\n");
    }
    printk("chrdevbase_init()\r\n"); 
    return 0;
 }

  /*
 * @description    : 驱动出口函数
 * @param - filp   :无
 * @return         :无
 */
 void __exit chrdevbase_exit(void)
 {
    /* 注销字符设备驱动 */
    unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
    printk("chrdevbase_exit()\r\n"); 
 }

 /*
  * 将上面两个函数指定为驱动的入口和出口函数
  */
 module_init(chrdevbase_init);
 module_exit(chrdevbase_exit);

 /*
  * LICENSE和作者信息
  */
 MODULE_LICENSE("GPL");
 MODULE_AUTHOR("junluoyu");



chrdevbaseApp.c
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include  <string.h>
/***************************************************************  
文件名  : chrdevbaseApp.c 
作者    : junluoyu 
版本    : V1.0 
描述    : chrdevbase驱动测试APP 
其他    : 无 
日志    : 初版 V1.0 2022/11/28 
其他    : ./chrdevbaseApp /dev/chrdevbase 1(读文件)|2(写文件)
***************************************************************/
static char usrdata[] = {"usr data!"};

/*
 * @description  : main主程序
 * @param -argc  :argv数组元素长度
 * @param -argv  :具体参数
 */ 
int main(int argc, char *argv[])
{
    int fd, retvalue;
    char *filename;
    char readbuf[100], writebuf[100];

    if (argc != 3) {
        printf("Error Usage!\r\n");
        return -1;
    }

    filename = argv[1];

    /* 打开驱动文件 */
    fd = open(filename, O_RDWR);
    if (fd < 0) {
        printf("Can't open file %s \r\n", filename);
        return -1;
    }

    if (atoi(argv[2]) == 1) { /* 从驱动文件读取数据 */
        retvalue = read(fd, readbuf, 50);
        if (retvalue < 0) {
            printf("read file %s failed!\r\n", filename);
        } else {
            /* 读取成功,打印出读取成功的数据*/
            printf("read data:%s \r\n", readbuf);
        }
    }

    if (atoi(argv[2]) == 2) { /* 向驱动文件写数据 */
        memcpy(writebuf, usrdata, sizeof(usrdata));
        retvalue = write(fd, writebuf, 50);
        if (retvalue < 0) {
            printf("write file %s failed!\r\n", filename);
        } 
    }

    /* 关闭设备 */
    retvalue = close(fd);
    if (retvalue < 0) {
        printf("Can't close file %s failed!\r\n", filename);
        return -1;
    } 

    return 0;
}
Makefile
KERNELDIR := /home/yinliang/linux/kernel/nxp_linux
CURERENT_PATH := $(shell pwd)
obj-m := chrdevbase.o

build: kernel_moduls

kernel_moduls:
	$(MAKE) -C $(KERNELDIR) M=$(CURERENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURERENT_PATH) clean

arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp
file chrdevbaseApp
复制chrdevbase.ko chrdevbaseApp 到 lib/modules/4.1.15/
sudo cp chrdevbase.ko chrdevbaseApp /home/yinliang/linux/nfs/rootfs/lib/modules/4.1.15/ -f 
启动板卡测试
cd /lib/modules/4.1.15

1.安装驱动模块
insmod chrdevbase.ko  

modprobe chrdevbase.ko          #会安装依赖文件

2.挂载
cat /proc/devices               #看有没有设备

mknod /dev/chrdevbase c 200 0   #mknod创建节点命令,/dev/chrdevbase是创建的节点文件,c表示是字符设备,200是主设备号,0表示设备的次设备号

ls /dev/chrdevbase -l

3.测试
chmod 777 /dev/chrdevbase

./chrdevbaseApp /dev/chedevbase 1

./chrdevbaseApp /dev/chedevbase 2

4.卸载驱动模块
rmmod chrdevbase.ko  

modprobe -r chrdevbase.ko    #会卸载依赖文件
41 Linux LED驱动开发实验
ioremap iounmap 物理地址映射到虚拟地址
IMX6U_CCM_CCGR1    = ioremap(CCM_CCGR1_BASE   ,4);
// 物理地址映射到虚拟地址  4代表4个字节
iounmap(IMX6U_CCM_CCGR1   );
// 取消映射关系
led.c
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

/***************************************************************  
文件名  : led.c 
作者    : junluoyu 
版本    : V1.0 
描述    : led驱动文件。 
其他    : 无 
日志    : 初版 V1.0 2022/11/20 
***************************************************************/

/* 寄存器物理地址 */
#define CCM_CCGR1_BASE         (0X020C406C)
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4)
#define GPIO1_DR_BASE          (0X0209C000)
#define GPIO1_GDIR_BASE        (0X0209C004)

/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

#define LED_MAJOR  200            /* 主设备号 */
#define LED_NAME  "led"            /* 设备名   */


#define LEDOFF 0 //关灯
#define LEDON  1 //开灯


/*
 * @description    : led打开/关闭
 * @param - sta    :LED0N 0 打开LED  LEDOFF 1 关闭LED
 * @return         :无
 */
 void led_switch(u8 sta)
 {
    u32 val = 0;
    if(sta == LEDON) {
        val = readl(GPIO1_DR);
        val &= ~(1<<3);
        writel(val,GPIO1_DR);
    } 
    else if (sta == LEDOFF) {
        val = readl(GPIO1_DR);
        val |= (1<<3);
        writel(val,GPIO1_DR);
    }
 }


/*
 * @description    : 打开设备
 * @param - inode  :传递给驱动的inode 
 * @param - filp   :设备文件,file结构体有给叫做private_data的成员变量
 *                   ,一般再open的时候将private_data指向设备的结构体 
 * @return         :0成功;其他失败
 */
 static int led_open(struct inode *inode, struct file *filp)
 {
    return 0;
 }

 /*
 * @description    : 从设备读取数据
 * @param - buf    :返回给用户空间的数据缓冲区
 * @param - filp   :要打开的设备文件(文件描述符)
 * @param - cnt    :要读取的数据长度
 * @param - offt   :相对于文件首地址的偏移
 * @return         :读取的字节数,负值表示读取失败
 */
 static ssize_t led_read(struct file *filp, char __user *buf,
                                size_t cnt, loff_t *offt)
 {
    return 0;
 }

/*
 * @description    : 从设备写数据
 * @param - buf    :要写给设备写入的数据
 * @param - filp   :设备文件,表示打开的文件描述符
 * @param - cnt    :要写入的数据长度
 * @param - offt   :相对于文件首地址的偏移
 * @return         :写入的字节数,负值表示读取失败
 */
 static ssize_t led_write(struct file *filp, const char __user *buf,
                                size_t cnt, loff_t *offt)
 {
    int retvalue = 0;
    unsigned char databuf[1];
    unsigned char ledstat;

    /*接收用户空间传递给内核的数据并打印出来*/
    retvalue = copy_from_user(databuf, buf, cnt);
    if(retvalue < 0) {
        printk("kernel write failed: \r\n");
        return -EFAULT;
    }

    ledstat = databuf[0];        //读取状态值

    if(ledstat == LEDON) {
        led_switch(LEDON);       //打开LED灯
    }
    else if(ledstat == LEDOFF) {
        led_switch(LEDOFF);      //关闭LED灯
    }

    return 0;
 }

 /*
 * @description    : 关闭、释放设备
 * @param - filp   :要关闭的设备文件
 * @return         :0成功;其他 失败
 */
 static int led_release(struct inode *inode, struct file *filp)
 {
    // printk("led release! \r\n");
    return 0;
 }

 /*
 * 设备操作结构体
 */
static struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .open  = led_open,
    .read  = led_read,
    .write = led_write,
    .release = led_release,
};

 /*
 * @description    : 驱动入口函数
 * @param - filp   :无
 * @return         :0成功; 其他 失败
 */
 static int __init led_init(void)
 {
    int retvalue = 0;
    u32 val = 0;

    /* 初始化LED */
    /* 1.寄存器地址映射 */
    IMX6U_CCM_CCGR1      = ioremap(CCM_CCGR1_BASE   ,4);
    SW_MUX_GPIO1_IO03    = ioremap(SW_MUX_GPIO1_IO03_BASE ,4);
    SW_PAD_GPIO1_IO03    = ioremap(SW_PAD_GPIO1_IO03_BASE ,4);
    GPIO1_DR             = ioremap(GPIO1_DR_BASE          ,4);
    GPIO1_GDIR           = ioremap(GPIO1_GDIR_BASE        ,4);

    /* 2.使能GPIO1时钟 */
    val = readl(IMX6U_CCM_CCGR1);
    val &= ~(3<<26);  /* 清除以前的设置 */
    val |= (3<<26);   /* 设置新值      */
    writel(val, IMX6U_CCM_CCGR1);

    /* 3.设置GPIO1_IO03的复用功能,将其复用为GPIO01_IO03,最后设置IO属性 */
    writel(5, SW_MUX_GPIO1_IO03);           //mux复用功能为5
    writel(0X10B0, SW_PAD_GPIO1_IO03);      //PAD设置IO属性

    /* 4.设置GPIO1_IO03为输出功能*/
    val = readl(GPIO1_GDIR);
    val &= ~(1<<3);  /* 清除以前的设置 */
    val |=  (1<<3);  /* 设置为输出     */
    writel(val,GPIO1_GDIR);

    /* 5.默认关闭LED*/
    val = readl(GPIO1_DR);
    val |= (1<<3); //默认为关,1电平
    writel(val, GPIO1_DR);

    /* 6.注册字符设备驱动 */
    retvalue = register_chrdev(LED_MAJOR, LED_NAME,
                                &led_fops);

    if (retvalue < 0)
    {
        printk("register led failed\r\n");
        return -EIO;
    }
    return 0;
 }

  /*
 * @description    : 驱动出口函数
 * @param - filp   :无
 * @return         :无
 */
 void __exit led_exit(void)
 {
    /* 取消映射 */
    iounmap(IMX6U_CCM_CCGR1   );
    iounmap(SW_PAD_GPIO1_IO03 );      
    iounmap(SW_MUX_GPIO1_IO03 );      
    iounmap(GPIO1_DR          );      
    iounmap(GPIO1_GDIR        );      

    /* 注销字符设备驱动 */
    unregister_chrdev(LED_MAJOR,LED_NAME);
 }

 /*
  * 将上面两个函数指定为驱动的入口和出口函数
  */
 module_init(led_init);
 module_exit(led_exit);

 /*
  * LICENSE和作者信息
  */
 MODULE_LICENSE("GPL");
 MODULE_AUTHOR("junluoyu");
ledApp.c
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
/***************************************************************  
文件名  : ledApp.c 
作者    : junluoyu 
版本    : V1.0 
描述    : led驱动测试APP 
其他    : 无 
日志    : 初版 V1.0 2022/11/28 
其他    : ./ledApp /dev/led 1(打开LED)|0(关闭LED)
***************************************************************/
#define LEDOFF 0 //关灯
#define LEDON  1 //开灯

/*
 * @description  : main主程序
 * @param -argc  :argv数组元素长度
 * @param -argv  :具体参数
 */ 
int main(int argc, char *argv[])
{
    int fd, retvalue;
    char *filename;
    unsigned char databuf[1];

    if (argc != 3) {
        printf("Error Usage!\r\n");
        return -1;
    }

    filename = argv[1];

    /* 打开驱动文件 */
    fd = open(filename, O_RDWR);
    if (fd < 0) {
        printf("Can't open file %s \r\n", filename);
        return -1;
    }

    databuf[0] = atoi(argv[2]);
    /* 向/dev/led文件写入数据 */
    retvalue = write(fd, databuf,sizeof(databuf));
    if (retvalue < 0) {
            printf("led control failed!\r\n");
            close(fd);
            return -1;
    } 
    /* 关闭led设备 */
    retvalue = close(fd);
    if (retvalue < 0) {
        printf("file %s close failed!\r\n", argv[1]);
        return -1;
    } 
    return 0;
}
Makefile
KERNELDIR := /home/yinliang/linux/kernel/nxp_linux
CURERENT_PATH := $(shell pwd)
obj-m := led.o

build: kernel_moduls

kernel_moduls:
	$(MAKE) -C $(KERNELDIR) M=$(CURERENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURERENT_PATH) clean
make -j16
arm-linux-gnueabihf-gcc led.c -o ledApp
file chrdevbaseApp
复制led.ko ledApp 到 lib/modules/4.1.15/

sudo cp led.ko ledApp /home/yinliang/linux/nfs/rootfs/lib/modules/4.1.15/ -f 
启动板卡测试
cd /lib/modules/4.1.15

1.安装驱动模块  第一次加载先使用depmod
insmod led.ko  

modprobe led.ko          #会安装依赖文件

2.挂载
cat /proc/devices               #看有没有设备

mknod /dev/led c 200 0   #mknod创建节点命令,/dev/led是创建的节点文件,c表示是字符设备,200是主设备号,0表示设备的次设备号

ls /dev/led -l

3.测试
chmod 777 /dev/led

./ledApp /dev/led 1  //开灯

./ledApp /dev/led 2  //关灯

4.卸载驱动模块
rmmod ledbase.ko  

modprobe -r ledbase.ko    #会卸载依赖文件
42 新字符设备驱动开发实验

不再需要自己去挂载设备,自动生成设备号并且挂载上去。

newchrled.c
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>

#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

/***************************************************************  
文件名  : led.c 
作者    : junluoyu 
版本    : V1.0 
描述    : led驱动文件 新版 
其他    : 无 
日志    : 初版 V1.0 2022/11/20 
***************************************************************/

#define NEWCHRLED_CNT       1                 /*设备号个数*/
#define NEWCHRLED_NAME     "newchrled"        /* 名字     */

/* 寄存器物理地址 */
#define CCM_CCGR1_BASE         (0X020C406C)
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4)
#define GPIO1_DR_BASE          (0X0209C000)
#define GPIO1_GDIR_BASE        (0X0209C004)

/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

#define LEDOFF 0 //关灯
#define LEDON  1 //开灯


/*newchrled设备结构体*/
struct newchrled_dev{
    dev_t devid;                     /* 设备号    */  
    struct cdev cdev;                /* cdev     */       
    struct class *class;             /* 类       */     
    struct device *device;           /* 设备     */    
    int major;                       /* 主设备号  */
    int minor;                       /* 次设备号  */
};

struct newchrled_dev newchrled;      /*led设备   */


/*
 * @description    : led打开/关闭
 * @param - sta    :LED0N 0 打开LED  LEDOFF 1 关闭LED
 * @return         :无
 */
 void led_switch(u8 sta)
 {
    u32 val = 0;
    if(sta == LEDON) {
        val = readl(GPIO1_DR);
        val &= ~(1<<3);
        writel(val,GPIO1_DR);
    } 
    else if (sta == LEDOFF) {
        val = readl(GPIO1_DR);
        val |= (1<<3);
        writel(val,GPIO1_DR);
    }
 }


/*
 * @description    : 打开设备
 * @param - inode  :传递给驱动的inode 
 * @param - filp   :设备文件,file结构体有给叫做private_data的成员变量
 *                   ,一般再open的时候将private_data指向设备的结构体 
 * @return         :0成功;其他失败
 */
 static int led_open(struct inode *inode, struct file *filp)
 {
    filp->private_data = &newchrled;  /*设置私有数据*/
    return 0;
 }

 /*
 * @description    : 从设备读取数据
 * @param - buf    :返回给用户空间的数据缓冲区
 * @param - filp   :要打开的设备文件(文件描述符)
 * @param - cnt    :要读取的数据长度
 * @param - offt   :相对于文件首地址的偏移
 * @return         :读取的字节数,负值表示读取失败
 */
 static ssize_t led_read(struct file *filp, char __user *buf,
                                size_t cnt, loff_t *offt)
 {
    return 0;
 }

/*
 * @description    : 从设备写数据
 * @param - buf    :要写给设备写入的数据
 * @param - filp   :设备文件,表示打开的文件描述符
 * @param - cnt    :要写入的数据长度
 * @param - offt   :相对于文件首地址的偏移
 * @return         :写入的字节数,负值表示读取失败
 */
 static ssize_t led_write(struct file *filp, const char __user *buf,
                                size_t cnt, loff_t *offt)
 {
    int retvalue = 0;
    unsigned char databuf[1];
    unsigned char ledstat;

    /*接收用户空间传递给内核的数据并打印出来*/
    retvalue = copy_from_user(databuf, buf, cnt);
    if(retvalue < 0) {
        printk("kernel write failed: \r\n");
        return -EFAULT;
    }

    ledstat = databuf[0];        //读取状态值

    if(ledstat == LEDON) {
        led_switch(LEDON);       //打开LED灯
    }
    else if(ledstat == LEDOFF) {
        led_switch(LEDOFF);      //关闭LED灯
    }

    return 0;
 }

 /*
 * @description    : 关闭、释放设备
 * @param - filp   :要关闭的设备文件
 * @return         :0成功;其他 失败
 */
 static int led_release(struct inode *inode, struct file *filp)
 {
    // printk("led release! \r\n");
    return 0;
 }

 /*
 * 设备操作结构体
 */
static struct file_operations newchrled_fops = {
    .owner = THIS_MODULE,
    .open  = led_open,
    .read  = led_read,
    .write = led_write,
    .release = led_release,
};

 /*
 * @description    : 驱动入口函数
 * @param - filp   :无
 * @return         :0成功; 其他 失败
 */
 static int __init led_init(void)
 {
    u32 val = 0;

    /* 初始化LED */
    /* 1.寄存器地址映射 */
    IMX6U_CCM_CCGR1      = ioremap(CCM_CCGR1_BASE   ,4);
    SW_MUX_GPIO1_IO03    = ioremap(SW_MUX_GPIO1_IO03_BASE ,4);
    SW_PAD_GPIO1_IO03    = ioremap(SW_PAD_GPIO1_IO03_BASE ,4);
    GPIO1_DR             = ioremap(GPIO1_DR_BASE          ,4);
    GPIO1_GDIR           = ioremap(GPIO1_GDIR_BASE        ,4);

    /* 2.使能GPIO1时钟 */
    val = readl(IMX6U_CCM_CCGR1);
    val &= ~(3<<26);  /* 清除以前的设置 */
    val |= (3<<26);   /* 设置新值      */
    writel(val, IMX6U_CCM_CCGR1);

    /* 3.设置GPIO1_IO03的复用功能,将其复用为GPIO01_IO03,最后设置IO属性 */
    writel(5, SW_MUX_GPIO1_IO03);           //mux复用功能为5
    writel(0X10B0, SW_PAD_GPIO1_IO03);      //PAD设置IO属性

    /* 4.设置GPIO1_IO03为输出功能*/
    val = readl(GPIO1_GDIR);
    val &= ~(1<<3);  /* 清除以前的设置 */
    val |=  (1<<3);  /* 设置为输出     */
    writel(val,GPIO1_GDIR);

    /* 5.默认关闭LED*/
    val = readl(GPIO1_DR);
    val |= (1<<3); //默认为关,1电平
    writel(val, GPIO1_DR);

    /* 注册字符设备驱动 */
    // retvalue = register_chrdev(LED_MAJOR, LED_NAME,
    //                             &led_fops);
    
    /* 1.创建设备号   */
    if(newchrled.major) { /* 定义了设备号 */
        newchrled.devid = MKDEV(newchrled.major, 0);
        register_chrdev_region(newchrled.devid, NEWCHRLED_CNT,NEWCHRLED_NAME);
    }
    else {                /* 没有定义设备号 */
        alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT, NEWCHRLED_NAME);   /*动态申请设备号*/
        newchrled.major = MAJOR(newchrled.devid);                                  /* 获取主设备号 */
        newchrled.minor = MINOR(newchrled.devid);                                  /* 获取次设备号*/
    }
    printk("newchrled major=%d, minor=%d\r\n", newchrled.major, newchrled.minor);
    /* 2.初始化cdev   */
    newchrled.cdev.owner = THIS_MODULE;
    cdev_init(&newchrled.cdev, &newchrled_fops);
    /* 3.添加一个cdev */
    cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT);
    /* 4.创建类      */
    newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME);
    if(IS_ERR(newchrled.class)) {
        return PTR_ERR(newchrled.class);
    }
    /* 5.创建设备    */
    newchrled.device = device_create(newchrled.class, NULL, newchrled.devid, NULL, NEWCHRLED_NAME);
    if(IS_ERR(newchrled.device)) {
        return PTR_ERR(newchrled.device);
    }

    return 0;
 }

  /*
 * @description    : 驱动出口函数
 * @param - filp   :无
 * @return         :无
 */
 void __exit led_exit(void)
 {
    /* 取消映射 */
    iounmap(IMX6U_CCM_CCGR1   );
    iounmap(SW_PAD_GPIO1_IO03 );      
    iounmap(SW_MUX_GPIO1_IO03 );      
    iounmap(GPIO1_DR          );      
    iounmap(GPIO1_GDIR        );      

    /* 注销字符设备驱动 */
    //unregister_chrdev(LED_MAJOR,LED_NAME);
    cdev_del(&newchrled.cdev);   /* 删除cdev */
    unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT);

    device_destroy(newchrled.class, newchrled.devid);
    class_destroy(newchrled.class);
 }

 /*
  * 将上面两个函数指定为驱动的入口和出口函数
  */
 module_init(led_init);
 module_exit(led_exit);

 /*
  * LICENSE和作者信息
  */
 MODULE_LICENSE("GPL");
 MODULE_AUTHOR("junluoyu");
newchrledApp.c
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
/***************************************************************  
文件名  : ledApp.c 
作者    : junluoyu 
版本    : V1.0 
描述    : led驱动测试APP 
其他    : 无 
日志    : 初版 V1.0 2022/11/28 
其他    : ./ledApp /dev/led 1(打开LED)|0(关闭LED)
***************************************************************/
#define LEDOFF 0 //关灯
#define LEDON  1 //开灯

/*
 * @description  : main主程序
 * @param -argc  :argv数组元素长度
 * @param -argv  :具体参数
 */ 
int main(int argc, char *argv[])
{
    int fd, retvalue;
    char *filename;
    unsigned char databuf[1];

    if (argc != 3) {
        printf("Error Usage!\r\n");
        return -1;
    }

    filename = argv[1];

    /* 打开驱动文件 */
    fd = open(filename, O_RDWR);
    if (fd < 0) {
        printf("Can't open file %s \r\n", filename);
        return -1;
    }

    databuf[0] = atoi(argv[2]);
    /* 向/dev/led文件写入数据 */
    retvalue = write(fd, databuf,sizeof(databuf));
    if (retvalue < 0) {
            printf("led control failed!\r\n");
            close(fd);
            return -1;
    } 
    /* 关闭led设备 */
    retvalue = close(fd);
    if (retvalue < 0) {
        printf("file %s close failed!\r\n", argv[1]);
        return -1;
    } 
    return 0;
}
Makefile
KERNELDIR := /home/yinliang/linux/kernel/nxp_linux
CURERENT_PATH := $(shell pwd)
obj-m := newchrled.o

build: kernel_moduls

kernel_moduls:
	$(MAKE) -C $(KERNELDIR) M=$(CURERENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURERENT_PATH) clean
复制newchrled.ko newchrledApp 到 lib/modules/4.1.15/
make -j16
arm-linux-gnueabihf-gcc newchrledApp.c  -o newchrledApp
cp newchrled.ko  newchrledApp  /home/yinliang/linux/nfs/rootfs/lib/modules/4.1.15/
启动板卡测试
cd /lib/modules/4.1.15
1.安装驱动模块  第一次加载先使用depmod
insmod newchrled.ko  
modprobe newchrled.ko          #会安装依赖文件
2.自动挂载
ls /dev/newchrled -l
3.测试
./newchrledApp /dev/newchrled 1  //开灯
./newchrledApp /dev/newchrled 0  //关灯
4.卸载驱动模块
rmmod newchrled.ko  
modprobe -r newchrled.ko    #会卸载依赖文件
43 linux设备树

imx6ull-alientek-emmc.dts在 make dtbs后产生 imx6ull-alientek-emmc.dtb

myfirst.dts
//①、I.MX6ULL这个 Cortex-A7架构的 32位 CPU。 
//②、I.MX6ULL内部 ocram,起始地址 0x00900000,大小为 128KB(0x20000)。 
//③、I.MX6ULL内部 aips1域下的 ecspi1外设控制器,寄存器起始地址为 0x02008000,大小为 0x4000。 
//④、I.MX6ULL内部 aips2域下的 usbotg1外设控制器,寄存器起始地址为 0x02184000,大小为 0x4000。 
//⑤、I.MX6ULL内部 aips3域下的 rngb外设控制器,寄存器起始地址为 0x02284000,大小为 0x4000。

/ {
    compatible = "fsl, imx6ull-alientek-evk", "fsl, imx6ull";
    cpus {
        #address-cells = <1>;
        #size-cells    = <0>;

        //cpu0节点
        cpu0: cpu@0 {
            compatible  = "arm, cortex-a7";
            device_type = "cpu";
            reg  = <0>;
        };
    };

    //soc节点
    soc {
        #address-cells = <1>;
        #size-cells = <1>;
        compatible = "simple-bus";
        ranges;      //ranges属性为空,表示子空间和父空间地址范围相同

        //ocram节点
        ocram:sram@00900000 {
            compatible = "fsl,lpm-sram";
            reg = <0x00900000, 0x20000>;
        };

        //aips1节点
        aips1:aips-bus@02000000 {
            compatible     = "fsl,aips-bus", "simple-bus";
            #address-cells = <1>;
            #size-cells    = <1>;
            reg = <0x02000000, 0x100000>;
            ranges;

            //ecspil节点
            ecspil:aips-bus@02008000 {
            #address-cells = <1>;
            #size-cells    = <0>;
            compatible     = "fsl,imx6ul-ecspi", "fsl,imx51-ecspi";
            reg = <0x02008000, 0x4000>;
            status = "disabled";
            };
        };

        //aips2节点
        aips2:aips-bus@02100000 {
            compatible = "fsl,aips-bus", "simple-bus";
            #address-cells = <1>;
            #size-cells    = <1>;
            reg = <0x02100000, 0x100000>;
            ranges;

            //usbotg1节点
            usbotg1:usb@02184000 {
            compatible = "fsl,imx6ul-usb", "fsl,imx27-usb";
            reg = <0x02184000, 0x4000>;
            status = "disabled";
            };
        };

        //aips3节点
        aips3:aips-bus@02200000 {
            compatible = "fsl,aips-bus", "simple-bus";
            #address-cells = <1>;
            #size-cells = <1>;
            reg = <0x02200000, 0x100000>;
            ranges;

            //rgnb节点
            rgnb:rgnb@02284000 {
            compatible = "fsl,imx6sl-rng", "fsl,imx-rng", "imx-rng";
            reg = <0x02284000, 0x4000>;
            };
        };
    };
}
dts语法(待补充)
44 设备树下的 LED驱动实验
修改设备树 imx6ull-alientek-emmc.dts
/ {
	model = "Freescale i.MX6 ULL 14x14 EVK Board";
	compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";

	/* 4_dtsled   alphaled--------------------------------------------------*/
	alphaled {
		#address-cells = <1>;
		#size-cells    = <1>;
		compatible     = "atkalpha-led";
		status         = "okay";
		reg = < 0X020C406C 0X04       /* CCM_CCGR1_BASE             */ 
                0X020E0068 0X04       /* SW_MUX_GPIO1_IO03_BASE      */ 
                0X020E02F4 0X04       /* SW_PAD_GPIO1_IO03_BASE      */ 
                0X0209C000 0X04       /* GPIO1_DR_BASE               */ 
                0X0209C004 0X04 >;    /* GPIO1_GDIR_BASE             */ 
	};
	/* 4_dtsled   alphaled---------------------------------------------------*/
make dtbs
cp /home/yinliang/linux/kernel/nxp_linux/arch/arm/boot/dts/imx6ull-alientek-emmc.dtb  /home/yinliang/linux/tftpboot
dtsled.c
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

/***************************************************************  
文件名  : led.c 
作者    : junluoyu 
版本    : V1.0 
描述    : led驱动文件 新版 
其他    : 无 
日志    : 初版 V1.0 2022/11/20 
***************************************************************/

#define DTSLED_CNT       1                 /*设备号个数*/
#define DTSLED_NAME     "dtsled"           /* 名字     */

// /* 寄存器物理地址 */
// #define CCM_CCGR1_BASE         (0X020C406C)
// #define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
// #define SW_PAD_GPIO1_IO03_BASE (0X020E02F4)
// #define GPIO1_DR_BASE          (0X0209C000)
// #define GPIO1_GDIR_BASE        (0X0209C004)

/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

#define LEDOFF 0 //关灯
#define LEDON  1 //开灯


/*dtsled设备结构体*/
struct dtsled_dev{
    dev_t devid;                     /* 设备号    */  
    struct cdev cdev;                /* cdev     */       
    struct class *class;             /* 类       */     
    struct device *device;           /* 设备     */    
    int major;                       /* 主设备号  */
    int minor;                       /* 次设备号  */
    struct device_node *nd;          /* 设备节点  */
};

struct dtsled_dev dtsled;      /*led设备   */


/*
 * @description    : led打开/关闭
 * @param - sta    :LED0N 0 打开LED  LEDOFF 1 关闭LED
 * @return         :无
 */
 void led_switch(u8 sta)
 {
    u32 val = 0;
    if(sta == LEDON) {
        val = readl(GPIO1_DR);
        val &= ~(1<<3);
        writel(val,GPIO1_DR);
    } 
    else if (sta == LEDOFF) {
        val = readl(GPIO1_DR);
        val |= (1<<3);
        writel(val,GPIO1_DR);
    }
 }

/*
 * @description    : 打开设备
 * @param - inode  :传递给驱动的inode 
 * @param - filp   :设备文件,file结构体有给叫做private_data的成员变量
 *                   ,一般再open的时候将private_data指向设备的结构体 
 * @return         :0成功;其他失败
 */
 static int led_open(struct inode *inode, struct file *filp)
 {
    filp->private_data = &dtsled;  /*设置私有数据*/
    return 0;
 }

 /*
 * @description    : 从设备读取数据
 * @param - buf    :返回给用户空间的数据缓冲区
 * @param - filp   :要打开的设备文件(文件描述符)
 * @param - cnt    :要读取的数据长度
 * @param - offt   :相对于文件首地址的偏移
 * @return         :读取的字节数,负值表示读取失败
 */
 static ssize_t led_read(struct file *filp, char __user *buf,
                                size_t cnt, loff_t *offt)
 {
    return 0;
 }

/*
 * @description    : 从设备写数据
 * @param - buf    :要写给设备写入的数据
 * @param - filp   :设备文件,表示打开的文件描述符
 * @param - cnt    :要写入的数据长度
 * @param - offt   :相对于文件首地址的偏移
 * @return         :写入的字节数,负值表示读取失败
 */
 static ssize_t led_write(struct file *filp, const char __user *buf,
                                size_t cnt, loff_t *offt)
 {
    int retvalue = 0;
    unsigned char databuf[1];
    unsigned char ledstat;

    /*接收用户空间传递给内核的数据并打印出来*/
    retvalue = copy_from_user(databuf, buf, cnt);
    if(retvalue < 0) {
        printk("kernel write failed: \r\n");
        return -EFAULT;
    }

    ledstat = databuf[0];        //读取状态值

    if(ledstat == LEDON) {
        led_switch(LEDON);       //打开LED灯
    }
    else if(ledstat == LEDOFF) {
        led_switch(LEDOFF);      //关闭LED灯
    }

    return 0;
 }

 /*
 * @description    : 关闭、释放设备
 * @param - filp   :要关闭的设备文件
 * @return         :0成功;其他 失败
 */
 static int led_release(struct inode *inode, struct file *filp)
 {
    // printk("led release! \r\n");
    return 0;
 }

 /*
 * 设备操作结构体
 */
static struct file_operations dtsled_fops = {
    .owner = THIS_MODULE,
    .open  = led_open,
    .read  = led_read,
    .write = led_write,
    .release = led_release,
};

 /*
 * @description    : 驱动入口函数
 * @param - filp   :无
 * @return         :0成功; 其他 失败
 */
 static int __init led_init(void)
 {
    u32 val = 0;
    int ret = 0;
    u32 regdata[14];
    const char *str;
    struct property *proper;

    /* 获取设备树中的属性数据 */
    /* 1.获取设备节点 alphaled */
    dtsled.nd = of_find_node_by_path("/alphaled");   //of_find_node_by_path得到alphaled节点
    if(dtsled.nd == NULL) {
        printk("alphaled node can not found !\r\n");
        return -EINVAL;
    } else {
        printk("alphaled node has been found !\r\n");
    }

    /* 2.获取compatible属性*/
    proper = of_find_property(dtsled.nd, "compatible", NULL); //of_find_property获取compatible属性,返回property结构体类型指针,property的成员变量value表示属性值
    if(proper == NULL) {
        printk("compatible property find failed!\r\n");
        return -EINVAL;
    } else {
        printk("compatible = %s\r\n", (char*)proper->value);
    }

    /* 3.获取status属性*/
    ret = of_property_read_string(dtsled.nd, "status", &str);//of_property_read_string获得status属性值
    if(ret < 0) {
        printk("status read failed!\r\n");
        return -EINVAL;
    } else {
        printk("status = %s\r\n", str);
    }

    /* 3.获取status属性*/
    ret = of_property_read_u32_array(dtsled.nd, "reg", regdata, 10);//of_property_read_u32_array得到reg属性值,保存到regdata数组
    if(ret < 0) {
        printk("reg property read failed!\r\n");
        return -EINVAL;
    } else {
        u8 i = 0;
        printk("reg data:\r\n");
        for( i = 0; i < 10; i++)
            printk("%#x", regdata[i]);
        printk("\r\n");
    }

    /* 初始化LED */
#if 0
    /* 1.寄存器地址映射 */
    IMX6U_CCM_CCGR1   = ioremap(regdata[0], regdata[1]);
    SW_MUX_GPIO1_IO03 = ioremap(regdata[2], regdata[3]);
    SW_PAD_GPIO1_IO03 = ioremap(regdata[4], regdata[5]);
    GPIO1_DR          = ioremap(regdata[6], regdata[7]);
    GPIO1_GDIR        = ioremap(regdata[8], regdata[9]);
#else 
    IMX6U_CCM_CCGR1      = of_iomap(dtsled.nd, 0); //of_iomap一次性完成读取reg属性以及内存映射,是设备树推荐的OF函数
    SW_MUX_GPIO1_IO03    = of_iomap(dtsled.nd, 1);
    SW_PAD_GPIO1_IO03    = of_iomap(dtsled.nd, 2);
    GPIO1_DR             = of_iomap(dtsled.nd, 3);
    GPIO1_GDIR           = of_iomap(dtsled.nd, 4);
#endif
    /* 2.使能GPIO1时钟 */
    val = readl(IMX6U_CCM_CCGR1);
    val &= ~(3<<26);  /* 清除以前的设置 */
    val |= (3<<26);   /* 设置新值      */
    writel(val, IMX6U_CCM_CCGR1);

    /* 3.设置GPIO1_IO03的复用功能,将其复用为GPIO01_IO03,最后设置IO属性 */
    writel(5, SW_MUX_GPIO1_IO03);           //mux复用功能为5
    writel(0X10B0, SW_PAD_GPIO1_IO03);      //PAD设置IO属性

    /* 4.设置GPIO1_IO03为输出功能*/
    val = readl(GPIO1_GDIR);
    val &= ~(1<<3);  /* 清除以前的设置 */
    val |=  (1<<3);  /* 设置为输出     */
    writel(val,GPIO1_GDIR);

    /* 5.默认关闭LED*/
    val = readl(GPIO1_DR);
    val |= (1<<3); //默认为关,1电平
    writel(val, GPIO1_DR);

    /* 注册字符设备驱动 */
    // retvalue = register_chrdev(LED_MAJOR, LED_NAME,
    //                             &led_fops);
    
    /* 1.创建设备号 */
    if(dtsled.major) { /* 定义了设备号 */
        dtsled.devid = MKDEV(dtsled.major, 0);
        register_chrdev_region(dtsled.devid, DTSLED_CNT,DTSLED_NAME);
    }
    else {/* 没有定义设备号 */
        alloc_chrdev_region(&dtsled.devid, 0, DTSLED_CNT, DTSLED_NAME);/*动态申请设备号*/
        dtsled.major = MAJOR(dtsled.devid);                               /* 获取主设备号 */
        dtsled.minor = MINOR(dtsled.devid);                               /* 获取次设备号*/
    }
    printk("dtsled major=%d, minor=%d\r\n", dtsled.major, dtsled.minor);

    /* 2.初始化cdev */
    dtsled.cdev.owner = THIS_MODULE;
    cdev_init(&dtsled.cdev, &dtsled_fops);

    /* 3.添加一个cdev */
    cdev_add(&dtsled.cdev, dtsled.devid, DTSLED_CNT);

    /* 4.创建类 */
    dtsled.class = class_create(THIS_MODULE, DTSLED_NAME);
    if(IS_ERR(dtsled.class)) {
        return PTR_ERR(dtsled.class);
    }

    /* 5.创建设备 */
    dtsled.device = device_create(dtsled.class, NULL, dtsled.devid, NULL, DTSLED_NAME);
    if(IS_ERR(dtsled.device)) {
        return PTR_ERR(dtsled.device);
    }

    return 0;
 }

  /*
 * @description    : 驱动出口函数
 * @param - filp   :无
 * @return         :无
 */
 static void __exit led_exit(void)
 {
    /* 取消映射 */
    iounmap(IMX6U_CCM_CCGR1   );
    iounmap(SW_PAD_GPIO1_IO03 );      
    iounmap(SW_MUX_GPIO1_IO03 );      
    iounmap(GPIO1_DR          );      
    iounmap(GPIO1_GDIR        );      

    /* 注销字符设备驱动 */
    //unregister_chrdev(LED_MAJOR,LED_NAME);
    cdev_del(&dtsled.cdev);   /* 删除cdev */
    unregister_chrdev_region(dtsled.devid, DTSLED_CNT);/*注销设备号*/

    device_destroy(dtsled.class, dtsled.devid);
    class_destroy(dtsled.class);
 }

 /*
  * 将上面两个函数指定为驱动的入口和出口函数
  */
 module_init(led_init);
 module_exit(led_exit);

 /*
  * LICENSE和作者信息
  */
 MODULE_LICENSE("GPL");
 MODULE_AUTHOR("junluoyu");
dtsledApp.c
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
/***************************************************************  
文件名  : ledApp.c 
作者    : junluoyu 
版本    : V1.0 
描述    : led驱动测试APP 
其他    : 无 
日志    : 初版 V1.0 2022/11/28 
其他    : ./ledApp /dev/led 1(打开LED)|0(关闭LED)
***************************************************************/
#define LEDOFF 0 //关灯
#define LEDON  1 //开灯

/*
 * @description  : main主程序
 * @param -argc  :argv数组元素长度
 * @param -argv  :具体参数
 */ 
int main(int argc, char *argv[])
{
    int fd, retvalue;
    char *filename;
    unsigned char databuf[1];

    if (argc != 3) {
        printf("Error Usage!\r\n");
        return -1;
    }

    filename = argv[1];

    /* 打开驱动文件 */
    fd = open(filename, O_RDWR);
    if (fd < 0) {
        printf("Can't open file %s \r\n", filename);
        return -1;
    }

    databuf[0] = atoi(argv[2]);
    /* 向/dev/led文件写入数据 */
    retvalue = write(fd, databuf,sizeof(databuf));
    if (retvalue < 0) {
            printf("led control failed!\r\n");
            close(fd);
            return -1;
    } 
    /* 关闭led设备 */
    retvalue = close(fd);
    if (retvalue < 0) {
        printf("file %s close failed!\r\n", argv[1]);
        return -1;
    } 
    return 0;
}
Makefile
KERNELDIR := /home/yinliang/linux/kernel/nxp_linux
CURERENT_PATH := $(shell pwd)
obj-m := dtsled.o

build: kernel_moduls

kernel_moduls:
	$(MAKE) -C $(KERNELDIR) M=$(CURERENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURERENT_PATH) clean
复制dtsled.ko dtsledApp 到 lib/modules/4.1.15/
make -j16
arm-linux-gnueabihf-gcc dtsledApp.c  -o dtsledApp
cp dtsled.ko  dtsledApp  /home/yinliang/linux/nfs/rootfs/lib/modules/4.1.15/
启动板卡测试
cd /lib/modules/4.1.15
1.安装驱动模块  第一次加载先使用depmod
insmod dtsled.ko  
modprobe dtsled.ko          #会安装依赖文件
2.自动挂载
ls /dev/dtsled -l
3.测试
./dtsledApp /dev/dtsled 1  //开灯
./dtsledApp /dev/dtsled 0  //关灯
4.卸载驱动模块
rmmod dtsled.ko  
modprobe -r dtsled.ko    #会卸载依赖文件
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jun_luo_yu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值