</>PatchNote
목록으로

2026-01-08

리눅스 커널 워치독 드라이버 뜯어보기: s3c2410_wdt

Linux KernelDevice DriverEmbedded

삼성전자 DS AP S/W 인턴 시절 임베디드 SoC의 BSP 커널 드라이버를 만졌다. 그때 다룬 코드 자체는 공개할 수 없으니, 같은 계열의 메인라인 리눅스 워치독 드라이버를 대신 뜯어보며 정리한다. 대상은 drivers/watchdog/s3c2410_wdt.c — S3C2410부터 최신 Exynos까지 쓰이는 Samsung SoC 워치독 드라이버다. (아래 코드는 모두 공개된 mainline 커널 소스 기준)

워치독이 하는 일

워치독 타이머는 "정해진 시간 안에 쓰다듬어(kick) 주지 않으면 시스템을 리셋" 하는 하드웨어다. 커널이나 유저 프로세스가 hang에 빠져 주기적인 kick을 못 하면, 타이머가 만료되어 SoC를 강제로 재부팅해 복구한다. 임베디드에서 필수적인 안전장치다.

리눅스는 이걸 워치독 코어(drivers/watchdog/watchdog_core.c, watchdog_dev.c)로 추상화한다. 개별 SoC 드라이버는 struct watchdog_devicestruct watchdog_ops만 채워서 watchdog_register_device()로 등록하면, 코어가 /dev/watchdog 캐릭터 디바이스와 ioctl 인터페이스를 제공한다.

레지스터 3개로 끝나는 하드웨어

s3c2410 워치독의 하드웨어 인터페이스는 놀랄 만큼 단순하다.

#define S3C2410_WTCON    0x00   // control
#define S3C2410_WTDAT    0x04   // reload count (만료 시 다시 채울 값)
#define S3C2410_WTCNT    0x08   // current count (실제로 감소하는 카운터)
#define S3C2410_WTCLRINT 0x0c   // interrupt clear (신형 SoC)
  • WTDAT: 카운터가 0이 되면 다시 채워 넣을 값(reload).
  • WTCNT: 지금 감소 중인 카운터. 이 값이 0에 도달하면 리셋/인터럽트 발생.
  • WTCON: 활성화·리셋 인에이블·분주비를 담는 제어 레지스터.

WTCON의 비트 정의를 보면 이 드라이버가 무엇을 제어하는지 한눈에 들어온다.

#define S3C2410_WTCON_RSTEN   BIT(0)   // 만료 시 SoC 리셋
#define S3C2410_WTCON_INTEN   BIT(2)   // 만료 시 인터럽트
#define S3C2410_WTCON_ENABLE  BIT(5)   // 워치독 동작
#define S3C2410_WTCON_DIV16   (0 << 3) // 클럭 분주 (16/32/64/128)
#define S3C2410_WTCON_DIV128  (3 << 3)
#define S3C2410_WTCON_PRESCALE(x)  ((x) << 8)  // 8비트 프리스케일러

즉 워치독 클럭 = PCLK / (prescaler+1) / division. 이 두 단계 분주로 타임아웃을 맞춘다.

start / stop / keepalive

watchdog_ops에 등록되는 핵심 콜백 세 개를 보자.

start — 카운터를 세팅하고 워치독을 켠다.

static int s3c2410wdt_start(struct watchdog_device *wdd)
{
	unsigned long wtcon;
	struct s3c2410_wdt *wdt = watchdog_get_drvdata(wdd);
	unsigned long flags;

	spin_lock_irqsave(&wdt->lock, flags);
	__s3c2410wdt_stop(wdt);                 // 일단 끄고 깨끗한 상태에서 시작

	wtcon = readl(wdt->reg_base + S3C2410_WTCON);
	wtcon |= S3C2410_WTCON_ENABLE | S3C2410_WTCON_DIV128;

	if (soft_noboot) {                      // 디버그용: 리셋 대신 인터럽트만
		wtcon |= S3C2410_WTCON_INTEN;
		wtcon &= ~S3C2410_WTCON_RSTEN;
	} else {                                // 실제: 만료 시 SoC 리셋
		wtcon &= ~S3C2410_WTCON_INTEN;
		wtcon |= S3C2410_WTCON_RSTEN;
	}

	writel(wdt->count, wdt->reg_base + S3C2410_WTDAT);
	writel(wdt->count, wdt->reg_base + S3C2410_WTCNT);
	writel(wtcon, wdt->reg_base + S3C2410_WTCON);
	spin_unlock_irqrestore(&wdt->lock, flags);
	return 0;
}

soft_noboot 모듈 파라미터가 인상적이다. 개발 중에는 워치독이 진짜로 보드를 리부팅하면 디버깅이 불가능하니, RSTEN 대신 INTEN만 켜서 리셋 대신 인터럽트만 받게 한다.

