跳转到内容

Aros/开发者/文档/设备

来自 Wikibooks,开放世界中的开放书籍

来自 http://wandel.ca/homepage/execdis/devices_doc.txt

AMIGA 设备驱动程序指南


版权所有 (c) 1990 Markus Wandel

版本 0.12,1990 年 5 月 21 日。

分发:以未修改的形式免费提供。

免责声明:我不知道我在说什么;不保证本文档中任何内容的正确性。并非所有这些都符合康懋达认可的编程和文档实践。Amiga OS 的 1.4/2.0 版本可能会使部分内容过时,尤其是我提到 exec 函数的内部结构的地方。

更正:如果您发现错误并希望更正,请联系我,以便我拥有最新的主副本。目前您可以通过 (613) 591-7698 联系到我。如果此号码失效,请致电我的父母 (705) 785-3383 或 (705) 736-2285(夏季)获取当前号码。

内容目录


0. 简介 1. 设备结构

   1.1. DEVICE NODES
   1.2. CONSTRUCTING A DEVICE NODE
   1.3. STANDARD DEVICE FORMAT
   1.4. DEVICES IN ROM
   1.5. DEVICES ON DISK
   1.6. JUMP VECTOR CHECKSUMS

2. 设备 I/O 协议

   2.1. THE I/O REQUEST STRUCTURE
   2.2. OPENING AND CLOSING A DEVICE
   2.3. EXPUNGING A DEVICE
   2.4. UNIT STRUCTURES
   2.5. THE BEGINIO FUNCTION
   2.6. THE ABORTIO FUNCTION
   2.7. EXEC I/O FUNCTIONS
   2.8. CALLING BEGINIO DIRECTLY
   2.8. SYNCHRONOUS I/O
   2.9. ASYNCHRONOUS I/O
       2.9.1. WAITING FOR A SPECIFIC I/O REQUEST
       2.9.2. WAITING ON A SPECIFIC REPLY PORT
       2.9.3. GENERAL CASE

3. 通用命令和错误编号

   3.1. COMMANDS
       3.1.1. CMD_RESET
       3.1.2. CMD_READ
       3.1.3. CMD_WRITE
       3.1.4. CMD_UPDATE
       3.1.5. CMD_CLEAR
       3.1.6. CMD_STOP
       3.1.7. CMD_START
       3.1.8. CMD_FLUSH
   3.2. ERROR NUMBERS

4. 磁盘设备驱动程序

   4.1. COMMANDS
       4.1.1. CMD_READ AND CMD_WRITE
       4.1.2. TD_MOTOR
       4.1.3. TD_SEEK
       4.1.4. TD_FORMAT
       4.1.5. TD_PROTSTATUS
       4.1.6. TD_RAWREAD AND TD_RAWWRITE
       4.1.7. TD_GETDRIVETYPE
       4.1.8. TD_GETNUMTRACKS
       4.1.9. TD_CHANGESTATE
       4.1.10. OTHER COMMANDS
   4.2. ERROR NUMBERS
   4.3. SCSIDIRECT PROTOCOL
   4.4. A MINIMAL DISK COMMAND SUBSET

5. 参考 6. 修订历史

0. 简介


很多人要求我向他们解释 Amiga 设备驱动程序。有很多内容需要解释,因此我决定写下我所知道的所有关于该主题的信息。这是结果。

这不是一个独立的文档。它假设您熟悉 Amiga 在多任务、消息传递级别的编程。它假设您拥有来自康懋达的 autodocs、包含文件和示例设备驱动程序。这些文档中的许多信息都被复制了,但并非全部。

我希望我对这个主题的了解比现在更多,并且我是一个更好的作家。唉,我没有,我也不是。本文档包含编写磁盘驻留、自动加载和可清除设备驱动程序所需的所有内容,但您可能需要阅读多次。要编写一个好的可移动介质磁盘驱动程序,您需要进行一些额外的研究。

1. 设备结构


本节描述与加载的设备相关联的数据结构,以及如何将设备引入系统。就本节讨论的内容而言,库和设备是相同的。

1.1. 设备节点

系统通过其设备节点了解设备。设备节点由三个部分组成

   (a) a jump table to the device functions
   (b) a library structure
   (c) any private data that the device has

“设备地址”是库节点的基地址;因此,跳转表位于地址的负偏移量处,其他所有内容都位于正偏移量处。

跳转表中的每个条目都是对 32 位地址的“jmp”。因此,第一个跳转位于设备地址的偏移量 -6 处,第二个位于偏移量 -12 处,依此类推。以下四个对于所有设备和库都是标准的

   -6: Open
  -12: Close
  -18: Expunge
  -24: ExtFunc

这些分别是打开、关闭和清除(卸载)设备的入口点。最后一个似乎是为将来的扩展。

设备驱动程序还有两个标准函数

  -30: BeginIO
  -36: AbortIO

这些分别是提交 I/O 请求和取消挂起请求的入口点。

