支持nvme的linux_linux里的nvme驱动代码分析

本文详细介绍了在Linux环境下,如何通过QEMU和Buildroot搭建一个支持nvme的模拟环境,以及对Linux内核中nvme驱动的代码分析,包括模块初始化、设备探测、注册驱动等关键步骤,帮助读者理解nvme驱动的工作原理。
摘要由CSDN通过智能技术生成

nvme物理层基于高速的pcie接口,pcie3.0一个lane就已经达到了8Gb/s的速度。x2 x4 x8 … N个lane在一起就是N倍的速度,非常强大。另外加上nvme协议本身的简洁高效,使得协议层的消耗进一步降低,最终达到高速的传输效果。

QEMU + BUILDROOT

出于好奇和兴趣,想要学习一下nvme。协议自然是看最新的pcie3.0和nvme1.2。但是只停留在文档和协议始终只是纸上谈兵,必须要进行一点实践才能理解深刻。要进行实践,无非是两条路。一是从设备端学习固件的nvme模块,二就是从主机端学习nvme的驱动。如今没再从事ssd的工作了,所以想走第一条路基本是不可能了,只能在第二条路上做文章。要是手上有一个nvme的ssd,那对于实验是非常不错的,可惜我没有,相信大多数的人也没有。然而曲线也能救国,总有一条路是给我们准备的,那就是qemu了!

qemu是一个模拟器,可以模拟x86 arm powerpc等等等等,而且支持nvme设备的模拟(不过当前貌似nvme设备只在x86下支持,我尝试过ARM+PCI的环境,但是qemu表示不支持nvme设备)。主机端的系统当然是选择linux,因为我们可以很方便的获得nvme相关的一切代码。

那…接下来到底要怎么上手哪?linux只是一个内核,还需要rootfs,需要这需要那。我们在这里想要学习的是nvme驱动,而不是如何从零开始搭建一个linux系统,所以我们需要一条快速而又便捷的道路。这时候,我就要给大家介绍下buildroot了。有了他,一切全搞定!^_^当然,前提是我们需要一个linux的主机(推荐ubuntu,个人比较喜欢,用的人也多,出了问题比较容易在网上找到攻略,最新的可以安装16.04),并安装了qemu。

buildroot最新的版本可以从这里下载:外链网址已屏蔽

解压后运行

make qemu_x86_64_defconfig

make

编译完成后,根据board/qemu/x86_64/readme.txt里描述的命令,可以在qemu里运行起来刚才编译好的linux系统。

qemu-system-x86_64 -M pc -kernel output/images/bzImage -drive file=output/images/rootfs.ext2,if=virtio,format=raw -append root=/dev/vda -net nic,model=virtio -net user

这个默认的系统log输出被重定向到了虚拟机的“屏幕”上,而非shell上,不能回滚,使得调试起来很不方便。我们需要修改一些东西把log重定向到linux的shell上。首先是编辑buildroot目录下的.config文件。

BR2_TARGET_GENERIC_GETTY_PORT="tty1"

改成

BR2_TARGET_GENERIC_GETTY_PORT="ttyS0"

然后重新编译。等到编译完成后运行下面修改过的命令,就得到我们想要的结果了。

make

qemu-system-x86_64 -M pc -kernel output/images/bzImage -drive file=output/images/rootfs.ext2,if=virtio,format=raw -append "console=ttyS0 root=/dev/vda" -net nic,model=virtio -net user -serial stdio

下面,我们再修改一些命令,加上nvme的支持。

qemu-img create -f raw nvme.img 1G

qemu-system-x86_64 -M pc -kernel output/images/bzImage -drive file=output/images/rootfs.ext2,if=virtio,format=raw -append "console=ttyS0 root=/dev/vda" -net nic,model=virtio -net user -serial stdio -drive file=nvme.img,if=none,format=raw,id=drv0 -device nvme,drive=drv0,serial=foo

linux系统起来后,我们可以在/dev下面查看到nvme相关的设备了。

# ls -l /dev

crw------- 1 root root 253, 0 Jun 3 13:00 nvme0

brw------- 1 root root 259, 0 Jun 3 13:00 nvme0n1

自此,我们的动手实践稍作暂停,可以去学习下nvme的代码了。在遇到问题的时候,我们可以修改代码并在qemu里运行查看效果,真棒!

RTFSC - Read The Fucking Source Code

