Hệ thống con pinctrl và gpio

1. Hệ thống con pinctrl

1.1 Hệ thống con pinctrl là gì?

Trước hết, hãy xem lại cách khởi tạo GPIO dùng cho LED:

① Sửa đổi device tree, thêm node tương ứng, trong đó trọng tâm là đặt thuộc tính reg, reg bao gồm các thanh ghi liên quan đến GPIO.

② Lấy địa chỉ của các thanh ghi GPIOI_MODER, GPIOI_OTYPER, GPIOI_OSPEEDR, GPIOI_PUPDR và GPIOI_BSRR từ thuộc tính reg, và khởi tạo chúng. Các thanh ghi này dùng để cấu hình chức năng ghép kênh, pull-up/pull-down, tốc độ của chân PI0.

③ Trong bước ②, chân PI0 được đặt làm chức năng output thông thường, do đó cần cấu hình thanh ghi liên quan đến GPIO của PI0, tức là thanh ghi GPIOI_MODER.

④ Trong bước ②, để đặt PI0 ở chế độ tốc độ cao, pull-up và push-pull, cần cấu hình các thanh ghi GPIOI_OTYPER, GPIOI_OSPEEDR và GPIOI_PUPDR.

Các cấu hình này tương tự như cấu hình GPIO trên vi điều khiển STM32. Tuy nhiên, cách cấu hình chân truyền thống là thao tác trực tiếp với thanh ghi, khá phức tạp và dễ gây ra lỗi như xung đột chức năng. Hệ thống con pinctrl được giới thiệu để giải quyết vấn đề này, với các nhiệm vụ chính sau:

① Lấy thông tin chân từ device tree.

② Dựa trên thông tin chân để cấu hình chức năng ghép kênh (multiplexing).

③ Dựa trên thông tin chân để cấu hình các đặc tính điện như pull-up/pull-down, tốc độ, khả năng dẫn dòng.

Đối với người dùng, chỉ cần đặt thuộc tính liên quan đến chân trong device tree; các công việc khởi tạo khác đều do hệ thống con pinctrl thực hiện. Nói cách khác, hệ thống con pinctrl sẽ ghép kênh một chân PIN thành GPIO.

1.2 Driver hệ thống con pinctrl trên STM32MP1

Để sử dụng hệ thống con pinctrl, chúng ta cần cấu hình thông tin chân PIN trong device tree. Thường tạo một node để mô tả cấu hình PIN. Mở file stm32mp157.dtsi và tìm node pinctrl:

pinctrl: pin-controller@50002000 {
    #address-cells = <1>;
    #size-cells = <1>;    
    compatible = "st,stm32mp157-pinctrl";
    ranges = <0 0x50002000 0xa400>;    
    interrupt-parent = <&exti>;    
    st,syscfg = <&exti 0x60 0xff>;
    hwlocks = <&hsem 0 1>;
    pins-are-numbered;
    ...
};

/*
   STM32MP1 có tối đa 176 GPIO, bao gồm PA0~PA15 ... PZ0~PZ15, PA~PK, địa chỉ bắt đầu từ 0X50002000, kết thúc tại 0X5000C3FF.
   Nhóm thanh ghi PZ bắt đầu từ 0X54004000, kết thúc tại 0X540043FF. Do đó, trong file stm32mp151.dtsi còn có node "pinctrl_z".
   Node pinctrl mô tả 11 nhóm IO PA~PK, vì vậy thuộc tính ranges có giá trị 0x50002000 là địa chỉ bắt đầu và 0xa400 là phạm vi địa chỉ thanh ghi.
 */

Trong node con pins, chúng ta lưu trữ thông tin mô tả chân của ngoại vi:

① Thuộc tính pinmux

Thuộc tính này dùng để lưu tất cả các IO mà ngoại vi sẽ sử dụng. Ví dụ:

pinmux = <STM32_PINMUX('H', 13, AF9)>;    

/*
 * @description : Cấu hình chân và chức năng ghép kênh
 * @param - port : Nhóm GPIO (ví dụ: 'H' là nhóm GPIOH)
 * @param - line : Chân trong nhóm GPIO (ví dụ: 13 là GPIOH_13, tức PH13)
 * @param - mode : Chức năng ghép kênh (ví dụ: AF9 là chức năng ghép kênh thứ 9). Cần tra cứu sổ tay dữ liệu STM32MP1 để xác định.
 * @return : Node cha tìm thấy
 * Khuyến nghị mỗi chân PIN chỉ được một ngoại vi sử dụng.
 */
STM32_PINMUX(port, line, mode);

