linux内核 GPIO操作

大概10年前学习过linux 内核,但当时的GPIO的操作简单粗暴,现在pin-ctrl gpio驱动 dts 一堆,搞得不知从何下手,折腾了好几天,做个笔记,免得忘记了又得重来。

一 操作寄存器

根据芯片手册得到GPIO相关的寄存器地址(这里s5p6818为例)

//这是芯片手册里给出的真实物理地址
#define PHYS_BASE_GPIOA_OUTDAT (0xC001A000)  //
#define PHYS_BASE_GPIOA_OUTEN  (0xC001A004)
#define PHYS_BASE_GPIOA_PAD	   (0xC001A018)
#define PHYS_BASE_GPIOA_FUNC0  (0xC001A020)
#define PHYS_BASE_GPIOA_FUNC1  (0xC001A024)

物理地址不能直接操作需要用ioremap 处理:
参数1 :真实物理地址
参数2 :地址空间长度,以byte为单位,arm寄存器一般为32为,所以为4

void __iomem *gpioa_outdat_addr;
void __iomem *gpioa_outen_addr;
void __iomem *gpioa_pad_addr;
void __iomem *gpioa_func0_addr;
void __iomem *gpioa_func1_addr;
gpioa_outdat_addr = ioremap(PHYS_BASE_GPIOA_OUTDAT,4);
gpioa_outen_addr = ioremap(PHYS_BASE_GPIOA_OUTEN,4);
gpioa_pad_addr = ioremap(PHYS_BASE_GPIOA_PAD,4);
gpioa_func0_addr =ioremap(PHYS_BASE_GPIOA_FUNC0,4);
gpioa_func1_addr =ioremap(PHYS_BASE_GPIOA_FUNC1,4);

得到的void __iomem * 值就可以用readl 读(地址可读),用writel 写(地址可写)

u32 data_en = readl(gpioa_outen_addr);
data_en = 0x000FFFE2;
writel(data_en,gpioa_outen_addr);

如何使用这些寄存器,看芯片手册
用完后记得释放:

iounmap(gpioa_outdat_addr);
iounmap(gpioa_outen_addr);
iounmap(gpioa_pad_addr);
iounmap(gpioa_func0_addr);
iounmap(gpioa_func1_addr);

readl ,writel操作寄存器的速度很慢,在GPIO模拟时序时可能达不到速度要求,但其实寄存器读写速度是非常快的,只是这两个函数加入了一些安全措施。追求速度可以用:
__raw_writel
__raw_readl
这是两个内联函数,只做了地址取值和赋值:

static inline void __raw_writel(u32 value, volatile void __iomem *addr)
{
	*(volatile u32 __force *)addr = value;
}
static inline u32 __raw_readl(const volatile void __iomem *addr)
{
	return *(const volatile u32 __force *)addr;
}

所以可以更直接的读写:

u32 data_en =*(const volatile u32 __force *)gpioa_outen_addr;
data_en = 0x000FFFE2;
*(volatile u32 __force *)gpioa_outen_addr = value;

这么做的弊端是在某些情况下,可能会被编译器优化掉,造成赋值不成功

二 用linux gpio驱动

直接操作寄存器的优点是速度快,可同时操作多个gpio脚位,弊端是寄存器太多,设置值非常繁琐,上面的例子中操作一个GPIO 需要设置 5个寄存器,还要找对相应的bit位,一不小心就错了,可能一天的时间都耗费在位运算中。gpio 驱动的作用就体现出来了。
GPIO 操作API:

//申请GPIO,参数1:gpio 编号,参数2:gpio 名字 (自定义)
int gpio_request(unsigned gpio, const char *label)

//释放GPIO ,参数1:gpio编号
void gpio_free(unsigned gpio)

//设置GPIO为输出模式,同时将输出设为value 
//参数1:gpio编号,参数2:输出的值 (0或1)
static inline int gpio_direction_output(unsigned gpio, int value)

//设置GPIO为输入模式
static inline int gpio_direction_input(unsigned gpio)

//判断gpio是否有效,就是判断gpio的编号是否在范围内
//比如soc的所有gpio编号是0~128,如果输入编号在此范围则认为有效
//返回值:true 为有效,false为无效。
static inline int gpio_is_valid(unsigned int gpio)

