第三十二章 详解中断系统

1. 导入

中断让单片机“被动响应”外部/内部事件,无需主循环轮询即可在恰当时刻处理计时、按键、串口收发等任务。本章系统梳理 8051/8052 的中断源、向量与优先级、触发方式与标志位清除规则,并给出可靠的代码范式:毫秒节拍、外部按键(边沿/电平)、串口收发缓冲,以及中断嵌套与关键区保护。


2. 中断源、向量与寄存器

  • 向量表(代码空间地址,C 编译器自动生成 RETI):
    • 0x0003INT0 外部中断0(P3.2)
    • 0x000BT0 定时器0溢出
    • 0x0013INT1 外部中断1(P3.3)
    • 0x001BT1 定时器1溢出
    • 0x0023:串口(RI/TI)
    • 0x002BT2 定时器2(仅 8052/增强型)
  • 使能/优先级寄存器(经典 8051):
    • IEEA 总开关;EX0 ET0 EX1 ET1 ES (ET2) 各源使能
    • IPPX0 PT0 PX1 PT1 PS (PT2) 各源优先级(1=高,0=低)
    • 增强型 51(如 STC)常有 IPH 扩展更多优先级级别,具体查手册
  • 触发与标志寄存器:
    • TCONTF1 TR1 TF0 TR0 IE1 IT1 IE0 IT0
      • IT0/IT1:外部中断触发方式(1=边沿触发下降沿,0=电平触发低电平)
      • IE0/IE1:外部中断请求标志(由硬件置位)
      • TF0/TF1:定时器溢出标志(硬件置位)
    • SCON(串口):SM0 SM1 SM2 REN TB8 RB8 TI RI

3. 触发与清除规则(高频考点)

  • 外部中断 INT0/1
    • ITx=1(下降沿触发):触发后硬件自动清 IEx,对抖动不敏感
    • ITx=0(低电平触发):只要引脚保持低就持续请求;退出前必须确保引脚回到高,否则会“连发”
  • 定时器 TF0/TF1
    • 进入中断时硬件自动清除 TFx
  • 串口 RI/TI
    • 不会自动清除,必须在 ISR 中由软件清零对应标志位(通常按需分别判断)

4. 中断优先级与嵌套

  • 两级优先系统(标准 8051):
    • 高优先级可打断低优先级 ISR
    • 同优先级之间不互相打断,响应顺序按内部轮询顺序:INT0 > T0 > INT1 > T1 > 串口 (> T2)
  • 嵌套注意:
    • 不建议在 C 里人为 EA=1 以允许同级互相嵌套,容易失控
    • 如需更深层次控制,使用多级优先(带 IPH 的增强型)或在高优先级 ISR 内短小处理、快速返回

5. 代码范式与完整示例

5.1 毫秒系统节拍(T0 1ms)+ 软定时

#include <reg52.h>

/* 1ms 节拍 @11.0592MHz:TH0/TL0=0xFC66 */
volatile unsigned long sys_ms = 0;

void t0_init_1ms(void){
    TMOD = (TMOD & 0xF0) | 0x01;   // T0 方式1
    TH0 = 0xFC; TL0 = 0x66;
    ET0 = 1; EA = 1; TR0 = 1;
}

void t0_isr(void) interrupt 1 {    // 向量 0x000B
    TH0 = 0xFC; TL0 = 0x66;
    sys_ms++;
}

/* 非阻塞延时:读取 sys_ms 轮询 */
void delay_ms_nb(unsigned int ms){
    unsigned long t0 = sys_ms;
    while ((unsigned long)(sys_ms - t0) < ms) { /* 允许主循环做其它事 */ }
}

要点:

  • ISR 极短,仅维护节拍变量;避免在 ISR 内 printf/长延时
  • 与主循环通信的变量用 volatile,防止优化器错误假定

5.2 外部中断按键(下降沿触发,硬消抖/软消抖)

#include <reg52.h>

sbit KEY_PIN = P3^2;     // INT0 脚
volatile bit key_event = 0;

void int0_init_falling(void){
    IT0 = 1;      // 下降沿触发
    EX0 = 1; EA = 1;
}

void ex0_isr(void) interrupt 0 {   // 向量 0x0003
    // 这里不做复杂逻辑,只置事件标志
    key_event = 1;
}

/* 主循环中消抖+处理 */
void key_task(void){
    if(key_event){
        key_event = 0;
        // 简单消抖:延时再读脚位(或用时间戳法更优)
        {
            unsigned int i,j; for(i=0;i<5;i++) for(j=0;j<125;j++);
        }
        if(KEY_PIN == 1){ // 下降沿后已回到高,确认一次有效按键
            // TODO: 执行按键事件
        }
    }
}

若必须用电平触发(IT0=0),务必确保 ISR 结束前把输入拉回高电平(硬件/上拉/释放),否则会“黏住”反复进入。

5.3 串口中断收发(环形缓冲)——可靠不丢字

#include <reg52.h>

#define RX_BUF_SZ 32
volatile unsigned char rx_buf[RX_BUF_SZ];
volatile unsigned char rx_head=0, rx_tail=0;

void uart_init_9600(void){
    TMOD |= 0x20; TH1=0xFD; TL1=0xFD; TR1=1;
    SCON = 0x50;          // 模式1,8N1,REN=1
    ES = 1; EA = 1;
}

void uart_isr(void) interrupt 4 {  // 向量 0x0023
    if(RI){    // 接收中断
        unsigned char nxt = (rx_head + 1) % RX_BUF_SZ;
        unsigned char c = SBUF;
        if(nxt != rx_tail){ rx_buf[rx_head] = c; rx_head = nxt; } // 不满则收
        RI = 0;  // 必须手动清
    }
    if(TI){    // 发送完成(若用阻塞发送可忽略)
        TI = 0; // 清发送标志
    }
}

bit uart_getc_nblk(unsigned char* pc){
    if(rx_head == rx_tail) return 0;
    *pc = rx_buf[rx_tail];
    rx_tail = (rx_tail + 1) % RX_BUF_SZ;
    return 1;
}

要点:

  • RI/TI 必须由软件清零
  • 在 ISR 内不做打印,仅搬运数据到缓冲区
  • 若需要发送中断驱动,可在 TI=1 时喂下一个字节

5.4 中断优先级设置与“关键区”保护

void intr_priority_setup(void){
    // 设定高/低优先级:让串口(PS)高于T0(PT0),T0高于外部INT0(PX0)
    IP = 0x10 /*PS*/ | 0x02 /*PT0*/ | 0x00 /*PX0*/;
    // 若为增强型带 IPH,可进一步细化级别
}

/* 关键区保护:保存并恢复 EA */
void critical_section_example(void){
    bit ea_save = EA;
    EA = 0;              // 关闭全局中断
    // ... 修改与ISR共享的多字节变量 ...
    EA = ea_save;        // 恢复
}

说明:

  • 两级优先:高优先能打断低优先 ISR;同级不嵌套
  • 不建议长时间关闭 EA;保护窗口尽量小

5.5 使用备用寄存器组降低 ISR 进入/退出开销(Keil C51)

// 使用寄存器组1,可减少R0~R7保护开销与延迟
void ex0_isr(void) interrupt 0 using 1 {
    // ...
}

注意:选择的 using 组需与主程序错开,避免抢占。


6. 定时器为中断源的常见配置

  • T0/T1 方式选择(TMOD):
    • 方式0:13位;方式1:16位常用;方式2:8位自动重装;方式3:拆分
  • 自动重装(方式2)做固定速率:
/* T1 方式2自动重装,波特率/方波等稳定周期用途 */
void t1_mode2_init(unsigned char reload){
    TMOD = (TMOD & 0x0F) | 0x20;
    TH1 = reload; TL1 = reload;
    ET1 = 1; EA = 1; TR1 = 1;
}
  • 8052 的 T2(捕获/自动重装):
    • 具备独立向量 0x002B 与标志 TF2,寄存器 T2CON/RCAP2H/L
    • 具体位定义不同于 T0/T1,参考芯片手册

7. 实战:把“毫秒节拍 + 按键 + 串口”整合成事件驱动

#include <reg52.h>

