【编者的话】了解了基本的内核模块开发、内核空间和用户空间交互之后,终于要开始和硬件设备直接交互了。Linux 内核提供了对通用输入输出接口、中断请求等的封装,让驱动开发者可以利用中断来控制硬件线路上的设备。本文来自 Derek Molloy 的博客,通过三个示例,讲解了通用输入输出接口和中断编程,内核对象和内核线程的使用。
前言
在本系列文章中,我们已经知道了如何为嵌入式 Linux 设备编写可加载内核模块(loadable kernel module,LKM)。这是本系列的第三篇文章,在阅读本文之前,请先阅读:
这两篇文章介绍了如何构建、加载和卸载可加载内核模块和字符设备驱动。这些细节不会在本文中重复。
本文描述了如何编写内核代码,使之能够和连接到嵌入式 Linux 系统通用输入输出接口(GPIO)的定制电子电路交互。同时也将介绍如何为这种嵌入式 Linux 系统中默认不支持的设备添加高级功能。本文使用 BeagleBone 作为部署平台,为了完成本文实验,使用 BeagleBone 是理想的方案,但不是必须的。
本文的重点是教学和实践,而不是解决一个深入实际的需求。但我相信如果读者能够阅读按键和发光二极管闪烁实验,那么也可以实现和大部分电子设备交互。因此,本文描述了三个不同的可加载内核模块,每个都有它自己独立的目的和需求:
- 示例 1:按钮按下,发光二极管亮起:在本例中,当按钮按下时,发光二极管将会亮起,非常简单(不,并不是)。为了完成这个任务,将会介绍内核中断相关概念,并且使用linux/gpio.h头文件提供的库来实现。本例用于测试中断性能。
- 示例 2:增强的按键通用输入输出接口驱动:本例用于介绍 kobjects 对象和在 sysfs 中添加自定义项的机制。这将允许用户在运行时和可加载内核模块收发数据。本例同时介绍了内核代码中计时功能的使用。
- 示例 3:增强的发光二极管通用输入输出接口驱动:本例用于让发光二极管闪烁,同时介绍了 Linux 内核线程。本质上,发光二极管通过可以在用户空间控制的内核模块,以一定频率闪烁。
和按钮、闪烁的发光二极管交互有其他简单的方式,但是这些示例介绍了一些对于处理复杂内核任务编程来说至关重要的概念。
视频演示
这里提供了一段 YouTube 上的短片,它呈现了本文开发的可加载内核模块的功能概述。
https://youtu.be/ltCZydX_zmk
视频 1:本文描述的可加载内核模块的功能
电路
本文使用的唯一电路如图 1 所示。这是和《Exploring BeagleBone》书第六章相同的图,为了方便直接在这里使用。作为书中详细介绍的,使用了一个场效应管(Field Effect Transistor,FET)(小信号晶体管)作为发光二极管门电路,它确保了点亮发光二极管的电流不会损坏 BeagleBone。可以通过串联一个大限流电阻,直接将发光二极管连接到通用输入输出接口上,但这是不建议的。图中按钮没有使用上拉或者下拉电阻,这是因为 BeagleBone 的 P9_27 端口默认配置了一个内置的下拉电阻。在连接类似电路的时候总是需要非常的小心,任何一个失误都可能导致 BeagleBone 损坏!
(点击放大图像)
图1:本文描述的发光二极管和按键电路
本次讨论的源码
本次讨论的所有代码都在为《Exploring BeagleBone》准备的GitHub 仓库上。代码可以在 ExploringBB GitHub 仓库内核工程目录中公开查看,或者也可以将代码复制到 BeagleBone(或者其他 Linux 设备):
molloyd@beaglebone:~$ sudo apt-get install git molloyd@beaglebone:~$ git clone https://github.com/derekmolloy/exploringBB.git
代码中 /extras/kernel/ 目录中的gpio_test、button和led是本文最重要的几个目录。为这些示例代码自动生成的 Doxygen 文档有 HTML 格式和 PDF 格式。
示例 1:按钮按下,发光二极管亮起可加载内核模块
当在嵌入式 Linux 设备中和电子电路进行交互的时候,开发者立即需要面对 sysfs 文件系统,同时需要使用低级别文件操作。这种方式可能会导致效率低下(特别是对传统嵌入式系统有开发经验时),然而,这些文件项会使用内存映射,对于大部分应用程序性能来说是足够的。在我的书中,我已经证明了,通过使用 pthread 库、回调函数和sys/poll.h,在忽略 CPU 开销后,可以实现响应时间大约在三分之一毫秒。
和 Linux 用户空间不同,Linux 内核空间支持中断。本文第一个示例演示了如何编写一个使用通用输入输出接口和中断的可加载内核模块,实现比在用户空间更快的响应时间。我不建议开发者将所有通用输入输出接口代码都在内核空间实现,但是这些示例可能可以提供一些启发,开发者可以在内核空间执行独立的任务,高级别代码仍然可以在 Linux 用户空间编写。
通用输入输出接口和内核
通用输入输出接口(General Purpose Input/Outputs,GPIOs)在书中第 6 章和前文 / 视频中有详细描述。这些软件控制的输入输出接口,能够在 Linux 用户空间使用通用输入输出接口的 sysfs 接口控制(直接使用 Linux shell 或者在可执行程序中操作),这些接口能够让开发者激活通用输入输出接口或者设置它的状态。比如,要通过 sysfs 激活图 1 中的发光二极管并开启 / 关闭发光二极管,可以用如下步骤实现(使用超级用户权限):
root@beaglebone:/sys/class/gpio# ls export gpiochip0 gpiochip32 gpiochip64 gpiochip96 unexport root@beaglebone:/sys/class/gpio# echo 49 > export root@beaglebone:/sys/class/gpio# ls export gpio49 gpiochip0 gpiochip32 gpiochip64 gpiochip96 unexport root@beaglebone:/sys/class/gpio# cd gpio49 root@beaglebone:/sys/class/gpio/gpio49# ls active_low direction edge power subsystem uevent value root@beaglebone:/sys/class/gpio/gpio49# echo out > direction root@beaglebone:/sys/class/gpio/gpio49# echo 1 > value root@beaglebone:/sys/class/gpio/gpio49# echo 0 > value root@beaglebone:/sys/class/gpio/gpio49# cd .. root@beaglebone:/sys/class/gpio# echo 49 > unexport root@beaglebone:/sys/class/gpio# ls export gpiochip0 gpiochip32 gpiochip64 gpiochip96 unexport
有趣的是,在 Linux 内核空间控制通用输入输出接口的步骤和上述步骤非常类似。Linux 通用输入输出接口,可以通过 linux/gpio.h(3.8.x 版本)头文件中定义的函数在内核空间方便的访问和控制。这里列举了包含在该头文件中的一些重要的函数:
static inline bool gpio_is_valid(int number) // 检查通用输入输出接口号是否有效(在 BeagleBone 开发版上最大值是 127) static inline int gpio_request(unsigned gpio, const char *label) // 分配通用输入输出接口号和提供给 sysfs 的标签 static inline int gpio_export(unsigned gpio, bool direction_may_change) // 通过 sysfs 激活接口,并且决定端口方向,可以从输入变成输出,反之亦然 static inline int gpio_direction_input(unsigned gpio) // 设置为输入方向(和通常一样,成功返回 0) static inline int gpio_get_value(unsigned gpio) // 获取输入输出接口设置的值(方向) static inline int gpio_direction_output(unsigned gpio, int value) // 值是当前状态 static inline int gpio_set_debounce(unsigned gpio, unsigned debounce) // 以毫秒为单位设置去抖动时间(平台相关) static inline int gpio_sysfs_set_active_low(unsigned gpio, int value) // 设置低电平有效(反转运行状态) static inline void gpio_unexport(unsigned gpio) // 从 sysfs 移除 static inline void gpio_free(unsigned gpio) // 释放通用输入输出端口 static inline int gpio_to_irq(unsigned gpio) // 与中断请求关联
重要的是,通过上述列表中的最后一个函数,可以将一个中断请求(interrupt request,IRQ)关联到通用输入输出接口上。中断请求允许开发者构建有效率、高性能的代码来检测输入状态改变。我们将在下面讨论中断和在 Linux 操作系统上的使用。有关 Linux 系统上使用输入输出接口使用的更多信息,参阅:
中断
中断是从连接的硬件设备、软件应用程序或者电路发送给微处理器的信号,该信号标示了有需要关注的事件发生。中断是高优先级条件,它大致的含义是“中断当前正在做的事情,做一些其他事情”。处理器挂起当前活动,保存当前状态并且执行中断处理函数,这也被成为中断服务例程(interrupt service routine,ISR)。一旦处理函数执行完毕,处理器重新加载之前的状态,继续进行之前的活动。
可加载内核模块驱动必须注册中断的处理函数,它定义了当中断发生时需要执行的操作。在本例中处理函数命名为 ebbgpio_irq_handler(),它的形式如下:
static irq_handler_t ebbgpio_irq_handler(unsigned int irq, void *dev_id, struct pt_regs *regs) { // 中断发生时需要执行的操作 … }
处理函数然后通过 request_irq() 函数注册为中断请求(IRQ),步骤如下:
result = request_irq(irqNumber, // 注册的中断号 (irq_handler_t) ebbgpio_irq_handler, // 指向中断处理函数的指针(如上所示) IRQF_TRIGGER_RISING, // 中断在上升沿(图 1 中按键按下) "ebb_gpio_handler", // 用于在 /proc/interrupts 中标识拥有者 NULL); // 共享中断的设备 id,这里为 NULL
中断号在列表 2 的示例代码中是自动确定的,它通过将中断号和各自的通用输入输出端口号关联,重要的是,通用输入输出号不是中断号,然而,他们有一对一映射。
要取消中断请求响应,有一个对应的 free_irq() 函数。在第一个示例中,free_irq() 函数在 ebbgpio_exit() 函数中调用,也就是在可加载模块卸载的时候调用。
在本例中,一个简单的瞬时按钮(如图 1 所示),被用于在按键按下去的上升沿产生一个中断。它也可以在下降沿创建一个中断,列表 1 的代码,通过/include/linux/interrupt.h头文件,提供了中断定义集合。这些标记可以通过按位或运算符进行结合,提供精确中断配置控制。
#define IRQF_TRIGGER_NONE 0x00000000 #define IRQF_TRIGGER_RISING 0x00000001 #define IRQF_TRIGGER_FALLING 0x00000002 #define IRQF_TRIGGER_HIGH 0x00000004 #define IRQF_TRIGGER_LOW 0x00000008 #define IRQF_TRIGGER_MASK (IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | \ IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING) #define IRQF_TRIGGER_PROBE 0x00000010 #define IRQF_DISABLED 0x00000020 // 当调用中断处理函数的时候关闭中断请求 #define IRQF_SHARED 0x00000080 // 允许多个设备间共享中断请求 #define IRQF_PROBE_SHARED 0x00000100 // 当希望共享错配发生时,由调用方设置 #define __IRQF_TIMER 0x00000200 // 用于标记当前中断是定时器中断的标志 #define IRQF_PERCPU 0x00000400 // 中断是每个 CPU 的 #define IRQF_NOBALANCING 0x00000800 // 将当前中断从中断请求平衡中排除的标志 #define IRQF_IRQPOLL 0x00001000 // 中断用于轮询 #define IRQF_ONESHOT 0x00002000 // 当中断处理函数结束后,中断不会再使能 #define IRQF_NO_SUSPEND 0x00004000 // 在挂起时不要禁止当前中断请求 #define IRQF_FORCE_RESUME 0x00008000 // 在挂起恢复是强制使能,即使设置了 IRQF_NO_SUSPEND 标志 #define IRQF_NO_THREAD 0x00010000 // 中断不能使用线程 #define IRQF_EARLY_RESUME 0x00020000 // 尽早在 syscore 阶段恢复,而不是在设备恢复时间恢复 #define IRQF_TIMER (__IRQF_TIMER | IRQF_NO_SUSPEND | IRQF_NO_THREAD)
列表 1:可用的中断标志位(参见:/usr/src/linux-headers…/include/linux/interrupt.h)
第一个可加载内核模块的完整源码在列表 2 中提供。示例代码中的注释为每个函数的角色提供了描述。参见 GitHub 仓库目录: /extras/kernel/gpio_test/gpio_test.c 。该示例中的代码很大程度上基于本系列前两篇文章中的描述。
/** * @file gpio_test.c * @author Derek Molloy * @date 2015 年 4 月 19 日 * @brief 控制通用输入输出接口上的发光二极管 / 按键对的内核模块。 * 该设备通过 sysfs 挂载到 /sys/class/gpio/gpio115 和 gpio49。因此该测试用可加载内核模块假设发光二极管连接到了 * 通用输入输出接口 49 端口(BeagleBone 的 P9_23 针脚),按键连接到了通用输入输出接口 115 端口(BeagleBone 的 P9_27 针脚)。 * 对针脚定义不需要自定义,因为这些针脚在处于其默认复用模式状态。 * @see http://www.derekmolloy.ie/ */ #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/gpio.h> // 通用输入输出接口相关函数需要 #include <linux/interrupt.h> // 中断请求代码需要 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Derek Molloy"); MODULE_DESCRIPTION("A Button/LED test driver for the BBB"); MODULE_VERSION("0.1"); static unsigned int gpioLED = 49; ///< 硬编码发光二极管端口,本例中为 P9_23 针脚(通用输入输出 49 端口) static unsigned int gpioButton = 115; ///< 硬编码按键端口,本例中尉 P9_27 针脚(通用输入输出 115 端口) static unsigned int irqNumber; ///< 用于本文件中共享中断请求号 static unsigned int numberPresses = 0; ///< 信息记录,保存按键按下次数 static bool ledOn = 0; ///< 发光二极管是关闭还是开启?用于反转它的状态(默认是关闭的) /// 自定义中断请求处理函数的函数原型,参见下方实现 static irq_handler_t ebbgpio_irq_handler(unsigned int irq, void *dev_id, struct pt_regs *regs); /** @brief 可加载内核模块初始化还书 * static 关键字将本函数的可见范围限制在本 C 文件中。 * __init 宏表示当该函数用于内置驱动(非可加载内核模块)时,只在初始化时候使用, * 在该阶段以后可以废弃并回收内存。在本例中, * 该函数设置通用输入输出接口和中断请求。 * @return 如果成功返回 0 */ static int __init ebbgpio_init(void){ int result = 0; printk(KERN_INFO "GPIO_TEST: Initializing the GPIO_TEST LKM\n"); // 通用输入输出接口号是否有效?(例如 BBB 有 4x32 个,但不都是有效的) if (!gpio_is_valid(gpioLED)){ printk(KERN_INFO "GPIO_TEST: invalid LED GPIO\n"); return -ENODEV; } // 开始设置发光二极管。它使用通用输入输出接口输出模式,默认会开启。 ledOn = true; gpio_request(gpioLED, "sysfs"); // gpioLED 变量被硬编码为 49,请求它 gpio_direction_output(gpioLED, ledOn); // 设置通用输入输出接口为输出默认,并且开启 // gpio_set_value(gpioLED, ledOn); // 因为上面已经设置过,这里不需要设置(仅作为参照) gpio_export(gpioLED, false); // 使得 gpio49 在 /sys/class/gpio 目录中显示 // 布尔参数防止方向被改变 gpio_request(gpioButton, "sysfs"); // 设置 gpioButton 变量 gpio_direction_input(gpioButton); // 设置按键的接口为输入 gpio_set_debounce(gpioButton, 200); // 设置按键去抖动时间延迟为 200 毫秒 gpio_export(gpioButton, false); // 使得 gpio115 在 /sys/class/gpio 目录中显示 // 布尔参数防止方向被改变 // 在可加载内核模块加载时做一个简单的测试,查看按键是否工作 printk(KERN_INFO "GPIO_TEST: The button state is currently: %d\n", gpio_get_value(gpioButton)); // 通用输入输出号和中断请求号不同!该函数完成二者的映射 irqNumber = gpio_to_irq(gpioButton); printk(KERN_INFO "GPIO_TEST: The button is mapped to IRQ: %d\n", irqNumber); // 下一个调用请求中断线路 result = request_irq(irqNumber, // 请求的中断号 (irq_handler_t) ebbgpio_irq_handler, // 指向下面介绍的中断处理函数的指针 IRQF_TRIGGER_RISING, // 在上升沿中断(按键按下时,非释放时) "ebb_gpio_handler", // 在 /proc/interrupts 识别中断拥有者 NULL); // 共享中断线路的设备 ID 指针,这里为 NULL 即可 printk(KERN_INFO "GPIO_TEST: The interrupt request result is: %d\n", result); return result; } /** @brief 可加载内核模块清理函数 * 和初始化函数类似,它是静态的。__exit 宏表示了如果这个代码用于内置驱动(非可加载内核模块),该函数是不需要的。 * 这里主要用于释放通用输入输出接口和展示清理消息。 */ static void __exit ebbgpio_exit(void){ printk(KERN_INFO "GPIO_TEST: The button state is currently: %d\n", gpio_get_value(gpioButton)); printk(KERN_INFO "GPIO_TEST: The button was pressed %d times\n", numberPresses); gpio_set_value(gpioLED, 0); // 关闭发光二极管,明确该设备将卸载 gpio_unexport(gpioLED); // 取消导出发光二极管通用输入输出接口 free_irq(irqNumber, NULL); // 释放中断请求号,在本例中不需要 *dev_id gpio_unexport(gpioButton); // 取消导出按键通用输入输出接口 gpio_free(gpioLED); // 释放发光二极管通用输入输出接口 gpio_free(gpioButton); // 释放按键通用输入输出接口 printk(KERN_INFO "GPIO_TEST: Goodbye from the LKM!\n"); } /** @brief 通用输入输出接口中断请求处理器 * 该函数是一个自定义的中断处理函数,关联到前面创建的通用输入输出接口上。 * 由于中断线路被屏蔽,相同的中断处理器无法并行执行,直到该函数执行完成。 * 该函数是静态的,确保它无法被文件外的函数调用到。 * @param irq 关联到通用输入输出接口的中断请求号,对于日志记录非常有用。 * @param dev_id 提供的 *dev_id,可以用于标记哪个设备引起这这个中断, * 本例中没有使用,传入 NULL。 * @param regs h/w 特殊寄存器的值,仅用于调试。 * return returns 如果成功返回 IRQ_HANDLED,其他结果返回 IRQ_NONE */ static irq_handler_t ebbgpio_irq_handler(unsigned int irq, void *dev_id, struct pt_regs *regs){ ledOn = !ledOn; // 在每次按键按下的时候反转发光二极管状态 gpio_set_value(gpioLED, ledOn); // 设置相应的实体发光二极管 printk(KERN_INFO "GPIO_TEST: Interrupt! (button state is %d)\n", gpio_get_value(gpioButton)); numberPresses++; // 全局计数器,当模块卸载的时候会输出 return (irq_handler_t) IRQ_HANDLED; // 宣布中断请求被成功处理 } /// 下面两个函数是强制性的,他们标识了初始化函数和清理函数(前面介绍过) module_init(ebbgpio_init); module_exit(ebbgpio_exit);
列表 2:通用输入输出接口发光二极管和按键测试内核模块源码
列表 2 中描述的可加载内核模块,和前文一样,可以通过如下步骤构建和加载:
molloyd@beaglebone:~/exploringBB/extras/kernel/gpio_test$ make make -C /lib/modules/3.8.13-bone70/build/ M=/home/molloyd/exploringBB/extras/kernel/gpio_test modules … molloyd@beaglebone:~/exploringBB/extras/kernel/gpio_test$ ls -l *.ko -rw-r--r-- 1 molloyd molloyd 5898 Apr 19 18:04 gpio_test.ko molloyd@beaglebone:~/exploringBB/extras/kernel/gpio_test$ sudo insmod gpio_test.ko molloyd@beaglebone:~/exploringBB/extras/kernel/gpio_test$ lsmod Module Size Used by gpio_test 1379 0
此时,kern.log文件给出了如下内核日志输出:
root@beaglebone:/var/log# tail -f kern.log Apr 19 18:25:23 beaglebone kernel: [174918.090753] GPIO_TEST: Initializing the GPIO_TEST LKM Apr 19 18:25:23 beaglebone kernel: [174918.094457] GPIO_TEST: The button state is currently: 0 Apr 19 18:25:23 beaglebone kernel: [174918.094472] GPIO_TEST: The button is mapped to IRQ: 243 Apr 19 18:25:23 beaglebone kernel: [174918.094625] GPIO_TEST: The interrupt request result is: 0
然后当按照图 1 方式连接的物理瞬时开关按下时,内核日志立即有一下反应:
Apr 19 18:29:21 beaglebone kernel: [175156.710964] GPIO_TEST: Interrupt! (button state is 1) Apr 19 18:29:21 beaglebone kernel: [175156.838235] GPIO_TEST: Interrupt! (button state is 1) Apr 19 18:29:23 beaglebone kernel: [175157.907436] GPIO_TEST: Interrupt! (button state is 1) Apr 19 18:29:23 beaglebone kernel: [175158.020004] GPIO_TEST: Interrupt! (button state is 1)
出于兴趣,此时可以查看 /proc/interrupts 文件项,可以看见有一个名为“ebb_gpio_handler”的中断处理器,它和列表 2 代码中配置的相同。同时也能看见关联通用输入输出接口的中断号为 243,和前面看见的内核日志输出可以对的上。
molloyd@beaglebone:~/exploringBB/extras/kernel/gpio_test$ cd /proc/ molloyd@beaglebone:/proc$ cat interrupts CPU0 … 42: 27721 INTC 4a100000.ethernet 43: 0 INTC 4a100000.ethernet 64: 0 INTC mmc0 67: 2052095 INTC gp_timer 70: 1116 INTC 44e0b000.i2c 72: 8 INTC 75: 0 INTC rtc0 76: 0 INTC rtc0 109: 0 INTC 53100000.sham 134: 0 GPIO mmc0 243: 45 GPIO ebb_gpio_handler …
再次强调,需要特别注意中断号不是通用输入输出接口号,图 1 电路图中的按键端口号为 115,发光二极管端口号为 49。在列表 2 代码中还可以发现,通用输入输出号使用通用输入输出函数导出(当内核模块卸载时,通用输入输出接口会自动取消导出):
molloyd@beaglebone:~/exploringBB/extras/kernel$ ls -l /sys/class/gpio/ total 0 --w------- 1 root root 4096 Jan 1 2000 export lrwxrwxrwx 1 root root 0 Apr 22 21:44 gpio115 -> ../../devices/virtual/gpio/gpio115 lrwxrwxrwx 1 root root 0 Apr 22 21:44 gpio49 -> ../../devices/virtual/gpio/gpio49 lrwxrwxrwx 1 root root 0 Jan 1 2000 gpiochip0 -> ../../devices/virtual/gpio/gpiochip0 lrwxrwxrwx 1 root root 0 Jan 1 2000 gpiochip32 -> ../../devices/virtual/gpio/gpiochip32 lrwxrwxrwx 1 root root 0 Jan 1 2000 gpiochip64 -> ../../devices/virtual/gpio/gpiochip64 lrwxrwxrwx 1 root root 0 Jan 1 2000 gpiochip96 -> ../../devices/virtual/gpio/gpiochip96 --w------- 1 root root 4096 Jan 1 2000 unexport
当模块使用 sudo rmmod gpio_test 命令卸载时,有以下日志输出:
Apr 19 18:29:35 beaglebone kernel: [175170.252260] GPIO_TEST: The button state is currently: 0 Apr 19 18:29:35 beaglebone kernel: [175170.252278] GPIO_TEST: The button was pressed 4 times Apr 19 18:29:35 beaglebone kernel: [175170.256753] GPIO_TEST: Goodbye from the LKM!
性能
这个可加载内核模块最有用的特性之一是可以整体评估响应时间(中断延迟时间)。瞬时开关按下结果是发光二极管状态反转,即当按键按下时如果发光二极管是关闭的,那么它将会打开。为了测量这个延时,示波器被使用,它被配置成在按键信号的上升沿触发。示波器提供了独立的时间测量,它的输出展示在图 2 中。
(点击放大图像)
图2:该可加载内核模块的性能,按键按下发光二极管状态改变
绿色的信号表示按键信号而蓝色信号表示发光二极管响应信号。在图中延时大约在17 微秒。重复该测试,延时变化在最小15 微秒,最大25 微秒之间。BeagleBone 上使用的镜像是普通的Debian 发行版,如果使用实时Linux 内核(如Xenomai),这一结果将会更好和偏差将会更小。
按键开关去抖动
按键的机械性质意味着,当按键按下时,按键接触将会触动开关触点,接通电路,然后会有微小的反弹,瞬间断开电路。这种机械反弹可能引起错误脉冲,引发按键误按。去抖动就是用于描述去除这种错误脉冲过程的术语。值得注意的是,本例中的按键去抖动采用软件实现。代码使用gpio_set_bounce() 函数,一旦检测到边沿转换,在给定的时间段内(通常是100 毫秒到200 毫秒)忽略重复的边沿转换。如果希望在“干净”的数字信号上使用代码检测多重边沿变化,应该移除gpio_set_debounce() 函数,因为软件去抖动将会严重影响性能。YouTube 视频(参见视频2)描述了一种对机械开关去抖动的硬件实现。
https://youtu.be/tmjuLtiAsc0
视频 2:来自 YouTube 频道的瞬时开关去抖动
示例 2:增强的按键通用输入输出接口驱动
示例 2 基于示例 1 构建,创建了一个增强的通用输入输出接口驱动,它能够使用 sysfs 让用户配置并和通用输入输出接口上的按键进行交互。该模块允许通用输入输出按键映射到 Linux 用户空间,因此用户可以和它直接交互。以下示例是解释该功能的最好方式,在示例中,按键被连接到通用输入输出 115 接口,可以通过以下方式访问和操作:
molloyd@beaglebone:/sys/ebb/gpio115$ ls -l total 0 -r--r--r-- 1 root root 4096 Apr 24 11:52 diffTime -rw-rw-rw- 1 root root 4096 Apr 24 11:54 isDebounce -r--r--r-- 1 root root 4096 Apr 24 11:52 lastTime -r--r--r-- 1 root root 4096 Apr 24 11:52 ledOn -rw-rw-rw- 1 root root 4096 Apr 24 11:52 numberPresses molloyd@beaglebone:/sys/ebb/gpio115$ cat numberPresses 55 molloyd@beaglebone:/sys/ebb/gpio115$ cat ledOn 0 molloyd@beaglebone:/sys/ebb/gpio115$ cat lastTime 10:54:48:487088428 molloyd@beaglebone:/sys/ebb/gpio115$ cat diffTime 0.628361750 molloyd@beaglebone:/sys/ebb/gpio115$ echo 0 > isDebounce molloyd@beaglebone:/sys/ebb/gpio115$ cat isDebounce 0
尽管创建该可加载内核模块有一些复杂性,但是用户接口非常简单,并且能够通过任何编程语言编写的可执行程序在嵌入式系统中使用。
sysfs 是一个基于内存的文件系统,它提供了将内核数据结构、属性导出并链接到 Linux 用户空间的机制。本文前面的章节“通用输入输出接口和内核”提供了一个如何操作/sys/class/gpio文件项的示例,它用于操作内核数据结构来开启和关闭发光二极管。使 sysfs 发挥作用的基础设施,主要基于 kboject 接口。
kobject 接口
Linux 中的驱动模型,使用了 kobject 抽象,为了理解这个模型,必须先领悟下面这些重要的概念【摘自 Greg Kroah-Hartman 的指南】:
- kobject:kobject 是一个由名称、引用计数、类型、sysfs 中的表示项和指向父对象的指针构成的结构体(参见下面的列表 3)。重要的是,kobjects 自身并不是很有用,有用的是嵌入在它们结构内的其他数据结构,用于控制访问。这和面向对象概念中的通用顶层父类(例如 Java 中的 Object 类,或者 Qt 中的 QObject 类)。
- ktype:ktype 是嵌入在 kobject 结构体内的一个对象类型。它控制对象创建和销毁时的行为。
- kset:kset 是 kboject 的集合,可以有不同的 ktype 类型。kobject 集合可以被人为 sysfs 目录中包含的子目录集合(kobject)。
#define KOBJ_NAME_LEN 20 struct kobject { char *k_name; // kobject name pointer (must not be NULL) char name[KOBJ_NAME_LEN]; // short kobject internal name data (can kmalloc() longer names) struct kref kref; // the reference count struct list_head entry; // circularly linked list to members of the kset struct kobject *parent; // the parent kobject struct kset *kset; // kobject can be a member of a set (otherwise NULL) struct kobj_type *ktype; // kobj_type is a struct that describes the type of the object struct dentry *dentry; // the sysfs directory entry };
列表 3: kobject 结构体
在此示例中,只需要一个 kobject 对象,它在文件系统中被映射到/sys/ebb。单个 kobject 包含了上述示例中的所有交互需要的属性(例如查看numberPresses项)。这在列表 4 中,通过使用 kobject_create_and_add() 函数实现,过程如下:
static struct kobject *ebb_kobj; ebb_kobj = kobject_create_and_add("ebb", kernel_kobj->parent);
kernel_kobj 指针提供了 **/sys/kernel/的引用。如果去掉了调用中的 ->parent 内容,那么ebb文件项将会创建在/sys/kernel/ebb,但是为了清晰,这里放置在了/sys/ebb**,这不是最佳实践!(另外,sysfs_create_dir() 函数提供了相同的功能。)
当 sysfs 属性被读取或者写入的时候,_show 和 _store 函数将会被分别调用。sysfs.h 头文件定义了以下帮助宏,使得定义属性会更加方便。
- __ATTR(_name,_mode,_show,_store):长内容版本。必须传递属性变量名称 _name,访问模式 (mode(例如 0666 表示读写权限),指向展示函数指针 _show 和指向保存函数指针 _store。
- __ATTR_RO(_name):简化版本,创建只读属性的宏。必须传递属性变量名 _name,宏将设置访问权限 _mode 变量为 0444(只读),并且设置展示函数名称为 _name_show。
- __ATTR_WO(_name):和 __ATTR_RW(_name),创建只写和读写属性。在内核 3.8.x 版本中无效,在 3.11.x 版本中添加。
示例的源码
列表 4 提供了增强的按键通用输入输出接口内核模块完整的源码。它看上去很长,因为添加了很多注释和额外的 printk() 函数调用,以方便在代码运行时查看正在发生的事情。该示例基于列表 2 中的代码构建,它也包含了发光二极管,让开发者能够观察到电路自身的互动。
/** * @file button.c * @author Derek Molloy * @date 2015 年 4 月 19 日 * @brief 控制连接到通用输入输出接口的按键(或任何信号)的内核模块 * 它包含对中断和 sysfs 项的全部支持,因此可以从 Linux 用户空间 * 创建和按键的接口,或者配置按键。 * sysfs 文件项在 /sys/ebb/gpio115 目录中 * @see http://www.derekmolloy.ie/ */ #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/gpio.h> // 通用输入输出接口函数需要 #include <linux/interrupt.h> // 中断请求代码需要 #include <linux/kobject.h> // 使用 kobject 和 sysfs 进行绑定 #include <linux/time.h> // 使用时钟来度量两次按键按下的间隔时间 #define DEBOUNCE_TIME 200 ///< 默认抖动时间:200 毫秒 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Derek Molloy"); MODULE_DESCRIPTION("A simple Linux GPIO Button LKM for the BBB"); MODULE_VERSION("0.1"); static bool isRising = 1; ///< 上升沿作为默认中断请求属性 module_param(isRising, bool, S_IRUGO); ///< 参数描述。S_IRUGO 表示可以读取,但不能修改。 MODULE_PARM_DESC(isRising, " Rising edge = 1 (default), Falling edge = 0"); ///< 参数描述 static unsigned int gpioButton = 115; ///< 默认通用输入输出接口号为 115 module_param(gpioButton, uint, S_IRUGO); ///< 参数描述,S_IRUGO 表示可以读取,但是不能修改。 MODULE_PARM_DESC(gpioButton, " GPIO Button number (default=115)"); ///< 参数描述 static unsigned int gpioLED = 49; ///< 默认通用输入输出端口号为 49 module_param(gpioLED, uint, S_IRUGO); ///< 参数描述,S_IRUGO 表示可以读取,但是不能修改 MODULE_PARM_DESC(gpioLED, " GPIO LED number (default=49)"); ///< 参数描述 static char gpioName[8] = "gpioXXX"; ///< 以 NULL 结尾的默认字符串,以防万一 static int irqNumber; ///< 用于在文件中共享中断请求号 static int numberPresses = 0; ///< 按下次数信息,保存按键按下的次数 static bool ledOn = 0; ///< 发光二极管开启还是关闭?用于反转它的状态(默认关闭) static bool isDebounce = 1; ///< 用于保存去抖动状态(默认开启) static struct timespec ts_last, ts_current, ts_diff; ///< linux/time.h 头文件提供的 timespecs 结构体(有纳秒精度) /// 自定义中断请求处理函数的函数原型,参见下面的实现 static irq_handler_t ebbgpio_irq_handler(unsigned int irq, void *dev_id, struct pt_regs *regs); /** @brief 输出 numberPresses 变量的回调函数 * @param kobj 代表显示在 sysfs 文件系统中的内核对象设备 * @param attr 指向 kobj_attribute 结构体的指针 * @param buf 用于写入按下次数的缓冲区 * @return 返回写入到缓冲区的总字符数(不包括 null) */ static ssize_t numberPresses_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf){ return sprintf(buf, "%d\n", numberPresses); } /** @brief 读入 numberPresses 变量的回调函数 * @param kobj 表示显示在 sysfs 文件系统中的内核对象设备 * @param attr 指向 kobj_attribute 结构体的指针 * @param buf 读取按下次数的缓冲区(重置为 0) * @param count 缓冲区中的字符数量 * @return return 应该返回缓冲区中使用的字符数 */ static ssize_t numberPresses_store(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count){ sscanf(buf, "%du", &numberPresses); return count; } /** @brief 展示发光二极管是开启还是关闭 */ static ssize_t ledOn_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf){ return sprintf(buf, "%d\n", ledOn); } /** @brief 展示按键最后一次按下的时间,手动输出日期(没有本地化) */ static ssize_t lastTime_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf){ return sprintf(buf, "%.2lu:%.2lu:%.2lu:%.9lu \n", (ts_last.tv_sec/3600)%24, (ts_last.tv_sec/60) % 60, ts_last.tv_sec % 60, ts_last.tv_nsec ); } /** @brief 以秒. 纳秒的格式展示时间差,最多展示 9 位 */ static ssize_t diffTime_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf){ return sprintf(buf, "%lu.%.9lu\n", ts_diff.tv_sec, ts_diff.tv_nsec); } /** @brief 展示按键去抖动功能是否开启 */ static ssize_t isDebounce_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf){ return sprintf(buf, "%d\n", isDebounce); } /** @brief 保存和设置去抖动状态 */ static ssize_t isDebounce_store(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count){ unsigned int temp; sscanf(buf, "%du", &temp); // 使用临时变量,确保整型到布尔类型转换正确 gpio_set_debounce(gpioButton,0); isDebounce = temp; if(isDebounce) { gpio_set_debounce(gpioButton, DEBOUNCE_TIME); printk(KERN_INFO "EBB Button: Debounce on\n"); } else { gpio_set_debounce(gpioButton, 0); // 设置去抖动时间为 0 printk(KERN_INFO "EBB Button: Debounce off\n"); } return count; } /** 使用帮助宏来定义 kobj_attribute 的名称和访问权限 * kobj_attribute 结构包含属性字段(名称和模式),展示和保存函数指针。 * count 变量关联到 numberPresses 变量,并通过 numberPresses_show 和 numberPresses_store 两个函数, * 以 0666(读写)权限暴露。 */ static struct kobj_attribute count_attr = __ATTR(numberPresses, 0666, numberPresses_show, numberPresses_store); static struct kobj_attribute debounce_attr = __ATTR(isDebounce, 0666, isDebounce_show, isDebounce_store); /** __ATTR_RO 宏定义了只读属性。使用该宏不需要定义展示回调函数,但是它必须存在。 * __ATTR_WO 宏可用于只写属性,但是只能用于 Linux 3.11.x 版本以上。 */ static struct kobj_attribute ledon_attr = __ATTR_RO(ledOn); ///< ledon kobject 属性 static struct kobj_attribute time_attr = __ATTR_RO(lastTime); ///< 最后一次按下时间 kobject 属性 static struct kobj_attribute diff_attr = __ATTR_RO(diffTime); ///< 时间差属性 /** ebb_attrs[] 是属性数组,用于创建一下属性组。 * The attr property of the kobj_attribute is used to extract the attribute struct */ static struct attribute *ebb_attrs[] = { &count_attr.attr, ///< 按键按下次数 &ledon_attr.attr, ///< 发光二极管开启或关闭 &time_attr.attr, ///< 最后一次按键按下时间,HH:MM:SS:NNNNNNNNN 格式 &diff_attr.attr, ///< 最后两次按下的时间差 &debounce_attr.attr, ///< 去抖动状态是否设置 NULL, }; /** 属性组结构使用这个属性数组和一个名称,它将暴露到 sysfs 中,在本例中 * 它会显示在 gpio115 目录中,这是由下面介绍的 ebbButton_init() 函数在模块加载时, * 使用传递给模块的自定义内核参数自动定义的。 */ static struct attribute_group attr_group = { .name = gpioName, ///< 名字在 ebbButton_init() 函数中生成 .attrs = ebb_attrs, ///< 上面定义的属性数组 }; static struct kobject *ebb_kobj; /** @brief 可加载内核模块初始化函数 * static 关键字限制了该函数的可见范围为此 C 文件内。__init 宏表示对于内置驱动(非可加载内核模块), * 本函数只在初始化时使用,之后将会废弃,内存将会回收。 * 在本例中,此函数设置通用输入输出接口和中断请求。 * @return 如果成功返回 0 */ static int __init ebbButton_init(void){ int result = 0; unsigned long IRQflags = IRQF_TRIGGER_RISING; // 默认使用上升沿中断 printk(KERN_INFO "EBB Button: Initializing the EBB Button LKM\n"); sprintf(gpioName, "gpio%d", gpioButton); // 为 /sys/ebb/gpio115 创建 gpio115 这个名称 // 在 /sys/ebb 中创建 kobject 的 sysfs 文件项,这可能不是一个立项的路径! ebb_kobj = kobject_create_and_add("ebb", kernel_kobj->parent); // kernel_kobj 指向 /sys/kernel if(!ebb_kobj){ printk(KERN_ALERT "EBB Button: failed to create kobject mapping\n"); return -ENOMEM; } // 给 /sys/ebb/ 文件项添加属性,比如 /sys/ebb/gpio115/numberPresses result = sysfs_create_group(ebb_kobj, &attr_group); if(result) { printk(KERN_ALERT "EBB Button: failed to create sysfs group\n"); kobject_put(ebb_kobj); // 清理,移除 kobject 在 sysfs 中的文件项 return result; } getnstimeofday(&ts_last); // 将最后按下时间设置为当前时间 ts_diff = timespec_sub(ts_last, ts_last); // 设置初始时间差为 0 // 开始设置发光二极管。它使用通用输入输出接口输出模式,默认开启。 ledOn = true; gpio_request(gpioLED, "sysfs"); // gpioLED 变量硬编码为 49,请求它 gpio_direction_output(gpioLED, ledOn); // 设置通用输入输出接口为输出模式,开启发光二极管 // gpio_set_value(gpioLED, ledOn); // 不需要设置,上一行代码已经设置(这里作为参考) gpio_export(gpioLED, false); // 使得 gpio49 显示在 /sys/class/gpio 目录中 // 布尔类型参数阻止方向被反转 gpio_request(gpioButton, "sysfs"); // 设置 gpioButton 变量 gpio_direction_input(gpioButton); // 设置按键的通用输入输出接口为输入模式 gpio_set_debounce(gpioButton, DEBOUNCE_TIME); // 设置按键去抖动演示为 200 毫秒 gpio_export(gpioButton, false); // 使得 gpio115 显示在 /sys/class/gpio 目录中 // 布尔类型参数阻止方向被反转 // 在可加载内核模块加载的时候做一个简单测试,查看按键工作和期望是否一致。 printk(KERN_INFO "EBB Button: The button state is currently: %d\n", gpio_get_value(gpioButton)); /// 通用输入输出接口号和中断请求号不相同!此函数完成二者映射。 irqNumber = gpio_to_irq(gpioButton); printk(KERN_INFO "EBB Button: The button is mapped to IRQ: %d\n", irqNumber); if(!isRising){ // 如果内核参数 isRising=0 提供了 IRQflags = IRQF_TRIGGER_FALLING; // 设置在下降沿中断 } // 下一个函数调用请求中断线路 result = request_irq(irqNumber, // 请求的中断号 (irq_handler_t) ebbgpio_irq_handler, // 指向下面中断处理函数的指针 IRQflags, // 使用自定义内核参数来设置中断类型 "ebb_button_handler", // 用于在 /proc/interrupts 中标识所有者 NULL); // 为了共享中断线路使用的 *dev_id,这里使用 NULL 是可以的 return result; } /** @brief 可加载内核模块清理函数 * 和初始化函数雷系,它是静态的。__exit 宏表示如果该代码用于内置驱动(非可加载内核模块), * 该函数是不需要的。 */ static void __exit ebbButton_exit(void){ printk(KERN_INFO "EBB Button: The button was pressed %d times\n", numberPresses); kobject_put(ebb_kobj); // 清理,移除 kobject 对象在 sysfs 中的文件项 gpio_set_value(gpioLED, 0); // 关闭发光二极管,明确该设备将被卸载 gpio_unexport(gpioLED); // 取消导出发光二极管的通用输入输出接口 free_irq(irqNumber, NULL); // 释放中断请求号,这里不需要提供 *dev_id gpio_unexport(gpioButton); // 取消导出按键的通用输入输出接口 gpio_free(gpioLED); // 释放发光二极管通用输入输出接口 gpio_free(gpioButton); // 释放按键通用输入输出接口 printk(KERN_INFO "EBB Button: Goodbye from the EBB Button LKM!\n"); } /** @brief 通用输入输出接口中断请求处理函数 * 此函数是一个关联到上述通用输入输出接口的自定义中断处理器。 * 由于进行了屏蔽,相同的中断处理器无法同时调用,直到函数执行结束。 * 此函数是静态的,因为它不应该在此文件外被直接调用。 * @param irq 关联到通用输入输出接口的中断请求号,对于日志记录非常有用 * @param dev_id 提供的 *dev_id,可以用于识别引起中断的设备。在本例中没有使用,传入 NULL。 * @param regs h/w 特殊寄存器的值,仅用于调试。 * return returns 如果成功返回 IRQ_HANDLED,否则返回 IRQ_NONE。 */ static irq_handler_t ebbgpio_irq_handler(unsigned int irq, void *dev_id, struct pt_regs *regs){ ledOn = !ledOn; // 在每次按键按下时反转发光二极管状态 gpio_set_value(gpioLED, ledOn); // 设置响应物理发光二极管 getnstimeofday(&ts_current); // 获取当前时间 ts_current ts_diff = timespec_sub(ts_current, ts_last); // 确定两次按下的时间差 ts_last = ts_current; // 将当前时间保存为最后一次按下时间 ts_last printk(KERN_INFO "EBB Button: The button state is currently: %d\n", gpio_get_value(gpioButton)); numberPresses++; // 全局计数器,将在模块卸载时输出 return (irq_handler_t) IRQ_HANDLED; // 宣告中断请求正确处理 } /// 下面两个函数是强制性的,他们标识了初始化函数和清理函数(前面介绍过) module_init(ebbButton_init); module_exit(ebbButton_exit);
列表 4:增强的通用输入输出接口按键内核模块
列表 4 中的代码通过注释描述。然而,还有一些点值得提一下:
- 当可加载内核模块在加载时,配置了三个模块参数(isRising、gpioButton 和 gpioLED)。内核模块参数的使用在本系列的第一篇文章中进行了介绍。这允许开发者为按键输入和发光二极管输出定义不同的通用输入输出接口,它们在 sysfs 上挂载名称会自动调整。代码允许将默认的上升沿中断替换为下降沿中断。
- 有 5 个属性关联到了 kobject 项(ebb)上。它们是:diffTime、isDebounce、lastTime、ledOn和numberPresses。除了isDebounce(即上升沿或者下降沿)和numberPresses(能够被设置成任何值,比如重置为 0)之外,所有的属性都是只读的。
- ebbgpio_irq_handler() 函数执行了主要的计时工作。当每次中断处理的时候,时钟时间被保存起来,两次按下时间被确定。
该模块可以通过以下方式以下降沿模式加载并测试:
molloyd@beaglebone:~/exploringBB/extras/kernel/button$ make … molloyd@beaglebone:~/exploringBB/extras/kernel/button$ ls -l *.ko -rw-r--r-- 1 molloyd molloyd 10639 Apr 24 11:50 button.ko molloyd@beaglebone:~/exploringBB/extras/kernel/button$ sudo insmod button.ko isRising=0 molloyd@beaglebone:~/exploringBB/extras/kernel/button$ cd /sys/ebb/gpio115/ molloyd@beaglebone:/sys/ebb/gpio115$ ls diffTime isDebounce lastTime ledOn numberPresses molloyd@beaglebone:/sys/ebb/gpio115$ cat numberPresses 4 molloyd@beaglebone:/sys/ebb/gpio115$ cat ledOn 1 molloyd@beaglebone:/sys/ebb/gpio115$ cat lastTime 16:18:04:249265532 molloyd@beaglebone:/sys/ebb/gpio115$ cat diffTime 1.126586000 molloyd@beaglebone:/sys/ebb/gpio115$ echo 0 > numberPresses molloyd@beaglebone:/sys/ebb/gpio115$ cat numberPresses 0 molloyd@beaglebone:/sys/ebb/gpio115$ cd ~/exploringBB/extras/kernel/button/ molloyd@beaglebone:~/exploringBB/extras/kernel/button$ sudo rmmod button
同时内核日志(/var/log/kern.log)的输出为:
Apr 24 17:17:25 beaglebone kernel: [19844.622090] EBB Button: Initializing the EBB Button LKM Apr 24 17:17:25 beaglebone kernel: [19844.625986] EBB Button: The button state is currently: 0 Apr 24 17:17:25 beaglebone kernel: [19844.626002] EBB Button: The button is mapped to IRQ: 243 Apr 24 17:18:01 beaglebone kernel: [19879.901265] EBB Button: The button state is currently: 0 Apr 24 17:18:02 beaglebone kernel: [19880.878163] EBB Button: The button state is currently: 0 Apr 24 17:18:03 beaglebone kernel: [19881.778028] EBB Button: The button state is currently: 0 Apr 24 17:18:04 beaglebone kernel: [19882.904615] EBB Button: The button state is currently: 0 Apr 24 17:18:49 beaglebone kernel: [19928.127631] EBB Button: The button was pressed 0 times Apr 24 17:18:49 beaglebone kernel: [19928.132038] EBB Button: Goodbye from the EBB Button LKM!
请注意日志显示按钮被按下了 0 次。这是因为在上面的 Linux 终端中执行了 echo 0 > numberPresses 命令。
下面是关于这一主题重要的扩展阅读:
注意:确保在卸载内核模块的时候已经离开了 **/sys/ebb目录,否则卸载后再执行类似ls** 命令的时候,将会引发内核崩溃(kernel panic)。
示例 3:增强的发光二极管通用输入输出接口驱动内核模块
本文最后一个示例是一个通过可编程内核模块控制发光二极管的驱动。这个示例目的是介绍内核线程(kthread)的使用,我们可以针对发生在可加载内核模块中的事件开始。
内核线程
列表 5 提供了本例中的代码大致结构。这是在 Linux 内核中相当不寻常的线程,因为我们需要明确的睡眠时间,以获取一个稳定的闪烁间隔。给内核线程调度器返回的资源,通常通过 schedule() 函数完成。
对于 kthread_run() 函数的调用和用户空间 pthread 库的 pthread_create()(参见书的第 236 页)函数类似。kthread_run() 函数需要一个指向线程函数(本例中的 flash() 函数)指针,它将显示在 top 或者 ps 命令中。kthread_run() 函数返回一个 task_struct 结构体,它在这个 C 文件中作为 *task 指针,被很多函数共享。
#include <linux/kthread.h> static struct task_struct *task; // 线程任务结构体指针 static int flash(void *arg){ … while(!kthread_should_stop()){ // 当 kthread_stop() 或等效的函数被调用时返回 true set_current_state(TASK_RUNNING); // 防止意外的临时休眠(只是一个示例) … // 执行状态改变指令(例如闪烁) set_current_state(TASK_INTERRUPTIBLE); // 开始休眠,但是在需要时能够被唤醒 msleep(…); // 毫秒级休眠 } … } static int __init ebbLED_init(void){ task = kthread_run(flash, NULL, "LED_flash_thread"); // 开始发光二极管闪烁内核线程 … } static void __exit ebbLED_exit(void){ kthread_stop(task); // 停止发光二极管闪烁内核线程 … }
列表 5:内核线程的概要实现
此可加载内核模块的最终代码在列表 6 中。
/** * @file led.c * @author Derek Molloy * @date 2015 年 4 月 19 日 * @brief 控制连接到通用输入输出接口的简单发光二极管(或者其他任何信号)的内核模块。 * 为了使发光二极管能够闪烁,本模块使用了线程。 * sysfs 文件项显示在 /sys/ebb/led49 * @see http://www.derekmolloy.ie/ */ #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/gpio.h> // 通用输入输出接口函数需要 #include <linux/kobject.h> // 使用 kobject 绑定到 sysfs #include <linux/kthread.h> // 使用内核线程来实现闪烁功能 #include <linux/delay.h> // 使用该头文件以使用 msleep() 函数 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Derek Molloy"); MODULE_DESCRIPTION("A simple Linux LED driver LKM for the BBB"); MODULE_VERSION("0.1"); static unsigned int gpioLED = 49; ///< 发光二极管默认的通用输入输出接口为 49 module_param(gpioLED, uint, S_IRUGO); ///< 参数描述,S_IRUGO 表示参数可以被读取,不能被修改 MODULE_PARM_DESC(gpioLED, " GPIO LED number (default=49)"); ///< 参数描述 static unsigned int blinkPeriod = 1000; ///< 以毫秒为单位的闪烁间隔 module_param(blinkPeriod, uint, S_IRUGO); ///< 参数描述,S_IRUGO 表示参数可以被读取,不能被修改 MODULE_PARM_DESC(blinkPeriod, " LED blink period in ms (min=1, default=1000, max=10000)"); static char ledName[7] = "ledXXX"; ///< 以防万一,使用以 NULL 结尾的默认字符串 static bool ledOn = 0; ///< 发光二极管是关闭的还是开启的?用于闪烁 enum modes { OFF, ON, FLASH }; ///< 发光二极管可选模式,static 关键字在这里没有用 static enum modes mode = FLASH; ///< 默认模式是闪烁 /** @brief 显示发光二极管状态的回调函数 * @param kobj 代表一个显示在 sysfs 文件系统中的内核对象设备 * @param attr 指向 kobj_attribute 结构体的指针 * @param buf 写入发光二极管状态的缓冲区 * @return 返回成功展示的状态字符串的字符个数 */ static ssize_t mode_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf){ switch(mode){ case OFF: return sprintf(buf, "off\n"); // 展示状态,简单的方法 case ON: return sprintf(buf, "on\n"); case FLASH: return sprintf(buf, "flash\n"); default: return sprintf(buf, "LKM Error\n"); // 不可能到这里 } } /** @brief 保存发光二极管模式(使用上述枚举类型表示)的回调函数 */ static ssize_t mode_store(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count){ // count-1 表达式是非常重要的,否则换行符\n 也会在比较中被使用 if (strncmp(buf,"on",count-1)==0) { mode = ON; } // strncmp() 函数比较固定数量的字符 else if (strncmp(buf,"off",count-1)==0) { mode = OFF; } else if (strncmp(buf,"flash",count-1)==0) { mode = FLASH; } return count; } /** @brief 展示发光二极管闪烁周期的回调函数 */ static ssize_t period_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf){ return sprintf(buf, "%d\n", blinkPeriod); } /** @brief 保存发光二极管闪烁周期的回调函数 */ static ssize_t period_store(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count){ unsigned int period; // 使用变量来校验发送的数据 sscanf(buf, "%du", &period); // 使用 unsigned int 类型读入时间段 if ((period>1)&&(period<=10000)){ // 必须大于等于 2 毫秒,小于等于 10 秒 blinkPeriod = period; // 在范围内,赋值给 blinkPeriod 变量 } return period; } /** 使用帮助宏来定义 kobj_attribute 的名称和访问级别。 * kobj_attribute 有属性字段(名称和模式),展示和保存函数指针构成。 * 周期变量被关联到 blinkPeriod 变量,并且以 0666(读写)模式,使用上述 period_show、period_store 函数暴露出去。 */ static struct kobj_attribute period_attr = __ATTR(blinkPeriod, 0666, period_show, period_store); static struct kobj_attribute mode_attr = __ATTR(mode, 0666, mode_show, mode_store); /** ebb_attrs[] 是属性的数组,用于创建下面的属性组。 * kobj_attribute 结构的 attr 字段用于抽取结构的属性。 */ static struct attribute *ebb_attrs[] = { &period_attr.attr, // 发光二极管闪烁的周期 &mode_attr.attr, // 发光二极管模式(关闭、开启、闪烁) NULL, }; /** 属性组使用属性数组和名称两个字段,将它们暴露到 sysfs 文件系统中。 * 在本例中是 gpio49 目录。它在下面介绍的 ebbLED_init() 函数中,通过模块加载时传递的自定义模块参数自动定义。 */ static struct attribute_group attr_group = { .name = ledName, // 名称在 ebbLED_init() 函数中生成 .attrs = ebb_attrs, // 属性数组在上面定义 }; static struct kobject *ebb_kobj; /// 指向 kobject 对象的指针 static struct task_struct *task; /// 指向线程任务对象的指针 /** @brief 发光二极管闪烁的主要内核线程循环 * * @param arg 用于将数据传递如线程的无类型(void) * @return 如果成功返回 0 */ static int flash(void *arg){ printk(KERN_INFO "EBB LED: Thread has started running \n"); while(!kthread_should_stop()){ // 当 kthread_stop() 函数调用的时候返回 true set_current_state(TASK_RUNNING); if (mode==FLASH) ledOn = !ledOn; // 反转发光二极管状态 else if (mode==ON) ledOn = true; else ledOn = false; gpio_set_value(gpioLED, ledOn); // 使用发光二极管状态来点亮、关闭发光二极管 set_current_state(TASK_INTERRUPTIBLE); msleep(blinkPeriod/2); // 毫秒级休眠,休眠时间为闪烁周期的一半 } printk(KERN_INFO "EBB LED: Thread has run to completion \n"); return 0; } /** @brief 可加载内核模块初始化函数 * static 关键字限制了本函数的可见范围为当前 C 文件。__init 宏表示对于内置驱动(非可加载内核模块), * 本函数只在初始化时候使用,在该时间点以后,可以被废弃,内存会被回收。 * 在本例中,本函数设置通用输入输出接口和中断请求。 * @return 如果成功返回 0 */ static int __init ebbLED_init(void){ int result = 0; printk(KERN_INFO "EBB LED: Initializing the EBB LED LKM\n"); sprintf(ledName, "led%d", gpioLED); // 为 /sys/ebb/led49 创建 gpio115 名字 ebb_kobj = kobject_create_and_add("ebb", kernel_kobj->parent); // kernel_kobj 对象指向 /sys/kernel if(!ebb_kobj){ printk(KERN_ALERT "EBB LED: failed to create kobject\n"); return -ENOMEM; } // 将属性添加到 /sys/ebb/ 目录中,例如 /sys/ebb/led49/ledOn result = sysfs_create_group(ebb_kobj, &attr_group); if(result) { printk(KERN_ALERT "EBB LED: failed to create sysfs group\n"); kobject_put(ebb_kobj); // 清理,移除 kobject 对象在 sysfs 文件系统中的文件项 return result; } ledOn = true; gpio_request(gpioLED, "sysfs"); // gpioLED 变量值默认是 49,请求它 gpio_direction_output(gpioLED, ledOn); // 设置通用输入输出接口为输出模式,并且开启 gpio_export(gpioLED, false); // 使得 gpio49 显示在 /sys/class/gpio 目录中 // 第二个参数阻止方向反转 task = kthread_run(flash, NULL, "LED_flash_thread"); // 开始发光二极管闪烁 if(IS_ERR(task)){ // 内核线程的名称为 LED_flash_thread printk(KERN_ALERT "EBB LED: failed to create the task\n"); return PTR_ERR(task); } return result; } /** @brief 可加载内核模块清理函数 * 和初始化函数类似,它是静态的。__exit 宏表示如果该代码用于内置驱动(非可加载内核模块),本函数是不需要的。 */ static void __exit ebbLED_exit(void){ kthread_stop(task); // 停止发光二极管闪烁线程 kobject_put(ebb_kobj); // 清理,移除 kobject 在 sysfs 文件系统中的文件项 gpio_set_value(gpioLED, 0); // 关闭发光二极管,明确设备已经被卸载 gpio_unexport(gpioLED); // 取消导出发光二极管的通用输入输出端口 gpio_free(gpioLED); // 释放发光二极管的通用输入输出接口 printk(KERN_INFO "EBB LED: Goodbye from the EBB LED LKM!\n"); } /// 下面两个函数是强制性的,他们标识了初始化函数和清理函数(前面介绍过) module_init(ebbLED_init); module_exit(ebbLED_exit);
列表 6:增强通用输入输出发光二极管控制器内核模块
列表 6 中的注释,提供了所有任务一体化的完整说明,然而,还有一些需要补充的点:
- 名为 modes 的枚举变量用于定义三种可能的运行状态。当传递命令到可加载内核模块的时候,必须非常小心处理这些数据,以确保他们是有效的,并且在允许范围之内。在本例中,字符串命令只能是三个值(“on”、“off”和“flash”)中的一个,而间隔值范围必须在 2 到 10,000 毫秒内。
- kthread_should_stop() 的返回值是一个布尔值。当类似 kthread_stop() 函数在内核线程中调用后,该函数会被唤醒,并且返回 true。这会导致内核线程运行完成之后,它从内核线程的返回值将会通过 kthread_stop() 函数返回。
该示例可以通过以下步骤构建和执行:
molloyd@beaglebone:~/exploringBB/extras/kernel/led$ make … molloyd@beaglebone:~/exploringBB/extras/kernel/led$ sudo insmod led.ko molloyd@beaglebone:~/exploringBB/extras/kernel/led$ cd /sys/ebb/led49/ molloyd@beaglebone:/sys/ebb/led49$ ls blinkPeriod mode molloyd@beaglebone:/sys/ebb/led49$ cat blinkPeriod 1000 molloyd@beaglebone:/sys/ebb/led49$ cat mode flash molloyd@beaglebone:/sys/ebb/led49$ echo 100 > blinkPeriod molloyd@beaglebone:/sys/ebb/led49$ ps aux|grep LED root 7042 0.0 0.0 0 0 ? D 18:36 0:00 [LED_flash_threa] molloyd 7062 0.0 0.1 3100 616 pts/0 S+ 18:37 0:00 grep LED
可以通过将休眠时间减少到 1 毫秒来提高闪烁频率,这样可以观察 CPU 的负载,操作如下:
molloyd@beaglebone:/sys/ebb/led49$ echo 2 > blinkPeriod molloyd@beaglebone:/sys/ebb/led49$ ps aux|grep LED root 7042 0.1 0.0 0 0 ? D 18:36 0:00 [LED_flash_threa] molloyd 7070 0.0 0.1 3100 616 pts/0 S+ 18:38 0:00 grep LED molloyd@beaglebone:/sys/ebb/led49$ echo off > mode molloyd@beaglebone:/sys/ebb/led49$ echo on > mode molloyd@beaglebone:/sys/ebb/led49$ cd ~/exploringBB/extras/kernel/led/ molloyd@beaglebone:~/exploringBB/extras/kernel/led$ sudo rmmod led
从输出可以看见,当闪烁的休眠周期为 1 毫秒的时候,线程负载非常小,CPU 占用率只有 0.1%。对于不同周期值的输出信号,详见在图 3 到图 6。
The kernel log output for this example is as follows:
本例的内核日志输出如下:
Apr 24 18:36:30 beaglebone kernel: [24588.981157] EBB LED: Initializing the EBB LED LKM Apr 24 18:36:30 beaglebone kernel: [24588.987821] EBB LED: Thread has started running Apr 24 18:40:57 beaglebone kernel: [24856.188934] EBB LED: Thread has run to completion Apr 24 18:40:57 beaglebone kernel: [24856.190471] EBB LED: Goodbye from the EBB LED LKM!
相比于 Linux 用户空间的类似测试,这种实现的结果相当可观的。结果有一致的约 50% 的占空比,并且频率的值的范围也相当的一致。例如,图 6 表示当休眠延时设置为 1 毫秒的时候,这种实现的性能,它的结果大约是每个周期 7.8 毫秒。频率范围在 127.935 赫兹到 128.07 赫兹之间,它的波动在 +/-0.05%。这种情况下,内核线程 CPU 使用率小于 0.1%。更高频率也是可能的,但是脉冲宽度将会有更大的波动,可能 BeagleBone 上的可编程实时单元子系统和工业通信子系统(Programmable Real-Time Unit Subsystem and Industrial Communication SubSystem,PRU-ICSS)更适合这样的任务。
(点击放大图像)
图3:50 毫秒休眠延迟的发光二极管闪烁模块的性能
(点击放大图像)
图4:25 毫秒休眠延迟的发光二极管闪烁模块的性能
(点击放大图像)
图5:5 毫秒休眠延迟的发光二极管闪烁模块的性能
(点击放大图像)
图6:1 毫秒休眠延迟的发光二极管闪烁模块的性能
总结
记住内核事实上是一个程序,一个大而复杂的程序,不过它仍然是一个程序。我们可以修改内核代码、重新编译、重新部署,然后重启,但这是一个漫长的过程。Linux 可加载内核模块允许开发者创建一个二进制代码,使得它能够在运行时向内核中加载或者卸载。希望这三篇文章已经说清楚如何通过可加载内核模块,将自定义功能构建到Linux 内核中。
随着时间的推移,在需要或者找到合适的嵌入式系统应用的时候,我将进一步向这个系列增加文章。
查看英文原文: Writing a Linux Kernel Module — Part 3: Buttons and LEDs
编后语
《他山之石》是 InfoQ 中文站新推出的一个专栏,精选来自国内外技术社区和个人博客上的技术文章,让更多的读者朋友受益,本栏目转载的内容都经过原作者授权。文章推荐可以发送邮件到 editors@cn.infoq.com。
感谢魏星对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群)。
评论