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

2019-07-13 05:56发布

【编者的话】了解了基本的内核模块开发、内核空间和用户空间交互之后,终于要开始和硬件设备直接交互了。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损坏! (点击放大图像)4dbd7308701b4a8552e3ff079d7a3393.png\图1:本文描述的发光二极管和按键电路

本次讨论的源码

本次讨论的所有代码都在为《Exploring BeagleBone》准备的GitHub仓库上。代码可以在ExploringBB GitHub仓库内核工程目录中公开查看,或者也可以将代码复制到BeagleBone(或者其他Linux设备):molloyd@beaglebone:~$ sudo apt-get install gitmolloyd@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# lsexport gpiochip0 gpiochip32 gpiochip64 gpiochip96 unexport oot@beaglebone:/sys/class/gpio# echo 49 u0026gt; export oot@beaglebone:/sys/class/gpio# lsexport gpio49 gpiochip0 gpiochip32 gpiochip64 gpiochip96 unexport oot@beaglebone:/sys/class/gpio# cd gpio49 oot@beaglebone:/sys/class/gpio/gpio49# lsactive_low direction edge power subsystem uevent value oot@beaglebone:/sys/class/gpio/gpio49# echo out u0026gt; direction oot@beaglebone:/sys/class/gpio/gpio49# echo 1 u0026gt; value oot@beaglebone:/sys/class/gpio/gpio49# echo 0 u0026gt; value oot@beaglebone:/sys/class/gpio/gpio49# cd .. oot@beaglebone:/sys/class/gpio# echo 49 u0026gt; unexport oot@beaglebone:/sys/class/gpio# lsexport 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