第 10 章. ISA 设备驱动程序

10.1. 概述

本章介绍了编写 ISA 设备驱动程序的相关问题。此处提供的伪代码相当详细,让人联想到真实的代码,但仍然只是伪代码。它避免了与讨论主题无关的细节。真实的示例可以在真实驱动程序的源代码中找到。特别是 epaha 驱动程序是获取信息的好来源。

10.2. 基本信息

一个典型的 ISA 驱动程序需要以下包含文件

#include <sys/module.h>
#include <sys/bus.h>
#include <machine/bus.h>
#include <machine/resource.h>
#include <sys/rman.h>

#include <isa/isavar.h>
#include <isa/pnpvar.h>

它们描述了 ISA 和通用总线子系统特有的内容。

总线子系统以面向对象的方式实现,其主要结构通过关联的方法函数访问。

ISA 驱动程序实现的总线方法列表与任何其他总线的方法列表类似。对于一个名为“xxx”的假设驱动程序,它们将是

  • static void xxx_isa_identify (driver_t *, device_t); 通常用于总线驱动程序,而不是设备驱动程序。但是对于 ISA 设备,此方法可能具有特殊用途:如果设备提供了一些设备特定的(非 PnP)方式来自动检测设备,则此例程可以实现它。

  • static int xxx_isa_probe (device_t dev); 在已知(或 PnP)位置探测设备。此例程还可以适应设备特定的自动检测部分配置设备的参数。

  • static int xxx_isa_attach (device_t dev); 连接和初始化设备。

  • static int xxx_isa_detach (device_t dev); 在卸载驱动程序模块之前分离设备。

  • static int xxx_isa_shutdown (device_t dev); 在系统关机之前执行设备的关机操作。

  • static int xxx_isa_suspend (device_t dev); 在系统进入省电状态之前挂起设备。也可以中止过渡到省电状态。

  • static int xxx_isa_resume (device_t dev); 从省电状态返回后恢复设备活动。

xxx_isa_probe()xxx_isa_attach() 是强制性的,其余例程是可选的,具体取决于设备的需求。

驱动程序通过以下描述集链接到系统。

    /* table of supported bus methods */
    static device_method_t xxx_isa_methods[] = {
        /* list all the bus method functions supported by the driver */
        /* omit the unsupported methods */
        DEVMETHOD(device_identify,  xxx_isa_identify),
        DEVMETHOD(device_probe,     xxx_isa_probe),
        DEVMETHOD(device_attach,    xxx_isa_attach),
        DEVMETHOD(device_detach,    xxx_isa_detach),
        DEVMETHOD(device_shutdown,  xxx_isa_shutdown),
        DEVMETHOD(device_suspend,   xxx_isa_suspend),
        DEVMETHOD(device_resume,    xxx_isa_resume),

	DEVMETHOD_END
    };

    static driver_t xxx_isa_driver = {
        "xxx",
        xxx_isa_methods,
        sizeof(struct xxx_softc),
    };

    static devclass_t xxx_devclass;

    DRIVER_MODULE(xxx, isa, xxx_isa_driver, xxx_devclass,
        load_function, load_argument);

这里 struct xxx_softc 是一个设备特定的结构,包含私有驱动程序数据和驱动程序资源的描述符。总线代码根据需要自动为每个设备分配一个 softc 描述符。

如果驱动程序作为可加载模块实现,则在加载或卸载驱动程序时调用 load_function() 来执行驱动程序特定的初始化或清理操作,并将 load_argument 作为其参数之一传递。如果驱动程序不支持动态加载(换句话说,它必须始终链接到内核),则这些值应设置为 0,最后一个定义将如下所示

 DRIVER_MODULE(xxx, isa, xxx_isa_driver,
       xxx_devclass, 0, 0);

如果驱动程序是针对支持 PnP 的设备的,则必须定义一个支持的 PnP ID 表。该表由此驱动程序支持的 PnP ID 列表以及具有这些 ID 的硬件类型和型号的人类可读描述组成。它看起来像

    static struct isa_pnp_id xxx_pnp_ids[] = {
        /* a line for each supported PnP ID */
        { 0x12345678,   "Our device model 1234A" },
        { 0x12345679,   "Our device model 1234B" },
        { 0,        NULL }, /* end of table */
    };

如果驱动程序不支持 PnP 设备,它仍然需要一个空的 PnP ID 表,例如

    static struct isa_pnp_id xxx_pnp_ids[] = {
        { 0,        NULL }, /* end of table */
    };

10.3. device_t 指针

device_t 是设备结构的指针类型。这里我们只考虑从设备驱动程序编写者的角度来看有趣的方法。操作设备结构中值的函数如下

  • device_t device_get_parent(dev) 获取设备的父总线。

  • driver_t device_get_driver(dev) 获取指向其驱动程序结构的指针。

  • char *device_get_name(dev) 获取驱动程序名称,例如我们示例中的 "xxx"

  • int device_get_unit(dev) 获取单元号(单元号从与每个驱动程序关联的设备的 0 开始编号)。

  • char *device_get_nameunit(dev) 获取包含单元号的设备名称,例如“xxx0”、“xxx1”等。

  • char *device_get_desc(dev) 获取设备描述。通常,它以人类可读的形式描述设备的确切型号。

  • device_set_desc(dev, desc) 设置描述。这使得设备描述指向字符串 desc,在之后不得释放或更改。

  • device_set_desc_copy(dev, desc) 设置描述。描述被复制到内部动态分配的缓冲区中,因此字符串 desc 之后可以更改,而不会产生不利影响。

  • void *device_get_softc(dev) 获取指向与此设备关联的设备描述符 (struct xxx_softc) 的指针。

  • u_int32_t device_get_flags(dev) 获取配置文件中为设备指定的标志。

可以使用便利函数 device_printf(dev, fmt, …​) 打印来自设备驱动程序的消息。它会自动在消息前加上单元名称和冒号。

device_t 方法在文件 kern/bus_subr.c 中实现。

10.4. 配置文件和自动配置期间识别和探测的顺序

ISA 设备在内核配置文件中描述如下

device xxx0 at isa? port 0x300 irq 10 drq 5
       iomem 0xd0000 flags 0x1 sensitive

port、IRQ 等的值被转换为与设备关联的资源值。它们是可选的,具体取决于设备的需求和自动配置能力。例如,某些设备根本不需要 DRQ,而某些设备允许驱动程序从设备配置端口读取 IRQ 设置。如果一台机器有多个 ISA 总线,则可以在配置行中指定确切的总线,例如 isa0isa1,否则将在所有 ISA 总线上搜索该设备。

sensitive 是一个资源请求,要求在所有非敏感设备之前探测此设备。它受支持,但在任何当前驱动程序中似乎都没有使用。

对于许多情况下的传统 ISA 设备,驱动程序仍然能够检测配置参数。但是,系统中要配置的每个设备都必须有一行配置。如果系统中安装了两种类型的设备,但对应驱动程序只有一行配置,例如

device xxx0 at isa?
then only one device will be configured.

但是对于通过即插即用或某些专有协议支持自动识别的设备,一行配置就足以配置系统中的所有设备,例如上面的一行,或者只是简单地

device xxx at isa?

如果驱动程序同时支持自动识别和传统设备,并且两种类型的设备都同时安装在一台机器中,那么只需在配置文件中描述传统设备即可。自动识别的设备将自动添加。

当 ISA 总线自动配置时,事件按以下顺序发生

所有驱动程序的识别例程(包括识别所有 PnP 设备的 PnP 识别例程)都是以随机顺序调用的。当它们识别设备时,会将设备添加到 ISA 总线上的列表中。通常,驱动程序的识别例程会将其驱动程序与新设备关联。PnP 识别例程尚不知道其他驱动程序,因此它不会将其中的任何一个与新添加的设备关联。

