目錄
- 應用層如何操控GPIO
- GPIO 應用程式設計之輸出
- GPIO 應用程式設計之輸入
- GPIO 應用程式設計之中斷
- 在開發闆上測試
-
- GPIO 輸出測試
- GPIO 輸入測試
- GPIO 中斷測試
本章介紹應用層如何控制GPIO,譬如控制GPIO 輸出高電平、或輸出低電平。
應用層如何操控GPIO
與LED 裝置一樣,GPIO 同樣也是通過sysfs 方式進行操控,進入到/sys/class/gpio 目錄下,如下所示:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnL1YzM0ITY2QWOiBTNhRGM4IWYwQTZ2cjY3UmMxIjMlV2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
可以看到該目錄下包含兩個檔案export、unexport 以及5 個gpiochipX(X 等于0、32、64、96、128)命名的檔案夾。
⚫ gpiochipX:目前SoC 所包含的GPIO 控制器,我們知道I.MX6UL/I.MX6ULL 一共包含了5 個GPIO控制器,分别為GPIO1、GPIO2、GPIO3、GPIO4、GPIO5,在這裡分别對應gpiochip0、gpiochip32、gpiochip64、gpiochip96、gpiochip128 這5 個檔案夾,每一個gpiochipX 檔案夾用來管理一組GPIO。随便進入到其中某個目錄下,可以看到這些目錄下包含了如下檔案:
在這個目錄我們主要關注的是base、label、ngpio 這三個屬性檔案,這三個屬性檔案均是隻讀、不可寫。
base:與gpiochipX 中的X 相同,表示該控制器所管理的這組GPIO 引腳中最小的編号。每一個GPIO引腳都會有一個對應的編号,Linux 下通過這個編号來操控對應的GPIO 引腳。
label:該組GPIO 對應的标簽,也就是名字。
ngpio:該控制器所管理的GPIO 引腳的數量(是以引腳編号範圍是:base ~ base+ngpio-1)。
對于給定的一個GPIO 引腳,如何計算它在sysfs 中對應的編号呢?其實非常簡單,譬如給定一個GPIO引腳為GPIO4_IO16,那它對應的編号是多少呢?首先我們要确定GPIO4 對應于gpiochip96,該組GPIO 引腳的最小編号是96(對應于GPIO4_IO0),是以GPIO4_IO16 對應的編号自然是96 + 16 = 112;同理
GPIO3_IO20 對應的編号是64 + 20 = 84。
⚫ export:用于将指定編号的GPIO 引腳導出。在使用GPIO 引腳之前,需要将其導出,導出成功之後才能使用它。注意export 檔案是隻寫檔案,不能讀取,将一個指定的編号寫入到export 檔案中即可将對應的GPIO 引腳導出,譬如:
echo 0 > export # 導出編号為0 的GPIO 引腳(對于I.MX6UL/I.MX6ULL 來說,也就是
GPIO1_IO0)
導出成功之後會發現在/sys/class/gpio 目錄下生成了一個名為gpio0 的檔案夾(gpioX,X 表示對應的編号),如圖16.1.7 所示。這個檔案夾就是導出來的GPIO 引腳對應的檔案夾,用于管理、控制該GPIO 引腳,稍後再給大家介紹。
⚫ unexport:将導出的GPIO 引腳删除。當使用完GPIO 引腳之後,我們需要将導出的引腳删除,同樣該檔案也是隻寫檔案、不可讀,譬如:
echo 0 > unexport # 删除導出的編号為0 的GPIO 引腳
删除成功之後,之前生成的gpio0 檔案夾就會消失!
以上就給大家介紹了/sys/class/gpio 目錄下的所有檔案和檔案夾,控制GPIO 引腳主要是通過export 導出之後所生成的gpioX(X 表示對應的編号)檔案夾,在該檔案夾目錄下存在一些屬性檔案可用于控制GPIO引腳的輸入、輸出以及輸出的電平狀态等。
Tips:需要注意的是,并不是所有GPIO 引腳都可以成功導出,如果對應的GPIO 已經在核心中被使用了,那便無法成功導出,列印如下資訊:
那也就是意味着該引腳已經被核心使用了,譬如某個驅動使用了該引腳,那麼将無法導出成功!
gpioX
将指定的編号寫入到export 檔案中,可以導出指定編号的GPIO 引腳,導出成功之後會在/sys/class/gpio目錄下生成對應的gpioX(X 表示GPIO 的編号)檔案夾,以前面所生成的gpio0 為例,進入到gpio0 目錄,該目錄下的檔案如下所示:
我們主要關心的檔案是active_low、direction、edge 以及value 這四個屬性檔案,接下來分别介紹這四個屬性檔案的作用:
⚫ direction:配置GPIO 引腳為輸入或輸出模式。該檔案可讀、可寫,讀表示檢視GPIO 目前是輸入還是輸出模式,寫表示将GPIO 配置為輸入或輸出模式;讀取或寫入操作可取的值為"out"(輸出模式)和"in"(輸入模式),如下所示:
⚫ value:在GPIO 配置為輸出模式下,向value 檔案寫入"0"控制GPIO 引腳輸出低電平,寫入"1"則控制GPIO 引腳輸出高電平。在輸入模式下,讀取value 檔案擷取GPIO 引腳目前的輸入電平狀态。譬如:
# 擷取GPIO 引腳的輸入電平狀态
echo "in" > direction
cat value
# 控制GPIO 引腳輸出高電平
echo "out" > direction
echo "1" > value
⚫ active_low:這個屬性檔案用于控制極性,可讀可寫,預設情況下為0,譬如:
# active_low 等于0 時
echo "0" > active_low
echo "out" > direction
echo "1" > value #輸出高
echo "0" > value #輸出低
# active_low 等于1 時
$ echo "1" > active_low
$ echo "out" > direction
$ echo "1" > value #輸出低
$ echo "0" > value #輸出高
由此看出,active_low 的作用已經非常明顯了,對于輸入模式來說也同樣适用。
⚫ edge:控制中斷的觸發模式,該檔案可讀可寫。在配置GPIO 引腳的中斷觸發模式之前,需将其設定為輸入模式:
非中斷引腳:echo “none” > edge
上升沿觸發:echo “rising” > edge
下降沿觸發:echo “falling” > edge
邊沿觸發:echo “both” > edge
當引腳被配置為中斷後可以使用poll()函數監聽引腳的電平狀态變化,在後面的示例中将向大家介紹。
GPIO 應用程式設計之輸出
上一小節已經向大家介紹了如何通過sysfs 方式控制開發闆上的GPIO 引腳,本小節我們編寫一個簡單地測試程式,控制開發闆上的某一個GPIO 輸出高、低不同的電平狀态,其示例代碼如下所示:
本例程源碼對應的路徑為:開發闆CD光牒->11、Linux C 應用程式設計例程源碼->16_gpio->gpio_out.c。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
static char gpio_path[100];
static int gpio_config(const char *attr, const char *val)
{
char file_path[100];
int len;
int fd;
sprintf(file_path, "%s/%s", gpio_path, attr);
if (0 > (fd = open(file_path, O_WRONLY))) {
perror("open error");
return fd;
}
len = strlen(val);
if (len != write(fd, val, len)) {
perror("write error");
close(fd);
return -1;
}
close(fd); //關閉檔案
return 0;
}
int main(int argc, char *argv[])
{
/* 校驗傳參 */
if (3 != argc) {
fprintf(stderr, "usage: %s <gpio> <value>\n", argv[0]);
exit(-1);
}
/* 判斷指定編号的GPIO是否導出 */
sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]);
if (access(gpio_path, F_OK)) {//如果目錄不存在 則需要導出
int fd;
int len;
if (0 > (fd = open("/sys/class/gpio/export", O_WRONLY))) {
perror("open error");
exit(-1);
}
len = strlen(argv[1]);
if (len != write(fd, argv[1], len)) {//導出gpio
perror("write error");
close(fd);
exit(-1);
}
close(fd); //關閉檔案
}
/* 配置為輸出模式 */
if (gpio_config("direction", "out"))
exit(-1);
/* 極性設定 */
if (gpio_config("active_low", "0"))
exit(-1);
/* 控制GPIO輸出高低電平 */
if (gpio_config("value", argv[2]))
exit(-1);
/* 退出程式 */
exit(0);
}
執行程式時需要傳入兩個參數,argv[1]指定GPIO 的編号、argv[2]指定輸出電平狀态(0 表示低電平、1 表示高電平)。
上述代碼中首先使用access()函數判斷指定編号的GPIO 引腳是否已經導出,也就是判斷相應的gpioX目錄是否存在,如果不存在則表示未導出,則通過"/sys/class/gpio/export"檔案将其導出;導出之後先配置了GPIO 引腳為輸出模式,也就是向direction 檔案中寫入"out";接着再配置極性,通過向active_low 檔案中寫入"0"(不用配置也可以);最後再控制GPIO 引腳輸出相應的電平狀态,通過對value 屬性檔案寫入"1"或"0"來使其輸出高電平或低電平。
使用交叉編譯工具編譯應用程式,如下所示:
GPIO 應用程式設計之輸入
本小節我們編寫一個讀取GPIO 電平狀态的測試程式,其示例代碼如下所示:
本例程源碼對應的路徑為:開發闆CD光牒->11、Linux C 應用程式設計例程源碼->16_gpio->gpio_in.c。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
static char gpio_path[100];
static int gpio_config(const char *attr, const char *val)
{
char file_path[100];
int len;
int fd;
sprintf(file_path, "%s/%s", gpio_path, attr);
if (0 > (fd = open(file_path, O_WRONLY))) {
perror("open error");
return fd;
}
len = strlen(val);
if (len != write(fd, val, len)) {
perror("write error");
close(fd);
return -1;
}
close(fd); //關閉檔案
return 0;
}
int main(int argc, char *argv[])
{
char file_path[100];
char val;
int fd;
/* 校驗傳參 */
if (2 != argc) {
fprintf(stderr, "usage: %s <gpio>\n", argv[0]);
exit(-1);
}
/* 判斷指定編号的GPIO是否導出 */
sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]);
if (access(gpio_path, F_OK)) {//如果目錄不存在 則需要導出
int len;
if (0 > (fd = open("/sys/class/gpio/export", O_WRONLY))) {
perror("open error");
exit(-1);
}
len = strlen(argv[1]);
if (len != write(fd, argv[1], len)) {//導出gpio
perror("write error");
close(fd);
exit(-1);
}
close(fd); //關閉檔案
}
/* 配置為輸入模式 */
if (gpio_config("direction", "in"))
exit(-1);
/* 極性設定 */
if (gpio_config("active_low", "0"))
exit(-1);
/* 配置為非中斷方式 */
if (gpio_config("edge", "none"))
exit(-1);
/* 讀取GPIO電平狀态 */
sprintf(file_path, "%s/%s", gpio_path, "value");
if (0 > (fd = open(file_path, O_RDONLY))) {
perror("open error");
exit(-1);
}
if (0 > read(fd, &val, 1)) {
perror("read error");
close(fd);
exit(-1);
}
printf("value: %c\n", val);
/* 退出程式 */
close(fd);
exit(0);
}
執行程式時需要傳入一個參數,argv[1]指定要讀取電平狀态的GPIO 對應的編号。
上述代碼中首先使用access()函數判斷指定編号的GPIO 引腳是否已經導出,若未導出,則通過
“/sys/class/gpio/export"檔案将其導出;導出之後先配置了GPIO 引腳為輸入模式,也就是向direction 檔案中寫入"in”;接着再配置極性、設定GPIO 引腳為非中斷模式(向edge 屬性檔案中寫入"none")。
最後打開value 屬性檔案,讀取GPIO 的電平狀态并将其列印出來。
使用交叉編譯工具編譯應用程式,如下所示:
GPIO 應用程式設計之中斷
在應用層可以将GPIO 配置為中斷觸發模式,譬如将GPIO 配置為上升沿觸發、下降沿觸發或者邊沿觸發,本小節我們來編寫一個測試程式,将GPIO 配置為邊沿觸發模式并監測中斷觸發狀态。其示例代碼如下所示:
本例程源碼對應的路徑為:開發闆CD光牒->11、Linux C 應用程式設計例程源碼->16_gpio->gpio_intr.c。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <poll.h>
static char gpio_path[100];
static int gpio_config(const char *attr, const char *val)
{
char file_path[100];
int len;
int fd;
sprintf(file_path, "%s/%s", gpio_path, attr);
if (0 > (fd = open(file_path, O_WRONLY))) {
perror("open error");
return fd;
}
len = strlen(val);
if (len != write(fd, val, len)) {
perror("write error");
return -1;
}
close(fd); //關閉檔案
return 0;
}
int main(int argc, char *argv[])
{
struct pollfd pfd;
char file_path[100];
int ret;
char val;
/* 校驗傳參 */
if (2 != argc) {
fprintf(stderr, "usage: %s <gpio>\n", argv[0]);
exit(-1);
}
/* 判斷指定編号的GPIO是否導出 */
sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]);
if (access(gpio_path, F_OK)) {//如果目錄不存在 則需要導出
int len;
int fd;
if (0 > (fd = open("/sys/class/gpio/export", O_WRONLY))) {
perror("open error");
exit(-1);
}
len = strlen(argv[1]);
if (len != write(fd, argv[1], len)) {//導出gpio
perror("write error");
exit(-1);
}
close(fd); //關閉檔案
}
/* 配置為輸入模式 */
if (gpio_config("direction", "in"))
exit(-1);
/* 極性設定 */
if (gpio_config("active_low", "0"))
exit(-1);
/* 配置中斷觸發方式: 上升沿和下降沿 */
if (gpio_config("edge", "both"))
exit(-1);
/* 打開value屬性檔案 */
sprintf(file_path, "%s/%s", gpio_path, "value");
if (0 > (pfd.fd = open(file_path, O_RDONLY))) {
perror("open error");
exit(-1);
}
/* 調用poll */
pfd.events = POLLPRI; //隻關心高優先級資料可讀(中斷)
read(pfd.fd, &val, 1);//先讀取一次清除狀态
for ( ; ; ) {
ret = poll(&pfd, 1, -1); //調用poll
if (0 > ret) {
perror("poll error");
exit(-1);
}
else if (0 == ret) {
fprintf(stderr, "poll timeout.\n");
continue;
}
/* 校驗高優先級資料是否可讀 */
if(pfd.revents & POLLPRI) {
if (0 > lseek(pfd.fd, 0, SEEK_SET)) {//将讀位置移動到頭部
perror("lseek error");
exit(-1);
}
if (0 > read(pfd.fd, &val, 1)) {
perror("read error");
exit(-1);
}
printf("GPIO中斷觸發<value=%c>\n", val);
}
}
/* 退出程式 */
exit(0);
}
執行程式時需要傳入一個參數,argv[1]指定要讀取電平狀态的GPIO 對應的編号。
上述代碼中首先使用access()函數判斷指定編号的GPIO 引腳是否已經導出,若未導出,則通過
"/sys/class/gpio/export"檔案将其導出。
對GPIO 進行配置:配置為輸入模式、配置極性、将觸發方式配置為邊沿觸發。
打開value 屬性檔案,擷取到檔案描述符,接着使用poll()函數對value 的檔案描述符進行監視,這裡為什麼要使用poll()監視、而不是直接對檔案描述符進行讀取操作?這裡簡單的描述一下。
13.2.3 小節給大家詳細介紹了poll()函數,這裡不再重述!poll()函數可以監視一個或多個檔案描述符上的I/O 狀态變化,譬如POLLIN、POLLOUT、POLLERR、POLLPRI 等,其中POLLIN 和POLLOUT 表示普通優先級資料可讀、可寫,而POLLPRI 表示有高優先級資料可讀取,中斷就是一種高優先級事件,當中斷觸發時表示有高優先級資料可被讀取。當然,除此之外還可使用13.4 小節所介紹的異步I/O 方式來監視
GPIO 中斷觸發。
使用交叉編譯工具編譯應用程式,如下所示:
在開發闆上測試
前面我們編寫了3 個測試程式,并編譯得到了對應的可執行檔案,本小節一個一個進行測試。在測試之前,選擇一個測試引腳,這裡筆者以闆子上的GPIO1_IO01 引腳為例,該引腳在底闆上已經引出,如下所示:
Mini 開發闆可以通過背面絲印标注的名稱或原理圖進行确認。
GPIO 輸出測試
将示例代碼16.2.1 編譯的到的可執行檔案拷貝到開發闆Linux 系統使用者家目錄下,執行該應用程式控制開發闆上的GPIO1_IO01 引腳輸出高或低電平:
./testApp 1 1 #控制GPIO1_IO01 輸出高電平
./testApp 1 0 #控制GPIO1_IO01 輸出低電平
執行相應的指令後,可以使用萬用表或者連接配接一個LED 小燈進行檢驗,以驗證明驗結果!
GPIO 輸入測試
将示例代碼16.3.1 編譯的到的可執行檔案拷貝到開發闆Linux 系統使用者家目錄下,執行該應用程式以讀取GPIO1_IO01 引腳此時的電平狀态,是高電平還是低電平?
首先通過杜邦線将GPIO1_IO01 引腳連接配接到闆子上的3.3V 電源引腳上,接着執行指令讀取GPIO 電平狀态:
列印出的value 等于1,表示讀取到GPIO 的電平确實是高電平;接着将GPIO1_IO01 引腳連接配接到闆子上的GND 引腳上,執行指令:
列印出的value 等于0,表示讀取到GPIO 的電平确實是低電平;測試結果與實際相符合!
GPIO 中斷測試
将示例代碼16.4.1 編譯的到的可執行檔案拷貝到開發闆Linux 系統使用者家目錄下,執行該應用程式可以監測GPIO 的中斷觸發。
執行應用程式監測GPIO1_IO01 引腳的中斷觸發情況,如下所示:
./testApp 1 # 監測GPIO1_IO01 引腳中斷觸發
當執行指令之後,我們可以使用杜邦線将GPIO1_IO01 引腳連接配接到GND 或3.3V 電源引腳上,來回切換,使得GPIO1_IO01 引腳的電平狀态發生由高到低或由低到高的狀态變化,以驗證GPIO 中斷的邊沿觸發情況;當發生中斷時,終端将會列印相應的資訊,如上圖所示。
Tips:測試完成後按Ctrl+C 退出程式!