ZYNQ嵌入式Linux——main 03 Linux第一个驱动的实用化改造


register_chrdev 和unregister_chrdev 这两个函数是老版本驱动使用的函数,现在新的字符设备驱动已经不再使用这两个函数,而是使用 Linux 内核推荐的新字符设备驱动 API 函数。

一 之前的驱动存在哪些问题

1、设备号需要提前写,而且需要写死,导致不够灵活:

register_chrdev(
	LED_MAJOR, 
	LED_NAME, 
	&led_fops);

之前写的注册字符设备函数如上,仅提供一个主设备号,设备名,这样会浪费设备号,并且需要事先确定哪个主设备没用。

2、需要查找设备号,手动创建节点,不方便。

二 新驱动的几个先决概念:

在这里插入图片描述

学习几个概念:

1,设备号:

一个32bit的无符号整数,其中,12bit的主设备号,20bit的次设备号。
通常,主设备号用于标识和硬件设备关联的驱动程序,一个主设备号也可以被多个设备驱动程序共享。
比方说硬盘的快速模式和慢速模式,就是一个主设备的俩次设备。

如何查看主设备号分配:cat /proc/devices
dev_t这个类型就是专指设备号,定义在/include/linux/types.h下。
常用法如下:

int MAJOR(dev_t dev) //获得dev的主设备号
int MINOR(dev_t dev) //获得dev的次设备号
dev_t MKDEV(unsignde int major,unsigned int minor) //由主次设备号获得dev_t数据的宏。

dev_t dev = MKDEV(int major,int minor);//两个整数变成dev_t
unsigned major = MAJOR(dev);//查看dev_t的主设备号
unsigned minor = MINOR(dev);

分配方式1,直接写死:

eg:
register_chrdev(
	LED_MAJOR, 
	LED_NAME, 
	&led_fops);

我们看下函数声明:

/include/linux/fs.h:

static inline int register_chrdev(unsigned int major, const char *name,
				  const struct file_operations *fops)
{
	return __register_chrdev(major, 0, 256, name, fops);
}
extern int __register_chrdev(unsigned int major, unsigned int baseminor,
			     unsigned int count, const char *name,
			     const struct file_operations *fops);

显然,这个函数会从主设备号开始,占用256个次设备号:

分配方式2,动态分配:(优选)

fs/char_dev.c:

int alloc_chrdev_region(
	dev_t *dev,
	unsigned int firstminor, //指定要分配的第一个次设备号
	unsigned int count,      //需要分配多少设备号
	char *name)              //设备名字
//eg:
ret = alloc_chrdev_region(
	&newchrled.devid, 
	0, //指定要分配的第一个次设备号。
	1, 
	"lednew")

这个函数注册成功后,会把指针指向的dev给修改好。下面看下定义:
__register_chrdev_region的函数比较长,总之,如果主设备号是0,就是尝试注册到成功为止。
如果非0,就是走手动注册路线,如果这次失败就失败。
如果已经到了major上限,则返回error。

fs/char_dev.c:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
			const char *name)
{
	struct char_device_struct *cd;
	cd = __register_chrdev_region(0, baseminor, count, name);
	if (IS_ERR(cd))
		return PTR_ERR(cd);
	*dev = MKDEV(cd->major, cd->baseminor);
	return 0;
}

分配方式3,静态分配(需要指定特定设备号):

如果指定的设备号分配失败(IS_ERR(cd)),就输出PTR_ERR(cd)(非0)

int register_chrdev_region(
	dev_t first,
	unsigned int count, 
	char *name);
//eg:
ret = register_chrdev_region(
	devid,  //dev_t类型的结构体
	1, 
	"lednew");

来看定义:

fs/char_dev.c:

int register_chrdev_region(dev_t from, unsigned count, const char *name)
{
	struct char_device_struct *cd;
	dev_t to = from + count;
	dev_t n, next;

	for (n = from; n < to; n = next) {
		next = MKDEV(MAJOR(n)+1, 0);
		if (next > to)
			next = to;
		cd = __register_chrdev_region(MAJOR(n), MINOR(n),
			       next - n, name);
		if (IS_ERR(cd))
			goto fail;
	}
	return 0;
fail:
	to = n;
	for (n = from; n < to; n = next) {
		next = MKDEV(MAJOR(n)+1, 0);
		kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
	}
	return PTR_ERR(cd);
}

2,内核驱动结构体cdev:

1、cdev定义

如下:

linux/cdev.h:

//cdev的定义
struct cdev {
	struct kobject kobj;              //内嵌的内核对象
	struct module *owner;             //该字符设备所在的内核模块的对象指针.
	const struct file_operations *ops;//该结构描述了字符设备所能实现的方法,即file_operations
	struct list_head list;            //用来将已经向内核注册的所有字符设备形成链表.
	dev_t dev;                        //字符设备的设备号,由主设备号和次设备号构成.
	unsigned int count;               //隶属于同一主设备号的次设备号的个数
};