使用 PnP 协议将 PnP 设备置于睡眠状态,以防止它们被探测为传统设备。

调用标记为sensitive的非 PnP 设备的探测例程。如果设备探测成功,则对其调用附加例程。

所有非 PnP 设备的探测和附加例程都以类似的方式调用。

PnP 设备从睡眠状态恢复,并分配其请求的资源:I/O 和内存地址范围、IRQ 和 DRQ,所有这些资源都不与已附加的传统设备冲突。

然后,对于每个 PnP 设备,都会调用所有存在的 ISA 驱动程序的探测例程。第一个声明该设备的驱动程序将被附加。多个驱动程序可能会以不同的优先级声明该设备;在这种情况下,最高优先级的驱动程序获胜。探测例程必须调用ISA_PNP_PROBE()以将实际的 PnP ID 与驱动程序支持的 ID 列表进行比较,如果 ID 不在表中,则返回失败。这意味着绝对每个驱动程序,即使是不支持任何 PnP 设备的驱动程序,也必须调用ISA_PNP_PROBE(),至少使用一个空的 PnP ID 表,以便对未知的 PnP 设备返回失败。

探测例程在出错时返回正值(错误代码),在成功时返回零或负值。

当 PnP 设备支持多个接口时,使用负返回值。例如,较旧的兼容性接口和较新的高级接口,由不同的驱动程序支持。然后两个驱动程序都会检测到该设备。在探测例程中返回值较高的驱动程序优先(换句话说,返回值为 0 的驱动程序优先级最高,返回值为 -1 的次之,返回值为 -2 的再次之)。结果,仅支持旧接口的设备将由旧驱动程序处理(该驱动程序应从探测例程返回 -1),而也支持新接口的设备将由新驱动程序处理(该驱动程序应从探测例程返回 0)。如果多个驱动程序返回相同的值,则先调用的驱动程序获胜。因此,如果驱动程序返回值为 0,则可以确定它赢得了优先级仲裁。

设备特定的识别例程也可以为设备分配驱动程序类而不是驱动程序。然后,对该设备探测类中的所有驱动程序,就像 PnP 的情况一样。此功能在任何现有驱动程序中均未实现,并且本文档中不再对此进行讨论。

由于在探测传统设备时 PnP 设备被禁用,因此它们不会被附加两次(一次作为传统设备,一次作为 PnP 设备)。但在设备相关的识别例程的情况下,驱动程序有责任确保不会由驱动程序两次附加同一设备:一次作为传统用户配置设备,一次作为自动识别设备。

自动识别设备(包括 PnP 和设备特定的)的另一个实际结果是,无法从内核配置文件向其传递标志。因此,它们要么根本不使用标志,要么对所有自动识别设备使用设备单元 0 中的标志,要么使用 sysctl 接口而不是标志。

其他不寻常的配置可以通过使用resource_query_*()resource_*_value()系列函数直接访问配置资源来适应。它们的实现位于kern/subr_bus.c中。旧的 IDE 磁盘驱动程序i386/isa/wd.c包含此类用法的示例。但是,必须始终优先使用标准配置方法。将配置资源的解析留给总线配置代码。

10.5. 资源

用户输入内核配置文件的信息会被处理并作为配置资源传递给内核。此信息由总线配置代码解析并转换为结构device_t的值及其关联的总线资源。对于更复杂的配置情况,驱动程序可以使用resource_*函数直接访问配置资源。但是,通常不需要也不推荐这样做,因此此处不再讨论此问题。

总线资源与每个设备相关联。它们通过类型和类型内的编号来识别。对于 ISA 总线,定义了以下类型

  • SYS_RES_IRQ - 中断号

  • SYS_RES_DRQ - ISA DMA 通道号

  • SYS_RES_MEMORY - 设备内存映射到系统内存空间的范围

  • SYS_RES_IOPORT - 设备 I/O 寄存器的范围

类型内的枚举从 0 开始,因此如果设备有两个内存区域,则它将具有编号为 0 和 1 的类型为SYS_RES_MEMORY的资源。资源类型与 C 语言类型无关,所有资源值都具有 C 语言类型unsigned long,并且必须根据需要进行强制转换。资源编号不必连续,尽管对于 ISA 它们通常是连续的。ISA 设备允许的资源编号为

          IRQ: 0-1
          DRQ: 0-1
          MEMORY: 0-3
          IOPORT: 0-7

所有资源都表示为范围,具有起始值和计数。对于 IRQ 和 DRQ 资源,计数通常等于 1。内存的值指的是物理地址。

可以在资源上执行三种类型的活动

  • 设置/获取

  • 分配/释放

  • 激活/停用

设置设置资源使用的范围。分配保留请求的范围,以便其他驱动程序无法保留它(并检查其他驱动程序是否尚未保留此范围)。激活通过执行必要的任何操作(例如,对于内存,它将映射到内核虚拟地址空间)使驱动程序能够访问该资源。

用于操作资源的函数为

  • int bus_set_resource(device_t dev, int type, int rid, u_long start, u_long count)

    为资源设置范围。如果成功,则返回 0,否则返回错误代码。通常,仅当typeridstartcount中的一个具有超出允许范围的值时,此函数才会返回错误。

    • dev - 驱动程序的设备

    • type - 资源类型,SYS_RES_*

    • rid - 类型内的资源编号(ID)

    • start, count - 资源范围

  • int bus_get_resource(device_t dev, int type, int rid, u_long *startp, u_long *countp)

    获取资源范围。如果成功,则返回 0,如果资源尚未定义,则返回错误代码。

  • u_long bus_get_resource_start(device_t dev, int type, int rid) u_long bus_get_resource_count (device_t dev, int type, int rid)

    便捷函数,仅获取开始或计数。在错误情况下返回 0,因此如果资源开始在合法值中为 0,则无法判断该值是 0 还是发生了错误。幸运的是,附加驱动程序的任何 ISA 资源都不能具有等于 0 的起始值。

  • void bus_delete_resource(device_t dev, int type, int rid)

    删除资源,使其未定义。

  • struct resource * bus_alloc_resource(device_t dev, int type, int *rid, u_long start, u_long end, u_long count, u_int flags)

    将资源分配为一个由 count 个值组成的范围,这些值未被其他人分配,位于 start 和 end 之间。唉,不支持对齐。如果资源尚未设置,则会自动创建它。start 为 0 和 end 为 ~0(全为 1)的特殊值表示必须使用之前由bus_set_resource()设置的固定值:start 和 count 本身以及 end=(start+count),在这种情况下,如果资源之前未定义,则返回错误。尽管 rid 通过引用传递,但它不会在 ISA 总线的资源分配代码中的任何位置设置。(其他总线可能使用不同的方法并修改它)。

标志是位图,调用方感兴趣的标志为

  • RF_ACTIVE - 导致资源在分配后自动激活。

  • RF_SHAREABLE - 资源可以被多个驱动程序同时共享。

  • RF_TIMESHARE - 资源可以被多个驱动程序时间共享,即,可以被许多驱动程序同时分配,但在任何给定时间点只能由一个驱动程序激活。

  • 出错时返回 0。可以使用方法rhand_*()从返回的句柄中获取分配的值。

  • int bus_release_resource(device_t dev, int type, int rid, struct resource *r)

  • 释放资源,r 是bus_alloc_resource()返回的句柄。如果成功,则返回 0,否则返回错误代码。

  • int bus_activate_resource(device_t dev, int type, int rid, struct resource *r) int bus_deactivate_resource(device_t dev, int type, int rid, struct resource *r)

  • 激活或停用资源。如果成功,则返回 0,否则返回错误代码。如果资源是时间共享的,并且当前由另一个驱动程序激活,则返回EBUSY

  • int bus_setup_intr(device_t dev, struct resource *r, int flags, driver_intr_t *handler, void *arg, void **cookiep) int bus_teardown_intr(device_t dev, struct resource *r, void *cookie)

  • 将中断处理程序与设备关联或解除关联。如果成功,则返回 0,否则返回错误代码。

  • r - 描述 IRQ 的已激活资源处理程序

    flags - 中断优先级级别,以下之一

    • INTR_TYPE_TTY - 终端和其他类似字符类型设备。要屏蔽它们,请使用spltty()

    • (INTR_TYPE_TTY | INTR_TYPE_FAST) - 输入缓冲区较小的终端类型设备,对于输入数据丢失至关重要(例如旧式串行端口)。要屏蔽它们,请使用spltty()

    • INTR_TYPE_BIO - 块类型设备,但 CAM 控制器上的设备除外。要屏蔽它们,请使用splbio()

    • INTR_TYPE_CAM - CAM(通用访问方法)总线控制器。要屏蔽它们,请使用splcam()

    • INTR_TYPE_NET - 网络接口控制器。要屏蔽它们,请使用splimp()

    • INTR_TYPE_MISC - 各种设备。除了splhigh()(屏蔽所有中断)之外,没有其他方法可以屏蔽它们。

