AT89C51中断系统详解:从原理到实战,掌握单片机多任务响应机制

发布时间:2026/8/2 1:27:31

AT89C51中断系统详解:从原理到实战,掌握单片机多任务响应机制 1. 项目概述从“单线程”到“多任务”的思维跃迁搞单片机开发的朋友尤其是从51系列入门的一定绕不开“中断”这个坎。很多人初学AT89C51时总觉得程序跑得挺顺干嘛要学中断这就像你一个人在家既要烧水又要等快递如果不用中断你就得一直盯着水壶快递来了你也听不见敲门而用了中断你就可以安心看书水开了中断源1或者门响了中断源2会立刻通知你你处理完再回来接着看书。AT89C51的中断系统就是给这颗“单线程”大脑安装的“多任务”响应机制它让单片机具备了处理突发事件的能力是嵌入式系统从简单循环迈向实时响应的关键一步。我当年做第一个基于AT89C51的电子钟项目就卡在了按键调时上。不用中断就得在主循环里不停扫描按键效率低还容易漏按用了中断按键按下瞬间单片机立即响应调时精准又省电。这个实验不仅仅是点亮一个LED更是思维模式的转变。本文将彻底拆解AT89C51的中断机制从内部寄存器每一位的作用到实际应用中的编程技巧和避坑指南最后通过一个综合性实验带你亲手实现一个中断驱动的系统。无论你是正在啃单片机教材的学生还是想夯实底层功底的工程师这篇详解都能让你对中断的理解从“知道”变成“精通”。2. AT89C51中断系统架构深度解析要玩转中断不能只停留在调用库函数必须深入其硬件架构。AT89C51的中断系统属于经典的中断向量固定优先级结构理解它是灵活应用的前提。2.1 中断源与中断向量五路“报警通道”AT89C51提供了5个独立的中断源你可以理解为5条专用的“报警热线”。每一条热线都连接着单片机内部或外部的一个特定事件一旦事件发生就会自动触发一条“跳转指令”让CPU暂停当前工作转去处理这个紧急事件。这5个中断源及其固定的“热线号码”中断号和“接警地址”中断向量如下中断源中断号 (C语言常用)中断向量地址 (汇编语言关注)产生条件典型应用场景外部中断0(INT0)00x0003P3.2引脚出现低电平或下降沿按键、紧急开关、限位传感器定时器/计数器0中断(TF0)10x000B定时器0计数溢出精准定时、产生PWM波、软件计时外部中断1(INT1)20x0013P3.3引脚出现低电平或下降沿另一个独立按键或外部事件定时器/计数器1中断(TF1)30x001B定时器1计数溢出串口波特率发生器、另一个定时任务串行口中断(RI/TI)40x0023串行接收完成(RI)或发送完成(TI)串口数据收发、与PC或其他设备通信注意中断向量地址是硬件固定的不可更改。当某个中断被触发CPU会自动跳转到对应的地址去执行代码。在C语言中我们通过interrupt 0这样的关键字声明函数编译器会自动帮我们处理跳转和返回但在汇编中你必须自己在这个地址上放置一条跳转到实际处理程序的指令。2.2 核心控制寄存器中断系统的“指挥中心”中断并非自动生效需要程序员通过配置几个特殊功能寄存器SFR来“开通热线”并设置“接警规则”。这是最核心的部分每一位都关乎中断能否正确响应。1. 中断允许寄存器 (IE – Interrupt Enable)这是个“总闸门分路开关”的组合。地址0xA8位可寻址。位符号地址功能说明7EA0xAF全局中断允许位。1 打开总闸允许所有中断0 关闭总闸禁止所有中断。任何中断要想被响应EA必须为1。6-0xAE保留位5ET20xAD定时器2中断允许位AT89C51无T2此位保留4ES0xAC串行口中断允许位。1 允许串口中断0 禁止。3ET10xAB定时器1中断允许位。1 允许T1中断0 禁止。2EX10xAA外部中断1允许位。1 允许INT1中断0 禁止。1ET00xA9定时器0中断允许位。1 允许T0中断0 禁止。0EX00xA8外部中断0允许位。1 允许INT0中断0 禁止。配置示例如果你想开启外部中断0和定时器1中断需要EA 1; EX0 1; ET1 1;。在C语言中可以直接操作位EX0 1;或给整个寄存器赋值IE 0x8B;(即10001011B)。2. 定时器/计数器控制寄存器 (TCON)这个寄存器低4位用于控制外部中断的触发方式。地址0x88位可寻址。位符号地址功能说明7TF10x8F定时器1溢出标志。硬件置1进中断后硬件清0或软件清0。6TR10x8E定时器1运行控制位。1启动0停止。5TF00x8D定时器0溢出标志。4TR00x8C定时器0运行控制位。3IE10x8B外部中断1请求标志。当INT1引脚检测到有效边沿/电平时硬件置1。进中断后硬件清0边沿触发模式。2IT10x8A外部中断1触发方式控制。1 下降沿触发0 低电平触发。1IE00x89外部中断0请求标志。0IT00x88外部中断0触发方式控制。1 下降沿触发0 低电平触发。关键选择电平触发 vs. 边沿触发低电平触发 (ITx0)只要INTx引脚为低电平就会持续产生中断请求。这就要求你的外部信号在中断服务程序执行期间必须恢复高电平否则退出中断后会立刻再次进入造成“中断重入”的假死现象。通常用于需要持续监测低电平状态且能保证及时清除的场景。下降沿触发 (ITx1)只有当INTx引脚电平从高变低的瞬间才产生一次中断请求。即使退出中断后引脚仍是低电平也不会再次触发。这是最常用、最可靠的方式特别适合按键、脉冲计数等场景。强烈建议新手默认使用下降沿触发可以避免很多意想不到的问题。3. 串行口控制寄存器 (SCON)其RI接收中断标志和TI发送中断标志共同占用串行口中断中断号4。需要注意的是进入串口中断服务程序后硬件不会自动清除RI和TI必须用软件判断并清零否则会连续不断地进入中断。4. 中断优先级寄存器 (IP – Interrupt Priority)AT89C51具有两级优先级高优先级和低优先级。通过IP寄存器地址0xB8设置。某位置1则对应中断源为高优先级清0则为低优先级。优先级规则高优先级中断可打断正在执行的低优先级中断实现中断嵌套。同优先级中断同时发生时按硬件查询顺序响应INT0 - T0 - INT1 - T1 - RI/TI。这个顺序是固定的无法更改。同优先级中断不能互相嵌套。实操心得对于大多数简单应用可以不设置IP默认全0均为低优先级让中断按自然顺序执行即可。除非有非常严格的实时性要求比如一个高速脉冲计数中断用T0必须立即响应不能被一个耗时的串口发送中断TI阻塞这时才需要将T0设为高优先级。滥用优先级会增加程序逻辑的复杂性。2.3 中断响应与处理的全过程理解CPU如何响应一个中断是调试中断程序的基础。整个过程完全由硬件自动执行但我们需要知其然中断请求某个中断源条件满足如按键按下产生下降沿对应的中断请求标志如IE0被硬件置1。中断裁决如果EA1且该中断允许位1CPU会在当前指令执行完毕后检查中断请求。现场保护CPU自动将当前程序计数器PC值压入堆栈保护断点。但不会自动保护ACC、PSW等寄存器这是新手常踩的坑。跳转执行CPU根据中断号跳转到固定的中断向量地址开始执行你的中断服务函数。中断服务执行你编写的处理代码。关键点对于边沿触发的外部中断和定时器中断硬件会自动清除中断标志IE0 TF0等对于串口中断和电平触发的外部中断必须软件清标志。中断返回执行到RETI指令时CPU从堆栈弹出断点地址到PC并恢复中断逻辑告知系统本级中断已处理完可响应新的同优先级中断然后返回主程序继续执行。注意事项中断服务函数ISR应该遵循“快进快出”原则。只做最紧急、最简单的处理比如置位一个标志、读取一个数据。复杂的计算、延时、串口打印等耗时操作应放到主循环中根据标志位来处理。长时间占用中断会导致其他中断无法及时响应系统实时性变差。3. 中断编程实战与代码详解理论说得再多不如一行代码。我们分别针对外部中断、定时器中断和串口中断给出最典型、最实用的C语言代码模板并逐行解析。3.1 外部中断应用按键消抖与单次触发按键是最常见的外部中断源。直接读取引脚电平会有抖动而在中断里加延时又是大忌。怎么办答案是“中断标记主循环处理”。#include reg51.h // 包含AT89C51寄存器定义 #define KEY_PIN P3_2 // 定义INT0引脚为按键输入 bit key_flag 0; // 定义一个位变量作为按键事件标志 /** * brief 初始化外部中断0 * param None * retval None */ void INT0_Init(void) { IT0 1; // 设置INT0为下降沿触发 (推荐方式) EX0 1; // 打开INT0中断允许 EA 1; // 打开全局中断开关 KEY_PIN 1; // 确保引脚设置为输入模式准双向口上拉 } /** * brief 外部中断0服务函数 * note interrupt 0 声明该函数对应中断号0 * note using 0 指定使用第0组寄存器组避免与主函数寄存器冲突 */ void INT0_ISR(void) interrupt 0 using 0 { // 这里不进行消抖和复杂处理仅设置标志位 key_flag 1; // 按键事件发生置位标志 // IE0 由硬件自动清零因为IT01边沿触发 } void main(void) { INT0_Init(); // 初始化中断 while(1) { if(key_flag) { // 主循环检测到按键标志 key_flag 0; // 首先清除标志防止重复处理 // 这里可以加入简单的延时消抖因为不在中断内 // 或者更优的做法记录第一次进入的时间稍后再确认 // 此处简化处理假设消抖已完成 // 执行真正的按键处理逻辑如LED翻转、数值增加等 P1 ~P1; // 示例P1口LED取反 // 此处可以执行任何耗时操作不影响中断响应 } // 主循环可以执行其他任务 // ... } }代码解析与避坑点using 0这个关键字指定中断服务函数使用第0组工作寄存器R0-R7而主程序默认使用第0组。这样切换时编译器会自动保护现场避免主程序的R0-R7被破坏。对于简单中断可以省略编译器会生成保护ACC、B、DPTR等寄存器的代码但使用using可以生成更高效的代码。消抖策略中断中只设标志消抖放在主循环。更精确的消抖可以在主循环中检测到标志后延时10-20ms再次读取引脚状态确认。标志位清零时机一定要在主循环中开始处理事件前清零标志而不是在中断末尾。这样可以确保即使主循环处理时间较长期间再次发生的中断也能被记录标志再次置1不会丢失事件。3.2 定时器中断应用精准定时与软件计数器定时器中断是产生精确时间基准的利器。我们配置定时器0工作在模式116位定时器实现50ms定时并以此为基础构建一个1秒的软件计数器。#include reg51.h #include intrins.h // 可能需要_nop_() unsigned int T0_Count 0; // T0中断次数计数器 bit Sec_Flag 0; // 1秒到达标志 /** * brief 初始化定时器050ms定时 * param None * retval None * note 假设晶振为11.0592MHz机器周期为12/11.0592MHz ≈ 1.085us */ void Timer0_Init(void) { TMOD 0xF0; // 高4位不变低4位清零设置T0模式 TMOD | 0x01; // 设置T0为模式116位定时器 // 计算50ms需要的计数值 // 1个机器周期时间 T 12 / 11.0592MHz ≈ 1.085us // 计次数 N 50000us / 1.085us ≈ 46080 // 初值 X 65536 - 46080 19456 0x4C00 TH0 0x4C; // 装入初值高8位 TL0 0x00; // 装入初值低8位 ET0 1; // 允许T0中断 TR0 1; // 启动T0 EA 1; // 开总中断 } /** * brief 定时器0中断服务函数 */ void Timer0_ISR(void) interrupt 1 using 1 { // 使用第1组寄存器 // 重装初值模式1下需软件重装 TH0 0x4C; TL0 0x00; T0_Count; // 中断次数加1 if(T0_Count 20) { // 50ms * 20 1000ms 1s T0_Count 0; Sec_Flag 1; // 置位1秒标志 } } void main(void) { Timer0_Init(); while(1) { if(Sec_Flag) { Sec_Flag 0; // 每秒执行一次的任务如刷新数码管显示、读取传感器等 // 例如P1口数值递增 P1; } // 主循环其他任务 } }关键计算与模式选择定时器初值计算这是定时器的核心。公式为初值 65536 - (所需时间(us) / 机器周期(us))。模式1是16位非自动重载最大定时约71ms12MHz晶振。如果需要更长定时就必须像示例一样配合软件计数器。模式1 vs 模式2模式116位需软件重装初值在重装指令执行前定时器从0开始计数会引入少量误差。模式28位自动重载精度高但最大定时时间短约256个机器周期。对于精准定时模式2是更好的选择但需要更频繁的中断。中断服务函数效率Timer0_ISR中的代码执行时间会叠加到定时周期中。如果中断服务程序执行时间过长会导致实际定时周期变长。因此中断内的代码必须极其精简。3.3 串口中断应用高效数据收发串口通信中使用中断方式接收数据可以解放CPU避免轮询等待。下面是一个中断接收、主循环处理的经典框架。#include reg51.h #define BUF_SIZE 32 unsigned char UART_Rx_Buf[BUF_SIZE]; // 接收缓冲区 unsigned char UART_Rx_Index 0; // 缓冲区写入索引 bit UART_Rx_Ready 0; // 一帧数据接收完成标志 /** * brief 初始化串口波特率96008N1中断方式接收 * param None * retval None * note 晶振11.0592MHz定时器1模式2做波特率发生器 */ void UART_Init(void) { // 设置定时器1为模式28位自动重载 TMOD 0x0F; // 高4位清零设置T1 TMOD | 0x20; // T1模式2 // 计算TH1重载值 for 9600 bps // 波特率 (2^SMOD / 32) * (Fosc / (12 * (256 - TH1))) // 设SMOD0 则 TH1 256 - Fosc/(波特率*12*32) // TH1 256 - 11059200/(9600*12*32) 256 - 3 253 0xFD TH1 0xFD; // 重载值 TL1 0xFD; // 初始值 TR1 1; // 启动定时器1 // 设置串口模式18位UART可变波特率允许接收 SCON 0x50; // 0101 0000: 模式1 REN1允许接收 // 开中断 ES 1; // 允许串口中断 EA 1; // 开总中断 } /** * brief 串口中断服务函数 */ void UART_ISR(void) interrupt 4 using 2 { // 使用第2组寄存器 if(RI) { // 如果是接收中断 RI 0; // 必须软件清零接收标志 UART_Rx_Buf[UART_Rx_Index] SBUF; // 读取数据到缓冲区 // 简单的帧判断以回车符\r0x0D作为一帧结束 if(SBUF \r || UART_Rx_Index (BUF_SIZE-1)) { UART_Rx_Buf[UART_Rx_Index] \0; // 添加字符串结束符 UART_Rx_Ready 1; // 设置数据就绪标志 UART_Rx_Index 0; // 重置索引 } else { UART_Rx_Index; // 索引递增准备接收下一个字节 } } if(TI) { // 如果是发送中断 TI 0; // 必须软件清零发送标志 // 通常在这里可以设置一个发送完成标志或者加载下一个要发送的字节 // 本例中发送采用查询方式故发送中断部分不做事 } } /** * brief 通过串口发送一个字符串查询方式非中断 */ void UART_SendString(char *str) { while(*str ! \0) { SBUF *str; // 发送一个字符 while(!TI); // 等待发送完成 TI 0; // 清零发送标志 } } void main(void) { UART_Init(); UART_SendString(System Ready.\r\n); while(1) { if(UART_Rx_Ready) { UART_Rx_Ready 0; // 处理接收到的数据例如回显 UART_SendString(Rx: ); UART_SendString(UART_Rx_Buf); UART_SendString(\r\n); } // 主循环其他任务 } }串口中断核心要点双标志位串口中断只有一个中断入口4号但中断源有两个RI和TI。进入中断后必须首先检查是RI还是TI触发的并立即用软件清除对应的标志位否则会连续进入中断。缓冲区机制中断服务函数中只做最快的数据搬运从SBUF到缓冲区和简单帧判断。复杂的协议解析如Modbus应放在主循环中根据UART_Rx_Ready标志进行。发送方式选择示例中发送使用了查询等待while(!TI);这在发送数据量不大时是简单的。如果发送数据量大或要求高效率也应采用中断发送即设置一个发送缓冲区在TI中断中连续发送。4. 综合实验中断驱动的智能按键计数器现在我们将外部中断、定时器中断结合起来做一个有实用价值的综合实验一个智能按键计数器。要求功能通过两个按键控制一个两位数码管的数字增减INT0加INT1减且按键具有连加/连减功能长按超过500ms后数值开始快速连续变化。设计思路使用外部中断0和1检测按键动作下降沿触发。使用定时器0产生10ms的时基用于数码管动态扫描和按键长按计时。中断服务程序只负责设置标志位和简单计时。所有逻辑判断消抖、长按判断、连加速度控制、数值边界处理都在主循环中完成。4.1 硬件连接与软件架构假设连接如下P0口接共阳数码管段选经限流电阻。P2.0, P2.1接数码管位选控制十位和个位。P3.2 (INT0) 接“加”按键P3.3 (INT1) 接“减”按键按键另一端接地。使用11.0592MHz晶振。软件架构采用“前台后台”系统前台中断快速响应事件按键按下、定时到时设置标志。后台主循环轮询标志执行耗时的状态机、显示刷新、业务逻辑。4.2 核心代码实现#include reg51.h #include intrins.h // 定义按键引脚 sbit KEY_ADD P3^2; // 加键INT0 sbit KEY_SUB P3^3; // 减键INT1 // 定义数码管位选 sbit DIG_TEN P2^0; // 十位 sbit DIG_ONE P2^1; // 个位 // 共阳数码管0-9段码表 (a~dp, 0点亮) unsigned char code SEG_CODE[10] {0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90}; // 全局变量 unsigned char Count 0; // 计数值 0-99 bit flag_10ms 0; // 10ms定时标志 bit flag_key_add 0; // 加键按下标志 bit flag_key_sub 0; // 减键按下标志 unsigned int key_add_press_time 0; // 加键按下持续时间单位10ms unsigned int key_sub_press_time 0; // 减键按下持续时间 /** * brief 初始化函数 */ void Sys_Init(void) { // 初始化IO口 P0 0xFF; // 段选关闭 P2 | 0x03; // 位选关闭 (高电平关闭共阳数码管) KEY_ADD 1; // 使能内部上拉 KEY_SUB 1; // 初始化外部中断 IT0 1; IT1 1; // 下降沿触发 EX0 1; EX1 1; // 允许外部中断 // 初始化定时器0为10ms中断 TMOD 0xF0; TMOD | 0x01; // 模式1 // 10ms 11.0592MHz: 初值 65536 - 10000/1.085 ≈ 65536 - 9216 56320 0xDC00 TH0 0xDC; TL0 0x00; ET0 1; // 允许T0中断 TR0 1; // 启动T0 EA 1; // 开总中断 } /** * brief 外部中断0服务函数加键 */ void INT0_ISR(void) interrupt 0 { flag_key_add 1; // 简单设置标志 } /** * brief 外部中断1服务函数减键 */ void INT1_ISR(void) interrupt 2 { flag_key_sub 1; // 简单设置标志 } /** * brief 定时器0中断服务函数10ms定时 */ void Timer0_ISR(void) interrupt 1 { // 重装初值 TH0 0xDC; TL0 0x00; flag_10ms 1; // 设置10ms标志 // 如果按键标志仍为1说明按键可能被按住增加按下时间计数 if(!KEY_ADD) { // 实际检测引脚仍为低电平消抖后确认 key_add_press_time; } else { key_add_press_time 0; } if(!KEY_SUB) { key_sub_press_time; } else { key_sub_press_time 0; } } /** * brief 数码管显示函数在10ms中断周期内调用实现动态扫描 */ void Display(void) { static unsigned char digit 0; // 当前显示位 if(digit 0) { // 显示十位 P0 SEG_CODE[Count / 10]; DIG_ONE 1; // 关闭个位 DIG_TEN 0; // 打开十位 digit 1; } else { // 显示个位 P0 SEG_CODE[Count % 10]; DIG_TEN 1; // 关闭十位 DIG_ONE 0; // 打开个位 digit 0; } } /** * brief 按键处理函数状态机在主循环中调用 */ void Key_Process(void) { static unsigned char add_step 1; // 加键连加速度 static unsigned char sub_step 1; // 减键连加速度 // 处理加键 if(flag_key_add) { flag_key_add 0; // 清除中断标志 // 简单延时消抖实际项目中可用更精确的状态机 _nop_();_nop_();_nop_();_nop_();_nop_(); if(!KEY_ADD) { // 确认按下 key_add_press_time 0; // 开始计时已在中断中进行 // 首次按下立即加1 if(Count 99) Count; } } // 处理减键 if(flag_key_sub) { flag_key_sub 0; _nop_();_nop_();_nop_();_nop_();_nop_(); if(!KEY_SUB) { key_sub_press_time 0; if(Count 0) Count--; } } // 长按连加处理每50ms加速一次 if(key_add_press_time 50) { // 长按超过500ms if((key_add_press_time % 5) 0) { // 每50ms执行一次 if(Count 99) Count; } } // 长按连减处理 if(key_sub_press_time 50) { if((key_sub_press_time % 5) 0) { if(Count 0) Count--; } } } void main(void) { Sys_Init(); while(1) { if(flag_10ms) { flag_10ms 0; Display(); // 每10ms刷新一次显示 Key_Process(); // 每10ms处理一次按键逻辑 } // 这里可以添加其他低优先级任务 } }4.3 实验总结与思维提升这个综合实验虽然不大但完整展示了多中断协同工作的经典范式中断分工明确外部中断负责“感知”事件定时器中断提供“心跳”时基。各司其职互不干扰。标志位通信中断与主循环之间通过标志位进行异步通信这是单片机前后台系统的精髓。耗时逻辑外移消抖、长按判断、连加速度控制这些需要多次判断和计时的逻辑全部放在主循环中基于flag_10ms标志处理保证了中断服务函数的简洁。资源冲突管理Count变量在中断可能修改和主循环显示读取中都被访问但在这个例子中中断只设置标志主循环修改Count因此不存在冲突。如果中断中也修改变量就需要考虑使用volatile关键字或关中断进行保护。通过这个实验你应该能深刻体会到中断不是用来执行复杂任务的而是用来“通知”和“计时”的。把正确的逻辑放在正确的位置是写出稳定、高效中断程序的关键。5. 中断系统调试与常见问题实录即使理解了原理实际调试中断程序时还是会遇到各种“坑”。下面是我多年调试中总结的一些典型问题和解决方法。5.1 中断根本不响应这是最让人头疼的问题。请按以下清单逐项排查全局中断开关EA是否打开这是最容易被忽略的一点。EA 1;必须执行。对应中断的允许位是否打开例如用了INT0EX01了吗中断触发条件是否满足外部中断检查硬件连接引脚是否真的产生了下降沿或低电平用示波器或逻辑分析仪看最直观。检查ITx位设置是否正确。定时器中断TRx启动了吗定时器初值计算对吗是否真的溢出了可以在中断函数里翻转一个IO口用示波器看是否有波形。串口中断波特率设置对吗数据真的发过来了吗REN1允许接收了吗中断向量地址是否正确C语言中函数声明interrupt x的x值必须与中断号严格对应。INT0是0T0是1以此类推。编译器/烧录器设置问题有些旧的IDE或烧录软件需要勾选“生成中断向量”或类似选项。检查编译后的.map或.lst文件看中断函数是否被正确链接到了向量地址。5.2 中断只响应一次之后“卡死”中断标志未清除这是最常见的原因对于串口中断RI和TI必须在中断服务函数内用软件清零。对于电平触发的外部中断如果在中断返回前外部引脚仍然是低电平中断标志IEx会再次置位导致不断进入中断。应在中断内等待电平变高或改为下降沿触发。对于某些单片机有些型号的中断标志清除方式比较特殊需查阅数据手册。中断服务函数执行时间过长导致其他中断包括自身的中断请求无法得到及时响应。优化中断服务函数只做必要操作。堆栈溢出如果中断嵌套太深或中断函数内局部变量太多可能导致堆栈溢出破坏程序执行。减少中断嵌套慎用大型局部变量。5.3 中断处理的数据出错或不稳定共享变量未保护如果主循环和中断都读写同一个全局变量如上面的Count可能会因为读写不同步而出错。简单方法在主循环中读写该变量前暂时关闭中断EA0;操作完再打开EA1;。但会牺牲实时性。推荐方法设计成“中断只写标志主循环读写数据”的模式如我们的实验所示。或者确保变量是“原子访问”的如8位单片机上的8位变量。使用了非可重入函数如果在中断和主循环中都调用了同一个函数而这个函数使用了静态局部变量或全局变量可能会产生冲突。避免在中断中调用复杂库函数。5.4 中断嵌套与优先级混乱意料之外的中断嵌套如果打开了多个中断且没有设置优先级它们之间的响应顺序是固定的见2.2节。一个低优先级的中断服务函数可能被高优先级中断打断如果它们共享资源如一个缓冲区就需要特别小心。解决方案对于共享资源在访问它的所有中断服务函数和主循环代码段中进行统一的关中断保护。或者精心设计数据流避免共享。调试王牌技巧IO口模拟示波器。当你没有专业工具时可以在中断服务函数的开头和结尾分别执行P1.0 1;和P1.0 0;。然后用一个LED或万用表测量P1.0的电压。如果LED微亮或电压介于0-5V之间说明中断正在以一定频率执行。你还可以通过测量高电平的持续时间来估算中断服务函数的执行时间。这是最原始但极其有效的调试手段。中断系统的调试是一个从“系统架构”到“代码细节”再到“硬件信号”的完整闭环。耐心地按照以上清单排查大部分问题都能迎刃而解。记住中断是让单片机“活”起来的关键理解它、驾驭它你才能真正踏入嵌入式实时系统的大门。

相关新闻