2、cdev初始化方法

先初始化:

/fs/char_dev.c:

定义:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
	memset(cdev, 0, sizeof *cdev);
	INIT_LIST_HEAD(&cdev->list);
	kobject_init(&cdev->kobj, &ktype_cdev_default);
	cdev->ops = fops;
}
示例:
static struct cdev led_cdev;//声明一个static的cdev结构体
cdev_init(&led_cdev, &led_fops);//用cdev_init函数进行初始化

cdev_add:向linux系统添加字符设备

int cdev_add(
	struct cdev *p,   //cdev结构体
	dev_t dev,        //设备号
	unsigned count);  //要添加的设备数量

注销字符设备之后要释放掉设备号,设备号释放函数如下:
void unregister_chrdev_region(dev_t from, unsigned count)
此函数有两个参数:
from:要释放的设备号。
count:表示从 from 开始,要释放的设备号数量。

3,自动创建device节点

我们在建立好file_operations这个类之后,使用device_create函数创建设备:
用device_destroy函数清理。如下:

struct device *device_create(
	struct class *class, 
	struct device *parent,
	dev_t devt,
	void *drvdata,
	const char *fmt, ...);
//参数 class 就是设备要创建哪个类下面
//参数 parent 是父设备,一般设置为NULL
//参数 devt 是设备号
//参数 drvdata 是设备可能会使用的一些数据,一般为NULL
// 参数 fmt 是设备名字,如果设置 fmt=xxx 的话,就会生成/dev/xxx 这个设备文件。返回值就是创建好的设备。
void device_destroy(struct class *class, dev_t devt);
//参数 class 就是设备要创建哪个类下面
//参数 devt 是设备号

注意到有个class,如何创建?
利用class_create(/include/linux/device/class.h)进行创建:

#define class_create(owner, name)		\
({						\
	static struct lock_class_key __key;	\
	__class_create(owner, name, &__key);	\
})

使用示例如下:

/* 设备驱动相关结构体*/
static struct cdev led_cdev;
static dev_t led_dev_t;
static struct class *led_dev_class;
static struct device *led_device;

........
    /* 4-1 创建类*/
    led_dev_class = class_create(THIS_MODULE, LED_NAME);
    if (IS_ERR(led_dev_class))
    {
        ret = PTR_ERR(led_dev_class);
        goto out3;
    }

    /* 4-2 创建设备节点*/
    led_device = device_create(
        led_dev_class,
        NULL,
        led_dev_t,
        NULL,
        LED_NAME);
    if (IS_ERR(led_device))
    {
        ret = PTR_ERR(led_device);
        goto out4;
    }
.......

三 修改上节的函数

先对齐思路,即新式驱动如何做:

  1. 要注册驱动,需要用cdev_add函数。
  2. cdev_add函数,需要一个cdev结构体设备号、数量
  3. cdev结构体的初始化,需要cdev_init函数,init的时候,需要设备号、fileop
  4. 设备号,需要分配,两种方法:
    1. 动态:alloc_chrdev_region()
    2. 静态:register_chrdev_region()
      register_chrdev_region直接将Major注册进入,而 alloc_chrdev_region从Major = 0 开始,逐个查找设备号,直到找到一个闲置的设备号,并将其注册进去
  5. **udev(mdev)**来实现设备文件的自动创建

一:修改点

修改点1:头文件

增加了这三个头文件:
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/cdev.h>

修改点2:声明几个static变量:

/* 设备驱动相关结构体*/
static struct cdev led_cdev;
static dev_t led_dev_t;
static struct class *led_dev_class;
static struct device *led_device;

修改点3:init函数中的注册字符设备部分

原来:

