GPIO 서브시스템 — GPIO / pinctrl
GPIO(General-Purpose Input/Output) 서브시스템의 아키텍처와 gpiod descriptor 기반 안전한 제어 패턴, gpio_chip 드라이버 구현, libgpiod 유저스페이스 접근, GPIO Expander, GPIO IRQ 컨트롤러(irqchip), pinctrl 핀 멀티플렉싱, Device Tree 바인딩까지 GPIO 관련 드라이버 개발에 필요한 핵심을 다룹니다.
핵심 요약
- gpiod API 사용 — Legacy 정수 기반 API 대신 descriptor 기반 gpiod API를 사용합니다.
- active-low 자동 처리 — gpiod_set_value()가 Device Tree의 GPIO_ACTIVE_LOW 플래그를 자동 반영합니다.
- gpio_chip 구현 — GPIO 컨트롤러 드라이버는 gpio_chip 구조체(Struct)를 등록합니다.
- IRQ 통합 — GPIO 핀을 인터럽트 소스로 사용할 때 gpio_irq_chip을 활용합니다.
- pinctrl 연동 — 핀 멀티플렉싱과 전기적 특성은 pinctrl 서브시스템과 협력합니다.
단계별 이해
- GPIO 서브시스템 아키텍처 파악
gpiolib, gpio_chip, gpiod 계층 구조를 이해합니다. - gpiod API로 GPIO 제어
descriptor 기반 획득, 값 읽기/쓰기, IRQ 변환을 익힙니다. - gpio_chip 드라이버 작성
GPIO 컨트롤러를 커널에 등록하는 방법을 학습합니다. - GPIO IRQ와 pinctrl 통합
인터럽트 지원과 핀 설정을 마무리합니다.
GPIO 개요
GPIO (General-Purpose Input/Output)는 소프트웨어로 제어 가능한 범용 디지털 핀입니다. LED, 버튼, 리셋 라인, 칩 셀렉트, 인터럽트 입력 등 다양한 용도로 사용됩니다.
Linux GPIO 서브시스템은 drivers/gpio/에 구현되며, 크게 두 가지 API가 있습니다:
| API | 헤더 | 상태 | 특징 |
|---|---|---|---|
| Legacy (integer-based) | <linux/gpio.h> | Deprecated | gpio_request(), gpio_direction_input() |
| Descriptor-based (gpiod) | <linux/gpio/consumer.h> | 현재 표준 | gpiod_get(), gpiod_set_value() |
gpio_request(), gpio_free(), gpio_get_value() 등 정수 기반 legacy API를 사용하지 마세요. 커널 메인라인에서는 legacy GPIO API를 사용하는 새 드라이버를 받아들이지 않습니다.
GPIO 서브시스템 아키텍처
Linux GPIO 서브시스템은 하드웨어 GPIO 컨트롤러부터 유저스페이스 접근까지 여러 계층으로 구성됩니다. gpiolib이 핵심 프레임워크 역할을 하며, gpio_chip이 하드웨어 추상화를, gpiod가 소비자(consumer) API를 제공합니다.
GPIO 네이밍 규칙
GPIO 서브시스템에서 사용하는 주요 네이밍 개념을 정리합니다:
| 용어 | 범위 | 설명 | 예시 |
|---|---|---|---|
| gpiochip | 시스템 전역 | GPIO 컨트롤러 식별자 (/dev/gpiochipN) | gpiochip0, gpiochip1 |
| offset | 칩 로컬 | 칩 내 핀 번호 (0부터 시작) | 0..ngpio-1 |
| gpio_desc | 커널 내부 | gpiod descriptor (핀의 커널 내부 표현) | struct gpio_desc * |
| con_id | 소비자 드라이버 | Device Tree property에서 <con_id>-gpios의 접두사 | "reset" → reset-gpios |
| line name | 유저스페이스 | DT의 gpio-line-names로 부여된 사람 읽기용 이름 | "user-led", "wifi-reset" |
GPIO 하드웨어 내부 구조
물리적 GPIO 핀은 여러 전기적 설정을 지원하며, 드라이버에서 이를 올바르게 구성하는 것이 중요합니다:
| 설정 | 설명 | 커널 API / DT 속성 | 용도 |
|---|---|---|---|
| Push-Pull | HIGH/LOW 모두 능동적으로 구동 | 기본 출력 모드 | LED 제어, 리셋 라인 |
| Open-Drain | LOW만 능동 구동, HIGH는 풀업 의존 | GPIO_OPEN_DRAIN / drive-open-drain | I2C SDA/SCL, 인터럽트 라인 |
| Open-Source | HIGH만 능동 구동, 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 서브시스템과 밀접하게 연관됩니다. 대부분의 SoC에서 GPIO 핀은 pinctrl 핀과 1:1 매핑(Mapping)되며, pinctrl_gpio_set_config()를 통해 gpiolib에서 pinctrl로 설정이 전달됩니다.
gpiod API (Descriptor-based)
현대 Linux 커널의 표준 GPIO 인터페이스인 gpiod API를 사용합니다. 아래 다이어그램은 devm_gpiod_get() 호출 시 내부에서 일어나는 descriptor 해석 과정을 보여줍니다.
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()는 Device Tree의 GPIO_ACTIVE_LOW 플래그를 자동 반영합니다. gpiod_set_raw_value()는 물리적 라인 레벨을 직접 제어합니다. 일반적으로 gpiod_set_value()를 사용하세요.
GPIO 인터럽트 흐름
GPIO를 인터럽트 소스로 사용하는 경우, GPIO 서브시스템과 IRQ 서브시스템이 협력하여 핀 상태 변화를 커널 인터럽트로 변환합니다. 아래 다이어그램은 GPIO 인터럽트의 전체 처리 흐름을 보여줍니다.
GPIO Debounce (글리치 필터링)
기계식 버튼이나 스위치는 접점 바운싱으로 인해 한 번의 누름에 여러 번의 에지 변화가 발생합니다. Debounce는 이러한 글리치를 필터링하여 깨끗한 신호를 제공합니다.
| 방식 | 구현 | 지연(Latency) | 장점 | 단점 |
|---|---|---|---|---|
| HW Debounce | SoC GPIO 컨트롤러 내장 필터 | HW에서 설정 (수십 us ~ 수 ms) | CPU 부하 없음, 정밀한 타이밍 | 모든 SoC 지원 아님 |
| SW Debounce | gpiolib hrtimer 기반 | gpiod_set_debounce() | 모든 GPIO에 적용 가능 | CPU 오버헤드(Overhead), 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 컨트롤러를 생성할 수 있습니다. 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 Bulk Operations
여러 GPIO 핀을 동시에 제어해야 할 때 gpiod_get_array()와 gpiod_set_array_value()를 사용하면 하드웨어 레벨에서 단일 레지스터(Register) 접근으로 최적화됩니다:
/* 여러 GPIO를 한 번에 획득 (Device Tree: data-gpios = <...>, <...>, <...>) */
struct gpio_descs *data_gpios;
data_gpios = devm_gpiod_get_array(&pdev->dev, "data", GPIOD_OUT_LOW);
if (IS_ERR(data_gpios))
return PTR_ERR(data_gpios);
dev_info(dev, "acquired %u data GPIOs\n", data_gpios->ndescs);
/* 8비트 병렬 데이터 버스 출력 예시 */
unsigned long *values;
values = bitmap_alloc(data_gpios->ndescs, GFP_KERNEL);
bitmap_zero(values, data_gpios->ndescs);
/* 0xA5 = 10100101 출력 */
__set_bit(0, values); /* bit 0 */
__set_bit(2, values); /* bit 2 */
__set_bit(5, values); /* bit 5 */
__set_bit(7, values); /* bit 7 */
/* 같은 칩의 GPIO들은 단일 writel()로 최적화됨 */
gpiod_set_array_value(data_gpios->ndescs,
data_gpios->desc, data_gpios->info, values);
bitmap_free(values);
gpio_chip에 속하는 GPIO들은 gpio_chip.set_multiple() 콜백(Callback)을 통해 단일 레지스터 접근으로 처리됩니다. 서로 다른 칩의 GPIO가 섞여 있으면 칩별로 그룹화하여 순차 처리됩니다. 고속 병렬 데이터 버스(Bus)(8080 LCD 인터페이스 등)에서 성능 차이가 큽니다.
GPIO Hog
GPIO Hog는 Device Tree에서 GPIO 라인의 초기 상태를 선언적으로 설정하는 메커니즘입니다. 부팅 시 gpiolib이 자동으로 해당 GPIO를 요청하고 방향/값을 설정합니다. 특정 드라이버 없이 보드 레벨에서 리셋 라인이나 전원 인에이블 핀을 고정해야 할 때 유용합니다:
&gpio1 {
/* GPIO Hog: 부팅 시 자동 설정 */
wifi-reset-hog {
gpio-hog;
gpios = <5 GPIO_ACTIVE_LOW>;
output-high; /* 논리적 HIGH (active-low이므로 물리적 LOW) */
line-name = "wifi-reset";
};
pmic-enable-hog {
gpio-hog;
gpios = <12 GPIO_ACTIVE_HIGH>;
output-high;
line-name = "pmic-enable";
};
debug-input-hog {
gpio-hog;
gpios = <20 GPIO_ACTIVE_HIGH>;
input; /* 입력으로 설정 */
line-name = "debug-detect";
};
};
gpiod_get()으로 획득할 수 없습니다(이미 사용 중). 따라서 드라이버가 나중에 제어해야 하는 핀에는 hog를 사용하지 마세요. Hog는 보드 레벨에서 부팅 직후 고정되어야 하는 핀(전원, 리셋 해제 등)에만 적합합니다.
gpio_chip 구현
GPIO 컨트롤러 드라이버를 작성하려면 gpio_chip 구조체를 구현하고 등록합니다. 아래 다이어그램은 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 인터페이스를 대체합니다.
/sys/class/gpio/export 인터페이스는 deprecated 상태입니다. 새 프로젝트에서는 chardev 기반(/dev/gpiochipN) 접근을 권장하며, 가능한 경우 libgpiod(v2 이상)를 사용하세요.
libgpiod 명령행 도구
| 도구 | 용도 | 예시 |
|---|---|---|
gpiodetect | 시스템의 GPIO 칩 목록 | gpiodetect |
gpioinfo | GPIO 라인 상세 정보 | gpioinfo gpiochip0 |
gpioget | GPIO 입력 값 읽기 | gpioget gpiochip0 7 |
gpioset | GPIO 출력 값 설정 | gpioset gpiochip0 7=1 |
gpiomon | GPIO 이벤트 모니터링 | 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 수 | 인터럽트 | 커널 드라이버 |
|---|---|---|---|---|
| MCP23017 | I2C | 16 | 지원 | gpio-mcp23s08 |
| MCP23S17 | SPI | 16 | 지원 | gpio-mcp23s08 |
| PCA9555 | I2C | 16 | 지원 | gpio-pca953x |
| PCA9535 | I2C | 16 | 지원 | gpio-pca953x |
| PCF8574 | I2C | 8 | 지원 | gpio-pcf857x |
| TCA6424A | I2C | 24 | 지원 | 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";
};
};
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 |
| Hierarchical | GPIO IRQ → 상위 IRQ 컨트롤러(GIC 등)에 계층적 매핑 | 부모 IRQ 컨트롤러가 별도 존재하는 경우 |
gpio_chip irqchip 구현
현대 커널(v5.10+)에서는 gpio_irq_chip을 gpio_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);
}
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처럼 슬립(Sleep) 가능한 버스 뒤에 있는 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;
}
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";
};
pm_runtime_suspend()에 들어가면 커널이 자동으로 "sleep" 상태의 핀 설정을 적용하고, resume 시 "default"로 복원합니다. 이 동작은 pinctrl-names에 "default"와 "sleep"이 정의되어 있을 때 활성화됩니다.
pinctrl 드라이버 구현 핵심
GPIO 컨트롤러와 pinctrl을 통합하는 SoC 드라이버의 핵심 구조를 보여줍니다. 대부분의 SoC GPIO 드라이버는 gpio_chip과 pinctrl_desc를 모두 등록합니다:
#include <linux/pinctrl/pinctrl.h>
#include <linux/pinctrl/pinmux.h>
#include <linux/pinctrl/pinconf.h>
#include <linux/pinctrl/pinconf-generic.h>
/* 핀 정의 */
static const struct pinctrl_pin_desc my_pins[] = {
PINCTRL_PIN(0, "PA0"),
PINCTRL_PIN(1, "PA1"),
/* ... */
PINCTRL_PIN(31, "PA31"),
};
/* 그룹 정의: 함께 mux되는 핀 집합 */
static const unsigned int i2c1_pins[] = { 9, 10 };
static const unsigned int spi1_pins[] = { 3, 4, 5 };
/* pinmux ops: 핀 기능 선택 */
static int my_pmx_set_mux(struct pinctrl_dev *pctldev,
unsigned int func_selector,
unsigned int group_selector)
{
/* SoC mux 레지스터에 기능 번호 기록 */
writel(func_selector, priv->base + MUX_REG(group_selector));
return 0;
}
/* GPIO 요청 시 핀을 GPIO 기능으로 전환 */
static int my_pmx_gpio_request_enable(struct pinctrl_dev *pctldev,
struct pinctrl_gpio_range *range,
unsigned int offset)
{
/* 해당 핀의 mux를 GPIO 기능(보통 0)으로 설정 */
writel(0, priv->base + MUX_REG(offset));
return 0;
}
static const struct pinmux_ops my_pmx_ops = {
.get_functions_count = my_pmx_get_funcs_count,
.get_function_name = my_pmx_get_func_name,
.get_function_groups = my_pmx_get_func_groups,
.set_mux = my_pmx_set_mux,
.gpio_request_enable = my_pmx_gpio_request_enable,
.strict = true, /* GPIO와 다른 기능 동시 사용 불가 */
};
/* pinconf ops: 전기적 특성 설정 */
static int my_pinconf_set(struct pinctrl_dev *pctldev,
unsigned int pin,
unsigned long *configs,
unsigned int num_configs)
{
for (int i = 0; i < num_configs; i++) {
u32 param = pinconf_to_config_param(configs[i]);
u32 arg = pinconf_to_config_argument(configs[i]);
switch (param) {
case PIN_CONFIG_BIAS_PULL_UP:
my_set_pullup(priv, pin, arg);
break;
case PIN_CONFIG_BIAS_PULL_DOWN:
my_set_pulldown(priv, pin, arg);
break;
case PIN_CONFIG_DRIVE_STRENGTH:
my_set_drive(priv, pin, arg);
break;
case PIN_CONFIG_INPUT_DEBOUNCE:
my_set_debounce(priv, pin, arg);
break;
}
}
return 0;
}
pinmux_ops.strict = true로 설정하면 하나의 핀이 GPIO와 다른 기능(I2C, SPI 등)에 동시에 할당되는 것을 방지합니다. 대부분의 SoC에서 물리적으로 불가능하므로 strict 모드가 권장됩니다.
GPIO 전원 관리(Power Management)
시스템 절전(Suspend) 시 GPIO 상태를 올바르게 관리하는 것이 중요합니다. 특히 wakeup 소스로 사용되는 GPIO 인터럽트와 suspend/resume 시 핀 상태 보존에 주의해야 합니다.
GPIO Wakeup 소스
/* GPIO를 wakeup 소스로 설정 */
int irq = gpiod_to_irq(button_gpio);
devm_request_threaded_irq(dev, irq, NULL, button_handler,
IRQF_TRIGGER_FALLING | IRQF_ONESHOT, "wakeup-button", data);
/* 이 IRQ를 시스템 wakeup 소스로 활성화 */
device_init_wakeup(dev, true);
dev_pm_set_wake_irq(dev, irq);
/* 또는 enable_irq_wake() 직접 사용 */
enable_irq_wake(irq);
Suspend/Resume 핀 상태
| 시나리오 | suspend 동작 | resume 동작 | 구현 방법 |
|---|---|---|---|
| 출력 GPIO (LED 등) | 현재 값 유지 또는 끔 | 이전 값 복원 | 드라이버 suspend/resume 콜백에서 처리 |
| Wakeup GPIO | IRQ 활성 유지 | IRQ 처리 후 복귀 | enable_irq_wake() |
| pinctrl sleep 상태 | sleep 핀 설정 적용 | default 핀 설정 복원 | DT pinctrl-names = "default", "sleep" |
| GPIO Expander (I2C) | I2C 버스 비활성화 전 설정 저장 | I2C 버스 활성화 후 설정 복원 | regmap_cache 활용 |
/* GPIO expander 드라이버의 suspend/resume 예시 */
static int my_gpio_exp_suspend(struct device *dev)
{
struct my_gpio_exp *priv = dev_get_drvdata(dev);
/* regmap 캐시에 현재 레지스터 값 보존 */
regcache_cache_only(priv->regmap, true);
regcache_mark_dirty(priv->regmap);
return 0;
}
static int my_gpio_exp_resume(struct device *dev)
{
struct my_gpio_exp *priv = dev_get_drvdata(dev);
/* 캐시된 값을 하드웨어에 복원 */
regcache_cache_only(priv->regmap, false);
return regcache_sync(priv->regmap);
}
static DEFINE_SIMPLE_DEV_PM_OPS(my_gpio_exp_pm,
my_gpio_exp_suspend, my_gpio_exp_resume);
dev_pm_domain_attach() 또는 DT의 power-domains로 의존성을 명시하세요. Resume 순서가 잘못되면 I2C 전송이 실패하여 GPIO 상태 복원에 실패합니다.
GPIO 테스트와 시뮬레이션
실제 하드웨어 없이 GPIO 드라이버를 개발하고 테스트하는 방법입니다.
gpio-sim: 가상 GPIO 컨트롤러
gpio-sim 모듈(v5.17+)은 configfs를 통해 가상 GPIO 컨트롤러를 생성합니다:
# gpio-sim 모듈 로드
modprobe gpio-sim
# configfs에서 가상 GPIO 칩 생성
mkdir -p /sys/kernel/config/gpio-sim/my-test-chip
mkdir -p /sys/kernel/config/gpio-sim/my-test-chip/bank0
# 8개 라인, 라벨 설정
echo 8 > /sys/kernel/config/gpio-sim/my-test-chip/bank0/num_lines
echo "test-line-0" > /sys/kernel/config/gpio-sim/my-test-chip/bank0/line0/name
echo "test-line-1" > /sys/kernel/config/gpio-sim/my-test-chip/bank0/line1/name
# 활성화 → /dev/gpiochipN 생성
echo 1 > /sys/kernel/config/gpio-sim/my-test-chip/live
# 외부에서 핀 값 주입 (입력 시뮬레이션)
echo 1 > /sys/kernel/config/gpio-sim/my-test-chip/bank0/line0/pull
# libgpiod로 확인
gpioget $(gpiodetect | grep gpio-sim | awk '{print $1}') 0
# 정리
echo 0 > /sys/kernel/config/gpio-sim/my-test-chip/live
rmdir /sys/kernel/config/gpio-sim/my-test-chip/bank0/line*/
rmdir /sys/kernel/config/gpio-sim/my-test-chip/bank0
rmdir /sys/kernel/config/gpio-sim/my-test-chip
gpio-sim과 gpio-aggregator를 조합하면 실제 하드웨어 없이도 GPIO 드라이버의 기능을 자동화 테스트할 수 있습니다. 커널의 tools/testing/selftests/gpio/에 셀프 테스트 예제가 있습니다.
Device Tree 통합: GPIO 바인딩 패턴
GPIO 서브시스템의 Device Tree 바인딩에서 공통적으로 사용되는 패턴을 정리합니다.
GPIO 관련 공통 프로퍼티
| 프로퍼티 | 적용 대상 | 설명 |
|---|---|---|
compatible | 모든 디바이스 | 드라이버 매칭 문자열 (vendor,device) |
gpio-controller | GPIO 컨트롤러 | 이 노드가 GPIO 제공자임을 표시 |
#gpio-cells | GPIO 컨트롤러 | GPIO specifier 셀 수 (보통 2) |
*-gpios | GPIO 사용 디바이스 | GPIO specifier (phandle + 번호 + 플래그) |
interrupts | 인터럽트 사용 디바이스 | IRQ 스펙 |
interrupt-controller | IRQ 지원 GPIO 컨트롤러 | 인터럽트 컨트롤러(Interrupt Controller) 표시 |
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 디버깅(Debugging)
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로 디컴파일
gpioinfo로 라인 상태 확인 (사용 중인지, 방향이 올바른지),
(2) dmesg | grep gpio로 GPIO 컨트롤러 등록 확인,
(3) Device Tree의 *-gpios 속성이 올바른 컨트롤러, 오프셋(Offset), 플래그를 지정하는지 확인,
(4) pinctrl 설정이 올바른지 확인 (핀이 GPIO 기능으로 mux 되었는지),
(5) active-low/active-high 플래그가 하드웨어 회로와 일치하는지 확인.
커널 6.x GPIO 최신 동향 (2025-2026)
Linux 6.6 LTS부터 6.18까지 GPIO 서브시스템에는 gpio-cdev v2 ABI 정착, gpio-aggregator의 configfs 인터페이스, GPIO 라인 이벤트의 하드웨어 타임스탬프(High-resolution Timer Engine, HTE) 지원 같은 변화가 누적되었습니다. Bartosz Golaszewski가 메인테이너입니다.
| 커널 버전 | 변경사항 | 영향 |
|---|---|---|
| 6.7 | gpio-aggregator sysfs 동적 묶음 생성, libgpiod 2.x 호환 안정화 | 가상 GPIO 칩 동적 구성 |
| 6.10 | Renesas RZ/V2H pinctrl 머지 | 차세대 임베디드 SoC |
| 6.12 | Snapdragon X Elite(X1E80100), MediaTek MT8196 pinctrl | ARM 노트북 지원 |
| 6.13 | GPIO line event timestamp가 CLOCK_HARDWARE_TIMESTAMP 기반(PTP 동기화) 지원 | 1ns 분해능 입력 캡처 |
| 6.14 | gpio-aggregator에 configfs 인터페이스 추가 | runtime 가상 칩 생성 |
| 6.15 | NXP i.MX95 pinctrl 머지, GPIO v1 ABI deprecated 경고 | v2 마이그레이션 권장 |
| 6.16 | Rockchip RK3576 pinctrl, GPIO_V2_LINE_FLAG_EVENT_CLOCK_HTE 정착 | NVIDIA Tegra234 GTE 1ns 캡처 |
| 6.17 | gpio-sim 양방향 시뮬레이션과 trigger 주입, AMD Ryzen AI 300 GPIO race fix | CI 자동화 테스트 |
| 6.18 | gpio-aggregator의 nested aggregation 안정화 | 다단 가상 GPIO 매핑 |
gpio-cdev v2 ABI와 하드웨어 타임스탬프 (HTE)
v2 ABI(GPIO_V2_LINE_*)는 6.6 시점 이미 안정화되어 libgpiod 2.x와 호환되며, 6.15에서 v1 ABI(linehandle, lineevent)는 deprecated 경고가 붙기 시작했습니다. 6.13에서 추가된 GPIO_V2_LINE_FLAG_EVENT_CLOCK_HTE 플래그는 NVIDIA Tegra234의 GTE(GPIO Timestamp Engine) 같은 하드웨어가 GPIO edge를 1ns 분해능으로 직접 타임스탬프하도록 합니다. 카메라 동기화, 모터 인코더, 정밀 측정 시나리오에서 결정적 타이밍을 제공합니다.
/* libgpiod 2.x로 HTE 타임스탬프 활성화 */
struct gpiod_line_settings *settings = gpiod_line_settings_new();
gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
gpiod_line_settings_set_edge_detection(settings, GPIOD_LINE_EDGE_BOTH);
gpiod_line_settings_set_event_clock(settings, GPIOD_LINE_CLOCK_HTE); /* HTE */
/* 이벤트 폴링 */
struct gpiod_edge_event_buffer *buffer = gpiod_edge_event_buffer_new(64);
gpiod_line_request_read_edge_events(req, buffer, 16);
gpio-aggregator와 gpio-sim — 가상 칩 동적 생성
drivers/gpio/gpio-aggregator.c는 다른 GPIO 컨트롤러의 라인을 모아 새 가상 GPIO 칩을 만듭니다. 6.7의 sysfs(new_device/delete_device)에 이어 6.14에서 configfs 인터페이스가 추가되어, runtime에 컨테이너처럼 GPIO 라인을 묶거나 풀 수 있습니다. 컨테이너/Kubernetes 환경에서 호스트의 GPIO 일부만 워크로드에 노출하는 시나리오에 유용합니다.
# configfs로 가상 GPIO 칩 생성 (6.14+)
$ mount -t configfs none /sys/kernel/config
$ mkdir /sys/kernel/config/gpio-aggregator/agg0
$ echo "gpiochip0 0,5,8 gpiochip2 1-3" > /sys/kernel/config/gpio-aggregator/agg0/lines
$ echo 1 > /sys/kernel/config/gpio-aggregator/agg0/live
# 새 /dev/gpiochipN 디바이스가 생성됨
drivers/gpio/gpio-sim.c는 6.17에서 양방향 시뮬레이션과 trigger event 주입을 지원하여, GPIO 기반 드라이버를 실제 하드웨어 없이 CI 파이프라인에서 테스트할 수 있습니다. KUnit 테스트와 결합하여 BSP(Board Support Package) 회귀 검증을 자동화합니다.
AMD Ryzen AI 300 GPIO 인터럽트 race 수정 (6.17)
drivers/pinctrl/pinctrl-amd.c의 wake interrupt 마스크 처리에 lockless가 부족하여 suspend 직전 edge가 손실되었습니다. 6.17에서 amd_gpio_irq_* 함수들이 spinlock 영역을 재정비하여 fix되었으며, 6.16.x stable 시리즈에 백포팅되었습니다.참고자료
커널 공식 문서
- GPIO Subsystem — GPIO 서브시스템 문서
- GPIO Consumer Interface — GPIO 소비자 인터페이스 (gpiod API)
- GPIO Driver Interface — GPIO 칩 드라이버 인터페이스
- GPIO Mappings — GPIO 매핑 (보드 레벨)
- Using GPIO — GPIO 사용 가이드
- Pin Control Subsystem — Pin Control 서브시스템
- GPIO Userspace ABI — GPIO 유저스페이스 ABI (chardev)
커널 소스 코드
drivers/gpio/gpiolib.c— GPIO 라이브러리 코어drivers/gpio/gpiolib-cdev.c— GPIO Character Device (v2 ABI)include/linux/gpio/consumer.h— GPIO consumer API (gpiod_*)include/linux/gpio/driver.h— GPIO 칩 드라이버 인터페이스drivers/pinctrl/core.c— pinctrl 코어 구현include/linux/pinctrl/pinctrl.h— pinctrl 디스크립터include/linux/pinctrl/pinmux.h— 핀 멀티플렉싱include/linux/pinctrl/pinconf.h— 핀 설정tools/gpio/— GPIO 유저스페이스 도구 (lsgpio, gpio-event-mon 등)
외부 자료
- GPIO in the kernel: an introduction — 커널 GPIO 소개
- The pin control subsystem — pin control 서브시스템 해설
관련 문서
이 주제와 관련된 다른 문서를 더 깊이 이해하고 싶다면 다음을 참고하세요.