目录

深入理解GPIO控制:从概念到实践

1. 什么是GPIO控制?

GPIO(General-purpose input/output)即通用输入输出端口,是嵌入式设备中非常基础的一部分。它们允许嵌入式系统与外界环境交互,可以被配置为输入或输出模式。在输入模式下,GPIO可以读取来自传感器、开关等外部设备的信号;在输出模式下,它可以控制LED灯、电机等外部设备。GPIO是硬件和软件之间通信的桥梁,通过编程可以灵活地控制它们进行各种操作。

GPIO控制则是指通过软件对这些GPIO引脚的电平状态进行读取和设置,实现对外接设备的控制和数据交换。

在嵌入式设备中,许多IO端口可以通过内部的复用机制被配置为特定的功能,如SPI(串行外设接口)、UART(通用异步收发传输器)、USB(通用串行总线)或网口等接口的引脚。这时,对这些引脚的控制通常需要通过特定的硬件控制器来进行,比如SPI控制器、UART控制器等。虽然物理引脚可能相同,但其在软件层面的控制方式会根据其配置的功能而有所不同。因此,当IO端口被设置为SPI、UART、USB或网络接口时,其控制通常不再称作是GPIO控制,而是称为相应接口的控制。例如,当一个IO端口被配置为SPI的MOSI(主设备输出从设备输入)线时,它就是SPI接口的一部分,由SPI控制器进行数据传输的控制,而不是作为一个通用的输入输出端来直接控制。

这里只介绍做为普通IO使用的GPIO的控制方法。掌握其控制方法对于嵌入式软件开发至关重要。

2. 命令行控制 – sysfs控制方法

在Linux系统中,GPIO设备可通过文件系统进行操作,OpenWRT亦采用这种方式。这种方式也被称为sysfs控制,即通过sysfs文件系统对GPIO设备进行控制。

sysfs (system filesystem)是 Linux 系统中的一种虚拟文件系统,它在 /sys 目录下提供一个文件系统视图,映射了内核的设备模型。主要用于导出内核对象的信息到用户空间,允许用户态应用程序以文件的形式访问设备和驱动程序的属性。

sysfs 是在 2.6 内核中引入的,旨在代替旧的 proc 文件系统中用于系统信息的部分,并提供一种结构化的方法来访问内核对象的属性。sysfs更详细的说明

具体而言,在/sys/class/gpio/目录下,每个GPIO都被映射为一个文件夹。通过对这些文件的读写,可以控制GPIO引脚的行为。

首先,要操作一个GPIO,要用export文件将其注册到用户空间。例如,通过执行echo GPIO8 > export命令,系统会在/sys/class/gpio/下创建一个名为gpio8的文件夹,其中GPIO8是要控制的GPIO编号。

在这个文件夹内部,有这些文件,包括:

  1. active_low:该文件用于设置GPIO引脚的电平触发方式。写入"1"意味着将该引脚的电平触发逻辑翻转(即,硬件上的高电平被视作逻辑上的低电平,硬件上的低电平被视作逻辑上的高电平)。写入"0"则表示使用正常的电平触发逻辑。

  2. direction:通过这个文件可以设置GPIO引脚是作为输入还是输出。写入"out"将GPIO配置为输出模式,写入"in"则将其配置为输入模式。

  3. edge:这个文件用于配置中断触发边缘,只对设置为输入模式的GPIO有效。可写入的值通常有:"none"(无触发),"rising"(上升沿触发),"falling"(下降沿触发),或"both"(上升沿和下降沿都触发)。

  4. uevent:这个文件允许用户向用户空间发送事件,通知关于GPIO引脚状态的变化。

  5. value:这个文件表示GPIO引脚的当前值。对于输出模式的GPIO,写入"1"或"0"可以改变引脚的电平状态。对于输入模式的GPIO,读取这个文件可以获取当前引脚的电平状态("1"表示高电平,"0"表示低电平)。

使用这些文件时,通常需要具有相应权限的用户操作,这通常意味着需要root权限或者通过配置udev规则授权给特定用户。

例如,要将GPIO8设置为输出并输出高电平,可以使用以下命令:

echo out > /sys/class/gpio/gpioGPIO8/direction //设置为输出
echo 1 > /sys/class/gpio/gpioGPIO8/value       //输出高电平

在/sys/class/gpio/gpioGPIO8/目录下,只需要执行以下命令即可将GPIO8设置为输出并输出高电平:

echo out > direction //设置为输出
echo 1 > value       //输出高电平

如要将GPIO8的设置为输入并读取,可以使用如下命令:

echo in > /sys/class/gpio/gpioGPIO8/direction   //设置为输入
cat /sys/class/gpio/gpioGPIO8/value             //读取输入电平

在/sys/class/gpio/gpioGPIO8/目录下,只需要执行以下命令即可将GPIO8设置为输入并读取:

echo in > direction //设置为输入
cat value           //读取输入电平

如果要取消对GPIO的控制,可以使用unexport文件。例如,执行echo GPIO8 > unexport命令即可取消对GPIO8的控制。

可能会遇到的问题:

  1. Device or resource busy 错误

如果执行echo GPIO8 > export命令时,得到Device or resource busy的错误,说明GPIO8已经被其他进程占用。此时,可以通过lsof命令查看占用GPIO8的进程,然后通过kill命令杀死该进程,再重新执行echo GPIO8 > export命令即可。

lsof 代表“List Open Files”,此命令用于查看文件被哪些进程占用。例如,执行lsof /sys/class/gpio/gpioGPIO8/value命令,可以查看占用GPIO8的进程。执行kill -9 PID命令杀死该进程,其中PID为进程号。注意,使用 -9 选项应谨慎对待,因为它不允许进程正常清理和关闭。只有在进程无法正常终止时,才推荐使用这个选项。lsof可能需要单独安装。

  1. Permission denied错误

