中国DOS联盟论坛

中国DOS联盟

-- 联合DOS 推动DOS 发展DOS --
联盟域名:www.cn-dos.net 论坛域名:www.cn-dos.net/forum
游客 | 登录 | 注册 | 会员 | 搜索 | 中国DOS联盟
中国DOS联盟论坛
现在时间是 2026-10-31 11:38
共 47,815 主题排行 / 349,930 发帖 / 今日 0 篇 / 48,290 会员排行
DOS开发编程 & 发展交流 (开发室) » [原创]DPMI下的硬件中断
可打印版本  1,659 / 2
第1楼 whowin 发表于 2008-05-09 11:53
初级用户 发帖 37 积分 174
[原创]DPMI下的硬件中断
文章是从我的网志中贴过来的,其中的图片可能过不来,看完整内容,请访问我的网志:
点击进入《DOS编程技术》

实模式下的硬件中断有很多书籍中都有介绍,保护模式下的硬件中断要复杂一些,但也有一些资料。本网志中的大部分文章将介绍在DOS环境下使用DJGPP完成保护模式下的程序,编译出来的程序需要在DPMI的支持下运行在保护模式下,通常情况下,并没有什么,当你需要调用DOS下的实模式下的功能调用时,DPMI会帮助你切换到实模式,完成后再返回保护模式,基本上不需要编程者去做任何处理,但在处理硬件中断方面就比较微妙了,我们究竟应该在实模式下完成硬件中断还是在保护模式下,如果原有的硬件中断工作在实模式下,而我却编了一个保护模式下的硬件中断程序,按规则,在我的中断例程完成后,我必须再执行原有的硬件中断,显然这里有些问题。

本文旨在探讨DPMI环境下的硬件中断机制,以及在此环境下硬件中断的编写方法,本文并不探讨实模式下和保护模式下硬件中断的原理,但文中难免涉及一些这方面的知识,请读者自行阅读相关资料解决。在本文的最后将举一个硬件中断的编写实例。

本文主要讨论的环境为:DOS6.22 + DJGPP + CWSDPMI。CWSDPMI为DJGPP自带的DPMI服务,也是一个好用的且免费的DPMI,有关这个环境的搭建,请参阅我以前的博文。

1、DPMI下的硬件中断机理

首先,不论是在实模式下还是在保护模式下,硬件中断都会产生,当程序运行在DPMI下时,尽管你的程序是32位的,运行在保护模式下,但当你调用实模式下DOS的功能调用时,DPMI会切换到实模式下,可能一个硬件中断恰恰在这个时候产生了,但不管什么时候发生了硬件中断,只要你的程序运行在DPMI下,DPMI将截获硬件中断,并将首先传送给保护模式下的硬件中断程序执行,然后再转到实模式下的中断例程;但并不是所有的DPMI都将在两种模式下执行中断例程,比如我们本文讨论的环境,CWSDPMI下,DPMI截获中断并交给保护模式下的中断例程,只有在保护模式的中断例程不存在的情况下,DPMI才会去执行实模式下的中断例程,在CWSDPMI下,DPMI只执行一种模式下的中断例程;但WINDOWS 95下的DPMI就不一样,windows宣称会执行把两种模式下的中断例程都执行一遍(抱歉,我并没有验证这一说法);所以,一定要了解你所处的环境,在下面的叙述中,如果不特别说明,均只在CWSDPMI下,综上所述,在DPMI下,你可以只编写一个保护模式下的中断例程,而无须管实模式下的中断例程。

但是,我们毕竟是在DOS下编程,很多资源来自于实模式的DOS,所以如果你的中断程序中需要大量地调用DOS的资源时,你必须要考虑编写实模式的中断程序,否则在你的中断程序中DPMI将不断地在实模式和保护模式之间切换,这将十分麻烦。

2、DPMI下编写硬件中断的方法

保护模式下的中断程序,按下面方法设置。

用汇编语言编写的中断程序,可以按照下面的步骤进行设置:

取得原有的中断程序结构
调用__dpmi_get_protected_mode_interrupt_vector函数,并把返回值存在结构中,以便程序退出时可以恢复原来的中断程序
锁定内存
调用__dpmi_lock_linear_region函数锁定在中断程序中可能用到的数据变量,代码,调用的函数;锁定失败有可能造成你的程序崩溃;
设置新的中断程序
调用__dpmi_set_protected_mode_interrupt_vector,把新中断程序的选择子(selector)和偏移(offset)填入__dpmi_paddr中,传给函数,其中,pm_selector可以通过函数__dpmi_cs()获得
如果中断程序用C语言写成,可以按照下面方法设置:

