字符设备驱动程序 --使用GPIO控制引脚高低电平(点亮LED)

本文参考所参考的原文链接:https://blog.csdn.net/qq_40334837/article/details/89574380

字符设备驱动框架

1. cdev是一个描述字符设备的结构体。

<include/linux/cdev.h>

struct cdev {
	struct kobject kobj;				// 内嵌的内核对象
	struct module *owner;				// 该字符设备所在的内核模块的对象指针
	const struct file_operations *ops;	// 该结构描述了字符设备所能实现的方法,是极为关键的一个结构体
	struct list_head list;				// 该结构描述了字符设备所能实现的方法,是极为关键的一个结构体
	dev_t dev;							// 字符设备的设备号,由主设备号和次设备号构成
	unsigned int count;					// 隶属于同一主设备号的次设备号的个数
};

void cdev_init(struct cdev *cdev, const struct file_operations *fops);

对 cdev 结构体做初始化, 最重要的就是建立 cdev 和 file_operations 之间的连接

cdev_init(&led_dev,&fops);

int cdev_add(struct cdev *p, dev_t dev, unsigned count);

1.该函数需要一个cdev结构体变量的地址,struct cdev结构体中包含了 dev_t和struct file_operations ,所以它能够把dev_t和驱动程序联系在一 起

2. dev参数就是之前申请到的设备号

3.申请的次设备号的个数

cdev_add(&led_dev,dev_num,1);

 2. dev_t 来定义设备号(分为主、次设备号)以确定字符设备的唯一性;

在32位机中是4个字节。主设备号占高12位,次设备号占20位。主设备号从0~4095,次设备号虽然占20位,但是它的有效范围是从 0~256。(在早期的 Unix 系统中,次设备号通常只占用一个字节(8 位),因此最大值为 256。)

主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型

次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备

3.file_operations

struct file_operations *fops:这是一个指向file_operations结构体变量的指针,这个结构体里面的成员绝大多数都是函数的指针。

这些函数的指针指向一个我们编写的函数,每个函数都有着各自的作用。这里列举常用的几个:

  1.  open 函数用于打开设备文件
  2.  release 函数用于释放(关闭)设备文件,与应用程序中的close 函数对应
  3. read 函数用于读取设备文件
  4. write 函数用于向设备文件写入(发送)数据
  5.  poll 是个轮询函数,用于查询设备是否可以进行非阻塞的读写
  6.  owner 拥有该结构体的模块的指针,一般设置为 THIS_MODULE
static struct file_operations fops =
{
	.owner = THIS_MODULE,
	.open = led_driver_open,
	.release = led_driver_close,
	.read = led_driver_read,
	.write = led_driver_write,
};

4.获取设备号,释放设备号;

字符设备驱动程序注册函数  在驱动初始化那里调用

分为静态注册和动态注册两种:

1.int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);注册字符设备(linux 版本 2.4 之前注册方式)。

register_chrdev_region( ):静态获取设备号

2. int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);动态获取设备号()

dev:参数dev的数据类型是dev_t,这个数据类型就是linux设备号的数据类型, 需要注意的是,这个数据类型的本身就是unsigned int型,其中包括了 主设备号和次设备号。linux系统提供了两个宏,分别用于从一个设备号中提取主设备号和次设备号:MAJOR和MINOR。这个参数就是需要我们定义一个dev_t变量,函数将获得的设备号通过这个参数传递出来。

baseminor:次设备号起始地址。alloc_chrdev_region 可以申请一段连续的多个设备号,这 些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor 为起始地址地址开始递 增。一般 baseminor 为 0,也就是说次设备号从 0 开始。

count:申请几个此设备 

name:为这些设备起个名字 

返回0表示成功,负数表示不成功

3.void unregister_chrdev_region(dev_t from, unsigned count);注销字符设备

from:要释放的设备号。(这里指的是主设备号,并非上面次设备号的起始地址)

count:表示从 from 开始,要释放的设备号数量。

5.自动添加设备节点

申请设备号 ——>初始化cdev——> 添加 cdev ——>创建 class——> 添加 device

class 设备类,设备类是一个逻辑上的分类,用于将一组相关设备归类在一起。

创建class使用带参宏#define class_create(owner, name),其实这个宏 又调用了__class_create(owner, name, &__key)函数.

. owenr指向模块的指针,这里只需传THIS_MODULE  

name为该设备的类型起个名字

该函数返回struct class *,用宏IS_ERR判断是否合法

struct device *device_create(struct class *class, struct device *parent,dev_t devt, void *drvdata, const char *fmt, ...);在一个已存在的设备类中注册一个具体的设备

