本文介绍qemu是如何模拟和使用intel 6300esb芯片组的watchdog功能的,watchdog的基本概念,可以参考1,本文不涉及详细介绍如何使用centos或者ubuntu等发行版自带的喂狗程序,如有相关操作,只是为了演示如何触发qemu的一些相关函数的调用。本文用的代码版本为开源的qemu2.8.0。
libvirt中添加
...
...
或者直接在qemu命令行中添加
-device i6300esb,id=watchdog0,bus=pci.0,addr=0x6 -watchdog-action reset
其中-watchdog-action是设置watchdog timeout时间过后的行为的,此处拿reset做实验,其他选项可以自行查阅qemu命令行或者libvirt xml2
添加成功之后可以在guest OS中看到外设
-device i6300esb,id=watchdog0,bus=pci.0,addr=0x6 -watchdog-action reset
原始代码引用自1,稍微做了点修改
#include
#include
#include
#include
#define WDT_DEVICE_FILE "/dev/watchdog"
int main(void)
{
int g_watchdog_fd = -1;
int timeout = 0;
int timeout_reset = 120;
int ret = 1;
int sleep_time = 10;
//开启watchdog
g_watchdog_fd = open(WDT_DEVICE_FILE, O_RDWR);
if (g_watchdog_fd == -1)
{
printf("Error in file open WDT device file(%s)...\n", WDT_DEVICE_FILE);
return 0;
}
//获取watchdog的超时时间(heartbeat)
ioctl(g_watchdog_fd, WDIOC_GETTIMEOUT, &timeout);
printf("default timeout %d sec.\n", timeout);
//设置watchdog的超时时间(heartbeat)
ioctl(g_watchdog_fd, WDIOC_SETTIMEOUT, &timeout_reset);
printf("We reset timeout as %d sec.\n", timeout_reset);
//喂狗
while(1){
//喂狗
ret = ioctl(g_watchdog_fd, WDIOC_KEEPALIVE, 0);
//喂狗也通过写文件的方式,向/dev/watchdog写入字符或者数字等
// static unsigned char food = 0;
//write(g_watchdog_fd, &food, 1);
if (ret != 0) {
printf("Feed watchdog failed. \n");
close(g_watchdog_fd);
return -1;
} else {
printf("Feed watchdog every %d seconds.\n", sleep_time);
}
//feed_watchdog_time是喂狗的时间间隔,要小于watchdog的超时时间
sleep(10);
}
//关闭watchdog
write(g_watchdog_fd, "V", 1);
//以下方式实测并不能关闭watchdog
//ioctl(g_watchdog_fd, WDIOC_SETOPTIONS, WDIOS_DISABLECARD)
close(g_watchdog_fd);
}
\* hw/watchdog/wdt_i6300esb.c *\
#define I6300ESB_DEBUG 1
然后重新编译安装qemu,打印的内容分三个阶段,刚启动qemu,就可以看到如下打印
i6300esb: i6300esb_realize: I6300State = 0x55e80b28d670
i6300esb: i6300esb_reset: I6300State = 0x55e80b28d670
i6300esb: i6300esb_disable_timer: timer disabled
以上打印,是由于qemu初始化虚拟外设i6300esb的时候调用的函数产生的,然后输入c启动guest os,会进一步的产生打印
(qemu) c
/home/qemu-2.8.0/cpus.c resume_all_vcpus 1372
/home/qemu-2.8.0/cpus.c resume_all_vcpus 1372
/home/qemu-2.8.0/cpus.c resume_all_vcpus 1372
/home/qemu-2.8.0/cpus.c resume_all_vcpus 1372
(qemu)
i6300esb: i6300esb_config_read: addr = 0, len = 2//Vendor ID 8086h Read Only
i6300esb: i6300esb_config_read: addr = a, len = 2//Sub Class Code Register (SCC) 80h Read Only
i6300esb: i6300esb_config_read: addr = e, len = 1//Header Type Register (HEDT) 00h Read Only
i6300esb: i6300esb_config_read: addr = 0, len = 2//Vendor ID 8086h Read Only
i6300esb: i6300esb_config_read: addr = a, len = 2
i6300esb: i6300esb_config_read: addr = e, len = 1
i6300esb: i6300esb_config_read: addr = 0, len = 2
i6300esb: i6300esb_config_read: addr = 0, len = 4
i6300esb: i6300esb_config_read: addr = 8, len = 4//Revision ID Register (RID) See NOTE: Read Only
......
i6300esb: i6300esb_config_write: addr = 60, data = 3, len = 2//WDT Configuration 00h Read/Write
i6300esb: i6300esb_config_read: addr = 68, len = 1//WDT Lock Register 00h Read/Write
i6300esb: i6300esb_config_write: addr = 68, data = 0, len = 1
i6300esb: i6300esb_disable_timer: timer disabled
i6300esb: i6300esb_mem_writew: addr = c, val = 80
i6300esb: i6300esb_mem_writew: addr = c, val = 86
i6300esb: i6300esb_mem_readw: addr = c
i6300esb: i6300esb_mem_writew: addr = c, val = 80
i6300esb: i6300esb_mem_writew: addr = c, val = 86
i6300esb: i6300esb_mem_writew: addr = c, val = 300//0000 0011 0000 0000//第8位是调用重置定时器函数,第九位是重置reboot flag
//这里其实调用了i6300esb_restart_timer,但是d->enabled为0,因此直接返回了,没有打印
i6300esb: i6300esb_mem_writew: addr = c, val = 80
i6300esb: i6300esb_mem_writew: addr = c, val = 86
i6300esb: i6300esb_mem_writel: addr = 0, val = 3c00//0011 1100 0000 0000//第12位也是重置reboot flag
i6300esb: i6300esb_mem_writew: addr = c, val = 80
i6300esb: i6300esb_mem_writew: addr = c, val = 86
i6300esb: i6300esb_mem_writel: addr = 4, val = 3c00//0011 1100 0000 0000
i6300esb: i6300esb_mem_writew: addr = c, val = 80
i6300esb: i6300esb_mem_writew: addr = c, val = 86
i6300esb: i6300esb_mem_writew: addr = c, val = 100//0000 0001 0000 0000,以后正常工作是写这个数字,上面的应该都是测试用的,出现100之后,这个函数就不打印了
i6300esb: i6300esb_config_read: addr = 0, len = 4
i6300esb: i6300esb_config_read: addr = 4, len = 4
....
这些打印是由于guest OS的内核在初始化的时候,产生了IO exit或者MMIO exit,由kvm返回到qemu,进行io读写的模拟产生的。在guest os中执行喂狗程序,qemu产生的打印如下
6300esb: i6300esb_config_write: addr = 68, data = 2, len = 1
i6300esb: i6300esb_restart_timer: stage 1, timeout 96000
i6300esb: i6300esb_mem_writew: addr = c, val = 80
i6300esb: i6300esb_mem_writew: addr = c, val = 86
i6300esb: i6300esb_mem_writel: addr = 0, val = 7800, stage = 2
i6300esb: i6300esb_mem_writew: addr = c, val = 80
i6300esb: i6300esb_mem_writew: addr = c, val = 86
i6300esb: i6300esb_mem_writel: addr = 4, val = 7800, stage = 2
i6300esb: i6300esb_mem_writew: addr = c, val = 80
i6300esb: i6300esb_mem_writew: addr = c, val = 86
i6300esb: i6300esb_mem_writew: addr = c, val = 100
i6300esb: i6300esb_restart_timer: stage 1, timeout 96000
i6300esb: i6300esb_mem_writew: addr = c, val = 80
i6300esb: i6300esb_mem_writew: addr = c, val = 86
i6300esb: i6300esb_mem_writew: addr = c, val = 100
i6300esb: i6300esb_restart_timer: stage 1, timeout 96000
i6300esb: i6300esb_mem_writew: addr = c, val = 80
i6300esb: i6300esb_mem_writew: addr = c, val = 86
i6300esb: i6300esb_mem_writew: addr = c, val = 100
i6300esb: i6300esb_restart_timer: stage 1, timeout 96000
i6300esb: i6300esb_mem_writew: addr = c, val = 80
i6300esb: i6300esb_timer_expired: stage 2
2019-07-23T08:58:45.111484Z qemu-system-x86_64: network script /etc/qemu-ifdown failed with status 256
最后强制使用ctrl+c退出喂狗程序,由于第3节中的喂狗程序没有处理强制退出的信号,因此程序没有将watchdog硬件关闭,导致i6300esb watchdog硬件等待超时,从而触发reset效果,这就是宏观上的工作方式,后面会分析几个主要函数。
/* vl.c:4574*/
if (qemu_opts_foreach(qemu_find_opts("device"),
device_init_func, NULL, NULL)) {
exit(1);
}
以上函数是在qemu主线程中,通过一个循环,遍历所有qemu命令行中的device配置,进行设备的初始化,这里的调用关系比较复杂,还涉及到不同的总线,此处只非精确的列出大致调用层级,可以看到i6300esb是PCI设备:
|----->device_init_func
|----->qdev_device_add
|----->device_set_realized
|----->pci_qdev_realize
|----->i6300esb_realize
因为这是通用架构,因此其他代码本文不分析,有兴趣的可以查阅其他文档,关于QDEV的,这里主要看i6300esb_realize函数:
/* hw/watchdog/wdt_i6300esb.c:418 */
/* 该函数负责实现虚拟外设,本质上和内存条pc.ram没有区别,只是io设备增加了ops,会执行一些动作,原理可以自行学习io虚拟化的流程*/
static void i6300esb_realize(PCIDevice *dev, Error **errp)
{
I6300State *d = WATCHDOG_I6300ESB_DEVICE(dev);//通用设备类型的参数,类型转换成I6300State类型
i6300esb_debug("I6300State = %p\n", d);
d->timer = timer_new_ns(QEMU_CLOCK_VIRTUAL, i6300esb_timer_expired, d);//添加一个定时器,第二个参数就是注册回调函数
//以后该定时器到期或者超时时候可以触发的行为,就由该函数决定
d->previous_reboot_flag = 0;
//这里就是初始化一个标记,代表之前没有重启过(应该是由于定时器超时造成的重启,人工重启不确定算不算)
memory_region_init_io(&d->io_mem, OBJECT(d), &i6300esb_ops, d,
"i6300esb", 0x10);
//为该外设分配存储空间,注意这里是io空间,不是内存空间,因此调用的是memory_region_init_io,因为这个函数的第三个参数会附带各种行为,
//这些行为就是为了模拟外设的读写的,而内存不需要这样模拟,所以没有这些ops
pci_register_bar(&d->dev, 0, 0, &d->io_mem);
//将该设备注册到PCI总线上,就代表设备插到PCI插槽上面了
/* qemu_register_coalesced_mmio (addr, 0x10); ? */
}
该函数是一个配置重置函数,设备最开始初始化完毕之后会调用一次,作为模拟真实硬件各种寄存器的初始值,注意,这里说的初始化,
是指的qemu程序模拟外设的初始化,而不是虚拟机的guest OS开机的过程中设备驱动的初始化,各主要字段详细的作用,需要对照手册看3,这里的初始化值没有那么重要,因为在guest OS启动过程中,驱动程序会调用i6300esb_mem_writel函数将配置都修改掉,当超时
的时候,guest OS重启或者关机之前又要调用本函数,之后guest OS再开机,再次加载驱动,又重新改写配置,反复循环。初始化的时候调用关系如下:
|----->qemu_system_reset//初始化完成后会调用一次重置函数,这样各种参数全部都刷新成初始值了,为启动做准备
|----->qemu_devices_reset
|----->qbus_reset_all_fn
|----->i6300esb_reset
在分析i6300esb_reset函数之前,得先了解i6300esb WDT功能流程3,该芯片组功能很强大,qemu2.8中仅仅模拟了watchdog的功能,因此,只需要看相应的章节就可以了,抽象出来的工作流程如下:
按照上图的逻辑,4.2节中的i6300esb_realize函数就相当于工厂里面生产出来了一个i6300esb设备,pci_register_bar函数就相当于把该设备插入到pci插槽上,随后上图中的‘1.上电’和‘2.硬件初始化’,就是开机自检,初始化各种硬件,设定各种寄存器的初始值,在qemu里,因为不是完全按照物理电路的时序来走的,稍微有点偏差,但是思想上是一样的,这里就可以大致对应i6300esb_reset函数,虽然并没有真正的reset,但是函数体的内容就是起的这个效果,对每个状态和寄存器变量进行初始化,来模拟真正硬件设备的初始值。i6300esb WDT开始工作后分两个阶段,一开始是进入stage1,开始计时,如果超时到设定的时间的一半的时候,会触发一个内部中断3,然后进入stage2,这个内部中断目前意义不大,qemu并没有做额外的行为,只是打印了一条告警信息,真实硬件具体有什么内涵,还不得而知。到了stage2,如果再次超时,则会触发watchdog注册的行为,在真实硬件中,芯片会通过WDT_TOUT引脚发送一个外部中断,会直接让系统关机或者重启,在qemu中,则会调用注册好的回调函数i6300esb_timer_expired。
/* hw/watchdog/wdt_i6300esb.c:149 */
/*
*/
static void i6300esb_reset(DeviceState *dev)
{
PCIDevice *pdev = PCI_DEVICE(dev);
I6300State *d = WATCHDOG_I6300ESB_DEVICE(pdev);
i6300esb_debug("I6300State = %p\n", d);
i6300esb_disable_timer(d);//去使能该设备的定时器
/* NB: Don't change d->previous_reboot_flag in this function. */
d->reboot_enabled = 1;
d->clock_scale = CLOCK_SCALE_1KHZ;
d->int_type = INT_TYPE_IRQ;//根据芯片手册,这个代表WDT配置寄存器的0-1bit位,初始化为0
d->free_run = 0;
d->locked = 0;
d->enabled = 0;//设备初始化是没有使能的
d->timer1_preload = 0xfffff;//stage1状态的超时时间,寄存器mmio地址为Base + 00h
d->timer2_preload = 0xfffff;//stage2状态的超时时间,寄存器mmio地址为Base + 04h
d->stage = 1;//从stage1开始
d->unlock_state = 0;
}
其他个别变量暂时不用管,只看最主要的几个,最核心的就是三个,timer1_preload,timer2_preload和stage,分别代表第一阶段超时时间,第二阶段超时时间,以及当前阶段是多少。
/* qemu-timer.c:404 */
static void i6300esb_disable_timer(I6300State *d)
{
i6300esb_debug("timer disabled\n");
timer_del(d->timer);//从该定时器所属的QEMUTimerList中,找到自己,并且删除
}
上一节的代码执行完成后,就是硬件已经做好准备了,直到qemu初始化完成,也不会再调用和i6300esb相关的函数了,在guest os开始启动后,qemu会反复调用i6300esb_config_write,i6300esb_config_read这两个函数(也会调用其他函数,下一节再分析),这两个函数的打印和产生原因在4.4节中已经进行了说明,本文不会深入对guest os的驱动进行分析,仅仅会简要提到部分关键内容。
/* hw/watchdog/wdt_i6300esb.c:215 */
static void i6300esb_config_write(PCIDevice *dev, uint32_t addr,
uint32_t data, int len)
{
I6300State *d = WATCHDOG_I6300ESB_DEVICE(dev);
int old;
i6300esb_debug("addr = %x, data = %x, len = %d\n", addr, data, len);
/*当qemu有以下打印的时候,就是配置WDT Configuration Register,对WDT_INT_TYPE等信息进行配置*/
/*i6300esb: i6300esb_config_write: addr = 60, data = 3, len = 2*/
if (addr == ESB_CONFIG_REG && len == 2) {//0x60,
d->reboot_enabled = (data & ESB_WDT_REBOOT) == 0;
d->clock_scale =
(data & ESB_WDT_FREQ) != 0 ? CLOCK_SCALE_1MHZ : CLOCK_SCALE_1KHZ;
d->int_type = (data & ESB_WDT_INTTYPE);//0x3 & 0x11
/*当qemu有以下打印的时候,就是配置WDT Lock Register,设置定时器的使能*/
/*i6300esb: i6300esb_config_write: addr = 68, data = 0, len = 1*/
} else if (addr == ESB_LOCK_REG && len == 1) {//0x68
if (!d->locked) {
d->locked = (data & ESB_WDT_LOCK) != 0;
d->free_run = (data & ESB_WDT_FUNC) != 0;
old = d->enabled;
d->enabled = (data & ESB_WDT_ENABLE) != 0;
/*如果之前定时器是关闭的,现在收到打开命令,则调用定时器重置功能,然后进入stage 1*/
if (!old && d->enabled) /* Enabled transitioned from 0 -> 1 */
i6300esb_restart_timer(d, 1);
else if (!d->enabled)
i6300esb_disable_timer(d);
}
} else {
pci_default_write_config(dev, addr, data, len);//绝大多数走的这里,这里都是一些PCI通用配置,没有涉及到
//定时器,不影响WDT定时器功能的理解。
}
}
i6300esb_config_read函数结构和以上函数相似,,可以看到,只有addr为ESB_CONFIG_REG或者ESB_LOCK_REG的时候,才会由本函数来模拟效果,其他addr,都会直接调用通用的pci函数进行处理。可以看出,这种虚拟化外设的架构层级的设计思想,将所有通用的pci操作,抽象出来,单独实现,然后把特定的功能截获,进行模拟,跟真正的硬件的层级还是有一定区别的,例如,该i6300esb代码是放在hw/watchdog目录下,说明该代码只模拟watchdog功能,而无视此芯片组完整的其他功能,而完整的i6300esb全称是Intel 6300ESB I/O Controller Hub,也就是我们通常所说的南桥芯片,是有很多功能的,因此在qemu的虚拟化主板上,是不需要模拟南桥芯片的物理形态的,只需要针对在相应的IO访问,或者mmio访问的时候,做相应的处理,使其能够达到硬件的效果,就可以了。
/* hw/watchdog/wdt_i6300esb.c:244 */
static uint32_t i6300esb_config_read(PCIDevice *dev, uint32_t addr, int len)
{
I6300State *d = WATCHDOG_I6300ESB_DEVICE(dev);
uint32_t data;
i6300esb_debug ("addr = %x, len = %d\n", addr, len);
if (addr == ESB_CONFIG_REG && len == 2) {
data =
(d->reboot_enabled ? 0 : ESB_WDT_REBOOT) |
(d->clock_scale == CLOCK_SCALE_1MHZ ? ESB_WDT_FREQ : 0) |
d->int_type;
return data;
} else if (addr == ESB_LOCK_REG && len == 1) {
data =
(d->free_run ? ESB_WDT_FUNC : 0) |
(d->locked ? ESB_WDT_LOCK : 0) |
(d->enabled ? ESB_WDT_ENABLE : 0);
return data;
} else {
return pci_default_read_config(dev, addr, len);
}
}
其中i6300esb_mem_writew和i6300esb_mem_writel函数是最重要的,read函数基本作用不大。i6300esb_mem_writew函数就是处理喂狗的函数。
/* hw/watchdog/wdt_i6300esb.c:312 */
static void i6300esb_mem_writew(void *vp, hwaddr addr, uint32_t val)
{
I6300State *d = vp;
i6300esb_debug("addr = %x, val = %x\n", (int) addr, val);
/*对0xc地址所代表的寄存器先写入0x80,然后写入0x86是一个固定操作,代表可以获取一次对0xc寄存器的写权限*/
/*所以guest os内核驱动程序需要执行一条喂狗指令的时候,必须执行这个,见下一个代码段*/
if (addr == 0xc && val == 0x80)
d->unlock_state = 1;//解锁第一阶段
else if (addr == 0xc && val == 0x86 && d->unlock_state == 1)
d->unlock_state = 2;//解锁第二阶段,代表解锁成功
else {
if (d->unlock_state == 2) {//如果是已经解锁成功
if (addr == 0xc) {//判断寄存器mmio地址
if ((val & 0x100) != 0)//如果满足这里,代表是用户程序在执行喂狗命令
/* This is the "ping" from the userspace watchdog in
* the guest ...
*/
i6300esb_restart_timer(d, 1);//喂狗成功则重启定时器
/* Setting bit 9 resets the previous reboot flag.
* There's a bug in the Linux driver where it sets
* bit 12 instead.
*/
if ((val & 0x200) != 0 || (val & 0x1000) != 0) {
d->previous_reboot_flag = 0;//这里可以不管
}
}
d->unlock_state = 0;
}
}
}
驱动执行写寄存器操作之前,例如writel(val, ESB_TIMER2_REG);,需要先执行一下代码打开该寄存器的写权限,而且有效次数只有一次,想写入下一个数据,比如再次重复此操作。
/*driver/watchdog/i6300esb.c:*/
static inline void esb_unlock_registers(void)
{
writew(ESB_UNLOCK1, ESB_RELOAD_REG);
writew(ESB_UNLOCK2, ESB_RELOAD_REG);
}
i6300esb_mem_writel函数是用来配置timeout时间
/*driver/watchdog/i6300esb.c:345*/
static void i6300esb_mem_writel(void *vp, hwaddr addr, uint32_t val)
{
I6300State *d = vp;
i6300esb_debug ("addr = %x, val = %x, stage = %d \n", (int) addr, val, d->unlock_state);
/*这里的0x80和0x86并没有使用到,应该只是为了以防万一*/
if (addr == 0xc && val == 0x80)
d->unlock_state = 1;
else if (addr == 0xc && val == 0x86 && d->unlock_state == 1)
d->unlock_state = 2;
else {
if (d->unlock_state == 2) {
if (addr == 0)
d->timer1_preload = val & 0xfffff;//这里是给stage 1阶段的定时器写时间
else if (addr == 4)
d->timer2_preload = val & 0xfffff;//这里是给stage 2阶段的定时器写时间
d->unlock_state = 0;
}
}
}
第一次超时会调用这个函数,此处stage=1,这里没有对i6300esb的内部中断进行模拟,因为没有意义,然后调用i6300esb_restart_timer(d, 2)进入stage2,并且重启计时器,记录下半阶段的时间,如果也超时了,则调用watchdog_perform_action()进行处理,这个处理函数比较简单,就不分析了。
/*driver/watchdog/i6300esb.c:182*/
static void i6300esb_timer_expired(void *vp)
{
I6300State *d = vp;
i6300esb_debug("stage %d\n", d->stage);
if (d->stage == 1) {
/* What to do at the end of stage 1? */
switch (d->int_type) {
case INT_TYPE_IRQ:
fprintf(stderr, "i6300esb_timer_expired: I would send APIC 1 INT 10 here if I knew how (XXX)\n");
break;
case INT_TYPE_SMI:
fprintf(stderr, "i6300esb_timer_expired: I would send SMI here if I knew how (XXX)\n");
break;
}
/* Start the second stage. */
i6300esb_restart_timer(d, 2);
} else {
/* Second stage expired, reboot for real. */
if (d->reboot_enabled) {
d->previous_reboot_flag = 1;
watchdog_perform_action(); /* This reboots, exits, etc */
i6300esb_reset(&d->dev.qdev);
}
/* In "free running mode" we start stage 1 again. */
if (d->free_run)
i6300esb_restart_timer(d, 1);
}
}
判断超时是在主循环里面做的,大致流程如下
|----->main_loop()//主循环,基于glib mainloop的
|----->main_loop_wait(nonblocking)
|----->qemu_clock_run_timers(type)
|----->timerlist_run_timers(main_loop_tlg.tl[type]);
|----->i6300esb_timer_expired(opaque)
主循环线程,会反复调用timerlist_run_timers来遍历每个定时器链表,判断超时时间,下面只粘贴关键代码
bool timerlist_run_timers(QEMUTimerList *timer_list)
{
QEMUTimer *ts;
int64_t current_time;
bool progress = false;
QEMUTimerCB *cb;
void *opaque;
/*omit*/
/*获取当前时间*/
current_time = qemu_clock_get_ns(timer_list->clock->type);
for(;;) {//这个循环,结束条件为本轮次函数调用没有任何定时器超时了为止
qemu_mutex_lock(&timer_list->active_timers_lock);
ts = timer_list->active_timers;//获取激活的定时器链表的head用来遍历
if (!timer_expired_ns(ts, current_time)) {//判断是否超时
qemu_mutex_unlock(&timer_list->active_timers_lock);
break;
}
//如果超时,则从当前链表中剔除,并且调用超时函数,此处涉及到i6300esb的定时器,则调用i6300esb_timer_expired
/* remove timer from the list before calling the callback */
timer_list->active_timers = ts->next;
ts->next = NULL;
ts->expire_time = -1;
cb = ts->cb;
opaque = ts->opaque;
qemu_mutex_unlock(&timer_list->active_timers_lock);
/* run the callback (the timer list can be modified) */
cb(opaque);//i6300esb_timer_expired
progress = true;
}
out:
qemu_event_set(&timer_list->timers_done_ev);
return progress;
}
整体设计就是这样,这个看门狗模块是学习qemu定时器的最佳案例。
linux下的watchdog ↩︎ ↩︎
Watchdog device ↩︎
Intel® 6300ESB I/O Controller Hub Datasheet ↩︎ ↩︎ ↩︎