库结构如下所示,“展开”形式。

   struct Library {
       struct  Node {
           struct  Node *ln_Succ;
           struct  Node *ln_Pred;
           UBYTE   ln_Type;
               /*  NT_DEVICE = 3  */
           BYTE    ln_Pri;
           char    *ln_Name;
       } lib_Node;
       UBYTE   lib_Flags;
               /*  LIBF_SUMMING = 1
                   LIBF_CHANGED = 2
                   LIBF_SUMUSED = 4
                   LIBF_DELEXP  = 8  */
       UBYTE   lib_pad;
       UWORD   lib_NegSize;
       UWORD   lib_PosSize;
       UWORD   lib_Version;
       UWORD   lib_Revision;
       APTR    lib_IdString;
       ULONG   lib_Sum;
       UWORD   lib_OpenCnt;
   };

设备通过顶部的节点结构链接到系统设备列表。设备列表可以在 ExecBase->DeviceList 中找到。值得注意的是“ln_Type”字段,它必须是 NT_DEVICE,以及“ln_Name”字段,它必须是标准形式的设备名称,例如“serial.device”。

“lib_PosSize”和“lib_NegSize”字段分别指示设备基地址上方和下方使用的字节数。“lib_NegSize”是跳转向量的大小,“lib_PosSize”是库结构加上用户私有数据(如果有)的大小。

“lib_Version”、 “lib_Revision”和“lib_IdString”存储有关设备的更多信息。对于“serial.device”,版本 34,修订版 12,字符串将如下所示

   "serial 34.12 (27 Mar 1989)"

这似乎是公认的标准格式。

“lib_Sum”字段以及标志位“LIBF_SUMMING”、“LIBF_CHANGED”和“LIBF_SUMUSED”用于跳转向量校验和机制,将在后面讨论。

“lib_OpenCnt”字段计算设备当前打开的次数。如果此值不为零并且请求清除,则设备可以使用“LIBF_DELEXP”标志记住它应该在最早的机会消失。

1.2. 构造设备节点

理论上,您可以只为设备节点分配所有所需的内存,手动初始化跳转向量和库节点中的必需字段,获取 ExecBase->DeviceList,执行 Forbid(),并使用 Enqueue()、AddHead() 或 AddTail() 将节点添加到列表中。但是有一种更简单的方法。它是以下函数

   AddDevice(device)
             A1

这将获取一个已初始化的设备节点,执行 Forbid()/Permit(),并将节点添加到 ExecBase->DeviceList。它还会调用 Sumkick()(稍后讨论)。节点本身可以使用此函数构造

   library = MakeLibrary(vectors, structure, init, dataSize, segList)
   D0                    A0       A1         A2    D0        D1

第一个参数指向跳转向量的函数地址表。它可以具有以下格式之一

   (a) Relative
       vectors: dc.w   -1
                dc.w   func1-vectors
                dc.w   func2-vectors
                ...
                dc.w   -1
   (b) Absolute
       vectors: dc.l   func1
                dc.l   func2
                ...
                dc.l   -1

“dataSize”参数确定设备节点的“正大小”。它必须至少是库结构的大小。从提供的跳转向量条目数可以隐式推导出“负大小”。

“MakeLibrary”函数将分配适当数量的内存,填充跳转向量,然后使用 InitStruct() 清除和初始化正偏移量区域。“structure”参数指向 InitStruct() 函数的数据表。此函数是一个复杂的东西,最好与提供的宏一起使用。以下是康懋达示例设备驱动程序中的表

   dataTable:
      INITBYTE   LN_TYPE,NT_DEVICE
      INITLONG   LN_NAME,myName
      INITBYTE   LIB_FLAGS,LIBF_SUMUSED!LIBF_CHANGED
      INITWORD   LIB_VERSION,VERSION
      INITWORD   LIB_REVISION,REVISION
      INITLONG   LIB_IDSTRING,idString
      DC.L   0

这意味着以下内容

      - store a byte NT_DEVICE at offset LN_TYPE
      - store the device name as a longword (pointer) at LN_NAME
      - store LIBF_SUMUSED!LIBF_CHANGED in the "lib_Flags" field
      - store the version, revision, and IdString in the appropriate fields.

如果您想了解有关 Initstruct() 函数的更多信息(它非常灵活,并且可以执行超出上述建议的功能),您应该阅读其 autodoc,并可能还需要阅读反汇编代码。

最后,我们有“init”和“segList”参数。“init”是设备自身初始化代码的地址。在节点构造完成后调用此代码。“segList”描述设备代码的加载位置。初始化代码在 D0 中使用设备节点地址和 A0 中的 SegList 参数进行调用。它返回的值是 MakeLibrary 返回的值。因此,初始化代码通常会像接收到的那样返回设备节点地址。

唯一需要说明的是,“structure”和“init”字段可以设置为零,以分别不初始化正偏移量区域和不调用任何初始化代码。

因此,为了总结我们到目前为止的内容,将设备引入系统的第一性原理方法如下所示

   - LoadSeg() the code into memory, if necessary
   - MakeLibrary() to build the library node
   - AddDevice() with the result to add the node to the device list.

1.3. 标准设备格式

