跳到主要内容

GPIO

1. GPIO 子系统

GPIO(通用输入/输出)是一种通用引脚,可以由微控制器(MCU)或 CPU 控制,具有多种功能,包括高低电平输入检测和输出控制。在 Linux 中,GPIO 引脚可以被导出到用户空间,通过 sysfs 文件系统进行控制,使得 GPIO 引脚可以用于各种用途,如串口通信、I2C、网络通信、电压检测等。

在 Linux 中,有一个专门的 GPIO 子系统驱动框架,用于处理 GPIO 设备。通过这个框架,用户可以轻松地与 CPU 的 GPIO 引脚进行交互。这个驱动框架支持将引脚用于基本的输入和输出功能,同时输入功能还支持中断检测。这使得开发者可以利用 GPIO 子系统轻松地将 GPIO 引脚用于各种用途,实现了灵活性和可编程性。有关 GPIO 子系统的更多详细信息可以在 <Linux内核源码>/Documentation/gpio 目录中找到。

2.GPIO 控制(shell)

2.1 引脚分布

在将 GPIO 引脚导出到用户空间时,通常需要用到引脚编号,我们可以通过接口图进行确定。更多详细功能请参考《资料下载》 Datasheet 手册。

  • Luckfox Lyra Ultra/Ultra W 引脚图:

2.2 GPIO 编号计算

GPIO对应的引脚编号均在引脚图中标出,您可以直接使用或按照下面方法进行计算。

  • GPIO 有5个 bank:GPIO0~GPIO4,每个 bank 分为4组,共有32个 pin:A0~A7, B0~B7, C0~C7, D0~D7

  • GPIO 的命名方式为 GPIO{bank}_{group}{X},如下所示:

    GPIO0_A0 ~ A7 
    GPIO0_B0 ~ B7
    GPIO0_C0 ~ C7
    GPIO0_D0 ~ D7

    GPIO1_A0 ~ A7
    ....
    GPIO1_D0 ~ D7
    ....
    GPIO4_D0 ~ D7
  • GPIO 编号计算方法如下:

    GPIO 引脚编号计算公式:pin = bank * 32 + number
    GPIO 组内编号计算公式:number = group * 8 + X
    综上:pin = bank * 32 + (group * 8 + X)

    以 GPIO1_B1_d 为例说明,其中:

    • bank :1
    • group :1 (A=0, B=1, C=2, D=3)
    • X :1

    所以 GPIO1_B1_d 的引脚编号为:1 x 32 + ( 1x 8 + 1) = 41

2.3 使用 GPIO sysfs 接口控制 I/O

  1. 将 GPIO 控制从内核空间导出到用户空间(只写)。

    sudo su 
    echo 41 > /sys/class/gpio/export
  2. 取消 GPIO 控制从内核空间到用户空间的导出(只写)。

    echo 41 > /sys/class/gpio/unexport  

2.3.1 设备目录和设备属性

  1. 当你写入 GPIO 编号到 /sys/class/gpio/export 时,内核会将这个 GPIO 导出到用户空间,使之可以通过 /sys/class/gpio/gpio<编号> 目录来进行后续操作(如配置方向、设置或读取电平)。

    root@luckfox:/home/luckfox# ls /sys/class/gpio
    export gpio41 gpiochip0 gpiochip128 gpiochip32 gpiochip509 gpiochip64 gpiochip96 unexport
  2. 在 /sys/class/gpio/gpioN(N=1,2,3,5,...)设备目录下有多种属性,可以通过这些属性控制,从而实现对 GPIO 的控制。

    cd /sys/class/gpio/gpio41
    root@luckfox:/sys/class/gpio/gpio41# ls
    active_low device direction edge power subsystem uevent value
    • direction属性:设置 GPIO 的输入(in)或输出(out)
    • value属性:用于读取输入电平或者控制输出电平,写入或者读取到1表示高电平,写入或者读取到0表示低电平
    • edge属性:用于设置触发电平,只有将 GPIO 设置为中断的时候才会出现和使用这个属性
      • 上升沿触发:rising
      • 下降沿触发:falling
      • 双边沿触发:both
      • 禁用中断:none

