简介:这份资源面向微机原理与接口技术的学习者,围绕8253定时器、8259A中断控制器与8255可编程并行接口三块芯片,给出一个完整的中断实验方案。实验以8253输出周期性信号作为中断请求,经8259A转交CPU处理,在屏幕上显示提示字符串并逐次点亮发光二极管,每秒触发一次中断,八灯全亮后退出返回DOS,可帮助读者理解中断向量表设置、中断屏蔽与恢复、芯片初始化等关键环节。压缩包内为1个docx文档,约14KB,集中呈现实验说明与完整汇编源码,包含数据段、堆栈段、代码段及中断服务程序、8253与8255初始化模块,便于对照阅读与上机调试。该资源已有1264人学习下载,适合课程实验、接口芯片练习及中断机制复习时参考,读者可从中获得可运行的代码框架与配置思路,并据此排查中断响应与端口操作中的常见问题。
1. 中断实验:为什么一条 IRET 能让整个系统“活”过来
很多人第一次跑通 8253、8259A、8255 这套组合时,都会有一种玄学般的错觉:代码明明只是往几个端口写了几条OUT,屏幕上却开始周期性冒字符串,发光二极管一个接一个亮起来。更反直觉的是,中断服务程序里既没有循环,也没有延时,它凭什么“记住”自己已经执行了八次?答案藏在COUNT这个变量和 8259A 的 EOI 时序里。搞懂这套机制,你才算真正理解“中断”不是函数调用,而是一次由硬件发起、由软件收尾的协作。这份实验资源把 8253 的定时输出、8259A 的请求仲裁、8255 的端口输出串成一条完整链路,正好用来拆解“定时器怎么变成中断源、中断怎么被 CPU 接住、外设怎么被驱动”这三个底层问题。
2. 中断链路:8253 的方波怎么变成 CPU 的一条指令
2.1 从计数到请求:8253 通道 0 的角色
8253 在这套系统里不是“定时器”这么笼统,它干的事非常具体:通道 0 被配置成方式 3(方波发生器),初值 1000,输出引脚上持续翻转电平。这个翻转信号物理上接到 8259A 的某一路 IR 输入上,于是“时间到了”这件事被翻译成一个电平跳变。方式 3 的好处是占空比接近 50%,对中断请求来说,边沿触发和电平触发都能稳定识别,不会因为脉宽太窄被漏掉。
初始化代码里MOV AL,00110111B写的是通道 0 的控制字:选择通道 0、先读写低字节再读写高字节、方式 3、二进制计数。紧接着MOV AL,01110101B是通道 1 的控制字,方式 2(速率发生器),初值同样 1000。这里有个容易看漏的细节:通道 1 的初值写到了IOPORT+281H,而通道 0 的初值写到IOPORT+280H,两个通道各占一个端口,控制字寄存器共用+283H。初值 1000 分两次写,先低字节00H再高字节10H,合起来就是 0x1000 即 4096?不对,这里写的是00H和10H,实际装入值是 0x1000,但 8253 是递减计数,装入 0x1000 意味着计 4096 个时钟。如果输入时钟是 1MHz,分频后大约 4ms 一个周期;实验描述里说“每秒一次中断”,那说明前级时钟或者级联分频另有安排,通道 1 很可能就是用来做这个慢速分频的。读这段代码时不要孤立看通道 0,两个通道的初值一样但方式不同,通道 1 的 out 很可能又回接到通道 0 的 CLK,形成两级分频,这是实验箱上常见的接法。
I_8253 PROC NEAR MOV DX, IOPORT+283H ; 控制字寄存器 MOV AL, 00110111B ; 通道0:先低后高,方式3,二进制 OUT DX, AL MOV AL, 01110101B ; 通道1:先低后高,方式2,二进制 OUT DX, AL MOV DX, IOPORT+280H ; 通道0数据端口 MOV AL, 00H OUT DX, AL ; 初值低字节 MOV AL, 10H OUT DX, AL ; 初值高字节 MOV DX, IOPORT+281H ; 通道1数据端口 MOV AL, 00H OUT DX, AL MOV AL, 10H OUT DX, AL RET I_8253 ENDP这段代码里OUT DX, AL的目标端口由DX决定,AL是数据。控制字 00110111B 逐位拆开:bit7-6 选通道 0,bit5-4 读/写低高字节,bit3-1 方式 3,bit0 二进制。方式 2 的通道 1 控制字 01110101B 对应 bit3-1 为 010,即速率发生器,输出负脉冲。两个通道初值都写 0x1000,但方式不同导致输出波形不同,通道 1 的脉冲输出更适合做级联触发。
2.2 8259A 的屏蔽与 EOI:为什么先关再开
主程序里有一段很关键的顺序:先CLI关中断,然后读21H端口、AND AL,0FBH、写回,再读0A1H、AND AL,0F7H、写回,最后才STI。21H是主片 8259A 的中断屏蔽寄存器(IMR),0A1H是从片的 IMR。0FBH是 11111011B,把 bit2 清零,意思是允许 IR2 这一路中断;0F7H是 11110111B,清 bit3,允许从片 IR3。为什么必须先关中断再改 IMR?因为如果中途来了一个未屏蔽的中断,CPU 会跳进一个还没准备好的服务程序,轻则跑飞,重则死循环。这种“先关后开”的写法是中断初始化的血泪经验,不是可有可无的仪式。
中断服务程序末尾的MOV AL,20H和OUT 20H,AL是向主片发 EOI(End of Interrupt)。8259A 在服务完一个中断后,必须收到 EOI 才会把当前中断标记为结束,否则同优先级或更低优先级的请求会被一直压着。这里只发主片 EOI,是因为实验里从片的中断最终汇聚到主片 IR2,主片收到 EOI 后整条链路才算释放。如果从片也直接挂 CPU,那就得分别发。很多人在实验箱上遇到“只中断一次就再也不来了”,八成就是漏了这条 EOI。
2.3 中断向量表:2573H 这五个字符在干什么
MOV AX,2573H配合INT 21H是 DOS 时代设置中断向量的标准写法:AH=25H 表示“设置中断向量”,AL=73H 是中断号,DS:DX 指向服务程序入口。这里选 73H 不是随便挑的,它落在用户可用的中断号区间,避开系统占用的 08H-0FH、1CH 等。MOV AX,CS和MOV DS,AX先把 DS 指向代码段,再MOV DX,OFFSET INTPROC取偏移,这样 DS:DX 就完整指向INTPROC。这条链路一旦建立,8259A 送来 IRQ 后,CPU 查向量表第 73H 项,直接跳进INTPROC。向量表本质就是内存最低端的 1024 字节,每项 4 字节,前两字节偏移、后两字节段基址,INT 21H的 25H 功能只是帮你把这两个字填进去。
3. 中断服务程序:COUNT 递减背后的状态机
3.1 为什么 INTPROC 里要重新装 DS
进入中断服务程序后第一件事是CLI,然后MOV AX,DATA、MOV DS,AX。主程序里 DS 已经被改成 CS 用来设向量表,中断发生时 CPU 不会自动恢复 DS,所以服务程序必须自己把 DS 重新指向数据段,否则后面MOV DX,OFFSET STRING取到的字符串地址就是错的。这是实模式下中断编程的经典坑:段寄存器不会随中断自动保存,除了 CS:IP 和 FLAGS 由硬件压栈,DS、ES 都得自己保护。这段代码没做完整的现场保护(没 PUSH 所有寄存器),是因为实验环境简单,但严格来说,一个合格的中断服务程序应该在入口 PUSH 受影响寄存器,出口再 POP,否则主程序里的寄存器状态会被悄悄改掉。
INTPROC PROC NEAR CLI MOV AX, DATA MOV DS, AX ; 恢复数据段寻址 MOV DX, OFFSET STRING MOV AH, 9 INT 21H ; 显示字符串 MOV DX, IOPORT+288H ; 8255 端口 A 数据口 MOV AL, LIGHT OUT DX, AL ; 点亮当前 LED MOV CL, 01H SAR AL, CL ; 算术右移,准备下一个 LED MOV LIGHT, AL MOV AL, 20H OUT 20H, AL ; 主片 EOI MOV AL, COUNT DEC AL MOV COUNT, AL ; 中断次数减一 STI IRET INTPROC ENDPLIGHT初值80H即 10000000B,第一次输出点亮最高位对应的 LED,然后SAR AL,1变成 01000000B,下一次点亮次高位,八次之后LIGHT变成 0。COUNT初值 08H,每次中断减一,主循环CMP COUNT,0判断是否退出。这里的状态机完全靠内存变量维持,中断服务程序本身不保存“第几次”的上下文,它只负责“读变量、改变量、写回”。这种设计的好处是服务程序极短,坏处是变量在主循环和服务程序之间共享,如果主循环正在读COUNT时被中断打断,读到的可能是半新半旧的值。实模式下DEC AL和MOV COUNT,AL不是原子操作,严格场景要用LOCK前缀或者关中断保护,实验里因为主循环只是空转比较,风险被掩盖了。
3.2 8255 的端口地址与方式字
IOPORT+288H是 8255 端口 A 的数据口,IOPORT+28BH是控制口。初始化写10001111B:bit7 为 1 表示方式设置,bit6-5 选 A 组方式 0,bit4 端口 A 输出,bit3 端口 C 高四位输出,bit0 端口 C 低四位输出。方式 0 是基本 I/O,没有握手信号,CPU 写什么端口就输出什么电平,正好用来直接驱动 LED。IOPORT定义为5400H-280H,所以+288H实际是5408H,+28BH是540BH。这种“基址减偏移再加偏移”的写法是为了让IOPORT+xxx直接对应实验箱上的绝对端口,读代码时先把IOPORT算清楚,不然很容易把+288H误当成段内偏移。
I_8255 PROC NEAR MOV DX, IOPORT+28BH ; 8255 控制口 MOV AL, 10001111B ; 方式0,A/B/C 全输出 OUT DX, AL RET I_8255 ENDP方式字 10001111B 里 bit4 置 1 让端口 A 输出,bit3 置 1 让端口 C 高四位输出,bit0 置 1 让端口 C 低四位输出,B 组由 bit2-1 选方式 0、bit1 置 1 输出。实验只用了端口 A 驱动八个 LED,但控制字把 C 口也设成输出,属于“多配了不亏”的保守写法。
3.3 主循环的空转与退出条件
主程序LL: CMP COUNT,0、JZ INTEND、JNZ LL是一个忙等待循环,CPU 在这里反复比较COUNT,直到它变成 0 才往下走。这种写法在裸机实验里很常见,因为没有任何操作系统调度,主程序除了等中断没别的事可做。COUNT从 08H 减到 00H 需要八次中断,每次中断点亮一个 LED,八次之后八个 LED 全亮,循环退出,然后恢复中断屏蔽寄存器、关闭 9054 芯片的中断设置、MOV AH,4CH返回 DOS。注意退出前把21H端口OR AL,80H再写回,是把主片 IMR 的 bit7 置 1,屏蔽掉所有可屏蔽中断,防止程序返回 DOS 后还有残留中断请求进来。这个收尾动作和开头的“先关后开”对称,体现的是同一套防御性编程习惯。
4. 避坑与排查:实验箱上最容易翻车的五个点
4.1 只中断一次就停:EOI 漏发或发错片
现象是字符串只显示一次,LED 只亮一个,之后程序卡死。原因通常是OUT 20H,AL没执行,或者AL里不是20H。8259A 在非自动 EOI 模式下,服务完必须收到 EOI 才释放当前中断,否则同级和低级请求全被挡住。排查时在IRET前加一句往某个空闲端口写标记,用逻辑笔或示波器看有没有执行到;也可以把 EOI 提到服务程序最前面试一下,如果变成连续中断,就说明原来的 EOI 位置或数值有问题。
4.2 字符串乱码或打印不出来:DS 没重装
现象是屏幕输出一堆乱码,或者INT 21H的 9 号功能完全没反应。原因是进入服务程序后 DS 还指着代码段,OFFSET STRING算出来的是代码段内偏移,指向的根本不是字符串。解决就是在服务程序入口立刻MOV AX,DATA、MOV DS,AX,并且确认DATA段寄存器装的是数据段基址。如果用了ASSUME但没实际装 DS,汇编器不会报错,运行时才翻车,这类问题只能靠单步调试看 DS 值。
4.3 LED 不按顺序亮:LIGHT 初值或移位方向错
现象是八个 LED 要么全亮、要么乱亮、要么只亮一个不往下走。先查LIGHT初值是不是80H,再查移位用的是SAR还是SHR。SAR是算术右移,最高位补符号位,80H是负数,右移后变成C0H而不是40H,LED 就会一次亮两个。正确做法是用SHR逻辑右移,或者初值用01H配合左移SHL。这个坑很隐蔽,因为SAR和SHR在正数上行为一样,只有最高位为 1 时才分道扬镳。
4.4 中断根本不进:IMR 位没清对或向量号冲突
现象是程序跑完初始化就卡在LL循环里,COUNT永远不减。先确认21H端口读改写时清的是哪一位,0FBH清 bit2,如果实验箱上 8253 实际接的是 IR3,那就该用0F7H。再确认INT 21H设的向量号73H没有被别的程序占用,DOS 下可以用调试器看0000:01CC附近四个字节是不是指向INTPROC。还有一个常见原因是 8253 的 GATE 没使能,通道不计数自然没有输出,用示波器量一下 8253 的 OUT 引脚有没有波形,能快速把问题切成“定时器没动”和“中断控制器没放行”两半。
4.5 返回 DOS 后系统异常:中断屏蔽没恢复
现象是程序退出后键盘失灵或者系统直接挂起。原因是退出前没有把 IMR 恢复成进入时的状态,残留的中断请求在 DOS 环境下被触发,而对应的服务程序已经不存在了。解决就是在INTEND里把21H和0A1H的原始值写回,最稳妥的做法是程序开头先把两个 IMR 读出来存到变量里,退出时原样写回,而不是靠OR AL,80H这种“一刀切屏蔽”。实验代码里用OR AL,80H是简化处理,真实项目里应该保存-恢复。
5. 设计取舍:为什么这套实验至今仍值得逐行读
这套代码最值得琢磨的地方,不是它用了多新的技术,而是它把“中断”拆成了三个可以独立验证的层次:8253 负责“什么时候发生”,8259A 负责“让不让 CPU 知道”,8255 负责“发生后做什么”。三层之间通过物理引脚和端口地址耦合,任何一层出问题,现象都能定位到具体芯片。这种分层不是教科书硬分的,而是硬件本身就这么长出来的。你在代码里看到的OUT DX,AL写控制字、读 IMR 改位、发 EOI,每一步都对应芯片内部一个真实的状态机,没有抽象泄漏。
从设计取舍上看,这段代码选择了“最短服务程序 + 内存变量记状态”的路子。INTPROC里没有循环、没有延时,只做显示、输出、移位、减计数、发 EOI 五件事,把“第几次中断”这种上下文完全外置到COUNT和LIGHT两个变量里。好处是中断响应快,坏处是共享变量没有保护,主循环和服务程序之间存在竞态窗口。如果换成更严谨的写法,应该在服务程序入口PUSH所有会用到的寄存器,对COUNT的读改写加CLI/STI包住,退出时再POP。但实验环境里主循环只是空转比较,竞态被时间掩盖了,这也是为什么很多人跑通之后回头再看,才发现“能跑”和“对”之间还有一段距离。
验证这套机制是否真的被理解,有个很直接的办法:把COUNT初值从08H改成03H,看是不是只亮三个 LED 就退出;把 8253 通道 0 初值从1000改成2000,看中断周期是不是翻倍;把SAR换成SHR,看 LED 点亮顺序是否从“一次亮两个”变成“逐个亮”。每一次改动都对应一个可观测的现象变化,这比背控制字位定义有用得多。我自己的习惯是,每次碰这类端口级代码,先把所有OUT的目标端口和值列成一张表,再逐条对照芯片手册的位定义,最后才上电跑。从那以后我每次调中断相关的东西,都强制先把 IMR 的读改写和 EOI 的发送顺序在纸上走一遍,再动手改代码。希望帮到你。
本文还有配套的精品资源,点击获取