[ARM Linux 驱动开发] “新”字符设备驱动
文章标题中的“新”,只是对于 [ARM Linux 驱动开发] 第一个字符设备驱动 这篇文章而言的。先前的字符设备驱动,我们需要自己检查还未被分配的设备号,之后传递给相关函数;并且我们还需要手动使用 mknod 命令来创建字符设备文件。本篇文章解决以上两个问题,让驱动更加“智能”。
1. 自动分配设备号
之前我们是通过 register_chrdev 函数来注册字符设备的。但如之前所说,设备号需要我们自己确定,且一个主设备号下的所有此设备号都会使用掉。
现在我们了解一组新的函数,它们可以解决当前的问题:
- int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
- const char *name);
alloc_chrdev_region 函数可用于动态注册一组设备号。因为是动态申请,所以第一个参数 dev 是一个指针,返回动态分配的设备号。第二个参数 baseminor 指明起始的次设备号。第三个参数 count 指明申请的设备号个数,一般为 1 个。最后一个参数 name 指明设备名称。
- int register_chrdev_region(dev_t from, unsigned count, const char *name);
register_chrdev_region 和 alloc_chrdev_region 函数类似,都是手动指定设备号的。不同点是 register_chrdev_region 是可以注册一组连续的设备号。第一个参数 from 指明起始设备号,其中必须指明主设备号。count 和 name 参数指明申请设备号个数和设备名称。
- void unregister_chrdev_region(dev_t from, unsigned count);
无论是 alloc_chrdev_region 还是 register_chrdev_region 注册的设备号,用完都需要使用 register_chrdev_region 函数进行卸载。
字符设备注册
之前的 register_chrdev 函数是设备号分配加注册设备一体的(可以看到有一个 file_operations 参数)。现在我们使用 alloc_chrdev_region 这系列函数“自行”分配设备号了,所以还需要“自行”注册设备。
linux 中使用 cdev 结构体来表示一个字符设备,在其上也有一系列函数进行操作:
- void cdev_init(struct cdev *cdev, const struct file_operations *fops);
cdev_init 函数对字符设备进行初始化。第一个参数 cdev 指定要初始化的字符设备结构体;第二个参数 fops 我们已经很熟悉了,是文件操作函数集合。
- int cdev_add(struct cdev *p, dev_t dev, unsigned count);
cdev_add 函数用于把指定的字符设备添加到 linux 系统。第一个参数 p 就是要添加的字符设备;第二个参数 dev 指明起始设备号;第三个参数 count 指明添加的设备数量。
- void cdev_del(struct cdev *p);
cdev_del 函数用于把指定的字符设备从 linux 系统中移除。参数 p 指明需要移除的字符设备。
驱动程序编写与测试
现在我们开始编写驱动程序,先将文件操作函数留空。重点关注模块加载和卸载函数。
模块加载函数 led_init:如果指定了主设备号,则通过 register_chrdev_region 注册(第 47 至 50 行);否则通过 alloc_chrdev_region 动态分配(第 54 至 56 行)。第 60 至 63 行初始化字符设备,并将字符设备添加到 linux 系统中。
模块卸载函数 led_exit:第 70 行将此字符设备从 linux 系统中移除;第 72 行卸载注册的设备号。
程序中将所有需要用到的全局变量放在了自定义的结构体 LedDev 中。相比于定义多个全局变量,定义一个全局的结构体变量,会显得更加紧凑一点。
- #include <linux/module.h>
- #include <linux/cdev.h>
- #include <linux/device.h>
- #include <linux/fs.h>
- #define LED_CNT 1
- #define LED_NAME "led"
- struct LedDev
- {
- /* 设备号相关 */
- dev_t devId;
- int major;
- int minor;
- /* cdev */
- struct cdev cdev;
- };
- struct LedDev ledDev;
- int led_open(struct inode* inode, struct file* filp)
- {
- return 0;
- }
- int led_release(struct inode *inode, struct file *filp)
- {
- return 0;
- }
- ssize_t led_write(struct file* file, const char __user* buf, size_t count, loff_t* ppos)
- {
- return 0;
- }
- const struct file_operations led_fops =
- {
- .owner = THIS_MODULE,
- .open = led_open,
- .release = led_release,
- .write = led_write,
- };
- static int __init led_init(void)
- {
- /* 注册设备号 */
- if (ledDev.major)
- {
- ledDev.devId = MKDEV(ledDev.major, ledDev.minor);
- register_chrdev_region(ledDev.devId, LED_CNT, LED_NAME);
- }
- else
- {
- alloc_chrdev_region(&ledDev.devId, 0, LED_CNT, LED_NAME);
- ledDev.major = MAJOR(ledDev.devId);
- ledDev.minor = MINOR(ledDev.devId);
- }
- printk("major = %d, minor = %d\n", ledDev.major, ledDev.minor);
- /* 设置 cdev */
- ledDev.cdev.owner = THIS_MODULE;
- cdev_init(&ledDev.cdev, &led_fops);
- cdev_add(&ledDev.cdev, ledDev.devId, LED_CNT);
- return 0;
- }
- static void __exit led_exit(void)
- {
- cdev_del(&ledDev.cdev);
- unregister_chrdev_region(ledDev.devId, LED_CNT);
- }
- module_init(led_init);
- module_exit(led_exit);
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("hanhan blog");
程序写好后,我们把驱动文件放入开发板上进行测试。因为没有指定主设备号,所以程序是动态分配的设备号,从图 1 中可以看到,当前给 led 驱动设备分配了 (249,0) 这个设备号。