2.3.2 控制 GPIO 引脚电平

  1. 设置方向。

    root@luckfox:/sys/class/gpio/gpio41# pwd   # 确保自己在想控制的设备目录下
    /sys/class/gpio/gpio41

    echo out > direction # 设置 GPIO 为输出
    echo in > direction # 设置 GPIO 为输入
  2. 设置value属性来控制 GPIO 电平。

    #输出
    echo 1 > value
    echo 0 > value

    # 输入
    cat value
  3. 注意:可能遇到的错误:

    root@luckfox:~# echo 22 > /sys/class/gpio/export
    bash: echo: write error: Device or resource busy
    • Linux 驱动调试中可能遇到 gpio 无法申请的问题,需要查找 gpio 被哪个驱动占用
    mount -t  debugfs none /media
    cat /media/gpio

    root@luckfox:~# cat /media/gpio
    gpiochip0: GPIOs 0-31, parent: platform/ff940000.gpio, gpio0:

    gpiochip1: GPIOs 32-63, parent: platform/ff870000.gpio, gpio1:
    gpio-32 ( |work-led ) out lo
    gpio-46 ( |cd ) in lo IRQ ACTIVE LOW

    gpiochip2: GPIOs 64-95, parent: platform/ff1c0000.gpio, gpio2:

    gpiochip3: GPIOs 96-127, parent: platform/ff1d0000.gpio, gpio3:

    gpiochip4: GPIOs 128-159, parent: platform/ff1e0000.gpio, gpio4:
    • 还可以看 IOMUX 的输出值,如果输出为 0,当前引脚被复用成 GPIO 功能,否则就复用其它功能
    # GPO0_C6 默认是调试串口

    root@luckfox:~# iomux 0 22
    mux get (GPIO0-22) = 1

3. 使用 Python 程序控制 I/O

  1. 控制一颗 LED 交替闪烁。

    #!/usr/bin/python3

    from periphery import GPIO
    import time

    LED_Pin = 41

    LED_GPIO = GPIO(LED_Pin, "out")

    while True:
    try:
    LED_GPIO.write(True)
    time.sleep(0.5)
    LED_GPIO.write(False)
    time.sleep(0.5)
    except KeyboardInterrupt:
    LED_GPIO.write(False)
    break
    except IOError:
    print ("Error")

    LED_GPIO.close()
  2. 运行程序:

    chmod 777 gpio.py 
    ./gpio.py

4.控制GPIO(C程序)

在前文中,我们演示了如何使用 echo 命令和 Python 程序控制GPIO。实际上也可以使用 vi 编辑器对文件进行修改,修改时需注意用户权限。此外,我们还可以使用C库函数或系统调用来读写设备文件,以达到控制设备的目的。请注意,为了在特定的嵌入式系统上运行程序,通常需要使用交叉编译工具来编译代码,以生成可在目标开发板上执行的可执行文件。接下来,让我们一起探讨具体的实施步骤。

4.1 完整代码

通过以下程序,可以实现 GPIO 引脚的电平控制。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
int gpio_pin;

printf("Please enter the GPIO pin number: ");
scanf("%d", &gpio_pin);

FILE *export_file = fopen("/sys/class/gpio/export", "w");
if (export_file == NULL) {
perror("Failed to open GPIO export file");
return -1;
}
fprintf(export_file, "%d", gpio_pin);
fclose(export_file);

char direction_path[50];
snprintf(direction_path, sizeof(direction_path), "/sys/class/gpio/gpio%d/direction", gpio_pin);
FILE *direction_file = fopen(direction_path, "w");
if (direction_file == NULL) {
perror("Failed to open GPIO direction file");
return -1;
}
fprintf(direction_file, "out");
fclose(direction_file);

char value_path[50];
char cat_command[100];
snprintf(value_path, sizeof(value_path), "/sys/class/gpio/gpio%d/value", gpio_pin);
snprintf(cat_command, sizeof(cat_command), "cat %s", value_path);
FILE *value_file = fopen(value_path, "w");
if (value_file == NULL) {
perror("Failed to open GPIO value file");
return -1;
}

for (int i = 0; i < 3; i++) {
fprintf(value_file, "1");
fflush(value_file);

system(cat_command);
sleep(1);

fprintf(value_file, "0");
fflush(value_file);

system(cat_command);
sleep(1);
}

fclose(value_file);

FILE *unexport_file = fopen("/sys/class/gpio/unexport", "w");
if (unexport_file == NULL) {
perror("Failed to open GPIO unexport file");
return -1;
}
fprintf(unexport_file, "%d", gpio_pin);
fclose(unexport_file);