nvme驱动代码的分析基于linux内核版本4.5.3,为什么选择这个版本?主要是因为buildroot-2016.05默认选择的是这个版本的内核。我们也可以手动修改内核的版本,但这里就不做详述了。nvme的代码位于drivers/nvme目录内,文件不多,主要就两个文件:core.c和pci.c。

分析驱动,首先是要找到这个驱动的入口。module_init把函数nvme_init声明为这个驱动的入口,在linux加载过程中会自动被调用。

static int __init nvme_init(void)

{

int result;

init_waitqueue_head(&nvme_kthread_wait);

nvme_workq = alloc_workqueue("nvme", WQ_UNBOUND | WQ_MEM_RECLAIM, 0);

if (!nvme_workq)

return -ENOMEM;

result = nvme_core_init();

if (result < 0)

goto kill_workq;

result = pci_register_driver(&nvme_driver);

if (result)

goto core_exit;

return 0;

core_exit:

nvme_core_exit();

kill_workq:

destroy_workqueue(nvme_workq);

return result;

}

static void __exit nvme_exit(void)

{

pci_unregister_driver(&nvme_driver);

nvme_core_exit();

destroy_workqueue(nvme_workq);

BUG_ON(nvme_thread && !IS_ERR(nvme_thread));

_nvme_check_size();

}

module_init(nvme_init);

module_exit(nvme_exit);

nvme_init流程分析:

创建一个全局的workqueue,有了这个workqueue之后,很多的work就可以丢到这个workqueue里执行了。

调用nvme_core_init。

调用pci_register_driver。

int __init nvme_core_init(void)

{

int result;

result = register_blkdev(nvme_major, "nvme");

if (result < 0)

return result;

else if (result > 0)

nvme_major = result;

result = __register_chrdev(nvme_char_major, 0, NVME_MINORS, "nvme",

&nvme_dev_fops);

if (result < 0)

goto unregister_blkdev;

else if (result > 0)

nvme_char_major = result;

nvme_class = class_create(THIS_MODULE, "nvme");

if (IS_ERR(nvme_class)) {

result = PTR_ERR(nvme_class);

goto unregister_chrdev;

}

return 0;

unregister_chrdev:

__unregister_chrdev(nvme_char_major, 0, NVME_MINORS, "nvme");

unregister_blkdev:

unregister_blkdev(nvme_major, "nvme");

return result;

}

nvme_core_init流程分析:

调用register_blkdev注册一个名字叫nvme的块设备。

调用__register_chrdev注册一个名字叫nvme的字符设备。

回到nvme_init,pci_register_driver注册了一个pci驱动。这里有几个重要的东西,一个是vendor id和device id,我们可以看到有一条是PCI_VDEVICE(INTEL, 0x5845),有了这个,这个驱动就能跟pci总线枚举出来的设备匹配起来,从而正确的加载驱动了。

static const struct pci_device_id nvme_id_table[] = {

{ PCI_VDEVICE(INTEL, 0x0953),

.driver_data = NVME_QUIRK_STRIPE_SIZE, },

{ PCI_VDEVICE(INTEL, 0x5845), /* Qemu emulated controller */

.driver_data = NVME_QUIRK_IDENTIFY_CNS, },

{ PCI_DEVICE_CLASS(PCI_CLASS_STORAGE_EXPRESS, 0xffffff) },

{ PCI_DEVICE(PCI_VENDOR_ID_APPLE, 0x2001) },

{ 0, }

};

MODULE_DEVICE_TABLE(pci, nvme_id_table);

static struct pci_driver nvme_driver = {

.name = "nvme",

.id_table = nvme_id_table,

.probe = nvme_probe,

.remove = nvme_remove,

.shutdown = nvme_shutdown,

.driver = {

.pm = &nvme_dev_pm_ops,

},

.err_handler = &nvme_err_handler,

};

在linux里我们通过lspci命令来查看当前的pci设备,发现nvme设备的device id就是0x5845。

# lspci -k

00:00.0 Class 0600: 8086:1237

00:01.0 Class 0601: 8086:7000

00:01.1 Class 0101: 8086:7010 ata_piix

00:01.3 Class 0680: 8086:7113

00:02.0 Class 0300: 1234:1111 bochs-drm

00:03.0 Class 0200: 1af4:1000 virtio-pci

