GPIO Pins
GPIO Pins trên máy Pi

A. GPIO Subsystem

GPIO là gì? GPIO là viết tắt của General Port Input output, các port này xử lý cả tín hiệu số cả vào lẫn ra. Khi hoạt động như một cổng vào, nó có thể được sử dụng để kết nối CPU với tín hiệu ON/OFF nhận được từ các công tắc (ví dụ các nút bấm trên router), hoặc đọc các tín hiệu số từ các sensor. Còn nếu hoạt động như một cổng ra, nó có thể truyền các tín hiệu điều khiển đến các thiết bị dựa trên kết quả của các lệnh thực thi trong CPU, chẳng hạn như điều khiển tín hiệu bật tắt của đèn LED (Cái này sẽ trình bày trong ví dụ). Các giá trị số vào/ra của GPIO chỉ có thể là 0 (LOW) hoặc 1(HIGH). Trong Linux kernel, để sử dụng GPIO, trước hết LKM cần phải giành lấy nó từ kernel, để trở thành chủ nhân của GPIO này, ngăn chặn việc các LKM khác truy cập vào cùng GPIO. Sau khi đã thành chủ sở hữu của GPIO, bạn có thể cài đặt chiều truyền tín hiệu số (Vào hoặc ra), thay đổi giá trị truyền vào đối với chiều ra, đọc giá trị nhận được đối với chiều vào, cấu hình các thứ linh tinh khác…

Có hai cách để làm việc với GPIO trong kernel, bao gồm:

  • Cách truyền thống (đã depreciate): biểu diễn các GPIO bằng các số int.
  • Cách mới: Biểu diễn bằng GPIO description.

I. Legacy GPIO interface trong Linux.

Mặc dù cách này đã bị đánh dấu là depreciate nhưng nó vẫn là cách được biết đến rộng rãi nhất. Các GPIO port được định danh bằng các số integer. Các function cần thiết được include từ header:

#include <linux/gpio.h>

1. Giành quyền sử dụng GPIO và cấu hình GPIO.

Bạn có thể chỉ định và giành quyền sử dụng của một GPIO bằng hàm gpio_request():

static int gpio_request(unsigned gpio, const char *label);

Tham số unsigned gpio chính là GPIO number, được biểu diễn dưới dạng số nguyên và label là tên sẽ được sử dụng bởi kernel trong sysfs. Hàm này sẽ trả về 0 nếu như việc request thành công, ngược lại nó sẽ trả về giá trị âm. Sau khi sử dụng xong GPIO, bạn nên giải phóng nó để những người khác có thể sử dụng:

static int gpio_free(unsigned gpio);

Ngoài ra, bạn cũng có thể kiểm tra xem GPIO number có hợp lệ hay không trước khi request nó:

static bool gpio_is_valid(int number);

Sau khi đã giành quyền sử dụng GPIO, bước tiếp theo cần làm là cài đặt chiều truyền tín hiệu số của nó, có 2 chiều: input và output được setup bằng hai hàm sau:

static int gpio_direction_input(unsigned gpio);
static int gpio_direction_output(unsigned gpio, value);

Tham số value trong trường hợp gpio output sẽ là giá trị mặc định được gán cho GPIO port đấy. Các hàm này nên được dùng trong context có thể ngủ được, thông thường chúng ta sẽ gọi những hàm này ở trong phần probe của driver.

2. Sử dụng legacy gpio để đọc ghi tín hiệu.

Việc truy cập GPIO thường liên quan đến các atomic context, đặc biệt là trong một interrupt handler, các đoạn code trong context này phải đảm bảo rằng GPIO controller callback function sẽ không thể ngủ. Một controller driver được thiết kế tốt sẽ có khả năng thông báo với các drivers khác việc lời gọi hàm có thể ngủ hay khôn. Việc này có thể được kiểm tra bằng : gpio_cansleep().

2.1 Trong atomic context.

Một số GPIO controller, đặc biệt là trên các SoC, có thể truy cập và quản lý thông qua các thao tác đọc ghi bộ nhớ thông thường, do đó việc ngủ nghỉ là không cần thiết. Đối với những GPIO này, gpio_cansleep() luôn luôn trả về giá trị false, và bạn có thể đọc/ghi giá trị ngay trong IRQ Handler với gpio_get_value()gpio_set_value().

2.2 Trong non-atomic context.