如果在/sys/class/gpio/gpioGPIO8/目录下执行echo 1 > value命令,得到Permission denied的错误。这是因为,GPIO的控制需要root权限,因此,要在此目录下执行命令,需要先使用sudo su命令切换到root用户。或者使用使用 sudo 命令在提升权限的情况下执行命令,如:

sudo sh -c 'echo 1 > /sys/class/gpio/gpio8/value' //令 gpio8输出高电平

sh 是 Bourne shell 的简写,它是许多shell的前身,如 Bash(Bourne Again shell)等。sh 可以执行命令和程序,提供用户界面来与操作系统交互。

sh -c 是使用 Bourne shell 来执行一个字符串中的命令。这里的 -c 选项告诉 sh,随后的字符串参数是要执行的命令。这个选项常常与 sudo 结合使用来执行那些需要管理员权限的命令,尤其是在命令中涉及重定向操作时,如这里的">"操作符。

如果不使用sh -c,而是直接运行:

sudo echo 1 > /sys/class/gpio/gpio8/value

在这种情况下,sudo 只会提升 echo 1 命令的权限,而不会提升重定向符号 > 的权限。由于 /sys/class/gpio/gpio8/value 文件通常需要 root 权限才能写入,将会收到 Permission denied 错误。

如果遇到其他错误,可以通过dmesg命令查看系统日志,以便找到错误原因。

如果还有其他问题,可以参考OpenWRT官方文档

通过这些基本命令,可快速上手GPIO控制,实现简单的硬件交互。这种简洁的命令行控制方法不仅方便实用,而且在开发过程中可以快速验证硬件功能,是硬件调试不可或缺的工具。

3. 在C/C++中控制GPIO

在C++中,我们可以使用标准库中的文件流操作(fstream)来操作sysfs文件系统,从而实现对GPIO的控制。以下是一个简单的例程,它将GPIO8设置为输出,并输出高电平:

#include <fstream>
#include <string>

void exportGPIO(const std::string& gpio) {
    std::ofstream exportFile("/sys/class/gpio/export");
    if (exportFile.is_open()) {
        exportFile << gpio;
        exportFile.close();
    } else {
        // 文件打开失败,可能是权限问题
    }
}

void unexportGPIO(const std::string& gpio) {
    std::ofstream unexportFile("/sys/class/gpio/unexport");
    if (unexportFile.is_open()) {
        unexportFile << gpio;
        unexportFile.close();
    } else {
        // 文件打开失败,可能是权限问题
    }
}

void setGPIODirection(const std::string& gpio, const std::string& direction) {
    std::ofstream directionFile("/sys/class/gpio/gpio" + gpio + "/direction");
    if (directionFile.is_open()) {
        directionFile << direction;
        directionFile.close();
    } else {
        // 文件打开失败,可能是权限问题或者GPIO编号错误
    }
}

void setGPIOValue(const std::string& gpio, const std::string& value) {
    std::ofstream valueFile("/sys/class/gpio/gpio" + gpio + "/value");
    if (valueFile.is_open()) {
        valueFile << value;
        valueFile.close();
    } else {
        // 文件打开失败,可能是权限问题或者GPIO编号错误
    }
}

std::string getGPIODirection(const std::string& gpio) {
    std::string direction;
    std::ifstream directionFile("/sys/class/gpio/gpio" + gpio + "/direction");
    if (directionFile.is_open()) {
        directionFile >> direction;
        directionFile.close();
    } else {
        // 文件打开失败,可能是权限问题或者GPIO编号错误
    }
    return direction;
}

std::string getGPIOValue(const std::string& gpio) {
    std::string value;
    std::ifstream valueFile("/sys/class/gpio/gpio" + gpio + "/value");
    if (valueFile.is_open()) {
        valueFile >> value;
        valueFile.close();
    } else {
        // 文件打开失败,可能是权限问题或者GPIO编号错误
    }
    return value;
}

int main() { 
    std::string gpio = "8"; // 要操作的GPIO编号
    exportGPIO(gpio); // 导出GPIO到用户空间
    setGPIODirection(gpio, "out"); // 设置为输出
    setGPIOValue(gpio, "1"); // 输出高电平
    unexportGPIO(gpio);// 取消导出

    std::string gpio = "9"; // 要操作的GPIO编号
    exportGPIO(gpio);   // 导出GPIO到用户空间
    setGPIODirection(gpio, "in"); // 设置为输入
    std::string direction = getGPIODirection(gpio); // 获取方向
    std::string value = getGPIOValue(gpio); // 获取值
    std::cout << "GPIO" << gpio << " direction: " << direction << ", value: " << value << std::endl;
    unexportGPIO(gpio);// 取消导出
    return 0;   // 程序正常退出
}

这段程序是用C++编程语言编写的,用于操作Linux系统中的GPIO设备。程序中定义了一系列函数,通过sysfs文件系统对GPIO设备进行导出(export)、取消导出(unexport)、设置方向(setGPIODirection)、设置值(setGPIOValue)、获取方向(getGPIODirection)和获取值(getGPIOValue)等操作。

在main函数中,程序首先定义了要操作的GPIO编号(这里是"8"),然后按照顺序调用exportGPIO、setGPIODirection、setGPIOValue和unexportGPIO函数,将GPIO8导出到用户空间,设置为输出并输出高电平,然后将其从用户空间取消导出。

接着,程序定义了另一个要操作的GPIO编号("9"),然后按照顺序调用exportGPIO、setGPIODirection、getGPIODirection、getGPIOValue和unexportGPIO函数,将GPIO9导出到用户空间,设置为输入,获取其方向和值,然后将其从用户空间取消导出。获取到的方向和值被打印出来。

