跳到主要内容

GPIO

在本章中,我们将学习如何使用 sysfs 文件系统来访问和控制 GPIO。

示例程序:Code.zip

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 引脚导出到用户空间时,通常需要用到引脚编号,我们可以通过接口图进行确定,如4号引脚的引脚名称为 GPIO1_C7_d,引脚编号为55。

  1. LuckFox Pico 引脚图:
  2. LuckFox Pico Mini A/B 引脚图:
  3. LuckFox Pico Plus 引脚图:
  4. LuckFox Pico Pro/Max 引脚图:
  5. LuckFox Pico Ultra/Ultra W 引脚图:

2.2 编号计算

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_C7_d 为例说明,其中:

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

    所以 GPIO1_C7_d 的引脚编号为:1 x 32 + ( 2 x 8 + 7 ) = 55

2.3 设备目录

/sys/class/gpio 目录中,每个 GPIO 设备都有其自己的文件夹。这些文件夹的名称是 gpio 加上引脚编号,例如 /sys/class/gpio/gpio55 表示引脚编号为 55 的引脚,即 GPIO1_C7_d。您可使用如下命令查看:

# ls /sys/class/gpio/ 
gpio55 gpiochip96 gpiochip0 gpiochip128 unexport export gpiochip32

2.4 设备属性

在这些设备目录中,您还可以找到与 GPIO 引脚相关的控制文件,包括方向、值和中断等。每个GPIO设备目录中都包含一组属性文件,这些属性文件用于配置和管理GPIO引脚。您可以在GPIO设备目录中使用如下命令查看:

  1. 查看gpio55设备的属性文件

    # echo 55 > /sys/class/gpio/export 
    # cd gpio55
    # ls
    value power subsystem active_low
    uevent edge device direction
  2. 属性文件

    通过使用这些属性文件,您可以轻松地配置和控制GPIO引脚,使其适应不同的应用场景和需求。其中一些关键的属性包括:

    • 方向(direction):
      • 配置为输入:in
      • 配置为输出:out
    • 值(value):
      • 输出低电平:0
      • 输出高电平:1
    • 中断边沿(edge):
      • 上升沿触发:rising
      • 下降沿触发:falling
      • 双边沿触发:both
      • 禁用中断:none

2.5 控制GPIO引脚电平

设备的属性文件就相当于一个函数的参数接口。对于 /sys/class/gpio/gpio55/value ,每次对文件执行写入操作时,会触发驱动代码,使用这次写入的内容作为参数来修改 gpio55 的引脚电平;而每次读取操作时,则触发驱动代码将当前 gpio55 的引脚电平更新到 /sys/class/gpio/gpio55/value 文件。

  1. 导出gpio55到用户空间

    echo 55 > /sys/class/gpio/export
  2. 读取 GPIO1_C7_d 引脚电平

    echo 55 > /sys/class/gpio/export         
    echo in > /sys/class/gpio/gpio55/direction
    cat /sys/class/gpio/gpio55/value
  3. 控制 GPIO1_C7_d 引脚电平

    echo out > /sys/class/gpio/gpio55/direction 
    echo 1 > /sys/class/gpio/gpio55/value
    echo 0 > /sys/class/gpio/gpio55/value
  4. 取消导出gpio55到用户空间

    echo 55 > /sys/class/gpio/unexport

3.控制GPIO(Python程序)

在前文中,我们演示了使用 echo 命令修改设备文件,以达到控制设备的目的。接下来,我们将使用 Python 程序实现 GPIO 的控制。

3.1 完整代码

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

from periphery import GPIO
import time

Write_Pin = 55
Read_Pin = 54

Write_GPIO = GPIO(Write_Pin, "out")
Read_GPIO = GPIO(Read_Pin, "in")

try:
while True:
try:
Write_GPIO.write(True)
pin_state = Read_GPIO.read()
print(f"Pin state: {pin_state}")

Write_GPIO.write(False)
pin_state = Read_GPIO.read()
print(f"Pin state: {pin_state}")

time.sleep(1)

except KeyboardInterrupt:
Write_GPIO.write(False)
break

except IOError:
print("Error")

finally:
Write_GPIO.close()
Read_GPIO.close()

3.2 配置GPIO方向

这段代码使用 periphery 库在 Python 中打开两个 GPIO 引脚,Write_GPIO 用于输出,Read_GPIO 用于输入。

Write_GPIO = GPIO(Write_Pin, "out")
Read_GPIO = GPIO(Read_Pin, "in")

3.3 控制引脚输出电平

