从树莓派的wiringPi库分析Linux对GPIO的控制原理

前一阵给媳妇买了个树莓派4B,教程里使用wiringPi这个c库对GPIO口进行控制。但是说到底硬件接口肯定是需要通过操作系统进行控制的,wiringPi这个库到底是怎么跟操作系统打交道的呢?换句话说,操作系统提供了什么样的接口让用户程序来控制硬件?操作系统又是怎样真正的完成了一次硬件的控制呢?答案是:树莓派上的Linux通过对映射到硬件寄存器的内存地址读写来真正的控制硬件,提供的接口为GPIO设备文件。不过root用户可以通过/dev/mem这个文件,来直接控制物理内存,从而绕过GPIO设备文件,对GPIO进行控制。这就是wiringPi这个库的原理,有些黑客。

wiringPi介绍

http://wiringpi.com/

WiringPi is a PIN based GPIO access library written in C for the BCM2835, BCM2836 and BCM2837 SoC devices used in all Raspberry Pi.

简单的说就是可以使用这个c库简单的控制树莓派的GPIO接口,GPIO接口是一个数字信号的接口,可以输入数字高低电平。在树莓派上的两排针就是GPIO接口:

image

使用wiringPi的控制示例如下:

// $ gcc -Wall -o blink blink.c -lwiringPi
// $ sudo ./blink
#include <wiringPi.h>
int main (void)
{
  wiringPiSetup () ;
  pinMode (0, OUTPUT) ;
  for (;;)
  {
    digitalWrite (0, HIGH) ; delay (500) ;
    digitalWrite (0,  LOW) ; delay (500) ;
  }
  return 0 ;
}

非常简单的封装,教小孩编程那些机器人就类似这种。不过这个库已经停止维护了:wiringPi – deprecated…,因为我自己的树莓派上是ubuntu,所以只能采用源码编译的方式安装,最后在github上找到最后一版源码WiringPi,然后执行./build就直接安装了。不过发现除了使用这个c库还提供了一个gpio的小工具:

➜  gpio -h
gpio: Usage: gpio -v
       gpio -h
       gpio [-g|-1] ...
       gpio [-d] ...
       [-x extension:params] [[ -x ...]] ...
       gpio [-p] <read/write/wb> ...
       gpio <mode/read/write/aread/awritewb/pwm/pwmTone/clock> ...
       gpio <toggle/blink> <pin>
       gpio readall
       gpio unexportall/exports
       gpio export/edge/unexport ...
       gpio wfi <pin> <mode>
       gpio drive <group> <value>
       gpio pwm-bal/pwm-ms 
       gpio pwmr <range> 
       gpio pwmc <divider> 
       gpio load spi/i2c
       gpio unload spi/i2c
       gpio i2cd/i2cdetect
       gpio rbx/rbd
       gpio wb <value>
       gpio usbp high/low
       gpio gbr <channel>
       gpio gbw <channel> <value>

即也可以直接使用这个工具直接控制GPIO接口,不过在分析原理之前,我们先来看看其他人是怎么使用GPIO的吧。

GPIO的使用

直接在操作系统层面,不使用任何第三方库控制GPIO一般如下两种方法:

使用GPIO文件接口

Linux GPIO口的控制(树莓派4B实现)(文件方式)

比如我们选择GPIO14,那我们就可以先将14口加入到export文件:

echo 14 > /sys/class/gpio/export
然后注意文件夹中就多了一个gpio14这个文件,然后设置这个IO口的方向,比如设置为输出:

echo out > /sys/class/gpio/gpio14/direction
再接下来就是设置他的高低电平了,比如设置高电平:

echo 1 > /sys/class/gpio/gpio14/value
假如你现在用完了,可以退出对这个IO口的使用:

echo 14 >/sys/class/gpio/unexport

这种方法就是直接使用文件系统中Linux提供的设备节点,直接对相应的文件进行读写即可完成控制。

直接使用物理内存

树莓派3B-linux控制GPIO(不用树莓派的库)

这种方式就比较黑客了,也揭示了操作系统自身到底是怎么控制硬件的。阅读大概阅读这篇文档:BCM2835-ARM-Peripherals.pdf,比如GPIO处:

image

可以看到,GPIO的控制器是由41个寄存器组成,访问这些寄存器是通过内存地址,而不是使用R1、R2、R3那种直接可以写在汇编指令的那种寄存器,即这里的寄存器在CPU看起来就是一个内存单元。

至此我终于想明白了为啥我从来没见过类似GPIO_ON,GPIO_DOWN的汇编指令,CPU是通过基本的访存指令来控制的硬件外设,而不是专用的机器码。也终于和之前学的《微机接口》课程对应上了,这里采用的就是,外设的编址方式中的统一编址。在x86上,存在in/out指令可以直接控制一些外设或者CPU内部的寄存器,这里称为端口,其实就是外设编制方式的独立编址。ARM没有in/out指令,所以ARM指令集的CPU控制外设的本质都是内存读写

