视频:第5.1讲 Linux新字符设备驱动实验-新驱动框架的搭建_哔哩哔哩_bilibili

内容详见《【正点原子】I.MX6U嵌入式Linux驱动开发指南》四十二章

开发板:imx6ull mini

虚拟机:VMware17

ubuntu:ubuntu20.04

        之前一直使用register_chrdev函数来注册字符设备,这会将整个主设备号下的次设备号都用掉,浪费次设备号,还需要手动指定主设备号,还需要确定哪个主设备号没有被使用,还需要mknod创建设备节点文件 巴拉巴拉……

        既然已经有系统了,最方便的方法就是直接由系统分配。

1、自动分配设备号       

1.1 分配

#include <linux/cdev.h>

// 不指定主设备号,系统自动分配:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name) 
// dev       设备号。分配的设备号通过这个指针返回
// baseminor 从几号开始连续分配次设备号。一般都是从0开始
// count     需要连续注册几个设备
// name      设备名。会显示在/proc/devices


// 指定主、次设备号:
int register_chrdev_region(dev_t from, unsigned count, const char *name) 
// from    起始设备号。包括主、次设备号
// count   需要连续注册几个设备
// name    设备名。会显示在/proc/devices

1.2 卸载

#include <linux/cdev.h>

unregister_chrdev_region(dev_t from, unsigned count);
// from    设备号(包括主、次设备号)
// count   卸载的设备数量

1.3 设备号操作函数

//(来自/include/linux/kdev_t.h)
#define MINORBITS	20    // 主设备号长度为 20 bits
#define MINORMASK	((1U << MINORBITS) - 1)
 
#define MAJOR(dev)	((unsigned int) ((dev) >> MINORBITS))   // 获取主设备号
#define MINOR(dev)	((unsigned int) ((dev) & MINORMASK))    // 获取次设备号
#define MKDEV(ma,mi)	(((ma) << MINORBITS) | (mi))        // 拼接设备号

1.4 示例

int major = 0;    // 主设备号
int minor;        // 次设备号
dev_t devid;      // 设备号

if(major){    // 如果已经给出设备号
    devid = MKDEV(major,minor);    // 组成设备号
    register chrdev region(devid, 1, "test"); // 从devid开始分配1个设备号,设备名test
} else {      // 如果未指定设备号
    // 从次设备号0开始分配1个设备号,设备名test。分配的设备号赋值给devid
    alloc_chrdev_region(&devid, 0, 1, "test");
    major = MAJOR(devid);    // 获取主设备号
    minor = MINOR(devid);    // 获取次设备号
}

unregister_chrdev_region(devid, 1); // 从devid开始,注销1个设备

2、自动创建设备节点

详见《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.81.pdf》42.2

第5.3讲 Linux新字符设备驱动实验-自动创建设备节点_哔哩哔哩_bilibili

2.1 创建类

        自动创建设备节点的工作在驱动程序的入口函数中完成。

        使用class_create函数进行类创建,这是个宏定义,包括了两行代码:

//定义在include/linux/device.h 

#define class_create(owner, name)     \ 
({                                    \ 
  static struct lock_class_key __key; \ 
  __class_create(owner, name, &__key);\ 
}) 
// owner : 模块指针,一般都是THIS_MODULE
// name  : 设备名



// 其中__class_create()定义为:
struct class *__class_create(struct module *owner, const char *name, struct lock_class_key *key) 

2.2 删除类

        一般写到设备驱动出口文件里。

void class_destroy(struct class *class)
// class: 要删除的类

2.3 创建设备

        创建好类以后还不能自动创建设备。还需要在类下面创建设备。

struct device *device_create(struct class *class,      // 创建到哪个类下面
                             struct device *parent,    // 父设备。没有就填NULL
                             dev_t  devt,              // 设备号
                             void  *drvdata,           // 私有数据指针,用于向设备的回调函数传递数据。没有就写NULL
                             const char *fmt, ...)     // 设备名

2.4 删除设备

void device_destroy(struct class *class, dev_t devt)   // 指向类的指针,设备号

2.5 示例

#include <linux/device.h>    // 要加这个头文件


//首先在设备结构体中加入class结构体指针
struct newchrled_dev{
    struct class *class;    /* 类 */
    struct device *device;  /* 设备 */

    /* 其他内容 */
    dev_t devid;  /* 设备号 */
};


/* 创建 */
newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME);  // 创建类
if(IS_ERR(newchrled.class)) return PTR_ERR(newchrled.class);  // 错误处理

newchrled.device = device_create(newchrled.class, NULL, newchrled.devid, NULL, NEWCHRLED_NAME); // 创建设备
if(IS_ERR(newchrled.device)) return PTR_ERR(newchrled.device);  // 错误处理

/* 注意 先删除设备 再删除类 */  
device_destroy(newchrled.class,newchrled.devid);
class_destroy(newchrled.class);

3、字符设备注册方法