这段代码使用 Write_GPIO 对象将逻辑值 True 写入 GPIO 引脚实现引脚输出高电平,将逻辑值 False 写入 GPIO 引脚实现引脚输出低电平。

Write_GPIO.write(True)
....
Write_GPIO.write(False)

3.4 控制引脚读取电平

这段代码通过读取 Read_GPIO 引脚的状态,并将其打印出来。Read_GPIO.read() 返回引脚的电平状态(高电平为 True,低电平为 False)。

pin_state = Read_GPIO.read()
print(f"Pin state: {pin_state}")

3.5 运行程序

  1. 使用 nano 工具在终端创建 py 文件,粘贴并保存 python 程序

    # nano gpio.py
  2. 运行程序

    # python3 gpio.py
  3. 实验现象

    55、54引脚无连接:

    55、54引脚相接:

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. 指定交叉编译工具

    首先,我们要将交叉编译工具的路径添加到系统的 PATH 环境变量中,以便可以在任何地方使用交叉编译工具,您可以在shell配置文件中添加以下行(通常是 ~/.bashrc~/.bash_profile~/.zshrc,具体取决于您使用的shell),注意 PATH= 后的路径为交叉编译工具所在的目录。

    • gcc路径

      <SDK Directory>/tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/bin/arm-rockchip830-linux-uclibcgnueabihf-gcc
    • 打开shell配置文件

      vi ~/.bashrc 
    • 将交叉编译工具的路径添加到系统的PATH环境变量中,将 <SDK Directory> 修改为自己的 SDK 路径,如 /home/luckfox/luckfox-pico/

      export PATH=<SDK Directory>/tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/bin:$PATH
    • 重新加载shell配置文件,使更改生效

      source ~/.bashrc  
  2. 使用交叉编译工具编译程序

    arm-rockchip830-linux-uclibcgnueabihf-gcc gpio.c -o gpio
  3. 交叉编译成功后,将在当前目录下生成可在开发板运行的可执行文件

    # ls
    gpio gpio.c

4.7 运行程序

  1. 文件传输

    先将 gpio 从虚拟机传输到 Windows,再通过 TFTP 或 ADB 传输到开发板,将文件从 Windows 通过 ADB 将文件传输到开发板的步骤如下:

    adb push 文件所在路径 开发板存储路径

    eg:(将当前目录下gpio文件传输到开发板的根目录)
    adb push gpio /
  2. 运行程序

    修改 gpio 文件的操作权限后运行程序

    # chmod 777 gpio
    # ./gpio
  3. 实验现象

    成功实现电平翻转:

5.修改设备树

程序中的55、54号引脚均可直接使用,无需修改设备树。若我们需要将其他引脚配置为普通IO,需要对设备树进行修改。

5.1 修改设备树文件

  1. 设备树文件路径

    • 开发板配置文件

      <SDK目录>/project/cfg/BoardConfig_IPC/ 目录下有不同型号开发板的配置文件,这些文件主要包含了针对 Luckfox Pico 不同型号开发板的配置参数,涵盖了目标架构、启动介质、Uboot和内核配置、分区设置等多方面的内容。SDK目录结构如下:

      ├── build.sh -> project/build.sh ---- SDK编译脚本
      ├── media --------------------------- 多媒体编解码、ISP等算法相关(可独立SDK编译)
      ├── sysdrv -------------------------- U-Boot、kernel、rootfs目录(可独立SDK编译)
      ├── project ------------------------- 参考应用、编译配置以及脚本目录
      │ ├── cfg
      │ ├── BoardConfig_IPC
      │ ├── BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico-IPC.mk
      │ ├── BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico_Mini_A-IPC.mk
      │ ├── BoardConfig-SPI_NAND-NONE-RV1103_Luckfox_Pico_Mini_B-IPC.mk
      │ ├── BoardConfig-SPI_NAND-NONE-RV1103_Luckfox_Pico_Plus-IPC.mk
      │ ├── BoardConfig-SPI_NAND-NONE-RV1106_Luckfox_Pico_Pro_Max-IPC.mk
      │ └── ...
      ├── output -------------------------- SDK编译后镜像文件存放目录
      ├── docs ---------------------------- SDK文档目录
      └── tools --------------------------- 烧录镜像打包工具以及烧录工具

      其中,RK_KERNEL_DTS 指定了内核的设备树文件。我们以 Luckfox Pico 为例,打开 BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico-IPC.mk 文件,可以看到 RK_KERNEL_DTS 指向的文件为 rv1103g-luckfox-pico.dts

    • 设备树文件路径

      根据RK_KERNEL_DTS,可以确定 Luckfox Pico 的设备树文件路径如下:

      <SDK目录>/sysdrv/source/kernel/arch/arm/boot/dts/rv1103g-luckfox-pico.dts
  2. 定义GPIO

    定义一个 GPIO,通常需要添加两段代码。注意按键为低电平有效,需要上拉。以下是一个示例,展示如何在设备树中添加 GPIO1_C7_d 引脚的定义。

    image
    image

    需要添加的代码片段如下:

    /{
    gpio1pc7:gpio1pc7 {
    compatible = "regulator-fixed";
    pinctrl-names = "default";
    pinctrl-0 = <&gpio1_pc7>;
    regulator-name = "gpio1_pc7";
    regulator-always-on;
    };
    };

    &pinctrl {
    gpio1-pc7 {
    gpio1_pc7:gpio1-pc7 {
    rockchip,pins = <1 RK_PC7 RK_FUNC_GPIO &pcfg_pull_none>;
    };
    };
    };
  3. 注释引脚的外设功能

    注释引脚外设功能可以通过在设备树中将相应的外设节点注释来实现,以下是一个示例,展示如何在设备树中禁用 GPIO1_C7_d 引脚的 PWM 功能。
    image

