Reset Controller 프레임워크

리눅스 reset controller 프레임워크를 "assert/deassert 몇 번 하는 API" 수준이 아니라, SoC 주변장치의 초기화와 복구를 표현하는 공통 상태 모델로 정리합니다. reset_control consumer API, shared/exclusive reset의 공식 의미, self-deasserting pulse reset, optional/array/acquire-release API, reset_controller_dev provider 등록, Device Tree의 resets/reset-names/#reset-cells 바인딩, clock/regulator/runtime PM과의 시퀀스, bootloader handoff, cold boot와 resume 차이, 실전 디버깅(Debugging)과 실패 패턴까지 자세히 다룹니다.

전제 조건: Common Clock Framework, Regulator 프레임워크, Device Tree, 전원 관리(Power Management) 문서를 먼저 읽으세요. reset은 단독으로 잘 동작해도 clock, regulator, runtime PM 순서가 어긋나면 장치가 전혀 다른 방식으로 망가집니다.
일상 비유: reset 라인은 장비 캐비닛의 재기동 스위치 + 인터록 시스템과 비슷합니다. 어떤 장비는 전원을 넣고 신호선을 맞춘 뒤 재기동해야 하고, 어떤 장비는 공유 스위치를 쓰기 때문에 내 장비만 따로 껐다 켠다고 생각하면 곤란합니다.

핵심 요약

  • reset line과 reset control은 다릅니다 — 물리적 선(reset line) 하나 또는 여러 개를 제어하는 방법이 reset control이고, 이를 모아 관리하는 하드웨어가 reset controller입니다.
  • exclusive vs shared — exclusive reset은 드라이버가 실제 물리 상태를 직접 바꾼다는 뜻이고, shared reset은 refcount 기반 의미만 보장됩니다.
  • shared reset에서는 assert가 진짜 assert가 아닐 수 있습니다 — 다른 소비자가 deassert를 유지하고 있으면 선은 계속 deassert 상태일 수 있습니다.
  • pulse reset과 level reset은 다릅니다 — self-deasserting reset은 reset_control_reset()로 pulse를 트리거하는 모델이고, level reset은 assert/deassert로 상태를 유지합니다.
  • 시퀀스가 본질입니다 — 대개 regulator on, clock 준비, reset deassert, MMIO 초기화 순서가 맞아야 합니다.
  • provider와 consumer 둘 다 중요합니다 — 소비자 드라이버만 보는 것으로는 부족하고, provider의 reset_control_ops와 DT xlate 규칙도 함께 봐야 합니다.

단계별 이해

  1. reset 선 식별
    데이터시트와 DT에서 이 장치가 어떤 reset control과 연결되는지 먼저 찾습니다.
  2. 모델 판단
    이 reset이 level 유지형인지, pulse형인지, 공유형인지 단독형인지 구분합니다.
  3. 시퀀스 정리
    전원, clock, reset, 펌웨어(Firmware) 로드, MMIO 초기화 순서를 글로 먼저 적어 봅니다.
  4. 에러 경로 설계
    probe 실패, suspend/resume 실패, remove 시 reset 상태를 어디로 되돌릴지 결정합니다.
  5. 공유 자원 여부 확인
    다른 IP와 reset bit를 공유하거나 provider가 여러 line을 한 control로 묶는지 확인합니다.

reset framework가 필요한 이유와 책임 경계

주변장치는 전원만 공급된다고 해서 자동으로 정상 동작하지 않습니다. 이전 부팅에서 남은 DMA 상태, internal state machine, firmware MCU, bus handshake, PHY calibration 상태가 뒤섞인 채 남아 있을 수 있습니다. 이때 reset line을 적절히 assert하거나 pulse를 걸어 known-good state로 되돌리는 것이 필요합니다. reset controller 프레임워크는 이 과정을 consumer-driver마다 제멋대로 구현하지 않도록 공통 API와 공통 DT binding으로 정리합니다.

공식 커널 문서는 reset controller API의 책임을 명확히 구분합니다. 특히 일부 reset controller 하드웨어가 system restart 기능도 같이 제공할 수는 있지만, system restart handler는 reset controller API의 범위 밖이라고 설명합니다. 즉 이 프레임워크는 "장치 블록을 known state로 되돌리는 것"이 주제이지, 시스템 전체 재부팅을 설명하는 프레임워크는 아닙니다.

하드웨어 리셋 신호의 물리적 구조

SoC(System on Chip) 내부에서 리셋 신호(Reset Signal)는 물리적으로 디지털 로직(Digital Logic)의 입력 핀에 연결됩니다. 이 신호가 활성화(Active)되면 해당 IP 블록의 모든 플립플롭(Flip-Flop)이 초기 상태로 되돌아가고, 내부 상태 머신(State Machine)이 리셋 벡터(Reset Vector)부터 다시 시작합니다. 리셋 신호의 극성(Polarity)은 SoC마다 다릅니다. 일부는 active-low(0이면 리셋 활성), 일부는 active-high(1이면 리셋 활성)입니다.

리셋 신호가 IP 블록에 미치는 영향은 하드웨어 설계에 따라 크게 다릅니다.

리셋 유형영향 범위복구 비용대표 사례
전체 IP 리셋(Full IP Reset)모든 레지스터, FIFO, 상태 머신 초기화높음 — 전체 재초기화 필요GPU core reset, USB controller reset
기능 블록 리셋(Functional Block Reset)특정 서브모듈(Submodule)만 초기화중간 — 해당 블록만 재설정MAC reset (PHY는 유지), DMA 채널 reset
소프트 리셋(Soft Reset)상태 머신만 초기화, 설정 레지스터 유지낮음 — 빠른 복구 가능UART FIFO flush, SPI controller soft reset
파워 온 리셋(Power-On Reset, POR)전원 공급 시 자동으로 발생하는 전체 리셋최대 — cold boot와 동일SoC 전체 POR, 외부 PMIC reset output

SoC 리셋 도메인(Reset Domain) 구조

현대 SoC는 하나의 거대한 리셋 트리(Reset Tree)가 아니라 여러 개의 리셋 도메인(Reset Domain)으로 나뉩니다. 각 도메인은 독립적으로 리셋할 수 있으며, 도메인 간의 종속성(Dependency)이 하드웨어 수준에서 정의됩니다.

SoC 리셋 도메인 계층 — 전원 도메인과 리셋 도메인은 보통 1:1이 아니다 System Reset Controller (SRC/CRU/PRCM) 전체 리셋 도메인을 관할하는 최상위 컨트롤러 CPU Reset Domain Core 0 Core 1 L2 Cache Peripheral Reset Domain UART SPI I2C GPIO Media Reset Domain GPU VPU Display Controller Network Domain Ethernet MAC USB Controller 도메인 간 종속성 규칙 1. CPU 도메인 리셋은 다른 모든 도메인에 영향을 줄 수 있습니다 (bus master가 사라지므로) 2. Peripheral 도메인 리셋은 해당 도메인의 모든 IP를 동시에 리셋하거나, 개별 IP만 리셋할 수 있습니다 3. Media 도메인은 보통 전용 전원 도메인과 1:1 매핑되며, power domain off 시 context가 완전히 사라집니다 4. Network 도메인의 USB/Ethernet은 PHY reset이 별도 라인으로 존재하는 경우가 많습니다 5. 하나의 reset controller가 여러 도메인을 관할할 수도 있고, 도메인마다 별도 controller가 있을 수도 있습니다

리셋 도메인 구조는 SoC마다 크게 다릅니다. ARM SoC에서는 보통 PRCM(Power, Reset and Clock Manager)이나 CRU(Clock and Reset Unit)가 리셋 도메인을 관리합니다. RISC-V SoC에서는 PRCI(Power, Reset, Clock, Interrupt)가 유사한 역할을 합니다. 중요한 점은 리셋 도메인과 전원 도메인(Power Domain)이 반드시 1:1로 대응하지는 않는다는 것입니다. 하나의 전원 도메인 안에 여러 개의 독립적인 리셋 라인이 존재할 수 있고, 반대로 하나의 리셋 라인이 여러 전원 도메인에 걸쳐 있는 경우도 드물지만 존재합니다.

리셋 신호와 워치독(Watchdog)의 차이: 워치독 타임아웃으로 인한 시스템 리셋은 보통 SoC 전체를 POR 상태로 되돌립니다. 반면 reset controller가 관리하는 리셋은 개별 IP 블록 단위의 세밀한 제어입니다. 일부 SoC에서는 워치독 리셋 출력이 reset controller를 거쳐 특정 도메인만 리셋하도록 설정할 수 있지만, 이는 SoC 설계에 따라 다릅니다.
개념의미예시
reset linereset 신호가 실제로 전달되는 물리 선USB PHY reset pin, GPU reset input
reset control하나 또는 여러 reset line의 상태를 결정하는 제어 방법레지스터(Register)의 한 비트, self-clearing trigger bit, 복합 시퀀스 엔진
reset controller여러 reset control을 제공하는 하드웨어 블록SoC CCU/CRU/PRCM 내 reset block
reset consumerreset 신호를 받아 리셋되는 장치UART, USB controller, DMA, PHY, 외부 IC
reset line과 reset control, reset controller는 서로 다른 개념입니다 Reset Controller 여러 reset control 보유 control A: bit[3] control B: pulse trigger reset line 0 USB core로 전달 reset line 1 + 2 복합 pulse 시퀀스 USB core consumer USB PHY consumer DMA engine consumer
핵심 구분: reset control 하나가 꼭 reset line 하나만 제어하는 것은 아닙니다. 공식 커널 문서도 "한 trigger action이 여러 reset line에 carefully timed pulse sequence를 일으킬 수 있다"고 설명합니다. 그래서 단순 비트 토글로만 사고하면 복합 reset 하드웨어를 오해하기 쉽습니다.

consumer 관점의 공식 의미: exclusive와 shared

reset controller API는 reset control을 요청할 때부터 의미를 나눕니다. exclusive reset은 드라이버가 해당 control을 직접 제어한다고 가정하고, shared reset은 여러 소비자가 같은 control을 함께 사용하는 상황을 모델링합니다. 이 차이는 단순한 ownership 표시가 아니라, assert(), deassert(), reset()의 물리적 효과 자체를 바꿉니다.

종류보장되는 의미물리적 효과언제 적합한가
exclusive소비자가 실제 reset 상태를 직접 제어assert는 즉시 assert, deassert는 즉시 deassertreset line이 장치 하나 또는 드라이버 한 개만의 책임일 때
shareddeassert 요청에 대한 refcount 의미만 보장첫 deassert와 마지막 assert만 물리 상태를 바꿀 수 있음여러 consumer가 같은 reset control을 함께 써야 할 때

공식 문서 기준으로 shared reset은 clock framework의 shared enable과 비슷합니다. 첫 번째 deassert만 실제 선을 deassert할 수 있고, 마지막 assert만 실제 선을 assert할 수 있습니다. 그래서 shared reset에서 reset_control_assert()를 호출했다고 해서 물리 선이 반드시 assert된다고 기대하면 안 됩니다. 다른 소비자가 계속 deassert를 요구하고 있으면 선은 여전히 deassert 상태일 수 있습니다.

shared reset은 "내가 assert했다"보다 "아직 누가 deassert를 원하는가"가 중요합니다 Consumer A deassert 요청 deassert_count = 1 Shared reset control A deassert → 물리 선 deassert refcount 기반 의미 Consumer B deassert 요청 deassert_count = 2 이후 A가 assert를 호출해도 물리 선은 아직 assert되지 않을 수 있습니다 B가 여전히 deassert를 원하면 line은 deassert 상태를 유지합니다 마지막 consumer가 assert해 refcount가 0이 될 때 비로소 물리 assert가 가능해집니다
공식 문서의 강한 경고: shared reset control을 쓰는 consumer는 "assert를 호출했으니 하드웨어 레지스터와 내부 상태가 reset되었을 것"이라고 가정하면 안 됩니다. 반대로 다른 consumer가 모두 물러났다면 어느 순간 실제 assert가 들어올 수도 있으므로, reset이 발생할 가능성도 염두에 둬야 합니다.

level reset과 self-deasserting pulse reset

모든 reset이 "assert 상태를 유지했다가 deassert"되는 것은 아닙니다. 어떤 하드웨어는 self-clearing bit를 쓰며, 소프트웨어가 trigger를 한 번 쓰면 하드웨어가 내부적으로 pulse를 발생시키고 자동으로 원래 상태로 돌아옵니다. 공식 커널 문서는 이런 경우를 self-deasserting reset control로 설명하고, consumer는 reset_control_reset()으로 pulse를 요청한다고 정의합니다.

모델대표 API특징주의점
level resetreset_control_assert(), reset_control_deassert()소프트웨어가 상태를 유지함assert 상태에서 bus access가 끊길 수 있습니다.
self-deasserting pulse resetreset_control_reset()하드웨어가 pulse를 자체 생성 후 자동 해제pulse 폭과 내부 시퀀스는 하드웨어가 결정합니다.

공식 문서는 일반적으로 self-deasserting pulse reset은 shared에 적합하지 않다고 설명합니다. 어느 consumer가 pulse를 요청해도 연결된 모든 주변장치가 reset되기 때문입니다. 다만 API는 shared pulse reset도 허용하며, 그 경우 첫 trigger만 실제 pulse를 내고 이후에는 모든 consumer가 reset_control_rearm()을 호출할 때까지 추가 pulse를 내지 않는 모델을 제공합니다.

level 유지형 reset과 self-deasserting pulse reset은 API 의미가 다르다 Level reset assert(): 선을 active 상태로 유지 deassert(): 선을 inactive 상태로 유지 소프트웨어가 유지 상태를 책임짐 Self-deasserting pulse reset reset(): trigger 비트를 한 번 씀 하드웨어가 pulse를 발생시키고 자동 해제 pulse 폭과 세부 시퀀스는 provider 하드웨어가 결정
혼용 금지: 공식 문서는 shared reset line에서 reset_control_reset()/reset_control_rearm()를 쓴 경우 reset_control_assert()/reset_control_deassert()와 섞어 쓰지 말라고 경고합니다. 의미 모델이 다르기 때문입니다.

consumer API 전체 지도

기본 getter 몇 개만 알아도 대부분의 드라이버는 동작하지만, 실제 API 표면은 더 넓습니다. exclusive/shared, optional, deasserted convenience, released/acquire-release, bulk array가 모두 존재합니다.

API 계열예시의미
exclusive getterdevm_reset_control_get_exclusive()단독 제어 reset을 얻습니다.
shared getterdevm_reset_control_get_shared()shared semantics의 reset을 얻습니다.
optional getterdevm_reset_control_get_optional_exclusive()DT에 reset이 없으면 NULL을 반환합니다.
deasserted getterdevm_reset_control_get_shared_deasserted()get과 deassert를 묶은 편의 API입니다.
released getterreset_control_get_exclusive_released()exclusive지만 처음에는 acquired 상태가 아닌 핸들을 얻습니다.
acquire/releasereset_control_acquire(), reset_control_release()temporarily exclusive 핸들에 대한 실제 획득/반납입니다.
bulk/arraydevm_reset_control_array_get(), reset_control_bulk_deassert()여러 reset을 한 번에 다룹니다.
#include <linux/reset.h>

struct reset_control *rst;

rst = devm_reset_control_get_exclusive(dev, "core");
if (IS_ERR(rst))
    return dev_err_probe(dev, PTR_ERR(rst),
                         "failed to get core reset\n");

reset_control_assert(rst);
usleep_range(10, 20);
reset_control_deassert(rst);

optional reset도 매우 중요합니다. 공식 문서 기준으로 optional getter는 reset이 DT에 아예 없을 때 NULL을 반환하고, 이후 reset API들은 NULL을 quietly 처리합니다. 즉 "플랫폼 A에는 reset이 있지만 플랫폼 B에는 없다" 같은 변종을 ifdef 없이 같은 driver로 처리할 수 있습니다.