驱动程序怎么控制硬件的? shikihane的回答 - 知乎
现在控制硬件,主要是两种技术,一个是端口IO,另一个是MMIO。对于MMIO而言,就是把硬件的寄存器映射在一段地址的内存地址上,对于CPU而言,访问那一段地址(寄存器)就能和硬件通讯。端口IO我接触的少就不说了。

在SoC中的设计,硬件控制寄存器的物理内存地址已经设计死了,这些硬件信息属于板级代码应该提供给Linux内核的信息,以便于内核可以正确的使用硬件。不过无论是x86还是arm,在操作系统的内核启动时都应该获取到了硬件设备信息。在x86的PC机上,操作系统内核使用BIOS提供的服务来获得硬件信息。在ARM的嵌入式上,uboot负责把描述板级信息的ARM设备树(Device Tree Source),传递给Linux内核,内核即可正确启动。总之操作系统内核一定要借助一些方法来获取这些硬件相关的板级信息,这也就告诉我们,想要研究清楚这些问题,还要知道操作系统底下还有什么?操作系统的启动过程又依赖了什么?

回到直接使用物理内存的方法控制GPIO这篇文章,可以看到作者直接使用/dev/mem文件,然后用mmap直接映射到用户内存,从而访问实际物理内存,这个是需要root权限的。映射完的内存就是GPIO控制器的寄存器,使用方法就按照官方手册即可,这种方法也是Linux内核控制GPIO最根本的原理。

int8_t paddr2vaddr()
{
    if( (memfd = open("/dev/mem", O_RDWR | O_SYNC))  >= 0 )
    {
    	//“/dev/mem”内是物理地址的映像
    	//通过mmap函数将物理地址映射为用户进程的虚拟地址
        bcm2837_peripherals_base = mmap(NULL, PERIPHERALS_ADDR_SIZE, (PROT_READ | PROT_WRITE),
                                        MAP_SHARED, memfd, (off_t)PERIPHERALS_PHY_BASE);

这也给我们一个控制硬件最底层的基本方法的启示,如果拿到一个嵌入式的root权限,即使不去逆向分析其控制硬件的应用层代码,我在最底层直接控制内存也能搞,就是麻烦点,不过一定可以行。

原理分析

首先分析的是gpio这个小工具WiringPi/gpio/gpio.c,可以看到其中也调用了digitalWrite函数:

static void doWrite (int argc, char *argv [])
{
  int pin, val ;

  if (argc != 4)
  {
    fprintf (stderr, "Usage: %s write pin value\n", argv [0]) ;
    exit (1) ;
  }
  pin = atoi (argv [2]) ;

  /**/ if ((strcasecmp (argv [3], "up") == 0) || (strcasecmp (argv [3], "on") == 0))
    val = 1 ;
  else if ((strcasecmp (argv [3], "down") == 0) || (strcasecmp (argv [3], "off") == 0))
    val = 0 ;
  else
    val = atoi (argv [3]) ;

  /**/ if (val == 0)
    digitalWrite (pin, LOW) ;
  else
    digitalWrite (pin, HIGH) ;
}

并且还看到了使用了linux提供的gpio文件接口

sprintf (fName, "/sys/class/gpio/gpio%d/value", i) ;
if ((fd = open (fName, O_RDONLY)) == -1)
{
    printf ("No Value file (huh?)\n") ;
    continue ;
}

分析WiringPi/wiringPi/wiringPi.c

  if ((fd = open ("/dev/mem", O_RDWR | O_SYNC | O_CLOEXEC)) < 0)
  {
    if ((fd = open ("/dev/gpiomem", O_RDWR | O_SYNC | O_CLOEXEC) ) >= 0)	// We're using gpiomem
    {
      piGpioBase   = 0 ;
      usingGpioMem = TRUE ;
    }
    else
      return wiringPiFailure (WPI_ALMOST, "wiringPiSetup: Unable to open /dev/mem or /dev/gpiomem: %s.\n"
	"  Aborting your program because if it can not access the GPIO\n"
	"  hardware then it most certianly won't work\n"
	"  Try running with sudo?\n", strerror (errno)) ;
  }

// Set the offsets into the memory interface.

  GPIO_PADS 	  = piGpioBase + 0x00100000 ;
  GPIO_CLOCK_BASE = piGpioBase + 0x00101000 ;
  GPIO_BASE	  = piGpioBase + 0x00200000 ;
  GPIO_TIMER	  = piGpioBase + 0x0000B000 ;
  GPIO_PWM	  = piGpioBase + 0x0020C000 ;

// Map the individual hardware components

//	GPIO:

  gpio = (uint32_t *)mmap(0, BLOCK_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fd, GPIO_BASE) ;
  if (gpio == MAP_FAILED)
    return wiringPiFailure (WPI_ALMOST, "wiringPiSetup: mmap (GPIO) failed: %s\n", strerror (errno)) ;

可以看到这里就是使用的mmap映射物理内存的方式进行的控制,这也解释了为什么运行使用这个库的二进制需要使用root权限才能成功的控制GPIO。最后,我们发现wiringPi这库控制GPIO的方法也没有跑出上述两种方法,其中物理内存控制是文件接口控制的本质。

其他阅读