当中断处理程序执行时,将屏蔽所有与它的优先级级别匹配的其他中断。唯一的例外是 MISC 级别,对于该级别,不会屏蔽任何其他中断,并且不会被任何其他中断屏蔽。

  • handler - 处理程序函数的指针,类型driver_intr_t定义为void driver_intr_t(void *)

  • arg - 传递给处理程序的参数,用于识别此特定设备。处理程序会将它从 void* 转换为任何实际类型。ISA 中断处理程序的旧约定是使用单元号作为参数,新的(推荐)约定是使用指向设备 softc 结构的指针。

  • cookie[p] - 从 setup() 收到的值,用于在传递给 teardown() 时识别处理程序。

定义了一些方法来操作资源处理程序(struct resource *)。设备驱动程序编写者感兴趣的方法有:

  • u_long rman_get_start(r) u_long rman_get_end(r) 获取分配的资源范围的起始和结束地址。

  • void *rman_get_virtual(r) 获取已激活内存资源的虚拟地址。

10.6. 总线内存映射

在许多情况下,驱动程序和设备之间的数据交换是通过内存进行的。可能有两种情况:

(a) 内存位于设备卡上

(b) 内存是计算机的主内存

在情况 (a) 中,驱动程序始终根据需要在板载内存和主内存之间来回复制数据。为了将板载内存映射到内核虚拟地址空间,必须将板载内存的物理地址和长度定义为 SYS_RES_MEMORY 资源。然后可以分配和激活该资源,并使用 rman_get_virtual() 获取其虚拟地址。旧的驱动程序为此目的使用了函数 pmap_mapdev(),现在不应该直接使用它了。现在它是资源激活的内部步骤之一。

大多数 ISA 卡的内存都配置为物理位置在 640KB-1MB 范围内。一些 ISA 卡需要更大的内存范围,这些范围应该放在 16MB 以下(因为 ISA 总线上的地址限制为 24 位)。在这种情况下,如果机器的内存比设备内存的起始地址大(换句话说,它们重叠),则必须在设备使用的地址范围内配置内存空洞。许多 BIOS 允许配置从 14MB 或 15MB 开始的 1MB 内存空洞。如果 BIOS 正确报告内存空洞,则 FreeBSD 可以正确处理它们(此功能在旧 BIOS 上可能存在问题)。

在情况 (b) 中,只需将数据的地址发送到设备,设备使用 DMA 实际访问主内存中的数据。存在两个限制:首先,ISA 卡只能访问 16MB 以下的内存。其次,虚拟地址空间中的连续页面在物理地址空间中可能不连续,因此设备可能必须执行分散/聚集操作。总线子系统为其中的一些问题提供了现成的解决方案,其余的必须由驱动程序自己完成。

两个结构用于 DMA 内存分配,bus_dma_tag_tbus_dmamap_t。标签描述了 DMA 内存所需的属性。映射表示根据这些属性分配的内存块。多个映射可以与同一个标签关联。

标签以树状层次结构组织,并继承属性。子标签继承其父标签的所有要求,并且可以使它们更严格,但绝不能更宽松。

通常,每个设备单元都会创建一个顶级标签(没有父标签)。如果每个设备需要多个具有不同要求的内存区域,则可以为每个区域创建一个标签作为父标签的子标签。

标签可以通过两种方式用于创建映射。

首先,可以分配一段符合标签要求的连续内存(稍后可以释放)。这通常用于分配相对较长生命周期的内存区域,用于与设备通信。将此类内存加载到映射中非常简单:它始终被视为适当物理内存范围内的单个块。

其次,可以将虚拟内存的任意区域加载到映射中。将检查此内存的每个页面是否符合映射要求。如果符合,则将其保留在原始位置。如果不符合,则会分配一个新的符合要求的“弹跳页面”并用作中间存储。当从不符合要求的原始页面写入数据时,它们将首先复制到其弹跳页面,然后从弹跳页面传输到设备。当读取数据时,数据将从设备传输到弹跳页面,然后复制到其不符合要求的原始页面。在原始页面和弹跳页面之间复制的过程称为同步。这通常在每次传输的基础上使用:为每次传输加载缓冲区,完成传输并卸载缓冲区。

