惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
人人都是产品经理
人人都是产品经理
Cisco Talos Blog
Cisco Talos Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
V
V2EX
博客园 - 三生石上(FineUI控件)
Martin Fowler
Martin Fowler
WordPress大学
WordPress大学
D
Docker
S
SegmentFault 最新的问题
博客园 - 聂微东
美团技术团队
Apple Machine Learning Research
Apple Machine Learning Research
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Last Week in AI
Last Week in AI
M
MIT News - Artificial intelligence
F
Fortinet All Blogs
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
The GitHub Blog
The GitHub Blog
GbyAI
GbyAI
L
LangChain Blog
Vercel News
Vercel News
博客园 - 叶小钗
MongoDB | Blog
MongoDB | Blog
Stack Overflow Blog
Stack Overflow Blog
H
Help Net Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Cloudflare Blog
Engineering at Meta
Engineering at Meta
T
Threat Research - Cisco Blogs
T
Threatpost
Scott Helme
Scott Helme
T
Tailwind CSS Blog
Latest news
Latest news
Stack Overflow Blog
Stack Overflow Blog
Blog — PlanetScale
Blog — PlanetScale
The Register - Security
The Register - Security
罗磊的独立博客
P
Proofpoint News Feed
腾讯CDC
S
Schneier on Security
雷峰网
雷峰网
A
About on SuperTechFans
T
Tenable Blog
F
Full Disclosure
Cyberwarzone
Cyberwarzone
博客园_首页
有赞技术团队
有赞技术团队
K
Kaspersky official blog

文章列表

谁是人民? 算盘上的童年 向死而生 爱,死亡与机器人 什么是真正的哲学 LG V30 开源之旅 VENI VIDI VICI 那些不曾察觉的事物 十八之行
倒计时器 · 中断原理
Exister · 2025-12-21 · via

这学期我抢到了学校开设的 51 单片机课程,想着借此入门嵌入式开发。在完成最近的一次作业时,我遇到了一个 bug,捣鼓了快一天才找出原因。此问题涉及 51 单片机的中断原理,我觉得还是有必要写成一篇博文记录一下的。

作业要求

  • 上电后,初始倒计时时长设置为 10 秒,按 时-分-秒 格式显示。
  • 按下设置键后,0.5 秒点亮,0.5 秒熄灭。此时可以使用增大键、减小键修改倒计时时长。长按 0.5 秒以上可快速对时间进行调整。
  • 倒计时完成后,维持显示 88-88-88-88 1 秒,然后恢复之前设置的时长,可进行下一轮倒计时。
  • 按下暂停键后,显示内容由 XX-XX-XX 改为 XX||XX||XX
  • 暂停状态下按下设置键,可结束倒计时过程。

原理图

分析问题

完成 51 单片机课程的第一次作业可能是我人生中第一次真正意义上的编程,那次经历让我真正体会到编程并不是直接上手写代码,而是先分析问题:我们要干什么?问题是怎样的?有没有简单的解决方法?这次我遵循了之前获得的经验。

这个程序要完成什么样的功能呢?首先按照状态列表格:

编号状态作用进入退出
0初始显示 00-00-10模式 3 完成后进入按 K2 至 1;按 K3 至 2
1设置设置时间:0.5s 点亮,0.5s 熄灭;K3 增加秒数,K4 减少秒数;长按:快速调整模式 0 下按 K2 进入K2 至 0
2倒计时逐秒减少数字模式 0 下按 K3 进入自动退出至模式 3;按 K4 至 4
3结束显示 88-88-88,维持 1 秒模式 2 完成后自动进入自动退出至模式 0
4暂停暂停倒计时,显示 XX|XX|XX模式 0 下按 K4 进入按 K4 至模式 2;按 K2 至模式 0

我们需要通过检测按键的按下来确定该转入什么模式,因此,按照按键列表格:

