
简介这份资源是西南科技大学计算机科学与技术专业《单片微机原理及应用》课程设计2的完整设计报告面向正在学习51单片机、需要完成课程设计或准备单片机实验考核的本科生。报告围绕AT89C51外部中断应用展开涵盖中断源与中断标志、中断类型号、IE与IP寄存器配置、中断初始化程序及中断函数编写等知识点并给出声光报警器的电路原理图、主程序与中断服务程序流程图及关键代码解析。压缩包内仅含1个docx文档约477KB按课程设计报告册规范排版包含知识要求、能力要求、设计目标与任务、电路设计、程序设计思路、代码实现和总结等章节可直接作为报告撰写模板与实验参考。已有89人学习适合需要理清中断法与查询法差异、掌握按键响应与数码管、蜂鸣器联动控制的学生对照仿写与查漏补缺。1. 从两颗按键说起为什么声光报警器是练中断的最佳切口单片机课程设计里最容易被写成流水账的题目就是外部中断。原因很简单需求只有两行字——按 K1 亮一号灯、按 K2 亮二号灯——但真要把它做成一份能过验收的设计报告中间隔着一整套东西中断源怎么选、IE 和 IP 寄存器哪几位该置 1、下降沿触发和电平触发差在哪、中断服务程序里能不能写长延时。这套 AT89C51 外部中断声光报警器的方案就是把 2 个按键、2 个发光二极管、1 个数码管和 1 个蜂鸣器拼成一个小系统用 K1/K2 走外部中断 INT0/INT1用 K3/K4 走查询法让两条技术路线在同一个工程里对照着跑起来。它适合正在做单片微机原理课程设计、需要交一份带电路图、流程图、代码和仿真结果的学生也适合手头有个 8051 小控制板、想把中断机制重新捋一遍的从业者。2. 需求拆解与器件选型四个指示器件怎么挂到 AT89C51 的引脚上题目给的功能描述很朴素初始状态两个灯灭K1 按下后数码管显示 1、D1 闪烁、蜂鸣器报警K2 按下后数码管显示 2、D2 闪烁、蜂鸣器报警。真正落到原理图上第一个要决定的事情不是写代码而是每个器件占哪个引脚。引脚一旦定错后面代码里所有的sbit声明、段码表、中断向量号都要跟着返工这是最容易在第二周推倒重来的地方。2.1 从功能清单反推引脚分配表我一般会先把功能清单摊成一张表左边写这个器件要干什么右边写它必须挂在哪个引脚上判断依据是引脚的复用功能而不是布线方便。AT89C51 的 P3 口是第二功能口P3.2 和 P3.3 分别复用为外部中断 0INT0和外部中断 1INT1K1、K2 必须落在这两个脚上否则中断源就无从谈起。K3、K4 走查询法放 P3.4、P3.5 就够也可以挪到 P1 口但放在 P3 剩下的脚上布线更整齐。器件引脚有效电平说明D1D4P2.0P2.3低电平点亮P2 灌电流能力强低电平驱动 LED 更稳蜂鸣器P1.6由程序翻转经三极管驱动不直连 IO数码管 ag、dpP0.0P0.7低电平点亮共阳极接法P0 需外接 10k 上拉排阻K1P3.2 / INT0低电平有效下降沿触发内部上拉K2P3.3 / INT1低电平有效下降沿触发内部上拉K3P3.4低电平有效查询法读取K4P3.5低电平有效查询法读取RSTRST高电平复位10μF 电容 10k 电阻构成上电复位XTAL1/XTAL218、19 脚—12MHz 晶振 两只 30pF 瓷片电容EA31 脚接 VCC使用片内程序存储器时必须拉高这张表里有两个容易被忽略的点。一是 EA 脚很多仿真图里忘了接Proteus 里可能还能跑一上实物就取不到指令程序像死了一样二是 P0 口它内部没有上拉场效应管做普通 I/O 输出时必须外加排阻否则数码管亮度发虚甚至完全不亮。通常的做法是在 P0 和 VCC 之间挂一个 10k 的排阻成本两毛钱能省掉半天的排查。2.2 数码管共阳共阴与段码推导数码管的段码是这份设计里唯一需要算的东西也是答辩时老师最爱问的地方。七段数码管的 a、b、c、d、e、f、g 七段分别对应端口的最低位到第六位最高位留给小数点 dp。本设计用的是共阳极数码管公共端接 VCC所以某一段要亮对应的端口位就必须输出 0这就是低电平有效的由来。以数字 1 为例只需要 b、c 两段亮其余段灭。按 a→bit0、b→bit1、c→bit2、d→bit3、e→bit4、f→bit5、g→bit6、dp→bit7 的顺序排列得到二进制 1111 1001bit0 是 a 段灭1bit1 是 b 段亮0bit2 是 c 段亮0其余全灭1转成十六进制就是 0xF9。同理数字 2 需要 a、b、d、e、g 五段亮得 1010 0100 即 0xA4数字 3 需要 a、b、c、d、g 亮得 1011 0000 即 0xB0数字 4 需要 b、c、f、g 亮得 1001 1001 即 0x99。熄灭全部段就是 0xFF。显示字符点亮段二进制dp g f e d c b a十六进制1b、c1111 10010xF92a、b、d、e、g1010 01000xA43a、b、c、d、g1011 00000xB04b、c、f、g1001 10010x99熄灭无1111 11110xFF把这五个值做成code数组存进程序存储器比在中断里写死P0 0xF9更耐改。code是 Keil C51 的关键字表示数据放在 ROM 里不占宝贵的 128 字节 RAM这一点在 8051 上不是可选项而是常识。段码表用code修饰数组下标就是要显示的数字取值即段码扩展成 09 也不用改逻辑。2.3 蜂鸣器与发光二极管的驱动余量单片机 IO 口的拉电流能力通常只有几十微安灌电流能力强得多能到十几毫安。所以 LED 用低电平点亮是合理的P2 口输出 0电流从 VCC 经 LED 和限流电阻灌进引脚。限流电阻一般取 330Ω1kΩ取 330Ω 时电流约 5mA八个段同时点亮时 P0 口的总灌电流要控制在不超出手册给出的上限数码管每一位串一只 330Ω 电阻即可。蜂鸣器不一样它是感性负载直接挂在 P1.6 上轻则声音微弱重则把端口管子击穿。通常的做法是用一只 PNP 三极管比如 S8550做开关发射极接 VCC集电极接蜂鸣器负极蜂鸣器正极接 VCC基极经 1kΩ 电阻接 P1.6。程序输出低电平时三极管导通蜂鸣器得电。这里要区分有源和无源有源蜂鸣器内部带振荡电路给电就响无源蜂鸣器需要外部提供 24kHz 方波。原设计里用的是buzz !buzz配合延时不断翻转说明是按无源的方式驱动的翻转周期决定了音调。这个细节在报告里要写清楚否则仿真时听到嗒嗒声答辩老师一问就露怯。3. 中断法还是查询法用两条路径把 4 个按键分开处理四个按键全部用查询也能实现功能全部用中断也能实现但这份设计的价值恰恰在于把两种方式放在同一个工程里做对照。K1、K2 响应要快、要和主循环脱钩走中断K3、K4 只是普通电平检测走查询代码简单直观。这种混搭不是偷懒而是真实项目里最常见的分工方式——紧急信号走中断状态量走轮询。3.1 IE 与 IP 寄存器到底该设哪几位AT89C51 的中断系统靠两个寄存器管起来IE 管开不开IP 管谁优先。IE 寄存器按位从高到低是 EA、—、ET2、ES、ET1、EX1、ET0、EX0。本设计只用到外部中断 0 和 1所以只关心 EA、EX0、EX1 三位EA 是总开关必须置 1EX0、EX1 分别允许 INT0 和 INT1定时器和串口中断全部关掉避免未使用的中断标志位被误触发。IP 寄存器按位是 —、—、PT2、PS、PT1、PX1、PT0、PX0。默认全部为 0表示两个外部中断同优先级。同优先级的中断不能互相嵌套也就是说 INT0 的服务程序执行期间INT1 的请求只能挂起等待。如果希望 K1 比 K2 更急把 PX0 置 1、PX1 清 0INT0 就成了高优先级可以打断正在执行的 INT1 服务程序。课程设计里这一步是可选项但写进报告里能体现对 IP 寄存器的理解答辩时是个加分点。#include reg51.h #define uchar unsigned char #define uint unsigned int sbit D1 P2^0; /* 报警灯 1 */ sbit D2 P2^1; /* 报警灯 2 */ sbit D3 P2^2; /* 报警灯 3查询通道 */ sbit D4 P2^3; /* 报警灯 4查询通道 */ sbit BUZZ P1^6; /* 蜂鸣器经三极管驱动 */ sbit K3 P3^4; /* 查询按键 3 */ sbit K4 P3^5; /* 查询按键 4 */ /* 共阳数码管 0~9 段码位为 0 表示该段点亮 */ code uchar SEG_CODE[10] { 0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90 }; volatile uchar g_alarm_ch 0; /* 0无请求1通道12通道2 */ volatile uchar g_alarm_req 0; /* 主循环与中断共享必须加 volatile */ void InitExtInt(void) { EA 1; /* CPU 开中断总开关 */ EX0 1; /* 允许外部中断 0对应 K1 */ EX1 1; /* 允许外部中断 1对应 K2 */ IT0 1; /* INT0 下降沿触发 */ IT1 1; /* INT1 下降沿触发 */ PX0 1; /* INT0 高优先级K1 可打断 K2 的处理 */ PX1 0; }IT0、IT1这两位其实不在 IE 里而在 TCON 寄存器里很多报告会写错位置。置 1 表示下降沿触发置 0 表示低电平触发。按键按下时引脚从高变低正好产生一个下降沿所以置 1 是对的。如果误设成电平触发按键按住不放会反复进中断表现就是按一下响个不停这是排查时第一个要看的地方。3.2 K1/K2 走中断、K3/K4 走查询的分工理由中断法的好处是事件驱动主程序在while(1)里空转也好、做别的事也好只要 K1 一按下CPU 立刻保存现场跳到中断向量表 0x0003 处执行服务程序响应延迟只有几个机器周期。查询法则是主程序主动去读引脚读的间隔取决于循环一圈多久如果循环里有个 3 秒的延时这 3 秒内按 K3 是没反应的。那为什么 K3、K4 还要用查询因为外部中断只有两个K1、K2 已经占满了。要四个都用中断就得借助定时器做软件扩展——定时器每 10ms 中断一次在定时器服务程序里扫描四个按键。这个方案更专业但对课程设计来说复杂度上去了而且失去了中断法和查询法对照的教学意义。所以分工的边界很清楚能直接挂到 INT0/INT1 上的用中断其余的用查询加软件消抖。对比项中断法K1/K2查询法K3/K4触发方式硬件检测下降沿自动跳转主程序主动读引脚电平响应延迟几个机器周期几乎实时取决于主循环一圈的耗时CPU 占用空闲时为 0每轮循环都要读一次是否需要消抖硬件触发仍需软件防抖必须软件延时消抖适用场景紧急信号、按键、外部事件状态量、慢变信号、按键数量多时本设计中的角色K1、K2 触发声光报警K3、K4 演示查询通道3.3 流程图怎么画成可交付的图报告里要求的两张流程图——主程序流程图和中断服务程序流程图——不是随手画几个框就行评审看的其实是你的程序结构和图上画的是不是一回事。我建议主流程图按初始化 → 空闲复位 → 判断有无报警请求 → 执行报警 → 扫描 K3/K4 → 回到空闲这个闭环来画把EA、EX0、EX1、IT0、IT1的赋值单独画成一个初始化框标注清楚每一步在配哪个寄存器。中断服务流程图则要刻意画得短进入中断 → 保存通道号到全局变量 → 置报警请求标志 → 中断返回。只有三个处理框这恰恰是这张图的得分点因为它和你后面要讲的中断里不做耗时操作是互相印证的。如果画成进入中断 → 显示数字 → 循环十次翻转灯和蜂鸣器 → 熄灭 → 返回图上一大串等于把设计缺陷画出来给老师看。画图工具不限Visio、Word 形状、draw.io 都行但框内文字要写函数名或寄存器名不要写处理数据判断条件这种空话。判断框的出口标是/否不要标Y/N混着来全文统一。4. 代码落地中断服务程序、段码表与主循环状态机的分工代码这一章决定报告的可信度。原始写法是在中断服务程序里直接做十次循环、每次延时 200ms 翻转灯和蜂鸣器功能上能看到效果但结构上是错的——中断服务执行了大约 4 秒这 4 秒里同优先级的另一个外部中断根本进不来K2 按了也白按。正确的做法是把中断服务压到最短只做记下是谁按的真正的声光动作交给主循环去做。4.1 中断服务只打标记报警动作交给主循环/* 外部中断 0 服务程序中断号 0对应 INT0 / K1 */ void ExtInt0_ISR(void) interrupt 0 { g_alarm_ch 1; /* 记录通道号 */ g_alarm_req 1; /* 置报警请求标志 */ } /* 外部中断 1 服务程序中断号 2对应 INT1 / K2 */ void ExtInt1_ISR(void) interrupt 2 { g_alarm_ch 2; g_alarm_req 1; }interrupt 0和interrupt 2是 Keil C51 的中断号不是随便编的0 对应外部中断 0向量地址 0x00031 对应定时器 00x000B2 对应外部中断 10x00133 对应定时器 14 对应串口。中断号写错编译不报错但程序会跳到错误的地方执行现象是一按键就跑飞或者完全没反应这类问题在仿真里尤其难查只能靠背下来。两个服务程序加起来不到十行执行时间只有几个微秒。g_alarm_ch和g_alarm_req必须加volatile因为它们被中断和主循环同时访问不加的话编译器优化时可能把变量缓存在寄存器里主循环永远读到旧值表现就是中断进了但灯不闪这是 8051 上最经典的黑匣子问题之一。4.2 主循环里的报警执行与非阻塞改造void DelayNms(uint ms) { uint i, j; for (i 0; i ms; i) for (j 0; j 120; j); /* 12MHz 晶振下约 1ms需实测校准 */ } /* 执行一次完整的声光报警数码管显示通道号对应灯闪 10 次蜂鸣器同步 */ void RunAlarm(uchar ch) { uchar i; uchar led (ch 1) ? D1 : D2; P0 SEG_CODE[ch]; /* ch1 显示 0xF9ch2 显示 0xA4 */ for (i 0; i 10; i) { led 0; /* 对应报警灯点亮低电平有效 */ BUZZ 0; /* 三极管导通蜂鸣器响 */ DelayNms(200); led 1; /* 灯灭 */ BUZZ 1; /* 蜂鸣器停 */ DelayNms(200); } P0 0xFF; /* 数码管熄灭 */ D1 1; D2 1; BUZZ 1; } void main(void) { P0 0xFF; /* 上电先熄灭数码管 */ D1 D2 D3 D4 1; /* 四只灯全灭 */ BUZZ 1; /* 蜂鸣器关闭 */ InitExtInt(); /* 配置 IE、IP、TCON */ while (1) { if (g_alarm_req) { g_alarm_req 0; /* 先清标志再执行避免漏掉后续请求 */ RunAlarm(g_alarm_ch); } ScanKey34(); /* 查询通道见 4.3 */ } }RunAlarm里用的是led 0这种写法靠sbit变量本身的赋值来点亮比P2 ~(1ch)更直观也避免误改其他引脚。整段报警耗时约 10×200ms200ms 4 秒这 4 秒里主循环被占住但中断依然能进——K2 在 K1 报警期间被按下中断服务程序照样把g_alarm_req置 1等RunAlarm返回后主循环立刻接着执行通道 2 的报警实现了排队。代价是如果连按三次中间的请求会被最后一次覆盖课程设计级别可以接受报告里加一句如需严格排队可改为环形缓冲区反而显得有余量。g_alarm_req 0放在RunAlarm之前清是为了防止执行期间新来的请求被误清。如果放在后面清报警 4 秒里来的新请求会在结束时被一键抹掉这是真实项目里踩过的坑。4.3 查询通道与按键消抖void ScanKey34(void) { if (K3 0) { /* 低电平有效读到 0 视为可能按下 */ DelayNms(20); /* 软件消抖跳过机械抖动的 10~20ms */ if (K3 0) { P0 SEG_CODE[3]; /* 显示 3 */ D3 0; /* 报警灯 3 点亮 */ BUZZ 0; /* 蜂鸣器响 */ DelayNms(3000); /* 报警持续 3 秒 */ while (K3 0); /* 等待按键释放防止连触发 */ P0 0xFF; D3 1; BUZZ 1; } } if (K4 0) { /* 通道 4 逻辑与通道 3 完全对称 */ DelayNms(20); if (K4 0) { P0 SEG_CODE[4]; /* 显示 4 */ D4 0; BUZZ 0; DelayNms(3000); while (K4 0); P0 0xFF; D4 1; BUZZ 1; } } }两次读引脚是消抖的核心第一次读到低电平不能信等 20ms 再读一次两次都是低才判定为真实按下。机械按键在闭合瞬间会有 520ms 的抖动期间电平在高低之间跳好几次只读一次很容易把一次按下识别成好几次。这段查询代码是阻塞式的DelayNms(3000)加while(K40)会让主循环卡住 3 秒以上期间中断照样能进、标志照样能置但RunAlarm要等它结束才执行。更规范的做法是把按键状态和报警动作也做成状态机用定时器 10ms 扫一次主循环完全不阻塞。课程设计里保持阻塞写法可以但报告里要主动指出这个局限说明你清楚它和中断通道的差别这比装作没问题强得多。4.4 延时函数的校准与晶振选择DelayNms的j 120是个经验值不是精确值。AT89C51 在 12MHz 晶振下机器周期是 1μs内层for循环编译出来大约 8 个机器周期120 次约 960μs接近 1ms。换成 11.0592MHz 晶振同样代码大约慢 8%200ms 的闪烁周期会变成 216ms肉眼看不出来但如果报告里写了精确 200ms就是硬伤。严谨的写法是用定时器做基准或者用示波器实测把某个引脚在延时前后翻转示波器读出方波周期的一半就是实际延时。我在验收前一般会挑一个通道测一次把实测值写进报告比如实测 200ms 延时误差 ±3ms满足设计要求。这句话的分量比十行代码都重因为它证明你真的测过。晶振的选型也要写进报告12MHz 便于按 1μs 机器周期手算延时11.0592MHz 便于串口通信时得到整数波特率。本设计不涉及串口选 12MHz 更直观。晶振旁边两只 30pF 电容不能省它们是起振电容省掉之后电路可能起振慢或者根本不起振现象是程序下载进去了但一动不动用示波器量 XTAL2 脚没有波形就能确认。5. 常见问题排查五个最容易翻车的点仿真跑通不等于实物能跑实物能跑不等于报告能过。下面这五条是这类题目里出现频率最高的问题每一条我都按现象—原因—解决记下来照着查能省掉大量时间。第一条K2 按下去没反应只有 K1 有效。现象是先按 K1 报警正常报警过程中按 K2 完全没动静等 K1 那 4 秒结束再按 K2 才有效。原因是中断服务程序里直接写了十次循环加延时整个服务程序执行了约 4 秒而同优先级的 INT1 无法打断 INT0 的服务程序请求只能挂起。解决办法是把中断服务压缩成记录通道号 置标志两句话声光动作搬到主循环的RunAlarm里执行中断服务执行时间降到微秒级。第二条数码管显示乱码或者根本不亮但蜂鸣器和灯正常。现象是按 K1 时数码管显示的不是 1而是像厂字或者几个段一起亮。原因多半是共阳共阴搞反了按共阳极推的段码 0xF9接到共阴极数码管上0 变成了亮的段整个编码逻辑翻转。另一种情况是 P0 口没接上拉排阻段码输出幅度不够数码管发暗甚至不亮。解决办法是先确认数码管型号看公共端接 VCC 还是 GND共阴就把段码按位取反0xF9 变 0x06P0 口加 10k 排阻每一段串 330Ω 限流电阻。第三条按一次键报警响好几遍。现象是手指按下去只按了一次蜂鸣器却嘀嘀嘀响了三五轮才停。原因是 IT0/IT1 设成 1 之后是下降沿触发而机械按键在闭合瞬间会产生多个下降沿硬件触发比软件查询更灵敏反而放大了抖动。解决办法是在中断服务程序里加软件防抖进中断后先延时 1020ms再读一次 P3.2 或 P3.3如果已经是高电平说明是抖动直接返回不置标志确认低电平才置位。更好的做法是硬件上加一只 0.1μF 电容并联在按键两端做 RC 滤波。第四条程序烧进去完全没反应灯不亮、蜂鸣器不响、按键无效。现象是仿真软件里一切正常下载到实物后像没上电。原因通常有三个EA 脚没接高电平CPU 取不到片内程序复位电路接错RST 一直保持高电平导致反复复位晶振不起振XTAL2 脚测不到正弦波。解决办法是按顺序查——先量 EA 是不是 VCC再量 RST 在运行时是不是低电平最后用示波器看晶振脚。这三步走完九成的完全没反应都能定位。第五条蜂鸣器声音很小或者响几声之后单片机死机。现象是蜂鸣器声音微弱或者报警几次后程序跑飞。原因是蜂鸣器直接挂在 IO 口上感性负载断开瞬间产生的反向电动势打穿了端口。解决办法是加三极管驱动S8550 或 S8050看是高电平还是低电平驱动蜂鸣器两端反向并联一只 1N4148 续流二极管基极串 1kΩ 限流电阻。这三样加起来成本不到一块钱但能保住整块板子。6. 验收与答辩用一张时序图和一份自检表把方案讲圆验收当天最尴尬的场面不是功能没做出来而是做出来了却讲不清楚。老师问你这个 200ms 是怎么来的回答随便写的问中断服务里为什么不能加延时答不上来。所以我在提交前会强制自己走一遍自检表把每个能被追问的点都落到具体数值上。检查项判定标准自查方法初始状态上电后 D1D4 全灭、数码管熄灭、蜂鸣器不响上电观察 3 秒K1 通道显示 1、D1 闪烁 10 次、蜂鸣器同步秒表计时约 4 秒K2 通道显示 2、D2 闪烁 10 次、蜂鸣器同步秒表计时约 4 秒K3/K4 通道显示 3/4、对应灯常亮 3 秒、蜂鸣器响观察灯是常亮而非闪烁中断嵌套K1 报警期间按 K2结束后自动执行通道 2连按验证排队延时精度单次 200ms 误差在 ±5% 内示波器测翻转周期复位可靠性连续复位 10 次每次都能正常进入初始状态反复按复位键答辩讲解我一般按这个顺序走先讲需求四个按键、四个指示器件再讲分工K1/K2 走中断、K3/K4 走查询为什么这么分然后讲寄存器IE 的 EA/EX0/EX1 三位、TCON 的 IT0/IT1 两位、IP 的 PX0接着讲代码结构中断只置标志、主循环执行动作最后讲实测数据延时误差、响应延迟。这五步讲完通常八分钟正好是一个课程设计答辩的时长。有一个技巧特别管用把示波器的截图打印出来贴在报告里标上P1.6 引脚实测方波周期 400ms占空比 50%。这一张图能同时证明三件事——蜂鸣器确实在被翻转驱动、延时函数确实工作在 200ms、你的方案真的测过而不只是仿真过。相比之下一堆 Proteus 截图的说服力要弱得多因为仿真里延时是理想值实物才有误差。还有个常被忽略的点报告里的电路图和代码必须对得上。代码里写sbit BUZZ P1^6原理图上蜂鸣器的驱动三极管基极就必须接在 P1.6代码里P0输出段码原理图上 P0 就必须连着数码管的段选。答辩老师翻报告时最爱干的事就是对照这两处一旦发现对不上前面讲得再好也会被打折扣。从那以后我每次做这类带中断的小系统都会在写代码之前先把引脚分配表和段码表钉在报告的第二章代码严格按表来写验收前再拿示波器实测一遍延时并截图存档——这三件事做完报告基本不会被退回来返工。希望帮到你。本文还有配套的精品资源点击获取