Các GPIO controllẻ được nối vào buses, chẳng hạn như SPI và I2C thì những function truy cập các bus này là có thể dẫn đến việc sleep, do đó gpio_cansleep() function sẽ luôn luôn trả về true. Trong trường hợp này, bạn không nên truy cập các GPIO này bên trong IRQ handled (thay vào đó, truy cập nó trong bottom half thông qua threaded irq). Hơn nữa, hãy sử dụng các hàm đọc ghi thay thế sau là gpio_get_value_cansleep()gpio_set_value_cansleep().

3. Ví dụ: sử dụng nút Reset đê bật tắt đèn LED trên Router wifi: Compex WPJ563.

Phần này mình sẽ ví dụ về việc sử dụng GPIO để thao tác với đèn LED và nút bấm, cũng như mapping từ GPIO Number sang IRQ number, ví dụ được thực hiện trên Board WPJ563, chạy firmware OpenWRT/LEDE. Bạn có thể xem hardware specific của nó ở đây: https://www.compex.com.sg/product/wpj563/. Dựa vào hardware specific, Board có 4 Đèn LED liên kết với các GPIO number:

  • GPIO1 : RSS1/EEPROM CLK
  • GPIO5 : RSS2/EEPROM data
  • GPIO6 : RSS3
  • GPIO7 : RSS4/Diag

Và một button: GPIO2: RESET. Ở ví dụ này mình sẽ dùng Button RESET trên để bật tắt các LED: RSS3 và RSS4. Module sẽ có tên là: wpj563_led

Trước hết, clone lede source ở đây: https://github.com/lede-project/source.git

Chúng ta sẽ tạo một package trong section kernel module để chứ module wpj563_led Chuyển đến thư mục chứa section kernel và tạo thư mục:

cd packages/kernel
mkdir wpj563-led

Bây giờ, bạn tạo một Makefile cho package mới, với nội dung như sau:

#copyright (C) 2018 Phi Nguyen
#
#

include $(TOPDIR)/rules.mk
include $(INCLUDE_DIR)/kernel.mk

PKG_NAME:=wpj563-led
PKG_VERSION:=0.1
PKG_RELEASE:=1

include $(INCLUDE_DIR)/package.mk

define KernelPackage/wpj563-led
  SUBMENU:=Oni Modules
  TITLE:=Simple GPIO Button driver by Oni
  FILES:=$(PKG_BUILD_DIR)/gpio_legacy.ko
  AUTOLOAD:=$(call AutoLoad,30,gpio_legacy,1)
  KCONFIG:=
endef

define KernelPackage/wpj563-led/description
	legacy gpio
endef

MAKE_OPTS:= \
        $(KERNEL_MAKE_FLAGS) \
        SUBDIRS="$(PKG_BUILD_DIR)"

define Build/Compile
        $(MAKE) -C "$(LINUX_DIR)" \
                $(MAKE_OPTS) \
                modules
endef

$(eval $(call KernelPackage,wpj563-led))

Tiếp theo tạo thư mục src để chứa file source:

mkdir src
cd src

Chúng ta sẽ viết file C cho module ở đây, đặt tên là wpj563-led.c Đầu tiên, include các header cần thiết:

#include <linux/init.h> 
#include <linux/module.h> 
#include <linux/kernel.h> 
#include <linux/gpio.h>        /* For Legacy integer based GPIO */ 
#include <linux/interrupt.h>   /* For IRQ */ 

Khai báo các GPIO number sẽ dùng:

//Declare GPIO number of LEDs and BTNs
static unsigned int GPIO_LED_GREEN2 = 6;
static unsigned int GPIO_BTN1 = 2;
static unsigned int GPIO_LED_GREEN = 7;
static unsigned int irq;

Cũng như những module khác, chúng ta cần khai báo hàm init cho module

static int __init gpiohello_init(void)
{
    int retval;
}

Phần tiếp theo sẽ áp dụng những gì đã nói trong phần 1, 2 ahíhí. Trước hết check xem các GPIO number đã khai báo có chính xác không, bằng cách thêm đoạn code sau vào hàm gpiohello_init()

...
if (!gpio_is_valid(GPIO_LED_GREEN)||!gpio_is_valid(GPIO_BTN1)||!gpio_is_valid(GPIO_LED_GREEN2))
    {
        pr_info("Invalid GPIO\n");
        return -ENODEV;
    }

Tiếp theo chúng ta gửi yêu cầu quyền sử dụng các GPIO trên tới kernel:

...
gpio_request(GPIO_LED_GREEN, "green-led");
gpio_request(GPIO_LED_GREEN2, "green2-led");
gpio_request(GPIO_BTN1, "button-1");

