GPIO 서브시스템 — GPIO / pinctrl

GPIO(General-Purpose Input/Output) 서브시스템의 아키텍처와 gpiod descriptor 기반 안전한 제어 패턴, gpio_chip 드라이버 구현, libgpiod 유저스페이스 접근, GPIO Expander, GPIO IRQ 컨트롤러(irqchip), pinctrl 핀 멀티플렉싱, Device Tree 바인딩까지 GPIO 관련 드라이버 개발에 필요한 핵심을 다룹니다.

전제 조건: 디바이스 드라이버인터럽트(Interrupt) 문서를 먼저 읽으세요. I2C는 I2C 서브시스템, SPI는 SPI 서브시스템 문서를 참조하세요.
일상 비유: GPIO는 만능 스위치 패널과 비슷합니다. 각 스위치(핀)를 입력 또는 출력으로 설정하고, 버튼 누름 감지, LED 점등, 리셋 라인 제어 등 다양한 용도로 활용합니다. pinctrl은 각 스위치의 기능을 결정하는 배선 설정표에 해당합니다.

핵심 요약

  • gpiod API 사용 — Legacy 정수 기반 API 대신 descriptor 기반 gpiod API를 사용합니다.
  • active-low 자동 처리 — gpiod_set_value()가 Device Tree의 GPIO_ACTIVE_LOW 플래그를 자동 반영합니다.
  • gpio_chip 구현 — GPIO 컨트롤러 드라이버는 gpio_chip 구조체를 등록합니다.
  • IRQ 통합 — GPIO 핀을 인터럽트 소스로 사용할 때 gpio_irq_chip을 활용합니다.
  • pinctrl 연동 — 핀 멀티플렉싱과 전기적 특성은 pinctrl 서브시스템과 협력합니다.

단계별 이해

  1. GPIO 서브시스템 아키텍처 파악
    gpiolib, gpio_chip, gpiod 계층 구조를 이해합니다.
  2. gpiod API로 GPIO 제어
    descriptor 기반 획득, 값 읽기/쓰기, IRQ 변환을 익힙니다.
  3. gpio_chip 드라이버 작성
    GPIO 컨트롤러를 커널에 등록하는 방법을 학습합니다.
  4. GPIO IRQ와 pinctrl 통합
    인터럽트 지원과 핀 설정을 마무리합니다.
GPIO 서브시스템 gpiolib / gpiod / gpio_chip Pinctrl 서브시스템 pinmux / pinconf IRQ 서브시스템 gpio_irq_chip

GPIO 개요

GPIO (General-Purpose Input/Output)는 소프트웨어로 제어 가능한 범용 디지털 핀입니다. LED, 버튼, 리셋 라인, 칩 셀렉트, 인터럽트 입력 등 다양한 용도로 사용됩니다.

Linux GPIO 서브시스템은 drivers/gpio/에 구현되며, 크게 두 가지 API가 있습니다:

API헤더상태특징
Legacy (integer-based)<linux/gpio.h>Deprecatedgpio_request(), gpio_direction_input()
Descriptor-based (gpiod)<linux/gpio/consumer.h>현재 표준gpiod_get(), gpiod_set_value()
Legacy API 사용 금지: 새 코드에서 gpio_request(), gpio_free(), gpio_get_value() 등 정수 기반 legacy API를 사용하지 마세요. 커널 메인라인에서는 legacy GPIO API를 사용하는 새 드라이버를 받아들이지 않습니다.

GPIO 서브시스템 아키텍처

Linux GPIO 서브시스템은 하드웨어 GPIO 컨트롤러부터 유저스페이스 접근까지 여러 계층으로 구성됩니다. gpiolib이 핵심 프레임워크 역할을 하며, gpio_chip이 하드웨어 추상화를, gpiod가 소비자(consumer) API를 제공합니다.

Linux GPIO 서브시스템 아키텍처 User Space /dev/gpiochipN (chardev) libgpiod (v2 API) gpioget / gpioset / gpiomon gpio-cdev (chardev 드라이버) sysfs GPIO (deprecated) gpiod Consumer API devm_gpiod_get() / gpiod_set_value() / gpiod_to_irq() 커널 드라이버 (GPIO 소비자) SPI CS, I2C recovery, LED, Key, Reset, Regulator Enable... gpiolib Core (drivers/gpio/gpiolib.c) gpio_desc 관리, 라인 요청/해제, 값 읽기/쓰기, IRQ 매핑, 이벤트 관리 gpio_chip Provider Interface (devm_gpiochip_add_data) SoC GPIO Controller gpio-mxc, gpio-tegra, gpio-rcar, gpio-stm32 I2C/SPI GPIO Expander gpio-pca953x, gpio-mcp23s08, gpio-pcf857x ACPI GPIO gpiolib-acpi.c, ACPI _DSD GPIO 매핑 GPIO Aggregator gpio-aggregator, gpio-sim (테스트용) pinctrl 서브시스템과 연동: 핀 멀티플렉싱, 풀업/풀다운, 드라이브 강도 설정

GPIO 하드웨어 내부 구조

물리적 GPIO 핀은 여러 전기적 설정을 지원하며, 드라이버에서 이를 올바르게 구성하는 것이 중요합니다:

설정설명커널 API / DT 속성용도
Push-PullHIGH/LOW 모두 능동적으로 구동기본 출력 모드LED 제어, 리셋 라인
Open-DrainLOW만 능동 구동, HIGH는 풀업 의존GPIO_OPEN_DRAIN / drive-open-drainI2C SDA/SCL, 인터럽트 라인
Open-SourceHIGH만 능동 구동, LOW는 풀다운 의존GPIO_OPEN_SOURCE / drive-open-source특수 전원 제어
Pull-Up내장 풀업 저항 활성화GPIO_PULL_UP / bias-pull-up버튼 입력 (active-low)
Pull-Down내장 풀다운 저항 활성화GPIO_PULL_DOWN / bias-pull-down기본 LOW 유지 필요 시
Schmitt Trigger히스테리시스 입력 (노이즈 내성)input-schmitt-enable느린 신호 에지, 노이즈 환경
Debounce글리치 필터링 (HW/SW)input-debounce = <usec>기계식 버튼/스위치
핀 설정과 pinctrl: GPIO 하드웨어 설정(풀업/풀다운, 드라이브 강도, 슬루율 등)은 pinctrl 서브시스템과 밀접하게 연관됩니다. 대부분의 SoC에서 GPIO 핀은 pinctrl 핀과 1:1 매핑되며, pinctrl_gpio_set_config()를 통해 gpiolib에서 pinctrl로 설정이 전달됩니다.

gpiod API (Descriptor-based)

현대 Linux 커널의 표준 GPIO 인터페이스인 gpiod API를 사용합니다:

GPIO 획득과 해제

#include <linux/gpio/consumer.h>

/* Device Tree에서 "reset-gpios" 속성을 참조하여 GPIO 획득 */
struct gpio_desc *reset_gpio;

reset_gpio = devm_gpiod_get(&pdev->dev, "reset", GPIOD_OUT_HIGH);
if (IS_ERR(reset_gpio))
    return PTR_ERR(reset_gpio);

/* 선택적(optional) GPIO: 없어도 에러 아님 */
struct gpio_desc *led_gpio;
led_gpio = devm_gpiod_get_optional(&pdev->dev, "led", GPIOD_OUT_LOW);

/* 인덱스로 여러 GPIO 획득 */
struct gpio_desc *cs_gpio;
cs_gpio = devm_gpiod_get_index(&pdev->dev, "cs", 0, GPIOD_OUT_HIGH);

GPIO 동작

/* 출력 값 설정 (active-low 자동 처리) */
gpiod_set_value(reset_gpio, 1);  /* active (논리적 1) */
gpiod_set_value(reset_gpio, 0);  /* inactive (논리적 0) */

/* sleepable context에서 사용 (I2C/SPI GPIO expander 등) */
gpiod_set_value_cansleep(reset_gpio, 1);

/* 입력 값 읽기 */
int val = gpiod_get_value(button_gpio);

/* 방향 변경 */
gpiod_direction_input(gpio);
gpiod_direction_output(gpio, 1);

/* GPIO → IRQ 번호 변환 */
int irq = gpiod_to_irq(button_gpio);
if (irq < 0)
    return irq;

ret = devm_request_threaded_irq(&pdev->dev, irq, NULL,
        my_irq_handler, IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
        "my-button", data);
gpiod_set_value vs gpiod_set_raw_value: gpiod_set_value()는 Device Tree의 GPIO_ACTIVE_LOW 플래그를 자동 반영합니다. gpiod_set_raw_value()는 물리적 라인 레벨을 직접 제어합니다. 일반적으로 gpiod_set_value()를 사용하세요.

GPIO 인터럽트 흐름

GPIO를 인터럽트 소스로 사용하는 경우, GPIO 서브시스템과 IRQ 서브시스템이 협력하여 핀 상태 변화를 커널 인터럽트로 변환합니다. 아래 다이어그램은 GPIO 인터럽트의 전체 처리 흐름을 보여줍니다.

GPIO 인터럽트 처리 흐름 GPIO Pin 에지/레벨 변화 gpio_chip irqchip 콜백 irq_mask/unmask/set_type irq_domain HW IRQ → Linux virq 매핑 Generic IRQ irq_desc / action handle_edge_irq() Threaded IRQ request_threaded_irq IRQF_ONESHOT Driver handler() gpiod_to_irq(gpio_desc) → irq_find_mapping(domain, hwirq) → Linux virq 반환 드라이버의 GPIO IRQ 설정 절차 1. gpio = devm_gpiod_get(dev, "alert", GPIOD_IN); 2. irq = gpiod_to_irq(gpio); 3. devm_request_threaded_irq(dev, irq, NULL, handler, IRQF_TRIGGER_FALLING | IRQF_ONESHOT, ...);

GPIO Debounce (글리치 필터링)

기계식 버튼이나 스위치는 접점 바운싱으로 인해 한 번의 누름에 여러 번의 에지 변화가 발생합니다. Debounce는 이러한 글리치를 필터링하여 깨끗한 신호를 제공합니다.

방식구현지연장점단점
HW DebounceSoC GPIO 컨트롤러 내장 필터HW에서 설정 (수십 us ~ 수 ms)CPU 부하 없음, 정밀한 타이밍모든 SoC 지원 아님
SW Debouncegpiolib hrtimer 기반gpiod_set_debounce()모든 GPIO에 적용 가능CPU 오버헤드, hrtimer 정밀도 의존
/* HW debounce 설정 (지원하는 컨트롤러만) */
ret = gpiod_set_debounce(button_gpio, 50000);  /* 50ms */
if (ret == -ENOTSUPP)
    dev_warn(dev, "HW debounce not supported, using SW\\n");

/* gpio_chip에서 HW debounce 구현 */
static int my_gpio_set_config(struct gpio_chip *gc,
                              unsigned int offset,
                              unsigned long config)
{
    if (pinconf_to_config_param(config) == PIN_CONFIG_INPUT_DEBOUNCE) {
        u32 debounce_us = pinconf_to_config_argument(config);
        /* HW 디바운스 레지스터 설정 */
        writel(debounce_us / 31, priv->base + DEBOUNCE_REG(offset));
        return 0;
    }
    return -ENOTSUPP;
}

GPIO Aggregator

gpio-aggregator는 여러 물리적 GPIO 라인을 하나의 가상 GPIO 컨트롤러로 묶어 유저스페이스에 노출하는 기능입니다. 보안 제한이나 권한 분리가 필요한 환경에서 특정 GPIO 라인만 선택적으로 컨테이너(Container)나 VM에 전달할 때 유용합니다.

/* GPIO Aggregator 사용 (sysfs 인터페이스) */
$ echo "gpiochip0 3,5,7" > /sys/bus/platform/drivers/gpio-aggregator/new_device
/* 새로운 /dev/gpiochipN 생성 (3개 라인: 0=pin3, 1=pin5, 2=pin7) */

$ echo "gpiochip0 3,5,7" > /sys/bus/platform/drivers/gpio-aggregator/delete_device
/* 가상 GPIO 컨트롤러 제거 */
gpio-sim: 테스트 환경에서는 gpio-sim 모듈을 사용하여 실제 하드웨어 없이 가상 GPIO 컨트롤러를 생성할 수 있습니다. configfs를 통해 라인 수, 라벨, 초기 값 등을 설정합니다. CI/CD 파이프라인(Pipeline)이나 유닛 테스트에 유용합니다.

GPIO Device Tree 바인딩

my_device: my-device@0 {
    compatible = "vendor,my-device";
    /* 프로퍼티 이름: <con-id>-gpios */
    reset-gpios = <&gpio1 7 GPIO_ACTIVE_LOW>;
    led-gpios = <&gpio2 3 GPIO_ACTIVE_HIGH>;
    cs-gpios = <&gpio1 4 GPIO_ACTIVE_LOW>,
               <&gpio1 5 GPIO_ACTIVE_LOW>;
};

gpio_chip 구현

GPIO 컨트롤러 드라이버를 작성하려면 gpio_chip 구조체를 구현하고 등록합니다:

#include <linux/gpio/driver.h>

struct my_gpio {
    struct gpio_chip gc;
    void __iomem *base;
    struct mutex lock;
};

static int my_gpio_get(struct gpio_chip *gc, unsigned int offset)
{
    struct my_gpio *priv = gpiochip_get_data(gc);
    u32 reg;

    reg = readl(priv->base + 0x10);  /* Data Input Register */
    return !!(reg & BIT(offset));
}

static void my_gpio_set(struct gpio_chip *gc,
                         unsigned int offset, int value)
{
    struct my_gpio *priv = gpiochip_get_data(gc);
    u32 reg;

    mutex_lock(&priv->lock);
    reg = readl(priv->base + 0x14);  /* Data Output Register */
    if (value)
        reg |= BIT(offset);
    else
        reg &= ~BIT(offset);
    writel(reg, priv->base + 0x14);
    mutex_unlock(&priv->lock);
}

static int my_gpio_direction_input(struct gpio_chip *gc,
                                    unsigned int offset)
{
    struct my_gpio *priv = gpiochip_get_data(gc);
    u32 reg;

    mutex_lock(&priv->lock);
    reg = readl(priv->base + 0x04);  /* Direction Register */
    reg &= ~BIT(offset);            /* 0 = input */
    writel(reg, priv->base + 0x04);
    mutex_unlock(&priv->lock);
    return 0;
}

static int my_gpio_direction_output(struct gpio_chip *gc,
                                     unsigned int offset, int value)
{
    my_gpio_set(gc, offset, value);

    struct my_gpio *priv = gpiochip_get_data(gc);
    u32 reg;

    mutex_lock(&priv->lock);
    reg = readl(priv->base + 0x04);
    reg |= BIT(offset);              /* 1 = output */
    writel(reg, priv->base + 0x04);
    mutex_unlock(&priv->lock);
    return 0;
}

static int my_gpio_probe(struct platform_device *pdev)
{
    struct my_gpio *priv;

    priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    priv->base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(priv->base))
        return PTR_ERR(priv->base);

    mutex_init(&priv->lock);

    priv->gc.label            = "my-gpio";
    priv->gc.parent           = &pdev->dev;
    priv->gc.owner            = THIS_MODULE;
    priv->gc.base             = -1;  /* 동적 번호 할당 */
    priv->gc.ngpio            = 32;
    priv->gc.get              = my_gpio_get;
    priv->gc.set              = my_gpio_set;
    priv->gc.direction_input  = my_gpio_direction_input;
    priv->gc.direction_output = my_gpio_direction_output;

    return devm_gpiochip_add_data(&pdev->dev, &priv->gc, priv);
}

libgpiod 유저스페이스

libgpiod는 Linux GPIO character device (/dev/gpiochipN)를 통한 유저스페이스 GPIO 접근 라이브러리입니다. 기존의 /sys/class/gpio/ sysfs 인터페이스를 대체합니다.

sysfs GPIO 폐기: /sys/class/gpio/export 인터페이스는 deprecated 상태입니다. 새 프로젝트에서는 chardev 기반(/dev/gpiochipN) 접근을 권장하며, 가능한 경우 libgpiod(v2 이상)를 사용하세요.

libgpiod 명령행 도구

도구용도예시
gpiodetect시스템의 GPIO 칩 목록gpiodetect
gpioinfoGPIO 라인 상세 정보gpioinfo gpiochip0
gpiogetGPIO 입력 값 읽기gpioget gpiochip0 7
gpiosetGPIO 출력 값 설정gpioset gpiochip0 7=1
gpiomonGPIO 이벤트 모니터링gpiomon gpiochip0 7

libgpiod C API (v2)

#include <gpiod.h>
#include <stdio.h>
#include <unistd.h>

int main(void)
{
    struct gpiod_chip *chip;
    struct gpiod_line_settings *settings;
    struct gpiod_line_config *line_cfg;
    struct gpiod_request_config *req_cfg;
    struct gpiod_line_request *request;
    unsigned int offsets[] = { 7 };
    enum gpiod_line_value value;

    chip = gpiod_chip_open("/dev/gpiochip0");
    settings = gpiod_line_settings_new();
    gpiod_line_settings_set_direction(settings,
                                       GPIOD_LINE_DIRECTION_INPUT);
    gpiod_line_settings_set_bias(settings,
                                  GPIOD_LINE_BIAS_PULL_UP);

    line_cfg = gpiod_line_config_new();
    gpiod_line_config_add_line_settings(line_cfg, offsets, 1, settings);

    req_cfg = gpiod_request_config_new();
    gpiod_request_config_set_consumer(req_cfg, "my-app");

    request = gpiod_chip_request_lines(chip, req_cfg, line_cfg);

    value = gpiod_line_request_get_value(request, 7);
    printf("GPIO 7 = %d\\n", value);

    gpiod_line_request_release(request);
    gpiod_request_config_free(req_cfg);
    gpiod_line_config_free(line_cfg);
    gpiod_line_settings_free(settings);
    gpiod_chip_close(chip);
    return 0;
}

GPIO Expander

GPIO expander는 I2C 또는 SPI를 통해 GPIO 핀 수를 확장하는 디바이스입니다. 커널에서는 일반 GPIO 컨트롤러와 동일한 gpio_chip 인터페이스로 통합됩니다.

디바이스인터페이스GPIO 수인터럽트커널 드라이버
MCP23017I2C16지원gpio-mcp23s08
MCP23S17SPI16지원gpio-mcp23s08
PCA9555I2C16지원gpio-pca953x
PCA9535I2C16지원gpio-pca953x
PCF8574I2C8지원gpio-pcf857x
TCA6424AI2C24지원gpio-pca953x

GPIO Expander Device Tree 예시

&i2c1 {
    gpio_exp: gpio-expander@20 {
        compatible = "nxp,pca9555";
        reg = <0x20>;
        gpio-controller;
        #gpio-cells = <2>;
        interrupt-parent = <&gpio1>;
        interrupts = <12 IRQ_TYPE_EDGE_FALLING>;
        interrupt-controller;
        #interrupt-cells = <2>;
    };
};

/* GPIO expander의 핀을 다른 디바이스에서 참조 */
my_led: led-controller {
    compatible = "gpio-leds";
    led-status {
        gpios = <&gpio_exp 3 GPIO_ACTIVE_HIGH>;
        label = "status";
        linux,default-trigger = "heartbeat";
    };
};
can_sleep 플래그: I2C/SPI 기반 GPIO expander는 버스 전송이 필요하므로 gpio_chip.can_sleep = true로 설정됩니다. 이 경우 인터럽트 컨텍스트에서 gpiod_get_value()를 호출할 수 없으며, 반드시 gpiod_get_value_cansleep()을 사용해야 합니다.

GPIO IRQ 컨트롤러 (irqchip)

GPIO 컨트롤러가 인터럽트를 지원하려면 gpio_chip에 IRQ chip 기능을 통합해야 합니다. Linux 커널은 GPIOLIB_IRQCHIP 인프라를 통해 이 과정을 크게 단순화합니다. gpio_irq_chip 구조체를 gpio_chip에 내장하여 등록하면, gpiolib이 자동으로 irq_domain을 생성하고 관리합니다.

IRQ Domain 유형

유형설명적용 대상
Flat (Linear)GPIO 번호가 직접 HW IRQ 번호로 매핑일반 SoC GPIO 컨트롤러, GPIO expander
HierarchicalGPIO IRQ → 상위 IRQ 컨트롤러(GIC 등)에 계층적 매핑부모 IRQ 컨트롤러가 별도 존재하는 경우
계층적 GPIO IRQ Domain vs Flat IRQ Domain Flat IRQ Domain GPIO 0..31 gpio_chip + irq_chip chained_irq_handler irq_domain (linear) hwirq 0..31 → virq Parent IRQ (GIC) 단일 parent IRQ line chained handler가 부모 IRQ에서 호출되어 개별 GPIO IRQ를 dispatch Hierarchical IRQ Domain GPIO 0 GPIO 1 GPIO 2 ... GPIO irq_domain (child) gpio_irq_chip + irq_domain_ops Parent irq_domain (GIC/INTC) 각 GPIO가 독립된 parent HW IRQ에 매핑 HW IRQ A HW IRQ B HW IRQ C ... 각 GPIO → 독립된 parent HW IRQ

gpio_chip irqchip 구현

현대 커널(v5.10+)에서는 gpio_irq_chipgpio_chip 내에 설정하고 devm_gpiochip_add_data()로 한 번에 등록하는 것이 권장 패턴입니다:

#include <linux/gpio/driver.h>
#include <linux/interrupt.h>

struct my_gpio_irq {
    struct gpio_chip gc;
    void __iomem *base;
    struct mutex lock;
    u32 irq_mask;     /* 소프트웨어 IRQ 마스크 상태 */
    u32 irq_type;     /* 에지/레벨 타입 비트맵 */
};

/* irq_chip 콜백: 인터럽트 마스크/언마스크 */
static void my_gpio_irq_mask(struct irq_data *d)
{
    struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
    struct my_gpio_irq *priv = gpiochip_get_data(gc);
    u32 mask = BIT(irqd_to_hwirq(d));

    priv->irq_mask &= ~mask;
    writel(priv->irq_mask, priv->base + IRQ_MASK_REG);
    gpiochip_disable_irq(gc, irqd_to_hwirq(d));
}

static void my_gpio_irq_unmask(struct irq_data *d)
{
    struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
    struct my_gpio_irq *priv = gpiochip_get_data(gc);
    u32 mask = BIT(irqd_to_hwirq(d));

    gpiochip_enable_irq(gc, irqd_to_hwirq(d));
    priv->irq_mask |= mask;
    writel(priv->irq_mask, priv->base + IRQ_MASK_REG);
}

/* 인터럽트 타입 설정 (에지/레벨) */
static int my_gpio_irq_set_type(struct irq_data *d,
                                 unsigned int type)
{
    struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
    struct my_gpio_irq *priv = gpiochip_get_data(gc);
    u32 bit = BIT(irqd_to_hwirq(d));

    switch (type & IRQ_TYPE_SENSE_MASK) {
    case IRQ_TYPE_EDGE_RISING:
        priv->irq_type |= bit;
        writel(priv->irq_type, priv->base + IRQ_EDGE_REG);
        break;
    case IRQ_TYPE_EDGE_FALLING:
        priv->irq_type &= ~bit;
        writel(priv->irq_type, priv->base + IRQ_EDGE_REG);
        break;
    case IRQ_TYPE_EDGE_BOTH:
        /* 하드웨어가 지원하면 both-edge 설정 */
        break;
    default:
        return -EINVAL;
    }
    return 0;
}

/* IMMUTABLE irq_chip: 런타임 수정 불가 (v6.0+ 필수) */
static const struct irq_chip my_gpio_irqchip = {
    .name         = "my-gpio-irq",
    .irq_mask     = my_gpio_irq_mask,
    .irq_unmask   = my_gpio_irq_unmask,
    .irq_set_type = my_gpio_irq_set_type,
    .flags        = IRQCHIP_IMMUTABLE,
    GPIOCHIP_IRQ_RESOURCE_HELPERS,
};

/* Chained IRQ handler: 부모 IRQ에서 호출 */
static void my_gpio_irq_handler(struct irq_desc *desc)
{
    struct gpio_chip *gc = irq_desc_get_handler_data(desc);
    struct my_gpio_irq *priv = gpiochip_get_data(gc);
    struct irq_chip *irqchip = irq_desc_get_chip(desc);
    u32 pending;

    chained_irq_enter(irqchip, desc);

    pending = readl(priv->base + IRQ_STATUS_REG);
    pending &= priv->irq_mask;

    while (pending) {
        int hwirq = __ffs(pending);
        generic_handle_domain_irq(gc->irq.domain, hwirq);
        pending &= ~BIT(hwirq);
    }

    /* 인터럽트 상태 클리어 (W1C) */
    writel(pending, priv->base + IRQ_STATUS_REG);

    chained_irq_exit(irqchip, desc);
}

/* probe에서 GPIO + IRQ chip 등록 */
static int my_gpio_irq_probe(struct platform_device *pdev)
{
    struct my_gpio_irq *priv;
    struct gpio_irq_chip *girq;
    int parent_irq;

    priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    priv->base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(priv->base))
        return PTR_ERR(priv->base);

    parent_irq = platform_get_irq(pdev, 0);
    if (parent_irq < 0)
        return parent_irq;

    mutex_init(&priv->lock);

    /* GPIO chip 기본 설정 */
    priv->gc.label            = "my-gpio-irq";
    priv->gc.parent           = &pdev->dev;
    priv->gc.owner            = THIS_MODULE;
    priv->gc.base             = -1;
    priv->gc.ngpio            = 32;
    priv->gc.get              = my_gpio_get;
    priv->gc.set              = my_gpio_set;
    priv->gc.direction_input  = my_gpio_direction_input;
    priv->gc.direction_output = my_gpio_direction_output;

    /* IRQ chip 내장 설정 (권장 패턴) */
    girq = &priv->gc.irq;
    gpio_irq_chip_set_chip(girq, &my_gpio_irqchip);
    girq->parent_handler = my_gpio_irq_handler;
    girq->num_parents = 1;
    girq->parents = devm_kcalloc(&pdev->dev, 1,
                                  sizeof(*girq->parents), GFP_KERNEL);
    if (!girq->parents)
        return -ENOMEM;
    girq->parents[0] = parent_irq;
    girq->default_type = IRQ_TYPE_NONE;
    girq->handler = handle_edge_irq;

    /* GPIO chip + IRQ chip 동시 등록 */
    return devm_gpiochip_add_data(&pdev->dev, &priv->gc, priv);
}
IRQCHIP_IMMUTABLE 필수: v6.0 이후 커널에서는 irq_chip 구조체에 IRQCHIP_IMMUTABLE 플래그를 설정해야 합니다. 이는 런타임에 irq_chip이 수정되는 것을 방지합니다. 또한 gpiochip_enable_irq()gpiochip_disable_irq()를 unmask/mask 콜백에서 호출하고, GPIOCHIP_IRQ_RESOURCE_HELPERS 매크로(Macro)를 포함해야 합니다.

Nested (Threaded) IRQ 패턴

I2C/SPI GPIO expander처럼 슬립 가능한 버스 뒤에 있는 GPIO 컨트롤러는 chained handler를 사용할 수 없습니다(인터럽트 컨텍스트에서 I2C/SPI 전송 불가). 이 경우 nested (threaded) IRQ 패턴을 사용합니다:

/* I2C GPIO expander의 threaded IRQ 패턴 */
girq = &priv->gc.irq;
gpio_irq_chip_set_chip(girq, &my_expander_irqchip);

/* parent_handler를 NULL로 설정하면 nested(threaded) IRQ 사용 */
girq->parent_handler = NULL;
girq->num_parents = 0;
girq->parents = NULL;
girq->default_type = IRQ_TYPE_NONE;
girq->handler = handle_bad_irq;  /* 직접 호출되면 안 됨 */
girq->threaded = true;           /* 핵심: threaded IRQ 사용 */

/* 부모 IRQ를 threaded handler로 직접 등록 */
ret = devm_request_threaded_irq(&client->dev, client->irq,
        NULL, my_expander_irq_thread,
        IRQF_ONESHOT | IRQF_SHARED,
        "my-expander", priv);

/* threaded IRQ handler에서 I2C 통신으로 상태 확인 */
static irqreturn_t my_expander_irq_thread(int irq, void *data)
{
    struct my_gpio_irq *priv = data;
    u32 pending;

    /* I2C/SPI 통신으로 인터럽트 상태 레지스터 읽기 (sleep 가능) */
    pending = i2c_smbus_read_byte_data(priv->client, INT_STATUS_REG);

    while (pending) {
        int hwirq = __ffs(pending);
        handle_nested_irq(irq_find_mapping(
            priv->gc.irq.domain, hwirq));
        pending &= ~BIT(hwirq);
    }
    return IRQ_HANDLED;
}
chained vs nested IRQ 선택 기준: SoC 내장 GPIO 컨트롤러(MMIO 접근)는 chained handler를 사용합니다. I2C/SPI GPIO expander처럼 슬립 가능한 버스 뒤의 컨트롤러는 nested (threaded) handler를 사용합니다. chained handler는 하드 IRQ 컨텍스트에서 실행되므로 더 빠르지만, 슬립이 불가능합니다.

pinctrl: 핀 멀티플렉싱

pinctrl 서브시스템은 SoC의 핀 멀티플렉싱(pinmux)과 핀 설정(pinconf)을 관리합니다. GPIO 서브시스템과 밀접하게 연동되며, 하나의 물리 핀이 GPIO, I2C SDA, SPI MOSI 등 여러 기능 중 하나로 설정될 수 있습니다.

pinctrl 핵심 개념

개념설명예시
Pin Group함께 설정되는 핀 그룹i2c1_pins: {SDA, SCL}
Function핀 그룹이 수행하는 기능i2c, spi, gpio, uart
pinmux핀과 기능의 매핑PA9 → I2C1_SDA
pinconf핀 전기적 특성 설정풀업, 드라이브 강도, 슬루율
State디바이스 상태별 핀 설정default, sleep, idle

Device Tree pinctrl 바인딩

/* SoC pinctrl 노드에서 핀 설정 정의 */
&pinctrl {
    i2c1_default: i2c1-default-pins {
        pins = "PA9", "PA10";
        function = "i2c1";
        bias-pull-up;
        drive-open-drain;
    };

    i2c1_sleep: i2c1-sleep-pins {
        pins = "PA9", "PA10";
        function = "gpio";
        bias-high-impedance;
    };

    spi1_default: spi1-default-pins {
        mosi-sck-pins {
            pins = "PB3", "PB5";
            function = "spi1";
            bias-disable;
            drive-push-pull;
            slew-rate = <1>;  /* high speed */
        };
        miso-pin {
            pins = "PB4";
            function = "spi1";
            bias-pull-down;
        };
    };

    user_led_pin: user-led-pin {
        pins = "PC13";
        function = "gpio";
        drive-push-pull;
        output-low;
    };
};

/* 디바이스 노드에서 pinctrl 상태 참조 */
&i2c1 {
    pinctrl-names = "default", "sleep";
    pinctrl-0 = <&i2c1_default>;
    pinctrl-1 = <&i2c1_sleep>;
    status = "okay";
};

&spi1 {
    pinctrl-names = "default";
    pinctrl-0 = <&spi1_default>;
    status = "okay";
};
pinctrl 자동 전환: 디바이스가 pm_runtime_suspend()에 들어가면 커널이 자동으로 "sleep" 상태의 핀 설정을 적용하고, resume 시 "default"로 복원합니다. 이 동작은 pinctrl-names에 "default"와 "sleep"이 정의되어 있을 때 활성화됩니다.

Device Tree 통합: GPIO 바인딩 패턴

GPIO 서브시스템의 Device Tree 바인딩에서 공통적으로 사용되는 패턴을 정리합니다.

GPIO 관련 공통 프로퍼티

프로퍼티적용 대상설명
compatible모든 디바이스드라이버 매칭 문자열 (vendor,device)
gpio-controllerGPIO 컨트롤러이 노드가 GPIO 제공자임을 표시
#gpio-cellsGPIO 컨트롤러GPIO specifier 셀 수 (보통 2)
*-gpiosGPIO 사용 디바이스GPIO specifier (phandle + 번호 + 플래그)
interrupts인터럽트 사용 디바이스IRQ 스펙
interrupt-controllerIRQ 지원 GPIO 컨트롤러인터럽트 컨트롤러 표시
pinctrl-*핀 설정 필요 디바이스pinctrl 상태

종합 예제: GPIO LED/버튼 + GPIO Expander

실제 임베디드 보드에서 GPIO LED, 버튼, GPIO Expander를 함께 사용하는 Device Tree 예제:

/ {
    model = "My Custom Board";
    compatible = "vendor,my-board";

    leds {
        compatible = "gpio-leds";
        pinctrl-names = "default";
        pinctrl-0 = <&user_led_pin>;

        led-status {
            gpios = <&gpioc 13 GPIO_ACTIVE_LOW>;
            label = "board:green:status";
            linux,default-trigger = "heartbeat";
        };
    };

    gpio-keys {
        compatible = "gpio-keys";

        button-user {
            label = "User Button";
            gpios = <&gpioa 0 GPIO_ACTIVE_LOW>;
            linux,code = <KEY_ENTER>;
            debounce-interval = <20>;
        };
    };
};

&i2c1 {
    status = "okay";

    /* GPIO expander */
    gpio_exp: gpio@20 {
        compatible = "nxp,pca9555";
        reg = <0x20>;
        gpio-controller;
        #gpio-cells = <2>;
        interrupt-parent = <&gpiob>;
        interrupts = <8 IRQ_TYPE_EDGE_FALLING>;
        interrupt-controller;
        #interrupt-cells = <2>;
    };
};

GPIO 디버깅

GPIO 관련 Device Tree 문제를 디버깅하는 방법:

# GPIO 상태 확인
gpiodetect                        # GPIO 칩 목록
gpioinfo                          # 모든 GPIO 라인 정보
cat /sys/kernel/debug/gpio        # debugfs GPIO 상태
cat /sys/kernel/debug/pinctrl/*/pins  # pinctrl 핀 매핑

# Device Tree 런타임 확인
ls /proc/device-tree/             # DT 노드 트리
dtc -I fs /proc/device-tree/      # 런타임 DT를 DTS로 디컴파일
GPIO가 동작하지 않을 때 체크리스트: (1) gpioinfo로 라인 상태 확인 (사용 중인지, 방향이 올바른지), (2) dmesg | grep gpio로 GPIO 컨트롤러 등록 확인, (3) Device Tree의 *-gpios 속성이 올바른 컨트롤러, 오프셋, 플래그를 지정하는지 확인, (4) pinctrl 설정이 올바른지 확인 (핀이 GPIO 기능으로 mux 되었는지), (5) active-low/active-high 플래그가 하드웨어 회로와 일치하는지 확인.

참고자료

커널 공식 문서

커널 소스 코드

외부 자료

이 주제와 관련된 다른 문서를 더 깊이 이해하고 싶다면 다음을 참고하세요.