真正的设备采用标准格式,这允许它们在无需了解其内部结构的情况下加载和初始化。标准格式基于“驻留结构”或“RomTag”。此类结构标记系统 ROM 以及磁盘上 LIBS: 和 DEVS: 目录中的各种库和设备。结构如下所示

   struct Resident {
       UWORD rt_MatchWord;
               /*  RTC_MATCHWORD = 0x4AFC  */
       struct Resident *rt_MatchTag;
       APTR  rt_EndSkip;
       UBYTE rt_Flags;
               /*  RTF_COLDSTART = 1
                   RTF_AUTOINIT  = 128 */
       UBYTE rt_Version;
       UBYTE rt_Type;
               /*  NT_DEVICE = 3 */
       BYTE  rt_Pri;
       char  *rt_Name;
       char  *rt_IdString;
       APTR  rt_Init;
   };

前两个字段是特殊的,允许在 ROM 扫描期间找到结构。它们是官方的 68000“非法指令”,操作码 0x4AFC,后跟指向该指令的指针。下一个字段通过指向扫描应继续的地址(通常在由此 RomTag 标记的区域的末尾)来加快 ROM 扫描速度。

“rt_Version”字段具有熟悉的版本号,“rt_Type”字段对于我们的应用程序是 NT_DEVICE。“rt_Pri”字段与“rt_Version”字段一起确定在出现这种情况时将接受多个具有相同名称的模块中的哪一个。“rt_Name”和“rt_IdString”字段标识模块;它们应设置为设备名称和 IdString,如前面针对设备节点所述。

由 RomTag 标记的事物称为“驻留模块”,并按以下方式初始化

   InitResident(resident, segList)
                A1        D1

这里,“resident”指向 RomTag,“segList”标识与其关联的代码(可选)。

InitResident() 函数首先检查 RomTag 中是否设置了 RTF_AUTOINIT 标志。如果没有,它只需调用“rt_Init”指向的例程,A0 包含作为“segList”传递的值,然后返回。

如果设置了 RTF_AUTOINIT 标志,则该函数将为您完成所有设备初始化工作。在这种情况下,“rt_Init”指向一个四长字数据表,其中包含“MakeLibrary”函数的这些参数

   - dataSize
   - vectors
   - structure
   - init

InitResident 使用这些值以及强制性的“segList”参数调用 MakeLibrary 函数。除非 MakeLibrary() 返回空值,否则新库节点现在将添加到相应的系统列表中,这基于节点中的“ln_Type”字段。如果它是 NT_DEVICE,则节点将添加到设备列表中,这正是我们想要的。

您现在应该拥有足够的信息来创建标准设备标头和初始化函数,并了解康懋达示例设备驱动程序中的一个。

1.4. ROM 中的设备

ROM 中的设备在引导时对整个 ROM 进行扫描以查找 RomTag 结构时会被发现。找到的 RomTag 将添加到 ExecBase 中的驻留模块列表中。所有设置了 RTF_COLDSTART 标志的模块都将使用 InitResident() 自动启动。

模块可以使用我不会在此处描述的机制进行“ramkick”。这使它们能够在重置后继续存在,在 RAM 中找到,并在冷启动时与 ROM 模块一起初始化。这就是 RAD: 可引导 RAM 磁盘的工作原理。

1.5. 磁盘上的设备

磁盘上的设备驻留在 DEVS: 目录中。它们是标准的目标模块,并使用 LoadSeg() 加载。加载后,将扫描 seglist 的第一个块以查找 RomTag,并使用 InitResident() 初始化设备。在构建磁盘驻留设备时,请注意链接器不要在代码前面添加一个虚拟块(旧版本的 BLINK 会这样做),因为这会导致找不到您的 RomTag。

1.6. 跳转向量校验和

库结构中的“lib_Sum”字段可用于保存跳转向量的校验和,以防止意外或未经授权的修改。“LIBF_SUMUSED”标志位指示应执行此操作。“LIBF_CHANGED”标志位指示跳转向量已修改且校验和无效。以下函数实现了该机制

   SumLibrary(library)
              A1

这样做以下操作

   IF the LIBF_SUMUSED flag is set THEN
       Forbid()
       IF the LIBF_CHANGED flag is set THEN
           Compute checksum and store in lib_Sum
           Clear the LIBF_CHANGED flag
       ELSE
           IF the lib_Sum field is zero THEN
               Compute checksum and store in lib_Sum
           ELSE
               Compute checksum
               IF checksum does not match lib_Sum THEN
                   Put up recoverable alert #81000003
                   Store new checksum in lib_Sum
               ENDIF
           ENDIF
       ENDIF
       Permit()
   ENDIF

因此,在创建新的设备节点时,最好设置 LIBF_SUMUSED 和 LIBF_CHANGED 位,以便第一次调用 SumLibrary() 更新校验和,并且后续调用检测跳转向量修改。

系统设置 LIBF_CHANGED 位并调用 SumLibrary() 作为 SetFunction() 调用的部分,以保持校验和有效。

2. 设备 I/O 协议


本节描述设备加载并初始化后与之交互的接口。

