Skip to main content

GPIO

In this chapter, we will learn how to use the sysfs file system to access and control GPIO (General-Purpose Input/Output) pins.

Sample program : Code.zip

1. GPIO Subsystem

GPIO (General-Purpose Input/Output) is a type of universal pin that can be controlled by microcontrollers (MCUs) or CPUs. It has various functions, including detecting high or low-level inputs and controlling outputs. In Linux, GPIO pins can be exported to user space and controlled through the sysfs file system, allowing GPIO pins to be used for various purposes such as serial communication, I2C, network communication, voltage detection, and more.

Linux has a dedicated GPIO subsystem driver framework for handling GPIO devices. Through this framework, users can easily interact with the GPIO pins of the CPU. This driver framework supports using pins for basic input and output functions, and input functions also support interrupt detection. This allows developers to easily use GPIO pins for various purposes, providing flexibility and programmability. More detailed information about the GPIO subsystem can be found in the <Linux Kernel Source>/Documentation/gpio directory.

2. GPIO Control (Shell)

2.1 Pin Distribution

When exporting GPIO pins to user space, pin numbers are typically needed. These pin numbers can be determined using interface diagrams. For example, the pin name for pin 4 is GPIO1_C7_d, and the pin number is 55.

  1. LuckFox Pico Diagram:

  2. LuckFox Pico Mini A/B Diagram:

  3. LuckFox Pico Plus Diagram:

  4. LuckFox Pico Pro/Max Diagram:

  5. LuckFox Pico Ultra/Ultra W Diagram:
    2.2 Calculating Pin Numbers

The pin numbers corresponding to GPIOs are marked in the pinout diagram, and you can use them directly or calculate them as follows:

  • GPIOs are divided into 5 banks: GPIO0 to GPIO4, with each bank having 4 groups, resulting in a total of 32 pins: A0 to A7, B0 to B7, C0 to C7, D0 to D7.

  • GPIOs are named in the format GPIO{bank}_{group}{X}, as shown below:

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

    GPIO1_A0 ~ A7
    ....
    GPIO1_D0 ~ D7
    ....
    GPIO4_D0 ~ D7
  • The GPIO pin number can be calculated using the following formula:

    GPIO pin number calculation formula: pin = bank * 32 + number
    GPIO group number calculation formula: number = group * 8 + X
    Therefore: pin = bank * 32 + (group * 8 + X)

    As an example, let's consider GPIO1_C7_d, where:

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

    So, the pin number of GPIO1_C7_d is: 1 x 32 + (2 x 8 + 7) = 55

2.3 Device Directory

In the /sys/class/gpio directory, each GPIO device has its own folder. The folder names are gpio followed by the pin number, for example, /sys/class/gpio/gpio55 represents pin number 55, which is GPIO1_C7_d. You can use the following command to check:

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

2.4 Device Attributes

In these device directories, you can also find control files related to GPIO pins, including direction, value, and interrupts, among others. Each GPIO device directory contains a set of attribute files used to configure and manage GPIO pins. You can use the following command in the GPIO device directory:

  1. View the attribute files of GPIO 55 device:

    # echo 55 > /sys/class/gpio/export 
    # cd gpio55
    # ls
    value power subsystem active_low
    uevent edge device direction
  2. Attribute Files

    Using these attribute files, you can easily configure and control GPIO pins to adapt to different application scenarios and requirements. Some of the key attributes include:

    • Direction (direction):
      • Configured as input: in
      • Configured as output: out
    • Value (value):
      • Output low level: 0
      • Output high level: 1
    • Interrupt edge (edge):
      • Rising edge-triggered: rising
      • Falling edge-triggered: falling
      • Both edge-triggered: both
      • Disable interrupts: none

2.5 Controlling GPIO Pin Levels