//设置GPIO的状态,参数1:GPIO编号,参数2:设置的值(0或1)
static inline void gpio_set_value(unsigned int gpio, int value)

//获取GPIO的状态,参数1:GPIO编号
//返回值;GPIO 状态,0:GPIO低点位,1:GPIO高电位
static inline int gpio_get_value(unsigned int gpio)

GPIO编号跟soc的BSP有关,可以阅读相关代码获得。
s5p6818有5个GPIO口(A,B,C,D,E)每个口有32个引脚,总共160个,所以依次编号为0~159,(A0=0,A1=1------E31=159)
这个编码规则应该是统一的,GPIO口的名字各芯片不一样,有些叫GPIO0 GPIO1------
gpio_request 的第二个参数是给申请到的gpio取个别名,自定义的,名字没什么要求,最好加一些前缀或后缀特异化,避免重复。

//这段代码将GPIOE_30 设置为输出模式并拉低,20us后再拉高
#define FPGA_POWER_GPIONUM     158     //GPIOE_30
//request PWOER GPIO
if(gpio_request(FPGA_POWER_GPIONUM,"FPGA_POWER")){
	gpio_direction_output(FPGA_POWER_GPIONUM,0);
	usleep(20);
	//因为已经设置为output模式,所以可以直接调用gpio_set_value
	//即使gpio为input模式也可以设置,只是不能作用到引脚
	gpio_set_value(FPGA_POWER_GPIONUM,1);
}

gpio_request 这个主要是判断有没有被其他模块占用,没有则注册,告诉GPIO系统我占用了,该函数如果失败,基本上就是有其他模块也用到了这个GPIO。这个问题在移植系统时非常常见,因为自己的板卡走线跟demo区别很大,用demo移植时经常会出现GPIO被占用,但是demo代码一般都是在dts中设置gpio脚,所以只要检查DTS哪里配置了这个脚就可以了。

三GPIO 中断

也是用GPIO驱动API来实现,直接操作寄存器太繁琐,不弄了,头发不多了!
中断单独讲,因为它牵涉到中断API,(这个超纲了*_*)

//根据gpio编号得到irq编号
static inline int gpio_to_irq(unsigned int gpio)

//使能中断,参数:irq编号
void enable_irq(unsigned int irq)

//禁用中断,参数:irq编号
void disable_irq(unsigned int irq)

// 申请中断
//参数1:中断号,参数2:中断处理函数,参数3:触发方式, 参数4:中断名(自定义)
//参数5:传递给中断处理函数的参数
//返回值:不为0表示失败。
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
	    const char *name, void *dev)
	    
//释放中断
//参数1 :中断号,参数2 :中断处理函数
void free_irq(unsigned int irq, void *dev_id)

gpio_to_irq 获得的中断号贯穿整个中断操作,输入参数就是上面提到的GPIO编号,前提是该GPIO能做中断引脚,这个跟soc有关,具体看芯片手册。
request_irq 的第二个参数是中断处理函数,其定义方式如下:

irqreturn_t bc_irq_handler(int irq, void *dev_id)

第一个参数就是中断号,不同的中断处理函数可以相同,具体是哪一个中断就用中断号来区分。第二个参数及request_irq 的最后一个参数。

以下代码是将gpioa-20引脚设置为中断模式,下降沿触发,处理函数为bc_irq_handler,中断处理函数中打印出函数名及中断号。

irqreturn_t bc_irq_handler(int irq, void *dev_id){
	printk("%s %d\n",__func__,irq);
}

#define FPGA_B_INT               20       // gpioa-20 intrupt
int b_irq = gpio_to_irq(FPGA_B_INT);
ret = request_irq(g_privdat->b_irq,bc_irq_handler,IRQF_TRIGGER_FALLING,"B int",NULL);
if(ret){
	printk("request_irq error: %d\n",b_irq);
	return;
}
enable_irq(b_irq);

四 gpio与dts

这一节也超纲了,会涉及到DTS(设备树),但是主要还是列举一些API的用法,不会深入解释。头发还够的话,再写DTS的笔记。

简单说一下dts note 与代码的对应关系。
每一个dts note 都有一个compatible 字段,如下列dts note描述:

//
bt_bcm {
	compatible = "bluetooth-platdata";
	uart_rts_gpios = <&gpio_c 6 GPIO_ACTIVE_LOW>; 
	uart_cts_gpios = <&gpio_c 5 GPIO_ACTIVE_LOW>; 
	pinctrl-names = "default", "rts_gpio";
	pinctrl-0 = <&serial1_flow>;
	pinctrl-1 = <&bt_flow_gpio>;

	reset-gpios    = <&gpio_b 26 GPIO_ACTIVE_HIGH>; 
	wake-gpios     = <&gpio_b 27 GPIO_ACTIVE_HIGH>;
	status = "okay";
};

compatible是自己定义的,它与驱动一一对应

static struct of_device_id bt_platdata_of_match[] = {
    { .compatible = "bluetooth-platdata" },
    { }
};

驱动中定义上面的结构体,并注册到系统中,那么系统就会匹配dts note 和驱动,并调用驱动中的probe函数:

static int rfkill_rk_probe(struct platform_device *pdev){
	......
}

probe函数的输入参数platform_device 结构体中有 pdev->dev->of_node 及dts的note内容
从note中获取gpio的两种方法:

//参数1 :对应的probe 函数输入参数下的pdev->dev->of_node 
//参数2 :note 中GPIO 列表的字段名
//参数3 :要取的gpio在列表中的下标
//参数4:gpio默认电位值
//返回值:GPIO编号
int of_get_named_gpio_flags(struct device_node *np, const char *list_name,
			    int index, enum of_gpio_flags *flags)

//参数1:probe函数输入参数中的设备结构体pdev->dev
//参数2:note中的字段名
//参数3:要将gpio设置为何种状态
//返回值:gpio_desc 结构体,该结构体是对GPIO编号的包装。
struct gpio_desc *__must_check devm_gpiod_get_optional(struct device *dev,
						       const char *con_id,
						       enum gpiod_flags flags)

示例:
第一种:

enum of_gpio_flags flags;
gpio = of_get_named_gpio_flags(node, "BT,reset_gpio", 0, &flags);

note不用解释了, “BT,reset_gpio” 及note中的字段,上面的note示例中可以看到。
0表示所在列表的下标。
这里解释一下,设置GPIO的字段其实是一个数组,这里的 "BT,reset_gpio"数组中只设置了一个GPIO
BT,reset_gpio = <&gpio_b 26 GPIO_ACTIVE_HIGH>;
它可以填充多个gpio:

BT,reset_gpio    = <&gpio_b 26 GPIO_ACTIVE_HIGH  //index =0
					&gpio_c 10 GPIO_ACTIVE_HIGH             //index =1
					&gpio_a 11 GPIO_ACTIVE_HIGH>;         //index=2

这种情况就可以通过index 获取对应的gpio 编号了。
flags 是输出参数,对应GPIO_ACTIVE_HIGH

第二种;

struct gpio_desc *bt_reset;
bt_reset = devm_gpiod_get_optional(&pdev->dev, "reset",
			GPIOD_OUT_LOW);

这种方法与第一种区别,比较大。两种方法获取的是同一个gpio
1 但是第二个参数代入名称并不一样,这是因为devm_gpiod_get_optional会对名称补全,及他会搜索reset-gpios;
2没有代入参数index,默认获取的是index 0 ,所以不能处理多个gpio设置的情况
3 第三个参数是输入,及它将把gpio设置为该状态,而不是获取DTS中设置的状态,就是说它忽略了DTS中对引脚状态的配置。
4 返回值是gpio_desc 结构体而不是GPIO编号。这个结构体是与对应的GPIO绑定的,及第一种方法获取的GPIO编号与这里获取的gpio_desc指向的是同一个GPIO脚

gpio_desc 结构体是用另外一组api设置:

int gpiod_request(struct gpio_desc *desc, const char *label)
void gpiod_set_value(struct gpio_desc *desc, int value)
int gpiod_get_value(const struct gpio_desc *desc)
int gpiod_direction_input(struct gpio_desc *desc)
int gpiod_direction_output(struct gpio_desc *desc, int value)

它与第二节中的方法不同就在函数名多了一个‘d’,输入的GPIO编号改为了gpio_desc结构体

关于GPIO的API还有很多,这里记录了一些常用的。

猜你喜欢

转载自blog.csdn.net/crazycat_dzw/article/details/109286002