2.1. I/O 请求结构

系统通过使用“I/O 请求”结构与设备驱动程序进行通信。下面显示了该结构最常见的版本,但“io_Error”字段之后的全部字段都是设备驱动程序特定的,可以省略或重新定义。

   struct IOStdReq {
       struct  Message {
           struct  Node {
               struct  Node *ln_Succ;
               struct  Node *ln_Pred;
               UBYTE   ln_Type;
                       /*  NT_MESSAGE  = 5
                           NT_REPLYMSG = 7 */
               BYTE    ln_Pri;
               char    *ln_Name;
           } mn_Node;
           struct  MsgPort *mn_ReplyPort;
           UWORD   mn_Length;
       } io_Message;
       struct  Device  *io_Device;
       struct  Unit    *io_Unit;
       UWORD   io_Command;
       UBYTE   io_Flags;
               /*  IOF_QUICK = 1 */
       BYTE    io_Error;
       ULONG   io_Actual;
       ULONG   io_Length;
       APTR    io_Data;
       ULONG   io_Offset;
   };

I/O 请求中的第一个字段是标准消息结构,允许将其排队到消息端口并进行回复。“io_Device”字段指向设备结构,“io_Unit”字段指向设备驱动程序为每个功能单元(例如磁盘驱动器)维护的“单元”结构。因此,这两个字段标识了 I/O 请求的目标。

2.2. 打开和关闭设备

要与设备通信,至少需要一个 I/O 请求,以及一个设备可以返回已完成处理的 I/O 请求的消息端口。可以使用以下两个 C 库函数轻松创建这些内容

   MyPort = CreatePort( name, pri )
   struct MsgPort *MyPort;
   char *name;
   BYTE pri;
   ioStdReq = CreateStdIO( ioReplyPort )
   struct IOStdReq *ioStdReq;
   struct MsgPort *ioReplyPort;

第一个函数创建一个消息端口和关联的信号位,并返回该端口的地址。第二个函数创建一个 IOStdReq 结构,填写回复端口的地址,并返回该结构的地址。然后可以使用此函数打开设备驱动程序

   error = OpenDevice(devName, unitNumber, iORequest, flags)
   D0                 A0       D0          A1         D1

参数是设备的名称(例如“trackdisk.device”),单元号(例如内部驱动器的 0),已完成 I/O 请求结构的地址,以及包含设备驱动程序特殊信息的“flags”字。以下 C 代码将打开内部驱动器的 trackdisk 驱动程序,并再次关闭它

   /*
    *  Do not supply a name for the message port, since supplying a name
    *  will put it on the public message port list.
    */
   td_mp = CreatePort(0L,0L);
   if(!td_mp) exit(99);
   td_iob = CreateStdIO(td_mp);
   if(!td_iob) {
       DeletePort(td_mp);
       exit(99);
   }
   if(OpenDevice("trackdisk.device",0L,td_iob,0L)) {
       printf("Unable to open floppy device driver.\n");
       DeleteStdIO(td_iob);
       DeletePort(td_mp);
       exit(99);
   }
   /*
    *  I/O request is ready for operations here.
    */
   CloseDevice(td_iob);
   DeleteStdIO(td_iob);
   DeletePort(td_mp);

OpenDevice() 和 CloseDevice() 是 exec 函数,但会被称为“ramlib.library”的东西拦截,它处理磁盘驻留设备和库的加载和卸载。因此,OpenDevice() 调用可能会导致设备加载并初始化,而 CloseDevice() 可能会导致设备卸载。

OpenDevice() 最终导致初始化的设备驱动程序通过其 Open() 函数(跳转向量中的偏移量 -6)被调用。这发生在调用程序的上下文中(任务)。设备驱动程序会传递以下信息

   D0:  Unit number
   D1:  Flags
   A1:  I/O request pointer
   A6:  Device node pointer

exec 将已清除 I/O 请求中的“io_Error”字段,并将设备节点指针存储在“io_Device”字段中。

如果 Open() 函数成功,则必须初始化“io_Unit”字段以供稍后使用 I/O 请求。如果失败,则必须将“io_Error”字段设置为相应的数字。退出时“io_Error”字段的值由 OpenDevice() 函数返回。

设备驱动程序应使用其设备节点中的“lib_OpenCnt”字段跟踪未完成打开的数量。某些设备驱动程序可以支持任意数量的并发打开(例如磁盘驱动程序),而其他驱动程序则可以以“独占访问”模式打开(例如串行端口)。

单元号和标志是两个 32 位字,其格式由设备驱动程序编写者决定。有关单元编号方案的示例,请参阅包含文件“devices/scsidisk.h”,有关标志位的某些示例用法,请参阅“devices/serial.h”。

CloseDevice() 调用最终导致设备驱动程序通过其 Close() 函数被调用,该函数位于跳转向量中的偏移量 -12 处。I/O 请求指针在 A1 中传递,设备节点指针在 A6 中传递。返回值决定关闭的设备会发生什么。如果为零,则保留设备。如果为非零,则表示设备已执行延迟删除并希望卸载。在这种情况下,返回值是初始化时传递给设备的“segList”参数。下一节将详细介绍。