这个程序需要以root权限运行,因为操作sysfs通常需要root权限。如果在运行时遇到权限问题,可以使用sudo命令运行这个程序。

在上面的C++ GPIO控制程序中,选择了使用sysfs文件系统来实现GPIO的操作。这种方法有其独特的优点,但也存在一些不足。让我们一起来深入了解一下。

首先,sysfs控制方法的最大优点就是其简单易用性。通过文件系统,我们可以直观地访问和控制GPIO,无需深入了解底层硬件的复杂细节。这使得我们的程序编写过程变得更加轻松,也使得代码更易于理解和维护。

其次,sysfs控制方法具有良好的跨平台兼容性。由于sysfs是Linux内核的一部分,因此我们的程序可以在各种Linux发行版和硬件平台上运行,这大大增加了我们程序的适用范围。

此外,sysfs控制方法还具有易于调试的优点。我们可以直接在命令行中查看和修改GPIO的状态,这对于快速验证我们的程序和排查问题非常有帮助。

然而,尽管sysfs控制方法有这么多优点,但它也存在一些不足。首先,由于sysfs是基于文件系统的,因此在进行GPIO操作时,需要进行文件读写操作。这种方式的性能相对于直接访问硬件寄存器的方法要低一些。对于对实时性要求较高的应用,这可能会成为一个问题。

其次,操作sysfs文件通常需要root权限,这可能会带来安全问题。虽然我们可以通过配置udev规则来授权给特定用户,但这会增加配置的复杂性。

最后,sysfs方法不再被推荐使用。这是因为在新的内核系统中,4.8版本之后,GPIO Character Device API已经取代了sysfs方法,成为了更加现代、更高效、更灵活的GPIO控制方法。

总的来说,sysfs控制方法在简单易用、跨平台兼容性和易于调试等、较早的linux系统中使用方面具有优势,但在性能、权限和可移植性方面存在一定的不足。在选择使用这种方法时,我们需要根据具体的应用需求和场景进行权衡。

4. 在C/C++中控制GPIO – GPIO Character Device API控制方法

在上一部分中,我们讨论了如何使用sysfs文件系统在C++中控制GPIO,以及这种方法的优点和不足。现在,我们使用GPIO Character Device API来控制GPIO。这种新的API提供了一种更现代、更高效、更灵活的方式来控制GPIO。是在Linux内核版本4.8之后引入的,因此在老的系统中无法使用。使用之前要查看所用系统的内核版本是否支持。

要使用GPIO Character Device API,首先需要包含linux/gpio.h头文件。这个头文件包含了所有与GPIO操作相关的API函数和数据结构。接下来,我们将介绍如何使用这些API函数来实现GPIO的导出、设置方向、设置值、获取值等操作。

4.1. 打开GPIO设备

在使用GPIO Character Device API之前,首先需要打开GPIO设备。这可以通过open系统调用来实现。例如,要打开GPIO设备/dev/gpiochip0,可以使用以下代码:

#include <fcntl.h>
#include <unistd.h>

int gpio_fd = open("/dev/gpiochip0", O_RDWR);
if (gpio_fd == -1) {
    // 打开设备失败,可能是权限问题或者设备不存在
}

这里,O_RDWR表示以读写模式打开设备。如果打开成功,open函数将返回一个文件描述符,用于后续的GPIO操作。如果打开失败,open函数将返回-1。

4.2. 导出GPIO

在使用GPIO之前,需要将其导出到用户空间。这可以通过ioctl系统调用和GPIO_GET_LINEHANDLE_IOCTL命令来实现。例如,要导出GPIO8,可以使用以下代码:

#include <linux/gpio.h>
#include <sys/ioctl.h>

struct gpiohandle_request req;
req.lineoffsets[0] = 8;
req.lines = 1;
req.flags = GPIOHANDLE_REQUEST_OUTPUT;
strcpy(req.consumer_label, "my_gpio");

if (ioctl(gpio_fd, GPIO_GET_LINEHANDLE_IOCTL, &req) == -1) {
    // 导出GPIO失败,可能是权限问题或者GPIO编号错误
}

这里,我们首先定义了一个gpiohandle_request结构体,并设置了相应的参数。lineoffsets数组表示要导出的GPIO编号,lines表示要导出的GPIO数量(这里只导出一个GPIO),flags表示GPIO的初始方向(这里设置为输出),consumer_label表示使用这个GPIO的应用程序名称(可以自定义)。

然后,我们调用ioctl函数,传入GPIO_GET_LINEHANDLE_IOCTL命令和gpiohandle_request结构体。如果导出成功,ioctl函数将返回0,否则返回-1。

4.3. 设置GPIO方向和值

在导出GPIO之后,可以使用gpiohandle_data结构体来设置GPIO的方向和值。例如,要将GPIO8设置为输出并输出高电平,可以使用以下代码:

struct gpiohandle_data data;
data.values[0] = 1;

if (ioctl(req.fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &data) == -1) {
    // 设置GPIO值失败,可能是权限问题或者GPIO编号错误
}

这里,我们首先定义了一个gpiohandle_data结构体,并设置了相应的参数。values数组表示要设置的GPIO值(这里设置为1,表示高电平)。

然后,我们调用ioctl函数,传入GPIOHANDLE_SET_LINE_VALUES_IOCTL命令和gpiohandle_data结构体。如果设置成功,ioctl函数将返回0,否则返回-1。

4.4. 获取GPIO方向和值

要获取GPIO的方向和值,可以使用gpiohandle_data结构体和ioctl系统调用。例如,要获取GPIO8的方向和值,可以使用以下代码:

struct gpiohandle_data data;