Sau khi đã dành được quyền sử dụng, bước tiếp theo là set direction. Đối với GPIO tương ứng với button, thì mục đích của nó là đọc tín hiệu từ button truyền đến kernel, nên chúng ta sẽ set nó theo chiều input, còn GPIO của các đèn LED sẽ được setup là output, vì chúng ta muốn ghi giá trị HIgh/low vào nó để tắt/bật LED. Đối với WPJ563 thì High(1) nghĩa là tắt đèn còn Low(0) nghĩa là bật đèn. Chúng ta sẽ truyền giá trị khởi tạo là High(1).

...
gpio_direction_input(GPIO_BTN1);
gpio_direction_output(GPIO_LED_GREEN,1);
gpio_direction_output(GPIO_LED_GREEN2, 1);

Việc bấm nút sẽ tạo ra interrupt, chúng ta sẽ điều khiển đèn khi có interrupt xảy ra, do đó cần liên kết Button GPIO trên với một Interrupt Line và đăng ký threaded handler cho nó:

    ...
    irq =  gpio_to_irq(GPIO_BTN1);
    retval = request_threaded_irq(irq, NULL, (irq_handler_t)btn1_pushed_irq_handler, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING | IRQF_ONESHOT, "Oni GPIO", NULL);
    if(retval < 0){
        pr_err("ONI_GPIO: Interrupt number %d isn't free\n",irq);
        gpio_free(GPIO_LED_GREEN2);
        gpio_free(GPIO_LED_GREEN);
        gpio_free(GPIO_BTN1);

        pr_info("ONI_GPIO: The Earth rejected you\n");
        return retval;
    }

Hàm gpio_to_irq() sẽ trả về interrupt line tương ứng với GPIO number mà chúng ta truyền vào. Hàm request_threađe_irq() ở trên sẽ gán hàm btn1_pushed_irq_handler() như là bottom half của interrupt handler cho irq line, và sử dụng hàm top half mặc định. (Cái này mình đã trình bày trong bài về Interrupt).

Rõ ràng chúng ta đang thiếu hàm btn1_pushed_irq_handler(), định nghĩa hàm này ở trước hàm init của module (ngay sau phần khai báo các gpio number):

static irq_handler_t btn1_pushed_irq_handler(unsigned int irq, void *dev_id, struct pt_regs *regs)
{
    int state, oni_led_state;
    state = gpio_get_value(GPIO_BTN1);
    oni_led_state = gpio_get_value(GPIO_LED_GREEN2);
    oni_led_state = 1-oni_led_state;
    gpio_set_value(GPIO_LED_GREEN2, oni_led_state);
    gpio_set_value(GPIO_LED_GREEN, oni_led_state);

    pr_info("GPIO_BTN1 interrupt: Interrupt! GPIO_BTN1 state is %d\n", state);
    return (irq_handler_t)IRQ_HANDLED;
}

Giải thích về hàm này: Trong hàm này chúng ta sẽ in ra giá trị của button (0 hoặc 1). Cùng với 1 biến oni_led_state để lưu giá trị hiện tại của led, mỗi lần gọi hàm chúng ta sẽ đổi giá trị của biến này và ghi nó vào các LED GPIO để thay đổi trạng thái của các đèn LED.

Phần còn lại là hàm exit module và các Macro linh tinh:

static void __exit gpiohello_exit(void)
{
    free_irq(irq, NULL);
    gpio_free(GPIO_LED_GREEN2);
    gpio_free(GPIO_LED_GREEN);
    gpio_free(GPIO_BTN1);

    pr_info("End of the world\n");
}

module_init(gpiohello_init);
module_exit(gpiohello_exit);

MODULE_AUTHOR("Phi Nguyen");
MODULE_LICENSE("GPL");

Lưu file này lại, sau đó cũng trong thư mục src này, chúng ta tạo file Makèile cho module:

echo "obj-m += gpio_legacy.o" > Makefile

OK. Done, Package đã xong, bây giờ đến đoạn config và build firmware mới. Đầu tiên phải cd quay lại thư mục source của LEDE:

./scripts/feeds update -a
./scripts/feeds install -a
make menuconfig

Tại đây, config các trường cần thiết như sau:

    Target System (Atheros AR7xxx/AR9xxx)
    Subtarget (Generic)
    Target Profile (Compex WPJ563 (16M flash))

    Kernel modules —> 
        Oni Modules ->
            (*) Simple GPIO Button driver by Oni

build nó nào: make

Sau khi hoàn thành, file firmware sẽ là: bin/targets/ar71xx/generic/openwrt-ar71xx-generic-wpj563-squashfs-sysupgrade.bin

Flash nó bằng các command sau (chạy trong uboot):