2. 自动创建设备文件
上一节我们已经解决了手动分配设备号的问题,现在接着解决需要通过 mknod 命令手动创建字符设备文件的问题。
在 linux 下通过 udev 来实现设备文件的创建和删除。使用 busybox 构建根文件系统的时候,busybox 会创建一个 udev 的简化版——mdev,来实现设备文件节点的自动创建与删除。这边我们先不细究原理,还是直接看需要使用到的函数:
- struct class *class_create(struct module *owner, const char *name);
class_create 函数(宏)用于创建一个 class 结构体。第一个参数 owner 指定其所属的模块,一般为 THIS_MODULE;第二个参数 name 指定其名称。
- void class_destroy(struct class *cls);
class_destroy 函数用于销毁一个 class。
- struct device *device_create(struct class *class, struct device *parent,
- dev_t devt, void *drvdata, const char *fmt, ...);
device_create 函数用于在类下自动创建设备节点。参数 class 指明设备文件要在哪个类下创建;参数 parent 指明父设备,一般为 NULL;参数 devt 指明设备号;参数 drvdata 是设备需要使用到的数据,一般为 NULL;参数 fmt 指定设备名称,可用可变参数指定。
- void device_destroy(struct class *class, dev_t devt);
device_destroy 函数用于删除创建的设备。参数 class 指明所属的类;参数 devt 指明要删除的设备号。
驱动程序编写与测试
在了解了自动创建设备文件的函数之后,我们在之前写的驱动代码上增加功能。在模块的加载和卸载函数上进行了添加。
模块加载函数:第 69 行创建 class 结构体;第 70 至 73 行对返回的地址进行判断,如果错误则进行返回。第 75 行创建设备节点;同样第 76 至 79 行对返回的地址进行判断。
模块卸载函数:第 89 行卸载创建的设备设备节点;第 90 行卸载创建的类。注意先卸载设备节点,后卸载类。
- #include <linux/module.h>
- #include <linux/cdev.h>
- #include <linux/device.h>
- #include <linux/fs.h>
- #define LED_CNT 1
- #define LED_NAME "led"
- struct LedDev
- {
- /* 设备号相关 */
- dev_t devId;
- int major;
- int minor;
- /* cdev */
- struct cdev cdev;
- /* mdev */
- struct class* class;
- struct device* device;
- };
- struct LedDev ledDev;
- int led_open(struct inode* inode, struct file* filp)
- {
- return 0;
- }
- int led_release(struct inode *inode, struct file *filp)
- {
- return 0;
- }
- ssize_t led_write(struct file* file, const char __user* buf, size_t count, loff_t* ppos)
- {
- return 0;
- }
- const struct file_operations led_fops =
- {
- .owner = THIS_MODULE,
- .open = led_open,
- .release = led_release,
- .write = led_write,
- };
- static int /*__init*/ led_init(void)
- {
- /* 注册设备号 */
- if (ledDev.major)
- {
- ledDev.devId = MKDEV(ledDev.major, ledDev.minor);
- register_chrdev_region(ledDev.devId, LED_CNT, LED_NAME);
- }
- else
- {
- alloc_chrdev_region(&ledDev.devId, 0, LED_CNT, LED_NAME);
- ledDev.major = MAJOR(ledDev.devId);
- ledDev.minor = MINOR(ledDev.devId);
- }
- printk("major = %d, minor = %d\n", ledDev.major, ledDev.minor);
- /* 设置 cdev */
- ledDev.cdev.owner = THIS_MODULE;
- cdev_init(&ledDev.cdev, &led_fops);
- cdev_add(&ledDev.cdev, ledDev.devId, LED_CNT);
- /* 设置 mdev */
- ledDev.class = class_create(THIS_MODULE, LED_NAME);
- if (IS_ERR(ledDev.class))
- {
- return PTR_ERR(ledDev.class);
- }
- ledDev.device = device_create(ledDev.class, NULL, ledDev.devId, NULL, LED_NAME);
- if (IS_ERR(ledDev.device))
- {
- return PTR_ERR(ledDev.device);
- }
- return 0;
- }
- static void __exit led_exit(void)
- {
- cdev_del(&ledDev.cdev);
- unregister_chrdev_region(ledDev.devId, LED_CNT);
- device_destroy(ledDev.class, ledDev.devId);
- class_destroy(ledDev.class);
- }
- module_init(led_init);
- module_exit(led_exit);
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("hanhan blog");
对编译的驱动程序进行测试,加载驱动之后可以发现自动创建了 /dev/led 字符设备文件。