struct reset_control *phy_rst;

phy_rst = devm_reset_control_get_optional_exclusive(dev, "phy");
if (IS_ERR(phy_rst))
    return PTR_ERR(phy_rst);

/* phy_rst가 NULL이어도 API는 조용히 성공 */
reset_control_deassert(phy_rst);
released/acquire-release 패턴: reset을 항상 한 driver가 독점하지 않고, 특정 구간에서만 잠깐 독점해야 하는 경우 *_exclusive_released()reset_control_acquire()/release() 조합이 필요합니다. 평소엔 놓아두고, 특정 민감 구간에서만 잠그는 모델입니다.

shared reset의 정확한 의미와 금지 패턴

shared reset은 가장 자주 오해되는 부분입니다. 공식 문서는 shared reset에 대해 아래 제약을 분명히 둡니다.

규칙이유
deassertassert 호출 수는 균형을 맞춰야 함deassert refcount를 사용하기 때문입니다.
shared reset에서 assert는 실제 물리 assert를 보장하지 않음다른 consumer가 deassert를 유지하고 있을 수 있기 때문입니다.
shared reset line에서는 하드웨어가 reset되었을 것이라고 가정하면 안 됨물리 선이 실제로 assert되지 않았을 수 있기 때문입니다.
shared line에서 pulse reset은 특별한 경우만 허용pulse는 연결된 모든 consumer를 재초기화할 수 있기 때문입니다.
/* 잘못된 기대: shared reset에서 assert()가 실제 reset을 보장한다고 가정 */
reset_control_assert(shared_rst);
/* 여기서 "레지스터 상태가 깨끗해졌을 것"이라고 가정하면 안 됨 */

/* 올바른 접근: shared line은 refcount 의미만 신뢰 */
reset_control_deassert(shared_rst);
/* 최소한 내 consumer가 reset에 잡혀 있지는 않음을 보장받음 */

또한 shared reset line에서 reset_control_reset()reset_control_rearm()를 썼다면, 공식 문서 기준으로 assert/deassert 모델과 섞어 쓰면 안 됩니다. driver가 "이 reset은 level 상태를 관리하는가, pulse를 트리거하는가"를 처음부터 하나로 결정해야 합니다.

array와 bulk reset: 편하지만 순서는 보장되지 않음

하나의 장치가 여러 reset line을 갖는 경우도 흔합니다. 예를 들어 USB 컨트롤러는 core reset, PHY reset, bus reset을 따로 가질 수 있습니다. 이런 경우 bulk API나 array handle이 편리합니다. 하지만 공식 문서는 reset control array API는 개별 control을 어떤 순서로 처리할지 보장하지 않는다고 명시합니다.

API용도주의점
devm_reset_control_array_get()DT에 있는 여러 reset을 opaque handle 하나로 묶기개별 처리 순서를 보장하지 않습니다.
reset_control_bulk_get_*()이름 배열을 기반으로 여러 reset을 얻기strict order bring-up에는 부적합할 수 있습니다.
reset_control_bulk_assert()/deassert()여러 reset을 한 번에 처리MMIO block 간 의존성이 있으면 수동 순서 제어가 낫습니다.
배열 API의 한계: "reset A를 먼저 deassert하고 20us 기다린 뒤 reset B를 deassert해야 한다" 같은 하드웨어에는 array/bulk API를 쓰면 안 됩니다. 이런 경우는 이름별로 개별 reset control을 얻어 driver가 순서를 직접 관리해야 합니다.
static struct reset_control_bulk_data rstcs[] = {
    { .id = "core" },
    { .id = "phy" },
};

ret = devm_reset_control_bulk_get_shared(dev, ARRAY_SIZE(rstcs), rstcs);
if (ret)
    return ret;

ret = reset_control_bulk_deassert(ARRAY_SIZE(rstcs), rstcs);
if (ret)
    return ret;

공식 문서는 array handle에 대해 reset_control_status()를 적용할 수 없다고도 설명합니다. 즉 array는 "묶음 처리"에는 좋지만, 개별 라인의 세밀한 상태 추적과 strict sequencing에는 적합하지 않습니다.

provider 관점: reset_controller_dev와 reset_control_ops

지금까지는 consumer 쪽만 봤지만, reset controller driver를 쓰려면 provider 인터페이스를 이해해야 합니다. 공식 문서는 provider driver가 struct reset_controller_dev를 채우고 reset_controller_register() 또는 devm_reset_controller_register()로 등록한다고 설명합니다. 실제 동작은 struct reset_control_ops의 콜백(Callback)이 담당합니다.

구성 요소역할주의점
reset_controller_devprovider의 핵심 descriptorops, DT xlate 정보, reset 개수, owner 등을 포함합니다.
reset_control_opsassert/deassert/reset/status 콜백 집합모든 콜백이 필수는 아니지만, consumer 기대치와 맞아야 합니다.
of_reset_n_cellsDT phandle 뒤 인자 개수#reset-cells와 맞아야 합니다.
of_xlateDT 인자를 내부 reset ID로 변환단순 인덱스면 helper/기본 xlate로도 충분합니다.
#include <linux/reset-controller.h>

struct my_reset {
    struct reset_controller_dev rcdev;
    void __iomem *base;
    spinlock_t lock;
};

#define to_my_reset(_rcdev) container_of(_rcdev, struct my_reset, rcdev)

static int my_reset_assert(struct reset_controller_dev *rcdev, unsigned long id)
{
    struct my_reset *rst = to_my_reset(rcdev);
    unsigned long flags;
    u32 val;

    spin_lock_irqsave(&rst->lock, flags);
    val = readl(rst->base + 0x100);
    val |= BIT(id);
    writel(val, rst->base + 0x100);
    spin_unlock_irqrestore(&rst->lock, flags);
    return 0;
}

static int my_reset_deassert(struct reset_controller_dev *rcdev, unsigned long id)
{
    struct my_reset *rst = to_my_reset(rcdev);
    unsigned long flags;
    u32 val;

    spin_lock_irqsave(&rst->lock, flags);
    val = readl(rst->base + 0x100);
    val &= ~BIT(id);
    writel(val, rst->base + 0x100);
    spin_unlock_irqrestore(&rst->lock, flags);
    return 0;
}

static const struct reset_control_ops my_reset_ops = {
    .assert = my_reset_assert,
    .deassert = my_reset_deassert,
    .status = my_reset_status,
};

provider는 단순 비트 set/clear만 할 수도 있고, 하드웨어가 self-clearing pulse register를 제공한다면 .reset 콜백을 구현할 수도 있습니다. status 조회를 지원하지 않는 controller도 많으므로 .status는 optional인 경우가 흔합니다.

reset_control_ops 콜백 상세

provider가 구현할 수 있는 콜백과 각 콜백이 호출되는 조건을 정확히 이해해야 consumer의 기대와 provider의 동작이 일치합니다.

콜백호출 조건반환값구현 필수 여부
.assertexclusive: 항상 호출
shared: deassert_count가 1→0일 때만 호출
0 = 성공, 음수 = 에러level reset 지원 시 필수
.deassertexclusive: 항상 호출
shared: deassert_count가 0→1일 때만 호출
0 = 성공, 음수 = 에러level reset 지원 시 필수
.resetexclusive: 항상 호출
shared: triggered_count가 0→1일 때만 호출
0 = 성공, 음수 = 에러pulse reset 지원 시 필수
.statusreset_control_status() 호출 시1 = asserted, 0 = deasserted, 음수 = 에러선택 (미구현 시 -ENOTSUPP)

프레임워크가 .assert/.deassert를 구현하지 않은 provider에 대해 reset_control_reset()이 호출되면, .reset 콜백이 있으면 그것을 호출하고, 없으면 .assert → delay → .deassert 시퀀스를 자동으로 수행합니다. 반대로 .reset만 구현한 provider에 대해 reset_control_assert()가 호출되면 -ENOTSUPP가 반환됩니다.

consumer API 호출이 provider ops 콜백으로 변환되는 과정 Consumer API reset_control_assert(rstc) reset_control_deassert(rstc) reset_control_reset(rstc) reset_control_status(rstc) exclusive 또는 shared 의미에 따라 refcount 관리 후 ops 호출 결정 Framework Core shared: refcount 확인 경계값일 때만 ops 호출 exclusive: 항상 ops 호출 acquired 상태 확인 .reset 미구현 시 assert→delay→deassert 폴백 Provider Ops ops->assert(rcdev, id) ops->deassert(rcdev, id) ops->reset(rcdev, id) ops->status(rcdev, id) 실제 하드웨어 레지스터 조작 (MMIO writel/readl 등) shared reset에서 refcount에 따른 ops 호출 결정 규칙 deassert 호출 시: deassert_count 0→1 이면 ops->deassert() 호출, 그 외에는 count만 증가 assert 호출 시: deassert_count 1→0 이면 ops->assert() 호출, 그 외에는 count만 감소 reset 호출 시: triggered_count 0→1 이면 ops->reset() 호출, 그 외에는 count만 증가 rearm 호출 시: triggered_count 감소, 모든 consumer가 rearm하면 다시 trigger 가능 결론: shared에서는 "첫 번째 deassert"와 "마지막 assert"만 물리적으로 의미가 있습니다

provider 등록과 DT xlate

Device Tree에서 consumer는 보통 resets = <&rst N> 형태로 provider를 참조합니다. provider는 자신이 phandle arguments를 몇 개 받는지 #reset-cells로 정의하고, 커널에서는 of_reset_n_cellsof_xlate로 이를 해석합니다.

static int my_reset_probe(struct platform_device *pdev)
{
    struct my_reset *rst;

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

    spin_lock_init(&rst->lock);
    rst->base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(rst->base))
        return PTR_ERR(rst->base);

    rst->rcdev.owner = THIS_MODULE;
    rst->rcdev.ops = &my_reset_ops;
    rst->rcdev.of_node = pdev->dev.of_node;
    rst->rcdev.of_reset_n_cells = 1;
    rst->rcdev.nr_resets = 64;

    return devm_reset_controller_register(&pdev->dev, &rst->rcdev);
}
rst: reset-controller@ff730000 {
    compatible = "vendor,soc-reset";
    reg = <0x0 0xff730000 0x0 0x1000>;
    #reset-cells = <1>;
};

usb@12340000 {
    compatible = "vendor,my-usb";
    reg = <0x0 0x12340000 0x0 0x10000>;
    resets = <&rst 12>, <&rst 13>;
    reset-names = "core", "phy";
};
DT 속성의미실전 포인트
#reset-cellsprovider가 phandle 뒤에 받는 인자 수provider의 of_reset_n_cells와 맞아야 합니다.
resetsconsumer가 사용하는 reset control 참조여러 개일 경우 순서가 ABI가 됩니다.
reset-namesconsumer가 reset 의미를 이름으로 구분index 의존 코드를 피하게 해 줍니다.
DT의 resets/reset-names가 consumer를 provider에 연결합니다 provider node rst: reset-controller@... #reset-cells = <1> 12 = usb core 13 = usb phy consumer node resets = <&rst 12>, <&rst 13> reset-names = "core", "phy" devm_reset_control_get_exclusive(dev, "core") devm_reset_control_get_optional_exclusive(dev, "phy") driver-side mapping "core" → index 0 "phy" → index 1 이름이 ABI를 설명함 reset-names가 없으면 consumer driver가 인덱스 의미를 암묵적으로 가정해야 합니다 가독성과 유지보수성을 위해 이름을 명확히 두는 편이 낫습니다
DT 작성 팁: 여러 reset line이 있는 IP는 reset-names를 두는 편이 좋습니다. 인덱스 0이 "core"인지 "phy"인지 driver가 암묵적으로 알고 있어야 하는 구조는 코드 리뷰와 보드 파생 관리에 취약합니다.

소비자 드라이버 구현 패턴

실전 consumer driver는 보통 다음 구조를 가집니다. reset만 따로 보는 것이 아니라 regulator, clock, runtime PM, firmware load와 한 흐름으로 다뤄야 합니다.

  1. 필요한 reset handle을 얻습니다.
  2. runtime PM 또는 power domain을 올립니다.
  3. regulator와 clock을 준비합니다.
  4. 필요 시 assert 후 최소 폭을 지킨 뒤 deassert합니다.
  5. MMIO 초기화와 firmware download를 수행합니다.
  6. 에러 시 되돌리는 순서를 명확히 정합니다.
static int my_ip_probe(struct platform_device *pdev)
{
    struct my_ip *priv;
    int ret;

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

    priv->core_rst = devm_reset_control_get_exclusive(&pdev->dev, "core");
    if (IS_ERR(priv->core_rst))
        return dev_err_probe(&pdev->dev, PTR_ERR(priv->core_rst),
                             "failed to get core reset\n");

    ret = pm_runtime_resume_and_get(&pdev->dev);
    if (ret)
        return ret;

    ret = regulator_bulk_enable(priv->num_supplies, priv->supplies);
    if (ret)
        goto err_put_pm;

    ret = clk_bulk_prepare_enable(priv->num_clks, priv->clks);
    if (ret)
        goto err_disable_regs;

    reset_control_assert(priv->core_rst);
    usleep_range(10, 20);

    ret = reset_control_deassert(priv->core_rst);
    if (ret)
        goto err_disable_clks;

    ret = my_ip_hw_init(priv);
    if (ret)
        goto err_assert_rst;

    return 0;

err_assert_rst:
    reset_control_assert(priv->core_rst);
err_disable_clks:
    clk_bulk_disable_unprepare(priv->num_clks, priv->clks);
err_disable_regs:
    regulator_bulk_disable(priv->num_supplies, priv->supplies);
err_put_pm:
    pm_runtime_put(&pdev->dev);
    return ret;
}

위 코드는 보편적인 패턴일 뿐이며, 하드웨어에 따라 assert가 필요 없거나, bootloader가 이미 assert 상태를 보장하거나, reset이 pulse형이어서 reset_control_reset() 하나로 끝날 수도 있습니다. 중요한 것은 데이터시트 요구를 API 의미에 정확히 매핑(Mapping)하는 것입니다.

pulse reset 소비자 패턴

self-deasserting pulse reset을 사용하는 장치의 consumer 코드는 reset_control_reset() 하나로 간결해집니다. 하지만 pulse 완료 후 장치가 정상 상태에 도달하기까지의 대기 시간은 여전히 driver의 책임입니다.

/* pulse reset 소비자 패턴 */
static int my_gpu_probe(struct platform_device *pdev)
{
    struct my_gpu *gpu;
    struct reset_control *rst;
    int ret;

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

    rst = devm_reset_control_get_exclusive(&pdev->dev, "gpu");
    if (IS_ERR(rst))
        return dev_err_probe(&pdev->dev, PTR_ERR(rst),
                             "failed to get gpu reset\n");

    /* clock 활성화 */
    ret = clk_prepare_enable(gpu->core_clk);
    if (ret)
        return ret;

    /* pulse reset: assert→pulse→자동 deassert */
    ret = reset_control_reset(rst);
    if (ret)
        goto err_clk;

    /* pulse 후 GPU 내부 초기화 완료 대기 (데이터시트: 최소 500us) */
    usleep_range(500, 1000);

    /* 이후 레지스터 접근 가능 */
    ret = my_gpu_hw_init(gpu);
    if (ret)
        goto err_clk;

    return 0;

err_clk:
    clk_disable_unprepare(gpu->core_clk);
    return ret;
}

shared reset 소비자 패턴

shared reset consumer는 "내가 deassert를 원한다"는 의사 표시만 할 수 있으며, 물리적 상태가 실제로 변했는지는 보장받지 못합니다. 따라서 shared consumer는 reset 상태에 의존하는 로직을 두지 않는 편이 안전합니다.