Nếu một chân chỉ được dùng làm GPIO cơ bản, thì sử dụng "GPIO"; nếu chân được dùng làm chức năng analog, như chân thu thập ADC, thì đặt thành "ANALOG".

② Thuộc tính điện

Thuộc tính điện không bắt buộc trong hệ thống con pinctrl, có thể không cấu hình, nhưng thuộc tính pinmux là bắt buộc.

Thuộc tính điện Kiểu Chức năng
bias-disable boolean Vô hiệu hóa điện áp thiên áp nội
bias-pull-down boolean Kéo xuống nội
bias-pull-up boolean Kéo lên nội
drive-push-pull boolean Đầu ra push-pull
drive-open-drain boolean Đầu ra open-drain
output-low boolean Đầu ra mức thấp
output-high boolean Đầu ra mức cao
slew-rate enum Tốc độ chân, có thể đặt: 0~3, 0 chậm nhất, 3 nhanh nhất.

Kiểu boolean có nghĩa là chỉ cần định nghĩa thuộc tính điện trong hệ thống con pinctrl. Ví dụ: để vô hiệu hóa điện áp nội, chỉ cần thêm "bias-disable" vào tập cấu hình PIN. Lúc đó bias-pull-down và bias-pull-up sẽ không thể sử dụng. Kiểu enum được dùng như sau: để đặt tốc độ chân thấp nhất, sử dụng "slew-rate=<0>".

1.3 Mẫu thêm node pinctrl trong device tree

Ví dụ, cần ghép kênh chân PG11 thành chân UART4_TX, quy trình thêm node pinctrl như sau:

① Tạo node tương ứng

Thêm node "uart4_pins" bên dưới node pinctrl:

&pinctrl {
  uart4_pins: uart4-0 {
    /* Thông tin PIN cụ thể */
  };
};

② Thêm thuộc tính "pins"

Thêm node con "pins", node này thực sự mô tả cấu hình PIN. Lưu ý, tất cả các PIN trong cùng một node con pins phải có cùng đặc tính điện. Nếu ngoại vi sử dụng các PIN có cấu hình khác nhau, cần nhiều node con pins. Ví dụ, chân TX và RX của UART4 có cấu hình khác nhau, do đó có hai node con pins1 và pins2.

&pinctrl {
  uart4_pins: uart4-0 {
    pins1 {
      /* Thông tin cấu hình PIN cho chân UART4 TX */
    };
  };
};

③ Thêm thông tin cấu hình PIN trong node "pins"

Cuối cùng, thêm thông tin cấu hình PIN cụ thể trong node "pins":

&pinctrl {
  uart4_pins: uart4-0 {
    pins1 {
      pinmux = <STM32_PINMUX('G', 11, AF6)>; /* UART4_TX */
      bias-disable;
      drive-push-pull;
    };
  };
};

Đối với STM32MP1, nếu một IO được sử dụng làm chức năng GPIO thì không cần tạo node pinctrl tương ứng!

2. Hệ thống con gpio

2.1 Giới thiệu hệ thống con gpio

Hệ thống con gpio dùng để khởi tạo GPIO và cung cấp các hàm API tương ứng, như cài đặt GPIO làm input/output, đọc giá trị GPIO, v.v. Mục đích là giúp nhà phát triển driver dễ dàng sử dụng GPIO. Nhà phát triển driver thêm thông tin GPIO vào device tree, sau đó có thể sử dụng các hàm API của hệ thống con gpio để thao tác GPIO.

2.2 Driver hệ thống con gpio trên STM32MP1

① Thông tin gpio trong device tree

Ví dụ với chân PI0 thuộc nhóm GPIOI, mở file stm32mp151.dtsi:

pinctrl: pin-controller@50002000 {
    #address-cells = <1>;
    #size-cells = &<1>;
    compatible = "st,stm32mp157-pinctrl";
    ...
    gpioi: gpio@5000a000 {
        gpio-controller;    
        #gpio-cells = <2>;    
        interrupt-controller;    
        #interrupt-cells = <2>;
        reg = <0x8000 0x400>;  
        clocks = <&rcc GPIOI>;    
        st,bank-name = "GPIOI";
        status = "disabled";     
    };
};

gpioi:gpio@5000a000 và các thông tin bên dưới là thông tin điều khiển của GPIOI, thuộc node con của pinctrl. Vì driver của hai hệ thống con này là cùng một file, nên khi đăng ký driver pinctrl, driver gpio cũng sẽ được đăng ký.

Trên board phát triển ST EVK, chân PG1 được sử dụng làm chân phát hiện thẻ SD (CD):