3. 补齐点灯功能
前两节已经把本章需要介绍的内容讲完了。最后需要把这个“新”的 led 驱动补齐一下,补上它最初的点灯功能。这部分内容和 [ARM Linux 驱动开发] LED 驱动开发(ioremap) 完全一样,这里便不再赘述了。
- #include <linux/module.h>
- #include <linux/cdev.h>
- #include <linux/device.h>
- #include <linux/fs.h>
- #include <asm/io.h>
- #include <asm/uaccess.h>
- #define LED_CNT 1
- #define LED_NAME "led"
- struct LedDev
- {
- /* 设备号相关 */
- dev_t devId;
- int major;
- int minor;
- /* cdev */
- struct cdev cdev;
- /* mdev */
- struct class* class;
- struct device* device;
- };
- struct LedDev ledDev;
- /* 寄存器物理地址 */
- #define CCM_CCGR1_BASE (0x020c406c)
- #define SW_MUX_GPIO1_IO03_BASE (0x020e0068)
- #define SW_PAD_GPIO1_IO03_BASE (0x020e02f4)
- #define GPIO1_GDIR_BASE (0x0209c004)
- #define GPIO1_DR_BASE (0x0209c000)
- /* 映射后的虚拟地址 */
- void __iomem* CCM_CCGR1;
- void __iomem* SW_MUX_GPIO1_IO03;
- void __iomem* SW_PAD_GPIO1_IO03;
- void __iomem* GPIO1_GDIR;
- void __iomem* GPIO1_DR;
- int led_open(struct inode* inode, struct file* filp)
- {
- return 0;
- }
- int led_release(struct inode *inode, struct file *filp)
- {
- return 0;
- }
- void turn_on_led(void)
- {
- int val = readl(GPIO1_DR);
- val &= ~(1 << 3);
- writel(val, GPIO1_DR);
- }
- void turn_off_led(void)
- {
- int val = readl(GPIO1_DR);
- val |= (1 << 3);
- writel(val, GPIO1_DR);
- }
- ssize_t led_write(struct file* file, const char __user* buf, size_t count, loff_t* ppos)
- {
- int res;
- unsigned char writeBuf[1], status;
- res = copy_from_user(writeBuf, buf, sizeof(writeBuf));
- if (res < 0)
- {
- printk("copy_from_user() failed\n");
- return -EFAULT;
- }
- status = writeBuf[0];
- if (status == 1)
- turn_on_led();
- else
- turn_off_led();
- return 0;
- }
- const struct file_operations led_fops =
- {
- .owner = THIS_MODULE,
- .open = led_open,
- .release = led_release,
- .write = led_write,
- };
- static int /*__init*/ led_init(void)
- {
- int val;
- /* 寄存器映射 */
- 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_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
- GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
- /* 使能 GPIO1 时钟*/
- val = readl(CCM_CCGR1);
- val |= (3 << 26);
- writel(val, CCM_CCGR1);
- /* 复用为 GPIO */
- writel(5, SW_MUX_GPIO1_IO03);
- /* IO 属性 */
- writel(0x10b0, SW_PAD_GPIO1_IO03);
- /* GPIO 设置为输出 */
- val = readl(GPIO1_GDIR);
- val |= (1 << 3);
- writel(val, GPIO1_GDIR);
- /* 默认关闭 */
- val = readl(GPIO1_DR);
- val |= (1 << 3);
- writel(val, GPIO1_DR);
- /* 注册设备号 */
- if (ledDev.major)
- {
- ledDev.devId = MKDEV(ledDev.major, ledDev.minor);
- register_chrdev_region(ledDev.devId, LED_CNT, LED_NAME);
- }
- else
- {
- alloc_chrdev_region(&ledDev.devId, 0, LED_CNT, LED_NAME);
- ledDev.major = MAJOR(ledDev.devId);
- ledDev.minor = MINOR(ledDev.devId);
- }
- printk("major = %d, minor = %d\n", ledDev.major, ledDev.minor);
- /* 设置 cdev */
- ledDev.cdev.owner = THIS_MODULE;
- cdev_init(&ledDev.cdev, &led_fops);
- cdev_add(&ledDev.cdev, ledDev.devId, LED_CNT);
- /* 设置 mdev */
- ledDev.class = class_create(THIS_MODULE, LED_NAME);
- if (IS_ERR(ledDev.class))
- {
- return PTR_ERR(ledDev.class);
- }
- ledDev.device = device_create(ledDev.class, NULL, ledDev.devId, NULL, LED_NAME);
- if (IS_ERR(ledDev.device))
- {
- return PTR_ERR(ledDev.device);
- }
- return 0;
- }
- static void __exit led_exit(void)
- {
- cdev_del(&ledDev.cdev);
- unregister_chrdev_region(ledDev.devId, LED_CNT);
- device_destroy(ledDev.class, ledDev.devId);
- class_destroy(ledDev.class);
- }
- module_init(led_init);
- module_exit(led_exit);
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("hanhan blog");