if (ioctl(req.fd, GPIOHANDLE_GET_LINE_VALUES_IOCTL, &data) == -1) {
    // 获取GPIO值失败,可能是权限问题或者GPIO编号错误
} else {
    int value = data.values[0];
    // 处理获取到的GPIO值
}

这里,我们调用ioctl函数,传入GPIOHANDLE_GET_LINE_VALUES_IOCTL命令和gpiohandle_data结构体。如果获取成功,ioctl函数将返回0,否则返回-1。获取到的GPIO值存储在values数组中。

4.5. 取消导出GPIO

在使用完GPIO之后,需要将其从用户空间取消导出。这可以通过关闭GPIO设备文件来实现。例如,要取消导出GPIO8,可以使用以下代码:

close(req.fd);

这里,我们调用close函数,传入之前导出GPIO时获取到的文件描述符。如果关闭成功,close函数将返回0,否则返回-1。

4.6. 示例程序

下面是一个完整的C++程序,使用GPIO Character Device API来控制GPIO8:

#include <fcntl.h>
#include <unistd.h>
#include <linux/gpio.h>
#include <sys/ioctl.h>
#include <iostream>

int main() {
    int gpio_fd = open("/dev/gpiochip0", O_RDWR);
    if (gpio_fd == -1) {
        std::cerr << "Failed to open GPIO device" << std::endl;
        return 1;
    }

    struct gpiohandle_request req;
    req.lineoffsets[0] = 8;
    req.lines = 1;
    req.flags = GPIOHANDLE_REQUEST_OUTPUT;
    strcpy(req.consumer_label, "my_gpio");

    if (ioctl(gpio_fd, GPIO_GET_LINEHANDLE_IOCTL, &req) == -1) {
        std::cerr << "Failed to export GPIO" << std::endl;
        close(gpio_fd);
        return 1;
    }

    struct gpiohandle_data data;
    data.values[0] = 1;

    if (ioctl(req.fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &data) == -1) {
        std::cerr << "Failed to set GPIO value" << std::endl;
        close(req.fd);
        close(gpio_fd);
        return 1;
    }

    close(req.fd);
    close(gpio_fd);
    return 0;
}

这个程序首先打开GPIO设备,然后导出GPIO8,设置为输出并输出高电平,最后将其从用户空间取消导出。注意,这个程序需要以root权限运行,因为操作GPIO设备通常需要root权限。如果在运行时遇到权限问题,可以使用sudo命令运行这个程序。

5. 在C/C++中控制GPIO – 编写内核程序的控制方法

上述的GPIO Character Device API是在linux内核版本4.8之后引入的,因此在老的系统中无法使用。如果所用内核版本较老,我们可以使用编写内核程序的方法来控制GPIO。

5.1. 内核程序

内核程序是位于内核空间的软件组件,它提供了对GPIO引脚的直接控制。该程序使得操作系统能够设置GPIO引脚的模式(输入、输出),并对它们进行读写操作。在嵌入式Linux系统中,如OpenWrt,这个内核程序通常作为内核模块存在,使得用户可以动态地加载和卸载它,以适应不同的硬件和需求。

在这里借用别人写的一个例子,来说明如何编写内核程序来控制GPIO。是用C语言写的,头文件与代码如下。

gpio_control_driver.h:

/*
 * gpio_control_driver.h
 *
 *  Created on: 2017-9-10
 *      Author: robinson
 */

#ifndef GPIO_CONTROL_DRIVER_H_
#define GPIO_CONTROL_DRIVER_H_
#include <linux/cdev.h>
#include <linux/ioctl.h>
#include <linux/kfifo.h>
#define GET_GPIO_NUM(arg1) (unsigned char)((arg1 >> 24) & 0xff)
#define GET_GPIO_VALUE(arg1) (unsigned char)((arg1 >> 16) & 0xff)
#define GPIO_CONTROL_MAJOR                  99//device major number
#define GPIO_CONTROL_DEV_NAME       "gpio_control"
//IOCTRL CMDs
#define GPIO_CONTROL_SET_OUT            0x01
#define GPIO_CONTROL_SET_IN         0x02
//#define GPIO_CONTROL_GET_DIRECTION        0x03
#define GPIO_CONTROL_SET_VALUE          0x04
#define GPIO_CONTROL_GET_VALUE          0x05
#define GPIO_CONTROL_REQUEST_GPIO       0x06
#define GPIO_CONTROL_FREE_GPIO          0x07
#endif /* GPIO_CONTROL_DRIVER_H_ */

gpio_control_driver.c:

/*
 *  Gpio control driver
 *
 *  Copyright (C) 2017 (C) <wurobinson@zhuotk.com>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 * 
 * Description: This driver is written for JS9331 and JS7628 development board. You can use it
 *      to control gpios
 *
 */

