WiringPi原理分析

1.前言

最近认真学习了树莓派,从浅到深认真分析了wiringPi实现代码,借助树莓派学习linux收获颇丰。深入学习linux一段时间后发现它非常有魅力,一个简单的IO口输出操作尽有那么多的“玩法”。wiringPi是一个简单易用的函数库,通过wiringPi可以扩展SPI和I2C等芯片。

本篇博文将通过一个简单的例子呈现wiringPi的使用,虽然例子简单但会深入分析wiringPi内部实现代码。

2.BCM2835 GPIO相关寄存器

(该部分表述可能有误,正在确认修改中)
树莓派平台的GPIO驱动,例如RPi.GPIO和WiringPi均采用直接操作GPIO寄存器的方式,树莓派的CPU采用博通的BCM2835,想要更好的了解树莓派的GPIO驱动实现就必须阅读BCM2835的数据手册。在BCM2835数据手册中需要认真关注两个内容:

外设寄存器物理地址和外设虚拟地址的映射关系。在linux操作系统中,借助ARM内部的MMU,CPU外设物理地址映射成了虚拟地址,外设的物理起始地址为0x7E00 0000,被MMU虚拟之后的起始地址为0x2000 0000。以此类推,GPIO外设物理起始地址为0x7E20 0000 = 0x7E00 0000+0x0020 0000,被MMU虚拟之后的GPIO外设地址为0x2000 0000+0x0020 0000。那么对于Linux系统而言,GPIO相关操作的起始地址为0x2020 0000。BCM2835的内部映射关系如下图所示。

图1 BCM2835 物理地址和虚拟地址映射关系

GPFSELx、GPSETx、GPCLRx和GPLEVn寄存器。简单来说,GPFSELx为IO口方向或复用寄存器,负责IO口方向例如输入或输出;GPSETx为IO口输出寄存器,负责IO口输出逻辑高电平;GPCLRx寄存器同为IO口输出寄存器,不过和GPSETx相反,负责输出逻辑低电平。GPLEVx为IO口输入寄存器,负责IO口输入状态。

(亲爱的网友们,如果您不理解这些寄存器也不理解MMU机制,也不会影响您使用wiringPi。请放心大胆地使用wiringPi,它已经帮你完成了很多基础性的工作)

3.简单测试代码

下面通过一个简单的代码实现树莓派流水灯,在树莓派(树莓派版本2)中可以直接利用的IO口共有8个,在wiringPi中的编号为GPIO0到GPIO7,对于BCM2835而言编号分别为17, 18, 27, 22, 23, 24, 25, 4。具体对应关系见下图。

图2 wiringPi GPIO 和 BCM2835 GPIO映射关系

#include <wiringPi.h>
int main( )
{
// 初始化wiringPi
wiringPiSetup();

int i = 0;
// 设置IO口全部为输出状态
for( i = 0 ; i < 8 ; i++ )
pinMode(i, OUTPUT);

for (;;)
{
for( i = 0 ; i < 8 ; i++ )
{
// 点亮500ms 熄灭500ms
digitalWrite(i, HIGH); delay(500);
digitalWrite(i, LOW); delay(500);
}
}

return 0;
}

为了方便生成可执行文件,可编写以下makefile文件,CD进入该目录之后直接make即可。

blink:blink.o
gcc blink.c -o blink -lwiringPi
clean:
rm -f blink blink.o

4.代码详解

上面的代码非常简单,可以分为四个部分——wiringPiSetupi初始化、pinMode设置IO为输出方向、digitalWrite输出高电平或低电平和delay系统延时函数。

4.1 wiringPiSetup