取得原有的中断程序结构
调用_go32_dpmi_get_protected_mode_interrupt_vector,将返回值存入适当的结构中,以便程序退出时恢复原有的中断程序
对中断程序进行再包装
调用_go32_dpmi_allocate_iret_wrapper函数,按照要求传递参数,该函数将按照中断程序的规范建立一个小的汇编函数对你的C语言写的中断程序进行包装,该函数填充的结构_go32_dpmi_seginfo将作为下面函数调用的参数。
设置新的中断程序
调用_go32_dpmi_set_protected_mode_interrupt_vector函数,把上一步骤返回的_go32_dpmi_seginfo作为参数传给该函数。
链接原有中断
如果需要在执行完你的中断程序后执行原有的中断程序,不必执行前面两个步骤,直接调用_go32_dpmi_chain_protected_mode_interrupt_vector即可,就是说不用调用_go32_dpmi_allocate_iret_wrapper和_go32_dpmi_set_protected_mode_interrupt_vector这两个函数,_go32_dpmi_chain_protected_mode_interrupt_vector将完成所有的工作,在填结构_go32_dpmi_seginfo时,把中断程序的偏移放到pm_offset中,把_my_cs()的返回值放到pm_selector中即可;用这种方式设置的中断你只能通过DJGPP的退出代码进行释放,别无他法。
用C语言写中断程序的主要问题是你根本没有办法完全锁定你的中断程序所用到的存储器,所以,这种方法只适用于那些较小的程序,你可以确认你的程序不会被page到磁盘上去;这一点要特别引起注意,一般情况下,中断程序尽量使用汇编语言完成比较好。

3、DPMI下一个时钟中断的实例

老实说,没有什么很好的例子可以给大家演示中断程序,本来想结合前面博文中的AC97的内容编写一个PCI的中断例子,但复杂一些,涉及的问题也比较多,好在比较幸运的是我找到了一个int 8h的例子,个人感觉写的比较简单,好懂,对这段代码做了大量的修改,原程序接管了int 8h,我修改后的程序接管了int 8h和int 70h两个中断,我觉得这样更能说明问题。


int 8h(IRQ 0)是时钟中断,来自可编程的时钟芯片8254,现在的机器中已经没有了这颗芯片,给集成到了别的芯片中,不过保留了原来的I/O端口及8254的控制方法,所以仍然可以使用控制8254的方法控制时钟;要说明这个例子,我们不得不先简单介绍一下8254这颗芯片。

8254定时器这颗芯片有三个定时通道,每个通道有一个I/O地址,timer0--timer2对应的I/O地址为40h--42h,这个I/O通道是8位的,通过这个8位的I/O通道可以对8254进行编程,8254内部有一个16位的锁存寄存器,编程时送入的技术值首先进入这个锁存寄存器,然后被复制到计数寄存器,当接到8254上的时钟信号每来一个脉冲时,这个计数寄存器中的数值将减一,当减到0时,8254将输出一个脉冲,同时再次将锁存寄存器中的内容复制到计数寄存器中,开始下一轮的计数。

所以,8254芯片的每个通道有两个输入信号:一个是时钟信号,一个是门控信号,用于控制该通道工作或者不工作(后面我们会提到);一个输出信号,即技术完毕后的输出信号。

一般情况下,timer0用于产生每秒18.2次的系统时钟中断(int 8h),这就是我们下面例子中要编的中断;timer1用于存储器刷新(现在应该不用了?);timer2连接扬声器,可以产生方波使扬声器发声。这里说到的知识可能有点老,尤其是timer1的用法,不过不妨碍我们的例子。

8254还有一个控制寄存器,I/O地址为43h,8位通道,该寄存器的定义如下:



还是简单说明一下:

bit 准备对那个定时器通道进行操作,timer0 or timer1 or timer2
bit 读/写当前计数值
00:读当前计数值,先读低8位,后读高8位,要两次in指令
01:只写高8位,一次out指令
10:只写低8位,一次out指令
11:先写低8位,后写高8位,要两次out指令
bit 工作模式,有6种工作模式,其中模式0(000)为计数结束后产生中断;
模式3(003)为方波频率发声器,这两种模式后面要用
bit 0 写入值得格式,0--二进制格式,1--BCD码格式

前面说到,8254的输入信号里还有一个门控信号,对于timer0和timer1,这个信号永远是选通的,但timer2的门控信号是可控的,由I/O端口61h的bit 0和bit 1控制,该I/O端口的bit0为1则关断timer2的门控信号,为0开启门控信号,bit1和可以控制timer2和扬声器的通断,我们不需要使用这个信号。

pc机种还有一个实时时钟中断,int 70h(IRQ 8),由以前的MC146818这颗芯片产生,现在这颗芯片也已经集成到其他芯片中了,不过操作和控制方法还是相同,MC146818可以产生多种类型的中断,在初始状态都是关闭的,我们仅用它的周期中断,缺省为每秒1024次,我们并没有更改任何参数,仅仅是在中断中增加了一个中断计数,我们希望以此中断来检验我们接管的int 8h的正确性。


好了,基本知识介绍完了。下面是程序清单:



为了说明方便,源程序中加了行号,所有的注释语句没有行号。