#include <linux/types.h> //这个头文件包含了Linux内核使用的一些基本数据类型的定义,例如__u8, __u16, __u32等。
#include <linux/gpio.h>   //这个头文件包含了GPIO(General Purpose Input/Output)系统的相关定义和函数,如gpio_direction_input(), gpio_direction_output(), gpio_set_value()等。
#include <linux/timer.h>  //提供定时器相关的函数和数据结构,如timer_list结构体,add_timer(), del_timer()等。
#include <linux/cdev.h>   //字符设备驱动开发的相关函数和结构体,如cdev_init(), cdev_add()等。
#include <linux/fs.h> //包含了文件系统相关的一些基本定义和函数,如文件操作的结构体file_operations。
#include <linux/module.h> //包含模块相关的一些函数和宏,如MODULE_LICENSE, MODULE_AUTHOR, THIS_MODULE等。
#include <linux/init.h>   //提供模块初始化和退出的宏定义,如module_init, module_exit。
#include <linux/irq.h>    //底层的IRQ处理,例如IRQ分配、释放、使能和禁止等
#include <linux/hrtimer.h>    //高分辨率定时器(High Resolution Timer)的相关函数和数据结构。
#include <linux/stat.h>   //文件状态的相关定义,例如文件权限。
#include <linux/device.h> //设备模型的相关函数和数据结构,例如device_create(),device_destroy()等。
#include <linux/interrupt.h>  //包含中断处理相关的函数和数据结构,例如request_irq(), free_irq()等。
#include <linux/time.h>   //时间相关的函数和数据结构,如用于获取系统时间的函数等。
#include <asm-generic/errno-base.h>   //定义了一些基本的错误号,如EINTR, ENOMEM, EBUSY等。
#include <linux/miscdevice.h> //为杂项设备提供的接口,例如misc_register(), misc_deregister()等。
#include "gpio_control_driver.h" //包含了GPIO控制驱动需要的各种定义和函数原型。

static int gpio_control_open(struct inode *pinode, struct file *pfile) //定义设备打开函数
{
    printk("***%s***\n",__func__);
    //initialize

    return 0;
}

static int gpio_control_release(struct inode *pinode, struct file *pfile) //定义设备关闭函数
{
    printk("***%s***\n",__func__);

    return 0;
}

static long gpio_control_ioctl(struct file *pfile, unsigned int cmd, unsigned long arg) //定义IO控制函数,ioctl是Linux系统中的一个系统调用,全称是"input/output control",即输入/输出控制。它提供了一种从用户空间向内核空间传递信息的方式,通常用于设备驱动程序中。
{
    int ret;
    unsigned char gpio_number;
    unsigned char gpio_value;

    //printk("***%s***\n",__func__);
    //printk("cmd:0x%02X\n", cmd);

    gpio_number = GET_GPIO_NUM(arg);
    gpio_value  = GET_GPIO_VALUE(arg);
    //printk("gpio number:%d\n", gpio_number);
    //printk("gpio value:0x%02X\n", gpio_value);

    switch (cmd){
    case GPIO_CONTROL_SET_OUT:
        //printk("command: GPIO_CONTROL_SET_OUT\n");
        ret = gpio_direction_output(gpio_number, gpio_value);
        if (ret < 0){
            //printk("###gpio_direction_output ERROR: can't set gpio %d output###\n", gpio_number);
            return -1;
        }
        //printk("command: GPIO_CONTROL_SET_OUT done\n");
        break;

    case GPIO_CONTROL_SET_IN:
        ret = gpio_direction_input(gpio_number);
        if (ret < 0){
            //printk("###gpio_direction_input ERROR: can't set gpio %d input###\n", gpio_number);
            return -1;
        }
        //printk("command: GPIO_CONTROL_SET_IN\n");
        break;
#if 0
    case GPIO_CONTROL_GET_DIRECTION:

        printk("command: GPIO_CONTROL_GET_DIRECTION\n");
        break;
#endif
    case GPIO_CONTROL_SET_VALUE:
        gpio_set_value(gpio_number, gpio_value);
        //printk("command: GPIO_CONTROL_SET_VALUE\n");
        break;

    case GPIO_CONTROL_GET_VALUE:
        ret = gpio_get_value(gpio_number);
        if (ret < 0){
            printk("###gpio_get_value ERROR: can't get gpio %d value###\n", gpio_number);
            return -1;
        }
        //printk("command: GPIO_CONTROL_GET_VALUE, value is %d\n", ret);
        break;

    case GPIO_CONTROL_REQUEST_GPIO:
        //printk("command: GPIO_CONTROL_REQUEST_ONE\n");
        if (0 > gpio_request(gpio_number, "gpio_ctrl")){
            //printk("###gpio_request ERROR: can't request %d pin for output###\n", gpio_number);
            return -1;
        }
        //printk("command: GPIO_CONTROL_REQUEST_GPIO done\n");
        break;

    case GPIO_CONTROL_FREE_GPIO:
        gpio_free(gpio_number);
        //printk("command: GPIO_CONTROL_FREE_GPIO done\n");
        break;

    default:
        printk("***Unknown command:0x%02X\n***\n", cmd);
        break;

    }

    return ret;
}

static const struct file_operations gpio_control_ops = { //定义设备操作函数
        .owner          = THIS_MODULE,  //THIS_MODULE是一个宏,表示当前模块的结构体指针。
        .open           = gpio_control_open,    //open字段是一个函数指针,指向设备的打开函数gpio_control_open。这个函数在用户空间程序打开设备时被调用。
        .release        = gpio_control_release, //release字段是一个函数指针,指向设备的释放函数gpio_control_release。这个函数在用户空间程序关闭设备时被调用。
        .unlocked_ioctl = gpio_control_ioctl, //unlocked_ioctl字段是一个函数指针,指向设备的IO控制函数gpio_control_ioctl。这个函数在用户空间程序发送IO控制命令时被调用。
};

static struct miscdevice s_gpio_control_dev = { //定义设备结构体
        .minor = MISC_DYNAMIC_MINOR,
        .fops = &gpio_control_ops,
        .name = GPIO_CONTROL_DEV_NAME
};

//module initialize function
static int gpio_control_init(void) //定义模块初始化函数
{
    int result;
    //initialize and register device
    result = misc_register(&s_gpio_control_dev); //misc_register()函数用于注册一个miscdevice结构体,从而将设备注册到内核中。它接收一个指向miscdevice结构体的指针作为参数,返回0表示成功,返回负数表示失败。
    if (result != 0) {
        //printk("###misc_register error###\n");
        return -1;
    }

    printk("**gpio_control module initiation OK**\n");
    return result;
}