int wiringPiSetup (void)
{
int fd ;
int boardRev ;
// 第一步,获得树莓派的版本编号,并根据版本编号映射IO口
boardRev = piBoardRev () ;
if (boardRev == 1)
{
pinToGpio = pinToGpioR1 ;
physToGpio = physToGpioR1 ;
}
else
{
pinToGpio = pinToGpioR2 ;
physToGpio = physToGpioR2 ;
}

// 第二步,打开/dev/mem设备,使得在用户空间可以直接操作内存地址
if ((fd = open ("/dev/mem", O_RDWR | O_SYNC | O_CLOEXEC) ) < 0)
return wiringPiFailure (WPI_ALMOST, "wiringPiSetup: Unable to open /dev/mem: %s\n", strerror (errno)) ;

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

// 第三步,设定wiringPi GPIO外设的操作模式
wiringPiMode = WPI_MODE_PINS ;
return 0 ;
}

该部分代码的实现可以分为三步(注意该部分并不是wiringPiSetup的完整代码,为了说明问题对代码进行简化)

第一步,获得树莓派的版本编号,并根据版本编号映射IO口。 pinToGpioR2为树莓派版本2的GPIO映射关系,不但包括GPIO,还包括SPI、I2C和UART等。此处physToGpioRx存在疑问。

第二步,打开/dev/mem设备,使得在用户空间可以直接操作内存地址。 /dev/mem是物理内存的全映像,可以用来访问物理内存(能够访问物理内存当然也包括MCU外设),一般用法是open(“/dev/mem”,O_RDWR|O_SYNC),接着可以用mmap的地址来访问物理内存(此处为GPIO_BASE),这是实现用户空间驱动的一种方法【参考博文】。(该部分需要深入,请关注后期博文)

第三步,设定wiringPi GPIO外设的操作模式。此处也存在若干疑惑,默认情况便是使用WPI_MODE_PINS 模式,wiringPi的IO管脚编号和BCM IO管脚编号存在一个固定映射关系,但是wiringPi其他代码中还存在wiringPiSetupSys函数,该函数操作GPIO端口时通过/sys/class/gpio中的驱动文件实现,这也是实现树莓派GPIO操作的另一个途径。这种方法便是应用Sysfs——Sysfs 是 Linux 2.6 所提供的一种虚拟文件系统,这个文件系统不仅可以把设备(devices)和驱动程序(drivers) 的信息从内核输出到 用户空间,也可以用来对设备和驱动程序做设置【wiki百科】。(该部分需要深入,请关注后期博文)。

int wiringPiSetupSys (void)
{
int boardRev ;
int pin ;
char fName [128] ;
// 获得树莓派版本编号,版本1或者版本2
boardRev = piBoardRev () ;
if (boardRev == 1)
{
pinToGpio = pinToGpioR1 ;
physToGpio = physToGpioR1 ;
}
else
{
pinToGpio = pinToGpioR2 ;
physToGpio = physToGpioR2 ;
}
// 查找/sys/class/gpio,并记录GPIOx操作文件fd
for (pin = 0 ; pin < 64 ; ++pin)
{
sprintf (fName, "/sys/class/gpio/gpio%d/value", pin) ;
sysFds [pin] = open (fName, O_RDWR) ;
}
// 设置操作模式为 sysfs模式 文件方式驱动GPIO而非寄存器方式
wiringPiMode = WPI_MODE_GPIO_SYS ;
return 0 ;
}

4.2 pinMode

void pinMode (int pin, int mode)
{
int fSel, shift, alt ;
struct wiringPiNodeStruct *node = wiringPiNodes ;

// 树莓派 板载GPIO设置,板载GPIO的管脚编号必须小于64
if ((pin & PI_GPIO_MASK) == 0)
{
// 第一步 确定BCM GPIO引脚编号
if (wiringPiMode == WPI_MODE_PINS)
pin = pinToGpio [pin] ;
// 第二步,确定该管脚对应的fsel寄存器
fSel = gpioToGPFSEL [pin] ;
shift = gpioToShift [pin] ;

// 第三步,根据输入和输出状态设置fsel寄存器
if (mode == INPUT)
*(gpio + fSel) = (*(gpio + fSel) & ~(7 << shift)) ;
else if (mode == OUTPUT)
*(gpio + fSel) = (*(gpio + fSel) & ~(7 << shift)) | (1 << shift) ;
}

// 树莓派 外扩GPIO设置
else
{
if ((node = wiringPiFindNode (pin)) != NULL)
node->pinMode (node, pin, mode) ;
return ;
}
}