按键短按长按
K2(设置)0:退出至 1,进入设置
1:退出至 0,进入初始
4:退出至 0,进入初始
K3(增加秒数、启动)0:退出至 2,开始倒计时
1:增加秒数

1:快速增加秒数
K4(减少秒数、暂停)1:减少秒数
2:退出至 4,暂停
4:退出至 2,恢复倒计时
1:快速减少秒数

编写代码

以下是我编写的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
#include <REG51.H>

// 长按与连发参数(单位:毫秒)
#define LONG_PRESS_MS 500
#define REPEAT_MS 80

// 倒计时器最大定时,59 * 3600 + 59 * 60 + 59 = 215999
#define MAX_TIME 215999

// 定义加减按键
sbit K2 = P3 ^ 0;
sbit K3 = P3 ^ 2;
sbit K4 = P3 ^ 3;

// 定义译码器
sbit P22 = P2 ^ 2;
sbit P23 = P2 ^ 3;
sbit P24 = P2 ^ 4;

// 共阴极数码管字模
const unsigned char code_table[10] = {
0x3F, // 0
0x06, // 1
0x5B, // 2
0x4F, // 3
0x66, // 4
0x6D, // 5
0x7D, // 6
0x07, // 7
0x7F, // 8
0x6F, // 9
};

const unsigned char char_table[2] = {
0x40, // -
0x36, // ||
};

// 长按状态记录
bit is_k3_pressed = 0;
bit is_k4_pressed = 0;
unsigned int k3_press_start_time = 0, k3_last_increment_time = 0;
unsigned int k4_press_start_time = 0, k4_last_decrement_time = 0;

unsigned int clock_ms_counter = 0; // 记录倒计时时间
unsigned int current_system_time = 0; // 记录其他时间

unsigned char seconds, minutes, hours;

unsigned char current_mode = 0;

long int set_num = 10, display_num = 10; // 倒计时数值

void trans(unsigned char digit); // 译码器

void detect_key(void);

void calc(void);

void display(char x, char y, char z, bit n);

void check_mode(void);

void mode_0(void);
void mode_1(void);
void mode_2(void);
void mode_3(void);
void mode_4(void);

void delay(unsigned int i) { while (i--); }

void init(void) {
IE |= 0x8A; // 启用 EA, T0, T1
TMOD = 0x11; // T0, T1 设置为模式 1
IT0 = 1; // 设置 INT0 为下降沿触发
IT1 = 1; // 设置 INT1 为下降沿触发
TH0 = 0xFC; // 装载 T0 高 8 位
TL0 = 0x18; // 装载 T0 低 8 位
TH1 = 0xFC; // 装载 T1 高 8 位
TL1 = 0x18; // 装载 T1 低 8 位
}

void main(void) {
init();
calc();

while (1) {
check_mode();
detect_key();
}
}

void trans(unsigned char digit) {
P22 = digit % 2;
P23 = (digit / 2) % 2;
P24 = (digit / 4) % 2;
}

void calc(void) {
seconds = display_num % 60;
minutes = (display_num / 60) % 60;
hours = (display_num / 3600) % 60;
}

void display(char x, char y, char z, bit n) {
char i;
for (i = 0; i <= 7; i++) {
trans(i);

if (i % 3 == 2) {
P0 = char_table[n]; // 显示字符 - 或 ||
}

else {
switch (i) {
case 0:
P0 = code_table[x % 10];
break;
case 1:
P0 = code_table[x / 10];
break;

case 3:
P0 = code_table[y % 10];
break;
case 4:
P0 = code_table[y / 10];
break;

case 6:
P0 = code_table[z % 10];
break;
case 7:
P0 = code_table[z / 10];
break;
}
}

delay(100);
P0 = 0x00; // 关显示
}
}

void check_mode(void) {
switch (current_mode) {
case 0:
mode_0();
break;
case 1:
mode_1();
break;
case 2:
mode_2();
break;
case 3:
mode_3();
break;
case 4:
mode_4();
break;
}
}

