我的内核学习笔记9:Intel内部看门狗iTCO_wdt驱动 | 迟思堂工作室
A-A+

我的内核学习笔记9:Intel内部看门狗iTCO_wdt驱动

2016-12-06 22:24 Linux内核研究, 嵌入式Linux 暂无评论 阅读 2,922 次

本文对Intel e3800内部看门狗驱动源码进行分析。

一、概述

Intel e3800内部看门狗在手册的PCU - Power Management Controller (PMC)章节中介绍——在这里,WDT属于电源管理范畴了。有些ARM芯片,将watchdog单独成一个章节,这个东西,笔者是在无意中发现的。涉及的寄存器有TCO_RLD、TCO1_CNT、TCO_TMR。这些寄存器在后文将再次介绍。

在Linux内核中使用的驱动名为iTCO_wdt(为了行文方便,将对应的设备称为“iTCO_wdt设备”)。驱动位于drivers/watchdog/iTCO_wdt.c文件中。

本文基于linux 3.17.1版本内核进行分析。

内核配置(make menuconfig)信息如下:

Device Drivers  --->
    [*] Watchdog Timer Support  --->
        <M>   Intel TCO Timer/Watchdog

使用模块形式编译,在加载该驱动后,就可以使用lsmod查看。另外,也会生成对应的驱动设备目录:/sys/bus/platform/devices/iTCO_wdt/。

二、iTCO_wdt设备注册

iTCO_wdt设备的注册实现过程在文件drivers/mfd/lpc_ich.c中。在LPC探测函数lpc_ich_probe对ACPI基地址进行赋值,代码如下:

    priv->abase = ACPIBASE; // ACPI基地址
    priv->actrl_pbase = ACPICTRL_PMCBASE;

其定义是:

#define ACPIBASE		0x40
#define ACPICTRL_PMCBASE	0x44

所有地址都可以在手册对应章节中找到。

初始化WDT在函数lpc_ich_init_wdt中。这个函数获取ACPI基地址。并初始化wdt_ich_res资源结构体数组,数组包含了ICH_RES_IO_TCO和ICH_RES_IO_SMI。TCO就是WDT使用到的部分。主要代码功能描述如下。

1、读取ACPI基地址值,即通过LPC这个PCI设备的配置空间偏移值ACPIBASE。

pci_read_config_dword(dev, priv->abase, &base_addr_cfg);
base_addr = base_addr_cfg & 0x0000ff80;

2、设置resource,即把前面获取到的base_addr地址加上TCO偏移值赋给resource的start成员变量。

res = wdt_io_res(ICH_RES_IO_TCO);
res->start = base_addr + ACPIBASE_TCO_OFF;
res->end = base_addr + ACPIBASE_TCO_END;

3、添加mfd设备。

lpc_ich_finalize_cell(dev, &lpc_ich_cells[LPC_WDT]);
ret = mfd_add_devices(&dev->dev, -1, &lpc_ich_cells[LPC_WDT],
              1, NULL, 0, NULL);

到这样就完成了mfd的添加。

三、iTCO_wdt驱动

3.1 寄存器介绍

iTCO_wdt.c文件对于TCO基地址和寄存器说明如下:

/* Address definitions for the TCO */
/* TCO base address */
#define TCOBASE		(iTCO_wdt_private.tco_res->start)
/* SMI Control and Enable Register */
#define SMI_EN		(iTCO_wdt_private.smi_res->start)


#define TCO_RLD		(TCOBASE + 0x00) /* TCO Timer Reload and Curr. Value */
#define TCOv1_TMR	(TCOBASE + 0x01) /* TCOv1 Timer Initial Value	*/
#define TCO_DAT_IN	(TCOBASE + 0x02) /* TCO Data In Register	*/
#define TCO_DAT_OUT	(TCOBASE + 0x03) /* TCO Data Out Register	*/
#define TCO1_STS	(TCOBASE + 0x04) /* TCO1 Status Register	*/
#define TCO2_STS	(TCOBASE + 0x06) /* TCO2 Status Register	*/
#define TCO1_CNT	(TCOBASE + 0x08) /* TCO1 Control Register	*/
#define TCO2_CNT	(TCOBASE + 0x0a) /* TCO2 Control Register	*/
#define TCOv2_TMR	(TCOBASE + 0x12) /* TCOv2 Timer Initial Value	*/