//module exit fuc
void gpio_control_exit(void)
{
    //unregister what we registered
    misc_deregister(&s_gpio_control_dev); //misc_deregister()函数用于注销一个miscdevice结构体,从而将设备从内核中注销。它接收一个指向miscdevice结构体的指针作为参数,返回0表示成功,返回负数表示失败。

    printk("**gpio_control module exit**\n");
}

module_init(gpio_control_init); //module_init()宏用于指定模块初始化函数,即模块加载时被调用的函数。
module_exit(gpio_control_exit); //module_exit()宏用于指定模块退出函数,即模块卸载时被调用的函数。

MODULE_VERSION("V1.0"); //MODULE_VERSION()宏用于指定模块的版本号。
MODULE_AUTHOR("wurobinson <wurobinson@zhuotk.com>"); //MODULE_AUTHOR()宏用于指定模块的作者。
MODULE_LICENSE("Dual BSD/GPL"); //MODULE_LICENSE()宏用于指定模块的许可证。

程序并不复杂易于理解,已经为大部分程序添加了注释。程序的核心功能是通过IO控制函数(gpio_control_ioctl)来实现GPIO的控制。这个函数根据用户空间程序发送的IO控制命令来执行相应的GPIO操作,如设置GPIO的输入/输出方向(gpio_direction_input/gpio_direction_output)、设置GPIO的值(gpio_set_value)、获取GPIO的值(gpio_get_value)、请求GPIO(gpio_request)和释放GPIO(gpio_free)。

总的来说,这个程序通过创建一个字符设备驱动,为用户空间的程序提供了一个接口,使得用户空间的程序可以通过发送IO控制命令到这个设备驱动,来实现对GPIO的控制。编译后可以得到一个内核模块,可以通过insmod命令加载到内核中,通过rmmod命令卸载。

5.2. 用户空间的GPIO控制程序

上面的gpio_control_driver内核程序为用户空间提供了一个接口,使得用户空间的程序可以通过发送IO控制命令到这个设备驱动,来实现对GPIO的控制。下面我们来编写一个用户空间的GPIO控制程序,来实现对GPIO的控制。

这个程序是用C++编程语言编写的,用于操作Linux系统中的GPIO设备。程序中定义了一系列函数,通过写入到特定的设备文件,或使用ioctl等系统调用与驱动程序通信,从而达到控制硬件GPIO状态的目的。

gpio_contrl_app.h:

#ifndef GPIO_CONTROL_APP_H_
#define GPIO_CONTROL_APP_H_

#define GPIO_CONTROL_DEVICE_PATH        "/dev/gpio_control"

#define GPIO_IOCTL_PRAM(gpio_num, arg1) (((unsigned long)gpio_num << 24) + ((unsigned long)arg1 << 16)) //定义IO控制命令的参数,用于向驱动程序发送IO控制命令。
#define GET_GPIO_NUM(arg1) (unsigned char)((arg1 >> 24) & 0xff) 
#define GET_GPIO_VALUE(arg1) (unsigned char)((arg1 >> 16) & 0xff) 

//IOCTRL CMDs
#define GPIO_CONTROL_SET_OUT            0x01
#define GPIO_CONTROL_SET_IN         0x02
//#define GPIO_CONTROL_GET_DIRECTION        0x03
#define GPIO_CONTROL_SET_VALUE          0x04
#define GPIO_CONTROL_GET_VALUE          0x05
#define GPIO_CONTROL_REQUEST_GPIO       0x06
#define GPIO_CONTROL_FREE_GPIO          0x07

#endif

gpio_control_app.cpp:

/*
 * gpio_control_app
 *
 * Copyright (C) 2017 wurobinson <wurobinson@zhuotk.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is written for JS7628 and JS9331 development board ,
 * work with gpio_control_driver
 */

#include "gpio_control_app.hpp"

void GPIOControl::release_gpio()    //定义释放GPIO的函数           
{
    ioctl(gpio_dev_fd, GPIO_CONTROL_SET_IN, GPIO_IOCTL_PRAM(gpio_pin, 0));
    ioctl(gpio_dev_fd, GPIO_CONTROL_FREE_GPIO, GPIO_IOCTL_PRAM(gpio_pin, 0));
}

void GPIOControl::release_output_demo1(int signal_no) //定义释放GPIO的函数
{
    release_gpio();
    exit(0);
}

void GPIOControl::release_input_demo1(int signal_no) //定义释放GPIO的函数
{
    release_gpio();
    exit(0);
}

int GPIOControl::Set(int gpio_pin, int setvalue) // set gpio output level, return 0 or -1(error).
{
    // 取消request此GPIO
    // ioctl(gpio_dev_fd, GPIO_CONTROL_FREE_GPIO, GPIO_IOCTL_PRAM(gpio_pin, 0));
    // close(gpio_dev_fd);
    int ret = 0;
    gpio_dev_fd = open(GPIO_CONTROL_DEVICE_PATH, O_RDWR); // open gpio device
    if (gpio_dev_fd < 0)
    {
        printf("###open %s ERROR###\n", GPIO_CONTROL_DEVICE_PATH);
        return -1;
    }
    else
    {
        printf("***open %s success***\n", GPIO_CONTROL_DEVICE_PATH);
    }
    printf("Use gpio:%d\n", gpio_pin);

    ret = ioctl(gpio_dev_fd, GPIO_CONTROL_REQUEST_GPIO, GPIO_IOCTL_PRAM(gpio_pin, 0));
    if (ret < 0)
    {
        perror("ioctl error");
        printf("###request GPIO %d error###\n", gpio_pin);
        close(gpio_dev_fd);
        return -1;
    }
    ret = ioctl(gpio_dev_fd, GPIO_CONTROL_SET_OUT, GPIO_IOCTL_PRAM(gpio_pin, 0));
    if (ret < 0)
    {
        perror("ioctl error");
        printf("###set GPIO %d error###\n", gpio_pin);
        // 取消request此GPIO
        ioctl(gpio_dev_fd, GPIO_CONTROL_FREE_GPIO, GPIO_IOCTL_PRAM(gpio_pin, 0));
        close(gpio_dev_fd);
        return -1;
    }
    // signal(SIGINT, output_fun);
    printf("Set GPIO level: %d\n", setvalue);
    ioctl(gpio_dev_fd, GPIO_CONTROL_SET_VALUE, GPIO_IOCTL_PRAM(gpio_pin, setvalue));
    // 取消request此GPIO
    ioctl(gpio_dev_fd, GPIO_CONTROL_FREE_GPIO, GPIO_IOCTL_PRAM(gpio_pin, 0));
    close(gpio_dev_fd);
}

