当前位置: 首页 > news >正文

六、GPIO中断控制器(1)—— pcf8575

前言

完成一个GPIO扩展芯片的GPIO驱动后,可以使用上一节实现的驱动进行简单的GPIO控制,但是对于中断输入功能,该驱动是不支持的。因此要实现完整的GPIO扩展芯片驱动,还需要使用内核中的中断子系统来实现中断功能。pcf8575是支持中断功能的,有一个中断引脚,在pcf8575的16个GPIO引脚发生电平变化时,会在终端引脚上发送脉冲信号。我们可以通过SoC的一个中断引脚来实现中断输入检测,只要检测到中断信号后,就能通过I2C通信读取GPIO扩展芯片的GPIO电平状态,找到真正触发中断的GPIO引脚。这样就实现了一个SoC的GPIO中断扩展成16个外部GPIO的中断。

外部GPIO控制器的中断扩展由于会使用到I2C通信,这会导致终端映射时,产生额外的时间消耗,对于变化频率较高的场景下,中断会漏。也就是说当GPIO电平变化速度超过一次I2C读取GPIO读取中断状态的时间,那么就会导致有中断产生,但是漏掉中断的情况,因此不适用于高速信号。

纠正一下我对pcf8575的理解。开始对于pcf8575的读写模式的设置,我认为它是只能将所有的引脚同时设置为输入或者输出模式,这个观点是错误的,对于datasheet,其中也没有提到这个说法,理解有误。后续对于代码的实践证明,每个引脚是可以单独进行输入输出控制的。对于它的读模式,需要对需要设置为读模式的引脚进行写1操作,对于写模式,对需要写电平的引脚写入相应的数据。这里很难理解,我认为应该是设置为输入模式,需要将引脚设置为1,对于输出高电平的状态,也是将引脚电平设置为1。对于输出低电平的状态,将引脚设置为0。也就是输入模式和输入高电平模式,对于GPIO引脚的设置是一致的。可以理解为,都是设置输出模式,但是在使用输入模式状态下,输出高电平,在输入模式下,如果外部是高电平,那么这个引脚还是高电平,如果外部是低电平,那么引脚会被拉低,输入模式可以正确判断内部引脚的电平变化。如果在使用输出模式状态下,输出高电平就是拉高,输出低电平就是拉低,逻辑也是正确的。和常规的GPIO理解起来不太一样。

中断控制器注册流程

irq_domain

这个结构是必须要使用的。可以理解为一个中断控制器的对象。在我们得GPIO控制器驱动的用户结构种定义它。

struct pcf8575_dev {
    struct i2c_client *client;
    struct gpio_chip chip;

    u16 value;
    u16 in_mask;
    u16 r_val;

    struct irq_domain *irq_domain;
};

对于 irq_chip ,简单功能的中断控制器可以不使用这个。这里就先不适用它,完成一个最简单的中断控制器驱动。后面完善完整中断控制器驱动时,再来补全它。

irq_domain_add_linear

选择添加的映射模型,一般都是使用线性映射。使用这个API设置 irq_domain 的映射模式就行了。在调用这个API前,先定义我们 irq_domain 要用到的操作。

static struct irq_domain_ops pcf8575_irq_domain_ops = {
    .map = pcf8575_irq_domain_map,
    .xlate = irq_domain_xlate_twocell,
};

.map 映射方法,这个是必须要使用的。
.map 需要自己定义,这里要设置 irq_chip ,定义这个中断控制器支持的方法,由于是最简单的中断控制器,我们可以不使用这些方式,设置内核中预定义的一个空对象 dummy_irq_chip 来实现框架的完整性。

static int pcf8575_irq_domain_map(struct irq_domain *domain, unsigned int virq,
                irq_hw_number_t hw)
{
    irq_set_chip_and_handler(virq, &dummy_irq_chip, handle_edge_irq);
    return 0;
}

.xlate 这个使用识别设备树中中断配置的数组是使用两个单元还是一个单元。这里使用内种预定义的就可以了。可以选择只支持一个cell,只支持两个cell,或者支持1个或者2个cell。

int irq_domain_xlate_onecell(struct irq_domain *d, struct device_node *ctrlr,
			const u32 *intspec, unsigned int intsize,
			irq_hw_number_t *out_hwirq, unsigned int *out_type);
int irq_domain_xlate_twocell(struct irq_domain *d, struct device_node *ctrlr,
			const u32 *intspec, unsigned int intsize,
			irq_hw_number_t *out_hwirq, unsigned int *out_type);
int irq_domain_xlate_onetwocell(struct irq_domain *d, struct device_node *ctrlr,
			const u32 *intspec, unsigned int intsize,
			irq_hw_number_t *out_hwirq, unsigned int *out_type);

之后就可以添加线性映射了:

	pcf8575->irq_domain = irq_domain_add_linear(dev->of_node,
        pcf8575->chip.ngpio, &pcf8575_irq_domain_ops, NULL);
    if (IS_ERR(pcf8575->irq_domain)) {
        return PTR_ERR(pcf8575->irq_domain);
    }

父中断处理

对于GPIO扩展芯片的中断引脚,我们需要注册中断处理函数,这里使用中断线程化方式。pcf8575产生中断时,是由高电平变化为低电平,产生向下的脉冲。因此这里使用下降沿的方式进行检测。

	ret = devm_request_threaded_irq(dev, pcf8575->client->irq, NULL, pcf8575_irq,
        IRQF_ONESHOT | IRQF_TRIGGER_FALLING, dev_name(dev), pcf8575);
    if (ret) {
        return ret;
    }
static irqreturn_t pcf8575_irq(int irq, void *data)
{
    struct pcf8575_dev *pcf8575 = data;
    unsigned int child_irq, i;
    u16 value;

    pcf8575_i2c_read(pcf8575->client, &value);
    // pr_info("pcf8575 irq handle, value:0x%02x, r_val:0x%02x\n", value, pcf8575->r_val);

    for (i = 0; i < pcf8575->chip.ngpio; i++) {
        if (((pcf8575->in_mask >> i) & 0x01) == 0x01) {
            if (((value >> i) & 0x01) != ((pcf8575->r_val >> i) & 0x01)) {
                // pr_info("pcf8575 irq pin: %d", i);
                child_irq = irq_find_mapping(pcf8575->irq_domain, i);
                handle_nested_irq(child_irq);
            }
        }
    }

    /* If only we read from ports, we must cache last input value after using pcf8575_i2c_read */
    pcf8575->r_val = value & pcf8575->in_mask;
    return IRQ_HANDLED;
}

这里要根据GPIO号找到对应的中断号,然后调用 handle_nested_irq 对这个中断进行处理。它会调用到我们对于某个GPIO引脚设置的中断号的注册的处理函数,实现中断继承扩展。

这样,GPIO扩展芯片的16个引脚,只要由引脚产生的中断,都会被映射到父中断上,我们只需要在父中断的中断处理函数中去识别到底是哪一个引脚产生的中断即可。在完成所有注册操作后,这16个引脚也会在内种注册生成中断线,就和一般的中断一样使用了,和SoC片上GPIO控制器一样。

gpio_to_irq

必须要注册 chip.to_irq 。为我们返回对应的GPIO号注册的中断号。

pcf8575->chip.to_irq = pcf8575_gpio_to_irq;
static int pcf8575_gpio_to_irq(struct gpio_chip *chip, unsigned offset)
{
    struct pcf8575_dev *pcf8575 = container_of(chip, struct pcf8575_dev, chip);

	return irq_find_mapping(pcf8575->irq_domain, offset);
}

创建中断映射

为每个GPIO创建对应的引脚号对应的中断号。

	for (i = 0; i < pcf8575->chip.ngpio; i++) {
        irq = irq_create_mapping(pcf8575->irq_domain, i);
    }

上述代码,添加到GPIO控制器驱动中,以上就完成了一个最简单的GPIO中断控制器的驱动。

设备树

&i2c4 {
    status = "okay";
    expander0: pcf8575@20 {
        status = "okay";
        compatible = "nxp,pcf8575";
        reg = <0x20>;
        interrupt-controller;
        #interrupt-cells = <2>;
        gpio-controller;
        #gpio-cells = <2>;
        interrupt-parent = <&gpio1>;
        interrupts = <10 IRQ_TYPE_EDGE_FALLING>;
    };
};

这个pcf8575是一个中断控制器,也是GPIO控制器,因此需要在设备树中进行申明。然后还需要在这里为其声明一个中断,这个中断是作为GPIO中断控制器的父中断使用的。我们在使用GPIO作为中断时,也可以使用这种方式。效果和使用下面的设备树申明时一样的。

button-gpio = <&gpio1 10 IRQ_TYPE_EDGE_FALLING>;

GPIO中断控制器使用

当完成中断控制器的注册,我们就可以像使用SoC中的GPIO控制器那样,使用外部GPIO扩展芯片生成的GPIO控制器,并使用中断控制器的功能。

设备树

    owner_timer: owner_timer {
        status = "okay";
        compatible = "owner,timer";

        intb-gpio = <&expander0 7 IRQ_TYPE_EDGE_FALLING>;
    };