注:在e3800手册上找不到TCO2_CNT和TCOv2_TMR的说明。但在ICH6手册上则可以找到。有兴趣的可以参考文后资源以了解更多。

TCO_RLD:重新计数寄存器。只有bit0~9有效。返回当前的计数,写入任何数值即可重新计数。
TCO_STS:状态寄存器。bit3表示计数达到0后产生SMI(System Management Interrupt)。
TCO1_CNT:控制寄存器。bit11表示是否开启计数(即开启或停止看门狗)。
TCO_TMR:计数寄存器。bit16~25有效。手册说每次计数为0.6秒,最大超时时间为1023*0.6=613.8秒。不过笔者从代码和实际测试结果来看,至少在e3800上是不正确的。e3800的这个寄存器数值即超时秒数。

3.2 入口代码

 iTCO_wdt是一个platform设备驱动,其入口代码片段如下:

static struct platform_driver iTCO_wdt_driver = {
    .probe          = iTCO_wdt_probe,
    .remove         = iTCO_wdt_remove,
    .shutdown       = iTCO_wdt_shutdown,
    .driver         = {
        .owner  = THIS_MODULE,
        .name   = DRV_NAME,
    },
};

static int __init iTCO_wdt_init_module(void)
{
    int err;

    pr_info("Intel TCO WatchDog Timer Driver v%s\n", DRV_VERSION);

    err = platform_driver_register(&iTCO_wdt_driver);
    if (err)
        return err;

    return 0;
}

static void __exit iTCO_wdt_cleanup_module(void)
{
    platform_driver_unregister(&iTCO_wdt_driver);
    pr_info("Watchdog Module Unloaded\n");
}

module_init(iTCO_wdt_init_module);
module_exit(iTCO_wdt_cleanup_module);

3.3、探测函数

iTCO_wdt看门狗的探测函数为iTCO_wdt_probe。

1、获取IO资源:

    iTCO_wdt_private.tco_res =
        platform_get_resource(dev, IORESOURCE_IO, ICH_RES_IO_TCO);
    if (!iTCO_wdt_private.tco_res)
        goto out;

IO资源已经在lpc_ich中设置了,根据ICH_RES_IO_TCO就能获取到TCO的IO资源。

2、设置NO_REBOOT标志

    /* Check chipset's NO_REBOOT bit */
    if (iTCO_wdt_unset_NO_REBOOT_bit() && iTCO_vendor_check_noreboot_on()) {
        pr_info("unable to reset NO_REBOOT flag, device disabled by hardware/BIOS\n");
        ret = -ENODEV;    /* Cannot reset NO_REBOOT bit */
        goto unmap_gcs_pmc;
    }

    /* Set the NO_REBOOT bit to prevent later reboots, just for sure */
    iTCO_wdt_set_NO_REBOOT_bit();

3、检测IO资源是否有冲突

    /* The TCO logic uses the TCO_EN bit in the SMI_EN register */
    if (!request_region(iTCO_wdt_private.smi_res->start,
            resource_size(iTCO_wdt_private.smi_res), dev->name)) {
        pr_err("I/O address 0x%04llx already in use, device disabled\n",
               (u64)SMI_EN);
        ret = -EBUSY;
        goto unmap_gcs_pmc;
    }
    if (turn_SMI_watchdog_clear_off >= iTCO_wdt_private.iTCO_version) {
        /*
         * Bit 13: TCO_EN -> 0
         * Disables TCO logic generating an SMI#
         */
        val32 = inl(SMI_EN);
        val32 &= 0xffffdfff;    /* Turn off SMI clearing watchdog */
        outl(val32, SMI_EN);
    }

    if (!request_region(iTCO_wdt_private.tco_res->start,
            resource_size(iTCO_wdt_private.tco_res), dev->name)) {
        pr_err("I/O address 0x%04llx already in use, device disabled\n",
               (u64)TCOBASE);
        ret = -EBUSY;
        goto unreg_smi;
    }

4、设置超时时间以及nowayout。

    iTCO_wdt_watchdog_dev.timeout = WATCHDOG_TIMEOUT;
    watchdog_set_nowayout(&iTCO_wdt_watchdog_dev, nowayout);

