目录
本章目标
一、LED驱动
二、基于中断的简单按键驱动
三、基于输入子系统的按键驱动
本章目标
本章综合前面的知识,实现了嵌入式系统的常见外设驱动,包括 LED、按键、ADC、PWM和RTC。本章从工程的角度、实用的角度探讨了某些驱动的实现。比如LED 只是编写了设备树节点,设备就能被正常驱动,按键驱动则分别讨论了基于中断的和基于输入子系统的,还特别讨论了按键的消抖处理。不仅如此,本章还引入了一些新的知识,比如内核统一 GPIO 接口、时钟子系统、pinctrl 子系统等。本章虽然叫“字符设备驱动实例”,但是有些设备并没有实现为字符设备,而是通过 sysfs 文件系统接口来操作的。
一、LED驱动
经常听到的一句话“无招胜有招”是用来形容武林人士的武术修炼的境界已经达到了最高,类似的还有“无声胜有声”、“大音希声,大象希形”等。其实对于驱动也是一样的道理,如果要实现一个设备的驱动,而不必大动干戈,或者连一行驱动代码都不用写,那是不是也意味着驱动开发者的境界达到最高了呢。听起来好像是天方夜谭,但这并不是不可实现的,因为全世界的内核开发者非常热心,只要是能写的驱动,他们基本都已经写了。我们如果能够善于站在这些巨人的肩膀上,那么我们就会工作得更轻松。接下来要讨论的LED驱动就要利用内核开发者已经写好的驱动来实现我们想要的功能。在你动手写一个驱动之前,应该先看看内核是否已经实现了这个驱动。如果是,那么这会极大地提高我们的工作效率,毕竟不敲一行代码就能拿到薪水是每一个程序员都追求的终极目标,但是这个千万不能告诉老板。
我们的LED 是基于 GPIO 的,为此,内核有两个对应的驱动程序,分别是 GPIO驱动和LED驱动,基于GPIO的LED 驱动调用了GPIO 驱动导出的函数,这一节我们并不关心GPIO的驱动(后面会有详细的说明)。关于 LED 驱动,内核文档Documentation/leds/leds-class.txt 有简单的描述,它实现了一个leds 类,通过 sysfs 的接口对 LED进行控制所以它并没有使用字符设备驱动的框架,严格来说,这一节内容和本章的标题是不符合的。
驱动的实现代码请参见 drivers/leds/leds-gpio.c
/*
* LEDs driver for GPIOs
*
* Copyright (C) 2007 8D Technologies inc.
* Raphael Assenat <raph@8d.com>
* Copyright (C) 2008 Freescale Semiconductor, Inc.
*
* 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/init.h>
#include <linux/platform_device.h>
#include <linux/gpio.h>
#include <linux/leds.h>
#include <linux/of.h>
#include <linux/of_platform.h>
#include <linux/of_gpio.h>
#include <linux/slab.h>
#include <linux/workqueue.h>
#include <linux/module.h>
#include <linux/err.h>
struct gpio_led_data {
struct led_classdev cdev;
unsigned gpio;
struct work_struct work;
u8 new_level;
u8 can_sleep;
u8 active_low;
u8 blinking;
int (*platform_gpio_blink_set)(unsigned gpio, int state,
unsigned long *delay_on, unsigned long *delay_off);
};
static void gpio_led_work(struct work_struct *work)
{
struct gpio_led_data *led_dat =
container_of(work, struct gpio_led_data, work);
if (led_dat->blinking) {
led_dat->platform_gpio_blink_set(led_dat->gpio,
led_dat->new_level,
NULL, NULL);
led_dat->blinking = 0;
} else
gpio_set_value_cansleep(led_dat->gpio, led_dat->new_level);
}
static void gpio_led_set(struct led_classdev *led_cdev,
enum led_brightness value)
{
struct gpio_led_data *led_dat =
container_of(led_cdev, struct gpio_led_data, cdev);
int level;
if (value == LED_OFF)
level = 0;
else
level = 1;
if (led_dat->active_low)
level = !level;
/* Setting GPIOs with I2C/etc requires a task context, and we don't
* seem to have a reliable way to know if we're already in one; so
* let's just assume the worst.
*/
if (led_dat->can_sleep) {
led_dat->new_level = level;
schedule_work(&led_dat->work);
} else {
if (led_dat->blinking) {
led_dat->platform_gpio_blink_set(led_dat->gpio, level,
NULL, NULL);
led_dat->blinking = 0;
} else
gpio_set_value(led_dat->gpio, level);
}
}
static int gpio_blink_set(struct led_classdev *led_cdev,
unsigned long *delay_on, unsigned long *delay_off)
{
struct gpio_led_data *led_dat =
container_of(led_cdev, struct gpio_led_data, cdev);
led_dat->blinking = 1;
return led_dat->platform_gpio_blink_set(led_dat->gpio, GPIO_LED_BLINK,
delay_on, delay_off);
}
static int create_gpio_led(const struct gpio_led *template,
struct gpio_led_data *led_dat, struct device *parent,
int (*blink_set)(unsigned, int, unsigned long *, unsigned long *))
{
int ret, state;
led_dat->gpio = -1;
/* skip leds that aren't available */
if (!gpio_is_valid(template->gpio)) {
dev_info(parent, "Skipping unavailable LED gpio %d (%s)\n",
template->gpio, template->name);
return 0;
}
ret = devm_gpio_request(parent, template->gpio, template->name);
if (ret < 0)
return ret;
led_dat->cdev.name = template->name;
led_dat->cdev.default_trigger = template->default_trigger;
led_dat->gpio = template->gpio;
led_dat->can_sleep = gpio_cansleep(template->gpio);
led_dat->active_low = template->active_low;
led_dat->blinking = 0;
if (blink_set) {
led_dat->platform_gpio_blink_set = blink_set;
led_dat->cdev.blink_set = gpio_blink_set;
}
led_dat->cdev.brightness_set = gpio_led_set;
if (template->default_state == LEDS_GPIO_DEFSTATE_KEEP)
state = !!gpio_get_value_cansleep(led_dat->gpio) ^ led_dat->active_low;
else
state = (template->default_state == LEDS_GPIO_DEFSTATE_ON);
led_dat->cdev.brightness = state ? LED_FULL : LED_OFF;
if (!template->retain_state_suspended)
led_dat->cdev.flags |= LED_CORE_SUSPENDRESUME;
ret = gpio_direction_output(led_dat->gpio, led_dat->active_low ^ state);
if (ret < 0)
return ret;
INIT_WORK(&led_dat->work, gpio_led_work);
ret = led_classdev_register(parent, &led_dat->cdev);
if (ret < 0)
return ret;
return 0;
}
static void delete_gpio_led(struct gpio_led_data *led)
{
if (!gpio_is_valid(led->gpio))
return;
led_classdev_unregister(&led->cdev);
cancel_work_sync(&led->work);
}
struct gpio_leds_priv {
int num_leds;
struct gpio_led_data leds[];
};
static inline int sizeof_gpio_leds_priv(int num_leds)
{
return sizeof(struct gpio_leds_priv) +
(sizeof(struct gpio_led_data) * num_leds);
}
/* Code to create from OpenFirmware platform devices */
#ifdef CONFIG_OF_GPIO
static struct gpio_leds_priv *gpio_leds_create_of(struct platform_device *pdev)
{
struct device_node *np = pdev->dev.of_node, *child;
struct gpio_leds_priv *priv;
int count, ret;
/* count LEDs in this device, so we know how much to allocate */
count = of_get_available_child_count(np);
if (!count)
return ERR_PTR(-ENODEV);
for_each_available_child_of_node(np, child)
if (of_get_gpio(child, 0) == -EPROBE_DEFER)
return ERR_PTR(-EPROBE_DEFER);
priv = devm_kzalloc(&pdev->dev, sizeof_gpio_leds_priv(count),
GFP_KERNEL);
if (!priv)
return ERR_PTR(-ENOMEM);
for_each_available_child_of_node(np, child) {
struct gpio_led led = {};
enum of_gpio_flags flags;
const char *state;
led.gpio = of_get_gpio_flags(child, 0, &flags);
led.active_low = flags & OF_GPIO_ACTIVE_LOW;
led.name = of_get_property(child, "label", NULL) ? : child->name;
led.default_trigger =
of_get_property(child, "linux,default-trigger", NULL);
state = of_get_property(child, "default-state", NULL);
if (state) {
if (!strcmp(state, "keep"))
led.default_state = LEDS_GPIO_DEFSTATE_KEEP;
else if (!strcmp(state, "on"))
led.default_state = LEDS_GPIO_DEFSTATE_ON;
else
led.default_state = LEDS_GPIO_DEFSTATE_OFF;
}
ret = create_gpio_led(&led, &priv->leds[priv->num_leds++],
&pdev->dev, NULL);
if (ret < 0) {
of_node_put(child);
goto err;
}
}
return priv;
err:
for (count = priv->num_leds - 2; count >= 0; count--)
delete_gpio_led(&priv->leds[count]);
return ERR_PTR(-ENODEV);
}
static const struct of_device_id of_gpio_leds_match[] = {
{ .compatible = "gpio-leds", },
{},
};
#else /* CONFIG_OF_GPIO */
static struct gpio_leds_priv *gpio_leds_create_of(struct platform_device *pdev)
{
return ERR_PTR(-ENODEV);
}
#endif /* CONFIG_OF_GPIO */
static int gpio_led_probe(struct platform_device *pdev)
{
struct gpio_led_platform_data *pdata = dev_get_platdata(&pdev->dev);
struct gpio_leds_priv *priv;
int i, ret = 0;
if (pdata && pdata->num_leds) {
priv = devm_kzalloc(&pdev->dev,
sizeof_gpio_leds_priv(pdata->num_leds),
GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->num_leds = pdata->num_leds;
for (i = 0; i < priv->num_leds; i++) {
ret = create_gpio_led(&pdata->leds[i],
&priv->leds[i],
&pdev->dev, pdata->gpio_blink_set);
if (ret < 0) {
/* On failure: unwind the led creations */
for (i = i - 1; i >= 0; i--)
delete_gpio_led(&priv->leds[i]);
return ret;
}
}
} else {
priv = gpio_leds_create_of(pdev);
if (IS_ERR(priv))
return PTR_ERR(priv);
}
platform_set_drvdata(pdev, priv);
return 0;
}
static int gpio_led_remove(struct platform_device *pdev)
{
struct gpio_leds_priv *priv = platform_get_drvdata(pdev);
int i;
for (i = 0; i < priv->num_leds; i++)
delete_gpio_led(&priv->leds[i]);
return 0;
}
static struct platform_driver gpio_led_driver = {
.probe = gpio_led_probe,
.remove = gpio_led_remove,
.driver = {
.name = "leds-gpio",
.owner = THIS_MODULE,
.of_match_table = of_match_ptr(of_gpio_leds_match),
},
};
module_platform_driver(gpio_led_driver);
MODULE_AUTHOR("Raphael Assenat <raph@8d.com>, Trent Piepho <tpiepho@freescale.com>");
MODULE_DESCRIPTION("GPIO LED driver");
MODULE_LICENSE("GPL");
MODULE_ALIAS("platform:leds-gpio");
既然驱动已经实现了,那么我们要怎么来让它工作起来呢?首先要配置内核,确保驱动被选配了。在内核源码下运行 make ARCH=arm menuconfig 命令,按照下面的选项进行选择。
驱动选配好后,保存配置,并使用下面的命令重新编译内核,然后再复制到 TFTP服务器指定的目录下。
make ARCH=arm uImage
驱动配置好之后,就应该在设备树中添加设备节点,设备节点的编写方法请参考内核文档Documentation/devicetree/bindings/leds/leds-gpio.txt(在编写设备节点之前都在内核文档中找找对应的说明,这是一个良好的习惯)。修改 arch/arm/boot/dts/exynos4412-fs4412.dts,删除之前添加的 LED 设备节点,添加下面的设备节点。
leds{
compatible = "gpio-leds";
led2 {
label = "led2";
gpios = <&gpx2 7 0>;
default-state = "off";
};
led3 {
label = "led3";
gpios = <&gpx1 0 0>;
default-state = "off";
};
led4 {
label = "led4";
gpios = <&gpf3 4 0>;
default-state = "off";
};
led5 {
label = "led5";
gpios = <&gpf3 5 0>;
default-state = "off";
};
};
compatible 属性为gpio-leds,可以和 LED驱动匹配。每个led 节点中的label是出现在sys目录下的子目录名字。gpios 则指定了该LED所连接的GPIO口,第三个值为0表示高电平点亮 LED灯,为1则表示低电平点亮 LED 灯。default-state 属性的值为off,则表示默认情况下 LED 灯是熄灭的,为 on 则默认点亮。修改好设备树源文件后,使用下面的命令编译设备树,然后复制到指定目录。
重新启动开发板,使用下面的命令可以看到对应的设备目录。
led2、led3、led4、led5 分别对应了 4个LED 灯,在每个目录下都有一个 brightness文件,通过读取该文件可以获取 LED 灯的当前亮度,通过写该文件可以修改 LED 灯的亮度。因为这些 LED 灯饰连接在 GPIO 端口上面,所以亮度只有 0和1,0表示熄灭,1表示点亮,命令如下。
当然,也可以编写一个应用程序来控制 LED 灯的亮灭,应用层测试代码如下
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#define LED_DEV_PATH "/sys/class/leds/led%d/brightness"
#define ON 1
#define OFF 0
int fs4412_set_led(unsigned int lednum, unsigned int mode)
{
int fd;
int ret;
char devpath[128];
char *on = "1\n";
char *off = "0\n";
char *m = NULL;
snprintf(devpath, sizeof(devpath), LED_DEV_PATH, lednum);
fd = open(devpath, O_WRONLY);
if (fd == -1) {
perror("fsled->open");
return -1;
}
if (mode == ON)
m = on;
else
m = off;
ret = write(fd, m, strlen(m));
if (ret == -1) {
perror("fsled->write");
close(fd);
return -1;
}
close(fd);
return 0;
}
int main(int argc, char *argv[])
{
unsigned int lednum = 2;
while (1) {
fs4412_set_led(lednum, ON);
usleep(500000);
fs4412_set_led(lednum, OFF);
usleep(500000);
lednum++;
if (lednum > 5)
lednum = 2;
}
}
效果是一个流水灯试试资源绑定功能,成功的话大家后面可以下载看一看。
代码比较简单,这里不再进行说明。需要注意的是,对sys目录下的文件操作要注意文件位置指针,简单的方法就是每次重新打开,使用后再关闭,编译和测试的命令如下4个LED灯会向前面的例子一样依次闪烁。我用了我的便捷小脚本,经过30多个驱动实验的打磨已经很好用了。后面有时间把内核编译和设备树编译用脚本管理起来,做个小sdk。(悄悄滴:看看能不能卖给华清哈哈哈哈)
二、基于中断的简单按键驱动
在 FS4412上面有三个按键,其相关的原理图如图所示。
K2和K3可以用作一般按键输入,K4 用于电源管理。进一步结合核心板原理图可知,K2 和K3 分别接到了 GPXL.和 GPX1.2 管脚上,并且这两个管脚还可以当外部中断输入管脚,断号分别是 EINT9和 EINT10。K2和K3 是常开的按键开关,所以这两个管脚平时都处于高电平,当按下按键后,管脚被直接接地,为低电平。也就是说在按键按下的瞬间会产生一个下降沿,在按键松开的时候会产生一个上升沿。我们可以将管脚设置为下降沿触发,从而能及时响应用户的按键输入,也可以设置为双沿触发,这样在按键按下和抬起的时候都会产生中断。
接下来我们首先在设备树中添加节点,为了要描述这两个管脚以中断方式工作,我们需要参考内核文档 Documentation/devicetree/bindings/interrupt-controller/interruptstxt,该文档中举例说明了中断属性该如何设置。
interrupt-parent = <&intcl>;
interrupts=<5 0>,<6 0>;
interrupt-parent 指定了中断线所连接的中断控制器节点,interrupts 的值如果有两个cell,那么第一个cell是中断线在中断控制器中的索引,第二个 cell 指的是中断触发方式,0表示没指定。
为了给出按键设备的节点,我们需要查看其中断控制器节点的内容。因为它们是接在GPX1这组管脚上的,根据gpx1 关键字我们在arch/arm/boot/dts/exynos4x12-pinctrl.dtsi文件中可以看到其定义。
这也解释了为什么前面我们设置led的设备树时没指定物理地址只说明了GPIO的名字依旧能找到并控制led因为其它的文件中已经定义好了。
由此可见,gpx1 确实是一个中断控制器,并且使用的是两个 cell,它总共有 8根中断线,刚好对应管脚GPX1.0到GPX1.7。我们使用的管脚是 GPX1.1和GPX1.2,那么索引自然就是1和2。这也可以通过 gpxl 的父节点 gic来确定,在 Documentation/devicetreebindings/arm/gic.txt内核文档中描述了 interrupts 的三个cell的含义,第一个cell是0表的是SPI中断;第二个 cell是 SPI的中断号:第三个 cell是中断触发方式,为0表示没指定。查看 Exynos4412 芯片手册的中断控制器部分,如图所示,我们可以看到EINT9和EINT10中断对应的SPI中断号刚好是25和26。
有了这些信息后,我们就可以给出按键的设备节点,内容如下,
keys {
compatible="fs4412,fskey";
interrupt-parent = <&gpx1>;
interrupts=<1 2>,<2 2>;
};
interrupts 属性的第二个 cell为2 表示下降沿发。将上面的代码添加到设备树文件arch/arm/boot/dts/exynos4412-fs4412.dts 中,并重新编译设备树,然后复制到TFTP 服务器指定的目录下。
结合前面中断编程和 Linux 设备模型的知识,我们很容易写出这个简单的按键驱动代码如下
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/ioctl.h>
#include <linux/uaccess.h>
#include <linux/of.h>
#include <linux/interrupt.h>
#include <linux/platform_device.h>
struct resource *key2_res;
struct resource *key3_res;
static irqreturn_t fskey_handler(int irq, void *dev_id)
{
if (irq == key2_res->start)
printk("K2 pressed\n");
else
printk("K3 pressed\n");
return IRQ_HANDLED;
}
static int fskey_probe(struct platform_device *pdev)
{
int ret;
key2_res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
key3_res = platform_get_resource(pdev, IORESOURCE_IRQ, 1);
if (!key2_res || !key3_res) {
ret = -ENOENT;
goto res_err;
}
ret = request_irq(key2_res->start, fskey_handler, key2_res->flags & IRQF_TRIGGER_MASK, "key2", NULL);
if (ret)
goto key2_err;
ret = request_irq(key3_res->start, fskey_handler, key3_res->flags & IRQF_TRIGGER_MASK, "key3", NULL);
if (ret)
goto key3_err;
return 0;
key3_err:
free_irq(key2_res->start, NULL);
key2_err:
res_err:
return ret;
}
static int fskey_remove(struct platform_device *pdev)
{
free_irq(key3_res->start, NULL);
free_irq(key2_res->start, NULL);
return 0;
}
static const struct of_device_id fskey_of_matches[] = {
{ .compatible = "fs4412,fskey", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, fskey_of_matches);
struct platform_driver fskey_drv = {
.driver = {
.name = "fskey",
.owner = THIS_MODULE,
.of_match_table = of_match_ptr(fskey_of_matches),
},
.probe = fskey_probe,
.remove = fskey_remove,
};
module_platform_driver(fskey_drv);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("name <e-mail>");
MODULE_DESCRIPTION("A simple device driver for Keys on FS4412 board");
代码第 33 行和第 34 行使用 platform_get_resouree 分别获取了这两个中断资源,代码 第41行和第 44 行则分别注册了这两个中断,起始的中断号就是资源中的 start 成员,两个中断的处理函数都是 fskey_handler。在 skey_handler 函数中,根据中断号 irq 来确定哪9音个按键按下了。这个驱动也不是字符设备驱动,只是一个简单的按键中断测试的模块而已。相应的编译及测试命令如下。
驱动加载后,按下按键在控制台上将会有打印。我们会发现,按键只按了一次,可能会打印几次,选成这个结果的原因就是大家所熟悉的按键抖动。因为一些机械特性,使得在按下和松开按键的时候,产生的波形并不是完美的,现实的波形大概如图所示
在上面的波形中我们看到,下降沿产生了好几次,这就导致中断被触发了好几次。为了解决这个问题,我们必须要对其做消抖处理,简单的做法是使用一个定时器来计算两次中断的时间间隔,如果太小则忽略之后的中断。不过使用中断的方式还是会有一个问题,试想一下,当你长按电脑键盘的一个键会出现什么情况?一般情况下,我们应该能够报告按键的按下、抬起和长按三种事件才比较友好,为此,我们使用下一节的方法来实现一个比较实用的按键设备驱动。
三、基于输入子系统的按键驱动
如果想让我们的按键像键盘一样很酷地工作,那么我们就不得不利用内核中的输入子系统。简单来说,输入子系统就是为所有输入设备对上层提供统一接口的一个子系统常见的输入设备有键盘、鼠标、手写输入板、游戏杆和触摸板等。输入子系统都为其定义了相应的标准,比如规定了输入事件的表示、按键值和鼠标的相对坐标等。输入子系统的大致层次结构如图所示。
驱动用于实时获取输入硬件的输入数据,事件处理层用于处理驱动报告的数据,并对上层提供标准的事件信息,输入核心层则用来管理这两层,并建立沟通的桥梁。这个输入子系统还是比较复杂的,但是我们只是编写一个输入设备驱动的话,认识一个重要的数据结构struct input_dev 和几个常用的API就可以了。
struct input_dev {
/* 设备名称 */
const char *name;
/* 设备驱动程序标识符 */
const char *phys;
/* 设备总线类型 */
const char *id;
struct input_id id_table[MAX_ID];
/* 设备支持的事件类型 */
unsigned long evbit[NBITS(EV_MAX)];
/* 事件处理回调函数 */
void (*event)(struct input_dev *dev, unsigned int type, unsigned int code, int value);
/* 设备索引 */
int devindex;
/* 设备支持的按键映射 */
unsigned long keybit[NBITS(KEY_MAX)];
/* ... */
};
一个输入设备用structinput dev 结构对象来表示,主要成员的意义如下
name:输入设备的名字。
phys: 在系统层次结构中设备的物理路径。
id:输入设备的id,包含总线类型、制造商ID、产品ID 和版本号等evbit: 设备能够报告的事件类型,比如 EV KEY 表示设备能够报告按键事件EV REL表示设备能够报告相对坐标事件。
keybit:设备能够报告的按键值
keycodemax:按键编码表的大小
keycodesize:按键编码表每个编码的大小
keycode:按键编码表。
输入设备驱动的几个重要API如下
struct input_dev *input_allocate_device(void);
void input_free_device(struct input_dev *dev);
int input_register_device(struct input_dev *dev);
void input_unregister_device(struct input_dev *dev);
void input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value);
void input_report_key(struct input_dev *dev, unsigned int code, int value);
void input_sync(struct input_dev *dev);
input_allocate_device:动态分配一个 struct input_dev 结对象,返回对象的地址NULL 表示失败。
input free_device:释放输入设备 dev。
input register_device:注册输入设备 dev。
input unregister_device:注销输入设备dev。
input_event:报告一个事件,dev 是报告事件的输入设备,type 是事件类型,code是编码,value 是具体值,根据事件类型的不同而不同。
input report_key:input_event的封装,报告的事件类型为EV_KEY
input sync:同步事件。
一个输入设备驱动的实现步骤一般如下。
(1)使用input_allocate_device 分配一个输入设备对象。
(2)初始化输入设备对象,包括名字、路径、ID、能报告的事件类型和与编码表相关的内容等。
(3)使用input_register_device 注册输入设备。
(4)在输入设备产生事件时使用input event 报告事件
(5)使用input sync同步事件。(6)在不需要输入设备时使用input_unregister_device注销设备,并用input free_device
介绍完输入子系统及其相关的编程步骤后,我们来讨论按键值的获取和消抖处理。释放其内存。首先,我们使用扫描的方式来获取按键值,而不是中断的方式。也就是将管脚配置为输入,然后定期读取管脚的电平,根据电平的高低来判断按键的状态。其次,关于消抖的处理可以按照下图的方式进行。
假设驱动程序每隔 50ms 来扫描按键,每次扫描则要多次获取按键的电平高低值,当连续 3次读到的按键的电平高低值都一致,才认为成功获取了按键的值,再进行报告。在图9.5 中,刚开始读到的电平为高,继续读时,电平为低,所以计数清零,再次读取,电平又变高,计数又清零,如此一直继续下去,直到计数为 2为止。下一次扫描是按镜一直被按下,没有抖动,所以计数很顺利到 2。再下一次扫描,也是出现了抖动,计数值不断清零,直到稳定成高电平为止。扫描的间隔时间、每次采样的间隔时间以及计数值可以根据具体的硬件设备而定。
我们前面说过,内核专门针对 GPIO 硬件编写了一个 GPIO 框架代码,这使得我们对GPIO的编程变得更加容易,下面就先来看看这些主要的API。
int gpio_request(unsigned gpio,const char *label);
int gpio_request_array(const struct gpio *array; size_t num);
void gpio_free(unsigned gpio);\
void gpio_free_array(const struct gpio *array,size_t num);
int gpio direction_input(unsigned gpio);
int gpio direction_output(unsigned gpio,int value);
int gpio get_value(unsigned gpio);
void gpio set_value(unsigned gpio,int value);
int of_get_gpio(struct device_node *np,int index);
gpio_request: 申请一个 GPIO,并取名为 label,返回0表示成功.
gpio_request_array:申请一组 GPIO,返回0表示成功。
gpio_free:释放一个GPIO。
gpio_free_array:释放一组GPIO。
gpio_direction_input:设置GPIO管脚为输入
gpio_direction_output: 设置GPIO 管脚为输出
gpio_get_value:获取输入 GPIO管脚的状态。
gpio_set_value:设置输出GPIO管脚的输出电平
of_get_gpio:从设备节点np 中获取第 index个GPIO,成功返GPIO编号,失败返回一个负数。
使用上面的API对GPIO进行编程通常包含下面几个步骤。
(1)使用of_get_gpio 获取设备节点中描述的GPIO对应的编号。
(2)使用 gpio_request 申请对 GPIO 管脚的使用权限如果已经被其他驱动先申请了那么再次申请会失败,这种用法和我们之前的 I/O 内存的使用方法是一样的。
(3)使用 gpio_direction input 或 gpio direction output 将 GPIO 管脚配置为输入或输出。
(4)使用 gpio_get_value 或 gpio set_value 来获取输入 GPIO管脚的状态或设置输出GPIO管脚的输出电平。
(5)如果不再使用GPIO管脚,则应该使用 gpio free 来释放GPIO管脚。有了上面的各方面知识后,我们就可以来编写一个基于输入子系统的按键驱动,代码如下
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/delay.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/gpio.h>
#include <linux/input.h>
#include <linux/input-polldev.h>
#include <linux/platform_device.h>
#define MAX_KEYS_NUM (8)
#define SCAN_INTERVAL (50) /* ms */
#define KB_ACTIVATE_DELAY (20) /* us */
#define KBDSCAN_STABLE_COUNT (3)
struct fskey_dev {
unsigned int count;
unsigned int kstate[MAX_KEYS_NUM];
unsigned int kcount[MAX_KEYS_NUM];
unsigned char keycode[MAX_KEYS_NUM];
int gpio[MAX_KEYS_NUM];
struct input_polled_dev *polldev;
};
static void fskey_poll(struct input_polled_dev *dev)
{
unsigned int index;
unsigned int kstate;
struct fskey_dev *fskey = dev->private;
for (index = 0; index < fskey->count; index++)
fskey->kcount[index] = 0;
index = 0;
do {
udelay(KB_ACTIVATE_DELAY);
kstate = gpio_get_value(fskey->gpio[index]);
if (kstate != fskey->kstate[index]) {
fskey->kstate[index] = kstate;
fskey->kcount[index] = 0;
} else {
if (++fskey->kcount[index] >= KBDSCAN_STABLE_COUNT) {
input_report_key(dev->input, fskey->keycode[index], !kstate);
index++;
}
}
} while (index < fskey->count);
input_sync(dev->input);
}
static int fskey_probe(struct platform_device *pdev)
{
int ret;
int index;
struct fskey_dev *fskey;
fskey = kzalloc(sizeof(struct fskey_dev), GFP_KERNEL);
if (!fskey)
return -ENOMEM;
platform_set_drvdata(pdev, fskey);
for (index = 0; index < MAX_KEYS_NUM; index++) {
ret = of_get_gpio(pdev->dev.of_node, index);
if (ret < 0)
break;
else
fskey->gpio[index] = ret;
}
if (!index)
goto gpio_err;
else
fskey->count = index;
for (index = 0; index < fskey->count; index++) {
ret = gpio_request(fskey->gpio[index], "KEY");
if (ret)
goto req_err;
gpio_direction_input(fskey->gpio[index]);
fskey->keycode[index] = KEY_2 + index;
fskey->kstate[index] = 1;
}
fskey->polldev = input_allocate_polled_device();
if (!fskey->polldev) {
ret = -ENOMEM;
goto req_err;
}
fskey->polldev->private = fskey;
fskey->polldev->poll = fskey_poll;
fskey->polldev->poll_interval = SCAN_INTERVAL;
fskey->polldev->input->name = "FS4412 Keyboard";
fskey->polldev->input->phys = "fskbd/input0";
fskey->polldev->input->id.bustype = BUS_HOST;
fskey->polldev->input->id.vendor = 0x0001;
fskey->polldev->input->id.product = 0x0001;
fskey->polldev->input->id.version = 0x0100;
fskey->polldev->input->dev.parent = &pdev->dev;
fskey->polldev->input->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
fskey->polldev->input->keycode = fskey->keycode;
fskey->polldev->input->keycodesize = sizeof(unsigned char);
fskey->polldev->input->keycodemax = index;
for (index = 0; index < fskey->count; index++)
__set_bit(fskey->keycode[index], fskey->polldev->input->keybit);
__clear_bit(KEY_RESERVED, fskey->polldev->input->keybit);
ret = input_register_polled_device(fskey->polldev);
if (ret)
goto reg_err;
return 0;
reg_err:
input_free_polled_device(fskey->polldev);
req_err:
for (index--; index >= 0; index--)
gpio_free(fskey->gpio[index]);
gpio_err:
kfree(fskey);
return ret;
}
static int fskey_remove(struct platform_device *pdev)
{
unsigned int index;
struct fskey_dev *fskey = platform_get_drvdata(pdev);
input_unregister_polled_device(fskey->polldev);
input_free_polled_device(fskey->polldev);
for (index = 0; index < fskey->count; index++)
gpio_free(fskey->gpio[index]);
kfree(fskey);
return 0;
}
static const struct of_device_id fskey_of_matches[] = {
{ .compatible = "fs4412,fskey", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, fskey_of_matches);
struct platform_driver fskey_drv = {
.driver = {
.name = "fskey",
.owner = THIS_MODULE,
.of_match_table = of_match_ptr(fskey_of_matches),
},
.probe = fskey_probe,
.remove = fskey_remove,
};
module_platform_driver(fskey_drv);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kevin Jiang <name<e-mail>");
MODULE_DESCRIPTION("A simple device driver for Keys on FS4412 board");
代码第20行至第 27行定义了一个结构类型 struct fskey_dev,count 成员表示共有多少个按键: kstate 用于记录按键的上一次状态;kcount用来记录已经连续多少次采样得到同样的状态,用于消抖;keycode 是报告给上层的按键编码;gpio 用来记录 GPIO 编号:polldev 是一个 polldev 输入设备,特指通过轮询来得到输入设备的输入数据的设备,相当于struct input_dev 的子类,主要是多了一个 poll函数来轮询输入设备。
代码第 62行动态分配了上面所说的结构对象。第68 行至第 79 行用于取设备树中描述的GPIO对应的编号,并得到了总的 GPIO管脚数量。第 81 行至第 89 行则分别申请了这些GPIO的使用权,并都配置为输入,还将按键编码设置为数字键2及之后顺序的值,状态为 1,表示按键是松开的。
代码第91行动态分配了一个struct input_polled_dev 结构对象。第97行将 fskey保存在private成员中,方便之后的函数通过struct input_polled_dev 结构对象来获取fskey。第98 行指定了轮询的函数 fskey_poll,在这个函数中将会检测按键的状态并上报键值。poll_interval是轮询的间隔,这里指定为50ms。第 101行至第 107行是对输入设备结构对象的初始化,包括名字、路径和ID。第 109 行指定该输入设备能够报告 EV_KEY 时间,并且EV_REP说明驱动支持长按键的检测。keycode、keycodesize 和 keycodemax 则是与按键编码表相关的初始化。第 114 行至第 116 行设置了输入设备能够报告的按键编码,回忆前面的 keybit的描述。KEY_RESERVED是编号为0的键,使用 __clear_bit清除,则表示不报告该键。代码第 118 行注册了该输入设备。
在 fskey_poll 函数中,使用了前面介绍的算法来进行消抖,每次采样的间隔设置为20us。当连续 3 次采样的状态都一致时则使用 imput_report_key 报告键值,然后继续对下一个按键进行扫描。最后使用 input_sync 来同步,即把之前报告的键值同步到上层。另外,报告的键值!kstate 表示按键是被按下还是被释放。
要编译该驱动,必须要确保内核支持了轮询的输入设备,使用 make ARCH=arm menuconfig命令,在配置界面中做如下配置。
需要特别注意的是,在 arch/arm/boot/dts/exynos4412-fs4412,dts 设备树文件中,上面的两个GPIO 已经在另外的节点中用到了,但是实际的硬件却没有用作其他节点中定义的功能,所以要保证我们的驱动能成功申请到 GPIO,必须要把那些节点修改好或删除。这里选择删除,涉及的节点有 regulators、pinctrl@11000000 和 keypad@100A0000。修改好设备树源文件后,按照前面的方法进行编译。最后将新生成的文件复制到TFTP 服务器指定的目录。
这里有一个致命bug有个io在mmc读取寄存器时用到了,这个取消配置后编译通不过。少写了一个分号吐了
测试用的应用层代码如下,因为代码很简单,这里不再进行说明。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <linux/input.h>
#define KEY_DEV_PATH "/dev/input/event0"
int fs4412_get_key(void)
{
int fd;
struct input_event event;
fd = open(KEY_DEV_PATH, O_RDONLY);
if(fd == -1) {
perror("fskey->open");
return -1;
}
while (1) {
if(read(fd, &event, sizeof(event)) == sizeof(event)) {
if (event.type == EV_KEY && event.value == 1) {
close(fd);
return event.code;
} else
continue;
} else {
close(fd);
fprintf(stderr, "fskey->read: read failed\n");
return -1;
}
}
}
int main(int argc, char *argv[])
{
while (1)
printf("key value: %d\n", fs4412_get_key());
}