/* shared reset 소비자: deasserted convenience getter 사용 */
static int my_audio_probe(struct platform_device *pdev)
{
    struct reset_control *rst;

    /*
     * devm_reset_control_get_shared_deasserted()는
     * get + deassert를 한 번에 수행하는 편의 함수
     * remove 시 자동으로 assert(refcount 감소)가 호출됨
     */
    rst = devm_reset_control_get_shared_deasserted(&pdev->dev, "audio");
    if (IS_ERR(rst))
        return dev_err_probe(&pdev->dev, PTR_ERR(rst),
                             "failed to get audio reset\n");

    /* 이 시점에서 최소한 내 consumer는 deassert를 요청한 상태
     * 다른 consumer가 아직 assert를 유지 중이면 물리 선은 assert일 수 있음
     * 하지만 모든 consumer가 deassert를 원하면 물리 선도 deassert됨 */

    return 0;
}

released/acquire 소비자 패턴

여러 driver가 시간 분할(Time-Division)로 같은 리셋 라인을 독점 사용해야 하는 드문 경우에 사용합니다. 한 driver가 작업 중일 때만 exclusive 권한을 획득하고, 작업 완료 후 반납합니다.

/* released/acquire 패턴: 특정 구간에서만 독점 사용 */
struct reset_control *rst;

/* probe 시: exclusive지만 acquired 상태가 아닌 핸들 획득 */
rst = devm_reset_control_get_exclusive_released(dev, "shared-line");

/* 특정 민감 작업 구간 시작 */
ret = reset_control_acquire(rst);
if (ret) {
    dev_err(dev, "다른 driver가 독점 중\n");
    return ret;
}

/* 이 구간에서는 assert/deassert/reset 호출 가능 */
reset_control_assert(rst);
usleep_range(10, 20);
reset_control_deassert(rst);

/* 민감 작업 완료 후 반납 */
reset_control_release(rst);

clock, regulator, runtime PM과의 순서

reset 문제는 사실상 시퀀스 문제인 경우가 많습니다. reset deassert는 대개 "이제 장치 내부 상태기가 돌아가기 시작한다"는 뜻이므로, 그 전에 bus clock과 core clock이 살아 있어야 하고, 필요한 전압 레일이 안정화되어 있어야 하며, PM domain이 register 접근을 허용해야 합니다.

전형적 순서왜 필요한가
1. PM domain / runtime PM resume레지스터가 응답 가능한 상태 확보
2. regulator enable전압 레일 안정화
3. clock prepare/enablestate machine이 clocked 상태가 되게 함
4. reset deassert 또는 pulse trigger장치를 실제 동작 상태로 전환
5. MMIO 초기화 / firmware load동작 조건 설정
흔한 버그: reset deassert 전에 bus clock이 안 살아 있으면 첫 번째 register access에서 hang이 나거나 timeout만 보일 수 있습니다. 겉보기엔 MMIO 버그나 인터럽트(Interrupt) 문제처럼 보여도 실제 원인은 reset/clock 순서인 경우가 매우 많습니다.
대부분의 주변장치는 "전원 → clock → reset → MMIO" 순서를 요구합니다 runtime PM / genpd register access 가능 regulator on 전압 안정화 clock enable bus/core clock reset deassert state machine 시작 MMIO init / firmware driver bring-up reverse path도 중요: driver stop 시에는 대개 MMIO quiesce → reset assert → clock off → regulator off 순서를 검토해야 합니다

resume 경로에서는 cold boot보다 더 미묘합니다. reset line을 유지해야 하는 장치도 있고, retention register를 믿지 못해 resume 때마다 pulse reset이 필요한 장치도 있습니다. 그래서 "cold boot에서 되면 끝"이 아니라 runtime suspend/resume까지 포함해서 시퀀스를 설계해야 합니다.

provider 내부 구현 패턴과 락

provider driver는 종종 shared MMIO register를 다룹니다. 예를 들어 reset bit와 clock gate bit가 같은 register bank에 있을 수 있고, 여러 reset line이 하나의 32비트 레지스터를 공유할 수 있습니다. 이때 provider가 자체 락을 두지 않으면 경쟁 조건(Race Condition)으로 잘못된 비트가 깨질 수 있습니다.

provider 구현 이슈왜 문제인가대응
공유 register RMW 경쟁동시에 다른 reset bit를 바꿀 때 비트 손실 가능spinlock 등으로 보호
self-clearing pulse bit pollingreset pulse 완료 시점을 모르고 다음 단계로 넘어갈 수 있음status bit 또는 하드웨어 완료 조건 확인
status 미지원consumer가 실제 asserted 여부를 읽지 못함API 의미를 문서화하고, 필요시 polling을 다른 레지스터로 보완
복수 line을 한 control로 묶음consumer가 "내 장치만 reset"이라 생각하기 쉬움binding과 문서에서 관계를 명확히 표현
static int my_reset_reset(struct reset_controller_dev *rcdev, unsigned long id)
{
    struct my_reset *rst = to_my_reset(rcdev);
    unsigned long flags;

    /* self-clearing pulse trigger 예시 */
    spin_lock_irqsave(&rst->lock, flags);
    writel(BIT(id), rst->base + 0x104);
    spin_unlock_irqrestore(&rst->lock, flags);

    /* 필요한 경우 완료 비트 polling */
    return 0;
}

provider가 .reset를 구현한다고 해서 항상 .assert/.deassert도 구현해야 하는 것은 아닙니다. 하드웨어가 pulse trigger만 제공한다면 .reset만 의미 있을 수 있습니다. 반대로 level reset만 지원하면 .assert/.deassert만 구현하고 .reset는 비워 둘 수 있습니다.

고급 API: released, acquire/release, rearm, status

실전에서는 드문 편이지만, 아래 API들을 알아두면 framework의 의미가 더 분명해집니다.

API의미언제 유용한가
reset_control_get_exclusive_released()exclusive지만 처음엔 acquired가 아닌 핸들평소엔 놓아두고 특정 구간에만 잠깐 독점할 때
reset_control_acquire()/release()temporarily exclusive reset control 잠금(Lock)/해제여러 consumer가 시간 분할로 독점 사용
reset_control_rearm()shared self-deasserting pulse reset을 다시 pulse 가능 상태로 돌림공유 pulse reset에서 다음 consumer가 다시 한 번 pulse를 쏠 수 있게 할 때
reset_control_status()assert 상태 조회provider가 지원하는 경우에만 의미 있음
rearm의 의미: shared pulse reset에서 실제 pulse는 첫 요청에만 발생합니다. 이후 다시 pulse가 필요하다면 모든 consumer가 reset_control_rearm()을 균형 있게 호출해야 합니다. 이 모델은 "probe나 resume 전에 한 번만 확실히 reset되면 된다"는 장치에 적합합니다.

부트로더(Bootloader) 핸드오프와 리셋 상태

부트로더(U-Boot, UEFI 등)가 커널에 제어권을 넘길 때, 주변장치의 리셋 상태는 부트로더가 설정한 그대로 남아 있습니다. 이 "핸드오프(Handoff)" 상태는 driver probe 시 중요한 전제 조건이 됩니다.

핸드오프 시나리오리셋 상태드라이버 설계 시 고려사항
부트로더가 장치를 사용하고 있었음 (예: UART 콘솔)deasserted, 동작 중probe에서 reset을 건드리면 부트 메시지가 끊길 수 있음. 필요 시 graceful takeover 패턴 사용
부트로더가 장치를 초기화했지만 사용 안 함불확실 (부트로더 구현에 따라 다름)probe에서 full reset 시퀀스(assert → delay → deassert)를 실행하는 것이 안전
부트로더가 장치를 전혀 건드리지 않음POR(Power-On Reset) 기본값POR 기본값이 asserted인지 deasserted인지 데이터시트로 확인 필요
Secure 부트로더(TF-A 등)가 보안 설정을 적용secure world에서만 접근 가능한 리셋일 수 있음PSCI나 SCMI를 통한 간접 제어가 필요할 수 있음
/* 부트로더 핸드오프를 고려한 probe 패턴 */

static int my_uart_probe(struct platform_device *pdev)
{
    struct reset_control *rst;
    int ret;

    rst = devm_reset_control_get_optional_exclusive(&pdev->dev, "uart");
    if (IS_ERR(rst))
        return PTR_ERR(rst);

    if (rst) {
        /*
         * 부트로더가 이미 deassert한 상태일 수 있지만,
         * known-good state를 보장하기 위해 assert → deassert 수행.
         * 부트 콘솔로 사용 중이었다면 이 구간에서 출력이 잠깐 끊김.
         */
        reset_control_assert(rst);
        usleep_range(10, 20);
        ret = reset_control_deassert(rst);
        if (ret)
            return ret;

        /* deassert 후 UART 안정화 시간 */
        usleep_range(100, 200);
    }

    /* 이후 MMIO 초기화 */
    return my_uart_hw_init(pdev);
}
부트로더 의존성 함정: "cold boot에서는 정상인데 warm reboot에서 실패한다"는 증상은 대부분 부트로더 핸드오프 의존성 때문입니다. 부트로더가 reset을 특정 상태로 설정해 놓았는데, warm reboot에서는 부트로더가 그 설정을 건너뛰는 경우가 있습니다. driver probe에서 reset 상태를 가정하지 말고 명시적으로 설정하는 것이 방어적 프로그래밍(Defensive Programming)입니다.

Device Tree binding 세부사항

consumer 노드가 여러 reset을 가질 때는 reset-names와 함께 쓰는 것이 좋고, provider 노드는 #reset-cells를 명확히 해야 합니다. 또한 어떤 SoC에서는 하나의 상위 노드가 clock provider와 reset provider를 동시에 제공하기도 합니다. 이 경우 같은 MMIO block에서 clock과 reset이 함께 관리되므로, CCF와 reset framework의 provider 드라이버가 분리되어도 register locking과 probe 순서는 신중히 설계해야 합니다.

ccu: clock-reset-controller@ff760000 {
    compatible = "vendor,soc-ccu";
    reg = <0x0 0xff760000 0x0 0x1000>;
    #clock-cells = <1>;
    #reset-cells = <1>;
};

ethernet@10020000 {
    compatible = "vendor,my-mac";
    clocks = <&ccu 4>, <&ccu 5>;
    clock-names = "stmmaceth", "ptp_ref";
    resets = <&ccu 9>;
    reset-names = "stmmaceth";
};
실수왜 문제인가더 나은 방식
resets만 있고 reset-names가 없음driver가 인덱스 의미를 암묵적으로 가정해야 함이름으로 의미를 명시
#reset-cells와 provider 구현 불일치phandle xlate 실패DT와 of_reset_n_cells를 일치시킴
strict sequence 장치에 array API 사용순서 보장(Ordering) 없음이름별로 개별 handle을 가져옴

디버깅 절차와 관찰 포인트

reset 문제는 종종 "device not responding", "timeout", "register read returns 0xffffffff", "DMA never starts" 같은 비특이적 증상으로 나타납니다. 따라서 reset 자체만 보기보다, reset 전후의 power/clock/MMIO/interrupt 상태를 함께 기록하는 편이 훨씬 효과적입니다.

  1. DT binding 확인
    resets, reset-names, provider #reset-cells가 실제 driver 기대와 맞는지 먼저 확인합니다.
  2. probe ordering 확인
    provider가 먼저 probe되었는지, consumer가 -EPROBE_DEFER를 적절히 처리하는지 확인합니다.
  3. 시퀀스 로깅
    regulator on, clock enable, reset deassert, MMIO init 순서를 로그로 남깁니다.
  4. 딜레이 검증
    assert 폭과 deassert 후 안정화 시간이 데이터시트 최소값을 만족하는지 확인합니다.
  5. resume와 cold boot 비교
    cold boot에서만 성공하고 resume에서 실패하면 retention/reset 복구 문제가 많습니다.
# DT에서 reset 관련 노드 찾기
grep -R "reset-names" /sys/firmware/devicetree/base 2>/dev/null

# dmesg에서 provider/consumer 초기화 추적
dmesg | grep -i -E "reset|deassert|assert|probe defer|clock|regulator"

# 소비자 노드의 리소스 확인
grep -R "resets" /sys/firmware/devicetree/base 2>/dev/null

provider가 status를 지원하면 훨씬 편하지만, 많은 reset controller는 status를 제공하지 않습니다. 이 경우 reset 상태는 MMIO side effect, register accessibility, interrupt arrival 여부로 간접 확인하는 수밖에 없습니다.

흔한 실패 패턴과 원인 추적

증상가능한 원인먼저 볼 것보통의 수정 방향
failed to get resetDT 이름 불일치, provider 미등록, phandle 인덱스 오류reset-names, provider probe, -EPROBE_DEFERdev_err_probe() 사용, DT binding 정리
deassert 후 첫 MMIO read에서 hangclock off, PM domain off, 전압 미안정clock/regulator/runtime PM 순서reset deassert 전 bring-up 순서 수정
shared reset에서 갑자기 다른 장치도 죽음pulse reset 또는 exclusive 가정 오류line 공유 여부, provider topologyshared semantics 재검토, 전체 block 재설계
cold boot는 되는데 resume에서 실패resume 시 reset/clock 복원 순서 누락PM callbacks, retention 여부resume 재초기화 경로 추가
bulk API로는 안 되고 수동 순서에선 됨reset 순서 의존 하드웨어데이터시트 sequence 요구array/bulk 대신 개별 reset control 사용
assert/deassert는 되는데 reset()은 이상함pulse형과 level형 의미 혼동provider의 .reset 구현 방식driver가 사용하는 API 모델 통일
가장 자주 놓치는 것: reset 문제를 reset API 호출 수만 세면서 디버깅하면 실패합니다. 실제 원인은 provider 미등록, clock off, regulator off, 잘못된 PM ordering, bootloader handoff 의존성인 경우가 훨씬 많습니다.

끝까지 따라가는 상태 전이 예제

아래는 USB controller가 cold boot와 runtime resume에서 reset을 어떻게 다르게 다뤄야 하는지 보여 주는 예제입니다.

시점사건reset 관점다른 서브시스템사용자에게 보이는 증상
T0bootloader가 core clock만 켜 둠reset 상태는 firmware 기본값bootloader handoff초기 콘솔은 살아 보일 수 있음
T1provider probereset_controller_dev 등록clock/reset provider 초기화consumer는 아직 defer 가능
T2USB driver probecore, phy reset handle 획득DT parsebinding 틀리면 probe 실패
T3power + clock 준비아직 reset 유지 또는 pulse 전runtime PM, regulator, CCF순서가 틀리면 MMIO hang
T4reset deassert / pulse장치 내부 상태기 시작clock active이후 firmware load 가능
T5runtime suspendassert 유지 여부는 retention 정책에 따름clock off, regulator off 가능resume 설계 미흡 시 장치 사망
T6runtime resume필요 시 reset 재트리거 또는 deassert 재확인PM restorecold boot만 성공하는 버그가 드러남
cold boot와 runtime resume를 가로지르는 reset 상태 전이 T0 bootloader partial setup T1 provider probe rcdev 등록 T2 consumer probe reset get T3 power + clock ready T4 reset release MMIO init T5 suspend policy-dependent T6 resume re-sync resume는 cold boot의 축소판이 아니라 별도 시퀀스입니다. reset을 다시 걸어야 하는지, 유지해야 하는지, shared semantics가 유지되는지까지 따로 검토해야 합니다.
상태 전이 해석 팁: cold boot만 성공하는 드라이버는 종종 bootloader가 남겨 둔 reset/clock 상태에 우연히 의존합니다. 진짜 검증은 module reload, runtime suspend/resume, probe error path에서 reset 상태가 스스로 복원되는지 보는 것입니다.

프레임워크 내부 구조: reset_control 핸들과 refcount 메커니즘

consumer가 devm_reset_control_get_exclusive() 등을 호출하면 프레임워크 내부에서는 struct reset_control 핸들이 생성됩니다. 이 핸들은 provider의 reset_controller_dev와 reset line ID를 내부적으로 연결하며, shared/exclusive 의미와 refcount를 관리합니다. 핵심 구조체(Struct)와 그 관계를 이해하면 디버깅이 훨씬 수월해집니다.