默认超时时间为30秒。nowayout由内核配置CONFIG_WATCHDOG_NOWAYOUT来确定。内核配置的解释为:Disable watchdog shutdown on close。如果为true,表示关闭watchdog设备也不会停止看门狗。亦即一旦使能看门狗,是无关法关闭的,是没有退路的,所以叫“no way out”。

5、向系统注册看门狗设备。并打印超时时间。

    ret = watchdog_register_device(&iTCO_wdt_watchdog_dev);
    if (ret != 0) {
        pr_err("cannot register watchdog device (err=%d)\n", ret);
        goto unreg_tco;
    }

    pr_info("initialized. heartbeat=%d sec (nowayout=%d)\n",
        heartbeat, nowayout);


3.4 watchdog子系统实现

在探测函数的最后,调用了watchdog_register_device向系统注册watchdong设备。这样就能使用Linux已有的watchdog子系统的框架了。

注册的iTCO_wdt_watchdog_dev及相关操作函数结构体定义如下:

static const struct watchdog_ops iTCO_wdt_ops = {
	.owner =		THIS_MODULE,
	.start =		iTCO_wdt_start,
	.stop =			iTCO_wdt_stop,
	.ping =			iTCO_wdt_ping,
	.set_timeout =		iTCO_wdt_set_timeout,
	.get_timeleft =		iTCO_wdt_get_timeleft,
};

static struct watchdog_device iTCO_wdt_watchdog_dev = {
	.info =		&ident,
	.ops =		&iTCO_wdt_ops,
};

其中包括了使能、停止、喂狗、设置超时时间等。“喂狗”是一种通称的叫法,——也有其它称呼,如笔者公司叫此操作为“打狗”。喂狗对应的实现函数是ping。

注册后,watchdog子系统会生成/dev/watchdog设备文件。并提供标准的ioctl控制命令,如WDIOC_SETTIMEOUT、WDIOC_KEEPALIVE。详情可参考drivers/watchdog目录下的watchdog_dev.c和watchdog_core.c文件。本文只关注iTCO如何实现start、stop、ping、set_timeout这几个函数。

3.5 iTCO相关操作

1、iTCO_wdt_start

static int iTCO_wdt_start(struct watchdog_device *wd_dev)
{
	unsigned int val;

	spin_lock(&iTCO_wdt_private.io_lock);

	iTCO_vendor_pre_start(iTCO_wdt_private.smi_res, wd_dev->timeout);

	/* disable chipset's NO_REBOOT bit */
	if (iTCO_wdt_unset_NO_REBOOT_bit()) {
		spin_unlock(&iTCO_wdt_private.io_lock);
		pr_err("failed to reset NO_REBOOT flag, reboot disabled by hardware/BIOS\n");
		return -EIO;
	}

	/* Force the timer to its reload value by writing to the TCO_RLD
	   register */
	if (iTCO_wdt_private.iTCO_version >= 2)
		outw(0x01, TCO_RLD);
	else if (iTCO_wdt_private.iTCO_version == 1)
		outb(0x01, TCO_RLD);

	// 0表示TCO计时,1表示禁止
	/* Bit 11: TCO Timer Halt -> 0 = The TCO timer is enabled to count */
	val = inw(TCO1_CNT);
	val &= 0xf7ff;
	outw(val, TCO1_CNT);
	val = inw(TCO1_CNT);
	spin_unlock(&iTCO_wdt_private.io_lock);

	// 如果bit11为1,则说明无法启动WDT,失败
	if (val & 0x0800)
		return -1;
	return 0;
}

2、iTCO_wdt_stop

static int iTCO_wdt_stop(struct watchdog_device *wd_dev)
{
	unsigned int val;

	spin_lock(&iTCO_wdt_private.io_lock);

	iTCO_vendor_pre_stop(iTCO_wdt_private.smi_res);

	/* Bit 11: TCO Timer Halt -> 1 = The TCO timer is disabled */
	val = inw(TCO1_CNT);
	val |= 0x0800; // 使能计时
	outw(val, TCO1_CNT);
	val = inw(TCO1_CNT);

	/* Set the NO_REBOOT bit to prevent later reboots, just for sure */
	iTCO_wdt_set_NO_REBOOT_bit();

	spin_unlock(&iTCO_wdt_private.io_lock);

    // 如果bit11为0,说明没有成功停止看门狗,返回失败
	if ((val & 0x0800) == 0)
		return -1;
	return 0;
}