// 初始模式
void mode_0(void) {
IE &= 0xFA; // 禁用 INT0, INT1
TR0 = 0;
TR1 = 0;
current_system_time = 0;

display_num = set_num;
calc();
display(seconds, minutes, hours, 0);
}

// 设置模式
void mode_1(void) {
IE |= 0x05; // 启用 INT0, INT1
TR0 = 0;
TR1 = 1;

if (K3 == 0 || K4 == 0) { // 按下 K3, K4,不闪烁
calc();
display(seconds, minutes, hours, 0);
}

else {
while (current_system_time <= 500) {
calc();
display(seconds, minutes, hours, 0);
}

if (current_system_time >= 1000)
current_system_time = current_system_time - 1000;
}
}

// 倒计时模式
void mode_2(void) {
if (clock_ms_counter >= 1000) {
display_num--;
clock_ms_counter = clock_ms_counter - 1000;
}

if (display_num == -1) {
TR0 = 0;
current_mode = 3; // 倒计时结束
} else {
calc();
display(seconds, minutes, hours, 0);
}
}

// 结束模式
void mode_3(void) {
TR0 = 0;
current_system_time = 0;
TR1 = 1;

while (current_system_time <= 1000) {
display(88, 88, 88, 0);
}

TR1 = 0;
current_system_time = 0;
current_mode = 0;
}

// 暂停模式
void mode_4(void) { display(seconds, minutes, hours, 1); }

void detect_key(void) {
if (K2 == 0) {
switch (current_mode) {
case 0:
while (K2 == 0);
current_mode = 1;
break;
case 1:
while (K2 == 0);
current_mode = 0;
break;
case 4:
while (K2 == 0);
current_mode = 0;
break;
}
}

if (K3 == 0) {
switch (current_mode) {
case 0:
while (K3 == 0);
TR0 = 1;
current_mode = 2;
break;
}
}

if (K4 == 0) {
switch (current_mode) {
case 2:
while (K4 == 0);
TR0 = 0;
current_mode = 4;
break;
case 4:
while (K4 == 0);
TR0 = 1;
current_mode = 2;
break;
}
}
}

// 为设置模式下修改时间响应迅速,将相关代码从 detect_key() 中移出转入 ISR

void plus_num(void) interrupt 0 using 1 {
if (!is_k4_pressed) { // 是否为首次按下,只在设置模式下生效
is_k3_pressed = 1;
k3_press_start_time = current_system_time;
k3_last_increment_time = current_system_time;

if (set_num < MAX_TIME)
display_num = ++set_num; // 首次按下,num + 1
else
display_num = set_num = 0; // 防止溢出
}
}

void minus_num(void) interrupt 2 using 1 {
if (!is_k4_pressed) { // 判断是否为首次按下,只在设置模式下生效
is_k4_pressed = 1;
k4_press_start_time = current_system_time;
k4_last_decrement_time = current_system_time;

if (set_num > 0)
display_num = --set_num; // 首次按下,num - 1
else
display_num = set_num = MAX_TIME; // 防止负数
}
}

void clock_time(void) interrupt 1 using 2 {
// 1ms = 1000 us, 65536 - 1000 = 64536

TH0 = 0xFC; // 重新装载高 8 位
TL0 = 0x18; // 重新装载低 8 位
clock_ms_counter++;
}