/* include/linux/reset-controller.h 핵심 구조 (단순화) */

struct reset_controller_dev {
    const struct reset_control_ops *ops;
    struct module *owner;
    struct list_head list;          /* 전역 provider 리스트 */
    struct list_head reset_control_head; /* 이 provider에서 발급된 핸들 목록 */
    struct device_node *of_node;
    int of_reset_n_cells;
    int (*of_xlate)(struct reset_controller_dev *rcdev,
                    const struct of_phandle_args *reset_spec);
    unsigned int nr_resets;
};

/* drivers/reset/core.c 내부 구조 (단순화) */

struct reset_control {
    struct reset_controller_dev *rcdev;
    struct list_head list;          /* provider의 reset_control_head에 연결 */
    unsigned int id;                /* reset line index */
    struct kref refcnt;             /* 같은 line의 shared handle 참조 카운트 */
    bool shared;                    /* shared semantics 여부 */
    bool array;                     /* array/bulk handle 여부 */
    atomic_t deassert_count;        /* shared deassert refcount */
    atomic_t triggered_count;       /* shared pulse trigger count */
    bool acquired;                  /* released/acquire 모델에서 획득 상태 */
    bool deasserted;                /* deasserted convenience getter */
};
reset framework 내부: provider 등록에서 consumer 핸들 발급까지 전역 provider 리스트 reset_controller_list mutex로 보호됨 provider probe 시 등록 reset_controller_dev A ops = &vendor_reset_ops nr_resets = 32 of_reset_n_cells = 1 reset_control_head → [핸들 리스트] reset_controller_dev B ops = &simple_reset_ops nr_resets = 8 of_reset_n_cells = 1 reset_control (shared) id = 5, shared = true deassert_count = 2 refcnt = 2 (consumer 2개) reset_control (excl) id = 12, shared = false acquired = true refcnt = 1 Consumer X shared handle ref Consumer Y shared handle ref Consumer Z exclusive handle

같은 reset line(같은 provider + 같은 id)에 대해 shared 요청이 들어오면 프레임워크는 기존 reset_control 핸들의 refcnt를 증가시키고 같은 핸들을 반환합니다. exclusive 요청이 들어오면 해당 line에 이미 다른 핸들이 있으면 -EBUSY를 반환합니다. 이 구분은 __reset_control_get() 내부에서 이루어지며, 한 번 shared로 열린 line은 exclusive로 재요청할 수 없고 그 반대도 마찬가지입니다.

__reset_control_get() 내부 흐름 상세

consumer가 devm_reset_control_get_*()를 호출하면 프레임워크 내부에서는 다음 단계를 거칩니다.

  1. DT 파싱(Parsing): consumer의 resets 속성에서 phandle과 인자를 추출합니다. reset-names가 있으면 이름으로 인덱스를 결정합니다.
  2. provider 탐색: 전역 reset_controller_list에서 phandle에 해당하는 reset_controller_dev를 찾습니다. 없으면 -EPROBE_DEFER를 반환합니다.
  3. xlate: provider의 of_xlate 콜백(또는 기본 xlate)을 호출하여 DT 인자를 내부 reset line ID로 변환합니다.
  4. 기존 핸들 검색: 같은 provider + 같은 ID를 가진 reset_control이 이미 있는지 reset_control_head 리스트를 순회합니다.
  5. 호환성 검사: 기존 핸들이 있으면 shared/exclusive 호환성을 확인합니다. 불호환이면 -EBUSY입니다.
  6. 핸들 생성 또는 재사용: 호환되면 refcnt를 증가시키고, 새로 만들어야 하면 reset_control을 할당하여 리스트에 추가합니다.
/* __reset_control_get() 핵심 로직 (단순화) */

static struct reset_control *
__reset_control_get(struct device *dev, const char *id,
                    int index, bool shared, bool optional,
                    bool acquired)
{
    struct reset_controller_dev *rcdev;
    struct reset_control *rstc;
    int rstc_id;

    /* 1. DT에서 provider phandle + 인자 추출 */
    /* 2. provider를 전역 리스트에서 탐색 */
    rcdev = __reset_controller_find(args.np);
    if (!rcdev)
        return optional ? NULL : ERR_PTR(-EPROBE_DEFER);

    /* 3. xlate로 reset line ID 결정 */
    rstc_id = rcdev->of_xlate ? rcdev->of_xlate(rcdev, &args)
                               : args.args[0];

    /* 4-5. 기존 핸들 검색 + 호환성 검사 */
    list_for_each_entry(rstc, &rcdev->reset_control_head, list) {
        if (rstc->id == rstc_id) {
            if (!rstc->shared && !shared)
                return ERR_PTR(-EBUSY);  /* 이미 exclusive */
            if (rstc->shared != shared)
                return ERR_PTR(-EBUSY);  /* 타입 불일치 */
            kref_get(&rstc->refcnt);
            return rstc;  /* 기존 핸들 재사용 */
        }
    }

    /* 6. 새 핸들 생성 */
    rstc = kzalloc(sizeof(*rstc), GFP_KERNEL);
    rstc->rcdev = rcdev;
    rstc->id = rstc_id;
    rstc->shared = shared;
    rstc->acquired = acquired;
    kref_init(&rstc->refcnt);
    list_add(&rstc->list, &rcdev->reset_control_head);

    return rstc;
}

exclusive/shared 요청 호환성 매트릭스

기존 핸들새 요청: exclusive새 요청: shared새 요청: exclusive_released
없음새 핸들 생성 (acquired)새 핸들 생성새 핸들 생성 (not acquired)
exclusive (acquired)-EBUSY-EBUSY-EBUSY
exclusive (released)-EBUSY-EBUSYrefcnt 증가
shared-EBUSYrefcnt 증가-EBUSY
deassert_count 추적: shared reset의 deassert_countreset_control_deassert() 호출 시 증가하고 reset_control_assert() 호출 시 감소합니다. count가 0에서 1로 될 때 provider의 .deassert 콜백이 실제로 호출되고, 1에서 0으로 될 때 .assert가 호출됩니다. 중간 값에서는 콜백이 호출되지 않습니다.

실전 커널 코드에서 보는 reset controller 드라이버 사례

리눅스 메인라인에는 수십 개의 reset controller provider 드라이버가 있습니다. 대표적인 SoC 벤더별 구현을 살펴보면 프레임워크 사용 패턴이 더 분명해집니다.

SoC / 벤더소스 경로reset 모델특이사항
Allwinner (sunxi)drivers/reset/reset-sunxi.clevel (bit set/clear)clock-reset 통합 CRU, #reset-cells = 1
Rockchipdrivers/clk/rockchip/rst-*.clevel (bit set/clear)CCF provider와 같은 module에서 reset 등록
TI (OMAP/AM)drivers/reset/reset-ti-sci.cSCI message 기반SCI firmware로 reset 요청을 전달하는 간접 모델
STM32drivers/reset/reset-stm32mp1.clevel + self-clearingRCC 레지스터에서 clock과 reset을 함께 관리
i.MX (NXP)drivers/reset/reset-imx7.cSCU/SRC 기반System Reset Controller 하드웨어 블록
MediaTekdrivers/clk/mediatek/reset.clevel (set/clear 레지스터 분리)SET/CLR 레지스터 쌍으로 atomic bit 조작
Simple resetdrivers/reset/reset-simple.clevel (공통 bit 패턴)범용 single-register reset에 재사용 가능한 helper

i.MX (NXP): System Reset Controller(SRC) 기반 구현

NXP i.MX 시리즈는 SRC(System Reset Controller)라는 전용 하드웨어 블록을 통해 리셋을 관리합니다. i.MX7과 i.MX8 이상에서는 SRC가 여러 개의 독립적인 리셋 도메인을 제공하며, 각 도메인은 개별 슬라이스(Slice) 레지스터로 제어됩니다. 특히 i.MX8M 계열에서는 GPU, VPU, Display 등 미디어 블록에 대한 리셋이 전원 도메인(Power Domain)과 긴밀하게 연결되어 있어, reset controller driver가 genpd(Generic Power Domain)와 협력해야 합니다.

/* drivers/reset/reset-imx7.c 핵심 패턴 (단순화) */

struct imx7_src {
    struct reset_controller_dev rcdev;
    struct regmap *regmap;
    const struct imx7_src_signal *signals;
};

struct imx7_src_signal {
    unsigned int offset;     /* 레지스터 오프셋 */
    unsigned int bit;        /* 비트 위치 */
};

static int imx7_reset_set(struct reset_controller_dev *rcdev,
                          unsigned long id, bool assert)
{
    struct imx7_src *src = container_of(rcdev, struct imx7_src, rcdev);
    const struct imx7_src_signal *signal = &src->signals[id];

    /* regmap은 내부적으로 락을 제공하므로 별도 spinlock 불필요 */
    return regmap_update_bits(src->regmap,
                              signal->offset,
                              BIT(signal->bit),
                              assert ? BIT(signal->bit) : 0);
}

static const struct reset_control_ops imx7_reset_ops = {
    .assert   = imx7_reset_assert,
    .deassert = imx7_reset_deassert,
};

i.MX SRC의 특징은 리셋 신호마다 레지스터 오프셋과 비트 위치가 다르다는 점입니다. 단순한 연속 비트맵이 아니라 SoC 데이터시트에 정의된 신호 테이블(Signal Table)을 기반으로 매핑(Mapping)합니다. 또한 일부 리셋 비트는 self-clearing이고, 일부는 level-hold 방식이어서 provider 내부에서 두 가지 모델을 모두 처리해야 합니다.

STM32: RCC 기반 clock-reset 통합 관리

STMicroelectronics의 STM32MP1 시리즈는 RCC(Reset and Clock Controller)라는 하드웨어 블록이 clock 게이팅(Gating)과 reset 제어를 모두 담당합니다. 같은 MMIO 레지스터 뱅크(Register Bank)에서 clock enable/disable 비트와 reset assert/deassert 비트가 함께 존재하므로, provider 드라이버는 CCF(Common Clock Framework)와 reset framework를 동시에 등록합니다.

/* STM32MP1 RCC: SET/CLR 레지스터 쌍 기반 리셋 */

static int stm32_reset_assert(struct reset_controller_dev *rcdev,
                               unsigned long id)
{
    struct stm32_reset_data *data = to_stm32_reset_data(rcdev);
    int reg_width = sizeof(u32);
    int bank = id / (reg_width * 8);       /* 레지스터 뱅크 인덱스 */
    int offset = id % (reg_width * 8);    /* 뱅크 내 비트 오프셋 */

    /* SET 레지스터에 쓰면 해당 비트가 1(asserted)로 설정됨 */
    writel(BIT(offset),
           data->membase + data->reset_set_offset + (bank * reg_width));

    return 0;
}

static int stm32_reset_deassert(struct reset_controller_dev *rcdev,
                                 unsigned long id)
{
    struct stm32_reset_data *data = to_stm32_reset_data(rcdev);
    int reg_width = sizeof(u32);
    int bank = id / (reg_width * 8);
    int offset = id % (reg_width * 8);

    /* CLR 레지스터에 쓰면 해당 비트가 0(deasserted)으로 클리어됨 */
    writel(BIT(offset),
           data->membase + data->reset_clr_offset + (bank * reg_width));

    return 0;
}

STM32의 SET/CLR 레지스터 쌍 방식은 하드웨어가 원자적(Atomic) 비트 조작을 보장하므로, RMW(Read-Modify-Write) 없이 spinlock 없이도 안전합니다. 이 방식은 MediaTek SoC에서도 동일하게 사용됩니다.

Rockchip: CRU에서의 clock-reset 통합

Rockchip SoC(RK3399, RK3588 등)는 CRU(Clock and Reset Unit) 블록이 clock과 reset을 모두 관리합니다. Rockchip의 특징은 reset controller driver가 CCF provider 모듈 안에 함께 구현된다는 점입니다. 즉 drivers/clk/rockchip/ 디렉터리에 reset 관련 코드가 포함됩니다.

/* Rockchip CRU 리셋: 쓰기 마스크(write mask) 방식 */

static int rockchip_reset_assert(struct reset_controller_dev *rcdev,
                                  unsigned long id)
{
    struct rockchip_cru *cru = container_of(rcdev, struct rockchip_cru, rcdev);
    int bank = id / 16;       /* 16비트 데이터 + 16비트 쓰기 마스크 */
    int offset = id % 16;

    /*
     * Rockchip 레지스터 규약: 상위 16비트는 write mask
     * 해당 비트의 mask를 1로 설정하고, 데이터 비트도 1로 설정하면
     * 해당 비트만 1(assert)로 변경됨 — RMW 불필요
     */
    writel(BIT(offset + 16) | BIT(offset),
           cru->base + cru->reset_offset + (bank * 4));

    return 0;
}

static int rockchip_reset_deassert(struct reset_controller_dev *rcdev,
                                    unsigned long id)
{
    struct rockchip_cru *cru = container_of(rcdev, struct rockchip_cru, rcdev);
    int bank = id / 16;
    int offset = id % 16;

    /* mask 비트만 1, 데이터 비트는 0 → 해당 비트만 0(deassert)으로 변경 */
    writel(BIT(offset + 16),
           cru->base + cru->reset_offset + (bank * 4));

    return 0;
}

Rockchip의 쓰기 마스크(Write Mask) 방식은 하드웨어가 atomic write를 보장합니다. 32비트 레지스터의 상위 16비트가 write enable mask이고 하위 16비트가 실제 데이터이므로, 한 번의 writel로 특정 비트만 안전하게 변경할 수 있습니다. 이 방식은 spinlock 없이도 비트 경쟁을 완전히 방지합니다.

Allwinner (sunxi): 단순 비트맵 RMW 패턴

Allwinner SoC(H3, A64, H616 등)의 리셋 컨트롤러는 가장 기본적인 형태입니다. 연속된 32비트 레지스터의 각 비트가 하나의 리셋 라인에 대응하며, 일반적인 read-modify-write 패턴을 사용합니다. 이 방식은 spinlock 보호가 필수입니다.

/* Allwinner H3 Device Tree 예시 */
ccu: clock@1c20000 {
    compatible = "allwinner,sun8i-h3-ccu";
    reg = <0x01c20000 0x400>;
    clocks = <&osc24M>, <&osc32k>;
    clock-names = "hosc", "losc";
    #clock-cells = <1>;
    #reset-cells = <1>;
};

emac: ethernet@1c30000 {
    compatible = "allwinner,sun8i-h3-emac";
    reg = <0x01c30000 0x10000>;
    clocks = <&ccu 27>;  /* CLK_BUS_EMAC */
    resets = <&ccu 12>;  /* RST_BUS_EMAC */
    reset-names = "stmmaceth";
};
SoC별 레지스터 방식 비교: Rockchip의 write mask, STM32/MediaTek의 SET/CLR 분리, Allwinner의 단순 RMW는 모두 같은 논리적 동작(비트 set/clear)을 구현하지만, 하드웨어 원자성(Atomicity) 보장 수준이 다릅니다. provider 드라이버를 작성할 때 SoC가 어떤 방식을 사용하는지 반드시 확인하고, 필요한 경우에만 소프트웨어 락(Lock)을 추가해야 합니다.
SoC 벤더레지스터 방식하드웨어 원자성SW 락 필요레지스터 밀도
RockchipWrite Mask (상위 16비트 = mask)지원불필요레지스터당 16개 리셋 라인
STM32, MediaTekSET/CLR 레지스터 쌍지원불필요레지스터당 32개 리셋 라인
Allwinner단일 레지스터 RMW미지원spinlock 필수레지스터당 32개 리셋 라인
NXP i.MX개별 신호 테이블 기반regmap 의존regmap 내부 락신호마다 다름
TI (SCI 기반)SCI 메시지 통신SCI FW 보장불필요해당 없음 (메시지 기반)
Qualcommself-clearing trigger + status부분적필요할 수 있음신호마다 다름