stop — ENABLE과 RSTEN 비트를 내리는 게 전부다.

static void __s3c2410wdt_stop(struct s3c2410_wdt *wdt)
{
	unsigned long wtcon = readl(wdt->reg_base + S3C2410_WTCON);
	wtcon &= ~(S3C2410_WTCON_ENABLE | S3C2410_WTCON_RSTEN);
	writel(wtcon, wdt->reg_base + S3C2410_WTCON);
}

keepalive(ping) — "쓰다듬기". WTCNT에 reload 값을 다시 써서 카운터를 리셋한다.

static int s3c2410wdt_keepalive(struct watchdog_device *wdd)
{
	struct s3c2410_wdt *wdt = watchdog_get_drvdata(wdd);
	unsigned long flags;

	spin_lock_irqsave(&wdt->lock, flags);
	writel(wdt->count, wdt->reg_base + S3C2410_WTCNT);   // 이 한 줄이 곧 "kick"
	spin_unlock_irqrestore(&wdt->lock, flags);
	return 0;
}

워치독의 본질이 이 한 줄에 담겨 있다. 유저스페이스가 /dev/watchdog에 주기적으로 write 하면, 코어가 이 ping을 호출해 WTCNT를 다시 채운다. write가 멈추면 카운터가 0으로 흘러 리셋이 걸린다.

타임아웃을 레지스터 값으로 환산하기

set_heartbeat은 "몇 초"라는 유저의 요구를 하드웨어의 prescaler/count로 번역한다.

static int s3c2410wdt_set_heartbeat(struct watchdog_device *wdd,
				    unsigned int timeout)
{
	struct s3c2410_wdt *wdt = watchdog_get_drvdata(wdd);
	unsigned long freq = s3c2410wdt_get_freq(wdt);
	unsigned long count;
	unsigned int divisor = 1;
	unsigned long wtcon;

	if (timeout < 1)
		return -EINVAL;

	freq = DIV_ROUND_UP(freq, 128);        // DIV128 반영
	count = timeout * freq;                // 필요한 총 tick 수

	if (count > wdt->max_cnt) {             // 카운터 폭을 넘으면 prescaler로 추가 분주
		divisor = DIV_ROUND_UP(count, wdt->max_cnt);
		if (divisor > S3C2410_WTCON_PRESCALE_MAX + 1) {
			dev_err(wdt->dev, "timeout %d too big\n", timeout);
			return -EINVAL;
		}
	}

	count = DIV_ROUND_UP(count, divisor);
	wdt->count = count;

	wtcon = readl(wdt->reg_base + S3C2410_WTCON);
	wtcon &= ~S3C2410_WTCON_PRESCALE_MASK;
	wtcon |= S3C2410_WTCON_PRESCALE(divisor - 1);

	writel(count, wdt->reg_base + S3C2410_WTDAT);
	writel(wtcon, wdt->reg_base + S3C2410_WTCON);

	wdd->timeout = (count * divisor) / freq;  // 실제 반영된 타임아웃 역산
	return 0;
}

핵심 로직은 이렇다.

  1. 소스 클럭을 128로 나눈다(고정 division).
  2. timeout * freq로 필요한 tick 수를 구한다.
  3. 그 값이 카운터 폭(max_cnt)을 넘으면, prescaler로 추가 분주해서 카운터 안에 들어오게 한다.
  4. 실제 반영된 값으로 wdd->timeout을 역산해 유저에게 정직하게 돌려준다.

freq는 어디서 오냐 하면:

static inline unsigned long s3c2410wdt_get_freq(struct s3c2410_wdt *wdt)
{
	return clk_get_rate(wdt->src_clk ? wdt->src_clk : wdt->bus_clk);
}

즉 하드코딩이 아니라 클럭 프레임워크에서 실제 동작 주파수를 읽어 계산한다. 보드마다 PCLK가 다르므로 이렇게 해야 이식성이 생긴다.

코어와 연결되는 지점: watchdog_ops

드라이버가 채우는 인터페이스는 결국 이 구조체다.

static const struct watchdog_ops s3c2410wdt_ops = {
	.owner       = THIS_MODULE,
	.start       = s3c2410wdt_start,
	.stop        = s3c2410wdt_stop,
	.ping        = s3c2410wdt_keepalive,
	.set_timeout = s3c2410wdt_set_heartbeat,
	.restart     = s3c2410wdt_restart,
};