这里在设备树中申明要使用GPIO扩展芯片的第8个引脚,作为一个中断输入方式。

驱动代码

使用的代码就和正常的GPIO引脚一样即可。

	tim->intb = devm_gpiod_get(&pdev->dev, "intb", GPIOD_IN);
    if (IS_ERR(tim->intb)) {
        ret = PTR_ERR(tim->intb);
        goto err3;
    }

    tim->irq = gpiod_to_irq(tim->intb);
    ret = devm_request_threaded_irq(&pdev->dev, tim->irq, NULL, intb_irq_handler,
        IRQF_ONESHOT | IRQF_TRIGGER_FALLING, "intb", tim);
    if (ret) {
        goto err3;
    }
static irqreturn_t intb_irq_handler(int irq, void *data)
{
    pr_err("intb irq handler\n");
    return IRQ_HANDLED;
}

验证

我们将SoC的一个引脚接到GPIO扩展芯片的第8个引脚上,然后控制电平变化,看能否触发我们上述申请的第8个引脚的中断处理函数。

echo 41 > export 
cd gpio41
echo out > direction
echo 1 > value
echo 0 > value
[root@rk3399:/sys/devices/platform/pinctrl/gpio/gpio41]# echo 1 > value
[root@rk3399:/sys/devices/platform/pinctrl/gpio/gpio41]# [135967.071085] pcf8575 irq handle, value:0xffff, r_val:0x00
[135967.081406] intb irq handler

[root@rk3399:/sys/devices/platform/pinctrl/gpio/gpio41]# echo 0 > value
[root@rk3399:/sys/devices/platform/pinctrl/gpio/gpio41]# [135973.446908] pcf8575 irq handle, value:0xff7f, r_val:0x80
[135973.457265] intb irq handler

[root@rk3399:/sys/devices/platform/pinctrl/gpio/gpio41]#

可以看到,第8个引脚的中断处理函数能正常触发。说明中断映射是成功的。

问题

上述驱动还有问题。在使用GPIO时,就算是申请中断线程化,使用高电平或者低电平触发,最终还是只会边缘触发。这是由于在父中断的中断处理函数中,我们代码实现的就是边缘触发,这是因为我们不知道,这个GPIO在申请时,到底设置的是电平触发,还是边沿触发。并且,如果要关闭某个GPIO引脚的中断,也是没有办法。父中断处理函数中,只要有中断产生就会调用其注册的中断处理函数。为了解决上述问题,那么需要我们使用 irq_chip 实现其相应的回调了,这样才能做到完整功能的支持。

代码

整个驱动代码只是在 pcf8575 的GPIO控制器驱动上,添加了少量代码实现了中断控制器的功能。

#include "linux/device.h"
#include "linux/gpio/driver.h"
#include "linux/interrupt.h"
#include "linux/irq.h"
#include <linux/kernel.h>
#include <linux/gpio.h>
#include <linux/i2c.h>
#include <linux/of_irq.h>

#define GPIO_NUM 16

struct pcf8575_dev {
    struct i2c_client *client;
    struct gpio_chip chip;

    u16 value;
    u16 in_mask;
    u16 r_val;

    struct irq_domain *irq_domain;
};

static int pcf8575_i2c_read(struct i2c_client *client, u16 *value)
{
    u8 data[2] = {0};
    struct i2c_msg msg[] = {
        {
            .flags = I2C_M_RD | I2C_M_STOP,
            .addr = client->addr,
            .buf = data,
            .len = ARRAY_SIZE(data),
        },
    };

    if (i2c_transfer(client->adapter, msg, ARRAY_SIZE(msg)) != ARRAY_SIZE(msg)) {
        dev_err(&client->dev, "i2c read error, chip=0x%02x\n", client->addr);
        return -EREMOTEIO;
    }

    *value = data[0] | (data[1] << 8);
    return 0;
}

static int pcf8575_i2c_write(struct i2c_client *client, u16 value)
{
    u8 data[2] = {(value & 0xFF), ((value >> 8) & 0xFF)};
    struct i2c_msg msg[] = {
        {
            .flags = I2C_M_STOP,
            .addr = client->addr,
            .buf = data,
            .len = ARRAY_SIZE(data),
        },
    };

    if (i2c_transfer(client->adapter, msg, ARRAY_SIZE(msg)) != ARRAY_SIZE(msg)) {
        dev_err(&client->dev, "i2c write error, chip=0x%02x\n", client->addr);
        return -EREMOTEIO;
    }

    return 0;
}