00:04.0 Class 0108: 8086:5845 nvme

00:05.0 Class 0100: 1af4:1001 virtio-pci

pci_register_driver还有一个重要的事情就是设置probe函数。有了probe函数,当设备和驱动匹配了之后,相应驱动的probe函数就会被调用,来实现驱动的加载。所以nvme_init返回后,这个驱动就啥事不做了,直到pci总线枚举出了这个nvme设备,然后就会调用我们的nvme_probe。

static int nvme_probe(struct pci_dev *pdev, const struct pci_device_id *id)

{

int node, result = -ENOMEM;

struct nvme_dev *dev;

node = dev_to_node(&pdev->dev);

if (node == NUMA_NO_NODE)

set_dev_node(&pdev->dev, 0);

dev = kzalloc_node(sizeof(*dev), GFP_KERNEL, node);

if (!dev)

return -ENOMEM;

dev->entry = kzalloc_node(num_possible_cpus() * sizeof(*dev->entry),

GFP_KERNEL, node);

if (!dev->entry)

goto free;

dev->queues = kzalloc_node((num_possible_cpus() + 1) * sizeof(void *),

GFP_KERNEL, node);

if (!dev->queues)

goto free;

dev->dev = get_device(&pdev->dev);

pci_set_drvdata(pdev, dev);

result = nvme_dev_map(dev);

if (result)

goto free;

INIT_LIST_HEAD(&dev->node);

INIT_WORK(&dev->scan_work, nvme_dev_scan);

INIT_WORK(&dev->reset_work, nvme_reset_work);

INIT_WORK(&dev->remove_work, nvme_remove_dead_ctrl_work);

mutex_init(&dev->shutdown_lock);

init_completion(&dev->ioq_wait);

result = nvme_setup_prp_pools(dev);

if (result)

goto put_pci;

result = nvme_init_ctrl(&dev->ctrl, &pdev->dev, &nvme_pci_ctrl_ops,

id->driver_data);

if (result)

goto release_pools;

queue_work(nvme_workq, &dev->reset_work);

return 0;

release_pools:

nvme_release_prp_pools(dev);

put_pci:

put_device(dev->dev);

nvme_dev_unmap(dev);

free:

kfree(dev->queues);

kfree(dev->entry);

kfree(dev);

return result;

}

nvme_probe流程分析:

为dev、dev->entry、dev->queues分配空间。

调用nvme_dev_map。

初始化三个work变量,关联回掉函数。

调用nvme_setup_prp_pools。

调用nvme_init_ctrl

通过workqueue调度dev->reset_work,也就是调度nvme_reset_work函数。

static int nvme_dev_map(struct nvme_dev *dev)

{

int bars;

struct pci_dev *pdev = to_pci_dev(dev->dev);

bars = pci_select_bars(pdev, IORESOURCE_MEM);

if (!bars)

return -ENODEV;

if (pci_request_selected_regions(pdev, bars, "nvme"))

return -ENODEV;

dev->bar = ioremap(pci_resource_start(pdev, 0), 8192);

if (!dev->bar)

goto release;

return 0;

release:

pci_release_regions(pdev);

return -ENODEV;

}

nvme_dev_map流程分析:

调用pci_select_bars,这个函数的返回值是一个mask值,每一位代表一个bar(base address register),哪一位被置位了,就代表哪一个bar为非零。这个涉及到pci的协议,pci协议里规定了pci设备的配置空间里有6个32位的bar寄存器,代表了pci设备上的一段内存空间(memory、io)。

在代码中我们可以尝试着增加一些调试代码,以便我们更好的理解。在修改好代码后,我们需要在buildroot目录下重新编译内核,这样可以很快速的得到一个新的内核,然后运行看结果。我查看过pci_select_bars的返回值,是0x11,代表bar0和bar4是非零的。

make linux-rebuild

调用pci_request_selected_regions,这个函数的一个参数就是之前调用pci_select_bars返回的mask值,作用就是把对应的这个几个bar保留起来,不让别人使用。

不调用pci_request_selected_regions的话/proc/iomem如下

# cat /proc/iomem

08000000-febfffff :PCIBus 0000:00

fd000000-fdffffff : 0000:00:02.0

fd000000-fdffffff : bochs-drm

feb80000-febbffff : 0000:00:03.0

febc0000-febcffff : 0000:00:02.0