ret = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
    if (0 > ret)
    {
        printk(KERN_ERR "Register LED driver failed!\r\n");
        return ret;

现在:


    /* 1 申请设备号 */
    ret = alloc_chrdev_region(
        &led_dev_t,
        0,
        1,
        LED_NAME);
    if (ret)
        goto out1;
    printk("lednew major=%d,minor=%d\r\n",
           MAJOR(led_dev_t),
           MINOR(led_dev_t));

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

    /* 3 向内核添加cdev */
    ret = cdev_add(&led_cdev, led_dev_t, 1);
    if (ret)
        goto out2;

    /* 4-1 创建类*/
    led_dev_class = class_create(THIS_MODULE, LED_NAME);
    if (IS_ERR(led_dev_class))
    {
        ret = PTR_ERR(led_dev_class);
        goto out3;
    }

    /* 4-2 创建设备节点*/
    led_device = device_create(
        led_dev_class,
        NULL,
        led_dev_t,
        NULL,
        LED_NAME);
    if (IS_ERR(led_device))
    {
        ret = PTR_ERR(led_device);
        goto out4;
    }

    return 0;
out4:
    class_destroy(led_dev_class);
out3:
    cdev_del(&led_cdev);
out2:
    unregister_chrdev_region(led_dev_t, 1);
out1:
    iounmap(data_addr);
    iounmap(dirm_addr);
    iounmap(outen_addr);
    iounmap(intdis_addr);
    iounmap(aper_clk_ctrl_addr);

    return ret;

二:源码

// #include <linux/typlinuxkes.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 <linux/uaccess.h>
#include <asm/io.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/cdev.h>

// #define LED_MAJOR 200  /* 主设备号 */
#define LED_NAME "lednew" /* 设备名字 */
/* GPIO 相关寄存器地址定义 */
#define ZYNQ_GPIO_REG_BASE 0xE000A000
#define DATA_OFFSET 0x00000040
#define DIRM_OFFSET 0x00000204
#define OUTEN_OFFSET 0x00000208
#define INTDIS_OFFSET 0x00000214
#define APER_CLK_CTRL 0xF800012C
/* 映射后的寄存器虚拟地址指针 */
static void __iomem *data_addr;
static void __iomem *dirm_addr;
static void __iomem *outen_addr;
static void __iomem *intdis_addr;
static void __iomem *aper_clk_ctrl_addr;
/* 设备驱动相关结构体*/
static struct cdev led_cdev;
static dev_t led_dev_t;
static struct class *led_dev_class;
static struct device *led_device;

static int led_open(struct inode *inode, struct file *filp)
{
    return 0;
}
static ssize_t led_read(struct file *filp, char __user *buf,
                        size_t cnt, loff_t *offt)
{
    return 0;
}
static ssize_t led_write(struct file *filp, const char __user *buf,
                         size_t cnt, loff_t *offt)
{
    int ret;
    int val;
    char kern_buf[1];

    ret = copy_from_user(kern_buf, buf, cnt); // 得到应用层传递过来的数据
    if (0 > ret)
    {
        printk(KERN_ERR "kernel write failed!\r\n");
        return -EFAULT;
    }

    val = readl(data_addr);
    if (0 == kern_buf[0])
        val &= ~(0x1U << 7); // 如果传递过来的数据是 0 则关闭 led
    else if (1 == kern_buf[0])
        val |= (0x1U << 7); // 如果传递过来的数据是 1 则点亮 led
    writel(val, data_addr);
    return 0;
}
static int led_release(struct inode *inode, struct file *filp)
{
    return 0;
}
static struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .read = led_read,
    .write = led_write,
    .release = led_release,
};
static int __init led_init(void)
{
    u32 val;
    int ret;

    /* 1.寄存器地址映射 */
    data_addr = ioremap(ZYNQ_GPIO_REG_BASE + DATA_OFFSET, 4);
    dirm_addr = ioremap(ZYNQ_GPIO_REG_BASE + DIRM_OFFSET, 4);
    outen_addr = ioremap(ZYNQ_GPIO_REG_BASE + OUTEN_OFFSET, 4);
    intdis_addr = ioremap(ZYNQ_GPIO_REG_BASE + INTDIS_OFFSET, 4);
    aper_clk_ctrl_addr = ioremap(APER_CLK_CTRL, 4);

    /* 2.使能 GPIO 时钟 */
    val = readl(aper_clk_ctrl_addr);
    val |= (0x1U << 22);
    writel(val, aper_clk_ctrl_addr);
    /* 3.关闭中断功能 */
    val |= (0x1U << 7);
    writel(val, intdis_addr);

    /* 4.设置 GPIO 为输出功能 */
    val = readl(dirm_addr);
    val |= (0x1U << 7);
    writel(val, dirm_addr);

    /* 5.使能 GPIO 输出功能 */
    val = readl(outen_addr);
    val |= (0x1U << 7);
    writel(val, outen_addr);

    /* 6.默认关闭 LED */
    val = readl(data_addr);
    val &= ~(0x1U << 7);
    writel(val, data_addr);

    /* 7.注册字符设备驱动 */
    /* 1 申请设备号 */
    ret = alloc_chrdev_region(
        &led_dev_t,
        0,
        1,
        LED_NAME);
    if (ret)
        goto out1;
    printk("lednew major=%d,minor=%d\r\n",
           MAJOR(led_dev_t),
           MINOR(led_dev_t));

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

    /* 3 向内核添加cdev */
    ret = cdev_add(&led_cdev, led_dev_t, 1);
    if (ret)
        goto out2;

    /* 4-1 创建类*/
    led_dev_class = class_create(THIS_MODULE, LED_NAME);
    if (IS_ERR(led_dev_class))
    {
        ret = PTR_ERR(led_dev_class);
        goto out3;
    }

    /* 4-2 创建设备节点*/
    led_device = device_create(
        led_dev_class,
        NULL,
        led_dev_t,
        NULL,
        LED_NAME);
    if (IS_ERR(led_device))
    {
        ret = PTR_ERR(led_device);
        goto out4;
    }

    return 0;
out4:
    class_destroy(led_dev_class);
out3:
    cdev_del(&led_cdev);
out2:
    unregister_chrdev_region(led_dev_t, 1);
out1:
    iounmap(data_addr);
    iounmap(dirm_addr);
    iounmap(outen_addr);
    iounmap(intdis_addr);
    iounmap(aper_clk_ctrl_addr);

    return ret;
}
static void __exit led_exit(void)
{
    /* 1.1 卸载设备 */
    // unregister_chrdev(LED_MAJOR, LED_NAME);
    device_destroy(led_dev_class, led_dev_t);

    /* 1.2 注销类 */
    class_destroy(led_dev_class);

    /* 1.3 删除cdev */
    cdev_del(&led_cdev);

    /* 1.4 注销设备号 */
    unregister_chrdev_region(led_dev_t, 1);

    /* 2.取消内存映射 */
    iounmap(data_addr);
    iounmap(dirm_addr);
    iounmap(outen_addr);
    iounmap(intdis_addr);
    iounmap(aper_clk_ctrl_addr);
}
/* 驱动模块入口和出口函数注册 */
module_init(led_init);
module_exit(led_exit);