先大概说一下这个程序完成的事情,PC机系统的时钟中断是18.2次/秒,本程序重新编写了系统时钟中断int 8h,试图做到1ms中断一次,链接到原中断上,这样int 8h(IRQ 0)的中断频率将从18.2Hz变成1000Hz,这势必影响到PC机的时钟,因为很多资源是依赖这个时钟中断的,比如时间;同时我们接管了实时时钟中断int 70h(IRQ 8),并启动了它的周期中断,中断频率应该是1024Hz,在这个中断中我们仅仅作了一个中断计数;主程序中在启动中断后做了10次的循环,每次循环前记录int 70h的中断次数计数值,然后等待int 8h中断1000次(应该刚好1秒钟),然后再记录int 70h的中断次数计数值,理论上说,int 8h中断1000次,int 70h应该刚好中断1024次,如果数值吻合,说明int 8h完全按照我们的预期在运行。

常量、变量说明:

ticks_8h:int 8h的中断次数计数
ticks_70h:int 70h的中断次数计数
PIT0:8254 timer0的端口地址,我们要修改timer0的设置
PITMODE:8254的控制寄存器
IMR1:第二颗8259中断控制器的中断屏蔽寄存器地址,因为IRQ 8连在第2颗8259上

RTC_ADDR:实时时钟芯片(MC146818)地址寄存器地址
RTC_DATA:实时时钟芯片(MC146818)数据寄存器地址
主程序从136行开始,145行调用pctimer_init初始化两个硬件中断,传递的参数1000表明把int 8h的中断频率设成1000Hz;146行--151行是测试部分的主循环,前面有过介绍,pctimer_sleep(1000)可以等待int 8h中断1000次,最后把int 70h在此期间的中断次数记录在数组elapsedtime中,共后面显示结果用;循环完毕后,调用pctimer_exit恢复以前的中断,然后显示测试记录。
比较复杂的部分是函数pctimer_init,从32行--67行。

35行关中断
36、37行锁定了两个中断程序的代码
38--40行锁定了中断程序中要用到的数据
41--44行设置了int 8h(IRQ 0)
45--49行设置了int 70h(IRQ 8),注意这两个中断的设置方法是有所不同的,我们把int 8h与原来的中断链接到了一起,而int 70h没有链接原来的中断,读者可以通过这段代码体会其中的不同
50、51行设置了实时时钟芯片的状态寄存器B,目的是开启周期中断,读者可以参考与实时时钟芯片相关的资料
52行重新设置了第二颗8259中断控制器,目的是清除对IRQ 8的屏蔽,这里要注意的是,在我做测试的机器上,我读取了第一颗8259的中断屏蔽寄存器,知道其用于级联第二颗8259的IRQ 2并没有被屏蔽,所以我并没有设置第一颗8259的中断屏蔽寄存器,如果你的机器与我的不同,请注意设置第一颗8259的中断屏蔽寄存器,打开IRQ 2,另外,我知道我的机器第二颗8259的中断屏蔽寄存器的缺省设置值是0dh,刚好IRQ 8被屏蔽了,所以我直接送了一个0ch到第二颗8259的中断屏蔽寄存器,以打开IRQ 8,在你的机器上可能也有些不同,请按实际情况设置,只要打开IRQ 8即可,不要改变其他的屏蔽位,以免麻烦,由于此处并非本文的重点,所以在代码上没有作更多处理
54--58行初始化8254的timer0,请参考前面关于8254芯片的介绍部分,另外在14和15行之间的注释也有一个对8254的简单介绍
60--64行设置了一些变量,明白变量的含义就好了,源程序中有注释
65行打开中断,此时两个新的中断开始运行了
93--95行是int 8h的中断程序,没有什么好说的,只是不断递增ticks_8h这个变量,注意int 8h这个中断程序是要链接到原有中断上的,所以最后不需要向8259发出中断结束的指令
99--107行是int 70h的中断程序,需要做一些说明,首先102、103行读取了实时时钟芯片的状态寄存器C,这是必须要做的,否则芯片认为你没有响应前一个中断,不会发出下一个中断;另外,这个中断和int 8h不同,没有链接到原有中断,所以你必须向8259发出中断完成的指令,又因为IRQ 8连接在第二颗8259上,而第二颗8259通过第一颗8259级联,所以必须向两颗8259均发出中断完成的指令,这就是104、105行的作用
117--135行这段程序对本文不重要,这是因为我们改变了系统时钟中断(int 8h)的中断频率,这势必导致本机时间混乱,所以在退出时,读取以下实时时钟芯片中的实时时间,然后重新设置一下时间
68--92行这段程序基本上是pctimer_init这个函数的逆动作,也不需要过多说明
至此,程序解释完了,希望能给你一些帮助。

更多关于DOS编程的文章看我的网志

点击进入《DOS编程技术》
第2楼 fhqiugeg 发表于 2008-09-11 09:07
新手上路 发帖 8 积分 18
好!终于找到了!
第3楼 fhqiugeg 发表于 2008-09-11 09:10
新手上路 发帖 8 积分 18
前几个月里把freedos很早版本的源代码看了编,觉得很没有收获,可能是自己太浮躁了。看到你的文章才知道什么是技术追求。
[ 联系联盟系统管理团队 - 中国DOS联盟 - 标准版 ]
Sponsored by ifanr Inc | © 2001–2023