在 DMA 内存上工作的函数有:

  • int bus_dma_tag_create(bus_dma_tag_t parent, bus_size_t alignment, bus_size_t boundary, bus_addr_t lowaddr, bus_addr_t highaddr, bus_dma_filter_t *filter, void *filterarg, bus_size_t maxsize, int nsegments, bus_size_t maxsegsz, int flags, bus_dma_tag_t *dmat)

    创建一个新的标签。成功返回 0,否则返回错误代码。

    • parent - 父标签,或 NULL 以创建顶级标签。

    • alignment - 为此标签分配的内存区域所需的物理对齐方式。使用值 1 表示“无特定对齐”。仅适用于将来的 bus_dmamem_alloc() 调用,而不适用于 bus_dmamap_create() 调用。

    • boundary - 分配内存时不得跨越的物理地址边界。使用值 0 表示“无边界”。仅适用于将来的 bus_dmamem_alloc() 调用,而不适用于 bus_dmamap_create() 调用。必须是 2 的幂。如果计划在非级联 DMA 模式下使用内存(即,DMA 地址不是由设备本身提供,而是由 ISA DMA 控制器提供),则由于 DMA 硬件的限制,边界不得大于 64KB (64*1024)。

    • lowaddr, highaddr - 名称略有误导;这些值用于限制用于分配内存的物理地址的允许范围。确切的含义取决于计划的未来用途

      • 对于 bus_dmamem_alloc(),从 0 到 lowaddr-1 的所有地址都被认为是允许的,更高的地址被禁止。

      • 对于 bus_dmamap_create(),[lowaddr; highaddr] 包含范围之外的所有地址都被认为是可访问的。范围内的页面的地址将传递给过滤器函数,该函数决定它们是否可访问。如果没有提供过滤器函数,则整个范围都被认为是不可访问的。

      • 对于 ISA 设备,正常值(没有过滤器函数)为:

        lowaddr = BUS_SPACE_MAXADDR_24BIT

        highaddr = BUS_SPACE_MAXADDR

    • filter, filterarg - 过滤器函数及其参数。如果为过滤器传递 NULL,则在执行 bus_dmamap_create() 时,整个范围 [lowaddr, highaddr] 被认为是不可访问的。否则,[lowaddr; highaddr] 范围内每个尝试页面的物理地址将传递给过滤器函数,该函数决定它是否可访问。过滤器函数的原型为:int filterfunc(void *arg, bus_addr_t paddr)。如果页面可访问,则必须返回 0,否则返回非零值。

    • maxsize - 通过此标签可能分配的内存的最大大小(以字节为单位)。如果难以估计或可以任意大,则 ISA 设备的值将为 BUS_SPACE_MAXSIZE_24BIT

    • nsegments - 设备支持的分散/聚集段的最大数量。如果不受限制,则应使用值 BUS_SPACE_UNRESTRICTED。此值推荐用于父标签,实际限制将在子标签中指定。nsegments 等于 BUS_SPACE_UNRESTRICTED 的标签可能无法用于实际加载映射,它们只能用作父标签。nsegments 的实际限制似乎约为 250-300,更高的值会导致内核堆栈溢出(硬件通常无法支持如此多的分散/聚集缓冲区)。

    • maxsegsz - 设备支持的分散/聚集段的最大大小。ISA 设备的最大值为 BUS_SPACE_MAXSIZE_24BIT

    • flags - 标志的位图。唯一有趣的标志是:

      • BUS_DMA_ALLOCNOW - 请求在创建标签时分配所有可能需要的弹跳页面。

    • dmat - 指向要返回的新标签的存储位置的指针。

  • int bus_dma_tag_destroy(bus_dma_tag_t dmat)

    销毁一个标签。成功返回 0,否则返回错误代码。

    dmat - 要销毁的标签。

  • int bus_dmamem_alloc(bus_dma_tag_t dmat, void** vaddr, int flags, bus_dmamap_t *mapp)

    分配由标签描述的连续内存区域。要分配的内存大小为标签的 maxsize。成功返回 0,否则返回错误代码。结果仍必须由 bus_dmamap_load() 加载才能使用,以便获取内存的物理地址。

    • dmat - 标签

    • vaddr - 指向要返回的已分配区域的内核虚拟地址的存储位置的指针。

    • flags - 标志的位图。唯一有趣的标志是:

      • BUS_DMA_NOWAIT - 如果内存无法立即使用,则返回错误。如果未设置此标志,则允许例程休眠,直到内存可用。

    • mapp - 指向要返回的新映射的存储位置的指针。

  • void bus_dmamem_free(bus_dma_tag_t dmat, void *vaddr, bus_dmamap_t map)

    释放 bus_dmamem_alloc() 分配的内存。目前,尚未实现释放使用 ISA 限制分配的内存的功能。因此,建议的使用模型是尽可能长时间地保留和重复使用已分配的区域。不要轻易释放某个区域,然后很快又分配它。这并不意味着 bus_dmamem_free() 完全不应该使用:希望它很快就会得到正确实现。

    • dmat - 标签

    • vaddr - 内存的内核虚拟地址

    • map - 内存的映射(由 bus_dmamem_alloc() 返回)

  • int bus_dmamap_create(bus_dma_tag_t dmat, int flags, bus_dmamap_t *mapp)

    为标签创建一个映射,以便稍后在 bus_dmamap_load() 中使用。成功返回 0,否则返回错误代码。

    • dmat - 标签

    • flags - 从理论上讲,它是标志的位图。但目前还没有定义任何标志,因此它始终为 0。

    • mapp - 指向要返回的新映射的存储位置的指针

  • int bus_dmamap_destroy(bus_dma_tag_t dmat, bus_dmamap_t map)

    销毁一个映射。成功返回 0,否则返回错误代码。

    • dmat - 与映射关联的标签

    • map - 要销毁的映射

  • int bus_dmamap_load(bus_dma_tag_t dmat, bus_dmamap_t map, void *buf, bus_size_t buflen, bus_dmamap_callback_t *callback, void *callback_arg, int flags)

    将缓冲区加载到映射中(映射必须先前由bus_dmamap_create()bus_dmamem_alloc()创建)。检查缓冲区的所有页面是否符合标签要求,对于不符合要求的页面,将分配跳跃页面。构建一个物理段描述符数组并传递给回调例程。然后,预期此回调例程以某种方式处理它。系统中的跳跃缓冲区数量有限,因此如果需要跳跃缓冲区但无法立即使用,则请求将被排队,并在跳跃缓冲区可用时调用回调。如果回调立即执行,则返回 0;如果请求已排队以供将来执行,则返回EINPROGRESS。在后一种情况下,与排队回调例程的同步由驱动程序负责。

    • dmat - 标签

    • map - 映射

    • buf - 缓冲区的内核虚拟地址

    • buflen - 缓冲区的长度

    • callback, callback_arg - 回调函数及其参数

      回调函数的原型为:void callback(void *arg, bus_dma_segment_t *seg, int nseg, int error)

    • arg - 与传递给bus_dmamap_load()callback_arg相同

    • seg - 段描述符数组

    • nseg - 数组中描述符的数量

    • error - 段编号溢出的指示:如果将其设置为EFBIG,则缓冲区不适合标签允许的最大段数。在这种情况下,数组中只有允许数量的描述符。此情况的处理由驱动程序决定:根据所需语义,它可以将其视为错误,也可以将缓冲区分成两部分并分别处理第二部分

      段数组中的每个条目包含以下字段

    • ds_addr - 段的物理总线地址

    • ds_len - 段的长度

  • void bus_dmamap_unload(bus_dma_tag_t dmat, bus_dmamap_t map)

    卸载映射。

    • dmat - 标签

    • map - 已加载的映射

  • void bus_dmamap_sync (bus_dma_tag_t dmat, bus_dmamap_t map, bus_dmasync_op_t op)

    在将物理传输到或从设备之前和之后,将已加载的缓冲区与其跳跃页面同步。这是执行原始缓冲区与其映射版本之间所有必要数据复制的功能。在进行传输之前和之后,都必须同步缓冲区。

    • dmat - 标签

    • map - 已加载的映射

    • op - 要执行的同步操作类型

    • BUS_DMASYNC_PREREAD - 在从设备读取到缓冲区之前

    • BUS_DMASYNC_POSTREAD - 从设备读取到缓冲区之后

    • BUS_DMASYNC_PREWRITE - 在将缓冲区写入设备之前

    • BUS_DMASYNC_POSTWRITE - 将缓冲区写入设备之后

截至目前,PREREAD 和 POSTWRITE 是空操作,但将来可能会更改,因此驱动程序中不能忽略它们。对于从bus_dmamem_alloc()获得的内存,不需要同步。

在从bus_dmamap_load()调用回调函数之前,段数组存储在堆栈中。并且它为标签允许的最大段数预分配。因此,在 i386 架构上,段数的实际限制约为 250-300(内核堆栈为 4KB 减去用户结构的大小,段数组条目的大小为 8 字节,并且必须保留一些空间)。由于数组是基于最大数量分配的,因此该值不得设置为高于实际需要的值。幸运的是,对于大多数硬件,最大支持的段数要低得多。但是,如果驱动程序想要处理具有大量分散-收集段的缓冲区,则应分批进行:加载缓冲区的一部分,将其传输到设备,加载缓冲区的下一部分,依此类推。

另一个实际结果是段数可能会限制缓冲区的大小。如果缓冲区中的所有页面碰巧在物理上不连续,则对于该碎片情况,最大支持的缓冲区大小将为 (nsegments * page_size)。例如,如果最大支持 10 个段,则在 i386 上最大保证支持的缓冲区大小将为 40K。如果需要更大的尺寸,则应在驱动程序中使用特殊的技巧。

如果硬件根本不支持分散-收集,或者驱动程序希望支持某些缓冲区大小(即使它严重碎片化),则解决方案是在驱动程序中分配一个连续的缓冲区,并在原始缓冲区不适合时将其用作中间存储。

以下是使用映射时根据映射的使用情况的典型调用序列。字符 → 用于显示时间的流逝。

对于在设备连接和断开期间始终保持实际固定的缓冲区

bus_dmamem_alloc → bus_dmamap_load → …使用缓冲区… → → bus_dmamap_unload → bus_dmamem_free