reset-simple: 범용 비트맵(Bitmap) reset controller

reset-simple은 "레지스터의 각 비트가 하나의 reset line을 제어한다"는 가장 흔한 패턴을 공통 드라이버로 추출한 것입니다. 많은 SoC reset controller가 이 패턴을 따르므로, 신규 SoC를 지원할 때 reset-simple을 먼저 검토하면 boilerplate를 크게 줄일 수 있습니다.

/* drivers/reset/reset-simple.c 핵심 패턴 (단순화) */

static int reset_simple_set(struct reset_controller_dev *rcdev,
                            unsigned long id, bool assert)
{
    struct reset_simple_data *data = to_reset_simple_data(rcdev);
    unsigned int reg = id / 32 * 4;  /* 32비트 레지스터당 32개 line */
    unsigned int bit = id % 32;
    unsigned long flags;
    u32 val;

    spin_lock_irqsave(&data->lock, flags);
    val = readl(data->membase + reg);

    if (assert ^ data->active_low)
        val |= BIT(bit);
    else
        val &= ~BIT(bit);

    writel(val, data->membase + reg);
    spin_unlock_irqrestore(&data->lock, flags);

    return 0;
}

static const struct reset_control_ops reset_simple_ops = {
    .assert   = reset_simple_assert,
    .deassert = reset_simple_deassert,
    .status   = reset_simple_status,
};
active_low 주의: 일부 SoC는 "비트를 1로 쓰면 deassert"이고 다른 SoC는 "비트를 1로 쓰면 assert"입니다. reset-simpleactive_low 플래그가 이 차이를 흡수합니다. 데이터시트를 읽을 때 polarity를 반드시 확인하세요.

SET/CLR 레지스터 분리 패턴 (MediaTek 스타일)

일부 SoC는 read-modify-write 대신 SET 레지스터와 CLR 레지스터를 분리해서 atomic bit 조작을 지원합니다. 이 방식은 spinlock 없이도 비트 경쟁을 피할 수 있다는 장점이 있습니다.

/* MediaTek 스타일: SET/CLR 레지스터 분리 */

static int mtk_reset_assert(struct reset_controller_dev *rcdev,
                             unsigned long id)
{
    struct mtk_reset *data = container_of(rcdev, struct mtk_reset, rcdev);
    unsigned int reg = id / 32 * 4;
    unsigned int bit = id % 32;

    /* SET 레지스터에 쓰면 해당 비트만 1로 변경 - RMW 불필요 */
    writel(BIT(bit), data->regbase + data->rst_set_ofs + reg);

    return 0;
}

static int mtk_reset_deassert(struct reset_controller_dev *rcdev,
                               unsigned long id)
{
    struct mtk_reset *data = container_of(rcdev, struct mtk_reset, rcdev);
    unsigned int reg = id / 32 * 4;
    unsigned int bit = id % 32;

    /* CLR 레지스터에 쓰면 해당 비트만 0으로 변경 */
    writel(BIT(bit), data->regbase + data->rst_clr_ofs + reg);

    return 0;
}
레지스터 방식assert 동작deassert 동작락 필요성대표 SoC
단일 레지스터 RMWread → set bit → writeread → clear bit → writespinlock 필수Allwinner, Rockchip
SET/CLR 분리SET 레지스터에 비트 writeCLR 레지스터에 비트 write락 불필요 (하드웨어 atomic)MediaTek, 일부 TI
self-clearing trigger해당 없음해당 없음pulse trigger bit write일부 Qualcomm, custom IP

실전 리셋 컨트롤러 드라이버 전체 코드

아래는 가상의 SoC reset controller를 위한 완전한 provider 드라이버 예제입니다. level reset(assert/deassert), pulse reset(reset), status 조회를 모두 지원하며, Device Tree 바인딩, probe/remove, 모듈 등록까지 포함합니다.

// SPDX-License-Identifier: GPL-2.0-only
/*
 * Reset controller driver for Example SoC
 * 레지스터 레이아웃:
 *   0x000: RESET_CTRL  - 리셋 제어 레지스터 (비트별 assert/deassert)
 *   0x004: RESET_STAT  - 리셋 상태 레지스터 (비트별 현재 상태)
 *   0x008: RESET_PULSE - 셀프 클리어링 펄스 트리거 레지스터
 *   각 레지스터는 32개 리셋 라인을 제어하며, 최대 64개 라인을 지원
 */

#include <linux/delay.h>
#include <linux/device.h>
#include <linux/err.h>
#include <linux/io.h>
#include <linux/mod_devicetable.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/reset-controller.h>
#include <linux/spinlock.h>

#define EXAMPLE_RESET_CTRL    0x000
#define EXAMPLE_RESET_STAT    0x004
#define EXAMPLE_RESET_PULSE   0x008
#define EXAMPLE_NR_RESETS     64
#define EXAMPLE_REGS_PER_BANK 32

struct example_reset {
    struct reset_controller_dev rcdev;
    void __iomem *base;
    spinlock_t lock;
};

static inline struct example_reset *
to_example_reset(struct reset_controller_dev *rcdev)
{
    return container_of(rcdev, struct example_reset, rcdev);
}

static int example_reset_assert(struct reset_controller_dev *rcdev,
                                unsigned long id)
{
    struct example_reset *rst = to_example_reset(rcdev);
    unsigned int bank = id / EXAMPLE_REGS_PER_BANK;
    unsigned int bit = id % EXAMPLE_REGS_PER_BANK;
    unsigned long flags;
    u32 val;

    spin_lock_irqsave(&rst->lock, flags);
    val = readl(rst->base + EXAMPLE_RESET_CTRL + bank * 0x10);
    val |= BIT(bit);   /* active-high: 1 = asserted */
    writel(val, rst->base + EXAMPLE_RESET_CTRL + bank * 0x10);
    spin_unlock_irqrestore(&rst->lock, flags);

    return 0;
}

static int example_reset_deassert(struct reset_controller_dev *rcdev,
                                  unsigned long id)
{
    struct example_reset *rst = to_example_reset(rcdev);
    unsigned int bank = id / EXAMPLE_REGS_PER_BANK;
    unsigned int bit = id % EXAMPLE_REGS_PER_BANK;
    unsigned long flags;
    u32 val;

    spin_lock_irqsave(&rst->lock, flags);
    val = readl(rst->base + EXAMPLE_RESET_CTRL + bank * 0x10);
    val &= ~BIT(bit);  /* 0 = deasserted */
    writel(val, rst->base + EXAMPLE_RESET_CTRL + bank * 0x10);
    spin_unlock_irqrestore(&rst->lock, flags);

    return 0;
}

static int example_reset_reset(struct reset_controller_dev *rcdev,
                               unsigned long id)
{
    struct example_reset *rst = to_example_reset(rcdev);
    unsigned int bank = id / EXAMPLE_REGS_PER_BANK;
    unsigned int bit = id % EXAMPLE_REGS_PER_BANK;

    /* PULSE 레지스터에 비트를 쓰면 하드웨어가 자동으로 펄스 생성 후 클리어 */
    writel(BIT(bit), rst->base + EXAMPLE_RESET_PULSE + bank * 0x10);

    /* 펄스 완료 대기 — 하드웨어가 클리어할 때까지 */
    usleep_range(1, 5);

    return 0;
}

static int example_reset_status(struct reset_controller_dev *rcdev,
                                unsigned long id)
{
    struct example_reset *rst = to_example_reset(rcdev);
    unsigned int bank = id / EXAMPLE_REGS_PER_BANK;
    unsigned int bit = id % EXAMPLE_REGS_PER_BANK;
    u32 val;

    val = readl(rst->base + EXAMPLE_RESET_STAT + bank * 0x10);

    /* 1 = asserted (리셋 중), 0 = deasserted (정상) */
    return !!(val & BIT(bit));
}

static const struct reset_control_ops example_reset_ops = {
    .assert   = example_reset_assert,
    .deassert = example_reset_deassert,
    .reset    = example_reset_reset,
    .status   = example_reset_status,
};

static int example_reset_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct example_reset *rst;

    rst = devm_kzalloc(dev, sizeof(*rst), GFP_KERNEL);
    if (!rst)
        return -ENOMEM;

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

    spin_lock_init(&rst->lock);

    rst->rcdev.owner = THIS_MODULE;
    rst->rcdev.ops = &example_reset_ops;
    rst->rcdev.of_node = dev->of_node;
    rst->rcdev.of_reset_n_cells = 1;  /* #reset-cells = <1> */
    rst->rcdev.nr_resets = EXAMPLE_NR_RESETS;
    /* of_xlate를 NULL로 두면 기본 xlate가 사용됨:
     * phandle 인자 하나를 그대로 reset line id로 사용 */

    return devm_reset_controller_register(dev, &rst->rcdev);
}

static const struct of_device_id example_reset_dt_ids[] = {
    { .compatible = "example,soc-reset" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, example_reset_dt_ids);

static struct platform_driver example_reset_driver = {
    .probe = example_reset_probe,
    .driver = {
        .name = "example-reset",
        .of_match_table = example_reset_dt_ids,
    },
};
module_platform_driver(example_reset_driver);

MODULE_AUTHOR("Example Author");
MODULE_DESCRIPTION("Example SoC Reset Controller Driver");
MODULE_LICENSE("GPL");

위 코드에서 주목할 점은 다음과 같습니다.

대응하는 Device Tree 바인딩

/* provider 노드 */
reset: reset-controller@ff800000 {
    compatible = "example,soc-reset";
    reg = <0x0 0xff800000 0x0 0x100>;
    #reset-cells = <1>;
};

/* consumer 노드: exclusive reset 사용 */
uart0: serial@10000000 {
    compatible = "example,uart";
    reg = <0x0 0x10000000 0x0 0x1000>;
    clocks = <&ccu 20>;
    resets = <&reset 5>;
    reset-names = "uart";
};

/* consumer 노드: 여러 개의 reset 사용 */
usb0: usb@20000000 {
    compatible = "example,dwc3";
    reg = <0x0 0x20000000 0x0 0x10000>;
    clocks = <&ccu 30>, <&ccu 31>;
    clock-names = "bus", "core";
    resets = <&reset 10>, <&reset 11>, <&reset 12>;
    reset-names = "core", "phy", "bus";
};

/* consumer 노드: shared reset 사용 (같은 reset line을 여러 consumer가 공유) */
i2s0: i2s@30000000 {
    compatible = "example,i2s";
    reg = <0x0 0x30000000 0x0 0x1000>;
    resets = <&reset 20>;
    reset-names = "audio";
    /* driver에서 devm_reset_control_get_shared()로 요청 */
};

codec0: codec@30001000 {
    compatible = "example,codec";
    reg = <0x0 0x30001000 0x0 0x1000>;
    resets = <&reset 20>;
    reset-names = "audio";
    /* 같은 reset line 20번을 i2s0과 공유 */
};

대응하는 소비자 드라이버 코드

/* USB consumer driver 예시: 여러 reset line을 순서대로 제어 */

struct example_usb {
    struct device *dev;
    void __iomem *base;
    struct reset_control *core_rst;
    struct reset_control *phy_rst;
    struct reset_control *bus_rst;
    struct clk_bulk_data clks[2];
};

static int example_usb_bring_up(struct example_usb *usb)
{
    int ret;

    /* 1단계: clock 활성화 (reset deassert 전에 반드시 필요) */
    ret = clk_bulk_prepare_enable(ARRAY_SIZE(usb->clks), usb->clks);
    if (ret)
        return ret;

    /* 2단계: bus reset 먼저 해제 (버스 접근 가능하게) */
    ret = reset_control_deassert(usb->bus_rst);
    if (ret)
        goto err_clk;

    /* 3단계: PHY reset 해제 (PHY 초기화 시간 확보) */
    ret = reset_control_deassert(usb->phy_rst);
    if (ret)
        goto err_bus_rst;

    /* 4단계: PHY 안정화 대기 */
    usleep_range(100, 200);

    /* 5단계: core reset 해제 (controller 동작 시작) */
    ret = reset_control_deassert(usb->core_rst);
    if (ret)
        goto err_phy_rst;

    /* 6단계: controller 내부 초기화 */
    usleep_range(50, 100);

    return 0;

err_phy_rst:
    reset_control_assert(usb->phy_rst);
err_bus_rst:
    reset_control_assert(usb->bus_rst);
err_clk:
    clk_bulk_disable_unprepare(ARRAY_SIZE(usb->clks), usb->clks);
    return ret;
}

static void example_usb_shut_down(struct example_usb *usb)
{
    /* 역순: core → PHY → bus → clock */
    reset_control_assert(usb->core_rst);
    reset_control_assert(usb->phy_rst);
    reset_control_assert(usb->bus_rst);
    clk_bulk_disable_unprepare(ARRAY_SIZE(usb->clks), usb->clks);
}

static int example_usb_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct example_usb *usb;
    int ret;

    usb = devm_kzalloc(dev, sizeof(*usb), GFP_KERNEL);
    if (!usb)
        return -ENOMEM;

    usb->dev = dev;
    platform_set_drvdata(pdev, usb);

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

    /* reset handle 획득 — 각각 이름으로 가져옴 (순서 무관) */
    usb->core_rst = devm_reset_control_get_exclusive(dev, "core");
    if (IS_ERR(usb->core_rst))
        return dev_err_probe(dev, PTR_ERR(usb->core_rst),
                             "failed to get core reset\n");

    usb->phy_rst = devm_reset_control_get_exclusive(dev, "phy");
    if (IS_ERR(usb->phy_rst))
        return dev_err_probe(dev, PTR_ERR(usb->phy_rst),
                             "failed to get phy reset\n");

    usb->bus_rst = devm_reset_control_get_exclusive(dev, "bus");
    if (IS_ERR(usb->bus_rst))
        return dev_err_probe(dev, PTR_ERR(usb->bus_rst),
                             "failed to get bus reset\n");

    /* clock handle 획득 */
    usb->clks[0].id = "bus";
    usb->clks[1].id = "core";
    ret = devm_clk_bulk_get(dev, ARRAY_SIZE(usb->clks), usb->clks);
    if (ret)
        return dev_err_probe(dev, ret, "failed to get clocks\n");

    /* bring-up: clock on → reset deassert (bus → phy → core 순서) */
    ret = example_usb_bring_up(usb);
    if (ret)
        return ret;

    /* 이하 MMIO 초기화, 인터럽트 등록 등 */
    dev_info(dev, "USB controller initialized\n");

    return 0;
}

static void example_usb_remove(struct platform_device *pdev)
{
    struct example_usb *usb = platform_get_drvdata(pdev);

    example_usb_shut_down(usb);
}
bring-up/shut-down 함수 분리: probe와 remove, runtime_resume와 runtime_suspend에서 같은 시퀀스를 반복하므로, bring_up()/shut_down() 내부 함수를 만들어 재사용하면 시퀀스 불일치 버그를 예방할 수 있습니다. 위 코드에서 example_usb_bring_up()은 runtime_resume에서도 그대로 호출할 수 있습니다.

전원 관리와 리셋의 통합

리셋 컨트롤러는 전원 관리(Power Management) 서브시스템과 밀접하게 연결됩니다. 전원 도메인(Power Domain)이 꺼지면 해당 도메인 내의 모든 리셋 상태가 사라지고, 전원이 다시 켜지면 리셋 라인이 하드웨어 기본값(보통 asserted)으로 돌아갑니다.

전원 도메인과 리셋의 관계