cd-gpios = <&gpiog 1 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;

/*
&gpiog : IO dùng cho chân CD thuộc nhóm GPIOG.
1 : IO số 1 của nhóm GPIOG, tức là PG1.
GPIO_ACTIVE_LOW | GPIO_PULL_UP : Mức thấp hiệu lực (khi PG1 ở mức thấp, thẻ SD được cắm) và có điện trở kéo lên.
*/

Khi PG1 được dùng làm GPIO, không cần thêm node pinctrl tương ứng.

② Giới thiệu driver GPIO

Driver pinctrl và gpio của STM32MP1 nằm chung trong file pinctrl-stm32mp157.c, hàm vào là stm32_pctl_probe:

int stm32_pctl_probe(struct platform_device *pdev)
{
    ...
    for_each_available_child_of_node(np, child) {
        if (of_property_read_bool(child, "gpio-controller")) {    
            ret = stm32_gpiolib_register_bank(pctl, child);    
            if (ret) {
                of_node_put(child);
                return ret;
            }
        }
    }
    ...
}

2.3 Các hàm API của hệ thống con gpio

① Hàm gpio_request

/*
 * @description : Yêu cầu một chân GPIO. Phải gọi hàm này trước khi sử dụng GPIO.
 * @param - gpio : Số hiệu GPIO cần yêu cầu. Dùng hàm of_get_named_gpio để lấy từ device tree.
 * @param - label : Đặt tên cho GPIO.
 * @return : 0 nếu thành công; giá trị khác nếu thất bại.
 */
int gpio_request(unsigned gpio, const char *label);

② Hàm gpio_free

/*
 * @description : Giải phóng một GPIO khi không còn sử dụng.
 * @param - gpio : Số hiệu GPIO cần giải phóng.
 * @return : Không có.
 */
void gpio_free(unsigned gpio);

③ Hàm gpio_direction_input

/*
 * @description : Đặt một GPIO làm đầu vào.
 * @param - gpio : Số hiệu GPIO cần đặt làm đầu vào.
 * @return : 0 nếu thành công; giá trị âm nếu thất bại.
 */
int gpio_direction_input(unsigned gpio);

④ Hàm gpio_direction_output

/*
 * @description : Đặt một GPIO làm đầu ra, và đặt giá trị mặc định.
 * @param - gpio : Số hiệu GPIO cần đặt làm đầu ra.
 * @param - value : Giá trị đầu ra mặc định.
 * @return : 0 nếu thành công; giá trị âm nếu thất bại.
 */
int gpio_direction_output(unsigned gpio, int value);

⑤ Hàm gpio_get_value

/*
 * @description : Lấy giá trị của một GPIO (0 hoặc 1), là một macro.
 * @param - gpio : Số hiệu GPIO cần lấy giá trị.
 * @return : Giá trị không âm nếu thành công (giá trị GPIO); giá trị âm nếu thất bại.
 */
#define gpio_get_value __gpio_get_value
int __gpio_get_value(unsigned gpio);

⑥ Hàm gpio_set_value

/*
 * @description : Đặt giá trị cho một GPIO, là một macro.
 * @param - gpio : Số hiệu GPIO cần đặt giá trị.
 * @param - value : Giá trị cần đặt.
 * @return : Không có.
 */
#define gpio_set_value __gpio_set_value
void __gpio_set_value(unsigned gpio, int value);

2.4 Mẫu thêm node gpio trong device tree

① Tạo node thiết bị led

led {
    /* Nội dung node */
};

② Thêm thuộc tính GPIO

led {
    compatible = "atk,led";
    gpio = <&gpioi 0 GPIO_ACTIVE_LOW>;
    status = "okay";
};

2.5 Các hàm OF liên quan đến gpio thông dụng

① Hàm of_gpio_named_count

/*
 * @description : Đếm số lượng thông tin GPIO được định nghĩa trong một thuộc tính. Lưu ý, thông tin GPIO rỗng cũng được tính.
 * @param - np : Node thiết bị.
 * @param - propname : Tên thuộc tính GPIO cần đếm.
 * @return : Giá trị dương nếu thành công (số lượng GPIO); giá trị âm nếu thất bại.
 */
int of_gpio_named_count(struct device_node *np, const char *propname);

② Hàm of_gpio_count

/*
 * @description : Đếm số lượng GPIO trong thuộc tính "gpios". Hàm of_gpio_named_count có thể đếm thông tin GPIO của bất kỳ thuộc tính nào.
 * @param - np : Node thiết bị.
 * @return : Giá trị dương nếu thành công (số lượng GPIO); giá trị âm nếu thất bại.
 */