对于经常更改并从驱动程序外部传递的缓冲区

          bus_dmamap_create ->
          -> bus_dmamap_load -> bus_dmamap_sync(PRE...) -> do transfer ->
          -> bus_dmamap_sync(POST...) -> bus_dmamap_unload ->
          ...
          -> bus_dmamap_load -> bus_dmamap_sync(PRE...) -> do transfer ->
          -> bus_dmamap_sync(POST...) -> bus_dmamap_unload ->
          -> bus_dmamap_destroy

加载由bus_dmamem_alloc()创建的映射时,传递的缓冲区地址和大小必须与bus_dmamem_alloc()中使用的地址和大小相同。在这种情况下,保证整个缓冲区将被映射为一个段(因此回调可以基于此假设),并且请求将立即执行(永远不会返回 EINPROGRESS)。在这种情况下,所有回调需要做的就是保存物理地址。

一个典型的例子是

          static void
        alloc_callback(void *arg, bus_dma_segment_t *seg, int nseg, int error)
        {
          *(bus_addr_t *)arg = seg[0].ds_addr;
        }

          ...
          int error;
          struct somedata {
            ....
          };
          struct somedata *vsomedata; /* virtual address */
          bus_addr_t psomedata; /* physical bus-relative address */
          bus_dma_tag_t tag_somedata;
          bus_dmamap_t map_somedata;
          ...

          error=bus_dma_tag_create(parent_tag, alignment,
           boundary, lowaddr, highaddr, /*filter*/ NULL, /*filterarg*/ NULL,
           /*maxsize*/ sizeof(struct somedata), /*nsegments*/ 1,
           /*maxsegsz*/ sizeof(struct somedata), /*flags*/ 0,
           &tag_somedata);
          if(error)
          return error;

          error = bus_dmamem_alloc(tag_somedata, &vsomedata, /* flags*/ 0,
             &map_somedata);
          if(error)
             return error;

          bus_dmamap_load(tag_somedata, map_somedata, (void *)vsomedata,
             sizeof (struct somedata), alloc_callback,
             (void *) &psomedata, /*flags*/0);

看起来有点长且复杂,但这就是操作方式。实际结果是:如果多个内存区域始终一起分配,则将它们全部组合到一个结构中并作为一个分配(如果对齐和边界限制允许)将是一个非常好的主意。

将任意缓冲区加载到由bus_dmamap_create()创建的映射中时,必须采取特殊措施才能在回调延迟的情况下与之同步。代码将如下所示

          {
           int s;
           int error;

           s = splsoftvm();
           error = bus_dmamap_load(
               dmat,
               dmamap,
               buffer_ptr,
               buffer_len,
               callback,
               /*callback_arg*/ buffer_descriptor,
               /*flags*/0);
           if (error == EINPROGRESS) {
               /*
                * Do whatever is needed to ensure synchronization
                * with callback. Callback is guaranteed not to be started
                * until we do splx() or tsleep().
                */
              }
           splx(s);
          }

处理请求的两种可能方法是

  1. 如果通过显式标记请求为已完成来完成请求(例如 CAM 请求),则将所有进一步的处理放入回调驱动程序中会更简单,该驱动程序将在请求完成时标记请求。然后不需要太多额外的同步。出于流量控制的原因,在该请求完成之前冻结请求队列可能是一个好主意。

  2. 如果请求在函数返回时完成(例如字符设备上的经典读或写请求),则应在缓冲区描述符中设置一个同步标志并调用tsleep()。稍后,当回调被调用时,它将执行其处理并检查此同步标志。如果已设置,则回调应发出唤醒。在这种方法中,回调函数可以执行所有必要的处理(就像前一种情况一样),或者简单地将段数组保存在缓冲区描述符中。然后,在回调完成后,调用函数可以使用此保存的段数组并执行所有处理。

10.7. DMA

直接内存访问 (DMA) 通过 DMA 控制器在 ISA 总线上实现(实际上是两个,但这无关紧要)。为了使早期的 ISA 设备简单且便宜,总线控制和地址生成的逻辑集中在 DMA 控制器中。幸运的是,FreeBSD 提供了一组函数,这些函数主要将 DMA 控制器的烦人细节隐藏在设备驱动程序之外。

最简单的情况是针对相当智能的设备。像 PCI 上的总线主设备一样,它们可以自行生成总线周期和内存地址。它们真正需要的只是 DMA 控制器的总线仲裁。因此,为此目的,它们假装是级联从属 DMA 控制器。并且系统 DMA 控制器唯一需要做的事情是在驱动程序连接时调用以下函数,在 DMA 通道上启用级联模式

void isa_dmacascade(int channel_number)

所有进一步的活动都是通过对设备进行编程来完成的。在分离驱动程序时,无需调用任何与 DMA 相关的函数。

对于更简单的设备,事情变得更加复杂。使用的函数是

  • int isa_dma_acquire(int chanel_number)

    保留一个 DMA 通道。成功时返回 0,如果该通道已被此驱动程序或其他驱动程序保留,则返回 EBUSY。大多数 ISA 设备无论如何都无法共享 DMA 通道,因此通常在连接设备时调用此函数。此保留已由总线资源的现代接口变得多余,但仍必须与后者一起使用。如果不使用,则以后其他 DMA 例程将出现恐慌。

  • int isa_dma_release(int chanel_number)

    释放先前保留的 DMA 通道。在释放通道时,不得进行任何传输(此外,设备不得尝试在释放通道后启动传输)。

  • void isa_dmainit(int chan, u_int bouncebufsize)

    分配一个跳跃缓冲区以与指定的通道一起使用。缓冲区的请求大小不能超过 64KB。如果传输缓冲区碰巧在物理上不连续或在 ISA 总线可访问的内存范围之外或跨越 64KB 边界,则此跳跃缓冲区将自动使用。如果传输将始终从符合这些条件的缓冲区(例如,使用适当的限制由bus_dmamem_alloc()分配的缓冲区)进行,则不必调用isa_dmainit()。但使用 DMA 控制器传输任意数据非常方便。跳跃缓冲区将自动处理分散-收集问题。

    • chan - 通道号

    • bouncebufsize - 跳跃缓冲区的大小(以字节为单位)

  • void isa_dmastart(int flags, caddr_t addr, u_int nbytes, int chan)

    准备启动 DMA 传输。必须调用此函数以在设备上实际开始传输之前设置 DMA 控制器。它检查缓冲区是否连续且位于 ISA 内存范围内,如果不是,则自动使用跳跃缓冲区。如果需要跳跃缓冲区但未由isa_dmainit()设置或对于请求的传输大小太小,则系统将出现恐慌。在使用跳跃缓冲区的写入请求的情况下,数据将自动复制到跳跃缓冲区。

  • flags - 一个位掩码,用于确定要执行的操作类型。方向位 B_READ 和 B_WRITE 是互斥的。

    • B_READ - 从 ISA 总线读取到内存

    • B_WRITE - 将内存写入 ISA 总线

    • B_RAW - 如果设置,则 DMA 控制器将记住缓冲区,并在传输结束时自动重新初始化自身以再次重复传输同一缓冲区(当然,驱动程序可以在设备中启动另一次传输之前更改缓冲区中的数据)。如果未设置,则参数仅适用于一次传输,并且在启动下一次传输之前必须再次调用isa_dmastart()。仅当未使用跳跃缓冲区时,使用 B_RAW 才有意义。

  • addr - 缓冲区的虚拟地址

  • nbytes - 缓冲区的长度。必须小于或等于 64KB。长度为 0 不允许:DMA 控制器会将其理解为 64KB,而内核代码会将其理解为 0,这会导致不可预测的后果。对于通道号 4 及以上,长度必须为偶数,因为这些通道一次传输 2 个字节。如果长度为奇数,则最后一个字节将不会被传输。

  • chan - 通道号

  • void isa_dmadone(int flags, caddr_t addr, int nbytes, int chan)

    在设备报告传输完成之后同步内存。如果这是一个带有跳跃缓冲区的读取操作,则数据将从跳跃缓冲区复制到原始缓冲区。参数与 isa_dmastart() 的参数相同。允许使用 B_RAW 标志,但它不会以任何方式影响 isa_dmadone()

  • int isa_dmastatus(int channel_number)

    返回当前传输中剩余要传输的字节数。如果在 isa_dmastart() 中设置了 B_READ 标志,则返回的数字将永远不会等于零。在传输结束时,它将自动重置回缓冲区的长度。通常的做法是在设备发出传输完成信号后检查剩余的字节数。如果字节数不为 0,则该传输可能出现问题。

  • int isa_dmastop(int channel_number)

    中止当前传输并返回未传输的字节数。