2.3. 删除设备

可以使用此函数从系统中删除设备

   error = RemDevice(device)
   D0                A1

如果系统内存不足并尝试回收空间,则系统本身可能会发出此函数。无论哪种方式,设备都将通过其 Expunge() 入口点(跳转向量中的偏移量 -18)被调用。寄存器 A6 被设置为指向设备节点。

设备现在应关闭其活动,即删除中断服务器、释放缓冲区等。然后,它应将其设备节点从设备列表中解除链接,并释放该节点,从而将事物恢复到 InitResident() 启动它之前的状态。最后,它应返回初始化时传递给它的“segList”参数。如果设备来自磁盘,系统将使用此参数卸载其代码。

如果在 Expunge() 调用到达时设备未处于空闲状态,则它可以延迟操作。为此,它在库结构中设置 LIBF_DELEXP 标志,并返回零。这表示它将在最早的机会删除自身。当最后一个 Close() 调用到达时,它将按上述方式关闭自身,并返回 segList 值以指示它已完成并且应卸载。

2.4. 单元结构

许多设备驱动程序管理多个功能单元。例如,trackdisk 驱动程序最多可以处理四个软盘驱动器。首选方法是为每个功能单元(例如驱动器)使用单独的“单元”结构。通常,单元结构至少包含以下内容

   struct Unit {
       struct  MsgPort unit_MsgPort;
       UBYTE   unit_flags;
               /*  UNITF_ACTIVE = 1
                   UNITF_INTASK = 2 */
       UBYTE   unit_pad;
       UWORD   unit_OpenCnt;
   };

当设备驱动程序打开时,它使用单元号选择相应的单元结构,并将指向该结构的指针存储在 I/O 请求中。稍后,它可以在消息端口上将挂起的 I/O 请求排队,以便由单元任务进行处理。

2.5. BeginIO 函数

所有 I/O 请求都通过其跳转向量中的 BeginIO() 函数进入设备驱动程序。设备驱动程序在请求任务的上下文中进入,A6 指向设备节点,A1 指向 I/O 请求结构。

通常,设备驱动程序现在将使用 PutMsg() 将 I/O 请求排队到消息端口(在单元结构中),以便由内部任务进行处理。然后它可以从 BeginIO() 函数返回。当 exec 检查 I/O 请求是否已完成时,它会检查其类型字段,如果它是 NT_MESSAGE(来自 PutMsg() 调用的结果),则知道它仍在进行中。最终,内部任务接收 I/O 请求,对其进行操作,并执行 ReplyMsg()。这将 I/O 请求返回到调用者的回复端口,并将它的类型设置为 NT_REPLYMSG,表示它已完成。

很明显,设备驱动程序不必完全遵循此过程。简短的命令(例如检查磁盘是否就绪)可以直接在调用者的上下文中执行。I/O 请求必须简单地在结束时使用 ReplyMsg() 返回,并且如果 BeginIO() 函数返回时 I/O 请求尚未完成,则它的类型字段必须是除 NT_REPLYMSG 之外的其他内容。

IOF_QUICK 标志表示 I/O 处理的一种特殊情况。当它被设置时,表示请求者使用了 DoIO() 函数,因此在 I/O 完成之前不会执行任何操作。在这种情况下,设备驱动程序可以在调用者的上下文中运行整个 I/O 操作并立即返回。消除了消息传递和任务切换开销。当 BeginIO() 函数返回时 IOF_QUICK 位仍被设置,表示 I/O 操作已完成。

如果设备驱动程序看到 IOF_QUICK 标志被设置但无法内联执行 I/O 处理,则它可以简单地清除该标志并照常使用 ReplyMsg() 返回 I/O 请求。

BeginIO() 函数对 I/O 请求中的命令和参数进行操作,并设置“io_Error”字段以指示结果。exec I/O 函数将此字段的值返回给调用者;BeginIO() 本身不返回值。“io_Error”设置为零表示没有发生错误。

2.6. AbortIO 函数

某些设备驱动程序操作(例如等待超时或串行端口上的输入)可能需要在完成之前中止。为此提供了 AbortIO() 函数。设备驱动程序通过其 AbortIO() 入口点进入,其中要中止的 I/O 请求的地址在 A1 中,设备节点指针在 A6 中。如果设备驱动程序确定 I/O 请求确实正在进行并且可以成功中止它,则返回零,否则返回非零错误代码。

成功中止的 I/O 请求通过正常方法返回,即 ReplyMsg()。“io_Error”字段应指示它没有正常完成。

2.7. Exec I/O 函数

以下原语用于与设备驱动程序通信。假设驱动程序已使用 OpenDevice() 打开,并且存在已初始化的 I/O 请求。

   SendIO(iORequest)
          A1

此函数调用设备驱动程序中的 BeginIO() 入口点,IOF_QUICK 清除。这意味着设备驱动程序应使用 ReplyMsg() 返回 I/O 请求。

   error = DoIO(iORequest)
   D0           A1