int of_gpio_count(struct device_node *np);

③ Hàm of_get_named_gpio

/*
 * @description : Lấy số hiệu GPIO. Số hiệu này rất quan trọng vì các hàm API GPIO trong nhân Linux đều sử dụng nó.
 * @param - np : Node thiết bị.
 * @param - propname : Tên thuộc tính chứa thông tin GPIO cần lấy.
 * @param - index : Chỉ mục GPIO, vì một thuộc tính có thể chứa nhiều GPIO. Tham số này chỉ định lấy GPIO thứ mấy. Nếu chỉ có một thông tin GPIO, đặt là 0.
 * @return : Giá trị dương nếu thành công (số hiệu GPIO); giá trị âm nếu thất bại.
 */
int of_get_named_gpio(struct device_node *np,
                      const char *propname,
                      int index);

Tóm lại: sự khác biệt chính giữa hệ thống con pinctrl và gpio là:

Hệ thống con pinctrl: Ghép kênh chân (có thể dùng chân làm GPIO thông thường hoặc UART TX) và cấu hình chân (đặc tính điện).

Hệ thống con gpio: Hỗ trợ đọc chân (khi là đầu vào) và xuất mức cao/thấp (khi là đầu ra).

Hệ thống con gpio chủ yếu điều khiển và đọc các chân I/O đa năng, trong khi hệ thống con pinctrl quản lý và cấu hình chức năng cũng như ghép kênh của các chân phần cứng.

3. Viết chương trình thí nghiệm

3.1 Sửa file device tree

Đầu tiên, tìm file stm32mp157d-atk.dts trong đường dẫn ~/linux/atk-mpl/linux/my_linux/linux-5.4.31/arch/arm/boot/dts. Tạo node LED bên dưới node gốc "/":

gpioled {
    compatible = "alientek,led";
    status = "okay";
    led-gpio = <&gpioi 0 GPIO_ACTIVE_LOW>;    
};

Sau đó, biên dịch file stm32mp157d-atk.dts trong thư mục /linux/atk-mpl/linux/my_linux/linux-5.4.31 bằng lệnh:

make dtbs
cd /arch/arm/boot/dts
sudo cp stm32mp157d-atk.dtb /home/alientek/linux/tftpboot/ -f

Kiểm tra xem node gpioled có tồn tại trong /proc/device-tree hay không:

3.2 Viết driver LED

Như phần trước, tạo thư mục 5_gpioled, tạo workspace Vscode trong đó, và tạo file gpioled.c.

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define GPIOLED_CNT			1		  	
#define GPIOLED_NAME		"gpioled"	
#define LEDOFF 				0			
#define LEDON 				1			

struct gpioled_dev{
    dev_t devid;			
    struct cdev cdev;		
    struct class *class;	
    struct device *device;	
    int major;				
    int minor;				
    struct device_node	*nd; 
    int led_gpio;			
};

struct gpioled_dev gpioled;	

static int led_open(struct inode *inode, struct file *filp)
{
    filp->private_data = &gpioled; 
    return 0;
}

static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    return 0;
}

static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    int retvalue;
    unsigned char databuf[1];
    unsigned char ledstat;
    struct gpioled_dev *dev = filp->private_data;   

    retvalue = copy_from_user(databuf, buf, cnt); 
    if(retvalue < 0) {
        printk("kernel write failed!\r\n");
        return -EFAULT;
    }

    ledstat = databuf[0];		

    if(ledstat == LEDON) {	
        gpio_set_value(dev->led_gpio, 0);	
    } else if(ledstat == LEDOFF) {
        gpio_set_value(dev->led_gpio, 1);	
    }
    return 0;
}

static int led_release(struct inode *inode, struct file *filp)
{
    return 0;
}

static struct file_operations gpioled_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .read = led_read,
    .write = led_write,
    .release = 	led_release,
};

static int __init led_init(void)
{
    int ret = 0;
    const char *str;

    gpioled.nd = of_find_node_by_path("/gpioled");
    if(gpioled.nd == NULL) {
        printk("gpioled node not find!\r\n");
        return -EINVAL;
    }

    ret = of_property_read_string(gpioled.nd, "status", &str);      
    if(ret < 0) 
        return -EINVAL;

    if (strcmp(str, "okay"))
        return -EINVAL;
    
    ret = of_property_read_string(gpioled.nd, "compatible", &str);
    if(ret < 0) {
        printk("gpioled: Failed to get compatible property\n");
        return -EINVAL;
    }

    if (strcmp(str, "alientek,led")) {
        printk("gpioled: Compatible match failed\n");
        return -EINVAL;
    }

    gpioled.led_gpio = of_get_named_gpio(gpioled.nd, "led-gpio", 0);    
    if(gpioled.led_gpio < 0) {
        printk("can't get led-gpio");
        return -EINVAL;
    }
    printk("led-gpio num = %d\r\n", gpioled.led_gpio);

    ret = gpio_request(gpioled.led_gpio, "LED-GPIO");       
    if (ret) {
        printk(KERN_ERR "gpioled: Failed to request led-gpio\n");
        return ret;
    }

    ret = gpio_direction_output(gpioled.led_gpio, 1);
    if(ret < 0) {
        printk("can't set gpio!\r\n");
    }

    if (gpioled.major) {		
        gpioled.devid = MKDEV(gpioled.major, 0);
        ret = register_chrdev_region(gpioled.devid, GPIOLED_CNT, GPIOLED_NAME);
        if(ret < 0) {
            pr_err("cannot register %s char driver [ret=%d]\n", GPIOLED_NAME, GPIOLED_CNT);
            goto free_gpio;
        }
    } else {						
        ret = alloc_chrdev_region(&gpioled.devid, 0, GPIOLED_CNT, GPIOLED_NAME);	
        if(ret < 0) {
            pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n", GPIOLED_NAME, ret);
            goto free_gpio;
        }
        gpioled.major = MAJOR(gpioled.devid);	
        gpioled.minor = MINOR(gpioled.devid);	
    }
    printk("gpioled major=%d,minor=%d\r\n",gpioled.major, gpioled.minor);	
	
    gpioled.cdev.owner = THIS_MODULE;
    cdev_init(&gpioled.cdev, &gpioled_fops);
	
    cdev_add(&gpioled.cdev, gpioled.devid, GPIOLED_CNT);
    if(ret < 0)
        goto del_unregister;
		
    gpioled.class = class_create(THIS_MODULE, GPIOLED_NAME);
    if (IS_ERR(gpioled.class)) {
        goto del_cdev;
    }

    gpioled.device = device_create(gpioled.class, NULL, gpioled.devid, NULL, GPIOLED_NAME);
    if (IS_ERR(gpioled.device)) {
        goto destroy_class;
    }
    return 0;
	
destroy_class:
    class_destroy(gpioled.class);
del_cdev:
    cdev_del(&gpioled.cdev);
del_unregister:
    unregister_chrdev_region(gpioled.devid, GPIOLED_CNT);
free_gpio:
    gpio_free(gpioled.led_gpio);
    return -EIO;
}

static void __exit led_exit(void)
{
    cdev_del(&gpioled.cdev);
    unregister_chrdev_region(gpioled.devid, GPIOLED_CNT); 
    device_destroy(gpioled.class, gpioled.devid);
    class_destroy(gpioled.class);
    gpio_free(gpioled.led_gpio); 
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");

Flowchart của driver:

Driver này đơn giản hóa việc cấu hình thanh ghi bằng cách sử dụng các hàm API do Linux cung cấp. Điểm quan trọng là khi sử dụng hệ thống con gpio, cần lấy số hiệu thiết bị và yêu cầu GPIO từ hệ thống con.

4. Chạy thử nghiệm

4.1 Viết chương trình

Tương tự như phần trước, chỉ cần thay đổi tên file đối tượng gpioled.o, và cách biên dịch cũng tương tự.

KERNELDIR := /home/alientek/linux/atk-mpl/linux/my_linux/linux-5.4.31   
CURRENT_PATH := $(shell pwd)        
obj-m := gpioled.o

build: kernel_modules

kernel_modules:
    $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
    $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

Sau đó, biên dịch file ledApp.c:

arm-none-linux-gnueabihf-gcc ledApp.c -o ledApp

Sao chép ledApp và gpioled.ko đã biên dịch vào:

sudo cp gpioled.ko ledApp /home/alientek/linux/nfs/rootfs/lib/modules/5.4.31

4.2 Chạy thử nghiệm

Giống như phần trước:

depmod             
modprobe gpioled   

Kiểm tra bật và tắt LED:

./ledApp /dev/gpioled 1     
./ledApp /dev/gpioled 0     

# Cuối cùng, gỡ driver
rmmod gpioled.ko

Tóm lại: Với hệ thống con pinctrl và gpio, lập trình driver trở nên đơn giản hơn, loại bỏ quá trình cấu hình thanh ghi và thay thế bằng các hàm API do nhân Linux cung cấp.

Thẻ: pinctrl GPIO stm32mp1 device-tree linux-kernel

Đăng vào ngày 18 tháng 6 lúc 03:44