// 定义在/include/linux.cdev.h
struct cdev { 
    struct kobject  kobj; 
    struct module   *owner;           // 指向该字符设备所属驱动模块的指针
    const struct file_operations *ops;// 指向操作函数结构体file_operations
    struct list_head  list;           // 链表节点,用于将多个 cdev 结构体链接成链表。
    dev_t     dev;                    // 设备号
    unsigned int   count;             // 连续设备号数量
}; 

//初始化cdev
void cdev_init(struct cdev *cdev, const struct file_operations *fops) 
// cdev: 指向定义的cdev结构体
// fops: 指向定义的设备操作集合file_operations 

// 向Linux内核添加字符设备
int cdev_add(struct cdev *p, dev_t dev, unsigned count) 
// p: 指向要添加的字符设备的cdev结构体变量
// dev: 设备号
// count: 要添加的设备数量

// 删除字符设备
void cdev_del(struct cdev *p)
// p: 指向要删除的设备的cdev结构体变量

示例:


struct my_dev {
    struct cdev testcdev;  /* 在设备结构体中增加cdev结构体 */
    dev_t devid;           /* 设备号 */
    …………    /* 其他参数 */
};

/* 设备操作函数 */ 
static struct file_operations test_fops = { 
      .owner = THIS_MODULE, 
      …………    /* 其他操作 */
}; 

my_dev.testcdev.owner = THIS_MODULE; 
cdev_init(&my_dev.testcdev, &test_fops); /* 初始化cdev结构体变量 */
cdev_add(&my_dev.testcdev, my_dev.devid, 1);    /* 向linux内核添加设备  */


cdev_del(&my_dev.testcdev);              /* 删除设备 */

4、代码 

4.1 文件结构

和上个笔记一样,直接把以前的工作区复制过来改个名:

NEWCHRLED(工作区)
└── 3_newchrled
    ├── .vscode
    │   ├── c_cpp_properties.json
    │   └── settings.json
    ├── newchrled.c
    ├── newchrled.code-workspace
    ├── newchrledAPP.c
    └── Makefile

4.2 修改Makefile

        修改obj-m这一行为:

obj-m := newchrled.o

4.3 修改newchrled.c代码:

加载驱动后可以看到分配的设备号:

        除了自动分配设备号、自动创建设备文件以外,其他功能和上一篇笔记的寄存器点灯功能一致。

        newchrled.c:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/stat.h>
#include <linux/device.h>

#define NEWCHRLED_NAME "newchrled" // 设备名
#define NEWCHRLED_COUNT 1          // led设备数量

/* 寄存器物理地址 */
#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_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

/* LED状态 */
#define LEDOFF 0
#define LEDON  1

///////////////////////////////////////////////////////////////////////////////////////////////////

/* 设备结构体 */
struct newchrled_dev{
    struct cdev cdev;
    dev_t devid;            /* 设备号   需要#include <linux/stat.h>*/
    int major;              /* 主设备号 */
    int minor;              /* 此设备号 */
    struct class *class;    /* 类 */
    struct device *device;  /* 设备 */
};

struct newchrled_dev newchrled; /* led设备 */

//字符设备操作/////////////////////////////////////////////////////////////////////////////////////////////////

/* LED状态翻转 */
static void led_swtich(u8 sta){
    u32 val = 0;

    if(sta == LEDOFF){
        /* 关灯 */
        printk("turn off\r\n");
        val = readl(GPIO1_DR);  // 32bit读取函数
        val |= (1 << 3);       // 将bit3置为高电平,关闭LED
        writel(val, GPIO1_DR);
    }
    else if(sta == LEDON){
        /* 开灯 */
        val = readl(GPIO1_DR);  // 32bit读取函数
        val &= ~(1 << 3);       // 将bit3置为低电平,打开LED
        writel(val, GPIO1_DR);
        printk("turn on\r\n");
    }
}

static int newchrled_open(struct inode *inode, struct file *filp){
    return 0;
}
static int newchrled_release(struct inode *inode, struct file *filp){
    return 0;
}
static ssize_t newchrled_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos){
    int retvalue;
    unsigned char databuf[1];
    retvalue = copy_from_user(databuf, buf, count);
    if(retvalue < 0){
        printk("kernel write fauled!\r\n");
        return -EFAULT;
    }

    led_swtich(databuf[0]);
    return 0;
}

/* 字符设备操作集合 */
static const struct file_operations newchrled_fops = {
    .owner = THIS_MODULE,
    .write = newchrled_write,
    .open = newchrled_open,
    .release = newchrled_release,
};