此函数调用设备驱动程序中的 BeginIO() 入口点,IOF_QUICK 设置。如果设备驱动程序保留 IOF_QUICK 设置,则立即返回给调用者。返回值是 I/O 请求中“io_Error”字段的扩展值。如果 IOF_QUICK 位被清除,则将贯穿到 WaitIO()。

   error = WaitIO(iORequest)
   D0             A1

此函数等待 I/O 请求完成。如果 I/O 请求设置了 IOF_QUICK 标志,则它不可能正在进行中,因此它会立即返回。否则,I/O 请求将使用 ReplyMsg() 返回,并且该函数将继续执行以下操作

   Get the signal number from the I/O request's reply port
   Disable()
   WHILE iORequest->io_Message.mn_Node.ln_Type != NT_REPLYMSG DO
       Wait() for the reply port signal
   ENDWHILE
   Unlink the I/O request from the reply port's message queue
   Enable()

最后,它返回 I/O 请求中的“io_Error”,如往常一样扩展为长字。

   result = CheckIO(iORequest)
   D0               A1

此函数检查指示的 I/O 请求是否已完成。如果它的 IOF_QUICK 位被设置,或者它的类型为 NT_REPLYMSG,则认为它已完成。在这种情况下,该函数返回 I/O 请求的地址。如果 I/O 请求未完成,则返回零。此函数不会将 I/O 请求从回复端口出队。

   error = AbortIO(iORequest)
   D0              A1

此函数调用设备驱动程序中的 AbortIO() 入口点,如前所述。

2.8. 直接调用 BeginIO

DoIO() 和 SendIO() 无法处理一项操作,那就是发送带有 IOF_QUICK 标志的 I/O 请求,但不等待其完成。这相当于“尽可能快地运行此操作,但如果需要一段时间,则不要等待”。对于此操作,用户必须手动设置 IOF_QUICK,然后通过其 BeginIO() 入口点直接调用设备驱动程序。以下 C 库函数将执行后者

   void BeginIO( ioRequest )
   struct IOStdReq *ioRequest;

由于 WaitIO() 和 CheckIO() 了解 IOF_QUICK 标志,因此以这种方式提交的 I/O 请求可以像往常一样由 WaitIO() 和 CheckIO() 处理。

2.8. 同步 I/O

当调用方不希望继续执行,直到 I/O 操作完成时,就会执行同步 I/O。在这种情况下,调用方只需设置 I/O 请求并对其执行 DoIO()。当 DoIO() 返回时,I/O 就完成了。

2.9. 异步 I/O

当调用方希望提交 I/O 请求并在其完成时执行其他操作时,就会执行异步 I/O。此类 I/O 由 SendIO() 或 BeginIO() 提交,如前所述。有多种方法可以等待异步 I/O 请求完成;下面将讨论其中几种。

2.9.1. 等待特定 I/O 请求

等待单个特定 I/O 请求可以使用 WaitIO() 函数完成。在该特定 I/O 请求完成之前,该函数不会返回。

2.9.2. 等待特定回复端口

如果有多个 I/O 请求未完成,并且所有请求都将到达相同的回复端口,则可以使用以下方法

   WHILE I/O requests are outstanding DO
       WaitPort() on the port
       GetMsg() on the port
   ENDWHILE

2.9.3. 一般情况

通常,程序将等待多个事件之一发生,而这些事件唯一的共同点是它们将设置信号。然后,程序必须从所有 I/O 回复端口获取信号位,将其与要等待的其他信号位合并,并对结果执行 Wait()。当 Wait() 返回并设置了对应于 I/O 回复端口的信号位时,就可以尝试对该端口执行 GetMsg() 以查看是否有 I/O 操作已完成。通过这种方式,可以以任何顺序将 I/O 完成与其他事件一起处理。

3. 通用命令和错误编号


本节列出了 Amiga 系统中为所有类型的设备驱动程序预定义的命令和错误编号。

3.1. 命令

命令编号存储在 I/O 请求的“io_Command”字段中。通用命令编号(在包含文件“exec/io.h”中找到)如下所示

   #define CMD_INVALID 0
   #define CMD_RESET   1
   #define CMD_READ    2
   #define CMD_WRITE   3
   #define CMD_UPDATE  4
   #define CMD_CLEAR   5
   #define CMD_STOP    6
   #define CMD_START   7
   #define CMD_FLUSH   8
   #define CMD_NONSTD  9

可以看出,命令编号零无效,大于 9 的命令编号是自定义定义的。其余命令如下所示

3.1.1. CMD_RESET

这会将设备重置到已知的初始状态。在此命令执行时未处理的未决 I/O 请求应返回错误。

3.1.2. CMD_READ

此命令请求从设备上的位置“io_Offset”读取“io_Length”个数据项,并存储在调用方内存中的“io_Data”中。实际传输的数据量将在“io_Actual”中返回。具体细节取决于设备类型。

3.1.3. CMD_WRITE

此命令请求将数据从调用方的内存传输到 I/O 设备。参数与 CMD_READ 相同。

3.1.4. CMD_UPDATE