3、iTCO_wdt_ping

static int iTCO_wdt_ping(struct watchdog_device *wd_dev)
{
	spin_lock(&iTCO_wdt_private.io_lock);

	iTCO_vendor_pre_keepalive(iTCO_wdt_private.smi_res, wd_dev->timeout);

    // 写1到TCO_RLD寄存器,重新计时
	/* Reload the timer by writing to the TCO Timer Counter register */
	if (iTCO_wdt_private.iTCO_version >= 2) {
		outw(0x01, TCO_RLD);
	} else if (iTCO_wdt_private.iTCO_version == 1) {
		/* Reset the timeout status bit so that the timer
		 * needs to count down twice again before rebooting */
		outw(0x0008, TCO1_STS);	/* write 1 to clear bit */

		outb(0x01, TCO_RLD);
	}

	spin_unlock(&iTCO_wdt_private.io_lock);
	return 0;
}

4、iTCO_wdt_set_timeout

static int iTCO_wdt_set_timeout(struct watchdog_device *wd_dev, unsigned int t)
{
	unsigned int val16;
	unsigned char val8;
	unsigned int tmrval;

	tmrval = seconds_to_ticks(t); // 时间(单位为秒)转换为tick

	/* For TCO v1 the timer counts down twice before rebooting */
	if (iTCO_wdt_private.iTCO_version == 1)
		tmrval /= 2;

	/* from the specs: */
	/* "Values of 0h-3h are ignored and should not be attempted" */
	if (tmrval < 0x04)
		return -EINVAL;
	if (((iTCO_wdt_private.iTCO_version >= 2) && (tmrval > 0x3ff)) ||
	    ((iTCO_wdt_private.iTCO_version == 1) && (tmrval > 0x03f)))
		return -EINVAL;

	iTCO_vendor_pre_set_heartbeat(tmrval);

    // 将新的超时时间写入TMR寄存器
	/* Write new heartbeat to watchdog */
	if (iTCO_wdt_private.iTCO_version >= 2) {
		spin_lock(&iTCO_wdt_private.io_lock);
		val16 = inw(TCOv2_TMR);
		val16 &= 0xfc00;
		val16 |= tmrval;
		outw(val16, TCOv2_TMR);
		val16 = inw(TCOv2_TMR);
		spin_unlock(&iTCO_wdt_private.io_lock);

		if ((val16 & 0x3ff) != tmrval)
			return -EINVAL;
	} else if (iTCO_wdt_private.iTCO_version == 1) {
		spin_lock(&iTCO_wdt_private.io_lock);
		val8 = inb(TCOv1_TMR);
		val8 &= 0xc0;
		val8 |= (tmrval & 0xff);
		outb(val8, TCOv1_TMR);
		val8 = inb(TCOv1_TMR);
		spin_unlock(&iTCO_wdt_private.io_lock);

		if ((val8 & 0x3f) != tmrval)
			return -EINVAL;
	}

	wd_dev->timeout = t;
	return 0;
}

3.6 其它杂项

1、设置NO_REBOOT标志。

static void iTCO_wdt_set_NO_REBOOT_bit(void)
{
    u32 val32;

    /* Set the NO_REBOOT bit: this disables reboots */
    if (iTCO_wdt_private.iTCO_version == 3) {
        val32 = readl(iTCO_wdt_private.gcs_pmc);
        val32 |= 0x00000010; // bit4为NO_REBOOT标志
        writel(val32, iTCO_wdt_private.gcs_pmc);
    } 
    //...
}

关于NO_REBOOT的解释如下:

No Reboot (NO_REBOOT) (no_reboot): This bit is set when the No Reboot strap is
sampled high on COREPWROK. This bit may be set or cleared by software if the strap is
sampled low but may not override the strap wh en it indicates No Reboot. When set, the
TCO timer will count down and generate th e SMI# on the first timeout, but will not
reboot on the second timeout.

参考资源:

1、baytrail手册:

2、ICH6手册:

3、内核源码官网:

4、内核源码查询:

李迟 2016.12.06 周二 夜



如果本文对阁下有帮助,不妨赞助笔者以输出更多好文章,谢谢!
donate



标签:

给我留言