#define OPTIONS (WDIOF_SETTIMEOUT | WDIOF_KEEPALIVEPING | WDIOF_MAGICCLOSE)
  • WDIOF_MAGICCLOSE — 유저가 /dev/watchdog를 그냥 닫으면 워치독이 멈추면 안 된다. 멈추려면 닫기 전에 매직 문자 'V' 를 써야 한다. 실수로 프로세스가 죽어 fd가 닫혀도 워치독이 계속 돌게 하는 안전장치다.
  • nowayout 모듈 파라미터가 켜져 있으면 한 번 시작한 워치독은 아예 멈출 수 없다. 운영 환경에서 워치독이 꺼지는 사고를 원천 차단한다.

restart는 워치독으로 의도적인 시스템 리셋을 거는 콜백이다. 타임아웃을 최소로 잡고 바로 만료시키면 SoC 리셋 경로를 재활용해 재부팅할 수 있다.

Exynos: PMU와 quirk

여기서부터가 실제 SoC 통합의 복잡함이다. 신형 Exynos에서는 워치독 리셋이 PMU(Power Management Unit) 를 거친다. 그래서 드라이버는 SoC별 차이를 quirks 플래그와 variant 구조체로 흡수한다.

#define QUIRK_HAS_WTCLRINT_REG     BIT(0)
#define QUIRK_HAS_PMU_MASK_RESET   BIT(1)
#define QUIRK_HAS_PMU_RST_STAT     BIT(2)
#define QUIRK_HAS_PMU_AUTO_DISABLE BIT(3)
#define QUIRK_HAS_PMU_CNT_EN       BIT(4)

struct s3c2410_wdt_variant {
	int disable_reg;
	int mask_reset_reg;
	bool mask_reset_inv;
	int mask_bit;
	int rst_stat_reg;
	int rst_stat_bit;
	int cnt_en_reg;
	int cnt_en_bit;
	u32 quirks;
};

PMU 레지스터 접근은 regmap(syscon)으로 추상화되어 있다. 예를 들어 워치독 리셋을 마스킹/언마스킹하는 함수는 이렇다.

static int s3c2410wdt_mask_wdt_reset(struct s3c2410_wdt *wdt, bool mask)
{
	const u32 mask_val = BIT(wdt->drv_data->mask_bit);
	const bool val_inv = wdt->drv_data->mask_reset_inv;
	const u32 val = (mask ^ val_inv) ? mask_val : 0;

	return regmap_update_bits(wdt->pmureg, wdt->drv_data->mask_reset_reg,
				  mask_val, val);
}

mask_reset_inv 같은 필드가 있는 이유가 재밌다. 어떤 SoC는 "1을 쓰면 마스크", 다른 SoC는 "0을 쓰면 마스크"로 극성이 반대다. (mask ^ val_inv)로 XOR를 걸어 극성 반전을 한 줄로 흡수한다. 하나의 드라이버가 여러 세대 SoC를 지원하려면 이런 세밀한 차이를 데이터로 빼내는 설계가 필수다.

IRQ 핸들러

soft_noboot나 pretimeout 상황에서 오는 인터럽트 처리는 단순하다.

static irqreturn_t s3c2410wdt_irq(int irqno, void *param)
{
	struct s3c2410_wdt *wdt = platform_get_drvdata(param);

	dev_info(wdt->dev, "watchdog timer expired (irq)\n");
	s3c2410wdt_keepalive(&wdt->wdt_device);   // 일단 kick 해서 리셋은 미룸

	if (wdt->drv_data->quirks & QUIRK_HAS_WTCLRINT_REG)
		writel(0x1, wdt->reg_base + S3C2410_WTCLRINT);  // 신형은 인터럽트 클리어 필요

	return IRQ_HANDLED;
}

정리 — 이 드라이버에서 배우는 것

  • 하드웨어 추상화 계층: 코어(watchdog_ops)가 정의한 얇은 인터페이스만 채우면, /dev/watchdog·ioctl·nowayout·magic-close 같은 정책은 전부 코어가 처리한다. 드라이버는 레지스터만 안다.
  • 정책과 메커니즘의 분리: "언제 kick 할지"는 유저스페이스, "어떻게 kick 하는지"는 드라이버, "어떤 안전 정책을 강제할지"는 코어가 나눠 갖는다.
  • variant/quirk 패턴: 세대별 SoC 차이를 코드 분기가 아니라 데이터 구조로 흡수해, 하나의 드라이버가 S3C2410부터 Exynos까지 커버한다. 극성 반전까지 필드 하나로 처리하는 게 백미다.

인턴 때 다룬 코드는 이 공개 드라이버와 계보를 공유하는, 특정 SoC용 BSP였다. 세부는 밝힐 수 없지만 "레지스터를 만지되 커널이 정해둔 서브시스템 규약 안에서 만진다"는 감각은 바로 이 구조에서 왔다.

원본: mainline drivers/watchdog/s3c2410_wdt.cDocumentation/watchdog/