febd0000-febd1fff : 0000:00:04.0

febd2000-febd2fff : 0000:00:02.0

febd2000-febd2fff : bochs-drm

febd3000-febd3fff : 0000:00:03.0

febd4000-febd4fff : 0000:00:04.0

febd5000-febd5fff : 0000:00:05.0

调用pci_request_selected_regions的话/proc/iomem如下,会多出两项nvme,bar0对应的物理地址就是0xfebd0000,bar4对应的是0xfebd4000。

# cat /proc/iomem

08000000-febfffff :PCIBus 0000:00

fd000000-fdffffff : 0000:00:02.0

fd000000-fdffffff : bochs-drm

feb80000-febbffff : 0000:00:03.0

febc0000-febcffff : 0000:00:02.0

febd0000-febd1fff : 0000:00:04.0

febd0000-febd1fff : nvme

febd2000-febd2fff : 0000:00:02.0

febd2000-febd2fff : bochs-drm

febd3000-febd3fff : 0000:00:03.0

febd4000-febd4fff : 0000:00:04.0

febd4000-febd4fff : nvme

febd5000-febd5fff : 0000:00:05.0

调用ioremap。前面说到bar0对应的物理地址是0xfebd0000,在linux中我们无法直接访问物理地址,需要映射到虚拟地址,ioremap就是这个作用。映射完后,我们访问dev->bar就可以直接操作nvme设备上的寄存器了。但是代码中,并没有根据pci_select_bars的返回值来决定映射哪个bar,而是直接hard code成映射bar0,原因是nvme协议中强制规定了bar0就是内存映射的基址。而bar4是自定义用途,暂时还不确定有什么用。

static int nvme_setup_prp_pools(struct nvme_dev *dev)

{

dev->prp_page_pool = dma_pool_create("prp list page", dev->dev,

PAGE_SIZE, PAGE_SIZE, 0);

if (!dev->prp_page_pool)

return -ENOMEM;

/* Optimisation for I/Os between 4k and 128k */

dev->prp_small_pool = dma_pool_create("prp list 256", dev->dev,

256, 256, 0);

if (!dev->prp_small_pool) {

dma_pool_destroy(dev->prp_page_pool);

return -ENOMEM;

}

return 0;

}

回到nvme_probe来看nvme_setup_prp_pools,主要是分配了一些dma相关的东西。

int nvme_init_ctrl(struct nvme_ctrl *ctrl, struct device *dev,

const struct nvme_ctrl_ops *ops, unsigned long quirks)

{

int ret;

INIT_LIST_HEAD(&ctrl->namespaces);

mutex_init(&ctrl->namespaces_mutex);

kref_init(&ctrl->kref);

ctrl->dev = dev;

ctrl->ops = ops;

ctrl->quirks = quirks;

ret = nvme_set_instance(ctrl);

if (ret)

goto out;

ctrl->device = device_create_with_groups(nvme_class, ctrl->dev,

MKDEV(nvme_char_major, ctrl->instance),

dev, nvme_dev_attr_groups,

"nvme%d", ctrl->instance);

if (IS_ERR(ctrl->device)) {

ret = PTR_ERR(ctrl->device);

goto out_release_instance;

}

get_device(ctrl->device);

dev_set_drvdata(ctrl->device, ctrl);

ida_init(&ctrl->ns_ida);

spin_lock(&dev_list_lock);

list_add_tail(&ctrl->node, &nvme_ctrl_list);

spin_unlock(&dev_list_lock);

return 0;

out_release_instance:

nvme_release_instance(ctrl);

out:

return ret;

}

回到nvme_probe,nvme_init_ctrl里主要做的事情就是通过device_create_with_groups创建一个名字叫nvme0的字符设备,也就是我们之前见到的。

crw------- 1 root root 253, 0 Jun 3 13:00 nvme0

再次回到nvme_probe,dev->reset_work被调度,也就是nvme_reset_work被调用了。

static void nvme_reset_work(struct work_struct *work)

{

struct nvme_dev *dev = container_of(work, struct nvme_dev, reset_work);

int result = -ENODEV;

if (WARN_ON(test_bit(NVME_CTRL_RESETTING, &dev->flags)))

goto out;

/*

* If we're called to reset a live controller first shut it down before

* moving on.

*/

if (dev->ctrl.ctrl_config & NVME_CC_ENABLE)

nvme_dev_disable(dev, false);

set_bit(NVME_CTRL_RESETTING, &dev->flags);

result = nvme_pci_enable(dev);

if (result)

goto out;

result = nvme_configure_admin_queue(dev);

if (result)

goto out;

nvme_init_queue(dev->queues[0], 0);

result = nvme_alloc_admin_tags(dev);

if (result)

goto out;

result = nvme_init_identify(&dev->ctrl);

if (result)

goto out;

result = nvme_setup_io_queues(dev);

if (result)

goto out;

dev->ctrl.event_limit = NVME_NR_AEN_COMMANDS;

result = nvme_dev_list_add(dev);

if (result)

goto out;

/*

* Keep the controller around but remove all namespaces if we don't have

* any working I/O queue.

*/

if (dev->online_queues < 2) {

dev_warn(dev->dev, "IO queues not created\n");

nvme_remove_namespaces(&dev->ctrl);

} else {

nvme_start_queues(&dev->ctrl);

nvme_dev_add(dev);

}

clear_bit(NVME_CTRL_RESETTING, &dev->flags);

return;

out:

nvme_remove_dead_ctrl(dev, result);

}

nvme_reset_work流程分析:

首先通过NVME_CTRL_RESETTING标志来确保nvme_reset_work不会被重复进入。

调用nvme_pci_enable。

调用nvme_configure_admin_queue。

调用nvme_init_queue

调用nvme_alloc_admin_tags

调用nvme_init_identify

调用nvme_setup_io_queues

调用nvme_dev_list_add

static int nvme_pci_enable(struct nvme_dev *dev)

{

u64 cap;

int result = -ENOMEM;

struct pci_dev *pdev = to_pci_dev(dev->dev);

if (pci_enable_device_mem(pdev))

return result;

dev->entry[0].vector = pdev->irq;

pci_set_master(pdev);

if (dma_set_mask_and_coherent(dev->dev, DMA_BIT_MASK(64)) &&

dma_set_mask_and_coherent(dev->dev, DMA_BIT_MASK(32)))

goto disable;

if (readl(dev->bar + NVME_REG_CSTS) == -1) {

result = -ENODEV;

goto disable;

}

/*

* Some devices don't advertse INTx interrupts, pre-enable a single

* MSIX vec for setup. We'll adjust this later.

*/

if (!pdev->irq) {

result = pci_enable_msix(pdev, dev->entry, 1);

if (result < 0)

goto disable;

}

cap = lo_hi_readq(dev->bar + NVME_REG_CAP);

dev->q_depth = min_t(int, NVME_CAP_MQES(cap) + 1, NVME_Q_DEPTH);

dev->db_stride = 1 << NVME_CAP_STRIDE(cap);

dev->dbs = dev->bar + 4096;

/*

* Temporary fix for the Apple controller found in the MacBook8,1 and

* some MacBook7,1 to avoid controller resets and data loss.

*/

if (pdev->vendor == PCI_VENDOR_ID_APPLE && pdev->device == 0x2001) {

dev->q_depth = 2;

dev_warn(dev->dev, "detected Apple NVMe controller, set "

"queue depth=%u to work around controller resets\n",

dev->q_depth);

}

if (readl(dev->bar + NVME_REG_VS) >= NVME_VS(1, 2))

dev->cmb = nvme_map_cmb(dev);

pci_enable_pcie_error_reporting(pdev);

pci_save_state(pdev);

return 0;

disable:

pci_disable_device(pdev);

return result;

}

nvme_pci_enable流程分析:

调用pci_enable_device_mem来使能nvme设备的内存空间,也就是之前映射的bar0空间。

之后就可以通过readl(dev->bar + NVME_REG_CSTS)来直接操作nvme设备上的控制寄存器了,也就是nvme协议中的如下这个表。

pci有两种中断模式,一种是INT,另一种是MSI。假如不支持INT模式的话,就使能MSI模式。在这里使用的是INT模式,irq号为11。

# cat /proc/interrupts

CPU0

0: 86 IO-APIC 2-edge timer

1: 9 IO-APIC 1-edge i8042

4: 250 IO-APIC 4-edge serial

9: 0 IO-APIC 9-fasteoi acpi

10: 100 IO-APIC 10-fasteoi virtio1

11: 13 IO-APIC 11-fasteoi virtio0, nvme0q0, nvme0q1