static int pcf8575_get_value(struct gpio_chip *chip, unsigned offset)
{
    u16 value = 0;
    struct pcf8575_dev *pcf8575 = container_of(chip, struct pcf8575_dev, chip);

    if (offset >= GPIO_NUM) {
        dev_err(&pcf8575->client->dev, "offset over %d\n", GPIO_NUM);
        return 0;
    }
    
    pcf8575_i2c_read(pcf8575->client, &value);
    pcf8575->r_val = value & pcf8575->in_mask;

    return (value & (1 << offset));
}

static void pcf8575_set_value(struct gpio_chip *chip, unsigned offset, int value)
{
    struct pcf8575_dev *pcf8575 = container_of(chip, struct pcf8575_dev, chip);

    if (offset >= GPIO_NUM) {
        dev_err(&pcf8575->client->dev, "offset over %d\n", GPIO_NUM);
        return ;
    }

    if (value) {
        pcf8575->value |= (1 << offset);
    } else {
        pcf8575->value &= (~(1 << offset));
    }

    pcf8575_i2c_write(pcf8575->client, pcf8575->value);
}

static int pcf8575_direction_input(struct gpio_chip *chip, unsigned offset)
{
    struct pcf8575_dev *pcf8575 = container_of(chip, struct pcf8575_dev, chip);
    pcf8575->value |= (1 << offset);
    pcf8575->in_mask |= (1 << offset);

    pcf8575_i2c_write(pcf8575->client, pcf8575->value);

    return 0;
}

static int pcf8575_direction_output(struct gpio_chip *chip, unsigned offset,
                                    int value)
{
    struct pcf8575_dev *pcf8575 = container_of(chip, struct pcf8575_dev, chip);
    pcf8575->in_mask &= (~(1 << offset));
    pcf8575_set_value(chip, offset, value);

    return 0;
}

static int pcf8575_irq_domain_map(struct irq_domain *domain, unsigned int virq,
                irq_hw_number_t hw)
{
    irq_set_chip_and_handler(virq, &dummy_irq_chip, handle_edge_irq);
    return 0;
}

static struct irq_domain_ops pcf8575_irq_domain_ops = {
    .map = pcf8575_irq_domain_map,
    .xlate = irq_domain_xlate_twocell,
};

static irqreturn_t pcf8575_irq(int irq, void *data)
{
    struct pcf8575_dev *pcf8575 = data;
    unsigned int child_irq, i;
    u16 value;

    pcf8575_i2c_read(pcf8575->client, &value);
    // pr_info("pcf8575 irq handle, value:0x%02x, r_val:0x%02x\n", value, pcf8575->r_val);

    for (i = 0; i < pcf8575->chip.ngpio; i++) {
        if (((pcf8575->in_mask >> i) & 0x01) == 0x01) {
            if (((value >> i) & 0x01) != ((pcf8575->r_val >> i) & 0x01)) {
                // pr_info("pcf8575 irq pin: %d", i);
                child_irq = irq_find_mapping(pcf8575->irq_domain, i);
                handle_nested_irq(child_irq);
            }
        }
    }

    /* If only we read from ports, we must cache last input value after using pcf8575_i2c_read */
    pcf8575->r_val = value & pcf8575->in_mask;
    return IRQ_HANDLED;
}

static int pcf8575_gpio_to_irq(struct gpio_chip *chip, unsigned offset)
{
    struct pcf8575_dev *pcf8575 = container_of(chip, struct pcf8575_dev, chip);

	return irq_find_mapping(pcf8575->irq_domain, offset);
}

static int pcf8575_irq_setup(struct pcf8575_dev *pcf8575)
{
    struct device *dev = &pcf8575->client->dev;
    int ret = 0;
    int i = 0;
    u32 irq;

    pcf8575->in_mask = 0;
    pcf8575->r_val = 0;

    // pcf8575->client->irq = irq_of_parse_and_map(pcf8575->client->dev.of_node, 0);

    pcf8575->irq_domain = irq_domain_add_linear(dev->of_node,
        pcf8575->chip.ngpio, &pcf8575_irq_domain_ops, NULL);
    if (IS_ERR(pcf8575->irq_domain)) {
        return PTR_ERR(pcf8575->irq_domain);
    }

    ret = devm_request_threaded_irq(dev, pcf8575->client->irq, NULL, pcf8575_irq,
        IRQF_ONESHOT | IRQF_TRIGGER_FALLING, dev_name(dev), pcf8575);
    if (ret) {
        return ret;
    }

    pcf8575->chip.to_irq = pcf8575_gpio_to_irq;

    for (i = 0; i < pcf8575->chip.ngpio; i++) {
        irq = irq_create_mapping(pcf8575->irq_domain, i);
    }
    return 0;
}

