2026-01-08
리눅스 커널 워치독 드라이버 뜯어보기: s3c2410_wdt
삼성전자 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_device와 struct 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;
}
핵심 로직은 이렇다.
- 소스 클럭을 128로 나눈다(고정 division).
timeout * freq로 필요한 tick 수를 구한다.- 그 값이 카운터 폭(
max_cnt)을 넘으면, prescaler로 추가 분주해서 카운터 안에 들어오게 한다. - 실제 반영된 값으로
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.c및Documentation/watchdog/