return 0;
}

4.2 导出引脚到用户空间

这段代码通过读取用户输入的引脚编号,打开 /sys/class/gpio/export 文件,并写入该编号,实现了 GPIO 引脚的导出操作。

printf("Please enter the GPIO pin number: ");
scanf("%d", &gpio_pin);

FILE *export_file = fopen("/sys/class/gpio/export", "w");
if (export_file == NULL) {
perror("Failed to open GPIO export file");
return -1;
}
fprintf(export_file, "%d", gpio_pin);
fclose(export_file);

4.3 配置GPIO方向

这段代码通过打开 /sys/class/gpio/gpiox/direction 文件,并写入 "out",确保了 GPIO 引脚被设置为输出模式。如需配置为输入模式,则写入 “in”。

char direction_path[50];
snprintf(direction_path, sizeof(direction_path), "/sys/class/gpio/gpio%d/direction", gpio_pin);
FILE *direction_file = fopen(direction_path, "w");
if (direction_file == NULL) {
perror("Failed to open GPIO direction file");
return -1;
}
fprintf(direction_file, "out");
fclose(direction_file);

4.4 控制引脚输出电平

这段代码用于控制 GPIO 引脚的电平。首先,需要打开 /sys/class/gpio/gpiox/value 文件,该文件用于设置 GPIO 引脚的电平值。在一个循环中,我们通过向文件依次写入 “1” 和 “0” 实现对引脚的控制,再调用 cat 命令查看 value 中存储的值,检查是否写入成功。

注意,标准C库通常使用缓冲区来提高文件操作的效率,而不是每次写入都立即将数据写入磁盘文件。这就意味着,尽管你调用了 fprintf 来写入数据,数据实际上可能会被暂时保存在缓冲区中,并且不会立即写入文件。如果你希望确保数据被立即写入文件,你可以使用 fflush 函数来刷新缓冲区,这样可以强制将缓冲区中的数据写入文件,或每次都使用 fopen->fprintf->fclose 的方式写入。

char value_path[50];
char cat_command[100];
snprintf(value_path, sizeof(value_path), "/sys/class/gpio/gpio%d/value", gpio_pin);
snprintf(cat_command, sizeof(cat_command), "cat %s", value_path);
FILE *value_file = fopen(value_path, "w");
if (value_file == NULL) {
perror("Failed to open GPIO value file");
return -1;
}

for (int i = 0; i < 3; i++) {
fprintf(value_file, "1");
fflush(value_file);

system(cat_command);
sleep(1);

fprintf(value_file, "0");
fflush(value_file);

system(cat_command);
sleep(1);
}

fclose(value_file);

4.5 取消导出引脚

这段代码通过打开 /sys/class/gpio/unexport 文件,并写入 GPIO 引脚的编号,实现了 GPIO 引脚的取消导出操作。

FILE *unexport_file = fopen("/sys/class/gpio/unexport", "w");
if (unexport_file == NULL) {
perror("Failed to open GPIO unexport file");
return -1;
}
fprintf(unexport_file, "%d", gpio_pin);
fclose(unexport_file);

4.6 交叉编译

  1. 首先设置交叉编译工具的环境变量,如果需要永久生效,编辑配置文件(如 .bashrc.profile)。

    vim ~/.bashrc 
  2. 添加工具链路径,将交叉编译工具的路径添加到系统的 PATH 环境变量中:

    export PATH=/home/ubuntu/luckfox-lyra-sdk/prebuilts/gcc/linux-x86/arm/gcc-arm-10.3-2021.07-x86_64-arm-none-linux-gnueabihf/bin:$PATH
    • 注意:请根据自己的 SDK 路径填写
  3. 更新环境变量,执行以下命令使配置生效:

    source ~/.bashrc  
  4. 使用交叉编译器编译程序。

    arm-none-linux-gnueabihf-gcc gpio.c -o gpio
  5. 最后将可执行文件传输到开发板。

    scp gpio root@192.168.10.103:/root

    adb push gpio /
    • 注意:请根据自己的实际 IP 地址填写
  6. 运行程序,修改 gpio 文件的操作权限后运行程序。

    # chmod 777 gpio
    # ./gpio
  7. 实验现象,成功实现电平翻转: