MFD (Multi-Function Device) — 복합 디바이스 프레임워크
MFD(Multi-Function Device)는 하나의 물리적 칩(I2C/SPI 슬레이브 등)이 레귤레이터·GPIO·RTC·배터리·오디오 CODEC 등 여러 기능을 내장할 때, 이를 독립적인 platform_device 서브 디바이스로 분리하여 각 기능 드라이버가 독립적으로 동작하게 하는 프레임워크입니다(drivers/mfd/). mfd_cell로 서브 디바이스를 선언하고 devm_mfd_add_devices()로 등록하는 구조, 공유 regmap·IRQ 도메인의 전달 방법, Device Tree 바인딩과 regmap-irq 통합 패턴을 실무 예제 중심으로 정리합니다.
핵심 요약
- mfd_cell — 서브 디바이스(자식 platform_device)를 기술하는 구조체입니다. 이름, 리소스, platform_data, DT 노드 이름 등을 포함합니다.
- devm_mfd_add_devices() —
mfd_cell배열을 받아 각각의platform_device를 생성하고 등록합니다. 부모 드라이버 언바인드 시 자동 제거됩니다. - 공유 Regmap — 부모 드라이버가 초기화한
regmap을dev_set_drvdata()또는mfd_cell.platform_data로 자식에 전달합니다. - IRQ 분배 — 물리적 단일 IRQ를
regmap-irq가 가상 IRQ 도메인으로 분리하여 각 서브 디바이스에 전달합니다. - mfd_get_cell() — 서브 디바이스 드라이버의 probe에서 부모로부터 전달된
mfd_cell을 얻을 때 사용합니다. - of_compatible —
mfd_cell의of_compatible필드로 DT 노드와 자동 연결합니다. - PMIC 패턴 — AXP20x, TPS65912, MAX77686 등 대부분의 PMIC 드라이버가 MFD + Regmap + regmap-irq + Regulator 프레임워크 조합을 사용합니다.
- num_resources = 0 — IRQ를 regmap-irq 도메인으로 전달할 때는
mfd_cell.resources를 비우고virq를platform_data로 전달합니다.
단계별 이해
- MFD 구조 파악 — 부모 드라이버가 공유 자원(regmap, IRQ 도메인)을 초기화하고,
devm_mfd_add_devices()로 서브 디바이스를 생성하는 계층 구조를 이해합니다. - mfd_cell 선언 — 필요한 서브 디바이스 목록과 각 서브 디바이스에 필요한 리소스를
mfd_cell배열로 정의합니다. - 공유 자원 전달 방법 선택 —
platform_data,dev_get_drvdata(), 또는devm_regmap_init()의 전역 접근 중 적합한 방법을 선택합니다. - IRQ 분배 설계 —
regmap-irq로 가상 IRQ를 생성하고, 각 서브 디바이스에 필요한 virq를 resource로 전달합니다. - 서브 드라이버 작성 — 각 서브 디바이스 드라이버는 표준
platform_driver로 작성하고, 부모로부터 전달받은 공유 자원을 사용합니다.
개요 — MFD 프레임워크
SoC 설계 시 물리적 비용 절감을 위해 PMIC, 오디오 CODEC, 배터리 관리 IC 등이 단일 칩에 통합됩니다. 예를 들어 Maxim MAX77686은 하나의 I2C 슬레이브 주소 아래 14채널 레귤레이터, 2채널 32kHz 클럭, RTC, GPIO를 내장합니다.
이를 하나의 거대 드라이버로 구현하면 기능별 서브시스템(Regulator, RTC, GPIO)의 표준 인터페이스를 재구현해야 하고 유지보수도 어렵습니다. MFD는 이 문제를 해결합니다.
- 부모 드라이버: 버스 접근, 공유 regmap, IRQ 디멀티플렉싱을 담당합니다.
- 서브 디바이스 드라이버: 각 기능에 특화된 표준 커널 서브시스템(Regulator, RTC 등)과 연동합니다.
mfd_cell 구조체
struct mfd_cell은 부모 드라이버가 생성할 서브 디바이스 하나를 기술합니다. 이름(name)이 platform_device의 드라이버 매칭 이름이 되고, of_compatible이 Device Tree 노드와의 자동 연결 키가 됩니다. resources 배열에는 IRQ나 MMIO 등 서브 디바이스에 독립적으로 전달할 하드웨어 리소스를 넣습니다. platform_data는 regmap 포인터나 초기화 파라미터처럼 커널 내부적으로 전달할 임의 데이터에 사용합니다.
/* include/linux/mfd/core.h */
struct mfd_cell {
const char *name; /* 서브 디바이스 platform_device 이름 */
int id; /* 동일 이름 복수 인스턴스 구분 */
/* 서브 디바이스에 전달할 platform_data */
const void *platform_data;
size_t pdata_size;
/* 서브 디바이스에 할당할 리소스 (IRQ, MEM 등) */
const struct resource *resources;
unsigned int num_resources;
/* DT 노드 매칭용 compatible 문자열 */
const char *of_compatible;
/* ACPI 매칭용 HID */
const char *acpi_pnpid;
};
부모 드라이버 구현 패턴
아래는 PMIC 스타일의 MFD 부모 드라이버 전형적 구조입니다. 부모 probe가 성공적으로 완료되면 devm_mfd_add_devices()가 서브 디바이스들을 platform bus에 등록하고, 커널은 매칭되는 서브 드라이버를 찾아 각 서브 디바이스의 probe를 순서대로 호출합니다. 의존 리소스(클럭, 레귤레이터 등)가 아직 준비되지 않은 서브 드라이버는 -EPROBE_DEFER를 반환하여 재시도 큐에 등록됩니다.
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/regmap.h>
#include <linux/mfd/core.h>
/* --- regmap 설정 --- */
static const struct regmap_config my_pmic_regmap_config = {
.reg_bits = 8,
.val_bits = 8,
.max_register = MY_PMIC_MAX_REG,
.cache_type = REGCACHE_RBTREE,
.volatile_reg = my_pmic_volatile_reg,
};
/* --- regmap-irq 설정 --- */
static const struct regmap_irq my_pmic_irqs[] = {
REGMAP_IRQ_REG(MY_PMIC_IRQ_PWRKEY, 0, BIT(0)),
REGMAP_IRQ_REG(MY_PMIC_IRQ_CHRG, 0, BIT(1)),
REGMAP_IRQ_REG(MY_PMIC_IRQ_BAT, 0, BIT(2)),
REGMAP_IRQ_REG(MY_PMIC_IRQ_RTC, 1, BIT(0)),
};
static const struct regmap_irq_chip my_pmic_irq_chip = {
.name = "my-pmic",
.irqs = my_pmic_irqs,
.num_irqs = ARRAY_SIZE(my_pmic_irqs),
.num_regs = 2,
.status_base = MY_PMIC_REG_IRQ_STATUS0,
.mask_base = MY_PMIC_REG_IRQ_MASK0,
};
/* --- 서브 디바이스 리소스 (virq는 probe에서 채워짐) --- */
static struct resource my_pmic_regulator_resources[] = {
/* 리소스 없음 — regmap 공유로 충분 */
};
static struct resource my_pmic_rtc_resources[1]; /* IRQ를 runtime에 채움 */
static struct resource my_pmic_charger_resources[1];
/* --- mfd_cell 배열 --- */
static struct mfd_cell my_pmic_cells[] = {
{
.name = "my-pmic-regulator",
.of_compatible = "my-pmic-regulator",
.num_resources = 0,
},
{
.name = "my-pmic-rtc",
.of_compatible = "my-pmic-rtc",
.resources = my_pmic_rtc_resources,
.num_resources = ARRAY_SIZE(my_pmic_rtc_resources),
},
{
.name = "my-pmic-charger",
.of_compatible = "my-pmic-charger",
.resources = my_pmic_charger_resources,
.num_resources = ARRAY_SIZE(my_pmic_charger_resources),
},
};
/* --- probe --- */
static int my_pmic_probe(struct i2c_client *client)
{
struct my_pmic_priv *priv;
struct regmap_irq_chip_data *irq_data;
int ret;
priv = devm_kzalloc(&client->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->dev = &client->dev;
/* 1. Regmap 초기화 */
priv->regmap = devm_regmap_init_i2c(client, &my_pmic_regmap_config);
if (IS_ERR(priv->regmap))
return PTR_ERR(priv->regmap);
/* 2. regmap-irq 설정 */
ret = devm_regmap_add_irq_chip(&client->dev, priv->regmap,
client->irq, IRQF_ONESHOT, 0,
&my_pmic_irq_chip, &irq_data);
if (ret)
return ret;
priv->irq_data = irq_data;
/* 3. 서브 디바이스에 virq 전달 */
my_pmic_rtc_resources[0] = DEFINE_RES_IRQ(
regmap_irq_get_virq(irq_data, MY_PMIC_IRQ_RTC));
my_pmic_charger_resources[0] = DEFINE_RES_IRQ(
regmap_irq_get_virq(irq_data, MY_PMIC_IRQ_CHRG));
/* 4. private 데이터 저장 (서브 드라이버가 dev_get_drvdata(dev->parent)로 접근) */
i2c_set_clientdata(client, priv);
/* 5. 서브 디바이스 등록 */
ret = devm_mfd_add_devices(&client->dev, PLATFORM_DEVID_NONE,
my_pmic_cells, ARRAY_SIZE(my_pmic_cells),
NULL, 0, regmap_irq_get_domain(irq_data));
if (ret)
return ret;
dev_info(&client->dev, "MFD initialized with %zu sub-devices\n",
ARRAY_SIZE(my_pmic_cells));
return 0;
}
static const struct of_device_id my_pmic_of_match[] = {
{ .compatible = "vendor,my-pmic" },
{ }
};
MODULE_DEVICE_TABLE(of, my_pmic_of_match);
static struct i2c_driver my_pmic_driver = {
.probe = my_pmic_probe,
.driver = {
.name = "my-pmic",
.of_match_table = my_pmic_of_match,
},
};
module_i2c_driver(my_pmic_driver);
서브 디바이스 드라이버 작성
서브 디바이스 드라이버는 표준 platform_driver로 작성합니다. 부모의 regmap은 dev_get_drvdata(dev->parent)로 접근합니다.
/* my-pmic-rtc.c */
static int my_pmic_rtc_probe(struct platform_device *pdev)
{
struct my_pmic_priv *pmic;
struct my_rtc_priv *priv;
int irq;
/* 부모 MFD 드라이버의 private 데이터(regmap 포함) 획득 */
pmic = dev_get_drvdata(pdev->dev.parent);
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->regmap = pmic->regmap; /* 공유 regmap 사용 */
priv->dev = &pdev->dev;
/* IRQ는 mfd_cell에서 전달된 virq */
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
/* RTC 디바이스 등록 */
priv->rtc = devm_rtc_allocate_device(&pdev->dev);
if (IS_ERR(priv->rtc))
return PTR_ERR(priv->rtc);
priv->rtc->ops = &my_pmic_rtc_ops;
priv->rtc->range_min = RTC_TIMESTAMP_BEGIN_2000;
priv->rtc->range_max = RTC_TIMESTAMP_END_2099;
return devm_rtc_register_device(priv->rtc);
}
static struct platform_driver my_pmic_rtc_driver = {
.probe = my_pmic_rtc_probe,
.driver = {
.name = "my-pmic-rtc",
},
};
module_platform_driver(my_pmic_rtc_driver);
Device Tree 바인딩
MFD 디바이스의 DT 노드는 부모 노드 아래에 서브 디바이스 노드를 중첩하는 구조를 사용합니다. mfd_cell.of_compatible에 지정한 문자열이 서브 노드의 compatible과 일치하면 커널이 자동으로 DT 노드를 서브 platform_device에 연결합니다. 이 연결 덕분에 서브 드라이버는 pdev->dev.of_node로 DT 프로퍼티에 접근할 수 있습니다. 레귤레이터 초기화 전압, RTC 알람 설정 등 하드웨어 의존 초기값을 보드별로 다르게 지정할 때 유용합니다.
/* PMIC 부모 노드 */
pmic: my-pmic@36 {
compatible = "vendor,my-pmic";
reg = <0x36>;
interrupt-parent = <&gpio1>;
interrupts = <5 IRQ_TYPE_LEVEL_LOW>;
/* 서브 디바이스 노드 (of_compatible으로 자동 매칭) */
regulators {
compatible = "my-pmic-regulator";
vdd_cpu: LDO1 {
regulator-name = "vdd-cpu";
regulator-min-microvolt = <900000>;
regulator-max-microvolt = <1400000>;
regulator-always-on;
};
};
rtc {
compatible = "my-pmic-rtc";
};
charger {
compatible = "my-pmic-charger";
monitored-battery = <&bat>;
};
};
공유 자원 전달 패턴 비교
| 방법 | 사용 시점 | 코드 |
|---|---|---|
dev_get_drvdata(dev->parent) |
부모 구조체 전체 접근 | pmic = dev_get_drvdata(pdev->dev.parent); |
mfd_cell.platform_data |
서브별 다른 초기 설정 전달 | mfd_get_cell(pdev)->platform_data |
| 전역 regmap 조회 | dev_get_regmap() | dev_get_regmap(pdev->dev.parent, NULL) |
| IRQ virq | platform_get_irq() | mfd_cell.resources에 DEFINE_RES_IRQ |
devm_mfd_add_devices API 상세
devm_mfd_add_devices()는 mfd_cell 배열 하나로 복수의 platform_device를 한 번에 등록합니다. devm_ 접두사로 인해 부모 드라이버가 언바인드될 때 모든 서브 디바이스가 자동으로 제거됩니다. id 인자로 PLATFORM_DEVID_AUTO를 사용하면 동일 PMIC가 시스템에 여러 개 있을 때 이름 충돌 없이 자동 번호를 부여합니다. irq_domain을 전달하면 mfd_cell.resources의 IRQ 번호가 해당 도메인에서 매핑된 virq로 해석됩니다.
/*
* devm_mfd_add_devices(dev, id, cells, n_devs, mem_base, irq_base, irq_domain)
*
* @dev : 부모 디바이스 (I2C client dev 등)
* @id : PLATFORM_DEVID_NONE / PLATFORM_DEVID_AUTO / 고정 숫자
* @cells : mfd_cell 배열
* @n_devs : 셀 개수
* @mem_base : 리소스 주소 오프셋 (NULL이면 0)
* @irq_base : 레거시 IRQ base (0 권장)
* @irq_domain : regmap-irq 도메인 (virq 매핑에 사용)
*/
ret = devm_mfd_add_devices(&client->dev, PLATFORM_DEVID_NONE,
cells, n_cells,
NULL, 0,
regmap_irq_get_domain(irq_data));
실제 커널 MFD 드라이버 예시
| 드라이버 | 파일 | 서브 디바이스 |
|---|---|---|
| AXP20x (X-Powers PMIC) | drivers/mfd/axp20x.c | Regulator, Charger, Battery, GPIO, RTC, NVM, CODEC |
| MAX77686 (Maxim PMIC) | drivers/mfd/max77686.c | Regulator, Clock, RTC |
| TPS65912 (TI PMIC) | drivers/mfd/tps65912-core.c | Regulator, IRQ |
| DA9063 (Dialog PMIC) | drivers/mfd/da9063-core.c | Regulator, Watchdog, RTC, GPIO, ONKEY |
| WM8994 (Wolfson CODEC) | drivers/mfd/wm8994-core.c | Regulator, CODEC, GPIO |
| LP8788 (TI PMIC) | drivers/mfd/lp8788.c | Regulator, Backlight, Charger, RTC, ADC |
서브 디바이스 활성화 제어
모든 서브 디바이스를 항상 등록하지 않고 하드웨어 상태나 DT 설정에 따라 선택적으로 등록할 수 있습니다.
/* 배열 일부만 등록 */
size_t n_cells = has_rtc ? ARRAY_SIZE(my_pmic_cells)
: ARRAY_SIZE(my_pmic_cells) - 1;
ret = devm_mfd_add_devices(&client->dev, PLATFORM_DEVID_NONE,
my_pmic_cells, n_cells, NULL, 0, domain);
또는 mfd_cell의 enable_reg/enable_val 필드로 regmap을 통한 활성화 조건을 지정할 수 있습니다.
static const struct mfd_cell my_pmic_cells[] = {
{
.name = "my-pmic-backlight",
.of_compatible = "vendor,my-pmic-backlight",
/* 이 레지스터 비트가 설정된 경우에만 등록 */
.check_if_exists = true,
},
};
디버깅
MFD 드라이버 문제는 대부분 세 가지 중 하나에서 발생합니다. 서브 디바이스가 생성되지 않으면 of_compatible 오타나 devm_mfd_add_devices() 반환값 확인 누락이 원인입니다. 서브 드라이버가 바인딩되지 않으면 드라이버 이름과 mfd_cell.name이 일치하지 않거나 모듈이 로드되지 않은 것입니다. IRQ가 전달되지 않으면 virq 할당 순서(regmap-irq 초기화 전에 devm_mfd_add_devices() 호출)를 확인하세요.
# 부모 MFD 디바이스 확인
ls /sys/bus/i2c/devices/0-0036/
# 생성된 서브 platform_device 확인
ls /sys/bus/platform/devices/ | grep my-pmic
# 서브 디바이스별 드라이버 바인딩 확인
cat /sys/bus/platform/devices/my-pmic-rtc.0/uevent
# IRQ 도메인 및 virq 확인
cat /proc/interrupts | grep my-pmic
cat /sys/kernel/debug/irq/domains/
# regmap 레지스터 덤프 (부모)
cat /sys/kernel/debug/regmap/0-0036/registers
IRQ 리소스 전달 심화 — virq를 mfd_cell에 넣는 두 가지 방법
서브 디바이스에 인터럽트를 전달하는 방법은 두 가지가 있습니다. 하드웨어 IRQ 라인을 직접 넣는 방식과 regmap-irq 가상 IRQ를 넣는 방식입니다.
방법 1: 하드웨어 IRQ를 resource로 직접 전달
/* 서브 디바이스마다 물리 IRQ 라인이 별도로 있는 경우 */
static struct resource my_pmic_rtc_res[] = {
{
.start = IRQ_MY_PMIC_RTC_ALARM,
.end = IRQ_MY_PMIC_RTC_ALARM,
.flags = IORESOURCE_IRQ,
.name = "alarm",
},
};
static const struct mfd_cell my_pmic_cells[] = {
{
.name = "my-pmic-rtc",
.of_compatible = "vendor,my-pmic-rtc",
.resources = my_pmic_rtc_res,
.num_resources = ARRAY_SIZE(my_pmic_rtc_res),
},
};
방법 2: regmap-irq 도메인 virq를 resource로 전달
/* 단일 물리 IRQ를 regmap-irq로 여러 virq로 분배하는 경우 */
static int my_pmic_probe(struct i2c_client *client)
{
struct regmap_irq_chip_data *irq_data;
struct resource rtc_res[1];
int virq, ret;
/* regmap-irq 초기화 */
ret = devm_regmap_add_irq_chip(&client->dev, priv->regmap,
client->irq, IRQF_ONESHOT, 0,
&my_pmic_irq_chip, &irq_data);
if (ret)
return ret;
/* 가상 IRQ 번호 조회 */
virq = regmap_irq_get_virq(irq_data, MY_PMIC_IRQ_RTC_ALARM);
if (virq < 0)
return virq;
/* mfd_cell에 virq를 IORESOURCE_IRQ로 넣어서 전달 */
memset(rtc_res, 0, sizeof(rtc_res));
rtc_res[0].start = virq;
rtc_res[0].end = virq;
rtc_res[0].flags = IORESOURCE_IRQ;
rtc_res[0].name = "alarm";
/* irq_domain도 함께 전달하여 서브 디바이스가 직접 domain_irq를 얻을 수 있게 함 */
return devm_mfd_add_devices(&client->dev, PLATFORM_DEVID_NONE,
my_pmic_rtc_cells, 1,
rtc_res, ARRAY_SIZE(rtc_res),
regmap_irq_get_domain(irq_data));
}
platform_get_irq(pdev, 0) 또는 platform_get_irq_byname(pdev, "alarm")으로 virq를 얻습니다. virq는 이미 IRQ 도메인에 매핑된 완전한 Linux IRQ 번호입니다.
공유 Regmap 심화 — 서브 드라이버에서 부모 regmap 접근
서브 디바이스 드라이버에서 부모가 초기화한 regmap을 얻는 방법은 세 가지입니다.
| 방법 | 코드 | 적합한 상황 |
|---|---|---|
| dev_get_drvdata(parent) | struct my_pmic *p = dev_get_drvdata(pdev->dev.parent); | 부모 구조체 전체에 접근이 필요할 때 |
| platform_data 사용 | struct regmap *m = pdev->dev.platform_data; | 간단한 단일 포인터 전달 |
| mfd_cell.platform_data | const struct my_cell_pdata *p = mfd_get_cell(pdev)->platform_data; | 셀별로 다른 초기화 데이터를 전달 |
/* 서브 드라이버 — dev_get_drvdata(parent) 패턴 */
static int my_pmic_rtc_probe(struct platform_device *pdev)
{
struct my_pmic *pmic = dev_get_drvdata(pdev->dev.parent);
struct regmap *regmap;
if (!pmic)
return -EPROBE_DEFER; /* 부모 probe가 아직 완료되지 않은 경우 */
regmap = pmic->regmap;
/* regmap으로 RTC 레지스터 직접 접근 */
return devm_rtc_register_device(
devm_rtc_allocate_device(&pdev->dev));
}
흔한 실수와 주의사항
- virq를 mfd_cell.resources에 넣기 전 할당 확인:
regmap_irq_get_virq()가 반환하기 전에devm_mfd_add_devices()를 호출하면 virq가 0인 상태로 전달됩니다. - dev_get_drvdata(parent) NULL 체크: 부모 probe가 아직 완료되지 않았거나 deferred 상태일 때
dev_get_drvdata()가 NULL을 반환할 수 있습니다. - 공유 regmap 잠금: 부모와 서브 드라이버가 같은 regmap을 동시에 접근하면 regmap 내부 잠금이 보호합니다만, 여러 레지스터에 대한 원자적 트랜잭션이 필요하면 별도 잠금을 추가하세요.
- of_compatible 오타: DT 노드의 compatible과
mfd_cell.of_compatible이 정확히 일치해야 자동 바인딩됩니다. - PLATFORM_DEVID_AUTO vs NONE: 동일 PMIC가 여러 개 있을 때는
PLATFORM_DEVID_AUTO를 사용하여 이름 충돌을 방지합니다.