int GPIOControl::Read(int gpio_pin) // get gpio input level, return 0, 1 or -1(error).
{
    int res = -1;
    int ret = 0;
    gpio_dev_fd = open(GPIO_CONTROL_DEVICE_PATH, O_RDWR); // open gpio device
    if (gpio_dev_fd < 0)
    {
        printf("###open %s ERROR###\n", GPIO_CONTROL_DEVICE_PATH);
        return -1;
    }
    else
    {
        printf("***open %s success***\n", GPIO_CONTROL_DEVICE_PATH);
    }
    printf("Use gpio: %d\n", gpio_pin);

    ret = ioctl(gpio_dev_fd, GPIO_CONTROL_REQUEST_GPIO, GPIO_IOCTL_PRAM(gpio_pin, 0));
    if (ret < 0)
    {
        printf("###request GPIO %d error###", gpio_pin);
        close(gpio_dev_fd);
        return -1;
    }
    ret = ioctl(gpio_dev_fd, GPIO_CONTROL_SET_IN, GPIO_IOCTL_PRAM(gpio_pin, 0));
    if (ret < 0)
    {
        printf("###set GPIO %d error###", gpio_pin);
        ioctl(gpio_dev_fd, GPIO_CONTROL_FREE_GPIO, GPIO_IOCTL_PRAM(gpio_pin, 0));
        close(gpio_dev_fd);
        return -1;
    }
    res = ioctl(gpio_dev_fd, GPIO_CONTROL_GET_VALUE, GPIO_IOCTL_PRAM(gpio_pin, 0));
    printf("Get GPIO level: %d\n", res);
    ioctl(gpio_dev_fd, GPIO_CONTROL_FREE_GPIO, GPIO_IOCTL_PRAM(gpio_pin, 0));
    close(gpio_dev_fd);
    return res;
}

bool GPIOControl::Read_with_debounce(int gpio_pin, int debounce_time_ms = 50, int debounce_count_in = 5) //debounce是去抖动,去除抖动的意思,这里是去除抖动的读函数。
{
    int debounce_count = debounce_count_in;
    int stable_count = 0;

    struct timespec originalSleepTime;                               // 原始时间
    struct timespec remaining;                                       // 剩余时间
    originalSleepTime.tv_sec = debounce_time_ms / 1000;              // 秒数
    originalSleepTime.tv_nsec = (debounce_time_ms % 1000) * 1000000; // 纳秒数

    for (int i = 0; i < debounce_count; i++)
    {
        struct timespec sleepTime = originalSleepTime; // 使用预先计算的时间

        int value = Read(gpio_pin); // 读取电平
        if (value == 1)
        {
            stable_count++; // 稳定高电平计数
        }
        else
        {
            stable_count = 0;
        }

        if (stable_count >= debounce_count)
        {
            return true; // 稳定高电平读取
        }

        while (nanosleep(&sleepTime, &remaining) == -1 && errno == EINTR) // 等待一段时间
        {
            sleepTime = remaining; // 如果被信号中断,则继续等待剩余时间
        }
    }
    return false; // 稳定低电平读取
}

void GPIOControl::Get_gpio_list() //获取GPIO列表
{
    struct gpiochip_info info;
    struct gpioline_info line_info;
    int fd, ret;
    fd = open(GPIO_CONTROL_DEVICE_PATH, O_RDONLY);
    if (fd < 0)
    {
        printf("Unabled to open %s: %s", GPIO_CONTROL_DEVICE_PATH, strerror(errno));
        return;
    }
    ret = ioctl(fd, GPIO_GET_CHIPINFO_IOCTL, &info);
    if (ret == -1)
    {
        printf("Unable to get chip info from ioctl: %s", strerror(errno));
        close(fd);
        return;
    }
    printf("Chip name: %s\n", info.name);
    std::cout << "Chip name:" << info.name << std::endl;
    printf("Chip label: %s\n", info.label);
    printf("Number of lines: %d\n", info.lines);

    for (int i = 0; i < info.lines; i++)
    {
        line_info.line_offset = i;
        ret = ioctl(fd, GPIO_GET_LINEINFO_IOCTL, &line_info);
        if (ret == -1)
        {
            printf("Unable to get line info from offset %d: %s", i, strerror(errno));
        }
        else
        {
            printf("offset: %d, name: %s, consumer: %s. Flags:\t[%s]\t[%s]\t[%s]\t[%s]\t[%s]\n",
                   i,
                   line_info.name,
                   line_info.consumer,
                   (line_info.flags & GPIOLINE_FLAG_IS_OUT) ? "OUTPUT" : "INPUT",
                   (line_info.flags & GPIOLINE_FLAG_ACTIVE_LOW) ? "ACTIVE_LOW" : "ACTIVE_HIGHT",
                   (line_info.flags & GPIOLINE_FLAG_OPEN_DRAIN) ? "OPEN_DRAIN" : "...",
                   (line_info.flags & GPIOLINE_FLAG_OPEN_SOURCE) ? "OPENSOURCE" : "...",
                   (line_info.flags & GPIOLINE_FLAG_KERNEL) ? "KERNEL" : "");
        }
    }
    close(fd);
}