10.8. xxx_isa_probe

此函数探测设备是否存在。如果驱动程序支持自动检测设备配置的某些部分(例如中断向量或内存地址),则必须在此例程中执行此自动检测。

与任何其他总线一样,如果无法检测到设备,或者检测到但自检失败,或者发生其他问题,则它将返回一个正值的错误。如果设备不存在,则必须返回 ENXIO 值。其他错误值可能表示其他情况。零或负值表示成功。大多数驱动程序返回零表示成功。

当 PnP 设备支持多个接口时,使用负返回值。例如,一个旧的兼容性接口和一个新的高级接口,由不同的驱动程序支持。然后这两个驱动程序都会检测到设备。在探测例程中返回较高值的驱动程序优先(换句话说,返回 0 的驱动程序优先级最高,返回 -1 的驱动程序次之,返回 -2 的驱动程序再次之)。结果,仅支持旧接口的设备将由旧驱动程序处理(该驱动程序应该从探测例程返回 -1),而也支持新接口的设备将由新驱动程序处理(该驱动程序应该从探测例程返回 0)。

系统在调用探测例程之前分配设备描述符结构 xxx_softc。如果探测例程返回错误,则系统将自动释放描述符。因此,如果发生探测错误,驱动程序必须确保在探测期间使用的所有资源都被释放,并且没有任何东西阻止描述符被安全释放。如果探测成功完成,则系统将保留描述符,并稍后将其传递给例程 xxx_isa_attach()。如果驱动程序返回负值,则不能确定它是否具有最高优先级,以及是否将调用其连接例程。因此,在这种情况下,它也必须在返回之前释放所有资源,并在必要时在连接例程中重新分配它们。当 xxx_isa_probe() 返回 0 时,在返回之前释放资源也是一个好主意,并且行为良好的驱动程序应该这样做。但在资源释放存在某些问题的情况下,驱动程序允许在从探测例程返回 0 和执行连接例程之间保留资源。

一个典型的探测例程从获取设备描述符和单元开始

         struct xxx_softc *sc = device_get_softc(dev);
          int unit = device_get_unit(dev);
          int pnperror;
          int error = 0;

          sc->dev = dev; /* link it back */
          sc->unit = unit;

然后检查 PnP 设备。检查通过一个包含此驱动程序支持的 PnP ID 列表以及与这些 ID 对应的设备模型的人类可读描述的表格进行。

        pnperror=ISA_PNP_PROBE(device_get_parent(dev), dev,
        xxx_pnp_ids); if(pnperror == ENXIO) return ENXIO;

ISA_PNP_PROBE 的逻辑如下:如果此卡(设备单元)未检测为 PnP,则将返回 ENOENT。如果将其检测为 PnP,但其检测到的 ID 与表中的任何 ID 不匹配,则返回 ENXIO。最后,如果它具有 PnP 支持并且它与表中的一个 ID 匹配,则返回 0,并且 device_set_desc() 将设置表中的相应描述。

如果驱动程序仅支持 PnP 设备,则条件将如下所示

          if(pnperror != 0)
              return pnperror;

对于不支持 PnP 的驱动程序,不需要特殊处理,因为它们传递一个空的 PnP ID 表,并且如果在 PnP 卡上调用,将始终获得 ENXIO。

探测例程通常需要至少一组最小的资源,例如 I/O 端口号,以找到卡并对其进行探测。根据硬件的不同,驱动程序可能能够自动发现其他必要的资源。PnP 设备的所有资源都由 PnP 子系统预设,因此驱动程序不需要自己发现它们。

通常,访问设备所需的最少信息是 I/O 端口号。然后,某些设备允许从设备配置寄存器获取其余信息(尽管并非所有设备都这样做)。因此,首先我们尝试获取端口起始值

 sc->port0 = bus_get_resource_start(dev,
        SYS_RES_IOPORT, 0 /*rid*/); if(sc->port0 == 0) return ENXIO;

基端口地址保存在结构 softc 中以备将来使用。如果经常使用它,则每次调用资源函数都会非常慢。如果我们没有获得端口,我们只需返回错误。一些设备驱动程序可以更聪明地尝试探测所有可能的端口,如下所示

          /* table of all possible base I/O port addresses for this device */
          static struct xxx_allports {
              u_short port; /* port address */
              short used; /* flag: if this port is already used by some unit */
          } xxx_allports = {
              { 0x300, 0 },
              { 0x320, 0 },
              { 0x340, 0 },
              { 0, 0 } /* end of table */
          };

          ...
          int port, i;
          ...

          port =  bus_get_resource_start(dev, SYS_RES_IOPORT, 0 /*rid*/);
          if(port !=0 ) {
              for(i=0; xxx_allports[i].port!=0; i++) {
                  if(xxx_allports[i].used || xxx_allports[i].port != port)
                      continue;

                  /* found it */
                  xxx_allports[i].used = 1;
                  /* do probe on a known port */
                  return xxx_really_probe(dev, port);
              }
              return ENXIO; /* port is unknown or already used */
          }

          /* we get here only if we need to guess the port */
          for(i=0; xxx_allports[i].port!=0; i++) {
              if(xxx_allports[i].used)
                  continue;

              /* mark as used - even if we find nothing at this port
               * at least we won't probe it in future
               */
               xxx_allports[i].used = 1;

              error = xxx_really_probe(dev, xxx_allports[i].port);
              if(error == 0) /* found a device at that port */
                  return 0;
          }
          /* probed all possible addresses, none worked */
          return ENXIO;

当然,通常驱动程序的 identify() 例程应该用于此类事情。但是,可能有一个充分的理由说明为什么在 probe() 中这样做会更好:如果此探测会使其他一些敏感设备发疯。探测例程的排序考虑了 sensitive 标志:敏感设备首先被探测,其余设备稍后被探测。但是 identify() 例程在任何探测之前都被调用,因此它们不尊重敏感设备,可能会扰乱它们。

现在,在获得起始端口后,我们需要设置端口计数(除了 PnP 设备),因为内核在配置文件中没有此信息。

         if(pnperror /* only for non-PnP devices */
         && bus_set_resource(dev, SYS_RES_IOPORT, 0, sc->port0,
         XXX_PORT_COUNT)<0)
             return ENXIO;

最后分配并激活一部分端口地址空间(起始和结束的特殊值表示“使用我们通过 bus_set_resource() 设置的值”)

          sc->port0_rid = 0;
          sc->port0_r = bus_alloc_resource(dev, SYS_RES_IOPORT,
          &sc->port0_rid,
              /*start*/ 0, /*end*/ ~0, /*count*/ 0, RF_ACTIVE);

          if(sc->port0_r == NULL)
              return ENXIO;

现在可以访问端口映射寄存器,我们可以以某种方式探测设备并检查它是否按预期反应。如果它没有反应,则可能在该地址处有其他设备或根本没有设备。

通常,驱动程序不会在连接例程之前设置中断处理程序。相反,它们使用 DELAY() 函数进行超时,以轮询模式进行探测。探测例程绝不能永远挂起,所有对设备的等待都必须使用超时。如果设备在规定时间内没有响应,则它可能已损坏或配置错误,驱动程序必须返回错误。在确定超时间隔时,为设备提供一些额外的时间以确保安全:尽管 DELAY() 应该在任何机器上延迟相同的时间,但它具有一定的误差范围,具体取决于 CPU。