1. class参数:之前获得的指针

2. parent:设备的父设备指针,传NULL

3. devt:设备号

4. drvdata:驱动需要的额外参数,这里传NULL

5. fmt:将来在/dev目录下生成的文件名

6.模块卸载函数

static void __exit led_driver_exit(void)
{
	iounmap(REG_GPBCON);
	iounmap(REG_GPBDAT);

	device_destroy(p_class,dev_num);
	class_destroy(p_class);
	cdev_del(&led_dev);

	unregister_chrdev_region(dev_num,1);
	printk("led_driver_exit ok\n");
}

如果不写这些卸载函数,下次就会注册失败。

字符设备驱动具体编写步骤

1.驱动模块的加载与卸载

module_init(xxx_init); //注册模块加载函数 

module_init() 函数用来向 Linux 内核注册一个模块加载函数,参数 xxx_init 就是需要注册的具体函数,当使用 “insmod” 命令加载驱动的时候,xxx_init 这个函数就会被调用。


module_exit(xxx_exit); //注册模块卸载函数

module_exit() 函数用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数,当使 用 “rmmod” 命令卸载具体驱动的时候 xxx_exit 函数就会被调用。

/* 驱动入口函数 */
static int __init xxx_init(void)
{
    /* 入口函数具体内容 */
    return 0;
}

/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
    /* 出口函数具体内容 */
}

// __init、__exit 定义在 inlucde/linux/init.h

// 声明在 inlucde/linux/init.h
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);

2.字符设备注册与注销

对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备

注册字符设备写在xxx_init(void)中,注销掉字符设备写在 xxx_exit(void)中。

在Linux内核源码顶层目录      linux-2.6.32.2/drivers/char   下创建  led_driver.c  文件 

static int __init led_driver_init(void);

3.具体设备操作函数的实现

file_operations 结构体就是设备的具体操作函数,我们定义了 file_operations 结构体类型的变量 test_fops,但是还没对其进行初始化,也就是初始化其中的 open、 releaseread 和 write 等具体的设备操作函数。

4.添加LICENSE 和作者信息

加入 LICENSE 信息和作者信息

MODULE_LICENSE宏是用来声明模块遵循的许可证。这对于确保模块与Linux内核的GPL兼容性非常重要。如果模块不是GPL兼容的,那么它就不能被加载到GPL许可的Linux内核中。

5.字符设备驱动模板

/* 打开设备 */
static int chrtest_open(struct inode *inode, struct file *filp)
{
    /* 用户实现具体功能 */
    return 0;
}

/* 从设备读取 */
static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    /* 用户实现具体功能 */
    return 0;
}

/* 向设备写数据 */
static ssize_t chrtest_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    /* 用户实现具体功能 */
    return 0;
}

/* 关闭/释放设备 */
static int chrtest_release(struct inode *inode, struct file *filp)
{
    /* 用户实现具体功能 */
    return 0;
}

static struct file_operations test_fops = {
    .owner = THIS_MODULE,
    .open = chrtest_open,
    .read = chrtest_read,
    .write = chrtest_write,
    .release = chrtest_release,
};

/* 驱动入口函数 */
static int __init xxx_init(void)
{
    /* 入口函数具体内容 */
    int retvalue = 0;
    
    /* 注册字符设备驱动 */
    retvalue = register_chrdev(200, "chrtest", &test_fops);
    if(retvalue < 0){
        /* 字符设备注册失败,自行处理 */
    }
    
    return 0;
}

/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
    /* 出口函数具体内容 */
    /* 注销字符设备驱动 */
    unregister_chrdev(200, "chrtest");
}

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

MODULE_LICENSE("GPL");

这个模板中没有自动添加设备节点,需要在insmod  chr_driver.ko后

使用  mknod  /dev/chrtest c 200 0   手动添加设备

然后再使用应用程序调用驱动。

 LED驱动编写

要想点亮LED,就要使用GPIO外设控制LED引脚的高低电平,就跟裸机程序一样,先要配置对应的寄存器。

操作led使用的两个寄存器GPBCON、 GPBDAT两个寄存器地址0x56000010、0x56000014这两个地址是物理地址,但是如果在程序中直接使用这两个地址linux会认为它们都是虚拟地址。

处理器中有一块叫做MMU(Memory Manage Unit)的内存映射单元,可以把引脚的物理地址映射到虚拟地址。

linux内核api函数提供了物理内存和虚拟内存的重映射带参宏:

#define ioremap(cookie,size)

第一个参数是物理地址,第二个参数是映射出的地址基类型字节数,宏 返回一个void *指针。

用字符设备驱动 控制gpio  来点亮 LED 的驱动程序。

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