The device's attribute files serve as an interface for function parameters. For /sys/class/gpio/gpio55/value, each time a write operation is performed on the file, it triggers the driver code to modify the level of GPIO pin 55 using the content written as a parameter. Each time a read operation is performed, it triggers the driver code to update the current level of GPIO pin 55 to the /sys/class/gpio/gpio55/value file.

  1. Export GPIO 55 to user space:

    echo 55 > /sys/class/gpio/export
  2. Read the level of GPIO1_C7_d:

    echo 55 > /sys/class/gpio/export         
    echo in > /sys/class/gpio/gpio55/direction
    cat /sys/class/gpio/gpio55/value
  3. Control the level of 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. Unexport GPIO 55 from user space:

    echo 55 > /sys/class/gpio/unexport

3. GPIO Control (Python Program)

In the previous sections, we demonstrated how to use the echo command to modify device files to achieve device control. Next, we will implement GPIO control using a Python program.

3.1 Complete Code

The following program enables the control and reading of GPIO pin levels.

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 Configure GPIO Direction

This code segment uses the periphery library to open two GPIO pins in Python. Write_GPIO is used for output, and Read_GPIO is used for input.

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

3.3 Control Pin Output Level

This code segment uses the Write_GPIO object to write a logic value of True to the GPIO pin to set the pin output to a high level. Writing a logic value of False to the GPIO pin sets the pin output to a low level.

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

3.4 Control Pin Read Level

This code reads the state of the Read_GPIO pin and prints it. Read_GPIO.read() returns the level state of the pin (True for high level, False for low level).

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

3.5 Running the Program

  1. Use the vi tool in the terminal to create a Python file, paste the Python program, and save it.

    # nano gpio.py
  2. Run the program.

    # python3 gpio.py
  3. Experimental Observations

    Pins 55 and 54 not connected:

    Pins 55 and 54 connected:

4. GPIO Control (C Program)

In the previous sections, we demonstrated how to control GPIO using the echo command and Python programs. Additionally, you can also use the vi editor to modify files, but be cautious about user permissions when making changes. Moreover, we can use C library functions or system calls to read and write device files for device control. Please note that to run programs on specific embedded systems, it's often necessary to use cross-compilation tools to compile the code and generate executable files that can run on the target development board. Let's explore the specific implementation steps together.

4.1 Complete Code

With the following program, you can control the GPIO pin levels.

#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 Exporting a Pin to User Space

This code reads the user-input pin number, opens the /sys/class/gpio/export file, and writes that number to it, achieving the export operation for a 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);

4.3 Configuring GPIO Direction

This code opens the /sys/class/gpio/gpiox/direction file and writes "out" to ensure that the GPIO pin is set to output mode. If you want to configure it as an input, you would write "in" instead.

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 Controlling Pin Output Level

This code is used to control the GPIO pin's logic level. First, you need to open the /sys/class/gpio/gpiox/value file, which is used to set the GPIO pin's logic level. In a loop, we control the pin by sequentially writing "1" and "0" to the file. Then, we use the cat command to check the value stored in the value file to verify if the write was successful.

Note that the standard C library typically uses buffers to improve the efficiency of file operations rather than writing data to disk files immediately after each write. This means that even though you use fprintf to write data, the data may be temporarily held in the buffer and not immediately written to the file. If you want to ensure that data is written to the file immediately, you can use the fflush function to flush the buffer, which forces the data in the buffer to be written to the file, or use the fopen->fprintf->fclose approach for each write.

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 Unexporting a Pin