如果探测例程确实想要检查中断是否正常工作,它也可以配置和探测中断。但不建议这样做。

          /* implemented in some very device-specific way */
          if(error = xxx_probe_ports(sc))
              goto bad; /* will deallocate the resources before returning */

函数 xxx_probe_ports() 还可以根据它发现的设备的确切型号设置设备描述。但是,如果只有一个受支持的设备型号,这也可以以硬编码的方式完成。当然,对于 PnP 设备,PnP 支持会自动从表中设置描述。

          if(pnperror)
              device_set_desc(dev, "Our device model 1234");

然后,探测例程应该通过读取设备配置寄存器发现所有资源的范围,或者确保它们由用户显式设置。我们将通过板载内存的示例来考虑它。探测例程应该尽可能地不具有侵入性,因此最好将其余资源(除了端口之外)的功能分配和检查留给连接例程。

内存地址可以在内核配置文件中指定,或者在某些设备上,它可以在非易失性配置寄存器中预配置。如果两个来源都可用且不同,应该使用哪个?可能如果用户费心在内核配置文件中显式设置地址,他们知道自己在做什么,并且应该优先使用它。实现示例可以是

          /* try to find out the config address first */
          sc->mem0_p = bus_get_resource_start(dev, SYS_RES_MEMORY, 0 /*rid*/);
          if(sc->mem0_p == 0) { /* nope, not specified by user */
              sc->mem0_p = xxx_read_mem0_from_device_config(sc);

          if(sc->mem0_p == 0)
                  /* can't get it from device config registers either */
                  goto bad;
          } else {
              if(xxx_set_mem0_address_on_device(sc) < 0)
                  goto bad; /* device does not support that address */
          }

          /* just like the port, set the memory size,
           * for some devices the memory size would not be constant
           * but should be read from the device configuration registers instead
           * to accommodate different models of devices. Another option would
           * be to let the user set the memory size as "msize" configuration
           * resource which will be automatically handled by the ISA bus.
           */
           if(pnperror) { /* only for non-PnP devices */
              sc->mem0_size = bus_get_resource_count(dev, SYS_RES_MEMORY, 0 /*rid*/);
              if(sc->mem0_size == 0) /* not specified by user */
                  sc->mem0_size = xxx_read_mem0_size_from_device_config(sc);

              if(sc->mem0_size == 0) {
                  /* suppose this is a very old model of device without
                   * auto-configuration features and the user gave no preference,
                   * so assume the minimalistic case
                   * (of course, the real value will vary with the driver)
                   */
                  sc->mem0_size = 8*1024;
              }

              if(xxx_set_mem0_size_on_device(sc) < 0)
                  goto bad; /* device does not support that size */

              if(bus_set_resource(dev, SYS_RES_MEMORY, /*rid*/0,
                      sc->mem0_p, sc->mem0_size)<0)
                  goto bad;
          } else {
              sc->mem0_size = bus_get_resource_count(dev, SYS_RES_MEMORY, 0 /*rid*/);
          }

IRQ 和 DRQ 的资源可以通过类比轻松检查。

如果一切顺利,则释放所有资源并返回成功。

          xxx_free_resources(sc);
          return 0;

最后,处理麻烦的情况。在返回之前,应释放所有资源。我们利用这样一个事实:在结构 softc 传递给我们之前,它会被清零,因此我们可以找出是否分配了一些资源:然后其描述符不为零。

          bad:

          xxx_free_resources(sc);
          if(error)
                return error;
          else /* exact error is unknown */
              return ENXIO;

这就是探测例程的全部内容。资源的释放是从多个地方完成的,因此它被移动到一个可能如下所示的函数中

static void
           xxx_free_resources(sc)
              struct xxx_softc *sc;
          {
              /* check every resource and free if not zero */

              /* interrupt handler */
              if(sc->intr_r) {
                  bus_teardown_intr(sc->dev, sc->intr_r, sc->intr_cookie);
                  bus_release_resource(sc->dev, SYS_RES_IRQ, sc->intr_rid,
                      sc->intr_r);
                  sc->intr_r = 0;
              }

              /* all kinds of memory maps we could have allocated */
              if(sc->data_p) {
                  bus_dmamap_unload(sc->data_tag, sc->data_map);
                  sc->data_p = 0;
              }
               if(sc->data) { /* sc->data_map may be legitimately equal to 0 */
                  /* the map will also be freed */
                  bus_dmamem_free(sc->data_tag, sc->data, sc->data_map);
                  sc->data = 0;
              }
              if(sc->data_tag) {
                  bus_dma_tag_destroy(sc->data_tag);
                  sc->data_tag = 0;
              }

              ... free other maps and tags if we have them ...

              if(sc->parent_tag) {
                  bus_dma_tag_destroy(sc->parent_tag);
                  sc->parent_tag = 0;
              }

              /* release all the bus resources */
              if(sc->mem0_r) {
                  bus_release_resource(sc->dev, SYS_RES_MEMORY, sc->mem0_rid,
                      sc->mem0_r);
                  sc->mem0_r = 0;
              }
              ...
              if(sc->port0_r) {
                  bus_release_resource(sc->dev, SYS_RES_IOPORT, sc->port0_rid,
                      sc->port0_r);
                  sc->port0_r = 0;
              }
          }

10.9. xxx_isa_attach

如果探测例程返回成功并且系统已选择连接该驱动程序,则连接例程实际上将驱动程序连接到系统。如果探测例程返回 0,则连接例程可以预期接收完整的设备结构 softc,因为它是由探测例程设置的。此外,如果探测例程返回 0,则可以预期将来某个时刻将调用此设备的连接例程。如果探测例程返回负值,则驱动程序可能不会做出任何这些假设。

如果连接例程成功完成,则返回 0,否则返回错误代码。

连接例程的开始与探测例程类似,将一些常用数据放入更易访问的变量中。

          struct xxx_softc *sc = device_get_softc(dev);
          int unit = device_get_unit(dev);
          int error = 0;

然后分配并激活所有必要的资源。由于通常在从探测返回之前会释放端口范围,因此必须重新分配它。我们期望探测例程已正确设置所有资源范围,并将其保存在结构 softc 中。如果探测例程已分配某些资源,则无需再次分配(这将被视为错误)。

          sc->port0_rid = 0;
          sc->port0_r = bus_alloc_resource(dev, SYS_RES_IOPORT,  &sc->port0_rid,
              /*start*/ 0, /*end*/ ~0, /*count*/ 0, RF_ACTIVE);

          if(sc->port0_r == NULL)
               return ENXIO;

          /* on-board memory */
          sc->mem0_rid = 0;
          sc->mem0_r = bus_alloc_resource(dev, SYS_RES_MEMORY,  &sc->mem0_rid,
              /*start*/ 0, /*end*/ ~0, /*count*/ 0, RF_ACTIVE);

          if(sc->mem0_r == NULL)
                goto bad;

          /* get its virtual address */
          sc->mem0_v = rman_get_virtual(sc->mem0_r);

DMA 请求通道 (DRQ) 以类似的方式分配。要初始化它,请使用 isa_dma*() 系列的函数。例如

isa_dmacascade(sc→drq0);