它使用了ioctl()函数来实现对GPIO的各种操作。在release_gpio()函数中,它将GPIO设置为输入模式并释放它。在Set()函数中,它打开GPIO设备,请求GPIO,将其设置为输出模式,设置GPIO的电平值,然后释放GPIO并关闭设备。在Read()函数中,它打开GPIO设备,请求GPIO,将其设置为输入模式,获取GPIO的电平值,然后释放GPIO并关闭设备。在Read_with_debounce()函数中,它通过多次读取GPIO的电平值并在每次读取之间等待一段时间来实现去抖动的效果。在Get_gpio_list()函数中,它打开GPIO设备,获取GPIO的信息并打印出来,然后对每一行的GPIO进行操作,获取并打印出其信息,最后关闭设备。总的来说,这个程序通过使用ioctl()函数来控制GPIO,实现了GPIO的设置、读取、释放和获取列表等功能。

可将这个程序引入到自己的项目中,然后调用相应的函数来实现GPIO的控制。比如,可以调用Set()函数来设置GPIO的电平值,调用Read()函数来获取GPIO的电平值,调用Read_with_debounce()函数来获取GPIO的去抖动电平值,调用Get_gpio_list()函数来获取GPIO的列表。

5.3. 调用示例

以下是一个简单的C++程序,它使用了上述的GPIO控制类GPIOControl来实现GPIO的读写操作。在这个程序中,我们首先创建了一个GPIOControl对象,然后使用Set()函数来设置GPIO的电平值,使用Read()函数来读取GPIO的电平值。

#include "gpio_control_app.h"

int main() {
    GPIOControl gpioControl;  // 创建GPIOControl对象
    int gpio_pin = 4;  // GPIO引脚编号
    int setvalue = 1;  // 设置的电平值

    // 设置GPIO的电平值
    if (gpioControl.Set(gpio_pin, setvalue) < 0) {
        printf("Set GPIO failed.\n");
        return -1;
    }

    // 读取GPIO的电平值
    int readvalue = gpioControl.Read(gpio_pin);
    if (readvalue < 0) {
        printf("Read GPIO failed.\n");
        return -1;
    }

    printf("Read GPIO value: %d\n", readvalue);

    return 0;
}

6. 其它GPIO的控制方法

本文是以openwrt系统为例来说明GPIO的控制方法,由于openwrt系统是基于Linux系统的,因此本文介绍的方法也适用于其它Linux系统。如果你使用的是其它系统,那么你可以参考本文的方法,来实现GPIO的控制。

如果你使用树莓派,有多种库可以用于控制GPIO。以下是一些常用的库及其使用方法:

  1. RPi.GPIO库:这是Python对树莓派的控制库,提供了很多函数可以让我们获取引脚信息、与外部设备进行数据交互等等。一般来讲,树莓派官方镜像已经默认安装了RPi.GPIO库的,我们可以用 pip3 list 来查看一下已安装库列表。如果没有安装的话,那么我们可以通过 pip3 install rpi.gpio 来对其进行安装,或者 apt-get install python3-rpi.gpio 进行安装。

  2. WiringPi库:这是应用于树莓派平台的GPIO控制库函数,其使用C或者C++开发并且可以被其他语言包转,例如python、ruby等。安装WiringPi库的命令是:sudo apt-get install wiringpi。WiringPi包括一套gpio命令,使用gpio命令可以控制树莓派上的各种接口。

  3. Bcm2835库:这是树莓派cpu芯片的库函数,相当于stm32的固件库一样,底层是直接操作寄存器。安装Bcm2835库的命令是:tar zxvf bcm2835-1.xx.tar.gz,然后进入解压后的目录,执行./configuremakesudo make checksudo make install

  4. Pi4J库:这是一个可以用Java开控制树莓派的GPIO口的库。

以上这些库都可以用于控制树莓派的GPIO,具体选择哪个库,取决于你的编程语言和具体需求。

7. 总结

在Linux系统中,控制GPIO的方法有多种,每种方法都有其适用场景和优缺点。首先,sysfs接口,这是一种通过文件系统来访问和控制GPIO的方法,它通过简单的文件操作提供GPIO的读写功能,非常直观且易于使用,但由于其性能限制以及Linux内核更新后逐渐被弃用的趋势,它可能不是长期的解决方案。

第二种方法是使用GPIO Character Device API,这是Linux内核提供的一个现代的编程接口,允许用户通过系统调用如ioctl来高效地控制GPIO。与sysfs相比,这个API提供了更丰富的功能和更好的性能,是推荐的方法。

第三种是直接在内核空间编写程序,这种方法虽然可以提供最大的灵活性和控制能力,但编程复杂,且需要深入的内核知识,对于驱动开发者来说是适用的,但一般应用程序开发者可能不需要这样做。

此外,对于不同的编程语言,还有相应的库来简化GPIO的控制。例如,在Python中,可以使用RPi.GPIO或WiringPi库来控制GPIO,这两个库提供了一套简单的API来进行GPIO操作,特别适合那些需要快速开发和原型制作的场合。尽管WiringPi已经不再维护,但其简洁的设计使得它在历史上被广泛使用,尤其是在树莓派这类单板计算机上。

总的来说,选择哪种方法和库来控制GPIO,需要根据项目的具体需求、开发者的技术背景以及所使用的硬件平台来决定。