此命令请求将所有已缓冲但未写入的数据强制输出到 I/O 设备。例如,它可能会写入磁盘设备中的磁道缓冲区。

3.1.5. CMD_CLEAR

此命令请求使设备为给定设备缓冲的所有数据无效。因此,例如,它将丢弃在串行输入缓冲区中等待的数据。

3.1.6. CMD_STOP

此命令请求设备停止处理命令。在收到 CMD_STOP 时未处理的 I/O 请求将等待收到 CMD_START 或 CMD_RESET 或被中止。

3.1.7. CMD_START

此命令请求设备清除 CMD_STOP 条件并恢复处理命令。无论收到多少个 CMD_STOP,只需要一个 CMD_START。

3.1.8. CMD_FLUSH

此命令请求设备刷新所有挂起的命令。所有已排队但尚未处理的 I/O 请求都应返回错误。

3.2. 错误编号

包含文件“exec/errors.h”列出了以下标准错误编号。

   #define IOERR_OPENFAIL  -1  /* device/unit failed to open */
   #define IOERR_ABORTED   -2  /* request aborted */
   #define IOERR_NOCMD     -3  /* command not supported */
   #define IOERR_BADLENGTH -4  /* not a valid length */

4. 磁盘设备驱动程序


真正的设备驱动程序通常支持比第 3 节中列出的更多的命令。本节描述了磁盘设备驱动程序使用的命令集。此信息需要编写与现有文件系统和磁盘修复程序兼容的磁盘驱动程序。

所有“正常”磁盘命令都使用前面给出的结构的 I/O 请求。“扩展”trackdisk 命令(使用更大的 I/O 请求)不在此处讨论。

4.1. 命令

包含文件“devices/trackdisk.h”列出了以下命令编号

   #define TD_MOTOR        (CMD_NONSTD+0)      /*  9 */
   #define TD_SEEK         (CMD_NONSTD+1)      /* 10 */
   #define TD_FORMAT       (CMD_NONSTD+2)      /* 11 */
   #define TD_REMOVE       (CMD_NONSTD+3)      /* 12 */
   #define TD_CHANGENUM    (CMD_NONSTD+4)      /* 13 */
   #define TD_CHANGESTATE  (CMD_NONSTD+5)      /* 14 */
   #define TD_PROTSTATUS   (CMD_NONSTD+6)      /* 15 */
   #define TD_RAWREAD      (CMD_NONSTD+7)      /* 16 */
   #define TD_RAWWRITE     (CMD_NONSTD+8)      /* 17 */
   #define TD_GETDRIVETYPE (CMD_NONSTD+9)      /* 18 */
   #define TD_GETNUMTRACKS (CMD_NONSTD+10)     /* 19 */
   #define TD_ADDCHANGEINT (CMD_NONSTD+11)     /* 20 */
   #define TD_REMCHANGEINT (CMD_NONSTD+12)     /* 21 */

其中一些命令特定于可移动介质和/或 trackdisk.device,硬磁盘驱动程序不需要支持这些命令。稍后将详细介绍。以下部分描述各个命令。

4.1.1. CMD_READ 和 CMD_WRITE

这些是通用命令,但细节特定于设备类型。对于磁盘设备,“io_Length”和“io_Offset”字段必须是设备支持的扇区大小的精确倍数。目前,这是 512 字节。“io_Actual”字段可能会返回与“io_Length”不同的值,如果错误在操作中途停止。

4.1.2. TD_MOTOR

此命令打开和关闭软盘驱动器电机。“io_Length”应设置为零以将其关闭,或设置为 1 以将其打开。“io_Actual”将返回电机先前的状态。电机将在需要时自动打开,但永远不会再次关闭;因此,用户必须发出 TD_MOTOR 命令将其关闭。

4.1.3. TD_SEEK

此命令将读/写磁头移动到“io_Offset”中指示的位置,“io_Offset”必须是扇区大小的精确倍数。其他命令中隐含了寻道,但此命令可用于在读取/写入操作之前预先寻道设备,如果预先知道所需位置。

4.1.4. TD_FORMAT

此命令采用与 CMD_WRITE 相同的参数并执行相同的操作,但以下两个例外情况除外

   (a) The area to be written must be a formattable unit, i.e. in the
       trackdisk.device it must be one or more complete tracks.
   (b) The area is written with the data regardless of its previous
       contents; it need not already be formatted.

这描述了 trackdisk.device 的观察到的行为。“TD_FORMAT”的自动文档与此信息不符。

硬磁盘驱动程序通常将 TD_FORMAT 实现为对 CMD_WRITE 的简单调用。这允许使用 AmigaDOS“format”命令对硬磁盘进行“高级格式化”。

一些古老的 SASI/SCSI 磁盘控制器板具有“格式化磁道”命令,因此专门针对它们的驱动程序可以像 trackdisk.device 一样实现 TD_FORMAT。

4.1.5. TD_PROTSTATUS

如果驱动器中有磁盘,则此命令返回“io_Actual”设置为零(如果磁盘可写),非零(如果受保护)。如果没有磁盘,则 I/O 请求将返回“io_Error”设置为“TDERR_DiskChanged”。