该部分代码的实现可以分为三步(注意该部分并不是pinMode 的完整代码,为了说明问题对代码进行简化)

【注意】在wiringPi中,pin编号小于64认为是板载GPIO,如果编号大于64则认为是外扩GPIO,例如使用外扩的MCP23017或者PCF8574,同时外扩的AD和DA芯片的相应pin也应该大于64。

第一步,确定BCM GPIO引脚编号。如果是树莓派2版本,那么映射关系由数组pinToGpioR2决定

static int pinToGpioR2 [64] =
{
17, 18, 27, 22, 23, 24, 25, 4, // GPIO 0 through 7: wpi 0 - 7
2, 3, // I2C - SDA0, SCL0 wpi 8 - 9
8, 7, // SPI - CE1, CE0 wpi 10 - 11
10, 9, 11, // SPI - MOSI, MISO, SCLK wpi 12 - 14
14, 15, // UART - Tx, Rx wpi 15 - 16
28, 29, 30, 31, // New GPIOs 8 though 11 wpi 17 - 20
} ;

第二步,根据输入和输出状态设置fsel寄存器 。作者采用简单明了的查表法,在一个FSEL寄存器中共可设置10个GPIO管脚。具体的含义可查看数据手册和gpioToGPFSEL、gpioToShift的具体定义

static uint8_t gpioToGPFSEL [] =
{
0,0,0,0,0,0,0,0,0,0,
1,1,1,1,1,1,1,1,1,1,
2,2,2,2,2,2,2,2,2,2,
3,3,3,3,3,3,3,3,3,3,
4,4,4,4,4,4,4,4,4,4,
5,5,5,5,5,5,5,5,5,5,
} ;
static uint8_t gpioToShift [] =
{
0,3,6,9,12,15,18,21,24,27,
0,3,6,9,12,15,18,21,24,27,
0,3,6,9,12,15,18,21,24,27,
0,3,6,9,12,15,18,21,24,27,
0,3,6,9,12,15,18,21,24,27,
}

第三步,根据输入和输出状态设置FSEL寄存器。结合第二步便可发现其中的设置技巧。例如操作wringPi的GPIO0对应BCM GPIO17;那么查找gpioToGPFSEL表,应操作第1个(从0开始计数)FSELl寄存器;*(gpio + fSel)中gpio指GPIO外设的虚拟起始地址,此处为0x2200000,第二个FSEL寄存器在此基础上地址偏移1。 shift决定置1或者置0的具体位,例如此时的GPIO17,对应该fsel寄存器的21位;如果是输入状态21-23位全部为0,如果是输出状态21位为1,具体代码如下:

*(gpio + fSel) = (*(gpio + fSel) & ~(7 << shift)) ;                      ——设置为输入IO

*(gpio + fSel) = (*(gpio + fSel) & ~(7 << shift)) | (1 << shift) ;  ——设置为输出IO

图4 BCM2835 FSEL寄存器含义

4.3 digitalWrite

void digitalWrite (int pin, int value)
{
struct wiringPiNodeStruct *node = wiringPiNodes ;
// 树莓派 板载GPIO设置,板载GPIO的管脚编号必须小于64
if ((pin &amp; PI_GPIO_MASK) == 0)
{
// 第一步 确定BCM GPIO引脚编号
if (wiringPiMode == WPI_MODE_PINS)
pin = pinToGpio [pin] ;

// 第二步 设置高电平和低电平
if (value == LOW)
*(gpio + gpioToGPCLR [pin]) = 1 &lt;&lt; (pin &amp; 31) ;
else
*(gpio + gpioToGPSET [pin]) = 1 &lt;&lt; (pin &amp; 31) ;
}
else
{
if ((node = wiringPiFindNode (pin)) != NULL)
node-&gt;digitalWrite (node, pin, value) ;
}
}