sbit LED = P1^0;
volatile unsigned long sys_ms = 0;
volatile bit key_event = 0;

/* --- T0 1ms --- */
void t0_init_1ms(void){
    TMOD = (TMOD & 0xF0) | 0x01;
    TH0 = 0xFC; TL0 = 0x66;
    ET0 = 1; EA = 1; TR0 = 1;
}
void t0_isr(void) interrupt 1 { TH0=0xFC; TL0=0x66; sys_ms++; }

/* --- INT0 下降沿 --- */
void int0_init(void){ IT0=1; EX0=1; EA=1; }
void ex0_isr(void) interrupt 0 { key_event = 1; }

/* --- UART 9600 RX 缓冲 --- */
#define RXN 32
volatile unsigned char rbuf[RXN], rh=0, rt=0;
void uart_init(void){
    TMOD |= 0x20; TH1=0xFD; TL1=0xFD; TR1=1;
    SCON=0x50; ES=1; EA=1;
}
void uart_isr(void) interrupt 4 {
    if(RI){ unsigned char n=(rh+1)%RXN, c=SBUF; if(n!=rt){ rbuf[rh]=c; rh=n; } RI=0; }
    if(TI){ TI=0; }
}
bit uart_getc(unsigned char* c){ if(rh==rt) return 0; *c=rbuf[rt]; rt=(rt+1)%RXN; return 1; }

/* --- 主循环 --- */
void main(void){
    unsigned long t = 0;
    LED = 1;
    t0_init_1ms();
    int0_init();
    uart_init();

    while(1){
        // 500ms 心跳
        if(sys_ms - t >= 500){ t += 500; LED = !LED; }

        // 按键处理(中断置位 + 主循环消抖)
        if(key_event){
            key_event = 0;
            // 简单消抖
            { unsigned int i,j; for(i=0;i<10;i++) for(j=0;j<125;j++); }
            if(P3^2){ /* 确认按键有效 */ /* TODO */ }
        }

        // 串口处理
        { unsigned char c;
          if(uart_getc(&c)){
              // 简单回显
              SBUF = c; while(!TI); TI=0;
          }
        }
    }
}

要点回顾:

  • ISR 只做“放旗子/搬数据”
  • 主循环消费事件、做相对耗时操作(打印、运算)
  • 变量跨 ISR/主循环需 volatile;多字节访问用“关键区保护”

8. 性能、栈与时序

  • 中断延迟与入口开销
    • 与指令边界、是否使用寄存器组、编译器保存现场策略有关
    • 需要极限实时性时,可将关键 ISR 设为高优先级 + using 专用寄存器组,并保持极短
  • 栈深度
    • 8051 栈在内部 RAM(默认从 SP=0x07 向上增长),每次中断压栈 PC(2B)等
    • 避免深层函数调用/可重入函数在 ISR 中使用;必要时调整 SP 起点或使用 Keil reentrant 谨慎管理
  • 结束指令
    • ISR 必须以 RETI 返回(C 编译器自动生成);不要自行 RET

9. 常见问题与排查

  • 外部中断“连发/黏住”
    • 使用电平触发但引脚未回高;改用边沿触发或保证释放;检查上拉/消抖
  • 串口收不全/丢字
    • 未在 ISR 清 RI;ISR 做了耗时操作;缓冲过小;波特率误差大
  • 定时不准/抖动
    • 装载值不对;ISR 被长时间屏蔽;频繁在 ISR 内关中断
  • 系统偶发跑飞
    • 栈溢出(ISR 嵌套过多/递归);ISR 使用了非可重入库函数(如 printf
  • 关键区冲突
    • 主循环与 ISR 同时改同一多字节变量;用 EA 保护并尽量缩短临界区

10. 小结

  • 掌握 8051 中断的“源—向量—寄存器—清除规则—优先级”全链路
  • 用范式代码实现:毫秒节拍、外部按键中断、串口 RX 环形缓冲
  • 避免在 ISR 内做重活;用事件标志+主循环处理
  • 需要更高实时性与更友好优先级,选用带 IPH/多级优先的增强型 51,并合理规划 ISR 使用寄存器组

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