1. 中断系统概述:中断的概念、作用与典型应用场景

各位同学,咱们今天聊聊中断系统。说实话,中断这玩意儿,是嵌入式系统的灵魂之一。没有中断,你的游戏机就是个傻转的循环,按键按下去半天没反应,画面卡成PPT。我刚开始做嵌入式那会儿,对中断的理解也就停留在“打断CPU干活”这个层面,直到亲手调一个手柄响应程序,才真正体会到它的精妙。

1.1 中断到底是什么?

中断,说白了就是CPU正在干一件事,突然被一个外部或内部信号叫停,转去处理更紧急的事,处理完再回来接着干。你想想看,就像你正在写代码,突然电话响了——你放下键盘接电话,讲完再继续写。这个“电话铃声”就是中断信号。

在嵌入式游戏机里,CPU通常是个单核的微控制器,没法同时干多件事。但游戏机需要同时处理按键、刷新屏幕、播放声音、传输数据……怎么办?中断就是解决方案。它让CPU能“分时”响应各种事件,而且响应速度极快。

核心概念:中断向量表、中断优先级、中断嵌套、中断屏蔽。这四个词你记牢了,后面所有内容都围绕它们展开。

1.2 中断的作用——为什么非用不可?

我见过一些新手,喜欢用轮询(polling)来处理按键——主循环里不断读IO口。这在简单场景下没问题,但一旦系统复杂起来,轮询的弊端就暴露了:

  • 实时性差:CPU得一直跑循环,按键按下去可能要等几十毫秒才能响应。玩游戏时这几十毫秒就是生与死的区别。
  • 浪费CPU资源:大部分时间IO口没变化,CPU却在空转。你想想看,这就像你一直盯着门口等快递,啥事也干不了。
  • 难以处理多事件:同时有按键、定时器、串口数据到来,轮询根本忙不过来。

中断正好解决这些问题。它让CPU只在事件发生时去处理,平时该干嘛干嘛。我在做一款掌机时,用中断处理按键和屏幕刷新,CPU利用率从轮询时的85%降到了30%——剩下的算力全用来跑游戏逻辑了。

我的经验:中断不是越多越好。每个中断都有入栈出栈的开销,中断太频繁反而会拖慢系统。我曾经在一个项目里开了8个中断,结果系统响应反而变慢了——中断嵌套把栈空间撑爆了。后来我合并了几个中断源,用标志位区分,问题就解决了。

1.3 嵌入式游戏机中的典型应用场景

咱们直接上干货。游戏机里中断最常见的三个场景:

1.3.1 按键响应——最直观的中断

按键按下时,GPIO电平变化,触发外部中断。CPU立即暂停当前任务,读取按键值,存入缓冲区,然后恢复执行。整个过程也就几微秒。

我曾经踩过一个坑:按键消抖。机械按键按下时会有抖动,电平会跳变好几次。如果每个跳变都触发中断,那CPU就光处理按键了。我的做法是:在中断服务函数里启动一个10ms的定时器,等定时器溢出后再读一次按键值——这叫“延迟确认法”。

// 按键中断服务函数(简化版)
void EXTI0_IRQHandler(void) {
    if(EXTI_GetITStatus(EXTI_Line0) != RESET) {
        // 启动10ms定时器用于消抖
        TIM_Cmd(TIM2, ENABLE);
        TIM_SetCounter(TIM2, 0);
        EXTI_ClearITPendingBit(EXTI_Line0);
    }
}

// 定时器溢出中断——确认按键
void TIM2_IRQHandler(void) {
    if(TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) {
        uint8_t key_val = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0);
        if(key_val == PRESSED) {
            key_buffer[key_index++] = key_val;  // 存入缓冲区
        }
        TIM_Cmd(TIM2, DISABLE);
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
    }
}

注意:中断服务函数里千万别做耗时操作!比如printf、延时、复杂计算。我见过有人直接在中断里写LCD刷屏,结果画面撕裂,系统还频繁死机。中断服务函数的原则是:快进快出,只做最必要的事。

1.3.2 定时器刷新——游戏的心脏

游戏画面需要以固定帧率刷新,比如60fps。这靠主循环的delay是做不到的——delay会阻塞CPU。正确做法是用定时器中断:

  • 配置一个定时器,每16.67ms(60fps)产生一次中断
  • 中断里设置一个“帧同步标志”
  • 主循环检测到标志后,执行画面渲染

这样CPU在两次中断之间可以安心跑游戏逻辑,画面刷新由定时器精确控制。我做的第一款掌机,就是用TIM3做帧同步,效果非常稳定。

// 帧同步定时器配置(STM32示例)
void FrameTimer_Init(void) {
    TIM_TimeBaseInitTypeDef TIM_InitStruct;
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
    
    TIM_InitStruct.TIM_Prescaler = 7200 - 1;      // 72MHz / 7200 = 10kHz
    TIM_InitStruct.TIM_Period = 167 - 1;           // 10kHz / 167 ≈ 60Hz
    TIM_InitStruct.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM3, &TIM_InitStruct);
    
    TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);
    TIM_Cmd(TIM3, ENABLE);
}

// 帧同步中断
void TIM3_IRQHandler(void) {
    if(TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) {
        frame_ready = 1;  // 通知主循环:可以渲染下一帧了
        TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
    }
}

1.3.3 DMA传输——解放CPU的利器

DMA(直接存储器访问)本身不是中断,但DMA传输完成时会触发中断。这个组合非常强大:

  • CPU启动DMA传输后,可以继续干别的事
  • DMA在后台搬运数据(比如从内存到LCD显存)
  • 传输完成后,DMA中断通知CPU:“数据搬完了,你可以用了”

我在做游戏机时,用DMA+中断的方式传输音频数据。CPU只需要把音频数据准备好,启动DMA,然后就去处理游戏逻辑。DMA传输完一帧音频后触发中断,CPU再填充下一帧数据。这样音频播放几乎不占用CPU时间。

关键点:DMA中断里通常做两件事:1)重新配置DMA传输下一批数据;2)更新缓冲区指针。千万别在DMA中断里做数据处理——那是主循环的事。

1.4 中断优先级——谁更紧急?

游戏机里中断有优先级之分。比如:

中断源 优先级 原因
定时器帧同步 最高 画面刷新不能等,否则会撕裂
DMA传输完成 音频数据必须连续,否则有杂音
按键中断 按键可以稍微延迟,但不能丢
串口接收 调试信息可以等

优先级设置不当会出问题。我遇到过:按键中断优先级设得比定时器还高,结果每次按键都打断画面刷新,导致屏幕闪烁。后来我把定时器优先级提到最高,问题就解决了。

避坑指南:我曾经把两个中断的优先级设成一样,结果它们同时触发时,系统随机响应其中一个——这会导致不可预测的行为。记住:同优先级的中断,硬件会按向量表顺序响应,但千万别依赖这个顺序。明确设置优先级才是正道。

1.5 小结

中断系统是嵌入式游戏机的基石。它让CPU能高效地处理多任务,同时保证实时性。咱们这节课只是开了个头,后面会深入讲中断向量表、中断嵌套、中断延迟优化等硬核内容。

嗯,最后说一句:学中断最好的方法就是动手写代码。找个开发板,配一个外部中断,再配一个定时器中断,让它们互相配合——你会发现,中断的世界远比想象中有趣。

下一节,咱们聊聊中断向量表的具体实现,以及如何用C语言编写高效的中断服务函数。