/* 入口 */
static int __init newchrled_init(void){
    printk("newchrled init\r\n");
    int ret = 0;
    int val = 0;
    
    /* 0.地址映射 ----------------------------------------------- */
    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);

    /* 1.初始化 ------------------------------------------------- */
    val = readl(IMX6U_CCM_CCGR1);  // 32bit读取函数
    val &= ~(3 << 26); // 清除26、27位的数据为0,其余位保持不变。  其中3的二进制为11,~为取反操作。
    val |= 3 << 26;    // 将26、27位置为1
    writel(val, IMX6U_CCM_CCGR1);   // 将 val 写入 IMX6U_CCM_CCGR1,使能CCM_CCGR1的gpio1时钟。

    writel(0x5, SW_MUX_GPIO1_IO03); // 复用GPIO1——IO03
    writel(0x10B0, SW_PAD_GPIO1_IO03); //设置GPIO1_IO03电气属性

    val = readl(GPIO1_GDIR);  // 32bit读取函数
    val |= 1 << 3;  // 设置PIO1_IO03为output。 将GPIO1_GDIR第3位 置为1
    writel(val, GPIO1_GDIR);

    // 关灯
    val = readl(GPIO1_DR);  // 32bit读取函数
    val |= (1 << 3);       // 将bit3置为高电平,关闭LED
    writel(val, GPIO1_DR);

    /* 2.分配设备号----------------------------------------------- */
    if (newchrled.major){ // 如果已经给定主设备号
        newchrled.devid = MKDEV(newchrled.major, 0);
        ret = register_chrdev_region(newchrled.devid, NEWCHRLED_COUNT, NEWCHRLED_NAME);
    }
    else{ // 没有给定主设备号
        ret = alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_COUNT, NEWCHRLED_NAME);
        newchrled.major = MAJOR(newchrled.devid);
        newchrled.minor = MINOR(newchrled.devid);
    }
    if (ret < 0)
    {
        printk("newchrled chrdev_region error!\r\n");
        return -1;
    }
    printk("devid = %d:%d\r\n", newchrled.major, newchrled.minor);

    /* 3.注册字符设备----------------------------------------------- */
    newchrled.cdev.owner = THIS_MODULE;
    cdev_init(&newchrled.cdev, &newchrled_fops);
    ret = cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_COUNT);

    /* 4.自动创建设备节点-------------------------------------------- */
    newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME); // 创建类
    if(IS_ERR(newchrled.class)){
        return PTR_ERR(newchrled.class);
    }

    newchrled.device = device_create(newchrled.class, NULL, newchrled.devid, NULL, NEWCHRLED_NAME); // 创建设备
    if(IS_ERR(newchrled.device)){
        return PTR_ERR(newchrled.device);
    }

    return 0;
}
/* 出口 */
static int __exit newchrled_exit(void){
    printk("newchrled exit\r\n");

    /* 1.删除字符设备 */
    cdev_del(&newchrled.cdev);

    /* 2.注销设备号 */
    unregister_chrdev_region(newchrled.devid, NEWCHRLED_COUNT);

    /* 3.删除类*/
    device_destroy(newchrled.class,newchrled.devid);
    class_destroy(newchrled.class);

    return 0;
}

/* 驱动加载&卸载 */
module_init(newchrled_init);
module_exit(newchrled_exit);
MODULE_LICENSE("GPL");

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>

/* 
 * @description    : main主程序 
 * @param - argc   : argv数组元素个数 
 * @param - argv   : 具体参数 
 * @return         : 0 成功; else失败
 * 调用   ./newchrledAPP <filename> <0:1>  0关灯,1开灯
 * ./newchrledAPP /dev/newchrled 0  关灯
 * ./newchrledAPP /dev/newchrled 1  开灯
 */ 


#define LEDOFF 0
#define LEDON  1

int main(int argc, char *argv[]){

    if(argc != 3){  // 判断用法是否错误
        printf("Error Usage!\r\n");
        return -1;
    }

    char *filename;
    int fd = 0;
    unsigned char databuf[1];
    int retvalue = 0;

    filename = argv[1];
    fd = open(filename, O_RDWR);  // 读写模式打开驱动文件filename

    if(fd <0){
        printf("file %s open failed!\r\n");
        return -1;
    }
    
    databuf[0] = atoi(argv[2]);  // char 2 int

    retvalue = write(fd, databuf, sizeof(databuf));   // 缓冲区数据写入fd
    if(retvalue <0){
        printf("LED Control Failed!\r\n");
        close(fd);
        return -1;
    }


    close(fd);
    return 0;
}


 4.4 运行

# VSCODE终端
make
arm-linux-gnueabihf-gcc newchrledAPP.c -o newchrledAPP
sudo cp newchrled.ko newchrledAPP .../rootfs/lib/modules/4.1.15/

# 串口
cd /lib/modules/4.1.15
modprobe newchrled.ko            # 此时lsmod和cat /prob/devices 应当都能看到设备了
/dev/newchrled -l                # 应当有对应输出
./newchrledAPP /dev/newchrled 1  # 此时开发板红灯亮起
./newchrledAPP /dev/newchrled 0  # 此时开发板红灯熄灭
rmmod newchrled.ko

更多推荐