void system_time(void) interrupt 3 using 3 { // 其他计时
// 1ms = 1000 us, 65536 - 1000 = 64536

TH1 = 0xFC; // 重新装载高 8 位
TL1 = 0x18; // 重新装载低 8 位
current_system_time++;

// 处理设置模式下 K3/K4 长按连发与松开
if (current_mode == 1) {
// K3 长按快增
if (is_k3_pressed) {
if (K3 == 1) {
is_k3_pressed = 0; // 短按重置
} else { // 长按处理
unsigned int hold_duration = current_system_time - k3_press_start_time;
unsigned int since_repeat =
current_system_time - k3_last_increment_time;

if (hold_duration >= LONG_PRESS_MS && since_repeat >= REPEAT_MS) {
k3_last_increment_time = current_system_time;
if (set_num < MAX_TIME)
display_num = ++set_num;
else
display_num = set_num = 0; // 防止溢出
}
}
}

// K4 长按快减
if (is_k4_pressed) {
if (K4 == 1) {
is_k4_pressed = 0; // 短按重置
} else { // 长按处理
unsigned int hold_duration = current_system_time - k4_press_start_time;
unsigned int since_repeat =
current_system_time - k4_last_decrement_time;

if (hold_duration >= LONG_PRESS_MS && since_repeat >= REPEAT_MS) {
k4_last_decrement_time = current_system_time;
if (set_num > 0)
display_num = --set_num;
else
display_num = set_num = MAX_TIME; // 防止负数
}
}
}
}
}

以 K3 为例,长按连发的流程图:

flowchart TD
  A[每 1 毫秒 system_time 中断触发] --> B{is_k3_pressed}
  B -- 0 --> Z[等待下一次中断]
  B -- 1 --> C{K3 当前是否仍在按下}
  C -- 否 --> D[短按]
  D --> E[标志位清零]
  C -- 是 --> F[长按]
  F --> G[计算 K3 按下时长和上次增加时间间隔]
  G --> H{是否满足连发条件}
  H -- 不满足 --> Z
  H -- 满足 --> I[增加时间]
  I --> Z
  Z --> A

  J[按下 K3] --> K{is_k3_pressed}
  K -- 0 --> L[is_k3_pressed 置 1]
  L --> M[记录当前时间] 
  K -- 1 --> N[退出]

发现问题

我以为这个程序写得非常完美,在愉悦中入睡了。然而,第二天起来后再测试就发现了一个 bug:倘若一上电就按下 K2,调整时间,不会出现任何问题。如果上电后先跑一轮倒计时,返回初始模式后再调整时间,则按下 K2 的一瞬间倒计时秒数会增加 1。这是因为 K3 除了启动倒计时,还有增加秒数的功能。但连续跑多轮倒计时或倒计时过程中多次按下 K3,进入设置模式后时间并不会增加更多秒数,仍旧只是增加 1。同样,在倒计时过程中按 K4,正负抵消,设定秒数不会发生变化。

我使用 Proteus 调试时发现 display_numset_num 的自增/减是在进入设置模式后才发生的,其余模式下变量取值均正常。

分析原因

为什么?我百思不得其解。我起初以为问题在切换进入 设置模式 的相关代码上,后来我觉得可能出在中断服务程序 system_time() 内,因为每隔 1ms 计数器就会溢出一次,执行频率非常高。但我读了好几遍相关代码,都没发现问题。

那么,一上电的状态和按下 K3/4 后的状态有什么区别,导致单片机执行结果不一样?我决定在 Proteus 调试中记录并比较两种情况下所有变量的取值。

在排除 IO 口、current_system_time 等无关紧要的变量后,可以看到有以下变量取值不同:

名称地址类型刚上电时的取值按下 K3 后取值按下 K4 后取值
IT1SFR Bit:8Astruct bit0b000001010b000001110b00001101
IT0SFR Bit:88struct bit0b000001010b000001110b00001101
IE1SFR Bit:8Bstruct bit0b000001010b000001110b00001101
IE0SFR Bit:89struct bit0b000001010b000001110b00001101

尽管 IT0/1、IE0/1 为单独位,但 Proteus 还是显示了一个 8 位二进制数。事实上,Proteus 显示的是整个 TCON 的值,选中这一项展开就可以查看 byte bitfield。因此,这四个不同实际上只是其中一位的不同:按下 K3/4 后,IE0/1 位被置 1。

