; =================================================================
; nmi_demo.asm - 8088 单板机 NMI 中断演示程序
; 脱离 DOS 环境,直接运行在裸机上
; =================================================================; 硬件配置假设:
; - 8088 CPU @ 4.77MHz
; - 8259 PIC (可编程中断控制器)
; - 8255 PPI (可编程外设接口) 连接 LED
; - 7 段数码管显示
; - NMI 按钮连接到 NMI 引脚PORT_PPI_A EQU 60h ; 8255 PPI 端口 A (LED 控制)
PORT_PPI_B EQU 61h ; 8255 PPI 端口 B (7 段数码管数据)
PORT_PPI_C EQU 62h ; 8255 PPI 端口 C (数码管位选)
PORT_PPI_CTRL EQU 63h ; 8255 PPI 控制端口PORT_PIC_CMD EQU 20h ; 8259 PIC 命令端口
PORT_PIC_DATA EQU 21h ; 8259 PIC 数据端口; 7 段数码管编码 (共阴极)
; 0 1 2 3 4 5 6 7 8 9
SEG7 db 3Fh, 06h, 5Bh, 4Fh, 66h, 6Dh, 7Dh, 07h, 7Fh, 6Fhorg 0FFF0h ; 8088 复位向量地址
reset:jmp 0F000h:start ; 跳转到实际起始地址org 0F000h ; BIOS 区域起始地址 (典型); === 主程序入口 ===
start:; 设置段寄存器cli ; 禁用中断mov ax, csmov ds, ax ; DS = CSmov ss, ax ; SS = CSmov sp, 0FFFEh ; 栈指针在 64KB 顶部; 初始化硬件call init_ppi ; 初始化 8255 PPIcall init_pic ; 初始化 8259 PICcall init_nmi ; 初始化 NMI 处理; 显示启动信息call clear_displaymov si, msg_welcomecall display_string; 主循环 - 等待 NMIsti ; 启用中断
main_loop:hlt ; 暂停 CPU 等待中断jmp main_loop; === 初始化 8255 PPI ===
init_ppi:; 设置 8255 控制字: 端口A输出, 端口B输出, 端口C输出, 模式0mov al, 10000000b ; 控制字: 1 (I/O模式) 00 (A组模式0) 0 (A输出) 1 (C上输出) 0 (B组模式0) 0 (B输出) 0 (C下输出)out PORT_PPI_CTRL, al; 初始状态: 关闭所有 LED 和数码管xor al, alout PORT_PPI_A, al ; 关闭 LEDout PORT_PPI_B, al ; 关闭数码管段out PORT_PPI_C, al ; 关闭数码管位选ret; === 初始化 8259 PIC ===
init_pic:; ICW1: 边沿触发, 级联, 需要 ICW4mov al, 00010011b ; ICW1: 1 (需要ICW4) 0 (级联) 0 (边沿触发) 1 (需要ICW4) 1 (主PIC)out PORT_PIC_CMD, al; ICW2: 中断向量基地址mov al, 08h ; 中断向量号从 08h 开始out PORT_PIC_DATA, al; ICW3: 主片 IR2 连接从片 (本示例不使用从片)mov al, 00000100b ; IR2 连接从片 (00000100b)out PORT_PIC_DATA, al; ICW4: 8086/88 模式mov al, 00000001b ; ICW4: 0 (非特殊全嵌套) 0 (非缓冲) 0 (主片) 1 (8086模式)out PORT_PIC_DATA, al; OCW1: 屏蔽所有中断 (NMI 不受影响)mov al, 11111111b ; 屏蔽所有 IRQout PORT_PIC_DATA, alret; === 初始化 NMI 处理 ===
init_nmi:; 保存原始 NMI 向量xor ax, axmov es, ax ; ES = 0000h (中断向量表段)mov di, 2*4 ; NMI 向量位置 (中断号 2 * 4 字节)mov ax, [es:di] ; 获取偏移mov [old_nmi_offset], axmov ax, [es:di+2] ; 获取段地址mov [old_nmi_segment], ax; 设置新的 NMI 处理程序climov word [es:di], nmi_handler ; 设置新偏移mov [es:di+2], cs ; 设置段地址 (当前代码段)sti; 启用 NMI (清除 CMOS 寄存器第7位)in al, 70hand al, 01111111b ; 清除第7位 (NMI 启用)out 70h, alret; === NMI 中断处理程序 ===
nmi_handler:; 保存寄存器push axpush bxpush cxpush dxpush sipush ds; 设置数据段mov ax, csmov ds, ax; 增加 NMI 计数器inc byte [nmi_count]; 在 LED 上显示 NMI 发生mov al, 0FFh ; 点亮所有 LEDout PORT_PPI_A, al; 在数码管上显示计数call update_display; 延迟一段时间 (视觉反馈)mov cx, 0FFFFh
delay_loop:noploop delay_loop; 关闭 LEDxor al, alout PORT_PPI_A, al; 发送中断结束信号 (EOI)mov al, 20h ; 非特定 EOIout PORT_PIC_CMD, al; 恢复寄存器pop dspop sipop dxpop cxpop bxpop axiret ; 中断返回; === 更新数码管显示 ===
update_display:; 显示 NMI 计数mov al, [nmi_count]and al, 0Fh ; 只显示低4位; 获取数码管编码mov bx, SEG7xlat ; AL = DS:[BX + AL]; 输出到数码管out PORT_PPI_B, al ; 段数据; 启用第一个数码管mov al, 00000001b ; 位选: 启用第一个数码管out PORT_PPI_C, alret; === 清除数码管显示 ===
clear_display:xor al, alout PORT_PPI_B, al ; 关闭所有段out PORT_PPI_C, al ; 关闭所有位选ret; === 显示字符串 (SI = 字符串地址) ===
display_string:push axpush bxpush cxpush dxpush simov cx, 0 ; 字符计数器
next_char:lodsb ; 加载下一个字符test al, al ; 检查是否结束jz display_done; 查找字符的7段编码mov bx, SEG7xlat ; AL = 7段编码; 输出到数码管out PORT_PPI_B, al ; 段数据; 设置位选mov al, clinc alout PORT_PPI_C, al ; 位选; 延迟push cxmov cx, 1000h
delay_char:loop delay_charpop cxinc cx ; 下一个数码管cmp cx, 4 ; 最多4个数码管jb next_chardisplay_done:pop sipop dxpop cxpop bxpop axret; === 数据区 ===
msg_welcome db 'NMI', 0 ; 欢迎消息 (以0结尾); NMI 计数器
nmi_count db 0; 原始 NMI 向量保存位置
old_nmi_offset dw 0
old_nmi_segment dw 0; === 填充到 64KB ROM 结束 ===
times 0FFFFh - $ + 1 db 0FFh
