嵌入式Linux设备驱动程序:用户空间中的设备驱动程序
Embedded Linux device drivers: Device drivers in user space
Interfacing with Device Drivers
Device drivers in user space
用户空间中的设备驱动程序
在开始编写设备驱动程序之前,请暂停片刻,考虑是否确实有必要。对于许多常见类型的设备,有通用的设备驱动程序,允许您直接从用户空间与硬件交互,而不必编写一行内核代码。用户空间代码当然更容易编写和调试。它也不包括在GPL中,尽管我觉得这本身并不是一个很好的理由来这样做。
这些驱动程序分为两大类:通过sysfs中的文件(包括GPIO和led)进行控制的驱动程序,以及通过设备节点(如I2C)公开通用接口的串行总线。
GPIO
通用输入/输出(GPIO)是最简单的数字接口形式,因为它可以直接访问各个硬件引脚,每个引脚可以处于两种状态之一:高或低。在大多数情况下,您可以将GPIO管脚配置为输入或输出。你甚至可以使用一组GPIO管脚,通过操作软件中的每个位来创建更高级的接口,比如I2C或SPI,这种技术被称为位碰撞。主要的限制是软件循环的速度和精度,以及您希望专用于它们的CPU周期数。一般来说,除非配置一个实时内核,否则很难达到比毫秒更好的计时器精度。GPIO更常见的用例是读取按钮和数字传感器以及控制led、电机和继电器。
大多数soc有很多GPIO位,这些位被组合在GPIO寄存器中,通常每个寄存器32位。片上GPIO位通过多路复用器(称为pinmux)路由到芯片封装上的GPIO管脚。在电源管理芯片和专用的GPIO扩展器中,可能有额外的GPIO引脚,通过I2C或SPI总线连接。所有这些多样性都由一个名为gpiolib的内核子系统来处理,它实际上不是一个库,而是GPIO驱动程序用来以一致的方式公开I/O的基础设施。在Documentation/gpio的内核源代码中有关于gpiolib实现的详细信息,驱动程序本身的代码在drivers/gpio中。
应用程序可以通过/sys/class/gpio目录中的文件与gpiolib交互。下面是一个典型的嵌入式板(BeagleBone Black)的示例:
# ls /sys/class/gpio
export
gpiochip0 gpiochip32 gpiochip64 gpiochip96
unexport
名为gpiochip0到gpiochip96的目录代表四个GPIO寄存器,每个寄存器有32个GPIO位。如果您查看其中一个gpiochip目录,您将看到以下内容:
# ls
/sys/class/gpio/gpiochip96
base
label ngpio power subsystem uevent
名为base的文件包含寄存器中第一个GPIO引脚的编号,ngpio包含寄存器中的位数。在本例中,gpiochip96/base是96,gpiochip96/ngpio是32,这说明它包含GPIO位96到127。在一个寄存器中的最后一个GPIO和下一个寄存器中的第一个GPIO之间可能存在间隙。 要从用户空间控制GPIO位,首先必须从内核空间导出它,这是通过将GPIO编号写入/sys/class/GPIO/export来完成的。此示例显示GPIO 53的处理过程,它连接到BeagleBone Black上的用户LED 0:
# echo 53 >
/sys/class/gpio/export
# ls
/sys/class/gpio
export gpio53 gpiochip0
gpiochip32 gpiochip64 gpiochip96 unexport
现在,有一个新的目录gpio53,其中包含控制pin所需的文件。
如果GPIO位已经被内核声明,您将无法以这种方式导出它。
目录gpio53包含以下文件:
# ls
/sys/class/gpio/gpio53
active_low direction
power uevent
device
edge subsystem value
管脚从输入开始。若要将其更改为输出,请写入方向文件。文件值包含管脚的当前状态,0表示低,1表示高。如果是输出,则可以通过将0或1写入值来更改状态。有时,在硬件中,低和高的含义是相反的(硬件工程师喜欢做这种事情),所以写1到active_low会颠倒值的含义,这样低电压报告为1,高电压报告为0。
通过将GPIO编号写入到/sys/class/gpio/unexport,可以从用户空间控件中删除GPIO。
处理来自GPIO的中断
在许多情况下,可以将GPIO输入配置为在状态改变时生成中断,这允许您等待中断,而不是在效率低下的软件循环中轮询。如果GPIO位可以生成中断,则存在名为edge的文件。最初,它的值称为none,这意味着它不会生成中断。要启用中断,可以将其设置为以下值之一:
上升:上升沿中断
下降:下降沿中断
两个:在上升和下降边缘都中断
无:无中断(默认)
可以使用poll()函数和POLLPRI作为事件等待中断。如果要等待GPIO 48上的上升沿,请首先启用中断:
# echo 48 >
/sys/class/gpio/export
# echo falling
> /sys/class/gpio/gpio48/edge
然后,使用poll(2)等待更改,如下面的代码示例所示,您可以在 MELP/chapter_09/gpio-int/gpio-int.c中的代码存档一书中看到
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <poll.h> int main(int argc, char *argv[]) { int f; struct pollfd poll_fds[1]; int ret; char value[4]; int n; f = open("/sys/class/gpio/gpio48/value", O_RDONLY); if (f == -1) { perror("Can't open gpio48"); return 1; } n = read(f, &value, sizeof(value)); if (n > 0) { printf("Initial value=%cn", value[0]); lseek(f, 0, SEEK_SET); } poll_fds[0].fd = f; poll_fds[0].events = POLLPRI | POLLERR; while (1) { printf("Waitingn"); ret = poll(poll_fds, 1, -1); if (ret > 0) { n = read(f, &value, sizeof(value)); printf("Button pressed: value=%cn", value[0]); lseek(f, 0, SEEK_SET); } } return 0; }
发光二极管
led通常通过GPIO管脚进行控制,但是还有另一个内核子系统提供了更专门的控制。LED内核子系统增加了设置亮度的功能,如果LED有这种能力,它可以处理以其他方式连接的LED,而不是简单的GPIO管脚。它可以配置为触发事件,如阻止设备访问或只是一个心跳信号,以显示设备正在工作。您必须使用选项CONFIG_LEDS_CLASS和适合您的LED触发器操作来配置内核。有关文档/led/的详细信息,驱动程序位于drivers/led/中。
与GPIOs一样,led是通过sysfs中目录为/sys/class/led的接口控制的。在BeagleBone Black的情况下,led的名称以d的形式编码在设备树中设备名称:颜色:函数devicename:colour:function,如下所示:
# ls /sys/class/leds
beaglebone:green:heartbeat beaglebone:green:usr2
beaglebone:green:mmc0
beaglebone:green:usr3
现在,我们可以查看其中一个指示灯的属性,注意shell要求路径名中的冒号字符“:”前面必须有一个反斜杠转义符“”:
# cd
/sys/class/leds/beaglebone:green:usr2
# ls
brightness max_brightness
subsystem uevent
device power trigger
亮度文件控制LED的亮度,可以是0(关闭)和最大亮度(完全打开)之间的数字。如果LED不支持中等亮度,任何非零值都会将其打开。名为trigger的文件列出了触发LED亮起的事件。触发器列表取决于实现。下面是一个例子:
# cat trigger
none mmc0 mmc1 timer oneshot
heartbeat backlight gpio [cpu0]
default-on
当前选定的触发器显示在方括号中。您可以通过将其他触发器之一写入文件来更改它。如果要完全通过亮度控制LED,请选择“无”。如果将触发器设置为计时器,则会出现两个额外文件,允许您以毫秒为单位设置开关时间:
# echo timer > trigger
#
ls
brightness delay_on max_brightness
subsystem uevent
delay_off device power trigger
# cat delay_on
500
# cat
/sys/class/leds/beaglebone:green:heartbeat/delay_off
500
如果LED具有片上定时器硬件,则闪烁不会中断CPU。
I2C
I2C是一种简单的低速2线总线,在嵌入式板上很常见,通常用于访问不在SoC上的外围设备,如显示控制器、摄像头传感器、GPIO扩展器等。在PCs上有一个称为系统管理总线(SMBus)的相关标准,用于访问温度和电压传感器。SMBus是I2C的一个子集。
I2C是一种主从协议,主机是SoC上的一个或多个主机控制器。从机有一个由制造商分配的7位地址(请阅读数据表),每个总线最多允许128个节点,但保留了16个节点,因此实际上只允许112个节点。主机可以与其中一个从机启动读或写事务。通常,第一个字节用于指定从机上的寄存器,其余字节是从该寄存器读取或写入的数据。
每个主机控制器有一个设备节点,例如,该SoC有四个:
# ls -l /dev/i2c*
crw-rw—- 1 root
i2c 89, 0 Jan 1 00:18 /dev/i2c-0
crw-rw—- 1 root i2c 89, 1 Jan 1 00:18 /dev/i2c-1
crw-rw—- 1 root i2c 89, 2 Jan 1 00:18 /dev/i2c-2
crw-rw—- 1 root i2c 89, 3 Jan 1 00:18 /dev/i2c-3
设备接口提供一系列ioctl命令,用于查询主机控制器并将读写命令发送到I2C从机。有一个名为i2c-tools的包,它使用这个接口提供与i2c设备交互的基本命令行工具。工具如下:
- i2cdetect : This lists the I2C adapters, and probes the bus
- i2cdump : This dumps data from all the registers of an I2C peripheral
- i2cget : This reads data from an I2C slave
- i2cset : This writes data to an I2C slave
i2c工具包可以在Buildroot和Yocto项目以及大多数主流发行版中获得。所以,只要你知道从机的地址和协议,编写一个用户空间程序来与设备对话是很简单的。下面的示例显示如何从安装在I2C总线0上BeagleBone Black上的从机地址0x50上的AT24C512B EEPROM读取前四个字节(代码在MELP/章节U 09/I2C示例中):
#include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <sys/ioctl.h> #include <linux/i2c-dev.h> #define I2C_ADDRESS 0x50 int main(void) { int f; int n; char buf[10]; f = open("/dev/i2c-0", O_RDWR); /* Set the address of the i2c slave device */ ioctl(f, I2C_SLAVE, I2C_ADDRESS); /* Set the 16-bit address to read from to 0 */ buf[0] = 0; /* address byte 1 */ buf[1] = 0; /* address byte 2 */ n = write(f, buf, 2); /* Now read 4 bytes from that address */ n = read(f, buf, 4); printf("0x%x 0x%x0 0x%x 0x%xn", buf[0], buf[1], buf[2], buf[3]); close(f); return 0; }
在Documentation/I2C/dev interface中有关于I2C的Linux实现的更多信息。主控制器驱动程序位于drivers/i2c/总线中。
串行外设接口(SPI)
SPI总线类似于I2C,但速度更快,最高可达数十兆赫。在双工线路中使用四条独立的双工线路进行操作。总线上的每个芯片都通过专用芯片选择线进行选择。它通常用于连接触摸屏传感器、显示控制器和串行或闪存设备。
与I2C一样,它是主从协议,大多数SOC实现一个或多个主主机控制器。有一个通用的SPI设备驱动程序,您可以通过内核配置CONFIG_SPI峎SPIDEV来启用它。它为每个SPI控制器创建一个设备节点,允许您从用户空间访问SPI芯片。设备节点被命名为spidev[bus].[chip select]:
# ls -l /dev/spi*
crw-rw—- 1 root root 153, 0 Jan 1 00:29 /dev/spidev1.0
有关使用spidev接口的示例,请参阅Documentation/spi中的示例代码。