5.2 编译内核

  1. 编译选择分支,分别是指定 LuckFox Pico 、LuckFox Pico Mini A 、LuckFox Pico Mini B、LuckFox Pico Plus 和 LuckFox Pico Pro/Max

    luckfox@luckfox:~/luckfox-pico$ ./build.sh lunch
    ls: cannot access 'BoardConfig*.mk': No such file or directory

    You're building on Linux
    Lunch menu...pick a combo:

    BoardConfig-*.mk naming rules:
    BoardConfig-"启动介质"-"电源方案"-"硬件版本"-"应用场景".mk
    BoardConfig-"boot medium"-"power solution"-"hardware version"-"applicaton".mk

    ----------------------------------------------------------------
    0. BoardConfig_IPC/BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico-IPC.mk
    boot medium(启动介质): EMMC
    power solution(电源方案): NONE
    hardware version(硬件版本): RV1103_Luckfox_Pico
    applicaton(应用场景): IPC
    ----------------------------------------------------------------

    ----------------------------------------------------------------
    1. BoardConfig_IPC/BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico_Mini_A-IPC.mk
    boot medium(启动介质): EMMC
    power solution(电源方案): NONE
    hardware version(硬件版本): RV1103_Luckfox_Pico_Mini_A
    applicaton(应用场景): IPC
    ----------------------------------------------------------------

    ----------------------------------------------------------------
    2. BoardConfig_IPC/BoardConfig-SPI_NAND-NONE-RV1103_Luckfox_Pico_Mini_B-IPC.mk
    boot medium(启动介质): SPI_NAND
    power solution(电源方案): NONE
    hardware version(硬件版本): RV1103_Luckfox_Pico_Mini_B
    applicaton(应用场景): IPC
    ----------------------------------------------------------------

    ----------------------------------------------------------------
    3. BoardConfig_IPC/BoardConfig-SPI_NAND-NONE-RV1103_Luckfox_Pico_Plus-IPC.mk
    boot medium(启动介质): SPI_NAND
    power solution(电源方案): NONE
    hardware version(硬件版本): RV1103_Luckfox_Pico_Plus
    applicaton(应用场景): IPC
    ----------------------------------------------------------------

    ----------------------------------------------------------------
    4. BoardConfig_IPC/BoardConfig-SPI_NAND-NONE-RV1106_Luckfox_Pico_Pro_Max-IPC.mk
    boot medium(启动介质): SPI_NAND
    power solution(电源方案): NONE
    hardware version(硬件版本): RV1106_Luckfox_Pico_Pro_Max
    applicaton(应用场景): IPC
    ----------------------------------------------------------------

    Which would you like? [0]: 0
    [build.sh:info] switching to board: /home/luckfox/luckfox-pico/project/cfg/BoardConfig_IPC/BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico-IPC.mk
    [build.sh:info] Running build_select_board succeeded.
  2. 重新编译内核

    luckfox@luckfox:~/Luckfox-Pico/luckfox-pico$ ./build.sh kernel

5.3 重新烧录固件

  1. 内核编译成功后生成的文件在 <SDK目录>output/image 目录下
    image
  2. 替换原固件中的 boot.imageenv.txt 文件
    image
  3. 重新创建SD(Luckfox Pico Plus/Pro/Max、Luckfox Pico Mini B 可只修改相应的分区)
    image