该部分代码的实现可以分为两步(注意该部分并不是digitalWrite  的完整代码,为了说明问题对代码进行简化)

第一步,确定BCM GPIO引脚编号。

第二步,设置高电平和低电平。该步骤用于设置GPCLR寄存器和GPSET寄存器。BCM GPIO0到GPIO31 位于GPIO Output Set Register 0 ,相对于GPIO_BASE的偏移量为7,而BCM GPIO32到GPIO53 位于GPIO Output Set Register 1,相对于GPIO_BASE的偏移量为8。例如操作wringPi的GPIO0,对应BCM GPIO17;查找gpioToGPSET表可获得GPIO17位于GPIO Output Set Register 0寄存器,该寄存器的偏移量(相对于GPIO_BASE)为7。通过*(gpio + gpioToGPSET [pin]) = 1 << (pin & 31) ,便可设置GPIO17为输出高电平。

图5 BCM2835 SET寄存器含义

4.4 delay

void delay (unsigned int howLong)
{
struct timespec sleeper, dummy ;
sleeper.tv_sec = (time_t)(howLong / 1000) ;
sleeper.tv_nsec = (long)(howLong % 1000) * 1000000 ;
nanosleep (&amp;sleeper, &amp;dummy) ;
}

delay是wiringPi提供的一个毫秒级别的延时函数,该函数通过nanosleep实现。nanosleep的声明如下:

#include &lt;time.h&gt;
int nanosleep(const struct timespec *req, struct timespec *rem);

调用nanosleep使得进程挂起,直到req所设定的时间耗尽。在该函数中,req至进程最终休眠的时间而rem只剩余的休眠时间,struct timespec结构体的定义如下,

struct timespec {
time_t tv_sec; /* 秒 */
long tv_nsec; /* 纳秒 */
};

从结构体的成员来说,nanosleep似乎可以实现纳秒级别的延时,但是受到linux时钟精度的影响无法实现纳秒级别的延时,但是微妙级别的延时也可以让人接受。

5.总结

深入分析wiringPi之后收获颇丰。wiringPi可通过两种方式实现GPIO的驱动——第一,在用户空间直接操作寄存器(RAM),在用户空间操作寄存器(RAM)需要借助 /dev/mem;第二,利用/sys/class/gpio,通过操作文件的方式控制GPIO。在wiringPi中pin编号小于64为板载设备,例如GPIO0到GPIO7为板载设备,pin编号大于64为扩展设备,例如扩展的AD和DA芯片。最后可以使用nanosleep实现定时休眠。

未来将利用wiringPi实现SPI和I2C设备。

时间: 2024-12-18 14:41:17

WiringPi原理分析的相关文章

kafka producer实例及原理分析

1.前言 首先,描述下应用场景: 假设,公司有一款游戏,需要做行为统计分析,数据的源头来自日志,由于用户行为非常多,导致日志量非常大.将日志数据插入数据库然后再进行分析,已经满足不了.最好的办法是存日志,然后通过对日志的分析,计算出有用的数据.我们采用kafka这种分布式日志系统来实现这一过程. 步骤如下: 搭建KAFKA系统运行环境 如果你还没有搭建起来,可以参考我的博客: http://zhangfengzhe.blog.51cto.com/8855103/1556650 设计数据存储格式

android脱壳之DexExtractor原理分析[zhuan]

http://www.cnblogs.com/jiaoxiake/p/6818786.html内容如下 导语: 上一篇我们分析android脱壳使用对dvmDexFileOpenPartial下断点的原理,使用这种方法脱壳的有2个缺点: 1.  需要动态调试 2.  对抗反调试方案 为了提高工作效率, 我们不希望把宝贵的时间浪费去和加固的安全工程师去做对抗.作为一个高效率的逆向分析师, 笔者是忍不了的,所以我今天给大家带来一种的新的脱壳方法——DexExtractor脱壳法. 资源地址: Dex

