树莓派GPIO驱动原理
生活随笔
收集整理的這篇文章主要介紹了
树莓派GPIO驱动原理
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
1.前言
最近認真學習了樹莓派,從淺到深認真分析了wiringPi實現代碼,借助樹莓派學習linux收獲頗豐。深入學習linux一段時間后發現它非常有魅力,一個簡單的IO口輸出操作盡有那么多的“玩法”。wiringPi是一個簡單易用的函數庫,通過wiringPi可以擴展SPI和I2C等芯片,關于wiringPi的介紹和安裝請參考我的另一篇【 博文 】。本篇博文將通過一個簡單的例子呈現wiringPi的使用,雖然例子簡單但會深入分析wiringPi內部實現代碼。
【 樹莓派學習筆記——索引博文 】
2.BCM2835 GPIO相關寄存器
樹莓派平臺的GPIO驅動,例如RPi.GPIO和WiringPi均采用直接操作GPIO寄存器的方式,樹莓派的CPU采用博通的BCM2835,想要更好的了解樹莓派的GPIO驅動實現就必須閱讀BCM2835的數據手冊。在BCM2835數據手冊中需要認真關注兩個內容:外設寄存器物理地址和外設虛擬地址的映射關系。 在linux操作系統中,借助ARM內部的MMU,CPU外設物理地址映射成了虛擬地址,外設的物理起始地址為0x7E000000,被MMU虛擬之后的起始地址為0x20000000。以此類推,GPIO外設物理起始地址為0x7E00000+0x200000,被MMU虛擬之后的GPIO外設地址為0x2000000+0x200000。那么對于linux系統而言, GPIO相關操作的起始地址為0x22000000。 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映射關系
[cpp] ?view
plain copy
#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即可。
[cpp] ?view
plain copy
blink:blink.o?
gcc blink.c -o blink -lwiringPi?
clean:?
rm -f blink blink.o?
圖3 代碼運行結果
4.代碼詳解
上面的代碼非常簡單,可以分為四個部分—— wiringPiSetupi初始化、pinMode設置IO為輸出方向、digitalWrite輸出高電平或低電平和delay系統延時函數。4.1 wiringPiSetup
[cpp] ?viewplain copy
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百科 】。(該部分需要深入,請關注后期博文)。
[cpp] ?view
plain copy
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
[cpp] ?viewplain copy
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決定
[cpp] ?view
plain copy
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的具體定義
[cpp] ?view
plain copy
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
[cpp] ?viewplain copy
void digitalWrite (int pin, int value)?
{?
struct wiringPiNodeStruct *node = wiringPiNodes ;?
// 樹莓派 板載GPIO設置,板載GPIO的管腳編號必須小于64?
if ((pin & PI_GPIO_MASK) == 0)?
{?
// 第一步 確定BCM GPIO引腳編號?
if (wiringPiMode == WPI_MODE_PINS)?
pin = pinToGpio [pin] ;?
// 第二步 設置高電平和低電平?
if (value == LOW)?
*(gpio + gpioToGPCLR [pin]) = 1 << (pin & 31) ;?
else?
*(gpio + gpioToGPSET [pin]) = 1 << (pin & 31) ;?
}?
else?
{?
if ((node = wiringPiFindNode (pin)) != NULL)?
node->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
[cpp] ?viewplain copy
void delay (unsigned int howLong)?
{?
struct timespec sleeper, dummy ;?
sleeper.tv_sec = (time_t)(howLong / 1000) ;?
sleeper.tv_nsec = (long)(howLong % 1000) * 1000000 ;?
nanosleep (&sleeper, &dummy) ;?
}?
delay是wiringPi提供的一個毫秒級別的延時函數,該函數通過nanosleep實現。nanosleep的聲明如下:
[cpp] ?view
plain copy
#include <time.h>?
int nanosleep(const struct timespec *req, struct timespec *rem);?
調用nanosleep使得進程掛起,直到req所設定的時間耗盡。在該函數中,req至進程最終休眠的時間而rem只剩余的休眠時間,struct timespec結構體的定義如下,
[cpp] ?view
plain copy
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設備。
6.參考資料和博文鏈接
6.1 【 elinux 樹莓派外設驅動開發指南 】6.2 【 樹莓派 GPIO入門指南 】 《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀
總結
以上是生活随笔為你收集整理的树莓派GPIO驱动原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 树莓派学习笔记——GPIO功能学习
- 下一篇: STM32启动BOOT0 BOOT1设置