전원 도메인 on/off에 따른 리셋 상태 변화 — 전원이 꺼지면 리셋 context도 사라집니다 Power Domain ON — 정상 동작 상태 Reset: deasserted Clock: enabled Registers: accessible State Machine: running Power Domain OFF — context 소멸 Reset: HW default (asserted) Clock: gated Registers: undefined State Machine: halted power off Power Domain 재활성화 후 필수 시퀀스 1. Power ON genpd resume 2. Clock ON clk_enable 3. Reset Deassert reset_control_deassert 4. Reg Restore MMIO 재설정 5. Resume OK 동작 가능 3단계에서 reset deassert를 빼먹으면 레지스터가 응답하지 않거나 0xFFFFFFFF를 반환할 수 있습니다
전원 상태리셋 라인레지스터 접근드라이버 책임
Always-on domainSW가 설정한 상태 유지항상 가능suspend 시 명시적 assert/deassert 선택
Retention domain일부 레지스터 값 유지, 리셋 상태는 SoC 의존resume 후 가능retention 보장 범위를 데이터시트로 확인
Off domain하드웨어 기본값(보통 asserted)으로 복귀power on 후에만 가능full bring-up 시퀀스 재실행 필수

Generic Power Domain(genpd)과 리셋의 상호작용

리눅스 커널의 genpd 프레임워크는 전원 도메인이 꺼질 때 power_off 콜백을, 켜질 때 power_on 콜백을 호출합니다. 일부 SoC에서는 genpd의 power_on 콜백 안에서 해당 도메인의 리셋을 자동으로 deassert하기도 합니다. 이 경우 개별 드라이버에서 별도로 reset_control_deassert()를 호출하면 이중 deassert가 될 수 있으므로 주의가 필요합니다.

/* genpd power_on에서 리셋을 자동 해제하는 SoC의 패턴 */

static int soc_pd_power_on(struct generic_pm_domain *genpd)
{
    struct soc_pm_domain *pd = to_soc_pd(genpd);

    /* 전원 도메인 활성화 */
    regmap_update_bits(pd->regmap, pd->pwr_reg,
                       pd->pwr_mask, pd->pwr_mask);

    /* 전원 안정화 대기 */
    usleep_range(50, 100);

    /* 해당 도메인의 리셋 자동 해제 — 드라이버에서 별도로 할 필요 없음 */
    regmap_update_bits(pd->regmap, pd->rst_reg,
                       pd->rst_mask, 0);

    return 0;
}

/* 반대로 power_off에서는 리셋을 assert */
static int soc_pd_power_off(struct generic_pm_domain *genpd)
{
    struct soc_pm_domain *pd = to_soc_pd(genpd);

    /* 리셋 assert → 전원 도메인 비활성화 */
    regmap_update_bits(pd->regmap, pd->rst_reg,
                       pd->rst_mask, pd->rst_mask);
    regmap_update_bits(pd->regmap, pd->pwr_reg,
                       pd->pwr_mask, 0);

    return 0;
}
이중 deassert 위험: genpd의 power_on 콜백이 이미 리셋을 해제하는 SoC에서, 드라이버가 probe나 runtime_resume에서 다시 reset_control_deassert()를 호출하면 refcount 불일치가 발생할 수 있습니다. SoC BSP 코드를 반드시 확인하고, genpd가 리셋을 관리하는 경우에는 드라이버에서 리셋을 별도로 제어하지 않거나 optional getter를 사용하는 것이 안전합니다.

레귤레이터(Regulator)와 리셋의 시퀀스 관계

외부 칩(External IC)이나 PHY를 사용하는 경우, 레귤레이터로 전원을 공급한 뒤 리셋을 해제하는 시퀀스가 필요합니다. 이때 레귤레이터의 ramp-up 시간(전압이 안정되는 시간)을 고려해야 합니다.

/* 외부 PHY의 전원 + 리셋 시퀀스 예시 */

static int phy_power_on(struct phy *phy)
{
    struct example_phy *priv = phy_get_drvdata(phy);
    int ret;

    /* 1. 전원 공급 */
    ret = regulator_enable(priv->supply);
    if (ret)
        return ret;

    /* 2. 전압 안정화 대기 (데이터시트: 최소 1ms) */
    usleep_range(1000, 1500);

    /* 3. 리셋 해제 */
    ret = reset_control_deassert(priv->rst);
    if (ret)
        goto err_reg;

    /* 4. PHY 초기화 시간 대기 (데이터시트: 최소 100us) */
    usleep_range(100, 200);

    return 0;

err_reg:
    regulator_disable(priv->supply);
    return ret;
}

Device Tree 바인딩 고급 사항

#reset-cells와 xlate 함수의 관계

대부분의 리셋 컨트롤러는 #reset-cells = <1>을 사용하며, phandle 뒤의 단일 정수가 리셋 라인 인덱스를 나타냅니다. 하지만 복잡한 SoC에서는 #reset-cells = <2> 이상을 사용하는 경우도 있습니다. 예를 들어 첫 번째 인자가 리셋 뱅크(Bank) 번호이고 두 번째 인자가 뱅크 내 비트 오프셋인 경우입니다.

/* #reset-cells = <2>를 위한 custom xlate 함수 예시 */

static int multi_cell_xlate(struct reset_controller_dev *rcdev,
                            const struct of_phandle_args *reset_spec)
{
    unsigned int bank = reset_spec->args[0];
    unsigned int bit = reset_spec->args[1];
    unsigned int id;

    if (bank >= 4 || bit >= 32)
        return -EINVAL;

    /* bank * 32 + bit = 선형 reset line ID */
    id = bank * 32 + bit;

    if (id >= rcdev->nr_resets)
        return -EINVAL;

    return id;
}

/* probe에서 설정 */
rst->rcdev.of_reset_n_cells = 2;
rst->rcdev.of_xlate = multi_cell_xlate;
/* 대응하는 DT: #reset-cells = <2> */
rst: reset-controller@ff900000 {
    compatible = "vendor,multi-bank-reset";
    reg = <0x0 0xff900000 0x0 0x100>;
    #reset-cells = <2>;
};

device@a0000000 {
    /* bank 1, bit 5 = linear id 37 */
    resets = <&rst 1 5>;
    reset-names = "core";
};

clock과 reset을 동시에 제공하는 DT 노드

많은 SoC에서 CCU(Clock and Control Unit)나 CRU(Clock and Reset Unit)가 하나의 DT 노드에서 #clock-cells#reset-cells를 동시에 제공합니다. 이 경우 clock ID와 reset ID는 서로 독립적인 네임스페이스(Namespace)이며, 같은 숫자라도 전혀 다른 리소스를 가리킵니다.

/* clock과 reset을 동시에 제공하는 CCU 노드 */
ccu: clock-controller@1c20000 {
    compatible = "allwinner,sun50i-h6-ccu";
    reg = <0x01c20000 0x1000>;
    clocks = <&osc24M>, <&osc32k>, <&iosc>;
    clock-names = "hosc", "losc", "iosc";
    #clock-cells = <1>;
    #reset-cells = <1>;
};

/* consumer: clocks와 resets의 인덱스는 독립적 */
mmc0: mmc@4020000 {
    compatible = "allwinner,sun50i-h6-mmc";
    clocks = <&ccu 71>, <&ccu 73>;   /* CLK_BUS_MMC0, CLK_MMC0 */
    clock-names = "ahb", "mmc";
    resets = <&ccu 22>;               /* RST_BUS_MMC0 — clock ID 22와 무관 */
    reset-names = "ahb";
};
ID 매핑 규칙: clock ID와 reset ID는 보통 SoC 벤더가 헤더 파일(Header File)로 정의합니다. 예: include/dt-bindings/clock/sun50i-h6-ccu.hinclude/dt-bindings/reset/sun50i-h6-ccu.h. DT 소스에서 매직 넘버(Magic Number) 대신 이 상수(Constant)를 사용해야 가독성과 유지보수성이 보장됩니다.

probe 순서 문제와 EPROBE_DEFER 처리

reset controller는 보통 SoC 초기화 과정에서 일찍 probe되지만, consumer 드라이버가 reset provider보다 먼저 probe를 시도할 수 있습니다. 이때 -EPROBE_DEFER가 반환되며, 커널의 deferred probe 메커니즘이 provider가 등록된 뒤 consumer probe를 재시도합니다.

/* consumer 드라이버에서 올바른 EPROBE_DEFER 처리 */

static int my_driver_probe(struct platform_device *pdev)
{
    struct reset_control *rst;

    /* dev_err_probe()는 -EPROBE_DEFER일 때 에러 로그를 남기지 않음 */
    rst = devm_reset_control_get_exclusive(&pdev->dev, "core");
    if (IS_ERR(rst))
        return dev_err_probe(&pdev->dev, PTR_ERR(rst),
                             "reset not available yet\n");

    /* 여기 도달하면 provider가 등록된 상태이며 핸들 사용 가능 */
    return reset_control_deassert(rst);
}
defer 무한 루프 주의: reset provider가 아예 등록되지 않으면(DT 노드가 없거나 provider 드라이버가 빌드에 포함되지 않은 경우) consumer는 계속 defer됩니다. 이 상태는 /sys/kernel/debug/devices_deferred에서 확인할 수 있으며, optional getter(devm_reset_control_get_optional_*)를 사용하면 provider가 없어도 NULL을 반환받아 진행할 수 있습니다.
상황결과대응 방법
provider가 아직 probe 전-EPROBE_DEFER 반환자동 재시도 (커널 deferred probe)
provider DT 노드 없음 + exclusive get-ENOENT 반환DT 점검 또는 optional getter 사용
provider DT 노드 없음 + optional getNULL 반환 (성공)이후 API 호출이 quietly 성공
provider 등록 완료 + 이미 exclusive 핸들 존재-EBUSY 반환shared getter 사용 또는 설계 재검토

suspend/resume에서의 reset 처리 전략

suspend/resume은 cold boot와 다른 reset 전략이 필요합니다. 하드웨어에 따라 suspend 중 reset line 상태를 유지할 수도 있고, deep sleep에서 context가 사라져 resume 시 reset을 다시 걸어야 할 수도 있습니다.

시나리오reset line 상태resume 시 동작주의점
shallow sleep (runtime suspend)deassert 유지 가능clock만 재활성화하면 충분retention이 보장되는지 데이터시트 확인
deep sleep (system suspend)assert로 전환될 수 있음full bring-up 시퀀스 재실행cold boot probe와 중복 코드 최소화
power domain offcontext 완전 소멸반드시 assert → deassert 재실행regulator/clock도 다시 설정해야 함
bootloader handoff 의존bootloader가 설정한 상태resume 경로에서는 bootloader 없음가장 흔한 "cold boot만 됨" 원인
/* suspend/resume에서 reset 처리 패턴 */

static int my_ip_runtime_suspend(struct device *dev)
{
    struct my_ip *priv = dev_get_drvdata(dev);

    /* 역순: MMIO quiesce → reset assert → clock off → regulator off */
    my_ip_hw_quiesce(priv);
    reset_control_assert(priv->core_rst);
    clk_bulk_disable_unprepare(priv->num_clks, priv->clks);

    return 0;
}

static int my_ip_runtime_resume(struct device *dev)
{
    struct my_ip *priv = dev_get_drvdata(dev);
    int ret;

    /* 정순: clock on → reset deassert → MMIO 재초기화 */
    ret = clk_bulk_prepare_enable(priv->num_clks, priv->clks);
    if (ret)
        return ret;

    usleep_range(10, 20);
    ret = reset_control_deassert(priv->core_rst);
    if (ret)
        goto err_clk;

    /* cold boot의 hw_init과 동일한 레지스터 재설정이 필요할 수 있음 */
    ret = my_ip_hw_init(priv);
    if (ret)
        goto err_rst;

    return 0;

err_rst:
    reset_control_assert(priv->core_rst);
err_clk:
    clk_bulk_disable_unprepare(priv->num_clks, priv->clks);
    return ret;
}

static DEFINE_RUNTIME_DEV_PM_OPS(my_ip_pm_ops,
    my_ip_runtime_suspend, my_ip_runtime_resume, NULL);
공통 helper 추출: probe의 bring-up 코드와 runtime_resume의 bring-up 코드가 거의 같다면, my_ip_hw_bring_up() 같은 내부 함수로 추출하면 코드 중복을 줄이고 시퀀스 불일치 실수를 방지할 수 있습니다.

debugfs를 통한 리셋 상태 관찰

리셋 프레임워크는 debugfs를 통해 시스템의 리셋 상태를 관찰할 수 있는 인터페이스를 제공합니다. 이를 활용하면 현재 등록된 리셋 컨트롤러, 각 리셋 라인의 사용 상태, 그리고 deferred probe 상태를 한눈에 파악할 수 있습니다.

# reset controller framework debugfs 확인
# (커널 빌드 시 CONFIG_DEBUG_FS=y 필요)
ls /sys/kernel/debug/reset-controller/

# 특정 provider의 리셋 라인 상태 확인
cat /sys/kernel/debug/reset-controller/ff800000.reset-controller/reset_line_status

# 전체 deferred probe 목록에서 reset 관련 항목 확인
cat /sys/kernel/debug/devices_deferred

# device tree에서 reset 바인딩 확인 (원시 바이너리이므로 hexdump 사용)
hexdump -C /sys/firmware/devicetree/base/usb@20000000/resets

# reset-names 문자열 확인
cat /sys/firmware/devicetree/base/usb@20000000/reset-names

# provider의 #reset-cells 확인
hexdump -C /sys/firmware/devicetree/base/reset@ff800000/#reset-cells

dynamic debug를 활용한 리셋 추적

커널의 dynamic debug 기능을 사용하면 리셋 프레임워크 코어(drivers/reset/core.c)의 내부 로그를 런타임에 활성화할 수 있습니다.

# reset framework core의 모든 debug 메시지 활성화
echo 'file drivers/reset/core.c +p' > /sys/kernel/debug/dynamic_debug/control

# 특정 함수의 debug 메시지만 활성화
echo 'func __reset_control_get +p' > /sys/kernel/debug/dynamic_debug/control
echo 'func reset_control_assert +p' > /sys/kernel/debug/dynamic_debug/control

# 특정 provider 드라이버의 debug 메시지 활성화
echo 'file drivers/reset/reset-imx7.c +p' > /sys/kernel/debug/dynamic_debug/control

# dmesg에서 결과 확인
dmesg | grep -i reset

에러 코드별 원인 분석

에러 코드반환 위치원인해결 방법
-EPROBE_DEFERreset_control_get*()provider가 아직 등록되지 않음provider probe 순서 확인, dev_err_probe() 사용
-ENOENTreset_control_get*()DT에 resets 속성이 없음DT 바인딩 확인, optional getter 사용 고려
-EBUSYreset_control_get_exclusive()이미 다른 consumer가 exclusive 핸들을 가지고 있음shared getter 사용 또는 다른 consumer 확인
-EINVALof_xlate()phandle 인자가 범위를 벗어남#reset-cells와 인자 값 확인
-ENOTSUPPreset_control_status()provider가 .status 콜백을 구현하지 않음status 조회 불가 — 다른 방법으로 상태 확인
-EPERMreset_control_assert()acquired 상태가 아닌 released 핸들에서 assert 시도reset_control_acquire()를 먼저 호출

일반적인 문제와 해결 패턴

문제 상황진단 방법근본 원인해결 패턴
부팅 시 "failed to get reset" 에러 후 probe 실패dmesg | grep -i "reset\|defer"DT의 reset-names와 드라이버의 devm_reset_control_get() 인자 불일치DT 소스와 드라이버 소스의 이름을 대조하여 일치시킴
장치가 가끔씩만 동작함 (간헐적 실패)usleep_range 값 증가 후 재테스트assert/deassert 사이의 최소 펄스 폭 위반데이터시트에서 최소 리셋 펄스 폭을 확인하고 usleep_range 값 조정
module reload 시 장치 사망modprobe -r && modprobe 후 dmesg 확인remove에서 reset assert를 안 하거나 재probe 시 시퀀스 불완전remove에서 명시적 assert, probe에서 full 시퀀스 실행
한 장치 리셋 시 다른 장치도 영향받음DT에서 같은 reset line을 공유하는 노드 검색shared reset line인데 exclusive로 사용 중관련 드라이버를 모두 shared getter로 변경하거나 하드웨어 설계 재검토
suspend 후 resume 시 레지스터 읽기 실패resume 콜백에서 reset deassert 여부 확인deep sleep에서 리셋 상태가 HW default로 복귀runtime_resume에서 clock enable + reset deassert 시퀀스 추가
QEMU에서는 되는데 실물 보드에서 실패실물 보드에서 오실로스코프(Oscilloscope)로 리셋 핀 확인QEMU의 리셋 모델이 실제 하드웨어 타이밍을 반영하지 못함실물 보드에서 테스트, 데이터시트 타이밍 준수 확인

