目录
一、何谓驱动框架
1、驱动是谁写的
(1)驱动开发工程师
(2)内核维护者
2、驱动编程协作要求
(1)接口标准化,不要乱七八糟,有好几套接口
(2)尽量降低驱动开发者难度
3、到底什么是驱动框架
(1)内核中驱动部分维护者针对每个种类的驱动设计一套成熟的、标准的、典型的驱动实现,然后把不同厂家的同类硬件驱动中相同的部分抽出来自己实现好,再把不同部分留出接口给具体的驱动开发工程师来实现,这就叫驱动框架。
(2)内核维护者在内核中设计了一些统一管控系统资源的体系,这些体系让内核能够对资源在各个驱动之间的使用统一协调和分配,保证整个内核的稳定健康运行。譬如系统中所有的GPIO就属于系统资源,每个驱动模块如果要使用某个GPIO就要先调用特殊的接口先申请,申请到后使用,使用完后要释放。又譬如中断号也是一种资源,驱动在使用前也必须去申请。这也是驱动框架的组成部分。
(3)一些特定的接口函数、一些特定的数据结构,这些是驱动框架的直接表现。
二、内核驱动框架中LED的基本情况
内核维护者提供的:kernel\drivers\leds\led-class.c
kernel\drivers\leds\led-core.c
1、相关文件
(1)drivers/leds目录,这个目录就是驱动框架规定的LED这种硬件的驱动应该待的地方。
(2)led-class.c和led-core.c,这两个文件加起来属于LED驱动框架的第一部分,这两个文件是内核开发者提供的,他们描述的是内核中所有厂家的不同LED硬件的相同部分的逻辑。
(3)leds-xxxx.c,这个文件是LED驱动框架的第2部分,是由不同厂商的驱动工程师编写添加的,厂商驱动工程师结合自己公司的硬件的不同情况来对LED进行操作,使用第一部分提供的接口来和驱动框架进行交互,最终实现驱动的功能。
2、九鼎科技s5pv210开发板移植的内核中led驱动
(1)九鼎实际未使用内核推荐的led驱动框架
(2)放在了: drivers/char/led/x210-led.c
3、案例分析驱动框架的使用
(1)以leds-s3c24xx.c为例。leds-s3c24xx.c中通过调用led_classdev_register来完成我们的LED驱动的注册,而led_classdev_register是在drivers/leds/led-class.c中定义的。所以其实SoC厂商的驱动工程师是调用内核开发者在驱动框架中提供的接口来实现自己的驱动的。
(2)驱动框架的关键点就是:分清楚内核开发者提供了什么,驱动开发者自己要提供什么
4、典型的驱动开发行业现状
(1)内核开发者对驱动框架进行开发和维护、升级,对应led-class.c和led-core.c
(2)SoC厂商的驱动工程师对设备驱动源码进行编写、调试,提供参考版本,对应leds-s3c24xx.c
(3)做产品的厂商的驱动工程师以SoC厂商提供的驱动源码为基础,来做移植和调试
三、初步分析led驱动框架源码
1、涉及到的文件
(1)led-core.c
/*
* LED Class Core
*
* Copyright 2005-2006 Openedhand Ltd.
*
* Author: Richard Purdie <rpurdie@openedhand.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
*/
#include <linux/kernel.h>
#include <linux/list.h>
#include <linux/module.h>
#include <linux/rwsem.h>
#include <linux/leds.h>
#include "leds.h"
DECLARE_RWSEM(leds_list_lock);//与具体功能无关,该宏声明一
//个读写信号量name并对其进行初始化
EXPORT_SYMBOL_GPL(leds_list_lock);//标签内定义的函数或者符号对
//全部内核代码公开,可以将一个函数以符号的方式导
//出给其他模块使用。
LIST_HEAD(leds_list);
EXPORT_SYMBOL_GPL(leds_list);
1)Linux的EXPORT_SYMBOL和EXPORT_SYMBOL_GPL的使用和区别:
简要说明使用方法:
一个模块mod1中定义一个函数func1;在另外一个模块mod2中定义一个函数func2,func2调用func1。
在模块mod1中,EXPORT_SYMBOL(func1);
在模块mod2中,extern int func1();
就可以在mod2中调用func1了。
同理EXPORT_SYMBOL_GPL使用相同。
2)EXPORT_SYMBOL的作用是什么?
EXPORT_SYMBOL标签内定义的函数或者符号对全部内核代码公开,不用修改内核代码就可以
在您的内核模块中直接调用,即使用EXPORT_SYMBOL可以将一个函数以符号的方式导出给其他模
块使用。
这里要和System.map做一下对比:System.map 中的是连接时的函数地址。连接完成以后,
在2.6内核运行过程中,是不知道哪个符号在哪个地址的。
EXPORT_SYMBOL的符号,是把这些符号和对应的地址保存起来,在内核运行的过程中,可以
找到这些符号对应的地址。而模块在加载过程中,其本质就是能动态连接到内核,
如果在模块中引用了内核或其它模块的符号,就要EXPORT_SYMBOL这些符号,这样才能找到
对应的地址连接。
使用方法:
第一、在模块函数定义之后使用EXPORT_SYMBOL(函数名);
第二、在调用该函数的模块中使用extern对之声明;
第三、首先加载定义该函数的模块,再加载调用该函数的模块;
区别:
EXPORT_SYMBOL(name);
EXPORT_SYMBOL_GPL(name);
这两个宏均用于将给定的符号导出到模块外, _GPL版本的宏定义只能使符号对GPL许可
的模块可用。
符号必须在模块文件的全局部分导出,不能在函数中导出,这是因为上述这两个宏将被
扩展成一个特殊用途的部分,而该变量必须是全局的。这个变量存储于模块的一个特殊的可执行部分(一个"ELF段"),在装载时,内核通过这个段来寻找模块导出的变量(感兴趣的读者可以
看<linux/module.h>获知更详细的信息)。
3)DECLARE_RWSEM(name)
该宏声明一个读写信号量name并对其进行初始化
4)LIST_HEAD(leds_list);
下面就是kernel/inclue/linux/types.h 中的list_head结构定义:
struct list_head {
struct list_head *next, *prev;
};
内核提供了下面的这些接口来初始化链表:
#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)
static inline void INIT_LIST_HEAD(struct list_head *list)
{
WRITE_ONCE(list->next, list);
list->prev = list;
}
如: 可以通过 LIST_HEAD(mylist) 进行初始化一个链表,mylist的prev 和 next 指针都是指向自己。
struct list_head mylist = {&mylist, &mylist} ;
(2)led-class.c,该文件的作用是为led这个设备提供了一个类
应该从下往上看:
/*
* LED Class Core
*
* Copyright (C) 2005 John Lenz <lenz@cs.wisc.edu>
* Copyright (C) 2005-2007 Richard Purdie <rpurdie@openedhand.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/list.h>
#include <linux/spinlock.h>
#include <linux/device.h>
#include <linux/sysdev.h>
#include <linux/timer.h>
#include <linux/err.h>
#include <linux/ctype.h>
#include <linux/leds.h>
#include "leds.h"
static struct class *leds_class;
static void led_update_brightness(struct led_classdev *led_cdev)
{
if (led_cdev->brightness_get)
led_cdev->brightness = led_cdev->brightness_get(led_cdev);
}
static ssize_t led_brightness_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct led_classdev *led_cdev = dev_get_drvdata(dev);
/* no lock needed for this */
led_update_brightness(led_cdev);
return sprintf(buf, "%u\n", led_cdev->brightness);
}
static ssize_t led_brightness_store(struct device *dev,
struct device_attribute *attr, const char *buf, size_t size)
{
struct led_classdev *led_cdev = dev_get_drvdata(dev);
ssize_t ret = -EINVAL;
char *after;
unsigned long state = simple_strtoul(buf, &after, 10);
size_t count = after - buf;
if (isspace(*after))
count++;
if (count == size) {
ret = count;
if (state == LED_OFF)
led_trigger_remove(led_cdev);
led_set_brightness(led_cdev, state);
}
return ret;
}
static ssize_t led_max_brightness_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct led_classdev *led_cdev = dev_get_drvdata(dev);
return sprintf(buf, "%u\n", led_cdev->max_brightness);
}
static struct device_attribute led_class_attrs[] = {
__ATTR(brightness, 0644, led_brightness_show, led_brightness_store),
__ATTR(max_brightness, 0444, led_max_brightness_show, NULL),
#ifdef CONFIG_LEDS_TRIGGERS
__ATTR(trigger, 0644, led_trigger_show, led_trigger_store),
#endif
__ATTR_NULL,
};
/**
* led_classdev_suspend - suspend an led_classdev.
* @led_cdev: the led_classdev to suspend.
*/
void led_classdev_suspend(struct led_classdev *led_cdev)
{
led_cdev->flags |= LED_SUSPENDED;
led_cdev->brightness_set(led_cdev, 0);
}
EXPORT_SYMBOL_GPL(led_classdev_suspend);
/**
* led_classdev_resume - resume an led_classdev.
* @led_cdev: the led_classdev to resume.
*/
void led_classdev_resume(struct led_classdev *led_cdev)
{
led_cdev->brightness_set(led_cdev, led_cdev->brightness);
led_cdev->flags &= ~LED_SUSPENDED;
}
EXPORT_SYMBOL_GPL(led_classdev_resume);
static int led_suspend(struct device *dev, pm_message_t state)
{
struct led_classdev *led_cdev = dev_get_drvdata(dev);
if (led_cdev->flags & LED_CORE_SUSPENDRESUME)
led_classdev_suspend(led_cdev);
return 0;
}
static int led_resume(struct device *dev)
{
struct led_classdev *led_cdev = dev_get_drvdata(dev);
if (led_cdev->flags & LED_CORE_SUSPENDRESUME)
led_classdev_resume(led_cdev);
return 0;
}
/**
* led_classdev_register - register a new object of led_classdev class.
* @parent: The device to register.
* @led_cdev: the led_classdev structure for this device.
*/
int led_classdev_register(struct device *parent, struct led_classdev *led_cdev)
{
led_cdev->dev = device_create(leds_class, parent, 0, led_cdev,
"%s", led_cdev->name);//创建led_classdev类的对象
if (IS_ERR(led_cdev->dev))
return PTR_ERR(led_cdev->dev);
#ifdef CONFIG_LEDS_TRIGGERS
init_rwsem(&led_cdev->trigger_lock);
#endif
/* add to the list of leds */
down_write(&leds_list_lock);
list_add_tail(&led_cdev->node, &leds_list);
up_write(&leds_list_lock);
if (!led_cdev->max_brightness)//设置亮度的默认值
led_cdev->max_brightness = LED_FULL;//255
led_update_brightness(led_cdev);//设置好灯的亮度后,更新灯的亮度
#ifdef CONFIG_LEDS_TRIGGERS
led_trigger_set_default(led_cdev);
#endif
printk(KERN_DEBUG "Registered led device: %s\n",
led_cdev->name);//打印信息表明注册成功
return 0;
}
EXPORT_SYMBOL_GPL(led_classdev_register);
/**
* led_classdev_unregister - unregisters a object of led_properties class.
* @led_cdev: the led device to unregister
*
* Unregisters a previously registered via led_classdev_register object.
*/
void led_classdev_unregister(struct led_classdev *led_cdev)
{
#ifdef CONFIG_LEDS_TRIGGERS
down_write(&led_cdev->trigger_lock);
if (led_cdev->trigger)
led_trigger_set(led_cdev, NULL);
up_write(&led_cdev->trigger_lock);
#endif
device_unregister(led_cdev->dev);
down_write(&leds_list_lock);
list_del(&led_cdev->node);
up_write(&leds_list_lock);
}
EXPORT_SYMBOL_GPL(led_classdev_unregister);
static int __init leds_init(void)
{
leds_class = class_create(THIS_MODULE, "leds");
if (IS_ERR(leds_class))
return PTR_ERR(leds_class);
leds_class->suspend = led_suspend; // 关联 LED设备休眠函数
leds_class->resume = led_resume; // 关联 LED设备唤醒函数
leds_class->dev_attrs = led_class_attrs;//创建设备属性文件 brightness、max_brightness、trigger
return 0;
}
static void __exit leds_exit(void)
{
class_destroy(leds_class);
}
subsys_initcall(leds_init);
module_exit(leds_exit);
MODULE_AUTHOR("John Lenz, Richard Purdie");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("LED Class Interface");
2、subsys_initcall
(1)经过基本分析,发现LED驱动框架中内核开发者实现的部分主要是led-class.c
(2)我们发现led-class.c就是一个内核模块,对led-class.c分析应该从下往上,遵从对模块的基本分析方法。这个模块是操作系统启动时就要执行的,故而我们不能将其编译成模块,在内核配置时只能“Y”或"N".
(3)为什么LED驱动框架中内核开发者实现的部分要实现成一个模块?
因为内核开发者希望这个驱动框架是可以被装载/卸载的。这样当我们内核使用者不需要这个驱动框架时可以完全去掉,需要时可以随时加上。
(4)subsys_initcall是一个宏,定义在linux/init.h中。经过对这个宏进行展开,发现这个宏的功能是:将其声明的函数放到一个特定的段:.initcall4.init
kernel\include\linux\init.h,该文件中有两个相同的宏定义,根据注释选择,一个
:Don’t use these in modules
#define subsys_initcall(fn) __define_initcall("4",fn,4)
#define __define_initcall(level,fn,id) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(".initcall" level ".init"))) = fn
subsys_initcall
__define_initcall("4",fn,4)
(5)分析module_init宏,可以看出它将函数放到了.initcall6.init段中。
kernel\include\linux\init.h
#define module_init(x) __initcall(x);
#define __initcall(fn) device_initcall(fn)
#define device_initcall(fn) __define_initcall("6",fn,6)
module_init
__initcall
device_initcall
__define_initcall("6",fn,6)
(6)内核在启动过程中需要顺序的做很多事,内核如何实现按照先后顺序去做很多初始化操作。内核的解决方案就是给内核启动时要调用的所有函数归类,然后每个类按照一定的次序去调用执行。这些分类名就叫.initcalln.init。n的值从1到8。内核开发者在编写内核代码时只要将函数设置合适的级别,这些函数就会被链接的时候放入特定的段,内核启动时再按照段顺序去依次执行各个段即可。
可去查看分析kernel/arch/arm/kernel/vmlinux.lds.S(内核源码的链接脚本,只不过这个是通过汇编文件实现的).先编译内核再去查看,否则无法看到下面的内容:
#摘录部分代码
SECTIONS
{
. = 0xC0000000 + 0x00008000;
.init : { /* Init code and data */
_stext = .;
_sinittext = .;
*(.head.text)
*(.init.text) *(.cpuinit.text) *(.meminit.text)
_einittext = .;
__proc_info_begin = .;
*(.proc.info.init)
__proc_info_end = .;
__arch_info_begin = .;
*(.arch.info.init)
__arch_info_end = .;
__tagtable_begin = .;
*(.taglist.init)
__tagtable_end = .;
. = ALIGN(16); __setup_start = .; *(.init.setup) __setup_end = .;
__initcall_start = .; *(.initcallearly.init) __early_initcall_end = .; *(.initcall0.init) *(.initcall0s.init) *(.initcall1.init) *(.initcall1s.init) *(.initcall2.init) *(.initcall2s.init) *(.initcall3.init) *(.initcall3s.init) *(.initcall4.init) *(.initcall4s.init) *(.initcall5.init) *(.initcall5s.init) *(.initcallrootfs.init) *(.initcall6.init) *(.initcall6s.init) *(.initcall7.init) *(.initcall7s.init) __initcall_end = .;
__con_initcall_start = .; *(.con_initcall.init) __con_initcall_end = .;
__security_initcall_start = .; *(.security_initcall.init) __security_initcall_end = .;
__init_begin = _stext;
*(.init.data) *(.cpuinit.data) *(.meminit.data) . = ALIGN(8); __ctors_start = .; *(.ctors) __ctors_end = .; *(.init.rodata) *(.cpuinit.rodata) *(.meminit.rodata)
}
. = ALIGN((1 << 12)); .data..percpu : AT(ADDR(.data..percpu) - 0) { __per_cpu_load = .; __per_cpu_start = .; *(.data..percpu..first) *(.data..percpu..page_aligned) *(.data..percpu) *(.data..percpu..shared_aligned) __per_cpu_end = .; }
. = ALIGN((1 << 12));
__init_end = .;
(7)经过分析,可以看出,subsys_initcall和module_init的作用是一样的,只不过前者所声明的函数要比后者在内核启动时的执行顺序更早。
static int __init leds_init(void)
{
leds_class = class_create(THIS_MODULE, "leds");
if (IS_ERR(leds_class))
return PTR_ERR(leds_class);
leds_class->suspend = led_suspend;
leds_class->resume = led_resume;
leds_class->dev_attrs = led_class_attrs;
return 0;
}
函数class_create()创建一个新的类在"/sys/class"目录下(即该目录下的一个文件),类名叫”leds”,并返回这个新生成的类。然后在对该类的电源管理函数、类的属性做初始化。关于类的初始化,led 的定义是:
static struct device_attribute led_class_attrs[] = {
__ATTR(brightness, 0644, led_brightness_show, led_brightness_store),
__ATTR(max_brightness, 0444, led_max_brightness_show, NULL),
#ifdef CONFIG_LEDS_TRIGGERS
__ATTR(trigger, 0644, led_trigger_show, led_trigger_store),
#endif
__ATTR_NULL,
};
正如注释所陈述的那样,结构体 “device_attribute” 是用来导出设备属性的
#define __ATTR(_name,_mode,_show,_store) { \
.attr = {.name = __stringify(_name), .mode = _mode }, \
.show = _show, \
.store = _store, \
}
struct device_attribute {
struct attribute attr;//用户操作权限
ssize_t (*show)(struct device *dev, struct device_attribute *attr,
char *buf);//读方法
ssize_t (*store)(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count);//写方法
};
对于一切设备皆文件的linux来说,设备的属性无非就是读写,所以,导出设备属性的结构体有:
读函数 [ssize_t (*show)(struct device *dev, struct_device_attribute *attr, chat *buf)]
写函数 [ssize_t (*store)(struct device *dev, struct_device_attribute *attr, chat *buf, size_t count)]
其中 dev,对应要被操作的dev, attr 对应要读写的属性,buf对应需要设备做什么,具体
的功能还需要设备驱动开发者实现。比如,输入1 或者on 对应led的亮,这里,1/on 就是
buf 的内容,dev 是led, 亮是 attr。
结构体struct attribute是用户的操作权限,由于linux是多文件系统(尽管嵌入式很多设
备都是单用户——root),所有这个属性规定着不同用户、用户组对设备的访问权限。
3、led_class_attrs
(1)什么是attribute,对应将来/sys/class/leds/目录里的内容,一般是文件和文件夹。这些文件其实就是sysfs开放给应用层的一些操作接口(非常类似于/dev/目录下的那些设备文件)
(2)attribute有什么用,作用就是让应用程序可以通过/sys/class/leds/目录下面的属性文件来操作驱动进而操作硬件设备。
(3)attribute其实是另一条驱动实现的路线。有区别于之前讲的file_operations那条线。
4、led_classdev_register
led_classdev_register//创建属于这个类的设备
device_create
(1)分析可知,led_classdev_register这个函数其实就是去创建一个属于leds这个类的一个设备。其实就是去注册一个设备。所以这个函数其实就是led驱动框架中内核开发者提供给SoC厂家驱动开发者的一个注册驱动的接口。
(2)当我们使用led驱动框架去编写驱动的时候,这个led_classdev_register函数的作用类似于我们之前使用file_operations方式去注册字符设备驱动时的register_chrdev函数。
struct class {
const char *name; // 类名称
struct module *owner; // 类所属的模块,比如 usb模块、led模块等
struct class_attribute *class_attrs; // 类所添加的属性
//const struct attribute_group 是4.3版本内核中的,struct device_attribute是2.6.35.7版本的,二者由于版本不同使用不同
// const struct attribute_group **dev_groups; // 类所包含的设备所添加的属性
struct device_attribute *dev_attrs; 类所包含的设备所添加的属性
struct kobject *dev_kobj; // 用于标识 类所包含的设备属于块设备还是字符设备
int (*dev_uevent)(struct device *dev, struct kobj_uevent_env *env); // 用于在设备发出 uevent 消息时添加环境变量
char *(*devnode)(struct device *dev, umode_t *mode); // 设备节点的相对路径名
void (*class_release)(struct class *class); // 类被释放时调用的函数
void (*dev_release)(struct device *dev); // 设备被释放时调用的函数
int (*suspend)(struct device *dev, pm_message_t state); // 设备休眠时调用的函数
int (*resume)(struct device *dev); // 设备被唤醒时调用的函数
const struct kobj_ns_type_operations *ns_type;
const void *(*namespace)(struct device *dev);
const struct dev_pm_ops *pm; // 用于电源管理的函数
struct subsys_private *p; // 指向 class_private 结构的指针
};
static int __init leds_init(void)
{
leds_class = class_create(THIS_MODULE, "leds"); // 创建 leds_class 类
if (IS_ERR(leds_class))
return PTR_ERR(leds_class);
leds_class->suspend = led_suspend; // 关联 LED设备休眠函数
leds_class->resume = led_resume; // 关联 LED设备唤醒函数
leds_class->dev_attrs = led_class_attrs; // 创建设备属性文件 brightness、max_brightness、trigger
return 0;
}
四、在内核中添加或去除某个驱动
1、去除九鼎移植的LED驱动
(1)九鼎移植的驱动在应用层的接口在/sys/devices/platform/x210-led/目录下,有led1、led2、led3、led4四个设备文件,各自管理一个led。使用如下文件写入设备文件控制开发板的灯亮与灭:
echo 1 > led1 #亮
echo 0 > led1 #灭
(2)要去掉九鼎自己移植的led驱动,要在make menucofig中去掉选择项,然后重新make得到zImage,然后重启时启动这个新的zImage即可。
(3)新的内核启动后,如果/sys/devices/platform/目录下已经没有了x210-led这个目录,就说明我们去掉这个驱动成功了。
(4)make menuconfig如何去除:
Device Drivers --->
Character devices --->
x210 led driver
使用’N’取消这个模块,使其不被编译,具体实现原理可翻阅《初窥Linux内核》专栏的《Linux内核的配置和编译原理》。
2、添加led驱动框架支持
(1)当前内核中是没有LED驱动框架的,我们要去添加它。
make menuconfig
Device Drivers --->
LED Support --->
3、完成leds-x210.c
添加相应的led-s5pv210.c,修改对应的makefile,kconfig文件
五、基于驱动框架写led驱动
1、分析
(1)参考哪里? drivers/leds/leds-s3c24xx.c
(2)关键点:led_classdev_register
2、动手写led驱动模块
3、代码实践
(1)调试
(2)分析
#include <linux/module.h> // module_init module_exit
#include <linux/init.h> // __init __exit
#include <linux/fs.h>
#include <linux/leds.h>
#include <mach/regs-gpio.h>
#include <mach/gpio-bank.h>
#include <linux/io.h>
#include <linux/ioport.h>
//使用静态映射来操作io,使用led
#define GPJ0CON S5PV210_GPJ0CON
#define GPJ0DAT S5PV210_GPJ0DAT
struct led_classdev led1_cdev;//定义结构体变量,用于描述led设备类的一个设备
struct led_classdev led2_cdev;//定义结构体变量,用于描述led设备类的一个设备
struct led_classdev led3_cdev;//定义结构体变量,用于描述led设备类的一个设备
static void s5pv210_led1_set(struct led_classdev *led_cdev,
enum led_brightness value)
{
printk(KERN_INFO "s5pv210_led1_set\n");
writel(0x11111111, GPJ0CON);
//在这里根据用户设定的值(value)操作硬件
if (value == LED_OFF)
{
//读改写进行操作,既控制了led1,又不影响其他的led
writel((readl(GPJ0DAT) | (1 << 3)), GPJ0DAT);//led1灭
}
else
{
writel((readl(GPJ0DAT) & ~(1 << 3)), GPJ0DAT);//led1灭
}
}
static void s5pv210_led2_set(struct led_classdev *led_cdev,
enum led_brightness value)
{
printk(KERN_INFO "s5pv210_led2_set\n");
writel(0x11111111, GPJ0CON);
//在这里根据用户设定的值(value)操作硬件
if (value == LED_OFF)
{
//读改写进行操作,既控制了led1,又不影响其他的led
writel((readl(GPJ0DAT) | (1 << 4)), GPJ0DAT);//led1灭
}
else
{
writel((readl(GPJ0DAT) & ~(1 << 4)), GPJ0DAT);//led1灭
}
}
static void s5pv210_led3_set(struct led_classdev *led_cdev,
enum led_brightness value)
{
printk(KERN_INFO "s5pv210_led3_set\n");
printk(KERN_INFO "s5pv210_led3_set\n");
writel(0x11111111, GPJ0CON);
//在这里根据用户设定的值(value)操作硬件
if (value == LED_OFF)
{
//读改写进行操作,既控制了led1,又不影响其他的led
writel((readl(GPJ0DAT) | (1 << 5)), GPJ0DAT);//led1灭
}
else
{
writel((readl(GPJ0DAT) & ~(1 << 5)), GPJ0DAT);//led1灭
}
}
//用户insmod安装驱动时会调用该函数,该函数的主要任务是去使用
//led驱动框架提供的设备注册函数来注册一个设备
static int __init s5pv210_led_init(void)
{
int ret = -1;
//填充描述led类设备的属性的结构体变量
led1_cdev.name = "led1";
led1_cdev.brightness = 255;
led1_cdev.brightness_set = s5pv210_led1_set;
ret = led_classdev_register(NULL, &led1_cdev);//来源于led-class.c,创建属于led类的
//一个设备
if (ret < 0) {
printk(KERN_INFO "led_classdev_register failed\n");
return ret;
}
//填充描述led类设备的属性的结构体变量
led2_cdev.name = "led2";
led2_cdev.brightness = 255;
led2_cdev.brightness_set = s5pv210_led2_set;
ret = led_classdev_register(NULL, &led2_cdev);//来源于led-class.c,创建属于led类的
//一个设备
if (ret < 0) {
printk(KERN_INFO "led_classdev_register failed\n");
return ret;
}
//填充描述led类设备的属性的结构体变量
led3_cdev.name = "led3";
led3_cdev.brightness = 255;
led3_cdev.brightness_set = s5pv210_led3_set;
ret = led_classdev_register(NULL, &led3_cdev);//来源于led-class.c,创建属于led类的
//一个设备
if (ret < 0) {
printk(KERN_INFO "led_classdev_register failed\n");
return ret;
}
return 0;
}
static void __exit s5pv210_led_exit(void)
{
led_classdev_unregister(&led1_cdev);//来源于led-class.c,注销属于led类的
//一个设备
led_classdev_unregister(&led2_cdev);
led_classdev_unregister(&led3_cdev);
}
module_init(s5pv210_led_init); //函数放到了.initcall6.init段
module_exit(s5pv210_led_exit); //函数放到了指定的段,这些段与执行时的顺序相关
/********************MODULE_xxx这种宏作用是用来添加模块描述信息*********************/
MODULE_AUTHOR("Ben Dooks <ben@simtec.co.uk>"); //描述模块的作者
MODULE_DESCRIPTION("s5pv210 LED driver"); //描述模块的介绍信息
MODULE_LICENSE("GPL"); //描述模块的许可证
MODULE_ALIAS("s5pv210_led"); //描述模块的别名信息
通过分析看出:
第1:我们写的驱动确实工作了,被加载了,/sys/class/leds/目录下确实多出来了一个表示设备的文件夹。文件夹里面有相应的操控led硬件的2个属性brightness和max_brightness
第2:led-class.c中brightness方法有一个show方法和store方法,这两个方法对应用户在/sys/class/leds/myled/brightness目录下直接去读写这个文件时实际执行的代码。
当我们show brightness时,实际就会执行led_brightness_show函数
当我们echo 1 > brightness时,实际就会执行led_brightness_store函数
(3)show方法实际要做的就是读取LED硬件信息,然后把硬件信息返回给我们即可。所以show方法和store方法必须要会去操控硬件。但是led-class.c文件又属于驱动框架中的文件,它本身无法直接读取具体硬件,因此在show和store方法中使用函数指针的方式调用了struct led_classdev结构体中的相应的读取/写入硬件信息的方法。
(4)struct led_classdev结构体中的实际用来读写硬件信息的函数,就是我们自己写的驱动文件leds-s5pv210.c中要提供的。
4、在驱动中将4个LED分开
(1)好处。驱动层实现对各个LED设备的独立访问,并向应用层展示出4个操作接口led1、led2、led3、led4,这样应用层可以完全按照自己的需要对LED进行控制。
驱动的设计理念:不要对最终需求功能进行假定,而应该只是直接的对硬件的操作。有一个概念就是:机制和策略的问题。在硬件操作上驱动只应该提供机制而不是策略。策略由应用程序来做。
eg:
机制:灯亮
策略:灯亮的快慢
(2)如何实现
在驱动文件led-s5pv210.c中注册多个led设备,每个led设备的注册流程及使用的函数相同。
5、gpiolib引入
(1)一个事实:GPIO会复用
(2)如果同一个GPIO被2个驱动同时控制了,就会出现bug
(3)所以内核提供gpiolib来统一管理系统中所有GPIO
(4)gpiolib本身属于驱动框架的一部分,不用这个也可以实现功能,但最好遵守gpiolib引入规范
六、linux内核的gpiolib学习
1、gpiolib学习重点
(1)gpiolib的建立过程
(2)gpiolib的使用方法:申请、使用、释放
(3)gpiolib的架构:涉及哪些目录的哪些文件
2、gpiolib的学习方法
(1)以一条主线进去,坚持主线
(2)中途遇到杂碎知识,彻底搞定之,然后继续主线
(3)随时做笔记以加深理解和记忆
(4)学习途中注意架构思想,提升自己大脑的空间复杂度
3、主线1:gpiolib的建立
(1)找到目标函数
kernel\arch\arm\mach-s5pv210\mach-smdkc110.c
smdkc110_map_io
s5pv210_gpiolib_init 这个函数就是我们gpiolib初始化的函数
static void __init smdkc110_map_io(void)
{
s5p_init_io(NULL, 0, S5P_VA_CHIPID);
s3c24xx_init_clocks(24000000);
s5pv210_gpiolib_init();
s3c24xx_init_uarts(smdkc110_uartcfgs, ARRAY_SIZE(smdkc110_uartcfgs));
s5p_reserve_bootmem(smdkc110_media_devs, ARRAY_SIZE(smdkc110_media_devs));
#ifdef CONFIG_MTD_ONENAND
s5pc110_device_onenand.name = "s5pc110-onenand";
#endif
#ifdef CONFIG_MTD_NAND
s3c_device_nand.name = "s5pv210-nand";
#endif
s5p_device_rtc.name = "smdkc110-rtc";
}
__init int s5pv210_gpiolib_init(void)
{
struct s3c_gpio_chip *chip = s5pv210_gpio_4bit;
int nr_chips = ARRAY_SIZE(s5pv210_gpio_4bit);//得到当前系统中的端口数量
int i = 0;
for (i = 0; i < nr_chips; i++, chip++) {
if (chip->config == NULL)
chip->config = &gpio_cfg;
if (chip->base == NULL)
chip->base = S5PV210_BANK_BASE(i);
}
samsung_gpiolib_add_4bit_chips(s5pv210_gpio_4bit, nr_chips);//向内核注册gpiolib
return 0;
}
kernel\arch\arm\mach-s5pv210\gpiolib.c
4、struct s3c_gpio_chip
kernel\arch\arm\mach-s5pv210\gpiolib.c
struct s3c_gpio_chip {
struct gpio_chip chip;//属性及一些设置信息
struct s3c_gpio_cfg *config;//配置相关的信息
struct s3c_gpio_pm *pm;//电源管理相关的
void __iomem *base;//GPIO的寄存器对应的虚拟地址的基地址
int eint_offset;
spinlock_t lock;
#ifdef CONFIG_PM
u32 pm_save[7];
#endif
};
struct gpio_chip {
const char *label;//记录GPIO的名字,/sys/class/gpio名/下的东西
struct device *dev;
struct module *owner;
int (*request)(struct gpio_chip *chip,//申请
unsigned offset);
void (*free)(struct gpio_chip *chip,//释放
unsigned offset);
int (*direction_input)(struct gpio_chip *chip,//输入模式
unsigned offset);
int (*get)(struct gpio_chip *chip,
unsigned offset);
int (*direction_output)(struct gpio_chip *chip,//输出模式
unsigned offset, int value);
int (*set_debounce)(struct gpio_chip *chip,
unsigned offset, unsigned debounce);
void (*set)(struct gpio_chip *chip,
unsigned offset, int value);
int (*to_irq)(struct gpio_chip *chip,//得到GPIO对应的中断的中断号
unsigned offset);
void (*dbg_show)(struct seq_file *s,
struct gpio_chip *chip);
int base;//当前这个io所在io端口的编号
u16 ngpio;
const char *const *names;
unsigned can_sleep:1;
unsigned exported:1;
};
struct s3c_gpio_cfg {
unsigned int cfg_eint;
s3c_gpio_pull_t (*get_pull)(struct s3c_gpio_chip *chip, unsigned offs);
//得到上拉或者下拉的状态
int (*set_pull)(struct s3c_gpio_chip *chip, unsigned offs,
s3c_gpio_pull_t pull);//设置上拉或者下拉
int (*set_pin)(struct s3c_gpio_chip *chip, unsigned offs,
s3c_gpio_pull_t level);//设置
unsigned (*get_config)(struct s3c_gpio_chip *chip, unsigned offs);
//获取设置内容
int (*set_config)(struct s3c_gpio_chip *chip, unsigned offs,
unsigned config);//设置
};
(1)这个结构体是一个GPIO端口的抽象,这个结构体的一个变量就可以完全的描述一个IO端口。
(2)端口和IO口是两个概念。S5PV210有很多个IO口(160个左右),这些IO口首先被分成N个端口(port group),然后每个端口中又包含了M个IO口。譬如GPA0是一个端口,里面包含了8个IO口,我们一般记作:GPA0_0(或GPA0.0)、GPA0_1、
(3)内核中为每个GPIO分配了一个编号,编号是一个数字(譬如一共有160个IO时编号就可以从1到160连续分布),编号可以让程序很方便的去识别每一个GPIO。
5、s5pv210_gpio_4bit
kernel/arch/arm/mach-s5pv210/gpiolib.c 64行
这个东西是一个结构体数组,数组中包含了很多个struct s3c_gpio_chip类型的变量。
kernel/arch/arm/mach-s5pv210/gpiolib.c
struct s3c_gpio_chip *chip = s5pv210_gpio_4bit;
int nr_chips = ARRAY_SIZE(s5pv210_gpio_4bit);//得到当前系统中的端口数量
samsung_gpiolib_add_4bit_chips(s5pv210_gpio_4bit, nr_chips);//向内核注册gpiolib
6、S5PV210_GPA0宏
s5pv210_gpio_4bit结构体的元素
.chip = {
.base = S5PV210_GPA0(0),//当前端口的基础编号
.ngpio = S5PV210_GPIO_A0_NR,//当前端口拥有的IO口数量
.label = "GPA0",//当前端口的名字
.to_irq = s5p_gpiolib_gpioint_to_irq,//当前端口中IO口编号换算成对应中断号的方法
}//可能要进行动态映射
#define S5PV210_GPA0(_nr) (S5PV210_GPIO_A0_START + (_nr))
enum s5p_gpio_number枚举元素S5PV210_GPIO_A0_START = 0,
#define S5PV210_GPA1(_nr) (S5PV210_GPIO_A1_START + (_nr))
enum s5p_gpio_number枚举元素S5PV210_GPIO_A1_START = S5PV210_GPIO_NEXT(S5PV210_GPIO_A0),
#define S5PV210_GPIO_NEXT(__gpio) \
((__gpio##_START) + (__gpio##_NR) + CONFIG_S3C_GPIO_SPACE + 1)
"##"的作用是将其左右两边的字符(比如__gpio与_START)连起来。
CONFIG_S3C_GPIO_SPACE该宏在源码搜不到,应该是内核编译生成的文件中才有,所以可在ubuntu中查看编译过的内核源码得到该宏的值
(1)S5PV210_GPA1(_nr)这个宏的返回值就是GPA0端口的某一个IO口的编号值,传参就是我们这个IO口在GPA0端口中的局部编号。
(2)samsung_gpiolib_add_4bit_chips这个函数才是具体进行gpiolib的注册的。这个函数接收的参数是我们当前文件中定义好的结构体数组s5pv210_gpio_4bit(其实2个参数分别是数组名和数组元素个数),这个数组中其实就包含了当前系统中所有的IO端口的信息(这些信息包含:端口的名字、端口中所有GPIO的编号、端口操作寄存器组的虚拟地址基地址、端口中IO口的数量、端口上下拉等模式的配置函数、端口中的IO口换算其对应的中断号的函数)。
{
.base = (S5P_VA_GPIO + 0xC00),//寄存器组的虚拟地址的基地址
.config = &gpio_cfg_noint,
.eint_offset = IRQ_EINT(0),
.chip = {
.base = S5PV210_GPH0(0),
.ngpio = S5PV210_GPIO_H0_NR,
.label = "GPH0",
.to_irq = s5p_gpiolib_eint_to_irq,
},
查看这个数组可发现有的是进行了静态映射,有的没有,可能是要进行动态映射。由这个数组的大小可知道当前系统中的端口数量。
__init int s5pv210_gpiolib_init(void)
{
struct s3c_gpio_chip *chip = s5pv210_gpio_4bit;
int nr_chips = ARRAY_SIZE(s5pv210_gpio_4bit);//得到当前系统中的端口数量
int i = 0;
for (i = 0; i < nr_chips; i++, chip++) {
if (chip->config == NULL)
chip->config = &gpio_cfg;
if (chip->base == NULL)
chip->base = S5PV210_BANK_BASE(i);
}
samsung_gpiolib_add_4bit_chips(s5pv210_gpio_4bit, nr_chips);//向内核注册gpiolib
return 0;
}
#define S5PV210_BANK_BASE(bank_nr) (S5P_VA_GPIO + ((bank_nr) * 0x20))
每两个端口的差值就是0x20
7、kernel\arch\arm\plat-samsung\gpiolib.c 183-199行
void __init samsung_gpiolib_add_4bit_chips(struct s3c_gpio_chip *chip,
int nr_chips)
{
for (; nr_chips > 0; nr_chips--, chip++) {
samsung_gpiolib_add_4bit(chip);
s3c_gpiolib_add(chip);
}
}
void __init samsung_gpiolib_add_4bit2_chips(struct s3c_gpio_chip *chip,
int nr_chips)
{
for (; nr_chips > 0; nr_chips--, chip++) {
samsung_gpiolib_add_4bit2(chip);
s3c_gpiolib_add(chip);
}
}
8、几个问题
samsung_gpiolib_add_4bit_chips(s5pv210_gpio_4bit, nr_chips);//向内核注册gpiolib
(1)哪个目录的哪个文件
(2)函数名中为什么有个4bit:三星的CPU中2440的CON寄存器是2bit对应一个IO口,而6410和210以及之后的系列中CON寄存器是4bit对应1个IO口。所以gpiolib在操作2440和210的CON寄存器时是不同的。
9、函数调用关系
samsung_gpiolib_add_4bit_chips
samsung_gpiolib_add_4bit(chip);
s3c_gpiolib_add(chip);
经过分析,发现samsung_gpiolib_add_4bit内部其实并没有做gpiolib的注册工作,而是还在做填充,填充的是每一个GPIO被设置成输入模式/输出模式的操作方法。
void __init samsung_gpiolib_add_4bit(struct s3c_gpio_chip *chip)
{
chip->chip.direction_input = samsung_gpiolib_4bit_input;
chip->chip.direction_output = samsung_gpiolib_4bit_output;
chip->pm = __gpio_pm(&s3c_gpio_pm_4bit);//电源管理,休眠模式相关的
}
```c
static int samsung_gpiolib_4bit_input(struct gpio_chip *chip,
unsigned int offset)
{
struct s3c_gpio_chip *ourchip = to_s3c_gpio(chip);
void __iomem *base = ourchip->base;
unsigned long con;
con = __raw_readl(base + GPIOCON_OFF);//读
con &= ~(0xf << con_4bit_shift(offset));//改
__raw_writel(con, base + GPIOCON_OFF);//写,全为0代表输入模式
gpio_dbg("%s: %p: CON now %08lx\n", __func__, base, con);
return 0;
}
static int samsung_gpiolib_4bit_output(struct gpio_chip *chip,
unsigned int offset, int value)
{
struct s3c_gpio_chip *ourchip = to_s3c_gpio(chip);
void __iomem *base = ourchip->base;
unsigned long con;
unsigned long dat;
con = __raw_readl(base + GPIOCON_OFF);//读
con &= ~(0xf << con_4bit_shift(offset));//改
con |= 0x1 << con_4bit_shift(offset);//写,全为1代表输出
dat = __raw_readl(base + GPIODAT_OFF);
if (value)
dat |= 1 << offset;
else
dat &= ~(1 << offset);
__raw_writel(dat, base + GPIODAT_OFF);
__raw_writel(con, base + GPIOCON_OFF);
__raw_writel(dat, base + GPIODAT_OFF);
gpio_dbg("%s: %p: CON %08lx, DAT %08lx\n", __func__, base, con, dat);
return 0;
}
10、s3c_gpiolib_add
(1)首先检测并完善chip的direction_input/direction_ouput/set/get这4个方法
(2)然后调用gpiochip_add方法进行真正的注册操作。其实这个注册就是将我们的封装了一个GPIO端口的所有信息的chip结构体变量挂接到内核gpiolib模块定义的一个gpio_desc数组中的某一个格子中。
__init void s3c_gpiolib_add(struct s3c_gpio_chip *chip)
{
struct gpio_chip *gc = &chip->chip;
int ret;
BUG_ON(!chip->base); //
断言
BUG_ON(!gc->label);
BUG_ON(!gc->ngpio);
spin_lock_init(&chip->lock);//自旋锁相关
if (!gc->direction_input)//不是空,samsung_gpiolib_add_4bit(),已经绑定初始化过
gc->direction_input = s3c_gpiolib_input;//针对每个io口为2bits
if (!gc->direction_output)
gc->direction_output = s3c_gpiolib_output;
if (!gc->set)
gc->set = s3c_gpiolib_set;
if (!gc->get)
gc->get = s3c_gpiolib_get;
#ifdef CONFIG_PM
if (chip->pm != NULL) {
if (!chip->pm->save || !chip->pm->resume)
printk(KERN_ERR "gpio: %s has missing PM functions\n",
gc->label);
} else
printk(KERN_ERR "gpio: %s has no PM function\n", gc->label);
#endif
/* gpiochip_add() prints own failure message on error. */
ret = gpiochip_add(gc);
if (ret >= 0)
s3c_gpiolib_track(chip);
}
ret = gpiochip_add(gc);
gpiochip_add函数:
/* these GPIO numbers must not be managed by another gpio_chip */
for (id = base; id < base + chip->ngpio; id++) {
if (gpio_desc[id].chip != NULL) {
status = -EBUSY;
break;
11、从驱动框架角度再来分析一下gpiolib
(1)之前的分析已经告一段落,截至目前我们已经搞清楚了gpiolib的建立工程。但是这只是整个gpiolib建立的一部分,是厂商驱动工程师负责的那一部分;还有另一部分是内核开发者提供的驱动框架的那一部分,就是我们后面要去分析的第2条主线。
(2)drivers/gpio/gpiolib.c这个文件中所有的函数(之前那个gpiolib.c和这个不一样,目录内容都不相同)构成了我们第2部分,也就是内核开发者写的gpiolib框架部分。这个文件中提供的函数主要有以下部分:
gpiochip_add: 是框架开出来的接口,给厂商驱动工程师用,用于向内核注册我们的gpiolib
gpio_request: 是框架开出来的接口,给使用gpiolib来编写自己的驱动的驱动工程师用的,
驱动中要想使用某一个gpio,就必须先调用gpio_request接口来向内核的
gpiolib部分申请,得到允许后才可以去使用这个gpio。
gpio_free: 对应gpio_request,用来释放申请后用完了的gpio
gpio_request_one/gpio_request_array: 这两个是gpio_request的变种
gpiochip_is_requested: 接口用来判断某一个gpio是否已经被申请了
gpio_direction_input/gpio_direction_output: 用来设置GPIO为输入/输出模式,
注意该函数内部实际并没有对硬件进行操作,只是通过chip结构体变量的
函数指针调用了将来SoC厂商的驱动工程师写的真正的操作硬件实现gpio
设置成输出模式的那个函数。
以上的接口属于一类,这些都是给写其他驱动并且用到了gpiolib的人使用的
剩下的还有另外一类函数,这类函数是gpiolib内部自己的一些功能实现的代码
对于这一块的源码请自行去查阅分析细节。
12、gpiolib的attribute(属性)部分
(1)CONFIG_GPIO_SYSFS(drivers/gpio/gpiolib.c194行)
该宏决定了gpio是否具有相关属性,在/sys/class目录下能否查看到相关信息
(2)GPIO的attribute演示
/*
* /sys/class/gpio/gpiochipN/
* /base ... matching gpio_chip.base (N)
* /label ... matching gpio_chip.label
* /ngpio ... matching gpio_chip.ngpio
*/
static ssize_t chip_base_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
const struct gpio_chip *chip = dev_get_drvdata(dev);
return sprintf(buf, "%d\n", chip->base);
}
static DEVICE_ATTR(base, 0444, chip_base_show, NULL);
static ssize_t chip_label_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
const struct gpio_chip *chip = dev_get_drvdata(dev);
return sprintf(buf, "%s\n", chip->label ? : "");
}
static DEVICE_ATTR(label, 0444, chip_label_show, NULL);
static ssize_t chip_ngpio_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
const struct gpio_chip *chip = dev_get_drvdata(dev);
return sprintf(buf, "%u\n", chip->ngpio);
}
static DEVICE_ATTR(ngpio, 0444, chip_ngpio_show, NULL);
static const struct attribute *gpiochip_attrs[] = {
&dev_attr_base.attr,
&dev_attr_label.attr,
&dev_attr_ngpio.attr,
NULL,
};
static const struct attribute_group gpiochip_attr_group = {
.attrs = (struct attribute **) gpiochip_attrs,
};
13、相关代码分析
gpiolib_sysfs_init
gpiochip_export
sysfs_create_group
查看对应编号的GPIO的属性
echo 23 > export
echo 23 > unexport
七、使用gpiolib完成led驱动
1、流程分析
(1)第1步:使用gpio_request申请要使用的一个GPIO
(2)第2步:gpio_direction_input/gpio_direction_output 设置输入/输出模式
(3)第3步:设置输出值gpio_set_value 获取IO口值gpio_get_value
2、代码实践
(1)在led1上编写代码测试通过
(2)扩展支持led2和led3、led4.可以分开注册也可以使用gpio_request_array去一次注册
(3)学习linux中查看gpio使用情况的方法
内核中提供了虚拟文件系统debugfs,里面有一个gpio文件,提供了gpio的使用信息。
使用方法:mount -t debugfs debugfs /tmp,然后cat /tmp/gpio即可得到gpio的所有信息,使用完后umount /tmp卸载掉debugfs
#include <linux/module.h> // module_init module_exit
#include <linux/init.h> // __init __exit
#include <linux/fs.h>
#include <linux/leds.h>
#include <mach/regs-gpio.h>
#include <mach/gpio-bank.h>
#include <linux/io.h>
#include <linux/ioport.h>
#include <mach/gpio.h>
//使用静态映射来操作io,使用led
#define GPJ0CON S5PV210_GPJ0CON
#define GPJ0DAT S5PV210_GPJ0DAT
#define GPIO_LED1 S5PV210_GPJ0(3)
#define GPIO_LED2 S5PV210_GPJ0(4)
#define GPIO_LED3 S5PV210_GPJ0(5)
#define x210_LED_OFF 1 //210开发板中led正极接电源,负极接GPIO
#define x210_LED_ON 0 //所以1是灭,0是亮
struct led_classdev led1_cdev;//定义结构体变量,用于描述led设备类的一个设备
struct led_classdev led2_cdev;//定义结构体变量,用于描述led设备类的一个设备
struct led_classdev led3_cdev;//定义结构体变量,用于描述led设备类的一个设备
static void s5pv210_led1_set(struct led_classdev *led_cdev,
enum led_brightness value)
{
printk(KERN_INFO "s5pv210_led1_set\n");
//在这里根据用户设定的值(value)操作硬件
if (value == LED_OFF)
{
gpio_set_value(GPIO_LED1, x210_LED_OFF);
}
else
{
gpio_set_value(GPIO_LED1, x210_LED_ON);
}
}
static void s5pv210_led2_set(struct led_classdev *led_cdev,
enum led_brightness value)
{
printk(KERN_INFO "s5pv210_led2_set\n");
writel(0x11111111, GPJ0CON);
//在这里根据用户设定的值(value)操作硬件
if (value == LED_OFF)
{
gpio_set_value(GPIO_LED2, x210_LED_OFF);
}
else
{
gpio_set_value(GPIO_LED2, x210_LED_ON);
}
}
static void s5pv210_led3_set(struct led_classdev *led_cdev,
enum led_brightness value)
{
printk(KERN_INFO "s5pv210_led3_set\n");
printk(KERN_INFO "s5pv210_led3_set\n");
writel(0x11111111, GPJ0CON);
//在这里根据用户设定的值(value)操作硬件
if (value == LED_OFF)
{
gpio_set_value(GPIO_LED3, x210_LED_OFF);
}
else
{
gpio_set_value(GPIO_LED3, x210_LED_ON);
}
}
//用户insmod安装驱动时会调用该函数,该函数的主要任务是去使用
//led驱动框架提供的设备注册函数来注册一个设备
static int __init s5pv210_led_init(void)
{
int ret = -1;
//在这里去申请驱动用到的各种资源,当前驱动中就是GPIO资源
if (gpio_request(GPIO_LED1, "led1_gpj0.3"))
{
printk(KERN_INFO "gpio_request failed.\n");
}
else
{
//设置为输出模式,并且默认输出1让led灯灭
gpio_direction_output(GPIO_LED1, 1);
}
if (gpio_request(GPIO_LED2, "led1_gpj0.4"))
{
printk(KERN_INFO "gpio_request failed.\n");
}
else
{
//设置为输出模式,并且默认输出1让led灯灭
gpio_direction_output(GPIO_LED2, 1);
}
if (gpio_request(GPIO_LED3, "led1_gpj0.5"))
{
printk(KERN_INFO "gpio_request failed.\n");
}
else
{
//设置为输出模式,并且默认输出1让led灯灭
gpio_direction_output(GPIO_LED3, 1);
}
//填充描述led类设备的属性的结构体变量
led1_cdev.name = "led1";
led1_cdev.brightness = 255;
led1_cdev.brightness_set = s5pv210_led1_set;
ret = led_classdev_register(NULL, &led1_cdev);//来源于led-class.c,创建属于led类的
//一个设备
if (ret < 0) {
printk(KERN_INFO "led_classdev_register failed\n");
return ret;
}
//填充描述led类设备的属性的结构体变量
led2_cdev.name = "led2";
led2_cdev.brightness = 255;
led2_cdev.brightness_set = s5pv210_led2_set;
ret = led_classdev_register(NULL, &led2_cdev);//来源于led-class.c,创建属于led类的
//一个设备
if (ret < 0) {
printk(KERN_INFO "led_classdev_register failed\n");
return ret;
}
//填充描述led类设备的属性的结构体变量
led3_cdev.name = "led3";
led3_cdev.brightness = 255;
led3_cdev.brightness_set = s5pv210_led3_set;
ret = led_classdev_register(NULL, &led3_cdev);//来源于led-class.c,创建属于led类的
//一个设备
if (ret < 0) {
printk(KERN_INFO "led_classdev_register failed\n");
return ret;
}
return 0;
}
static void __exit s5pv210_led_exit(void)
{
led_classdev_unregister(&led1_cdev);//来源于led-class.c,注销属于led类的
//一个设备
led_classdev_unregister(&led2_cdev);
led_classdev_unregister(&led3_cdev);
gpio_free(GPIO_LED1);
gpio_free(GPIO_LED2);
gpio_free(GPIO_LED3);
}
module_init(s5pv210_led_init); //函数放到了.initcall6.init段
module_exit(s5pv210_led_exit); //函数放到了指定的段,这些段与执行时的顺序相关
/********************MODULE_xxx这种宏作用是用来添加模块描述信息*********************/
MODULE_AUTHOR("Ben Dooks <ben@simtec.co.uk>"); //描述模块的作者
MODULE_DESCRIPTION("s5pv210 LED driver"); //描述模块的介绍信息
MODULE_LICENSE("GPL"); //描述模块的许可证
MODULE_ALIAS("s5pv210_led"); //描述模块的别名信息
八、将驱动添加到内核中
1、驱动的存在形式
(1)野生,优势是方便调试开发,所以在开发阶段都是这种
(2)家养,优势可以在内核配置时make menuconfig决定内核怎么编译,方便集成
2、驱动开发的一般步骤
(1)以模块的形式在外部编写、调试
(2)将调试好的驱动代码集成到kernel中
3、实践
(1)关键点:Kconfig、Makefile、make menuconfig
(2)操作步骤:
第1步:将写好的驱动源文件放入内核源码中正确的目录下
第2步:在Makefile中添加相应的依赖(led平台 )
obj-$(CONFIG_LEDS_S5PV210) += led-s5pv210.o
第3步:在Kconfig中添加相应的配置项
config LEDS_S5PV210
tristate "LED Support for Marvell S5PV210(X210))"
help
This option enables support for on-board LED drivers on X210
第4步:make menuconfig添加配置项
//led-s5pv210.c
#include <linux/module.h> // module_init module_exit
#include <linux/init.h> // __init __exit
#include <linux/fs.h>
#include <linux/leds.h>
#include <mach/regs-gpio.h>
#include <mach/gpio-bank.h>
#include <linux/io.h>
#include <linux/ioport.h>
//使用静态映射来操作io,使用led
#define GPJ0CON S5PV210_GPJ0CON
#define GPJ0DAT S5PV210_GPJ0DAT
struct led_classdev led1_cdev;//定义结构体变量,用于描述led设备类的一个设备
struct led_classdev led2_cdev;//定义结构体变量,用于描述led设备类的一个设备
struct led_classdev led3_cdev;//定义结构体变量,用于描述led设备类的一个设备
static void s5pv210_led1_set(struct led_classdev *led_cdev,
enum led_brightness value)
{
printk(KERN_INFO "s5pv210_led1_set\n");
writel(0x11111111, GPJ0CON);
//在这里根据用户设定的值(value)操作硬件
if (value == LED_OFF)
{
//读改写进行操作,既控制了led1,又不影响其他的led
writel((readl(GPJ0DAT) | (1 << 3)), GPJ0DAT);//led1灭
}
else
{
writel((readl(GPJ0DAT) & ~(1 << 3)), GPJ0DAT);//led1灭
}
}
static void s5pv210_led2_set(struct led_classdev *led_cdev,
enum led_brightness value)
{
printk(KERN_INFO "s5pv210_led2_set\n");
writel(0x11111111, GPJ0CON);
//在这里根据用户设定的值(value)操作硬件
if (value == LED_OFF)
{
//读改写进行操作,既控制了led1,又不影响其他的led
writel((readl(GPJ0DAT) | (1 << 4)), GPJ0DAT);//led1灭
}
else
{
writel((readl(GPJ0DAT) & ~(1 << 4)), GPJ0DAT);//led1灭
}
}
static void s5pv210_led3_set(struct led_classdev *led_cdev,
enum led_brightness value)
{
printk(KERN_INFO "s5pv210_led3_set\n");
printk(KERN_INFO "s5pv210_led3_set\n");
writel(0x11111111, GPJ0CON);
//在这里根据用户设定的值(value)操作硬件
if (value == LED_OFF)
{
//读改写进行操作,既控制了led1,又不影响其他的led
writel((readl(GPJ0DAT) | (1 << 5)), GPJ0DAT);//led1灭
}
else
{
writel((readl(GPJ0DAT) & ~(1 << 5)), GPJ0DAT);//led1灭
}
}
//用户insmod安装驱动时会调用该函数,该函数的主要任务是去使用
//led驱动框架提供的设备注册函数来注册一个设备
static int __init s5pv210_led_init(void)
{
int ret = -1;
//填充描述led类设备的属性的结构体变量
led1_cdev.name = "led1";
led1_cdev.brightness = 255;
led1_cdev.brightness_set = s5pv210_led1_set;
ret = led_classdev_register(NULL, &led1_cdev);//来源于led-class.c,创建属于led类的
//一个设备
if (ret < 0) {
printk(KERN_INFO "led_classdev_register failed\n");
return ret;
}
//填充描述led类设备的属性的结构体变量
led2_cdev.name = "led2";
led2_cdev.brightness = 255;
led2_cdev.brightness_set = s5pv210_led2_set;
ret = led_classdev_register(NULL, &led2_cdev);//来源于led-class.c,创建属于led类的
//一个设备
if (ret < 0) {
printk(KERN_INFO "led_classdev_register failed\n");
return ret;
}
//填充描述led类设备的属性的结构体变量
led3_cdev.name = "led3";
led3_cdev.brightness = 255;
led3_cdev.brightness_set = s5pv210_led3_set;
ret = led_classdev_register(NULL, &led3_cdev);//来源于led-class.c,创建属于led类的
//一个设备
if (ret < 0) {
printk(KERN_INFO "led_classdev_register failed\n");
return ret;
}
return 0;
}
static void __exit s5pv210_led_exit(void)
{
led_classdev_unregister(&led1_cdev);//来源于led-class.c,注销属于led类的
//一个设备
led_classdev_unregister(&led2_cdev);
led_classdev_unregister(&led3_cdev);
}
module_init(s5pv210_led_init); //函数放到了.initcall6.init段
module_exit(s5pv210_led_exit); //函数放到了指定的段,这些段与执行时的顺序相关
/********************MODULE_xxx这种宏作用是用来添加模块描述信息*********************/
MODULE_AUTHOR("Ben Dooks <ben@simtec.co.uk>"); //描述模块的作者
MODULE_DESCRIPTION("s5pv210 LED driver"); //描述模块的介绍信息
MODULE_LICENSE("GPL"); //描述模块的许可证
MODULE_ALIAS("s5pv210_led"); //描述模块的别名信息
重要目录和文件结构:
mach-s5pv210/gpiolib.c s5pv210_gpiolib_init
mach-s5pv210/include/mach/gpio.h #define S5PV210_GPA0(_nr) (S5PV210_GPIO_A0_START + (_nr))
arch/arm/plat-samsung/gpiolib.c 里面是210/6410这种4bit CON寄存器类型的操作方法
arch/arm/plat-samsung/gpio.c 里面是24XX这种2bit CON寄存器类型的操作方法
drivers/gpio/gpiolib.c 里面是内核开发者提供的gpiolib的驱动框架部分
4、/dev、/sys/devices 和 /sys/dev 之间区别
(1)/dev,设备文件存储目录,应用程序通过对这些文件的读写和控制,可以访问实际的设备;
(2)/sys/devices 目录,按照设备挂接的总线类型,组织成层次结构,保存了系统所有的设备;是文件系统管理设备的最重要的目录结构;
这是内核设备按总线类型分层放置的目录结构, devices 中的所有设备都是连接于某种总线之下,在这里的每一种具体总线之下可以找到每一个具体设备的符号链接,它也是构成 Linux 统一设备模型的一部分;
(3)/sys/dev 下有两个子目录,block和char,存放的是块设备和字符设备的主次号码,形式为(major:minor),它指向 /sys/devices 目录下的设备。
(4)sysfs的挂载点是/sys目录,sysfs是一个虚拟的文件系统(还有其它的虚拟文件系统,例如usbfs和procfs),sysfs导出了内核的数据结构。
(5)/sys/dev/和/sys/devices是sysfs按面向对象管理的思想来组织,sysfs最主要是用来描绘Linux kernel2.6中的设备驱动模型,用户态的后台程序会动态地周期性的扫描/sys目录中的属性项来自动管理设备文件(也称为设备节点),从而在/dev目录会建立或者删除对应的设备文件。
注:本资料大部分由朱老师物联网大讲堂课程笔记整理而来并且引用了部分他人博客的内容,如有侵权,联系删除!水平有限,如有错误,欢迎各位在评论区交流。