static int pcf8575_probe(struct i2c_client *client,
                        const struct i2c_device_id *id)
{
    struct pcf8575_dev *pcf8575;
    int ret;

    if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) {
        dev_err(&client->dev, "i2c_check_functionality not support I2C_FUNC_I2C\n");
        return -EIO;
    }

    pcf8575 = devm_kzalloc(&client->dev, sizeof(struct pcf8575_dev), GFP_KERNEL);
    if (IS_ERR(pcf8575)) {
        dev_err(&client->dev, "kzalloc error: 0x%lx\n", PTR_ERR(pcf8575));
        return PTR_ERR(pcf8575);
    }

    pcf8575->chip.label = client->name;
    pcf8575->chip.base = -1;
    pcf8575->chip.parent = &client->dev;
    pcf8575->chip.owner = THIS_MODULE,
    pcf8575->chip.ngpio = GPIO_NUM;
    pcf8575->chip.can_sleep = 1;
    pcf8575->chip.get = pcf8575_get_value,
    pcf8575->chip.set = pcf8575_set_value,
    pcf8575->chip.direction_input = pcf8575_direction_input,
    pcf8575->chip.direction_output = pcf8575_direction_output,

    /* default value is 0xFFFF after power on */
    pcf8575->value = 0xFFFF;

    i2c_set_clientdata(client, pcf8575);
    pcf8575->client = client;

    ret = devm_gpiochip_add_data(&client->dev, &pcf8575->chip, NULL);
    if (ret) {
        return ret;
    }

    ret = pcf8575_irq_setup(pcf8575);
    if (ret) {
        return ret;
    }

    dev_info(&client->dev, "pcf8575 probe success\n");
    return 0;
}

static int pcf8575_remove(struct i2c_client *client)
{
    return 0;
}

static struct i2c_device_id pcf8575_id_table[] = {
    {"pcf8575", 0},
    {},
};
MODULE_DEVICE_TABLE(i2c, pcf8575_id_table);

static const struct of_device_id pcf8575_of_match[] = {
    {.compatible = "nxp,pcf8575"},
    { },
};
MODULE_DEVICE_TABLE(of, pcf8575_of_match);

static struct i2c_driver pcf8575_driver = {
    .probe = pcf8575_probe,
    .remove = pcf8575_remove,
    .driver = {
        .owner = THIS_MODULE,
        .name = "pcf8575_drv",
        .of_match_table = of_match_ptr(pcf8575_of_match),
    },
    .id_table = pcf8575_id_table,
};

module_i2c_driver(pcf8575_driver);

MODULE_AUTHOR("duapple <duapple2@gmail.com>");
MODULE_LICENSE("GPL");

相关文章:

  • CSRF跨站请求伪造(Cross - Site Request Forgery)
  • 蓝桥杯 劲舞团
  • 简要分析IPPROTO_TCP参数
  • 解决Selenium滑动页面到指定元素,点击失效的问题
  • Swagger2 使用教程
  • 为什么 Redis 选择单线程模型?
  • 【3-22 list 详解STL C++ 】
  • RAG知识库的数据方案:图数据库、向量数据库和知识图谱怎么选?
  • React Native进阶(六十):webview实现屏蔽所嵌套web页面异常弹窗
  • 数据通信与计算机网络——网络模型
  • AI 代理错误的复合效应
  • 如何在MySQL中创建定时任务?
  • Web3网络生态中数据保护合规性分析
  • Redis主从复制实验
  • STM32定时器-01定时器概述
  • vue如何获取 sessionStorage的值,获取token
  • 全文 - MLIR: A Compiler Infrastructure for the End of Moore’s Law
  • 【并发编程】聊聊forkJoin的原理和最佳实践
  • 融合与创新:人工智能、数字化转型及计算机科学在高中教育管理中的应用探索
  • 六西格玛遇上Python:统计学的高效实践场
  • 工行回应两售出金条发现疑似杂质:情况不属实,疑似杂质应为金条售出后的外部附着物
  • 2025年度上海市住房城乡建设管理委工程系列中级职称评审工作启动
  • 101条关于减重的知识,其中一定有你不知道的
  • 远离军事前线的另一面暗斗:除了“断水”,印度还试图牵制对巴国际援助
  • 盖茨:20年内将捐出几乎全部财富,盖茨基金会2045年关闭
  • 黄玮接替周继红出任国家体育总局游泳运动管理中心主任