日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

树莓派GPIO驱动原理

發布時間:2025/3/21 编程问答 49 豆豆
生活随笔 收集整理的這篇文章主要介紹了 树莓派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] ?view
plain 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] ?view
plain 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] ?view
plain 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] ?view
plain 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驱动原理的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。