tftp 0x80060000 openwrt-ar71xx-generic-wpj563-squashfs-sysupgrade.bin
erase 0x9f030000 +$filesize;cp.b $fileaddr 0x9f030000 $filesize;boot

Lúc mới khởi động xong thì cả 2 đèn LED đều bị tắt. Sau khi nó khởi động xong, bạn cần vào /etc/rc.button xóa hết các file trong này đi rồi reboot lại, nếu không lúc bấm nút nó sẽ Reboot board =)).

Đây là nút bấm: (push button): OK, bây giờ bấm nút và tận hưởng thành quả thôi. Sau khi bấm nút thì đèn sẽ sáng lên như này, bây giờ bấm 1 lần nữa thì đèn sẽ bị tắt.

II. GPIO interface dựa trên descriptor.

Đối với descriptor-based GPIO, một GPIO được mô tả bằng cấu trúc:

struct gpio_desc{
    struct gpio_chip *chip;
    unsigned log flags;
    const char *label;
};

Cấu trúc này nằm trong header:

	#include <linux/gpio/consumer.h>

Trước khi cấp phát và giành quyền sử dụng GPIO, những GPIO này phải được ánh xạ tới đâu đó, tức là chúng phải được gắn với device của bạn (đối với integer-based, thì chúng ta chỉ cần request gpio number và dùng là được). Các phương pháp ánh xạ (mapping) gồm có:

  • Platform data mapping: Thực hiện trong board file.
  • Device tree: Thực hiện bằng device tree.
  • ACPI: Sử dụng ACPI mapping, thường được sử dụng trong các system x86.

1. Cấp phát và sử dụng descriptor-base GPIO

Bạn có thể sử dụng gpiod_get() hoặc gpio_get_index() để cấp phát một GPIO descriptor:

struct gpio_desc *gpiod_get_index(struct device *dev, const char *const_id, unsigned int idx, enum gpio_flags flags);
struct gpio_desc *gpiod_get(struct device *dev, const char *con_id, enum gpiod_flags flags);

Hai hàm này trả về cấu trúc GPIO descriptor tương ứng với GPIO đã được truyền vào, điểm khác nhau là hàm đầu tiên thế trả về GPIO theo index trong biến idx, còn hàm thứ hai sẽ trả về GPIO ở index 0, (nếu bạn kiểm tra kernel sourc, thì sẽ thấy hàm thứ 2 thật ra chỉ là 1 lời gọi đến hàm thứ nhất). dev là device mà GPIO descriptor thuộc về. flag dùng để cấu hình chiều truyền của GPIO, nó là một thể hiện của enum gpio_flags:

enum gpio_flags{
    GPIOD_ASIS = 0,
    GPIOD_IN = GPIOD_FLAGS_BIT_DIR_SET,
    GPIOD_OUT_LOW = GPIOD_FLAGS_BIT_DIR_SET |
                    GPIOD_FLAGS_BIT_DIR_OUT,
    GPIOD_OUT_HIGH = GPIO_FLAGS_BIT_DIR_SET |
                    GPIO_FLAGS_BIT_DIR_OUT |
                    GPIO_FLAGS_BIT_DIR_VAL,
};

Sau khi đã cấp phát và dành quyền sử dụng GPIO, chúng ta có thể thao tác đọc, ghi thay đổi debounce với các hàm sau:

int gpiod_direction_input(struct gpio_desc *desc); 
int gpiod_direction_output(struct gpio_desc *desc, int value);
int gpiod_set_debounce(struct gpio_desc *desc, unsigned debounce);
struct gpio_desc *gpio_to_desc(unsigned gpio); 
int desc_to_gpio(const struct gpio_desc *desc);
int gpiod_get_value(const struct gpio_desc *desc); 
void gpiod_set_value(struct gpio_desc *desc, int value);

B. GPIO Controller Driver.

Bây giờ chẳng may chúng ta vào kernel config và set CONFIG_GPIO_ATH79 bằng No, và quay lại insert cái module vừa viết vào kernel thì sẽ không thấy con bò gì xảy ra nữa, có thể sẽ xuất hiện những pha chửi thề mạnh mẽ: sao *** chạy nữa. Không chạy là chuẩn cmnr, vì trong Linux thì GPIO subsystem được hiện thực bằng một cái giống như provider-consumer design pattern ấy. Cái module ở trên vừa viết nó chỉ là consumer thôi, còn cái provider là một thằng khác, thường được implement bằng struct gpio_chip hoặc struct irq_chip gọi là gpio controller (sai thì thôi). :v

Leave a Comment