中断机制(硬件视角) Interrupt Hardware
轮询(Polling)让 CPU 不停地问"按键按下没、串口收到没、定时器到点没",既浪费周期又增加延迟。中断(Interrupt) 让外设在需要时主动打断 CPU:硬件自动保存现场、从向量表取出服务程序入口、跳过去执行,返回时再恢复现场。整个过程在几十个时钟内完成,而且全是硬件行为,不依赖软件调度。
硬件工程师关心中断,不是因为它"高级",而是因为它的时间特性会直接决定系统能不能达标:中断延迟是多少、关中断 10µs 会让控制环抖动多少、60µs 的 ISR 在 100µs 周期里还剩多少时间给主循环、多级嵌套的最坏响应时间会不会超过 deadline、中断里调用 printf 为什么会把整块板拖死。本章把这些问题变成可计算的周期数与时序,并给出可复算的算例。
本章与GPIO 原理与驱动能力衔接:GPIO 的边沿中断要先同步、再去抖;与最小系统衔接:调试口和复位决定你能不能看到中断现场;与定时器与 PWM 原理衔接:定时器中断是控制环的节拍源;与MCU 概览衔接:NVIC 的优先级位数与内核型号有关。异步输入的亚稳态与同步器的完整推导见数字系统时钟与复位设计。
1. 设计动机
现场一:100µs 控制中断,ISR 写了 60µs。 单看"60µs < 100µs"觉得没问题,但 CPU 已经被吃掉 60%;再加上 1ms 的通信中断 20µs、500µs 的 CAN 中断 10µs,总占用 64%,主循环只剩 36µs/100µs。主循环里还要做显示、协议解析、参数存储,一旦某个任务需要 50% CPU,系统就开始丢 deadline。
现场二:关中断 10µs 保护一个共享变量。 控制中断每 100µs 来一次,最坏情况下它刚好在关中断期间到达,要等 10µs 才能响应;中断延迟从 0.4µs 变成 10.4µs,抖动放大 26 倍。控制环的采样时刻因此漂移,电机电流出现低频纹波。
现场三:串口中断里调用 printf。 printf 内部可能要几百微秒到几毫秒,还不可重入;高优先级中断被它挡住,看门狗喂狗被延迟,系统随机复位。工程师以为是"串口丢数据",实际是 ISR 太长。
现场四:按键边沿中断不消抖。 机械按键抖动 5~20ms,边沿中断把每个毛刺都当成一次按下;一次按下进 20 次中断,状态机来回跳。边沿中断只告诉你有变化,不告诉你变化稳定了没有。
| 你遇到的问题 | 先查 | 判据(本手册建议值,非标准强制) |
|---|---|---|
| 控制环抖动、采样时刻漂移 | 关中断时长、ISR 时长、优先级 | 最坏中断延迟 < 周期的 10% |
| 主循环饿死、通信丢包 | 各 ISR 占用率之和 | CPU 总占用 ≤70%,主循环有确定余量 |
| 高优先级中断被低优先级挡住 | 优先级分组、临界区、不可抢占段 | 高优先级可抢占,临界区 <10µs |
| 中断进一次出不来 | 标志是否清零、是否重复触发 | 退出前清挂起/外设标志 |
| 按键/外部信号误触发 | 同步、去抖、边沿/电平触发 | 两级同步 + ≥20ms 确认窗口 |
| 中断嵌套后死机 | 栈深度、FPU 压栈、栈溢出 | 栈高水位 <80%,嵌套层数有上限 |
不算会摔在哪,三个带数字的代价:
- ISR 60µs、周期 100µs。 CPU 占用 60%;加上通信 2%、CAN 2%,总 64%;主循环只剩 36%。如果主循环需要 50%,系统必然丢任务。
- 关中断 10µs。 控制中断最坏延迟 10+0.4=10.4\mu\text{s},占 100µs 周期的 10.4%;如果关中断 50µs,延迟 50.4µs,控制环直接错过半个周期。
- ISR 里延时 1ms。 1kHz 事件下,1\text{ms}\times1\text{kHz}=100\% CPU,主循环完全饿死;100Hz 事件下也有 10%,足以让低优先级通信超时。
2. 定义与直观
2.1 中断、异常与向量表
在 ARM Cortex-M 的术语里,异常(Exception) 是统称,包括复位、NMI、HardFault、SVCall、PendSV、SysTick 和外部中断;中断(Interrupt) 通常指外设产生的外部异常(IRQ)。两者走同一套硬件入口,区别在来源和优先级。
向量表(Vector Table) 是一张放在存储器固定位置的地址表:第 0 项是初始主栈指针(MSP),第 1 项是复位处理函数入口,后面依次是 NMI、HardFault、各 IRQ 的入口。CPU 响应异常时,用异常号作为索引,从表里取出入口地址,跳到对应函数。Cortex-M 的向量表可以通过 VTOR(Vector Table Offset Register)重定位,Bootloader 和应用程序各用一张表,就是靠它。
向量表里放的是地址,不是代码。 启动文件通常用弱符号(weak symbol)给每个向量一个默认处理函数,用户重写同名函数即可覆盖;如果某个中断没写处理函数而意外触发,就会落到默认死循环里——这是"程序跑飞后停住"的常见原因之一。
2.2 从请求到 ISR:一条硬件流水线
一次中断的硬件路径大致是:
- 外设置标志:UART 收到字节、定时器计数到点、GPIO 检测到边沿,外设把状态寄存器里的标志置 1。
- NVIC 挂起:如果该中断使能,NVIC 把对应的 pending 位置 1;即使 CPU 暂时不响应,请求也不会丢。
- 优先级仲裁:NVIC 在多个 pending 中断里选优先级最高的;优先级相同则按异常号/先来后到。
- CPU 异常进入:完成当前指令、自动压栈 8 个字(R0~R3、R12、LR、PC、xPSR)、从向量表取入口、清 pending、跳转。注意是 8 个寄存器,不是 7 个:常见资料漏写 R12,只列 xPSR/PC/LR/R0~R3,合计只有 7 个,压栈字节数会算错。
- ISR 执行:软件读数据、清标志、把数据放进缓冲、通知主循环。
- 异常返回:自动出栈、恢复现场;如果有同优先级或更低优先级的 pending,可能尾链(Tail-Chaining) 直接进入下一个 ISR,而不先返回主程序。
尾链的意义:背靠背中断不用完整出栈再压栈,Cortex-M3/M4 的尾链开销典型 6 个时钟;晚到(Late Arrival) 则允许更高优先级中断在压栈过程中抢占,减少高优先级请求的等待。
2.3 优先级、抢占与子优先级
Cortex-M 的 NVIC 给每个异常一个优先级寄存器。典型实现用 4 位优先级(16 级),但具体位数由芯片厂商决定,必须查数据手册。优先级数值越小,优先级越高。
- 抢占优先级(Preemption Priority):决定一个中断能不能打断正在执行的 ISR。高抢占优先级可以抢占低抢占优先级。
- 子优先级(Subpriority):只在多个同抢占优先级中断同时挂起时决定谁先执行;不能抢占。
- 相同抢占优先级:不能互相打断;一个 ISR 执行期间,另一个同组中断只能等它退出。
这就解释了"为什么优先级不能全设成一样":全设成一样,紧急中断只能等慢中断做完,实时性完全靠 ISR 长度保证;一旦某个 ISR 变长,所有同组中断的延迟一起变大。
2.4 中断、轮询与 DMA:三种取数方式
| 方式 | CPU 参与 | 延迟 | CPU 开销 | 适用 |
|---|---|---|---|---|
| 轮询 | 周期性读状态 | 取决于轮询周期 | 固定,事件少时浪费 | 低频、确定性要求不高 |
| 中断 | 事件到来才进 ISR | 微秒级(含入口) | 与事件率成正比 | 中高频、需要及时响应 |
| DMA + 中断 | 只在块完成时进 ISR | 块级延迟 | 最低 | 大批量、连续数据 |
DMA 不是"没有中断",而是"把每字节一次中断变成每块一次中断"。 例如 UART 115200、每字节中断一次是 11,520 次/s;用 64 字节 DMA 块,中断降到 180 次/s,CPU 开销从 2.4% 降到 0.075%(按每次 150/300 周期估算,见 4.5 节)。
2.5 异步输入与亚稳态
外部按键、总线信号、连接器输入对 MCU 时钟来说是异步的:它们可能在触发器的建立/保持窗口内翻转,使触发器输出停在 0 和 1 之间的亚稳态(Metastability),要过一段不确定时间才落定。直接把异步信号接到中断线,可能偶发误触发、状态机跑飞或采到错值。
两级同步器的思路:第一级可能亚稳,但给它一个完整时钟周期收敛;第二级再采样,输出几乎一定是合法电平。单级同步的残余亚稳概率近似:
p_{res}=p\,e^{-T_{clk}/\tau_{meta}},\qquad p=\frac{t_{su}+t_h}{T_{clk}}其中 p 是输入变化落在建立/保持窗口内的概率(无量纲);t_{su}、t_h 是建立/保持时间(s);T_{clk} 是时钟周期(s);\tau_{meta} 是亚稳分辨率时间常数(s,典型几十 ps);p_{res} 是下一级仍采到非法电平的概率。每多一级同步,MTBF 乘上 e^{T_{clk}/\tau_{meta}},这就是"两级起步、越高速越要三级"的定量依据。
多 bit 总线不能逐位同步:各位到达时刻不同,逐位打拍会拼出错误组合;要用握手、格雷码或异步 FIFO。
2.6 ISR 的结构:读、清、存、通知
一个合格的 ISR 通常只做四件事:
- 读:从外设数据寄存器取走数据;
- 清:清外设标志和 NVIC pending(按手册的顺序);
- 存:把数据放进环形缓冲或全局变量;
- 通知:置一个标志、发一个信号量,让主循环去处理。
不在 ISR 里做:阻塞等待、长延时、printf、malloc、浮点运算(除非硬件 FPU 且已评估压栈开销)、Flash 写、复杂协议解析。ISR 的目标是"最短时间把事件接住",不是"把事情做完"。
3. 定量关系
3.1 中断延迟的构成
中断延迟定义为"中断请求有效"到"ISR 第一条指令执行"的时间:
t_{lat}=\frac{N_{entry}+N_{wait}+N_{prologue}}{f_{cpu}}其中 N_{entry} 是异常入口的时钟数(含压栈、取向量、流水线刷新);N_{wait} 是存储器等待周期带来的额外时钟;N_{prologue} 是 ISR 入口保护现场、清标志的时钟数;f_{cpu} 是主频(Hz);t_{lat} 是延迟(s)。
算例: 72MHz、Cortex-M3,入口 12 个时钟、Flash 等待 4 个时钟、ISR 入口 13 个时钟:
t_{lat}=\frac{12+4+13}{72\times10^6}=\frac{29}{72\times10^6}=403\text{ns}如果当前指令是 17 个时钟的 LDM(多寄存器加载),最坏延迟再加 17/72\text{MHz}=236\text{ns},到 639ns。若使用 FPU 且发生惰性压栈,再加约 32 个时钟(32/72\text{MHz}=444\text{ns}),总延迟约 1.08µs。这些数都是"典型量级",具体以内核手册和芯片的 Flash 等待周期表为准。
3.2 CPU 占用率与最大中断频率
各中断的 CPU 占用率相加:
U_{cpu}=\sum_{i=1}^{n}f_i t_i其中 f_i 是第 i 个中断的频率(Hz),t_i 是第 i 个 ISR 的执行时间(s),U_{cpu} 无量纲。若要求留 30% 余量,则 U_{cpu}\leq0.70。单个中断的最大频率:
f_{max}=\frac{U_{max}}{t_{ISR}}其中 U_{max} 是允许的占用率(如 0.5),t_{ISR} 是 ISR 时间(s)。60µs 的 ISR,若只允许 50% 占用,最高 8.33kHz;超过这个频率,CPU 没有时间做别的事。
3.3 临界区与关中断的代价
关中断期间,所有中断都被屏蔽(或低于某个优先级的中断被屏蔽)。最坏中断延迟:
t_{lat,worst}=t_{lat}+t_{crit}其中 t_{crit} 是最长关中断时间(s)。抖动近似等于 t_{crit}(因为请求可能落在关中断窗口的任意位置)。关中断造成的"阻塞占空比":
U_{block}=f_{crit}t_{crit}其中 f_{crit} 是单位时间关中断次数(Hz)。10µs 关中断、每 1ms 一次,阻塞占空比 1%;每 100µs 一次,10%;每 10µs 一次,100%——系统直接失去实时性。
3.4 中断 vs 轮询的定量取舍
轮询的 CPU 占用:
U_{poll}=f_{poll}t_{poll}中断的 CPU 占用:
U_{irq}=f_{event}t_{ISR}两者相等的转折点:
f_{event}=\frac{f_{poll}t_{poll}}{t_{ISR}}其中 f_{poll} 是轮询频率(Hz);t_{poll} 是每次轮询的周期数换算成的时间(s);f_{event} 是事件频率(Hz);t_{ISR} 是 ISR 时间(s)。但 CPU 占用不是唯一判据:轮询的延迟最坏是 1/f_{poll},中断的延迟是 t_{lat};如果 deadline 小于轮询周期,轮询再省 CPU 也不能用。
3.5 嵌套与最坏响应时间
固定优先级抢占式调度的最坏响应时间用迭代式:
R_i=C_i+B_i+\sum_{j\in hp(i)}\left\lceil\frac{R_i}{T_j}\right\rceil C_j其中 R_i 是任务/ISR i 的最坏响应时间(s);C_i 是自身执行时间(s);B_i 是低优先级临界区造成的阻塞(s);hp(i) 是优先级高于 i 的集合;T_j 是 j 的周期(s);C_j 是 j 的执行时间(s)。从 R_i=C_i+B_i 开始迭代,直到收敛;若 R_i 超过 i 的周期(或 deadline),就不可调度。
栈用量:Cortex-M 每次异常压栈 8 个字(32B);若使用 FPU,惰性压栈再加 17 个字(68B),合计约 100B,按 8 字节对齐通常算 104B。N 层嵌套的栈需求:
S_{stack}\approx N\times104\text{B}+S_{locals}其中 S_{locals} 是 ISR 局部变量与函数调用深度(B)。4 层嵌套就是 416B,再加局部变量,1KB 栈并不宽裕。
3.6 中断频率、事件率与去抖
去抖计数:
N_{cnt}=\lceil t_{debounce}f_{samp}\rceil,\qquad W=\lceil\log_2 N_{cnt}\rceil其中 t_{debounce} 是确认窗口(s),f_{samp} 是采样频率(Hz),N_{cnt} 是计数值,W 是位宽(bit)。20ms 抖动、1kHz 采样 → 20 个计数、5 位;10kHz 采样 → 200 个计数、8 位。
4. 典型计算
4.1 算例一:100µs 控制中断,ISR 耗时 60µs
已知: 控制中断周期 T=100\mu\text{s},ISR 执行 C=60\mu\text{s};通信中断 T=1\text{ms}、C=20\mu\text{s};CAN 中断 T=500\mu\text{s}、C=10\mu\text{s};主频 72MHz,入口延迟约 0.4µs。
CPU 占用:
U_{cpu}=\frac{60}{100}+\frac{20}{1000}+\frac{10}{500}=0.60+0.02+0.02=0.64=64\%主循环可用时间 100-64=36\mu\text{s}/100µs。若主循环需要 50µs/100µs,就不够。
| 控制 ISR 时间 | 控制占用 | 加通信/CAN 后 | 主循环余量 | 判断 |
|---|---|---|---|---|
| 20µs | 20% | 24% | 76% | 宽裕 |
| 40µs | 40% | 44% | 56% | 够 |
| 60µs | 60% | 64% | 36% | 紧,主循环需 <36% |
| 80µs | 80% | 84% | 16% | 很紧,任何突发都超 |
| 95µs | 95% | 99% | 1% | 不可用 |
抖动: 若有一个更高优先级的 5µs 安全中断每 10ms 来一次,控制 ISR 最坏被推迟 5µs,完成时间从 60µs 变成 65µs,仍小于 100µs;如果这个安全中断每 20µs 来一次(25% 占用),控制 ISR 在一个周期内可能被推迟两次,完成 70µs;若每 10µs 来一次(50% 占用),控制 60% + 安全 50% = 110%,不可调度。
结论:60µs 的 ISR 在 100µs 周期里"能跑",但把主循环压到 36%,没有余量应对突发;如果主循环要做显示、协议、存储,应把控制 ISR 压到 20~30µs,或把非紧急工作搬到主循环/DMA。
4.2 算例二:中断延迟分解与主频选择
按 3.1 节:12 个入口时钟 + 4 个等待 + 13 个入口保护 = 29 个时钟。不同主频下:
| 主频 | 29 周期延迟 | 最坏 LDM(+17 周期) | FPU 惰性压栈(+32 周期) |
|---|---|---|---|
| 8MHz | 3.63µs | 5.75µs | 9.75µs |
| 16MHz | 1.81µs | 2.88µs | 4.88µs |
| 48MHz | 604ns | 958ns | 1.63µs |
| 72MHz | 403ns | 639ns | 1.08µs |
计算示例:29/8\text{MHz}=3.625\mu\text{s};46/8\text{MHz}=5.75\mu\text{s};78/8\text{MHz}=9.75\mu\text{s}。主频翻倍,延迟近似减半;但 8MHz 下 9.75µs 的延迟,对 100µs 控制周期已占 10%,这就是低主频 MCU 做高速控制吃力的原因。
注意:表中"入口 12 周期"是 Cortex-M3/M4 在零等待存储器下的典型值;Cortex-M0+ 等内核可能不同,Flash 等待周期、总线仲裁、Cache 命中率都会改变实际值。不要拿一个内核的数套到另一个内核上。
4.3 算例三:临界区与最坏延迟
设控制中断周期 100µs,正常入口延迟 0.4µs;代码里有一段 10µs 的关中断临界区,每 1ms 执行一次。
最坏延迟:
t_{lat,worst}=0.4+10=10.4\mu\text{s}占周期的 10.4/100=10.4\%。阻塞占空比:
U_{block}=1\text{kHz}\times10\mu\text{s}=1\%若临界区改成 50µs、每 500µs 一次:
t_{lat,worst}=0.4+50=50.4\mu\text{s},\qquad U_{block}=2\text{kHz}\times50\mu\text{s}=10\%控制环可能在半个周期内得不到响应。
结论:临界区要按"最坏延迟"和"阻塞占空比"两个指标同时控制。 10µs 临界区对应 10.4% 抖动,已经超过"最坏延迟 < 周期 10%"的目标,应压到 9µs 以内;更好的做法是把共享数据改成无锁环形缓冲、原子操作或双缓冲,把关中断缩到几个指令。
4.4 算例四:多中断嵌套的最坏响应时间
设优先级从高到低:安全 5µs/10ms、控制 60µs/100µs、CAN 10µs/500µs、通信 20µs/1ms;忽略阻塞 B_i 和入口开销(或把入口算进 C_i)。
控制中断:
R_{ctrl}=60+\left\lceil\frac{R_{ctrl}}{10000}\right\rceil\times5从 R=60 开始:\lceil60/10000\rceil=1,R=65;再迭代 \lceil65/10000\rceil=1,收敛 R=65\mu\text{s}。小于 100µs,可调度,余量 35µs。若安全中断周期改成 20µs:
R=60+\left\lceil\frac{R}{20}\right\rceil\times5R=60 → \lceil3\rceil=3 → R=75;\lceil75/20\rceil=4 → R=80;\lceil80/20\rceil=4 → 收敛 80µs。仍小于 100µs,但余量只剩 20µs。若安全周期 10µs:R=60+5\lceil R/10\rceil,迭代会发散(安全占用 50% + 控制 60% = 110%),不可调度。
CAN 中断(低于控制、高于通信):
R_{CAN}=10+60+\left\lceil\frac{R_{CAN}}{10000}\right\rceil\times5\approx75\mu\text{s}<500\mu\text{s}通信中断(最低):
R_{comm}=20+60+10+5=95\mu\text{s}<1000\mu\text{s}结论:在给定周期下,四级中断都满足 deadline;控制余量 35µs,CAN 余量 425µs,通信余量 905µs。真正危险的是"高优先级中断频率升高"或"ISR 变长"——安全中断从 10ms 提到 10µs,系统立刻不可调度。
4.5 算例五:中断 vs 轮询 vs DMA
设每次轮询 0.2µs、每次 ISR 2µs、轮询频率 10kHz:
| 事件率 | 轮询占用 | 中断占用 | 轮询最坏延迟 | 中断延迟 | 选择 |
|---|---|---|---|---|---|
| 100Hz | 0.2% | 0.02% | 100µs | 0.4µs | 轮询更省,但延迟大 |
| 1kHz | 0.2% | 0.2% | 100µs | 0.4µs | 看 deadline |
| 10kHz | 0.2% | 2% | 100µs | 0.4µs | 中断(若 deadline <100µs) |
转折点:
f_{event}=\frac{10\text{kHz}\times0.2\mu\text{s}}{2\mu\text{s}}=1\text{kHz}事件率低于 1kHz,轮询的 CPU 开销更低;高于 1kHz,中断更省。但"省 CPU"不等于"能用":如果 deadline 是 50µs,1kHz 事件也必须用中断,因为轮询最坏要等 100µs。
DMA: UART 115200、10 位/字节,数据率 11,520B/s;每字节中断一次、每次 150 周期:
U_{byte}=\frac{11520\times150}{72\times10^6}=2.4\%64 字节 DMA 块,中断率 11520/64=180 次/s,每次 300 周期:
U_{block}=\frac{180\times300}{72\times10^6}=0.075\%DMA 把中断开销从 2.4% 降到 0.075%,省 2.3 个百分点;数据量越大,收益越明显。
5. 工程含义
5.1 优先级分配:按 deadline,不按"重要性"
优先级分配的第一原则是截止时间越短,优先级越高(截止时间单调,Deadline Monotonic 的思路),而不是"这个功能更重要"。理由:高优先级中断会推迟低优先级中断;如果低优先级中断的 deadline 更紧,它会被高优先级的长 ISR 拖死。
典型分配:
- 安全/故障:过流、过压、急停,最高优先级,ISR 极短(几微秒);
- 控制环:PWM/ADC 节拍,次高,ISR 控制在周期的 20~30%;
- 通信:CAN、UART,中等,允许几十微秒延迟;
- 显示/日志:最低,允许毫秒级延迟。
不要把所有中断设成同一优先级:同优先级不能抢占,一个慢 ISR 会挡住所有同组中断;也不要设太多优先级,优先级分组和子优先级会把配置复杂度推高。
还要防优先级反转(Priority Inversion):低优先级任务持有共享资源(互斥量/临界区),高优先级任务或中断等它释放,实际优先级被拉低,延迟可能远超预期。RTOS 用优先级继承或优先级天花板缓解;裸机里则应把共享资源改成无锁环形缓冲、原子操作或双缓冲,缩短临界区,避免"高优先级等低优先级"的结构。
5.2 ISR 结构模板
void TIM2_IRQHandler(void) {
/* 1. 读:取走数据/状态 */
uint32_t v = TIM2->CCR1;
/* 2. 清:按手册顺序清标志(先读 SR 再读 DR 等) */
TIM2->SR = ~TIM_SR_CC1IF;
/* 3. 存:放进环形缓冲,不做解析 */
ring_push(&adc_ring, v);
/* 4. 通知:置标志/发信号量,主循环处理 */
adc_ready = 1;
}要点:
- 短:只做必须立即做的事;协议解析、显示、存储放主循环。
- 可重入:ISR 与主循环共享的变量加
volatile,多字节数据用临界区或原子操作保护。 - 清标志顺序:UART 常要求"先读 SR 再读 DR"或"写 1 清标志",顺序错了会丢中断或重复进中断;以参考手册为准。
- 不要阻塞:任何
while等待、延时、printf、malloc都不应出现在 ISR 里。
5.3 临界区设计:越短越好,能不用就不用
- 优先无锁:单生产者/单消费者环形缓冲、双缓冲、原子标志,可以完全避免关中断。
- 必须关中断时:只包住共享数据的读写,不包住计算、循环、外设等待。
- 嵌套临界区:保存并恢复 PRIMASK,不要直接置 0/1,否则内层退出会意外开中断。
- 测量:用 GPIO 翻转 + 示波器量临界区实际长度,不要靠"看代码估计"。
5.4 中断与低功耗
- 唤醒源:只有需要唤醒 CPU 的中断才使能;其他外设用 DMA 或保持睡眠。
- 中断风暴:边沿抖动、清标志不及时、电平触发持续有效,都会让 CPU 反复进 ISR,功耗飙升。
- tickless:低功耗 RTOS 可以关掉周期 SysTick,用下一个到期定时器唤醒;代价是时间基准精度与移植复杂度。
- 唤醒延迟:从深度睡眠唤醒要等稳压器、Flash、时钟稳定,延迟可能是微秒到毫秒级,必须在 deadline 预算里算进去。
5.5 功能安全与看门狗
- 看门狗喂狗位置:要放在"能证明系统还活着"的地方。放在主循环,能证明主循环没死;如果关键任务是某个 ISR,也可以由该 ISR 的完成标志驱动喂狗,但要防止"中断还在跑、主循环已死"的假活。"喂狗必须放主循环"是一个过于绝对的说法,正确做法是"喂狗必须能证明被监控的任务还活着"。
- 窗口看门狗:喂得太早也复位,防止程序跑飞后恰好落进喂狗代码。
- 独立看门狗时钟:用独立 RC/晶振,不要用系统时钟,否则系统时钟坏了狗也死了。
- 故障注入:量产前故意让主循环死循环、让 ISR 超时,验证看门狗确实复位。
5.6 验证与测量
- 中断延迟:在 ISR 入口翻转 GPIO,用示波器同时看请求信号和 GPIO,测最小/最大/抖动;注意探头和代码开销。
- CPU 占用:用空闲计数器或 GPIO 翻转测主循环剩余时间;不要只看"感觉"。
- 栈高水位:启动时把栈填成 0xA5,运行一段时间后统计未被改写的区域,确认高水位 <80%。
- 中断计数:统计每个 ISR 的进入次数与最大执行时间,找出异常频繁或异常长的中断。
6. 常见坑
- ISR 里写重活。 现象:主循环饿死、其他中断延迟暴涨、通信丢包。原因:ISR 占用 CPU 过多,低优先级中断得不到响应。解决:ISR 只读数据、清标志、存缓冲,处理放主循环;用 DMA 降低中断频率。
- 中断标志没清或清错顺序。 现象:ISR 一退出立刻又进,看似死循环;或者丢中断。原因:外设标志/NVIC pending 没清,或 UART 没按"先读 SR 再读 DR"的顺序清。解决:按参考手册的清标志顺序写,退出前确认 pending 已清。
- 优先级全设成同一级。 现象:紧急中断被慢中断挡住,延迟不可预测。原因:同抢占优先级不能互相抢占。解决:按 deadline 分级,安全 > 控制 > 通信 > 显示;同组内再考虑子优先级。
- 临界区太长。 现象:控制环抖动、采样时刻漂移。原因:关中断 50µs,控制中断最坏延迟 50.4µs。解决:临界区只包共享数据,改用无锁环形缓冲/原子操作,目标压到 9µs 以内(对 100µs 周期)。
- 异步输入不同步、按键不消抖。 现象:偶发误触发、一次按键进多次中断、状态机跑飞。原因:亚稳态 + 机械抖动。解决:两级同步器 + ≥20ms 确认窗口;边沿中断只置标志,主循环去抖。
- 在 ISR 里调用
printf/malloc/浮点。 现象:偶发死机、堆碎片、延迟暴涨。原因:printf不可重入且慢,malloc非确定,浮点在无 FPU 的内核上要软件模拟、有 FPU 时要额外压栈。解决:ISR 只做搬运,格式化和内存分配放主循环。 - 喂狗放在定时器中断里。 现象:主循环死了系统却不复位,看门狗形同虚设。原因:定时器中断仍在跑,继续喂狗。解决:喂狗放主循环,或由"被监控任务的完成标志"驱动;用窗口看门狗。
- 中断嵌套栈溢出。 现象:偶发死机、返回地址错乱。原因:每层嵌套约 104B(含 FPU),4 层就 416B,再加局部变量超过栈。解决:限制嵌套层数、增大栈、用栈高水位实测。
7. 自测题
中断响应时硬件自动做了哪几件事?压栈的 8 个字具体是哪些寄存器?
完成当前指令→自动压栈→从向量表取 ISR 入口→清 pending→跳转;返回时自动出栈。8 个字是 R0、R1、R2、R3、R12、LR、PC、xPSR,共 32B。很多资料漏掉 R12,写成 7 个寄存器,这是错的。
72MHz、入口 12 周期、等待 4 周期、ISR 入口保护 13 周期,中断延迟多少?若当前指令是 17 周期的 LDM 呢?
t=(12+4+13)/72\text{MHz}=29/72\text{MHz}=403\text{ns}。加 LDM 17 周期:(29+17)/72\text{MHz}=46/72\text{MHz}=639\text{ns}。若 FPU 惰性压栈再加约 32 周期,总约 1.08µs。
控制中断 100µs 周期、ISR 60µs;通信 1ms/20µs;CAN 500µs/10µs。CPU 总占用多少?主循环还剩多少?
U=60/100+20/1000+10/500=0.60+0.02+0.02=0.64=64\%。主循环剩 36µs/100µs。若主循环需要 50µs/100µs,就不够;应把控制 ISR 压到 20~30µs,或用 DMA/主循环分担。
关中断 10µs,控制中断正常延迟 0.4µs,周期 100µs。最坏延迟和抖动是多少?若每 1ms 关一次,阻塞占空比多少?
最坏延迟 10+0.4=10.4\mu\text{s},占周期 10.4%;抖动约 10µs。阻塞占空比 1\text{kHz}\times10\mu\text{s}=1\%。若临界区 50µs、每 500µs 一次,最坏延迟 50.4µs(50.4%),阻塞占空比 10%,控制环不可接受。
事件率 1kHz、每次 ISR 2µs、轮询 10kHz 每次 0.2µs。中断和轮询的 CPU 占用各多少?转折点在哪?
中断 1\text{kHz}\times2\mu\text{s}=0.2\%;轮询 10\text{kHz}\times0.2\mu\text{s}=0.2\%,两者相同。转折点 f_{event}=10\text{kHz}\times0.2\mu\text{s}/2\mu\text{s}=1\text{kHz}。低于 1kHz 轮询更省 CPU,但轮询最坏延迟 100µs;deadline 小于 100µs 时必须用中断。
UART 115200、每字节 150 周期、72MHz。每字节中断的 CPU 占用多少?64 字节 DMA 块、每次 300 周期呢?
数据率 115200/10=11520\text{B/s};每字节中断 11520\times150/72\times10^6=2.4\%。64 字节块中断率 11520/64=180 次/s,占用 180\times300/72\times10^6=0.075\%。DMA 省 2.3 个百分点。
为什么多 bit 总线跨时钟域不能逐位用两级同步器?应该怎么做?
各位到达时间不同,逐位打拍会在同一时钟沿采到"新旧混合"的组合,拼出错误值;即使每位都不亚稳,组合也可能非法。要用握手(请求/应答)、格雷码(一次只变一位)或异步 FIFO。单 bit 控制信号才适合两级同步器。
8. 参考资料
- ARM DUI 0552A,Cortex-M3 Devices Generic User Guide:Chapter 2 The Cortex-M3 Processor 给出异常模型、压栈寄存器与中断延迟,Chapter 4 NVIC 给出优先级、悬起、尾链与晚到;本章 2.2、3.1 节按该指南口径。
- ARM DUI 0553A,Cortex-M4 Devices Generic User Guide:Chapter 2 与 Chapter 4 给出 FPU 惰性压栈与异常入口的额外开销,是本章 3.1、3.5 节 104B 栈估算的依据。
- ARM DDI 0403,ARMv7-M Architecture Reference Manual:Chapter B1 Exception Model 与 Chapter B3 The System Control Block 定义向量表、VTOR、异常进入/返回与优先级,是本章 2.1、2.3 节的架构依据。
- STMicroelectronics RM0008,STM32F101/102/103/105/107 Reference Manual:Chapter 10 Interrupts and events 给出 EXTI 触发、挂起位与 NVIC 中断表,是本章边沿/电平触发与清标志做法的依据。
- STMicroelectronics DS5319,STM32F103x8/xB Datasheet:Section 5 Electrical characteristics 给出中断引脚电气参数与时钟频率,用于本章延迟算例的主频与 IO 假设。
- Clifford E. Cummings,Synthesis and Scripting Techniques for Designing Multi-Asynchronous Clock Designs(SNUG 2001)与 Asynchronous and Synchronous Reset Design Techniques(SNUG 2003):两级同步器、亚稳态与 MTBF 的经典论述,是本章 2.5 节与 3.1 节同步器部分的依据。
延伸阅读:GPIO 原理与驱动能力 讲边沿中断、去抖与输入阈值;最小系统 讲复位、时钟与调试口;定时器与 PWM 原理 讲控制环的节拍与 PWM 中断;MCU 概览 讲 NVIC 与内核选型;数字系统时钟与复位设计 讲亚稳态、同步器与 MTBF 的完整推导。