中断请求线 (IRQ) 有一些特殊之处。除了分配之外,驱动程序的中断处理程序还应与其关联。从历史上看,在旧的 ISA 驱动程序中,系统传递给中断处理程序的参数是设备单元号。但在现代驱动程序中,约定建议传递指向结构 softc 的指针。重要的原因是,当结构 softc 被动态分配时,从 softc 获取单元号很容易,而从单元号获取 softc 则很困难。此外,此约定使不同总线的驱动程序看起来更统一,并允许它们共享代码:每个总线都有自己的探测、连接、分离和其他特定于总线的例程,而驱动程序的大部分代码可以在它们之间共享。

          sc->intr_rid = 0;
          sc->intr_r = bus_alloc_resource(dev, SYS_RES_MEMORY,  &sc->intr_rid,
                /*start*/ 0, /*end*/ ~0, /*count*/ 0, RF_ACTIVE);

          if(sc->intr_r == NULL)
              goto bad;

          /*
           * XXX_INTR_TYPE is supposed to be defined depending on the type of
           * the driver, for example as INTR_TYPE_CAM for a CAM driver
           */
          error = bus_setup_intr(dev, sc->intr_r, XXX_INTR_TYPE,
              (driver_intr_t *) xxx_intr, (void *) sc, &sc->intr_cookie);
          if(error)
              goto bad;

如果设备需要对主内存进行 DMA,则应像之前描述的那样分配此内存

          error=bus_dma_tag_create(NULL, /*alignment*/ 4,
              /*boundary*/ 0, /*lowaddr*/ BUS_SPACE_MAXADDR_24BIT,
              /*highaddr*/ BUS_SPACE_MAXADDR, /*filter*/ NULL, /*filterarg*/ NULL,
              /*maxsize*/ BUS_SPACE_MAXSIZE_24BIT,
              /*nsegments*/ BUS_SPACE_UNRESTRICTED,
              /*maxsegsz*/ BUS_SPACE_MAXSIZE_24BIT, /*flags*/ 0,
              &sc->parent_tag);
          if(error)
              goto bad;

          /* many things get inherited from the parent tag
           * sc->data is supposed to point to the structure with the shared data,
           * for example for a ring buffer it could be:
           * struct {
           *   u_short rd_pos;
           *   u_short wr_pos;
           *   char    bf[XXX_RING_BUFFER_SIZE]
           * } *data;
           */
          error=bus_dma_tag_create(sc->parent_tag, 1,
              0, BUS_SPACE_MAXADDR, 0, /*filter*/ NULL, /*filterarg*/ NULL,
              /*maxsize*/ sizeof(* sc->data), /*nsegments*/ 1,
              /*maxsegsz*/ sizeof(* sc->data), /*flags*/ 0,
              &sc->data_tag);
          if(error)
              goto bad;

          error = bus_dmamem_alloc(sc->data_tag, &sc->data, /* flags*/ 0,
              &sc->data_map);
          if(error)
               goto bad;

          /* xxx_alloc_callback() just saves the physical address at
           * the pointer passed as its argument, in this case &sc->data_p.
           * See details in the section on bus memory mapping.
           * It can be implemented like:
           *
           * static void
           * xxx_alloc_callback(void *arg, bus_dma_segment_t *seg,
           *     int nseg, int error)
           * {
           *    *(bus_addr_t *)arg = seg[0].ds_addr;
           * }
           */
          bus_dmamap_load(sc->data_tag, sc->data_map, (void *)sc->data,
              sizeof (* sc->data), xxx_alloc_callback, (void *) &sc->data_p,
              /*flags*/0);

在分配所有必要的资源后,应初始化设备。初始化可能包括测试所有预期功能是否正常。

          if(xxx_initialize(sc) < 0)
               goto bad;

总线子系统将自动在控制台上打印由探测设置的设备描述。但是,如果驱动程序想要打印有关设备的一些额外信息,它可以这样做,例如

        device_printf(dev, "has on-card FIFO buffer of %d bytes\n", sc->fifosize);

如果初始化例程遇到任何问题,建议在返回错误之前打印有关这些问题的消息。

附加例程的最后一步是将设备附加到内核中的其功能子系统。具体操作方式取决于驱动程序的类型:字符设备、块设备、网络设备、CAM SCSI 总线设备等等。

如果一切顺利,则返回成功。

          error = xxx_attach_subsystem(sc);
          if(error)
              goto bad;

          return 0;

最后,处理麻烦的情况。在返回错误之前,应释放所有资源。我们利用这样一个事实:在结构 softc 传递给我们之前,它会被清零,因此我们可以找出是否分配了一些资源:然后它的描述符非零。

          bad:

          xxx_free_resources(sc);
          if(error)
              return error;
          else /* exact error is unknown */
              return ENXIO;

关于附加例程就这些了。

10.10. xxx_isa_detach

如果驱动程序中存在此函数,并且驱动程序被编译为可加载模块,则驱动程序将获得卸载的能力。如果硬件支持热插拔,这是一个重要的功能。但是 ISA 总线不支持热插拔,因此此功能对于 ISA 设备来说并不特别重要。卸载驱动程序的能力在调试驱动程序时可能很有用,但在许多情况下,只有在旧版本以某种方式卡住系统并且需要重新引导后,才需要安装新版本的驱动程序,因此花费在编写分离例程上的精力可能不值得。另一个论点是,卸载将允许在生产机器上升级驱动程序,这似乎大多是理论上的。安装新版本的驱动程序是一项危险的操作,绝不应在生产机器上执行(并且在系统以安全模式运行时不允许执行)。尽管如此,为了完整起见,仍可能提供分离例程。

如果驱动程序成功分离,则分离例程返回 0,否则返回错误代码。

分离的逻辑是附加的镜像。首先要做的是从其内核子系统分离驱动程序。如果设备当前处于打开状态,则驱动程序有两种选择:拒绝分离或强制关闭并继续分离。使用的选择取决于特定内核子系统执行强制关闭的能力以及驱动程序作者的偏好。通常,强制关闭似乎是首选方案。

          struct xxx_softc *sc = device_get_softc(dev);
          int error;

          error = xxx_detach_subsystem(sc);
          if(error)
              return error;

接下来,驱动程序可能希望将硬件重置为某个一致状态。这包括停止任何正在进行的传输,禁用 DMA 通道和中断,以避免设备损坏内存。对于大多数驱动程序来说,这正是关闭例程所做的,因此如果它包含在驱动程序中,我们可以直接调用它。

xxx_isa_shutdown(dev);

最后释放所有资源并返回成功。

          xxx_free_resources(sc);
          return 0;

10.11. xxx_isa_shutdown

当系统即将关闭时,将调用此例程。预计它会将硬件带到某个一致状态。对于大多数 ISA 设备,不需要任何特殊操作,因此该函数实际上并不必要,因为设备将在重新引导时重新初始化。但是,某些设备必须使用特殊程序关闭,以确保它们在软重启后能够正确检测到(对于许多具有专有识别协议的设备尤其如此)。无论如何,在设备寄存器中禁用 DMA 和中断以及停止任何正在进行的传输是一个好主意。具体操作取决于硬件,因此我们在此处不做详细介绍。

10.12. xxx_intr

当收到可能来自此特定设备的中断时,将调用中断处理程序。ISA 总线不支持中断共享(某些特殊情况除外),因此实际上,如果调用了中断处理程序,则中断几乎可以肯定来自其设备。尽管如此,中断处理程序必须轮询设备寄存器并确保中断是由其设备生成的。如果不是,它应该只返回。

ISA 驱动程序的旧约定是将设备单元号作为参数获取。这已过时,新的驱动程序接收在附加例程中调用 bus_setup_intr() 时为其指定的任何参数。根据新约定,它应该是指向结构 softc 的指针。因此,中断处理程序通常以以下方式开始

          static void
          xxx_intr(struct xxx_softc *sc)
          {

它在由 bus_setup_intr() 的中断类型参数指定的 interrupt 优先级级别运行。这意味着所有相同类型以及所有软件 interrupt 都被禁用。

为了避免竞争条件,它通常被写成循环

          while(xxx_interrupt_pending(sc)) {
              xxx_process_interrupt(sc);
              xxx_acknowledge_interrupt(sc);
          }

中断处理程序必须向设备确认中断,但不要向中断控制器确认,系统负责后者。


上次修改时间:2024 年 3 月 9 日,作者:Danilo G. Baio