嵌入式Linux设备驱动程序:用户空间中的设备驱动程序

嵌入式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中的示例代码。

 

posted @ 2020-07-11 11:39  吴建明wujianming  阅读(432)  评论(0编辑  收藏  举报