커널 5.x 이상에서는 devlink 프레임워크가 provider와 consumer 사이의 의존성을 자동으로 추적합니다. reset provider와 consumer 사이에도 devlink가 생성되므로, 이를 통해 의존성 그래프(Dependency Graph)를 확인할 수 있습니다.

# devlink supplier-consumer 관계 확인
ls /sys/class/devlink/

# 특정 장치의 supplier(provider) 확인
ls -la /sys/devices/platform/usb@20000000/supplier:*

# supplier의 status 확인
cat /sys/devices/platform/usb@20000000/supplier:platform:ff800000.reset-controller/status

# 전체 devlink 관계를 트리 형태로 보기
find /sys/class/devlink/ -name "status" -exec sh -c 'echo "$(dirname {}): $(cat {})"' \;
devlink 상태 해석: consumer probe는 consumer가 아직 probe되지 않았음을, active는 양쪽 모두 정상임을, available는 supplier만 준비되었음을 의미합니다. reset 관련 probe defer가 의심될 때 devlink status를 먼저 확인하면 원인을 빠르게 좁힐 수 있습니다.

테스트와 검증 전략

reset 동작은 하드웨어 의존적이라 순수 유닛 테스트가 어렵지만, 다음 전략으로 커버리지를 높일 수 있습니다.

테스트 방법검증 대상명령어 / 도구
module reloadprobe/remove 시 reset 상태 정합성modprobe -r my_driver && modprobe my_driver
runtime PM 강제 togglesuspend/resume reset 복원echo on > /sys/devices/.../power/control
fault injectionprobe error path에서 leak 없는지echo 1 > /sys/kernel/debug/fail_function/...
DT overlay 변형optional reset 누락, 이름 변경 시 동작DT overlay로 resets 속성 제거 후 boot
ftrace function_graphreset 콜백 호출 순서 추적trace-cmd record -p function_graph -g reset_control_assert
# ftrace로 reset 관련 함수 호출 추적
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo reset_control_assert > /sys/kernel/debug/tracing/set_graph_function
echo reset_control_deassert >> /sys/kernel/debug/tracing/set_graph_function
echo reset_control_reset >> /sys/kernel/debug/tracing/set_graph_function
echo 1 > /sys/kernel/debug/tracing/tracing_on

# 트리거 동작 후 로그 확인
cat /sys/kernel/debug/tracing/trace

# deferred probe 상태 확인
cat /sys/kernel/debug/devices_deferred
# runtime PM을 통한 reset 순서 검증
# autosuspend delay를 짧게 설정하여 빠르게 suspend/resume 반복
echo 100 > /sys/devices/platform/my-device/power/autosuspend_delay_ms
echo auto > /sys/devices/platform/my-device/power/control

# 연속 I/O 발생으로 suspend/resume 사이클 유발
# dmesg에서 reset assert/deassert 순서 확인
dmesg | grep -E "reset|assert|deassert|suspend|resume"
실물 보드 없이 테스트: QEMU 등의 에뮬레이터에서 reset controller를 완벽하게 에뮬레이션하기는 어렵습니다. QEMU의 SoC 모델에 reset 레지스터가 구현되어 있어도 실제 하드웨어의 timing, retention, self-clearing bit 동작을 정확히 재현하지 못하는 경우가 많습니다. 실물 보드에서의 최종 검증은 반드시 필요합니다.

KUnit 기반 리셋 프레임워크 테스트

커널 6.x부터 리셋 프레임워크에 대한 KUnit 테스트가 추가되고 있습니다. mock provider를 사용하여 프레임워크의 refcount 로직, exclusive/shared 충돌, boundary conditions를 자동으로 검증합니다.

# KUnit 리셋 테스트 실행 (CONFIG_RESET_KUNIT_TEST=y 필요)
# UML(User Mode Linux)에서 실행하는 방법:
./tools/testing/kunit/kunit.py run reset_test

# 또는 특정 테스트만 실행:
./tools/testing/kunit/kunit.py run --kunitconfig=drivers/reset/.kunitconfig

스트레스 테스트 패턴

리셋 관련 race condition이나 refcount 누수(Leak)를 발견하려면 반복적인 suspend/resume 사이클과 module reload를 병행하는 스트레스 테스트가 효과적입니다.

# suspend/resume 반복 스트레스 테스트
for i in $(seq 1 100); do
    echo mem > /sys/power/state
    sleep 2
    dmesg | tail -5 | grep -i -E "error|fail|reset|timeout"
    echo "=== cycle $i done ==="
done

# module reload 반복 테스트 (리셋 관련 리소스 누수 발견용)
for i in $(seq 1 50); do
    modprobe -r my_driver
    sleep 0.5
    modprobe my_driver
    sleep 0.5
    # 메모리 누수 확인
    grep my_driver /proc/slabinfo 2>/dev/null
done

# runtime PM 빠른 전환 스트레스 테스트
echo 10 > /sys/devices/platform/my-device/power/autosuspend_delay_ms
echo auto > /sys/devices/platform/my-device/power/control
# 짧은 간격으로 I/O 발생시켜 rapid suspend/resume 유도
for i in $(seq 1 200); do
    cat /dev/my-device > /dev/null 2>&1
    sleep 0.01
done

주요 서브시스템에서의 리셋 활용 패턴

리셋 컨트롤러 API는 커널의 다양한 서브시스템에서 사용됩니다. 서브시스템마다 리셋을 활용하는 패턴이 조금씩 다르므로, 대표적인 사례를 정리합니다.

USB 서브시스템

USB 컨트롤러(DWC3, EHCI, XHCI 등)는 보통 3개의 리셋 라인을 가집니다: core reset, PHY reset, bus(AXI/AHB) reset. 이 중 bus reset은 가장 먼저 해제해야 하고, PHY reset은 PHY 캘리브레이션(Calibration) 전에 해제해야 하며, core reset은 마지막에 해제합니다. suspend 시에는 역순으로 assert합니다.

리셋 라인대상해제 시점assert 시점
bus resetAXI/AHB 버스 인터페이스clock enable 직후 (가장 먼저)마지막 (clock disable 직전)
PHY resetUSB PHY 아날로그 블록bus reset 해제 후, PHY 초기화 전core reset assert 후
core resetUSB 컨트롤러 코어 로직PHY 초기화 완료 후 (가장 나중에)가장 먼저

네트워크 서브시스템

Ethernet MAC 드라이버(stmmac, macb 등)는 MAC reset과 PHY reset을 별도로 관리합니다. MAC reset은 SoC reset controller가, PHY reset은 GPIO 기반 리셋이나 MDIO reset이 담당하는 경우가 많습니다.

/* stmmac 드라이버의 리셋 패턴 (단순화) */

static int stmmac_reset(struct stmmac_priv *priv)
{
    struct reset_control *rst = priv->plat->stmmac_rst;

    if (!rst)
        return 0;  /* optional: 없으면 skip */

    /* pulse reset 또는 assert → delay → deassert */
    reset_control_assert(rst);
    udelay(2);
    reset_control_deassert(rst);

    /* MAC 내부 DMA engine 초기화 대기 */
    return readl_poll_timeout(priv->ioaddr + DMA_BUS_MODE,
                              val, !(val & DMA_BUS_MODE_SFT_RESET),
                              10, 10000);
}

GPU 서브시스템

GPU 드라이버(Mali, Adreno, Panfrost 등)는 리셋 후 firmware 로드, shader 코어 초기화, MMU 설정 등 복잡한 재초기화 시퀀스가 필요합니다. GPU 리셋은 보통 GPU hang 복구(Recovery)에도 사용되므로, runtime에 동적으로 리셋을 트리거하는 경로도 존재합니다.

/* GPU hang 복구에서의 리셋 사용 (개념적 패턴) */

static void gpu_recover_from_hang(struct gpu_device *gpu)
{
    /* 1. 진행 중인 작업 중단 */
    gpu_cancel_all_jobs(gpu);

    /* 2. GPU core 리셋 */
    reset_control_assert(gpu->rst);
    usleep_range(10, 20);
    reset_control_deassert(gpu->rst);
    usleep_range(100, 200);

    /* 3. 전체 재초기화 */
    gpu_init_hw(gpu);
    gpu_load_firmware(gpu);
    gpu_setup_mmu(gpu);

    /* 4. 대기 중인 작업 재제출 */
    gpu_resubmit_pending_jobs(gpu);
}

디스플레이(Display) 서브시스템

디스플레이 컨트롤러는 CRTC, encoder, bridge 등 여러 파이프라인(Pipeline) 컴포넌트가 리셋 라인을 공유하거나 순차적으로 리셋해야 하는 경우가 많습니다. 특히 HDMI/DisplayPort PHY는 PLL 락(Lock) 후에 리셋을 해제해야 하므로 타이밍이 까다롭습니다.

컴포넌트리셋 유형특이사항
CRTC (Display Controller)전체 또는 채널별 리셋active display 중 리셋 금지 — vblank에서 수행
HDMI TXcore + PHY 분리PHY PLL 안정화 후 core 리셋 해제
MIPI DSIhost + D-PHY 분리LP-11 상태 진입 후 리셋 해제 순서 중요
DisplayPortAUX channel + main link 분리link training 전 리셋 완료 필요

암호화(Crypto) 엔진

하드웨어 암호화 엔진(CAAM, CESA, CryptoCell 등)은 보안상 리셋 후 키(Key) 재로드와 self-test가 필요합니다. 리셋이 완료되어도 self-test를 통과해야 정상 동작하므로, deassert 후 상태 레지스터를 폴링(Polling)하여 엔진이 ready 상태인지 확인해야 합니다.

/* 암호화 엔진의 리셋 후 ready 확인 패턴 */

static int crypto_engine_init(struct crypto_dev *cdev)
{
    int ret;
    u32 status;

    /* 리셋 해제 */
    ret = reset_control_deassert(cdev->rst);
    if (ret)
        return ret;

    /* 엔진 self-test 완료 대기 (최대 100ms) */
    ret = readl_poll_timeout(cdev->base + CRYPTO_STATUS_REG,
                              status,
                              status & CRYPTO_READY,
                              100,     /* 100us 간격 */
                              100000); /* 100ms 타임아웃 */
    if (ret) {
        dev_err(cdev->dev,
                "crypto engine not ready after reset (status=0x%x)\n",
                status);
        reset_control_assert(cdev->rst);
        return ret;
    }

    return 0;
}

리셋 컨트롤러 베스트 프랙티스(Best Practices) 요약

지금까지 다룬 내용을 종합하여, 리셋 컨트롤러 관련 코드를 작성할 때의 핵심 원칙을 정리합니다.

#원칙근거
1데이터시트를 먼저 읽고 리셋 모델을 결정하라level인지 pulse인지, active-high인지 active-low인지가 API 선택을 결정합니다.
2시퀀스를 글로 먼저 적어라power → clock → reset → MMIO 순서를 코드 전에 문서화하면 실수가 줄어듭니다.
3exclusive/shared를 처음부터 확정하라나중에 바꾸면 다른 consumer와의 호환성 문제가 발생합니다.
4optional getter를 적극 활용하라플랫폼 간 이식성을 높이고 #ifdef를 줄여줍니다.
5dev_err_probe()로 에러를 반환하라-EPROBE_DEFER 시 불필요한 에러 로그를 방지합니다.
6bring-up/shut-down을 내부 함수로 분리하라probe, resume, error-path에서 시퀀스 불일치를 방지합니다.
7에러 경로에서 역순 cleanup을 빠뜨리지 마라deassert 없는 assert, clock enable 없는 reset deassert는 리소스 누수입니다.
8resume 경로를 cold boot와 별도로 검증하라부트로더 의존성은 resume에서만 드러납니다.
9bulk/array API는 순서 무관한 경우에만 사용하라순서 의존 하드웨어에서는 개별 handle이 더 안전합니다.
10pulse reset과 level reset을 절대 혼용하지 마라같은 핸들에서 reset()assert()/deassert()를 섞으면 의미가 깨집니다.
코드 리뷰 체크포인트: 리셋 관련 패치를 리뷰할 때는 (1) DT 바인딩과 driver 이름 일치, (2) exclusive/shared 선택의 근거, (3) assert/deassert 균형, (4) error-path cleanup, (5) resume 경로 존재 여부를 우선적으로 확인하세요. 이 5가지가 리셋 관련 버그의 90% 이상을 차지합니다.

안티패턴(Anti-Pattern) 모음

실전에서 자주 발견되는 잘못된 패턴을 정리합니다. 이런 코드를 발견하면 즉시 수정이 필요합니다.

/* 안티패턴 1: devm이 아닌 manual get 후 put 누락 */
struct reset_control *rst = reset_control_get_exclusive(dev, "core");
/* ... 사용 ... */
/* reset_control_put(rst)를 호출하지 않음 → 리소스 누수 */
/* 해결: devm_reset_control_get_exclusive() 사용 */

/* 안티패턴 2: shared reset에서 assert 후 HW 상태 가정 */
reset_control_assert(shared_rst);
/* 여기서 "모든 레지스터가 초기화되었을 것"이라고 가정 → 위험 */
writel(0, base + SOME_REG); /* 다른 consumer가 여전히 사용 중일 수 있음 */

/* 안티패턴 3: error path에서 cleanup 순서 오류 */
ret = reset_control_deassert(rst);
if (ret)
    goto err;
ret = clk_prepare_enable(clk);
if (ret)
    goto err; /* reset은 deassert된 채로 남음 → 누수 */
/* 해결: err_rst 레이블에서 reset_control_assert() 호출 */

/* 안티패턴 4: pulse와 level 혼용 */
reset_control_reset(rst);      /* pulse 의미 */
reset_control_deassert(rst);   /* level 의미 — 같은 핸들에 혼용 금지 */

/* 안티패턴 5: 부트로더 상태 의존 */
/* assert를 안 하고 바로 deassert만 호출 — 이미 deassert 상태라고 가정 */
reset_control_deassert(rst);
/* warm reboot에서 부트로더가 상태를 안 건드리면 문제 발생 가능 */
/* 해결: 명시적 assert → delay → deassert 시퀀스 실행 */

/* 안티패턴 6: usleep 없이 즉시 deassert */
reset_control_assert(rst);
reset_control_deassert(rst);  /* 최소 pulse 폭 위반 가능 */
/* 해결: assert 후 usleep_range()로 최소 폭 보장 */
안티패턴결과올바른 패턴
manual get 후 put 누락리소스 누수, module unload 시 경고devm_reset_control_get_*() 사용
shared assert 후 HW 상태 가정다른 consumer 동작 방해, data corruptionshared에서는 assert의 물리적 효과를 가정하지 않음
error path cleanup 누락리셋 라인이 deassert된 채 방치goto chain에서 역순 cleanup 보장
pulse/level 혼용refcount 불일치, 예기치 않은 물리 상태하나의 핸들에 하나의 모델만 사용
부트로더 상태 의존warm reboot 시 간헐적 실패probe에서 명시적 assert → deassert
최소 pulse 폭 미준수리셋이 유효하지 않아 IP 불안정usleep_range()로 데이터시트 최소값 보장

consumer API 빠른 참조표

아래 표는 consumer가 사용할 수 있는 주요 API를 한눈에 정리합니다. devm_ 접두사가 붙은 함수는 managed 리소스로 자동 해제됩니다.