android脱壳之DexExtractor原理分析

导语: 上一篇我们分析android脱壳使用对dvmDexFileOpenPartial下断点的原理,使用这种方法脱壳的有2个缺点: 1.  需要动态调试 2.  对抗反调试方案 为了提高工作效率, 我们不希望把宝贵的时间浪费去和加固的安全工程师去做对抗.作为一个高效率的逆向分析师, 笔者是忍不了的,所以我今天给大家带来一种的新的脱壳方法--DexExtractor脱壳法. 资源地址: DexExtractor源码:https://github.com/bunnyblue/DexExtracto

Adaboost算法原理分析和实例+代码(简明易懂)

Adaboost算法原理分析和实例+代码(简明易懂) [尊重原创,转载请注明出处] http://blog.csdn.net/guyuealian/article/details/70995333     本人最初了解AdaBoost算法着实是花了几天时间,才明白他的基本原理.也许是自己能力有限吧,很多资料也是看得懵懵懂懂.网上找了一下关于Adaboost算法原理分析,大都是你复制我,我摘抄你,反正我也搞不清谁是原创.有些资料给出的Adaboost实例,要么是没有代码,要么省略很多步骤,让初学者

Android视图SurfaceView的实现原理分析

附:Android控件TextView的实现原理分析 来源:http://blog.csdn.net/luoshengyang/article/details/8661317 在Android系统中,有一种特殊的视图,称为SurfaceView,它拥有独立的绘图表面,即它不与其宿主窗口共享同一个绘图表面.由于拥有独立的绘图表面,因此SurfaceView的UI就可以在一个独立的线程中进行绘制.又由于不会占用主线程资源,SurfaceView一方面可以实现复杂而高效的UI,另一方面又不会导致用户输

AbstractQueuedSynchronizer的介绍和原理分析(转)

简介 提供了一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架.该同步器(以下简称同步器)利用了一个int来表示状态,期望它能够成为实现大部分同步需求的基础.使用的方法是继承,子类通过继承同步器并需要实现它的方法来管理其状态,管理的方式就是通过类似acquire和release的方式来操纵状态.然而多线程环境中对状态的操纵必须确保原子性,因此子类对于状态的把握,需要使用这个同步器提供的以下三个方法对状态进行操作: java.util.concurrent.locks.Abstra

linux中mmap系统调用原理分析与实现

参考文章:http://blog.csdn.net/shaoguangleo/article/details/5822110 linux中mmap系统调用原理分析与实现 1.mmap系统调用(功能)      void* mmap ( void * addr , size_t len , int prot , int flags ,int fd , off_t offset )      内存映射函数mmap, 负责把文件内容映射到进程的虚拟内存空间, 通过对这段内存的读取和修改,来实现对文件的

Android 4.4 KitKat NotificationManagerService使用详解与原理分析(一)__使用详解

概况 Android在4.3的版本中(即API 18)加入了NotificationListenerService,根据SDK的描述(AndroidDeveloper)可以知道,当系统收到新的通知或者通知被删除时,会触发NotificationListenerService的回调方法.同时在Android 4.4 中新增了Notification.extras 字段,也就是说可以使用NotificationListenerService获取系统通知具体信息,这在以前是需要用反射来实现的. 转载请

一个日期算法的原理分析

1.问题描述 在 OSC 问答频道有一个问题:时间算法:帮忙解答下 简单的复述一遍就是能够通过如下式子来计算month月day日是一年的第几天. 闰年是 day_of_year=(275*month)/9 - (month+9)/12 + day - 30 非闰年比这个少1天.可以简单的验证,这个式子中每个部分计算后都取整,整个结果总是对的. 我们知道1.3.5.7.8.10.12都是31天,2月的天数有点诡异,其他都是30天,正常情况下我们写程序会写很多if来判断月份,进而计算累积的天数.但是