This code opens the /sys/class/gpio/unexport file and writes the GPIO pin's number to it, achieving the unexport operation for a GPIO pin.

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 Cross-Compilation

  1. Specify the Cross-Compilation Tool

    First, you need to add the path to the cross-compilation tool to the system's PATH environment variable so that you can use the cross-compilation tool from anywhere. You can add the following line to your shell configuration file (usually ~/.bashrc or ~/.bash_profile or ~/.zshrc, depending on your shell). Note that the path after PATH= should point to the directory where the cross-compilation tool is located.

    • gcc path

      <SDK Directory>/tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/bin/arm-rockchip830-linux-uclibcgnueabihf-gcc
    • Open the shell configuration file.

      vi ~/.bashrc 
    • Add the path of the cross-compilation tool to the system's PATH environment variable. Replace <SDK Directory> with your own SDK path, such as /home/luckfox/luckfox-pico/.

      export PATH=<SDK Directory>/tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/bin:$PATH
    • Reload the shell configuration file to apply the changes:

      source ~/.bashrc  
  2. Compile the Program Using the Cross-Compilation Tool

    arm-rockchip830-linux-uclibcgnueabihf-gcc gpio.c -o gpio
  3. After successful cross-compilation, an executable file that can run on the development board will be generated in the current directory.

    # ls
    gpio gpio.c

4.7 Running the Program

  1. File Transfer

    First, transfer the gpio program from the virtual machine to Windows, and then transfer it to the development board via TFTP or ADB. Here are the steps to transfer the file from Windows to the development board using ADB:

    adb push path_to_file destination_on_development_board

    eg: (Transferring the `gpio` file from the current directory to the root directory of the development board)
    adb push gpio /
  2. Running the Program

    Modify the permissions of the gpio file and then run the program:

    # chmod 777 gpio
    # ./gpio
  3. Experimental Observations

    Successful level flipping:

5. Modifying Device Tree

The pins 55 and 54 in the program can be used directly without modifying the device tree. If we need to configure other pins as general-purpose IO, we need to modify the device tree.

5.1 Modify Device Tree File

  1. Device Tree File Paths

    • Development Board Configuration File

      In the <SDK directory>/project/cfg/BoardConfig_IPC/ directory, there are configuration files for different models of development boards. These files primarily include configuration parameters for various Luckfox Pico board models, covering aspects such as target architecture, boot medium, Uboot and kernel configurations, and partition settings. The SDK directory structure is as follows:

      ├── build.sh -> project/build.sh ---- SDK compilation script
      ├── media --------------------------- Multimedia encoding, ISP, and related algorithms (can be compiled independently in the SDK)
      ├── sysdrv -------------------------- U-Boot, kernel, rootfs directory (can be compiled independently in the SDK)
      ├── project ------------------------- Reference applications, compilation configurations, and script directories
      │ ├── 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 -------------------------- Directory for storing SDK compiled image files
      ├── docs ---------------------------- SDK documentation directory
      └── tools --------------------------- Image burning packaging tools and burning tools

      Among them, RK_KERNEL_DTS specifies the Device Tree Source (DTS) file for the kernel. Taking Luckfox Pico as an example, by opening the BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico-IPC.mk file, we can see that the file pointed to by RK_KERNEL_DTS is rv1103g-luckfox-pico.dts

    • Device Tree File Path

      Based on RK_KERNEL_DTS, the device tree file path for Luckfox Pico can be determined as follows:

      <SDK directory>/sysdrv/source/kernel/arch/arm/boot/dts/rv1103g-luckfox-pico.dts
  2. Define GPIO

    Defining a GPIO typically requires adding two sections of code. Note that the button is active-low and needs to be pulled up. Here's an example of how to add the definition of GPIO1_C7_d pin in the device tree.

    image
    image

    The code snippets to add are as follows:

    /{
    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. Comment Out the Pin's Peripheral Function

    Disabling the peripheral function of a pin can be done by commenting out the relevant peripheral node in the device tree. Here's an example of how to disable the PWM function of GPIO1_C7_d in the device tree.
    image

5.2 Compile the Kernel

  1. Compile by selecting branches for LuckFox Pico, LuckFox Pico Mini A, LuckFox Pico Mini B, LuckFox Pico Plus, and 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. Recompile the kernel

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

5.3 Flash the Firmware Again

  1. After successfully compiling the kernel, the generated files can be found in the <SDK directory>/output/image directory.
    image
  2. Replace the boot.image and env.txt files in the original firmware.
    image
  3. Recreate the SD card. For Luckfox Pico Plus, you may only need to modify the relevant partition.
    image