함수반환형의미실패 시
devm_reset_control_get_exclusive(dev, id)struct reset_control *exclusive reset 핸들 획득ERR_PTR(-ENOENT) 또는 -EPROBE_DEFER
devm_reset_control_get_shared(dev, id)struct reset_control *shared reset 핸들 획득ERR_PTR(-ENOENT)
devm_reset_control_get_optional_exclusive(dev, id)struct reset_control *없으면 NULL 반환ERR_PTR() (실제 에러만)
devm_reset_control_get_optional_shared(dev, id)struct reset_control *shared + optionalERR_PTR() (실제 에러만)
reset_control_assert(rstc)intassert (shared: refcount 감소)provider ops 에러
reset_control_deassert(rstc)intdeassert (shared: refcount 증가)provider ops 에러
reset_control_reset(rstc)intpulse triggerprovider ops 에러
reset_control_rearm(rstc)intshared pulse를 재트리거 가능 상태로provider ops 에러
reset_control_status(rstc)intassert 상태 조회 (1=asserted)-ENOTSUPP (미구현 시)
reset_control_acquire(rstc)intreleased 핸들을 잠시 독점 획득-EBUSY
reset_control_release(rstc)voidacquire한 핸들을 반납-
NULL 핸들 안전성: 모든 consumer API는 NULL 핸들을 받으면 조용히 성공을 반환합니다. 이 특성 덕분에 optional getter와 조합하면 플랫폼별 #ifdef 없이 코드를 작성할 수 있습니다.

실전 체크리스트: 흔히 놓치는 함정

reset controller 관련 코드를 작성하거나 리뷰할 때 다음 항목을 점검하면 대부분의 문제를 예방할 수 있습니다.

#체크 항목확인 방법
1reset deassert 전에 clock이 활성화되어 있는가?probe 순서에서 clk_prepare_enable()reset_control_deassert() 앞에 오는지 확인
2shared reset에서 assert/deassert 호출 수가 균형을 이루는가?error path와 remove에서 누락된 assert가 없는지 추적
3pulse reset과 level reset을 혼용하고 있지 않은가?reset_control_reset()assert()/deassert()가 같은 핸들에 사용되지 않는지 확인
4bulk/array API를 사용하는 장치에 순서 의존성이 없는가?데이터시트에서 reset 해제 순서 요구사항 확인
5dev_err_probe()를 사용하여 EPROBE_DEFER를 자연스럽게 처리하는가?PTR_ERR() 직접 반환 코드를 dev_err_probe()로 교체
6resume 경로에서 cold boot와 동일한 reset 시퀀스를 실행하는가?runtime_resume 콜백에서 reset deassert가 포함되어 있는지 확인
7reset assert 후 deassert 전에 데이터시트 최소 폭을 지키는가?usleep_range() 등으로 최소 pulse width를 보장하는지 확인
8provider의 #reset-cells와 consumer의 resets phandle 인자 수가 일치하는가?DT 컴파일 경고와 부팅 시 xlate 에러 확인
9remove/unbind에서 reset을 assert 상태로 되돌리는가?devm 관리가 아닌 수동 cleanup 경로 점검
10같은 reset line을 exclusive와 shared로 동시에 요청하지 않는가?다른 드라이버의 DT 바인딩과 getter 타입 확인

리셋 토폴로지(Topology) 패턴

실제 SoC에서 리셋 라인의 연결 구조(토폴로지)는 다양합니다. 단순한 1:1 매핑(하나의 리셋 비트가 하나의 IP만 제어)부터, 여러 IP가 하나의 리셋 라인을 공유하는 구조, 계층적(Hierarchical) 리셋 구조까지 존재합니다.

토폴로지구조특징드라이버 설계 시 고려사항
1:1 단독 리셋reset bit → 단일 IP가장 단순하고 안전한 구조exclusive reset 사용, 시퀀스 자유도 높음
1:N 공유 리셋reset bit → 여러 IP한 비트로 여러 IP가 동시에 리셋됨shared reset 필수, 다른 IP 영향 고려
N:1 다중 리셋여러 reset bit → 단일 IPIP가 여러 리셋 입력을 받음 (기능별 분리)bulk API 또는 순차적 개별 제어
계층적 리셋상위 reset → 하위 reset 게이팅상위 리셋이 하위 모든 IP에 영향상위 리셋 해제 후 하위 리셋 제어
크로스 도메인 리셋한 도메인의 리셋이 다른 도메인에 영향버스 마스터 리셋 시 슬레이브 측 영향도메인 간 동기화 메커니즘 필요
리셋 토폴로지 패턴: 1:1, 1:N 공유, N:1 다중 1:1 단독 리셋 (가장 단순) Reset Bit 0 Reset Bit 1 UART SPI exclusive reset 적합 다른 IP에 영향 없음 1:N 공유 리셋 (주의 필요) Reset Bit 5 I2S Codec Audio DMA shared reset 필수 하나만 assert하면 나머지도 영향 N:1 다중 리셋 (순서 중요) Core RST PHY RST Bus RST USB Ctrl 개별 handle로 순서 제어 bulk API 사용 시 순서 미보장 토폴로지별 드라이버 설계 가이드 1:1 → exclusive reset + 자유로운 assert/deassert 타이밍 1:N → shared reset 필수 + 다른 consumer가 assert 중이어도 내 IP가 동작할 수 있는지 확인 N:1 → 개별 handle + 데이터시트 기반 해제 순서 명시 + error path에서 역순 assert 계층적 → 상위 도메인 리셋 해제 확인 후 하위 리셋 제어 + genpd 연동 검토 크로스 도메인 → 버스 프로토콜의 outstanding transaction 완료 확인 후 리셋 + timeout 처리 토폴로지를 모르고 드라이버를 작성하면, "이상하게 다른 장치도 죽는다" 버그에 빠집니다

계층적 리셋의 실전 사례

일부 SoC에서는 상위 레벨 리셋(예: 전체 미디어 서브시스템 리셋)이 하위 레벨 리셋(예: 개별 코덱(Codec) 리셋)을 게이팅(Gating)합니다. 상위 리셋이 asserted 상태이면 하위 리셋을 deassert해도 IP는 동작하지 않습니다.

/* 계층적 리셋: 상위 도메인 먼저 해제 후 하위 해제 */

static int media_subsystem_init(struct media_dev *mdev)
{
    int ret;

    /* 1. 상위 서브시스템 리셋 해제 (전체 미디어 블록 활성화) */
    ret = reset_control_deassert(mdev->subsys_rst);
    if (ret)
        return ret;

    /* 2. 상위 블록 안정화 대기 */
    usleep_range(50, 100);

    /* 3. 하위 코덱 리셋 해제 (이제 유효해짐) */
    ret = reset_control_deassert(mdev->codec_rst);
    if (ret)
        goto err_subsys;

    /* 4. 하위 DMA 리셋 해제 */
    ret = reset_control_deassert(mdev->dma_rst);
    if (ret)
        goto err_codec;

    return 0;

err_codec:
    reset_control_assert(mdev->codec_rst);
err_subsys:
    reset_control_assert(mdev->subsys_rst);
    return ret;
}
계층적 리셋 주의: 상위 리셋을 assert하면 하위 모든 IP가 함께 리셋됩니다. 하위 IP가 DMA 전송 중이었다면 bus hang이 발생할 수 있습니다. 상위 리셋을 assert하기 전에 하위 모든 IP의 진행 중인 작업을 먼저 중단(quiesce)해야 합니다.

커널 설정(Kconfig) 옵션

리셋 컨트롤러 프레임워크를 사용하려면 커널 빌드 시 관련 설정을 활성화해야 합니다.

Kconfig 옵션의미의존성
CONFIG_RESET_CONTROLLER리셋 컨트롤러 프레임워크 코어 활성화기본 필수 — 이것 없이는 API 자체가 stub
CONFIG_RESET_SIMPLE범용 비트맵 리셋 드라이버CONFIG_RESET_CONTROLLER
CONFIG_RESET_IMX7i.MX7/8 SRC 리셋 드라이버CONFIG_ARCH_MXC
CONFIG_RESET_SUNXIAllwinner sunxi 리셋 드라이버CONFIG_ARCH_SUNXI
CONFIG_RESET_TI_SCITI SCI 기반 리셋 드라이버CONFIG_TI_SCI_PROTOCOL
CONFIG_CLK_ROCKCHIPRockchip CRU (clock + reset 통합)CONFIG_ARCH_ROCKCHIP
# 현재 커널에서 리셋 관련 설정 확인
grep RESET /boot/config-$(uname -r) | grep -v "^#"

# menuconfig에서 위치:
# Device Drivers → Reset Controller Support
CONFIG_RESET_CONTROLLER=n일 때: 이 옵션이 비활성화되면 모든 reset_control_get*() 함수는 -ENOTSUPP를 반환하는 stub으로 대체됩니다. 이 경우 consumer 드라이버가 optional getter를 사용하지 않으면 probe가 실패합니다. 플랫폼에 리셋 하드웨어가 없더라도 CONFIG_RESET_CONTROLLER=y로 두고 optional getter를 사용하는 것이 이식성(Portability) 면에서 유리합니다.

리셋 프레임워크와 유사 메커니즘의 비교

커널에는 "장치를 초기 상태로 되돌리는" 여러 메커니즘이 있습니다. 리셋 컨트롤러 프레임워크와 이들의 차이를 정확히 이해하면 올바른 도구를 선택할 수 있습니다.

메커니즘범위제어 주체API리셋 프레임워크와의 관계
Reset Controller Framework개별 IP 블록 또는 도메인소프트웨어 (드라이버)reset_control_assert/deassert/reset()해당 프레임워크 자체
GPIO Reset외부 칩 또는 보드 레벨 리셋 핀소프트웨어 (GPIO 드라이버)gpio_set_value()보완적 — 외부 IC에 대해 GPIO로 리셋 신호를 제어
Watchdog Reset시스템 전체 또는 특정 도메인하드웨어 타이머watchdog_device별도 — 시스템 수준 비상 리셋
System Restart시스템 전체커널 (kernel_restart())register_restart_handler()별도 — reset controller HW가 제공할 수 있지만 API 범위 밖
Power Domain Off/On전원 도메인 단위genpd / PM 프레임워크pm_genpd_poweron/poweroff()보완적 — 전원 off/on은 리셋의 부수 효과를 가짐
PCIe FLR (Function Level Reset)PCIe function 단위PCIe 표준 메커니즘pci_reset_function()별도 — 버스 표준 리셋 메커니즘
MMIO Soft ResetIP 내부 소프트 리셋 레지스터소프트웨어 (드라이버)직접 writel()비공식 — 프레임워크를 거치지 않는 드라이버 내부 리셋
GPIO Reset과의 조합: 보드(Board)에 외부 PHY나 PMIC의 리셋 핀이 GPIO로 연결된 경우, gpio-reset 드라이버(drivers/reset/reset-gpio.c)를 사용하면 GPIO를 reset controller API로 통합할 수 있습니다. 이를 통해 consumer 드라이버는 리셋 소스가 SoC 내부인지 외부 GPIO인지 신경 쓸 필요 없이 동일한 API로 리셋을 제어할 수 있습니다.
/* GPIO 기반 리셋을 reset controller API로 통합하는 DT 예시 */
phy_reset: reset-gpio {
    compatible = "gpio-reset";
    reset-gpios = <&gpio1 15 GPIO_ACTIVE_LOW>;
    #reset-cells = <0>;
    reset-delay-us = <100>;
    initially-in-reset;
};

ethernet_phy: phy@0 {
    /* consumer는 GPIO인지 SoC 내부인지 몰라도 됨 */
    resets = <&phy_reset>;
    reset-names = "phy";
};

리셋 프레임워크의 발전 과정

리눅스 커널의 리셋 컨트롤러 프레임워크는 커널 3.11(2013년)에 Philipp Zabel이 최초로 도입했습니다. 그 이전에는 각 SoC 벤더 드라이버가 리셋 관련 코드를 자체적으로 구현했으며, 통일된 API가 없었습니다.

커널 버전주요 변경영향
3.11 (2013)reset controller framework 최초 도입기본 assert/deassert/reset API, provider 등록
4.xshared reset, optional getter 추가여러 consumer가 같은 reset line을 공유 가능
5.xbulk/array API, deasserted getter 추가여러 reset을 한 번에 처리하는 편의 API
5.xreleased/acquire-release 모델 추가시간 분할 exclusive 사용 모델
5.xreset_control_rearm() 추가shared pulse reset의 재트리거 지원
6.xdevlink 기반 자동 의존성 추적provider-consumer probe 순서 자동 관리 강화
6.xreset-gpio 드라이버 개선GPIO 기반 리셋을 프레임워크에 통합

프레임워크 도입 이전의 코드에서는 SoC 벤더 BSP에 하드코딩된 레지스터 주소와 비트 조작이 산재해 있었습니다. 현재는 drivers/reset/ 디렉터리에 40개 이상의 provider 드라이버가 있으며, 대부분의 SoC 벤더가 이 프레임워크를 적극적으로 활용하고 있습니다.

커널 소스 탐색 가이드

리셋 컨트롤러 관련 커널 소스를 탐색할 때 참고할 디렉터리와 파일을 정리합니다.

경로내용읽어야 할 때
include/linux/reset.hconsumer API 헤더 — getter, assert, deassert 등consumer 드라이버 작성 시
include/linux/reset-controller.hprovider API 헤더 — reset_controller_dev, reset_control_opsprovider 드라이버 작성 시
drivers/reset/core.c프레임워크 코어 — 핸들 관리, refcount, xlate프레임워크 동작 원리 이해 시
drivers/reset/reset-simple.c범용 비트맵 리셋 드라이버 — 참조 구현새 provider 작성 시 참고용
drivers/reset/reset-gpio.cGPIO 기반 리셋 드라이버보드 레벨 GPIO 리셋 통합 시
drivers/clk/rockchip/rst-*.cRockchip CRU 리셋 드라이버write-mask 방식 참고 시
drivers/reset/reset-imx7.ci.MX SRC 리셋 드라이버signal table 방식 참고 시
drivers/reset/reset-stm32mp1.cSTM32 RCC 리셋 드라이버SET/CLR 레지스터 쌍 방식 참고 시
Documentation/devicetree/bindings/reset/DT 바인딩 문서DT 노드 작성 시
include/dt-bindings/reset/SoC별 리셋 ID 상수 정의DT에서 매직 넘버 대신 상수 사용 시
# 커널 소스에서 리셋 관련 provider 드라이버 목록 확인
ls drivers/reset/reset-*.c

# 특정 SoC의 리셋 ID 상수 확인
cat include/dt-bindings/reset/sun50i-h6-ccu.h

# consumer 드라이버에서 리셋 API 사용 패턴 검색
grep -r "devm_reset_control_get" drivers/ --include="*.c" | head -20

# framework core.c에서 refcount 관련 로직 확인
grep -n "deassert_count" drivers/reset/core.c
새 SoC 지원 시 워크플로: (1) reset-simple.c가 적용 가능한지 먼저 확인, (2) 맞지 않으면 기존 유사 SoC 드라이버를 복사하여 수정, (3) DT 바인딩 문서 작성, (4) include/dt-bindings/reset/에 ID 상수 헤더 추가, (5) consumer 드라이버에서 resets/reset-names 바인딩 사용. 이 순서를 따르면 리뷰 과정이 수월해집니다.

참고자료

추가 참고 자료: 리셋 프레임워크의 공식 커널 문서는 Documentation/driver-api/reset.rst에 있으며, DT 바인딩 규약은 Documentation/devicetree/bindings/reset/reset.txt(또는 YAML 형식의 reset.yaml)에 정의되어 있습니다. 프레임워크 메인테이너(Maintainer) Philipp Zabel의 리뷰 의견은 LKML 아카이브에서 "reset controller" 키워드로 검색하면 실전적인 설계 판단 근거를 많이 찾을 수 있습니다.