名称位地址功能
TCON.7TF10x8F定时器/计数器 1 溢出标志位
TCON.6TR10x8E定时器/计数器 1 运行控制位
TCON.5TF00x8D定时器/计数器 0 溢出标志位
TCON.4TR00x8C定时器/计数器 0 运行控制位
TCON.3IE10x8B外部中断 1 标志位
TCON.2IT10x8A外部中断 1 触发方式选择位
TCON.1IE00x89外部中断 0 标志位
TCON.0IT00x88外部中断 0 触发方式选择位

中断原理

标志位的置位

这其实说明一件事:即使没有在中断允许寄存器(IE)中打开外部中断 0/1(EX0/1 = 1),单片机也会响应 P3.2/P3.3 电平的变化,并将相应标志位(IE0/1)置 1。这颠覆了我之前对中断的理解。我以为 IE 中 EA 是「总开关」、其余位是「分开关」,两类开关「串联」,只有两个开关都打开,单片机才会检测中断源并响应中断。

我们以外部中断 0(INT 0)为例,编写一个简单的程序来验证这件事,倘若标志位 IE0 被置 1,则点亮 LED:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <REG51.H>

void main(void) {
// 开启 IE 总开关
// EA = 1;

while (1) {
if (IE0 == 1) {
P2 = 0x00;
} else {
P2 = 0xFF;
}
}
}

// 结果:按下按键,LED 不亮

这是为什么?回到倒计时器程序,可以看到在初始化时开启了 EA。现在取消上方代码中第 5 行的注释,就会发现 LED 点亮了;但是松开按键,LED 却又熄灭了。

点亮 LED

与倒计时器程序进行比较,可以发现,倒计时程序使用的是下降沿触发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <REG51.H>

void main(void) {
EA = 1;
IT0 = 1; // INT0 设置为下降沿触发

while (1) {
if (IE0 == 1) {
P2 = 0x00;
} else {
P2 = 0xFF;
}
}
}

// 结果:按下按键,LED 维持常亮

我推测低电平触发与下降沿触发的检测原理不同。低电平触发时,单片机会一直检测相应引脚的电平状态,低电平时相应标志位置 1,高电平时清零;下降沿触发时,单片机检测到相应引脚的电平降低就将相应标志位置 1,并不会自动清零,响应中断后才会清零。

也就是说,这种中断任务「挂起」、启用中断后迅速执行导致的问题只存在于下降沿触发的外部中断。第一次遇到这种问题好像觉得非常麻烦,但这其实是一种非常精妙的设计:如果需要处理许多中断,而低优先级中断是下降沿触发的,倘若不这样设计,正在处理一个高优先级中断的时候,触发低优先级外部中断,这个低优先级中断就会被忽略,无法触发。

下降沿触发和低电平触发的用途不同。前者主要用于对「事件」的检测,譬如按下按键,只要检测到按键按下这一事件发生,就触发中断。即使现在无法执行中断服务程序,也要「记下来」在合适的时候执行。而后者主要用于对「状态」的检测,只要处于某种状态,就触发中断。并且这种状态具有时效性,一旦不再处于这种状态,就没必要做出中断,因此并没有设计「中断任务挂起」这样的功能。

类似地,我验证了定时器中断与下降沿触发的外部中断相似:只要 EA = 1,并打开记时器开关 TR0/1,即使 ET0/1 = 0TF0/1 也会置 1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <REG51.H>

void main(void) {
EA = 1;
TR0 = 1;

while (1) {
if (TF0 == 1 && ET0 == 0) {
P2 = 0x00;
} else {
P2 = 0xFF;
}
}
}

// 结果:LED 被点亮

我想这样表述我的发现:

中断允许寄存器(IE)最高位 EA检测中断触发条件的开关,如果 EA = 1,CPU 会检测所有中断源触发条件,对相应的中断标志位进行操作。低电平触发时,单片机会检测相应端口是否为低电平,若是,则将标志位置 1;下降沿触发时,单片机会检测相应端口电平是否出现下降,若是,则将标志位置 1。