#define GPBCON  (0x56000010)
#define GPBDAT  (0x56000014)

static unsigned int *REG_GPBCON;
static unsigned int *REG_GPBDAT;


static int led_driver_open(struct inode *pNode,struct file *fp)
{
	return 0;
}

static int led_driver_close(struct inode *pNode,struct file *fp)
{
	return 0;
}

static ssize_t led_driver_read(struct file *fp,char __user*userBuffer,size_t len,loff_t *offset)
{
	return 0;
}

static ssize_t led_driver_write(struct file *fp,const char __user *userBuffer,size_t len,loff_t*offset)
{

	static char c;
	copy_from_user(&c,userBuffer,1);//从用户空间复制一个字节到内核空间的c变量中。
	printk("c=%c\n",c);
	//c &= 0x0F;//将c的高四位清零,保留低四位。
	if(c == '0')
	{
		printk("00000\n");
		*REG_GPBDAT &=~(0x0f<<5);//将REG_GPBDAT寄存器的第5位至第9位清零。 点亮led
	}
	else if(c == '1')
	{		
		printk("11111\n");
		*REG_GPBDAT |=(0x0f<<5);//将REG_GPBDAT寄存器的第5位至第9位 1111。 关闭led
	}	
	//*REG_GPBDAT|=(c<<5);//将c的低四位左移5位后赋值给REG_GPBDAT的第5位至第9位。w
	return 1;
}

static struct file_operations fops =
{
	.owner = THIS_MODULE,
	.open = led_driver_open,
	.release = led_driver_close,
	.read = led_driver_read,
	.write = led_driver_write,
};

static dev_t dev_num;//设备号
struct cdev led_dev;//设备
static struct class *p_class;
static struct device *p_device;

static int __init led_driver_init(void)
{
	int ret;
	//申请设备号  此设备号的起始值为 0    申请1个设备   设备起个名字叫做 led_device
	ret = alloc_chrdev_region(&dev_num,0,1,"led_device");
	if(ret)
	{
		printk("alloc_chrdev_region is error\n");
		goto alloc_chrdev_region_err;
	}

	printk("major = %u,minior = %u\n",MAJOR(dev_num),MINOR(dev_num));

	//将设备初始化
	cdev_init(&led_dev,&fops);

	//向内核添加驱动程序  相当于  register_chrdev函数 的注册
	ret = cdev_add(&led_dev,dev_num,1);
	if(ret)
	{
		printk("cdev_add is error\n");
		goto cdev_add_err;
	}

	//创建一个设备类  led class
	p_class = class_create(THIS_MODULE,"led class");
	if(IS_ERR(p_class))
	{
		printk("class_create is error!");
		goto class_create_err;
	}

	//创建一个设备类 属于 之前led class的类别   这两步可以实现设备节点的自动添加
	//不需要使用mknod /dev/led 手动添加设备节点
	p_device = device_create(p_class,NULL,dev_num,NULL,"led");
	if(p_device == NULL)
	{
		printk("device_create is error\n");
		goto device_create_err;
	}



    //把需要配置引脚的物理地址映射到虚拟地址
	REG_GPBCON = ioremap(GPBCON,4);  //io引脚的控制寄存器
	REG_GPBDAT = ioremap(GPBDAT,4);  //io引脚的数据寄存器

	//配置寄存器
	*REG_GPBCON &= ~(0xff << 10);
	*REG_GPBCON |=(0X55<< 10);
	*REG_GPBDAT |=(0X0F << 5);
	return 0;
device_create_err:
	class_destroy(p_class);
class_create_err:
	cdev_del(&led_dev);
cdev_add_err:
	unregister_chrdev_region(dev_num,1);
alloc_chrdev_region_err:
	return ret;
}

static void __exit led_driver_exit(void)
{
	iounmap(REG_GPBCON);
	iounmap(REG_GPBDAT);

	device_destroy(p_class,dev_num);
	class_destroy(p_class);
	cdev_del(&led_dev);

	unregister_chrdev_region(dev_num,1);
	printk("led_driver_exit ok\n");
}


module_init(led_driver_init);

module_exit(led_driver_exit);

MODULE_LICENSE("GPL");

应用程序

#include<stdio.h>
#include<stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>

int main(int argc, const char *argv[])
{
	int fd = open("/dev/led",O_RDWR);
	if(fd < 0)
	{
		printf("fail to open led\n");
		return -1;
	}

	char s = 1;
	while(1)
	{
		s = '0';
		write(fd,&s,1);
		printf("LIANG");
		sleep(3);

		s = '1';
		write(fd,&s,1);
		sleep(3);

		printf("MIE");
	}
	return 0;
}

