Aros/开发者/Docs/Devices1.3
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 编程。它假设您拥有来自康懋达的自动文档、包含文件和示例设备驱动程序。这些文档中有很多信息是重复的,但并非全部。
我希望我对这个主题比现在了解得更多,我希望我是一个更好的作家。唉,我没有,我也不是。本文档包含编写磁盘驻留、自动加载和可删除设备驱动程序所需的一切,但您可能需要阅读不止一次。要编写一个好的、可移动介质磁盘驱动程序,您将不得不做一些额外的研究。
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() 函数的更多信息(它非常灵活,可以执行比上面建议的更多操作),您应该阅读它的自动文档,可能还需要阅读反汇编代码。
最后,我们有 "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,后面跟着指向该指令的指针。下一个字段通过指向扫描应该继续的地址来加快 ROM 扫描速度,通常在由此 RomTag 标记的区域的末尾。
"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 标志。如果没有,它只是用 A0 作为“segList”传递的值调用 "rt_Init" 指向的例程,然后返回。
如果设置了 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() 更新校验和,而以后的调用会检测对跳转向量的修改。
系统在 SetFunction() 调用中设置 LIBF_CHANGED 位并调用 SumLibrary(),以保持校验和有效。
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 请求结构的地址,以及包含设备驱动程序特殊信息的“标志”字。以下 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 请求
使用 WaitIO() 函数等待单个特定 I/O 请求。该函数将在该特定 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 请求。不讨论使用更大 I/O 请求的 "扩展" trackdisk 命令。
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" 应设置为零以关闭,或设置为一以打开。"io_Actual" 将返回电机的先前状态。电机将在需要时自动打开,但永远不会再次关闭;因此,用户必须发出 TD_MOTOR 命令将其关闭。
4.1.3. TD_SEEK
此命令将读写磁头移动到 "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
由此产生的驱动程序可以与快速和慢速文件系统以及我尝试过的所有磁盘编辑/修复实用程序完美配合。如果你从头开始设置硬磁盘,你可以始终让一个 hack 驱动程序运行,然后在硬磁盘运行的情况下编写一个 "正确的" 驱动程序。
5. 参考资料
- 1.3 包含文件
- 1.2 自动文档
- Commodore 的示例磁盘设备驱动程序,来自 DevCon 88 磁盘。
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) 修正了一些拼写错误