MODULE_AUTHOR("lyw <xx@qq.com>");
MODULE_DESCRIPTION("Lyw ZYNQ GPIO LED Driver");
MODULE_LICENSE("GPL");

四 编译一个可以实时加载的内核模块

make -C $(KERN_DIR) M=`pwd` modules
  1. -C参数:指定一个目录,在该目录中查找名为Makefile的文件,并执行那个makefile。
  2. M参数:在linux内核编译的过程中,M参数可以指定内核模块的源码所在的目录。
  3. modules参数:指示构建系统仅编译内核模块,而不编译整个内核。生成相应的.ko(内核对象)文件。

为了方便,我们写一个makefile,放在和led.c一个目录下面:Makefile:

KERN_DIR := /home/lyw/xilinx-linux/linux-kernel-driver-develop/linux-xlnx-xlnx_rebase_v5.15_LTS_2022.1

obj-m := led.o

all:
	make -C $(KERN_DIR) M=`pwd` modules

clean:
	make -C $(KERN_DIR) M=`pwd` clean

make编译成功后,就会生成一个名为“led.ko”的驱动模块文件

五 运行测试

  1. 将上一小节编译出来的 led.ko 和 ledApp 这两个文件拷贝到 NFS 共享目录下根文件系统的/lib/modules/5.4.0-xilinx 文件夹下
  2. 启动开发板进入这个目录加载驱动
depmod //第一次加载驱动的时候需要运行此命令
modprobe led.ko //加载驱动
  1. 创建“/dev/led”设备节点,命令如下:
mknod /dev/led c 200 0

这里面就是创建字符设备驱动节点,
c代表字符设备,200代表主设备号,0代表次设备号。
必须先确保内核中有相应的驱动程序来支持这个设备号,并且该驱动程序已经加载到内核中!才可以成功创建这个节点。

  1. 测试点亮小灯:
    编写一个测试APP:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[])
{
    int fd, ret;
    unsigned char buf[1];

    if (3 != argc)
    {
        printf("Usage:\n"
               "\t./ledApp /dev/led 1 @ close LED\n"
               "\t./ledApp /dev/led 0 @ open LED\n");
        return -1;
    }

    /* 打开设备 */
    fd = open(argv[1], O_RDWR);
    if (0 > fd)
    {
        printf("file %s open failed!\r\n", argv[1]);
        return -1;
    }

    /* 将字符串转换为 int 型数据 */
    buf[0] = atoi(argv[2]);

    /* 向驱动写入数据 */
    ret = write(fd, buf, sizeof(buf));
    if (0 > ret)
    {
        printf("LED Control Failed!\r\n");
        close(fd);
        return -1;
    }

    /* 关闭设备 */
    close(fd);
    return 0;
}

编译出来:

$CC ledApp.c -o ledApp
//拷贝到开发板root的home文件夹下面
cp ledApp ~/linux/nfs/rootfs/home/root/

进入开发板shell

//点亮 LED 灯
./ledApp /dev/led 1
//熄灭 LED 灯
./ledApp /dev/led 0
  1. 卸载驱动:
    rmmod led.ko
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值