而 IE 中的局部使能位检查中断标志位的开关,只有对应位为 1 时,CPU 才会检查该中断的标志位;若标志位也为 1,才会响应中断。

试想一下,如果单片机响应中断的原理真是这样,运行下面的程序会有什么样的结果呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <REG51.H>

void main(void) {
EA = 1;

while (1) {
if (IE0 == 1) {
P2 = 0x00;
EA = 0;
} else {
P2 = 0xFF;
}
}
}

按下按键后,LED 会常亮,无论按键是否松开。因为外部中断 0 为低电平触发,P3.2 为低电平的第一个时刻,IE0 被置 1,执行 if 中的内容,点亮 LED 之后,又关闭了 EA。这样单片机不会再更改 IE0 的值,LED 也就常亮了。事实上,这也正是实际的运行结果。

这就解释了程序 bug 的原因:虽然没有启用外部中断 0/1,但由于外部中断 0/1 被设置为下降沿触发,其标志位依然在被按下时被置 1,中断被「挂起」没有响应。进入 设置模式 后,启用了外部中断 0/1,单片机会瞬间产生中断响应。

标志位的清零

但我还有一个问题,中断标志位什么时候清零呢?可以运行一下这个程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <REG51.H>

void main(void) {
EA = 1;
EX0 = 1; // 允许响应外部中断 0
IT0 = 1;

while (1) {
if (IE0 == 1) {
P2 = 0x00;
} else {
P2 = 0xFF;
}
}
}

void int0_isr(void) interrupt 0 { while (1); }

// 结果:按下按键,LED 不亮

这样,触发外部中断 0 后,会一直卡在中断服务程序出不来。LED 不亮说明按下按键后立刻进入中断服务程序,还没来得及执行主函数中的判断和赋值。无论怎样按按键,LED 均不亮,但使用 Proteus 调试发现:第一次按下按键,IE0 为 0;第二次按下按键,IE0 保持为 1。

这说明对于下降沿触发的外部中断,CPU 在执行中断函数前就已经自动将标志位 IEx 清零。而第二次按下按键时,CPU 卡在中断的死循环中。而中断不能被同优先级或低优先级中断打断,因此 CPU 无法对标志位清零,IE0 最终保持为 1。(下降沿触发,标志位的置位可能是由硬件完成。我先让 CPU 陷入一个高优先级中断的死循环,再按下按键,发现 IE0 依然能置 1。定时器中断我也测试过了,与下降沿触发的外部中断结果相同。)

将上方程序的第 6 行删掉,就可以测试低电平触发的外部中断将标志位清零的时刻。经过我的测试,LED 始终不亮。但与下降沿触发不同,IE0 只在按键按下时为 1,一旦按键松开就清零。这说明低电平触发的外部中断标志位的置位和清零也不占用 CPU 的执行时间,可能是由硬件完成的。

至于低电平触发的外部中断在跳转中断服务程序之前会不会将标志位清零,我猜测是不会的,因为确实没有这样做的意义。但其实比较难通过编写程序测试出来,我并没有测试。

中断类型中断标志位置位(EA = 1)中断标志位清零(EA = 1)
外部中断(下降沿触发)硬件(检测到端口电平降低)CPU(软件)(在跳转中断服务程序前)
外部中断(低电平触发)硬件(检测到端口为低电平)硬件(检测到端口不为低电平)
定时器中断硬件(计数溢出)CPU(软件)(在跳转中断服务程序前)

修复问题

知道了标志位置位的原理,修复问题就很简单了,只需要在进入 设置模式 之前将 IE0/1 清零即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void detect_key(void) {
if (K2 == 0) {
switch (current_mode) {
case 0:
while (K2 == 0);
IE0 = 0;
IE1 = 0; // IE0, IE1 清零,修复进入 mode_1 后出现 +1 或 -1 的 bug
current_mode = 1;
break;
case 1:
while (K2 == 0);
current_mode = 0;
break;
case 4:
while (K2 == 0);
current_mode = 0;
break;
}
}
}