cpp
复制代码
; =================================================================
; 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, 6Fh
org 0FFF0h ; 8088 复位向量地址
reset:
jmp 0F000h:start ; 跳转到实际起始地址
org 0F000h ; BIOS 区域起始地址 (典型)
; === 主程序入口 ===
start:
; 设置段寄存器
cli ; 禁用中断
mov ax, cs
mov ds, ax ; DS = CS
mov ss, ax ; SS = CS
mov sp, 0FFFEh ; 栈指针在 64KB 顶部
; 初始化硬件
call init_ppi ; 初始化 8255 PPI
call init_pic ; 初始化 8259 PIC
call init_nmi ; 初始化 NMI 处理
; 显示启动信息
call clear_display
mov si, msg_welcome
call display_string
; 主循环 - 等待 NMI
sti ; 启用中断
main_loop:
hlt ; 暂停 CPU 等待中断
jmp main_loop
; === 初始化 8255 PPI ===
init_ppi:
; 设置 8255 控制字: 端口A输出, 端口B输出, 端口C输出, 模式0
mov 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, al
out PORT_PPI_A, al ; 关闭 LED
out PORT_PPI_B, al ; 关闭数码管段
out PORT_PPI_C, al ; 关闭数码管位选
ret
; === 初始化 8259 PIC ===
init_pic:
; ICW1: 边沿触发, 级联, 需要 ICW4
mov 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 ; 屏蔽所有 IRQ
out PORT_PIC_DATA, al
ret
; === 初始化 NMI 处理 ===
init_nmi:
; 保存原始 NMI 向量
xor ax, ax
mov es, ax ; ES = 0000h (中断向量表段)
mov di, 2*4 ; NMI 向量位置 (中断号 2 * 4 字节)
mov ax, [es:di] ; 获取偏移
mov [old_nmi_offset], ax
mov ax, [es:di+2] ; 获取段地址
mov [old_nmi_segment], ax
; 设置新的 NMI 处理程序
cli
mov word [es:di], nmi_handler ; 设置新偏移
mov [es:di+2], cs ; 设置段地址 (当前代码段)
sti
; 启用 NMI (清除 CMOS 寄存器第7位)
in al, 70h
and al, 01111111b ; 清除第7位 (NMI 启用)
out 70h, al
ret
; === NMI 中断处理程序 ===
nmi_handler:
; 保存寄存器
push ax
push bx
push cx
push dx
push si
push ds
; 设置数据段
mov ax, cs
mov ds, ax
; 增加 NMI 计数器
inc byte [nmi_count]
; 在 LED 上显示 NMI 发生
mov al, 0FFh ; 点亮所有 LED
out PORT_PPI_A, al
; 在数码管上显示计数
call update_display
; 延迟一段时间 (视觉反馈)
mov cx, 0FFFFh
delay_loop:
nop
loop delay_loop
; 关闭 LED
xor al, al
out PORT_PPI_A, al
; 发送中断结束信号 (EOI)
mov al, 20h ; 非特定 EOI
out PORT_PIC_CMD, al
; 恢复寄存器
pop ds
pop si
pop dx
pop cx
pop bx
pop ax
iret ; 中断返回
; === 更新数码管显示 ===
update_display:
; 显示 NMI 计数
mov al, [nmi_count]
and al, 0Fh ; 只显示低4位
; 获取数码管编码
mov bx, SEG7
xlat ; AL = DS:[BX + AL]
; 输出到数码管
out PORT_PPI_B, al ; 段数据
; 启用第一个数码管
mov al, 00000001b ; 位选: 启用第一个数码管
out PORT_PPI_C, al
ret
; === 清除数码管显示 ===
clear_display:
xor al, al
out PORT_PPI_B, al ; 关闭所有段
out PORT_PPI_C, al ; 关闭所有位选
ret
; === 显示字符串 (SI = 字符串地址) ===
display_string:
push ax
push bx
push cx
push dx
push si
mov cx, 0 ; 字符计数器
next_char:
lodsb ; 加载下一个字符
test al, al ; 检查是否结束
jz display_done
; 查找字符的7段编码
mov bx, SEG7
xlat ; AL = 7段编码
; 输出到数码管
out PORT_PPI_B, al ; 段数据
; 设置位选
mov al, cl
inc al
out PORT_PPI_C, al ; 位选
; 延迟
push cx
mov cx, 1000h
delay_char:
loop delay_char
pop cx
inc cx ; 下一个数码管
cmp cx, 4 ; 最多4个数码管
jb next_char
display_done:
pop si
pop dx
pop cx
pop bx
pop ax
ret
; === 数据区 ===
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