如何在板子上电后自动加载驱动?

(1)静态加载就是把驱动程序直接编译进内核,系统启动后可以直接调用。静态加载的缺点是调试起来比较麻烦,每次修改一个地方都要重新编译和下载内核,效率较低。若采用静态加载的驱动较多,会导致内核容量很大,浪费存储空间。

(2)动态加载利用了Linux的module特性,可以在系统启动后用insmod命令添加模块(.ko),在不需要的时候用rmmod命令卸载模块,采用这种动态加载的方式便于驱动程序的调试,同时可以针对产品的功能需求,进行内核的裁剪,将不需要的驱动去除,大大减小了内核的存储容量。

modprobe和insmod的区别是什么呢?

驱动编译完成以后扩展名为 .ko文件,有两种命令可以加载驱动模块:insmod 和 modprobe

insmod与modprobe命令

insmod 是最简单的模块加载命令,此命令用于加载指定的 .ko 模块

insmod drv.ko

insmod 命令不能解决模块的依赖关系,比如 drv.ko 依赖 first.ko 这个模块,就必须先使用 insmod 命令加载 first.ko 这个模块,然后再加载 drv.ko 这个模块。

modprobe 会分析模块的依赖关系,然后会将所有的依赖模块都加载到内核中,因此 modprobe 命令相比 insmod 要智能一些。modprobe 命令主要智能在提供了模块的依赖性分析、 错误检查、错误报告等功能,推荐使用modprobe 命令来加载驱动


在Linux中,modprobe和insmod都可以用来加载module,不过现在一般都推荐使用modprobe而不是insmod了。

(1)modprobe可以解决load module时的依赖关系,比如load moudleA就必须先load mouduleB之类的,它是通过/lib/modules//modules.dep文件来查找依赖关系的。而insmod不能解决依赖问题。
(2)modprobe默认会去/lib/modules/目录下面查找module,而insmod只在给它的参数中去找module(默认在当前目录找)。

这样,有时insmod也有它的有用之处,举个例子吧。
有/root/my-mod.ko这个module,cd /root/,然后用insmod my-mod.ko(insmod /root/my-mod.ko)就可以insert这个module了,但是用modprobe my-mod.ko(modprobe /root/my-mod.ko)却提示"FATAL: Module my-mod.ko not found",这就是因为modprobe是到/lib/modules/`uname -r`/下去找module的,如果没找到就是这样了。

                        

字符驱动设备控制 LED 灯需要使用 GPIO 接口来控制引脚的状态。在 Linux 内核中,可以使用 gpio_leds_probe 函数来初始化引脚的状态: ``` static int __init gpio_leds_probe(struct platform_device *pdev) { int ret = 0; int i; struct gpio_led *led = pdev->dev.platform_data; for (i = 0; i < ARRAY_SIZE(gpio_leds); i++) { gpio_direction_output(gpio_leds[i].gpio, !gpio_leds[i].default_state); ret = gpio_request(gpio_leds[i].gpio, "gpio_leds"); if (ret < 0) { pr_err("gpio_request failed for pin %d\n", gpio_leds[i].gpio); goto err_gpio_request; } gpio_leds[i].cdev.name = "gpio-leds"; gpio_leds[i].cdev.brightness_get = gpio_leds_get_brightness; gpio_leds[i].cdev.brightness_set = gpio_leds_set_brightness; gpio_leds[i].cdev.default_trigger = led->default_trigger; gpio_leds[i].cdev.brightness = !gpio_leds[i].default_state; ret = gpio_led_classdev_register(&pdev->dev, &gpio_leds[i].cdev); if (ret < 0) { pr_err("gpio_led_classdev_register failed for pin %d\n", gpio_leds[i].gpio); goto err_gpio_led_classdev_register; } } return 0; err_gpio_led_classdev_register: err_gpio_request: for (i = 0; i < ARRAY_SIZE(gpio_leds); i++) { gpio_free(gpio_leds[i].gpio); } return ret; } ``` 该函数会遍历一个 gpio_leds 数组,该数组包含了需要控制LED 灯的引脚信息。对于每个引脚,函数会使用 gpio_direction_output 函数来设置其方向为输出,并设置其初始状态为默认状态的反向(因为 LED 灯是低电平点亮的,所以默认状态为电平)。然后使用 gpio_request 函数来申请引脚,如果失败则返回错误码。接下来,函数会创建一个 gpio_led 结构体,并设置该结构体的成员变量。最后,使用 gpio_led_classdev_register 函数来注册字符设备驱动。如果注册失败,则会释放申请的引脚并返回错误码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值