12: 125 IO-APIC 12-edge i8042

14: 0 IO-APIC 14-edge ata_piix

15: 5 IO-APIC 15-edge ata_piix

从CAP寄存器中获得一些配置参数,并把dev->dbs设置成dev->bar+4096。4096的由来是上面表里doorbell寄存器的起始地址是0x1000。

假如nvme协议的版本大于等于1.2的话,需要调用nvme_map_cmb映射controller memory buffer。但是现在2.5版的qemu实现的nvme是1.1版的,所以这些不被支持。

回到nvme_reset_work分析nvme_configure_admin_queue。

static int nvme_configure_admin_queue(struct nvme_dev *dev)

{

int result;

u32 aqa;

u64 cap = lo_hi_readq(dev->bar + NVME_REG_CAP);

struct nvme_queue *nvmeq;

dev->subsystem = readl(dev->bar + NVME_REG_VS) >= NVME_VS(1, 1) ?

NVME_CAP_NSSRC(cap) : 0;

if (dev->subsystem &&

(readl(dev->bar + NVME_REG_CSTS) & NVME_CSTS_NSSRO))

writel(NVME_CSTS_NSSRO, dev->bar + NVME_REG_CSTS);

result = nvme_disable_ctrl(&dev->ctrl, cap);

if (result < 0)

return result;

nvmeq = dev->queues[0];

if (!nvmeq) {

nvmeq = nvme_alloc_queue(dev, 0, NVME_AQ_DEPTH);

if (!nvmeq)

return -ENOMEM;

}

aqa = nvmeq->q_depth - 1;

aqa |= aqa << 16;

writel(aqa, dev->bar + NVME_REG_AQA);

lo_hi_writeq(nvmeq->sq_dma_addr, dev->bar + NVME_REG_ASQ);

lo_hi_writeq(nvmeq->cq_dma_addr, dev->bar + NVME_REG_ACQ);

result = nvme_enable_ctrl(&dev->ctrl, cap);

if (result)

goto free_nvmeq;

nvmeq->cq_vector = 0;

result = queue_request_irq(dev, nvmeq, nvmeq->irqname);

if (result) {

nvmeq->cq_vector = -1;

goto free_nvmeq;

}

return result;

free_nvmeq:

nvme_free_queues(dev, 0);

return result;

}

nvme_configure_admin_queue流程分析:

从CAP寄存器中获悉对Subsystem Reset的支持

调用nvme_disable_ctrl

调用nvme_alloc_queue

调用nvme_enable_ctrl

调用queue_request_irq

int nvme_disable_ctrl(struct nvme_ctrl *ctrl, u64 cap)

{

int ret;

ctrl->ctrl_config &= ~NVME_CC_SHN_MASK;

ctrl->ctrl_config &= ~NVME_CC_ENABLE;

ret = ctrl->ops->reg_write32(ctrl, NVME_REG_CC, ctrl->ctrl_config);

if (ret)

return ret;

return nvme_wait_ready(ctrl, cap, false);

}

这里的ctrl->ops就是之前nvme_init_ctrl时传进去的nvme_pci_ctrl_ops

static const struct nvme_ctrl_ops nvme_pci_ctrl_ops = {

.reg_read32 = nvme_pci_reg_read32,

.reg_write32 = nvme_pci_reg_write32,

.reg_read64 = nvme_pci_reg_read64,

.io_incapable = nvme_pci_io_incapable,

.reset_ctrl = nvme_pci_reset_ctrl,

.free_ctrl = nvme_pci_free_ctrl,

};

static int nvme_wait_ready(struct nvme_ctrl *ctrl, u64 cap, bool enabled)

{

unsigned long timeout =

((NVME_CAP_TIMEOUT(cap) + 1) * HZ / 2) + jiffies;

u32 csts, bit = enabled ? NVME_CSTS_RDY : 0;

int ret;

while ((ret = ctrl->ops->reg_read32(ctrl, NVME_REG_CSTS, &csts)) == 0) {

if ((csts & NVME_CSTS_RDY) == bit)

break;

msleep(100);

if (fatal_signal_pending(current))

return -EINTR;

if (time_after(jiffies, timeout)) {

dev_err(ctrl->dev,

"Device not ready; aborting %s\n", enabled ?

"initialisation" : "reset");

return -ENODEV;

}

}

return ret;

}

未完待续…

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值