GPIO是写单片机出身的人的“白月光”
GPIO,全称 General-Purpose Input/Output(通用输入输出),是一种软件运行期间能够动态配置和控制的通用引脚。对于GPIO的开发包括,通过用户空间设置其输入抑或输出,设置或获取其高低电平状态;在内核里设置引脚输入输出,设置中断;以及完成复用等。
一、Userspace使用GPIO
1.硬件以及规格书
以firefly ROC-RK3588S-PC 开发板为例,
2.GPIO的计算
ROC-RK3588S-PC 有 5 组 GPIO bank:GPIO0~GPIO4,每组又以 A0~A7, B0~B7, C0~C7, D0~D7 作为编号区分,常用以下公式计算引脚:
GPIO pin脚计算公式:pin = bank * 32 + number
GPIO 小组编号计算公式:number = group * 8 + X
下面演示GPIO3_C6 pin脚计算方法:
bank = 3; //GPIO3_C6 => 3, bank ∈ [0,4]
group = 2; //GPIO3_C6 => 2, group ∈ {(A=0), (B=1), (C=2), (D=3)}
X = 6; //GPIO3_C6 => 0, X ∈ [0,7]
number = group * 8 + X = 2 * 8 + 6 = 22
pin = bank32 + number= 3 * 32 + 22 = 118;
3.设备节点控制gpio
当GPIO3_C6 脚没有被其它外设复用时, 我们可以通过export导出该引脚去使用
# cd /sys/class/gpio/
# ls
export gpiochip128 gpiochip509 gpiochip96
gpiochip0 gpiochip32 gpiochip64 unexport
# echo 118 > export
# ls /sys/class/gpio/gpio118
active_low device direction edge power subsystem uevent value
#设置输出
# echo out > gpio118/direction
#输出低电平,可以测得该引脚电压为0V
# echo 0 > gpio118/value
#输出高电平,可以测得该引脚电压为1.8V
# echo 1 > gpio118/value
#设置输入
# echo in > gpio118/direction
#获取该引脚的电平,可以测得为1.8V
# cat gpio118/value
1
二、内核控制GPIO
以Firefly-RK3399为例,增加一个gpio输出控制等的驱动
1.硬件与原理图
如图绿灯在DIY_LED是高电平时点亮,其中WORK_LED是gpio0_B5
2.设备树描述硬件设备以及复用信息
由于已经有驱动控制该引脚,需要注释掉之前存在的
leds {
compatible = "gpio-leds";
work {
label = "firefly:blue:power";
linux,default-trigger = "ir-power-click";
gpios = <&gpio2 27 GPIO_ACTIVE_HIGH>;
pinctrl-names = "default";
pinctrl-0 = <&led_power>;
default-state = "on";
};
/*
user {
label = "firefly:yellow:user";
linux,default-trigger = "ir-user-click";
gpios = <&gpio0 13 GPIO_ACTIVE_HIGH>;
pinctrl-names = "default";
pinctrl-0 = <&led_user>;
default-state = "off";
};*/
};
usergpio {
compatible = "my_user_gpio";
gpios = <&gpio0 13 GPIO_ACTIVE_HIGH>;
pinctrl-names = "default";
pinctrl-0 = <&led_green>;
};
作为一个描述gpio设备的设备节点,包含有该设备的compatible描述符,有能被驱动计算出gpio索引号的语句,有配置引脚复用的语句
&pinctrl {
...
leds {
led_power: led-power {
rockchip,pins = <2 27 RK_FUNC_GPIO &pcfg_pull_none>;
};
/*led_user: led-user {
rockchip,pins = <0 13 RK_FUNC_GPIO &pcfg_pull_none>;
};*/
};
usergpio {
led_green: led-green {
rockchip,pins = <0 13 RK_FUNC_GPIO &pcfg_pull_none>;
};
};
...
}
在设备树中配置 GPIO,需要配置引脚的功能复用与电气属性
对于 rockchip 引脚,配置如下:
rockchip,pins = <PIN_BANK PIN_BANK_IDX MUX &phandle>
其中:
PIN_BANK
:引脚所在的 bankPIN_BANK_IDX
:引脚所在 bank 的引脚号MUX
:功能复用配置,0
表示普通 GPIO,1-N
表示特殊的功能复用phandle
:引脚一般配置,例如内部上拉、电流强度等,在Documentation/devicetree/bindings/pinctrl/pinctrl-bindings.txt
文件中描述
配置 GPIO0_B5 引脚:
rockchip,pins = <0 13 RK_FUNC_GPIO &pcfg_pull_none>;
此处的含义:
PIN_BANK
等于0
PIN_BANK_IDX
等于13
RK_FUNC_GPIO
代表使用普通 GPIO 功能pcfg_pull_none
代表普通配置
3.probe对设备树的解析
int gpio_num = 0;
int usergpio_probe(struct platform_device *pdev)
{
enum of_gpio_flags flag;
struct device_node *usergpio_device_node = pdev->dev.of_node;
//gpio_num = of_get_named_gpio(usergpio_device_node, "gpios", 0);
gpio_num = of_get_named_gpio_flags(usergpio_device_node, "gpios", 0, &flag);
//printk ("gpio_num is %d \n ",gpio_num);
//printk ("flag is %d \n ",flag);
of_get_named_gpio
和of_get_named_gpio_flags
都可以从设备树中读取 "gpios"
的 GPIO 配置编号,后者还能可以从传入的flag指针获取设备树里的gpio配置
4.gpio的控制
if(!gpio_is_valid(gpio_num))
{
printk("of_get_named_gpio_flags is error \n");
return -1;
}
if(gpio_request(gpio_num,"usergpio_gpios"))
{
printk("gpio_request is error \n");
gpio_free(gpio_num);
return -1;
}
gpio_direction_output(gpio_num,flag);
msleep(500);
gpio_direction_output(gpio_num,~flag);
msleep(500);
gpio_direction_output(gpio_num,flag);
msleep(500);
gpio_direction_output(gpio_num,~flag);
gpio_is_valid
判断该 GPIO 编号是否有效,gpio_request
则申请占用该 GPIO。如果初始化过程出错,需要调用 gpio_free
来释放之前申请过且成功的 GPIO 。在驱动中调用 gpio_direction_output
就可以设置输出高还是低电平,这里默认输出从 DTS 获取得到的有效电平 GPIO_ACTIVE_HIGH
,即为高电平,后面对flag取反,使灯闪烁
实际中如果要读出 GPIO,需要先设置成输入模式,然后再读取值:
int val;
gpio_direction_input(your_gpio);
val = gpio_get_value(your_gpio);
下面是常用的 GPIO API 定义:
#include <linux/gpio.h>
#include <linux/of_gpio.h>
enum of_gpio_flags {
OF_GPIO_ACTIVE_LOW = 0x1,
};
int of_get_named_gpio_flags(struct device_node *np, const char *propname,
int index, enum of_gpio_flags *flags);
int gpio_is_valid(int gpio);
int gpio_request(unsigned gpio, const char *label);
void gpio_free(unsigned gpio);
int gpio_direction_input(int gpio);
int gpio_direction_output(int gpio, int v);
三、设备节点控制gpio
上面让gpio输出高低电平加延时的做法显然不符合Linux驱动的思想
理想状态应该为,用户空间有设备节点能写入或读取灯的状态,驱动能根据用户空间传递来的控制命令的字符串,控制灯GPIO。
下面为优化驱动的结果
#echo on灯会亮,echo off灯灭,cat user_led_status能看出灯的亮灭
root@firefly:/sys/devices/virtual/user_led_class/user_led# echo on > user_led_status
root@firefly:/sys/devices/virtual/user_led_class/user_led# echo off > user_led_status
root@firefly:/sys/devices/virtual/user_led_class/user_led# cat user_led_status
off
其优化的思路为:
由原来的platform_driver的probe里获取设备树里的gpio信息,并控制
改为platform_driver的probe里获取设备树里的gpio信息,创建一个字符设备,在给字符设备创建一个文件,在文件的attr(属性)下的操作符里控制gpio
int usergpio_probe(struct platform_device *pdev)
{
...
major_num = register_chrdev(0,DEVICE_NAME,&user_led_fops);
user_led_class = class_create(THIS_MODULE, "user_led_class");
user_led_device = device_create(user_led_class, NULL, MKDEV(major_num, 0), NULL, DEVICE_NAME);
device_create_file(user_led_device, &dev_attr_user_led_status);
gpio_num = of_get_named_gpio_flags(usergpio_device_node, "gpios", 0, &flag);
...
}
//cat命令时,将会调用该函数
static ssize_t usr_led_status_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
return sprintf(buf, "%s", kernel_buffer);
}
//echo命令时,将会调用该函数
static ssize_t usr_led_status_set(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t len)
{
sprintf(kernel_buffer, "%s", buf);
if(!strcmp(kernel_buffer, "on\n"))
{
gpio_direction_output(gpio_num,~flag);
}
if(!strcmp(kernel_buffer, "off\n"))
{
gpio_direction_output(gpio_num,flag);
}
return len;
}
//定义一个名字为my_device_test的设备属性文件
static DEVICE_ATTR(user_led_status, S_IWUSR|S_IRUSR, usr_led_status_show, usr_led_status_set);
**PS:**字符设备、class、DEVICE_ATTR是什么与本文标题关系不大,故略过。应用程序中当然也可以使用c程序,直接操作字符设备fd,我觉得效果不如echo、cat直观,故略过。本文更适合那种喜欢看现象再编程的小白。
四、附录源码
改进后的源码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/delay.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include "usergpio.h"
#define DEVICE_NUMBER 1
#define DEVICE_NAME "user_led"
#define DEVICE_MINOR_NUMBER 0
int gpio_num = 0;
enum of_gpio_flags flag;
static struct class *user_led_class;
struct device *user_led_device;
int ioarg = 500;
static int major_num = 0;
//module_param(major_num, int, S_IRUSR);
//module_param(minor_num, int, S_IRUSR);
static char kernel_buffer[] = "!!!";
//cat命令时,将会调用该函数
static ssize_t usr_led_status_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
return sprintf(buf, "%s", kernel_buffer);
}
//echo命令时,将会调用该函数
static ssize_t usr_led_status_set(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t len)
{
sprintf(kernel_buffer, "%s", buf);
if(!strcmp(kernel_buffer, "on\n"))
{
gpio_direction_output(gpio_num,~flag);
}
if(!strcmp(kernel_buffer, "off\n"))
{
gpio_direction_output(gpio_num,flag);
}
return len;
}
//定义一个名字为my_device_test的设备属性文件
static DEVICE_ATTR(user_led_status, S_IWUSR|S_IRUSR, usr_led_status_show, usr_led_status_set);
static int user_led_open(struct inode *inode, struct file *file) {
printk("user_led_open\n");
return 0;
}
static int user_led_release(struct inode *inode, struct file *file) {
printk("user_led_release\n");
return 0;
}
static const struct file_operations user_led_fops = {
.owner = THIS_MODULE,
.open = user_led_open,
.release = user_led_release,
};
int usergpio_probe(struct platform_device *pdev)
{
struct device_node *usergpio_device_node = pdev->dev.of_node;
printk( "\033[0;32m ***func is %s , line is %d *** \033[0m",__func__,__LINE__);
//usergpio_device_node = of_find_node_by_path("/usergpio");
//gpio_num = of_get_named_gpio(usergpio_device_node, "gpios", 0);
if(usergpio_device_node == NULL)
{
printk("of_find_node_by_pdev is error \n");
return -1;
}
major_num = register_chrdev(0,DEVICE_NAME,&user_led_fops);
user_led_class = class_create(THIS_MODULE, "user_led_class");
user_led_device = device_create(user_led_class, NULL, MKDEV(major_num, 0), NULL, DEVICE_NAME);
device_create_file(user_led_device, &dev_attr_user_led_status);
gpio_num = of_get_named_gpio_flags(usergpio_device_node, "gpios", 0, &flag);
if(!gpio_is_valid(gpio_num))
{
printk("of_get_named_gpio_flags is error \n");
return -1;
}
if(gpio_request(gpio_num,"usergpio_gpios"))
{
printk("gpio_request is error \n");
gpio_free(gpio_num);
return -1;
}
gpio_direction_output(gpio_num,flag);
return 0;
}
int usergpio_remove(struct platform_device *pdev)
{
printk("usergpio_remove, Bye Bye!\n");
device_remove_file(user_led_device,&dev_attr_user_led_status);
device_destroy(user_led_class,MKDEV(major_num,0));
class_unregister(user_led_class);
class_destroy(user_led_class);
unregister_chrdev(major_num,DEVICE_NAME);
gpio_free(gpio_num);
return 0;
}
const struct of_device_id of_match_table_wxdtest[] = {
{.compatible = "my_user_gpio"},
{}
};
struct platform_driver usergpio_driver = {
.probe = usergpio_probe,
.remove = usergpio_remove,
.driver = {
.owner = THIS_MODULE,
.name = "my_user_gpio",
.of_match_table = of_match_table_wxdtest
},
};
static int usergpio_init(void)
{
int ret = 0;
printk(KERN_ALERT "wxd kernel driver enter\n");
ret = platform_driver_register(&usergpio_driver);
if(ret < 0)
{
printk("platform_driver_register error\n");
return ret;
}
printk("platform_driver_register ok \n");
return 0;
}
static void usergpio_exit(void)
{
printk(KERN_ALERT "wxd kernel driver exit\n");
platform_driver_unregister(&usergpio_driver);
}
module_init(usergpio_init);
module_exit(usergpio_exit);
MODULE_LICENSE("Dual BSD/GPL");
MODULE_DESCRIPTION("A Sample user_led Module");
MODULE_ALIAS("A Sample module");