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)과 실패 패턴까지 자세히 다룹니다.
핵심 요약
- 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 규칙도 함께 봐야 합니다.
단계별 이해
- reset 선 식별
데이터시트와 DT에서 이 장치가 어떤 reset control과 연결되는지 먼저 찾습니다. - 모델 판단
이 reset이 level 유지형인지, pulse형인지, 공유형인지 단독형인지 구분합니다. - 시퀀스 정리
전원, clock, reset, 펌웨어(Firmware) 로드, MMIO 초기화 순서를 글로 먼저 적어 봅니다. - 에러 경로 설계
probe 실패, suspend/resume 실패, remove 시 reset 상태를 어디로 되돌릴지 결정합니다. - 공유 자원 여부 확인
다른 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마다 크게 다릅니다. 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로 대응하지는 않는다는 것입니다. 하나의 전원 도메인 안에 여러 개의 독립적인 리셋 라인이 존재할 수 있고, 반대로 하나의 리셋 라인이 여러 전원 도메인에 걸쳐 있는 경우도 드물지만 존재합니다.
| 개념 | 의미 | 예시 |
|---|---|---|
| reset line | reset 신호가 실제로 전달되는 물리 선 | 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 consumer | reset 신호를 받아 리셋되는 장치 | UART, USB controller, DMA, PHY, 외부 IC |
consumer 관점의 공식 의미: exclusive와 shared
reset controller API는 reset control을 요청할 때부터 의미를 나눕니다. exclusive reset은 드라이버가 해당 control을 직접 제어한다고 가정하고, shared reset은 여러 소비자가 같은 control을 함께 사용하는 상황을 모델링합니다. 이 차이는 단순한 ownership 표시가 아니라, assert(), deassert(), reset()의 물리적 효과 자체를 바꿉니다.
| 종류 | 보장되는 의미 | 물리적 효과 | 언제 적합한가 |
|---|---|---|---|
| exclusive | 소비자가 실제 reset 상태를 직접 제어 | assert는 즉시 assert, deassert는 즉시 deassert | reset line이 장치 하나 또는 드라이버 한 개만의 책임일 때 |
| shared | deassert 요청에 대한 refcount 의미만 보장 | 첫 deassert와 마지막 assert만 물리 상태를 바꿀 수 있음 | 여러 consumer가 같은 reset control을 함께 써야 할 때 |
공식 문서 기준으로 shared reset은 clock framework의 shared enable과 비슷합니다. 첫 번째 deassert만 실제 선을 deassert할 수 있고, 마지막 assert만 실제 선을 assert할 수 있습니다. 그래서 shared reset에서 reset_control_assert()를 호출했다고 해서 물리 선이 반드시 assert된다고 기대하면 안 됩니다. 다른 소비자가 계속 deassert를 요구하고 있으면 선은 여전히 deassert 상태일 수 있습니다.
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 reset | reset_control_assert(), reset_control_deassert() | 소프트웨어가 상태를 유지함 | assert 상태에서 bus access가 끊길 수 있습니다. |
| self-deasserting pulse reset | reset_control_reset() | 하드웨어가 pulse를 자체 생성 후 자동 해제 | pulse 폭과 내부 시퀀스는 하드웨어가 결정합니다. |
공식 문서는 일반적으로 self-deasserting pulse reset은 shared에 적합하지 않다고 설명합니다. 어느 consumer가 pulse를 요청해도 연결된 모든 주변장치가 reset되기 때문입니다. 다만 API는 shared pulse reset도 허용하며, 그 경우 첫 trigger만 실제 pulse를 내고 이후에는 모든 consumer가 reset_control_rearm()을 호출할 때까지 추가 pulse를 내지 않는 모델을 제공합니다.
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 getter | devm_reset_control_get_exclusive() | 단독 제어 reset을 얻습니다. |
| shared getter | devm_reset_control_get_shared() | shared semantics의 reset을 얻습니다. |
| optional getter | devm_reset_control_get_optional_exclusive() | DT에 reset이 없으면 NULL을 반환합니다. |
| deasserted getter | devm_reset_control_get_shared_deasserted() | get과 deassert를 묶은 편의 API입니다. |
| released getter | reset_control_get_exclusive_released() | exclusive지만 처음에는 acquired 상태가 아닌 핸들을 얻습니다. |
| acquire/release | reset_control_acquire(), reset_control_release() | temporarily exclusive 핸들에 대한 실제 획득/반납입니다. |
| bulk/array | devm_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);
*_exclusive_released()와 reset_control_acquire()/release() 조합이 필요합니다. 평소엔 놓아두고, 특정 민감 구간에서만 잠그는 모델입니다.
shared reset의 정확한 의미와 금지 패턴
shared reset은 가장 자주 오해되는 부분입니다. 공식 문서는 shared reset에 대해 아래 제약을 분명히 둡니다.
| 규칙 | 이유 |
|---|---|
deassert와 assert 호출 수는 균형을 맞춰야 함 | 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 간 의존성이 있으면 수동 순서 제어가 낫습니다. |
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_dev | provider의 핵심 descriptor | ops, DT xlate 정보, reset 개수, owner 등을 포함합니다. |
reset_control_ops | assert/deassert/reset/status 콜백 집합 | 모든 콜백이 필수는 아니지만, consumer 기대치와 맞아야 합니다. |
of_reset_n_cells | DT phandle 뒤 인자 개수 | #reset-cells와 맞아야 합니다. |
of_xlate | DT 인자를 내부 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의 동작이 일치합니다.
| 콜백 | 호출 조건 | 반환값 | 구현 필수 여부 |
|---|---|---|---|
.assert | exclusive: 항상 호출 shared: deassert_count가 1→0일 때만 호출 | 0 = 성공, 음수 = 에러 | level reset 지원 시 필수 |
.deassert | exclusive: 항상 호출 shared: deassert_count가 0→1일 때만 호출 | 0 = 성공, 음수 = 에러 | level reset 지원 시 필수 |
.reset | exclusive: 항상 호출 shared: triggered_count가 0→1일 때만 호출 | 0 = 성공, 음수 = 에러 | pulse reset 지원 시 필수 |
.status | reset_control_status() 호출 시 | 1 = asserted, 0 = deasserted, 음수 = 에러 | 선택 (미구현 시 -ENOTSUPP) |
프레임워크가 .assert/.deassert를 구현하지 않은 provider에 대해 reset_control_reset()이 호출되면, .reset 콜백이 있으면 그것을 호출하고, 없으면 .assert → delay → .deassert 시퀀스를 자동으로 수행합니다. 반대로 .reset만 구현한 provider에 대해 reset_control_assert()가 호출되면 -ENOTSUPP가 반환됩니다.
provider 등록과 DT xlate
Device Tree에서 consumer는 보통 resets = <&rst N> 형태로 provider를 참조합니다. provider는 자신이 phandle arguments를 몇 개 받는지 #reset-cells로 정의하고, 커널에서는 of_reset_n_cells와 of_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-cells | provider가 phandle 뒤에 받는 인자 수 | provider의 of_reset_n_cells와 맞아야 합니다. |
resets | consumer가 사용하는 reset control 참조 | 여러 개일 경우 순서가 ABI가 됩니다. |
reset-names | consumer가 reset 의미를 이름으로 구분 | index 의존 코드를 피하게 해 줍니다. |
reset-names를 두는 편이 좋습니다. 인덱스 0이 "core"인지 "phy"인지 driver가 암묵적으로 알고 있어야 하는 구조는 코드 리뷰와 보드 파생 관리에 취약합니다.
소비자 드라이버 구현 패턴
실전 consumer driver는 보통 다음 구조를 가집니다. reset만 따로 보는 것이 아니라 regulator, clock, runtime PM, firmware load와 한 흐름으로 다뤄야 합니다.
- 필요한 reset handle을 얻습니다.
- runtime PM 또는 power domain을 올립니다.
- regulator와 clock을 준비합니다.
- 필요 시 assert 후 최소 폭을 지킨 뒤 deassert합니다.
- MMIO 초기화와 firmware download를 수행합니다.
- 에러 시 되돌리는 순서를 명확히 정합니다.
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/enable | state machine이 clocked 상태가 되게 함 |
| 4. reset deassert 또는 pulse trigger | 장치를 실제 동작 상태로 전환 |
| 5. MMIO 초기화 / firmware load | 동작 조건 설정 |
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 polling | reset 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가 지원하는 경우에만 의미 있음 |
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);
}
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 상태를 함께 기록하는 편이 훨씬 효과적입니다.
- DT binding 확인
resets,reset-names, provider#reset-cells가 실제 driver 기대와 맞는지 먼저 확인합니다. - probe ordering 확인
provider가 먼저 probe되었는지, consumer가-EPROBE_DEFER를 적절히 처리하는지 확인합니다. - 시퀀스 로깅
regulator on, clock enable, reset deassert, MMIO init 순서를 로그로 남깁니다. - 딜레이 검증
assert 폭과 deassert 후 안정화 시간이 데이터시트 최소값을 만족하는지 확인합니다. - 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 reset | DT 이름 불일치, provider 미등록, phandle 인덱스 오류 | reset-names, provider probe, -EPROBE_DEFER | dev_err_probe() 사용, DT binding 정리 |
| deassert 후 첫 MMIO read에서 hang | clock off, PM domain off, 전압 미안정 | clock/regulator/runtime PM 순서 | reset deassert 전 bring-up 순서 수정 |
| shared reset에서 갑자기 다른 장치도 죽음 | pulse reset 또는 exclusive 가정 오류 | line 공유 여부, provider topology | shared 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 모델 통일 |
끝까지 따라가는 상태 전이 예제
아래는 USB controller가 cold boot와 runtime resume에서 reset을 어떻게 다르게 다뤄야 하는지 보여 주는 예제입니다.
| 시점 | 사건 | reset 관점 | 다른 서브시스템 | 사용자에게 보이는 증상 |
|---|---|---|---|---|
| T0 | bootloader가 core clock만 켜 둠 | reset 상태는 firmware 기본값 | bootloader handoff | 초기 콘솔은 살아 보일 수 있음 |
| T1 | provider probe | reset_controller_dev 등록 | clock/reset provider 초기화 | consumer는 아직 defer 가능 |
| T2 | USB driver probe | core, phy reset handle 획득 | DT parse | binding 틀리면 probe 실패 |
| T3 | power + clock 준비 | 아직 reset 유지 또는 pulse 전 | runtime PM, regulator, CCF | 순서가 틀리면 MMIO hang |
| T4 | reset deassert / pulse | 장치 내부 상태기 시작 | clock active | 이후 firmware load 가능 |
| T5 | runtime suspend | assert 유지 여부는 retention 정책에 따름 | clock off, regulator off 가능 | resume 설계 미흡 시 장치 사망 |
| T6 | runtime resume | 필요 시 reset 재트리거 또는 deassert 재확인 | PM restore | cold boot만 성공하는 버그가 드러남 |
프레임워크 내부 구조: 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 line(같은 provider + 같은 id)에 대해 shared 요청이 들어오면 프레임워크는 기존 reset_control 핸들의 refcnt를 증가시키고 같은 핸들을 반환합니다. exclusive 요청이 들어오면 해당 line에 이미 다른 핸들이 있으면 -EBUSY를 반환합니다. 이 구분은 __reset_control_get() 내부에서 이루어지며, 한 번 shared로 열린 line은 exclusive로 재요청할 수 없고 그 반대도 마찬가지입니다.
__reset_control_get() 내부 흐름 상세
consumer가 devm_reset_control_get_*()를 호출하면 프레임워크 내부에서는 다음 단계를 거칩니다.
- DT 파싱(Parsing): consumer의
resets속성에서 phandle과 인자를 추출합니다.reset-names가 있으면 이름으로 인덱스를 결정합니다. - provider 탐색: 전역
reset_controller_list에서 phandle에 해당하는reset_controller_dev를 찾습니다. 없으면-EPROBE_DEFER를 반환합니다. - xlate: provider의
of_xlate콜백(또는 기본 xlate)을 호출하여 DT 인자를 내부 reset line ID로 변환합니다. - 기존 핸들 검색: 같은 provider + 같은 ID를 가진
reset_control이 이미 있는지reset_control_head리스트를 순회합니다. - 호환성 검사: 기존 핸들이 있으면 shared/exclusive 호환성을 확인합니다. 불호환이면
-EBUSY입니다. - 핸들 생성 또는 재사용: 호환되면 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 | -EBUSY | refcnt 증가 |
| shared | -EBUSY | refcnt 증가 | -EBUSY |
deassert_count는 reset_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.c | level (bit set/clear) | clock-reset 통합 CRU, #reset-cells = 1 |
| Rockchip | drivers/clk/rockchip/rst-*.c | level (bit set/clear) | CCF provider와 같은 module에서 reset 등록 |
| TI (OMAP/AM) | drivers/reset/reset-ti-sci.c | SCI message 기반 | SCI firmware로 reset 요청을 전달하는 간접 모델 |
| STM32 | drivers/reset/reset-stm32mp1.c | level + self-clearing | RCC 레지스터에서 clock과 reset을 함께 관리 |
| i.MX (NXP) | drivers/reset/reset-imx7.c | SCU/SRC 기반 | System Reset Controller 하드웨어 블록 |
| MediaTek | drivers/clk/mediatek/reset.c | level (set/clear 레지스터 분리) | SET/CLR 레지스터 쌍으로 atomic bit 조작 |
| Simple reset | drivers/reset/reset-simple.c | level (공통 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 벤더 | 레지스터 방식 | 하드웨어 원자성 | SW 락 필요 | 레지스터 밀도 |
|---|---|---|---|---|
| Rockchip | Write Mask (상위 16비트 = mask) | 지원 | 불필요 | 레지스터당 16개 리셋 라인 |
| STM32, MediaTek | SET/CLR 레지스터 쌍 | 지원 | 불필요 | 레지스터당 32개 리셋 라인 |
| Allwinner | 단일 레지스터 RMW | 미지원 | spinlock 필수 | 레지스터당 32개 리셋 라인 |
| NXP i.MX | 개별 신호 테이블 기반 | regmap 의존 | regmap 내부 락 | 신호마다 다름 |
| TI (SCI 기반) | SCI 메시지 통신 | SCI FW 보장 | 불필요 | 해당 없음 (메시지 기반) |
| Qualcomm | self-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,
};
reset-simple의 active_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 |
|---|---|---|---|---|
| 단일 레지스터 RMW | read → set bit → write | read → clear bit → write | spinlock 필수 | Allwinner, Rockchip |
| SET/CLR 분리 | SET 레지스터에 비트 write | CLR 레지스터에 비트 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");
위 코드에서 주목할 점은 다음과 같습니다.
devm_reset_controller_register()를 사용하면 driver detach 시 자동으로 등록 해제됩니다. 별도의 remove 함수가 필요 없습니다.of_xlate를 NULL로 두면 프레임워크 기본 xlate 함수가 사용됩니다.#reset-cells = <1>인 경우 phandle 인자를 그대로 reset line ID로 사용합니다.- spinlock은 CTRL 레지스터가 RMW 방식이므로 필수입니다. PULSE 레지스터는 write-only이므로 락 없이도 안전합니다.
.status콜백은 별도의 STATUS 레지스터를 읽어 현재 assert 상태를 반환합니다. 지원하지 않는 하드웨어에서는 이 콜백을 생략할 수 있습니다.
대응하는 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() 내부 함수를 만들어 재사용하면 시퀀스 불일치 버그를 예방할 수 있습니다. 위 코드에서 example_usb_bring_up()은 runtime_resume에서도 그대로 호출할 수 있습니다.
전원 관리와 리셋의 통합
리셋 컨트롤러는 전원 관리(Power Management) 서브시스템과 밀접하게 연결됩니다. 전원 도메인(Power Domain)이 꺼지면 해당 도메인 내의 모든 리셋 상태가 사라지고, 전원이 다시 켜지면 리셋 라인이 하드웨어 기본값(보통 asserted)으로 돌아갑니다.
전원 도메인과 리셋의 관계
| 전원 상태 | 리셋 라인 | 레지스터 접근 | 드라이버 책임 |
|---|---|---|---|
| Always-on domain | SW가 설정한 상태 유지 | 항상 가능 | 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;
}
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";
};
include/dt-bindings/clock/sun50i-h6-ccu.h와 include/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);
}
/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 get | NULL 반환 (성공) | 이후 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 off | context 완전 소멸 | 반드시 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);
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_DEFER | reset_control_get*() | provider가 아직 등록되지 않음 | provider probe 순서 확인, dev_err_probe() 사용 |
-ENOENT | reset_control_get*() | DT에 resets 속성이 없음 | DT 바인딩 확인, optional getter 사용 고려 |
-EBUSY | reset_control_get_exclusive() | 이미 다른 consumer가 exclusive 핸들을 가지고 있음 | shared getter 사용 또는 다른 consumer 확인 |
-EINVAL | of_xlate() | phandle 인자가 범위를 벗어남 | #reset-cells와 인자 값 확인 |
-ENOTSUPP | reset_control_status() | provider가 .status 콜백을 구현하지 않음 | status 조회 불가 — 다른 방법으로 상태 확인 |
-EPERM | reset_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의 리셋 모델이 실제 하드웨어 타이밍을 반영하지 못함 | 실물 보드에서 테스트, 데이터시트 타이밍 준수 확인 |
devlink를 통한 provider-consumer 관계 추적
커널 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 {})"' \;
consumer probe는 consumer가 아직 probe되지 않았음을, active는 양쪽 모두 정상임을, available는 supplier만 준비되었음을 의미합니다. reset 관련 probe defer가 의심될 때 devlink status를 먼저 확인하면 원인을 빠르게 좁힐 수 있습니다.
테스트와 검증 전략
reset 동작은 하드웨어 의존적이라 순수 유닛 테스트가 어렵지만, 다음 전략으로 커버리지를 높일 수 있습니다.
| 테스트 방법 | 검증 대상 | 명령어 / 도구 |
|---|---|---|
| module reload | probe/remove 시 reset 상태 정합성 | modprobe -r my_driver && modprobe my_driver |
| runtime PM 강제 toggle | suspend/resume reset 복원 | echo on > /sys/devices/.../power/control |
| fault injection | probe error path에서 leak 없는지 | echo 1 > /sys/kernel/debug/fail_function/... |
| DT overlay 변형 | optional reset 누락, 이름 변경 시 동작 | DT overlay로 resets 속성 제거 후 boot |
| ftrace function_graph | reset 콜백 호출 순서 추적 | 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"
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 reset | AXI/AHB 버스 인터페이스 | clock enable 직후 (가장 먼저) | 마지막 (clock disable 직전) |
| PHY reset | USB PHY 아날로그 블록 | bus reset 해제 후, PHY 초기화 전 | core reset assert 후 |
| core reset | USB 컨트롤러 코어 로직 | 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 TX | core + PHY 분리 | PHY PLL 안정화 후 core 리셋 해제 |
| MIPI DSI | host + D-PHY 분리 | LP-11 상태 진입 후 리셋 해제 순서 중요 |
| DisplayPort | AUX 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 순서를 코드 전에 문서화하면 실수가 줄어듭니다. |
| 3 | exclusive/shared를 처음부터 확정하라 | 나중에 바꾸면 다른 consumer와의 호환성 문제가 발생합니다. |
| 4 | optional getter를 적극 활용하라 | 플랫폼 간 이식성을 높이고 #ifdef를 줄여줍니다. |
| 5 | dev_err_probe()로 에러를 반환하라 | -EPROBE_DEFER 시 불필요한 에러 로그를 방지합니다. |
| 6 | bring-up/shut-down을 내부 함수로 분리하라 | probe, resume, error-path에서 시퀀스 불일치를 방지합니다. |
| 7 | 에러 경로에서 역순 cleanup을 빠뜨리지 마라 | deassert 없는 assert, clock enable 없는 reset deassert는 리소스 누수입니다. |
| 8 | resume 경로를 cold boot와 별도로 검증하라 | 부트로더 의존성은 resume에서만 드러납니다. |
| 9 | bulk/array API는 순서 무관한 경우에만 사용하라 | 순서 의존 하드웨어에서는 개별 handle이 더 안전합니다. |
| 10 | pulse reset과 level reset을 절대 혼용하지 마라 | 같은 핸들에서 reset()과 assert()/deassert()를 섞으면 의미가 깨집니다. |
안티패턴(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 corruption | shared에서는 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 + optional | ERR_PTR() (실제 에러만) |
reset_control_assert(rstc) | int | assert (shared: refcount 감소) | provider ops 에러 |
reset_control_deassert(rstc) | int | deassert (shared: refcount 증가) | provider ops 에러 |
reset_control_reset(rstc) | int | pulse trigger | provider ops 에러 |
reset_control_rearm(rstc) | int | shared pulse를 재트리거 가능 상태로 | provider ops 에러 |
reset_control_status(rstc) | int | assert 상태 조회 (1=asserted) | -ENOTSUPP (미구현 시) |
reset_control_acquire(rstc) | int | released 핸들을 잠시 독점 획득 | -EBUSY |
reset_control_release(rstc) | void | acquire한 핸들을 반납 | - |
#ifdef 없이 코드를 작성할 수 있습니다.
실전 체크리스트: 흔히 놓치는 함정
reset controller 관련 코드를 작성하거나 리뷰할 때 다음 항목을 점검하면 대부분의 문제를 예방할 수 있습니다.
| # | 체크 항목 | 확인 방법 |
|---|---|---|
| 1 | reset deassert 전에 clock이 활성화되어 있는가? | probe 순서에서 clk_prepare_enable()이 reset_control_deassert() 앞에 오는지 확인 |
| 2 | shared reset에서 assert/deassert 호출 수가 균형을 이루는가? | error path와 remove에서 누락된 assert가 없는지 추적 |
| 3 | pulse reset과 level reset을 혼용하고 있지 않은가? | reset_control_reset()과 assert()/deassert()가 같은 핸들에 사용되지 않는지 확인 |
| 4 | bulk/array API를 사용하는 장치에 순서 의존성이 없는가? | 데이터시트에서 reset 해제 순서 요구사항 확인 |
| 5 | dev_err_probe()를 사용하여 EPROBE_DEFER를 자연스럽게 처리하는가? | PTR_ERR() 직접 반환 코드를 dev_err_probe()로 교체 |
| 6 | resume 경로에서 cold boot와 동일한 reset 시퀀스를 실행하는가? | runtime_resume 콜백에서 reset deassert가 포함되어 있는지 확인 |
| 7 | reset assert 후 deassert 전에 데이터시트 최소 폭을 지키는가? | usleep_range() 등으로 최소 pulse width를 보장하는지 확인 |
| 8 | provider의 #reset-cells와 consumer의 resets phandle 인자 수가 일치하는가? | DT 컴파일 경고와 부팅 시 xlate 에러 확인 |
| 9 | remove/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 → 단일 IP | IP가 여러 리셋 입력을 받음 (기능별 분리) | bulk API 또는 순차적 개별 제어 |
| 계층적 리셋 | 상위 reset → 하위 reset 게이팅 | 상위 리셋이 하위 모든 IP에 영향 | 상위 리셋 해제 후 하위 리셋 제어 |
| 크로스 도메인 리셋 | 한 도메인의 리셋이 다른 도메인에 영향 | 버스 마스터 리셋 시 슬레이브 측 영향 | 도메인 간 동기화 메커니즘 필요 |
계층적 리셋의 실전 사례
일부 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;
}
커널 설정(Kconfig) 옵션
리셋 컨트롤러 프레임워크를 사용하려면 커널 빌드 시 관련 설정을 활성화해야 합니다.
| Kconfig 옵션 | 의미 | 의존성 |
|---|---|---|
CONFIG_RESET_CONTROLLER | 리셋 컨트롤러 프레임워크 코어 활성화 | 기본 필수 — 이것 없이는 API 자체가 stub |
CONFIG_RESET_SIMPLE | 범용 비트맵 리셋 드라이버 | CONFIG_RESET_CONTROLLER |
CONFIG_RESET_IMX7 | i.MX7/8 SRC 리셋 드라이버 | CONFIG_ARCH_MXC |
CONFIG_RESET_SUNXI | Allwinner sunxi 리셋 드라이버 | CONFIG_ARCH_SUNXI |
CONFIG_RESET_TI_SCI | TI SCI 기반 리셋 드라이버 | CONFIG_TI_SCI_PROTOCOL |
CONFIG_CLK_ROCKCHIP | Rockchip 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 Reset | IP 내부 소프트 리셋 레지스터 | 소프트웨어 (드라이버) | 직접 writel() | 비공식 — 프레임워크를 거치지 않는 드라이버 내부 리셋 |
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.x | shared reset, optional getter 추가 | 여러 consumer가 같은 reset line을 공유 가능 |
| 5.x | bulk/array API, deasserted getter 추가 | 여러 reset을 한 번에 처리하는 편의 API |
| 5.x | released/acquire-release 모델 추가 | 시간 분할 exclusive 사용 모델 |
| 5.x | reset_control_rearm() 추가 | shared pulse reset의 재트리거 지원 |
| 6.x | devlink 기반 자동 의존성 추적 | provider-consumer probe 순서 자동 관리 강화 |
| 6.x | reset-gpio 드라이버 개선 | GPIO 기반 리셋을 프레임워크에 통합 |
프레임워크 도입 이전의 코드에서는 SoC 벤더 BSP에 하드코딩된 레지스터 주소와 비트 조작이 산재해 있었습니다. 현재는 drivers/reset/ 디렉터리에 40개 이상의 provider 드라이버가 있으며, 대부분의 SoC 벤더가 이 프레임워크를 적극적으로 활용하고 있습니다.
커널 소스 탐색 가이드
리셋 컨트롤러 관련 커널 소스를 탐색할 때 참고할 디렉터리와 파일을 정리합니다.
| 경로 | 내용 | 읽어야 할 때 |
|---|---|---|
include/linux/reset.h | consumer API 헤더 — getter, assert, deassert 등 | consumer 드라이버 작성 시 |
include/linux/reset-controller.h | provider API 헤더 — reset_controller_dev, reset_control_ops | provider 드라이버 작성 시 |
drivers/reset/core.c | 프레임워크 코어 — 핸들 관리, refcount, xlate | 프레임워크 동작 원리 이해 시 |
drivers/reset/reset-simple.c | 범용 비트맵 리셋 드라이버 — 참조 구현 | 새 provider 작성 시 참고용 |
drivers/reset/reset-gpio.c | GPIO 기반 리셋 드라이버 | 보드 레벨 GPIO 리셋 통합 시 |
drivers/clk/rockchip/rst-*.c | Rockchip CRU 리셋 드라이버 | write-mask 방식 참고 시 |
drivers/reset/reset-imx7.c | i.MX SRC 리셋 드라이버 | signal table 방식 참고 시 |
drivers/reset/reset-stm32mp1.c | STM32 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
reset-simple.c가 적용 가능한지 먼저 확인, (2) 맞지 않으면 기존 유사 SoC 드라이버를 복사하여 수정, (3) DT 바인딩 문서 작성, (4) include/dt-bindings/reset/에 ID 상수 헤더 추가, (5) consumer 드라이버에서 resets/reset-names 바인딩 사용. 이 순서를 따르면 리뷰 과정이 수월해집니다.
참고자료
- docs.kernel.org — Reset Controller API — reset controller 프레임워크의 공식 커널 API 문서입니다
- drivers/reset/core.c — Bootlin Elixir — reset controller 프레임워크 코어 구현입니다
- include/linux/reset.h — Bootlin Elixir — consumer API 헤더 정의를 확인할 수 있습니다
- include/linux/reset-controller.h — Bootlin Elixir — provider API와 reset_controller_dev 구조체 정의입니다
- drivers/reset/reset-simple.c — Bootlin Elixir — 가장 기본적인 MMIO 기반 reset provider 구현 참고입니다
- LWN: Reset controller subsystem — reset controller 서브시스템 도입 배경과 설계를 설명합니다
- Reset Controller DT Bindings — reset controller Device Tree 바인딩 디렉터리입니다
- reset.txt DT 바인딩 문서 — Bootlin Elixir — reset provider/consumer DT 바인딩 규약 원본입니다
- drivers/reset/ 디렉터리 — Bootlin Elixir — SoC별 reset provider 드라이버 전체 목록을 확인할 수 있습니다
- include/dt-bindings/reset/ — Bootlin Elixir — SoC별 reset ID 상수 헤더 디렉터리입니다
관련 문서
- Common Clock Framework — reset과 같이 맞물리는 clock 시퀀스
- Regulator 프레임워크 — reset 전후 전압 레일과 DVFS 순서
- Device Tree —
resets,reset-names,#reset-cells바인딩 - 전원 관리 — runtime PM, suspend/resume 복구
- 디바이스 드라이버 — probe/remove와 devm 리소스 관리
- USB — PHY/core reset과 suspend/resume 복구의 대표 사례
- Watchdog — watchdog timeout 시 system reset과의 관계
- hwmon — 센서 장치의 reset 및 초기화 시퀀스
- PWM — PWM 컨트롤러의 reset 처리와 SoC 초기화
- 인터럽트 — reset 후 인터럽트 재등록과 상태 복원
- NVMEM 프레임워크 — eFuse/OTP 읽기 전 reset 요구사항
- DMA 엔진 — DMA 전송 중 리셋의 위험과 quiesce 패턴
Documentation/driver-api/reset.rst에 있으며, DT 바인딩 규약은 Documentation/devicetree/bindings/reset/reset.txt(또는 YAML 형식의 reset.yaml)에 정의되어 있습니다. 프레임워크 메인테이너(Maintainer) Philipp Zabel의 리뷰 의견은 LKML 아카이브에서 "reset controller" 키워드로 검색하면 실전적인 설계 판단 근거를 많이 찾을 수 있습니다.