4.1.6. TD_RAWREAD 和 TD_RAWWRITE

这些命令在 trackdisk.device 上读取和写入原始 MFM 数据的整个磁道。有关详细信息,请参阅自动文档。

4.1.7. TD_GETDRIVETYPE

此命令特定于 trackdisk.device,并在“io_Actual”中返回磁盘驱动器的类型。这以下列之一表示:

   1:  3.5", 80 track drive
   2:  5.25", 40 track drive

4.1.8. TD_GETNUMTRACKS

此命令特定于 trackdisk.device,并在“io_Actual”中返回磁盘设备上的磁道数。

4.1.9. TD_CHANGESTATE

此命令检查可移动介质驱动器中是否有磁盘。如果存在,则返回“io_Actual”设置为零,如果不存在,则返回非零。

4.1.10. 其他命令

其余命令未作描述,因为目前我对它们了解不够。读者应参考相应的自动文档。在某些情况下,需要使用 trackdisk.device 进行实验才能获得所有详细信息。

4.2. 错误编号

包含文件“devices/trackdis.h”列出了以下错误编号。

   #define TDERR_NotSpecified   20 /* general catchall */
   #define TDERR_NoSecHdr       21 /* couldn't even find a sector */
   #define TDERR_BadSecPreamble 22 /* sector looked wrong */
   #define TDERR_BadSecID       23 /* ditto */
   #define TDERR_BadHdrSum      24 /* header had incorrect checksum */
   #define TDERR_BadSecSum      25 /* data had incorrect checksum */
   #define TDERR_TooFewSecs     26 /* couldn't find enough sectors */
   #define TDERR_BadSecHdr      27 /* another "sector looked wrong" */
   #define TDERR_WriteProt      28 /* can't write to a protected disk */
   #define TDERR_DiskChanged    29 /* no disk in the drive */
   #define TDERR_SeekError      30 /* couldn't find track 0 */
   #define TDERR_NoMem          31 /* ran out of memory */
   #define TDERR_BadUnitNum     32 /* asked for a unit > NUMUNITS */
   #define TDERR_BadDriveType   33 /* not a drive that trackdisk groks */
   #define TDERR_DriveInUse     34 /* someone else allocated the drive */
   #define TDERR_PostReset      35 /* user hit reset; awaiting doom */

4.3. SCSIDIRECT 协议

大多数 SCSI 设备驱动程序都支持发出通用 SCSI 命令的附加命令。这在包含文件“devices/scsidisk.h”中进行了详细描述。

4.4. 最小磁盘命令子集

很明显,要实现支持可移动介质的完整 Amiga 磁盘驱动程序是一项艰巨的任务,我甚至不假装知道所有需要的东西。但是,如果您只需要一个简单的硬磁盘驱动程序,则只需要很少一部分命令。首先,可以将 Expunge() 设为存根,始终返回零,表示延迟清除,该清除永远不会完成。可以将 AbortIO() 设为存根,始终返回非零结果,表示失败。可以取消设备结构;您可以在 I/O 请求的“io_Unit”字段中存储任何内容。在我的设备驱动程序中,我只是在那里存储了 SCSI 设备号和一些标志。您可以仅对任何出错的情况返回 I/O 错误 #20(通用万能错误),除了可能未实现的命令。无需执行快速 I/O;只需始终清除 IOF_QUICK 并将其忽略。将所有 I/O 请求发送到单个内部任务以进行处理。最后,可以如下实现命令

   CMD_READ, CMD_WRITE:        implement fully
   TD_FORMAT:                  same as CMD_WRITE
   TD_GETDRIVETYPE:            return 3.5" drive
   CMD_RESET, CMD_UPDATE,
   CMD_CLEAR, CMD_STOP,
   CMD_START, CMD_FLUSH,
   TD_MOTOR, TD_SEEK,
   TD_REMOVE, TD_CHANGENUM,
   TD_CHANGESTATE,
   TD_PROTSTATUS,
   TD_ADDCHANGEINT,
   TD_REMCHANGEINT:            clear "io_Actual" and return
   Others:                     reject with IOERR_NOCMD

生成的驱动程序与快速和慢速文件系统以及我尝试过的所有磁盘编辑/修复实用程序完美配合。如果您要从头开始启动硬磁盘,则始终可以使一个临时驱动程序工作,然后在硬磁盘运行时编写一个“正确的”驱动程序。

5. 参考


- 1.3 包含文件

- 1.2 自动文档

- 来自 DevCon 88 磁盘的 Commodore 示例磁盘设备驱动程序。

 Be sure you get the one which launches a task, not the older ones which
 launch a process.  That doesn't work in an autoboot context.

- 由我自己完成的 Exec 1.2 反汇编。从 Fred Fish 磁盘 188 获取。

6. 修订历史


0.10 (90/05/20) 初始版本 0.11 (90/05/21) 校对,小幅更新,格式更美观 0.12 (90/05/21) 修正了一些错别字

华夏公益教科书