BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

编写Linux内核模块——第三部分:按键和发光二极管

| 作者 金灵杰 关注 5 他的粉丝 发布于 2015年11月10日. 估计阅读时间: 128 分钟 | 如何结合区块链技术,帮助企业降本增效?让我们深度了解几个成功的案例。

【编者的话】了解了基本的内核模块开发、内核空间和用户空间交互之后,终于要开始和硬件设备直接交互了。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_testbuttonled是本文最重要的几个目录。为这些示例代码自动生成的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)上。它们是:diffTimeisDebouncelastTimeledOnnumberPresses。除了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读者交流群InfoQ好读者)。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

好评 by 刘 俊浩

干货

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

1 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT