嵌入式学习--异常与中断

中断的处理流程

arm 对异常(中断)处理过程:
① 初始化:
a. 设置中断源,让它可以产生中断
b. 设置中断控制器(可以屏蔽某个中断,优先级)
c. 设置 CPU 总开关(使能中断)

② 执行其他程序:正常程序

③ 产生中断:比如按下按键—>中断控制器—>CPU

④ CPU 每执行完一条指令都会检查有无中断/异常产生

⑤ CPU 发现有中断/异常产生,开始处理。

对于不同的异常,跳去不同的地址执行程序。

这地址上,只是一条跳转指令,跳去执行某个函数(地址),这个就是异常向量。

③④⑤都是硬件做的。

⑥ 这些函数做什么事情?
软件做的:
a. 保存现场(各种寄存器)
b. 处理异常(中断):
分辨中断源,再调用不同的处理函数
c. 恢复现场
在这里插入图片描述
异常向量表
异常向量表,每一条指令对应一种异常。

u-boot 或是 Linux 内核,都有类似如下的代码:

_start: b  reset
ldr pc, _undefined_instruction
ldr pc, _software_interrupt
ldr pc, _prefetch_abort
ldr pc, _data_abort
ldr pc, _not_used
ldr pc, _irq //发生中断时,CPU 跳到这个地址执行该指令 **假设地址为 0x18**
ldr pc, _fiq

发生复位时,CPU 就去 执行第 1 条指令:b reset。
发生中断时,CPU 就去执行“ldr pc, _irq”这条指令。
这些指令存放的位置是固定的,比如对于 ARM9 芯片中断向量的地址是 0x18。
当发生中断时,CPU 就强制跳去执行 0x18 处的代码。
在向量表里,一般都是放置一条跳转指令,发生该异常时,CPU 就会执行向量表中的跳转指令,去调用更复杂的函数。

当然,向量表的位置并不总是从 0 地址开始,很多芯片可以设置某个 vector base 寄存器,指定向量
表在其他位置,比如设置 vector base 为 0x80000000,指定为 DDR 的某个地址。但是表中的各个异常向量的偏移地址,是固定的:复位向量偏移地址是 0,中断是 0x18。

Linux 系统对中断的处理

在这里插入图片描述
CPU 运行时,先去取得指令,再执行指令:
① 把内存 a 的值读入 CPU 寄存器 R0
② 把内存 b 的值读入 CPU 寄存器 R1
③ 把 R0、R1 累加,存入 R0
④ 把 R0 的值写入内存 a

CPU 内部的寄存器很重要,如果要暂停一个程序,中断一个程序,就需要把这些寄存器的
值保存下来:这就称为保存现场。
保存在哪里?内存,这块内存就称之为栈。
程序要继续执行,就先从栈中恢复那些 CPU 内部寄存器的值。

a. 函数调用:
在函数 A 里调用函数 B,实际就是中断函数 A 的执行。
那么需要把函数 A 调用 B 之前瞬间的 CPU 寄存器的值,保存到栈里;
再去执行函数 B;

b. 中断处理
进程 A 正在执行,这时候发生了中断。
CPU 强制跳到中断异常向量地址去执行,
这时就需要保存进程 A 被中断瞬间的 CPU 寄存器值,
可以保存在进程 A 的内核态栈,也可以保存在进程 A 的内核结构体中。
中断处理完毕,要继续运行进程 A 之前,恢复这些值。

c. 进程切换
在所谓的多任务操作系统中,我们以为多个程序是同时运行的。
如果我们能感知微秒、纳秒级的事件,可以发现操作系统时让这些程序依次执行一小段时间,进程 A 的
时间用完了,就切换到进程 B。
怎么切换?
切换过程是发生在内核态里的,跟中断的处理类似。
进程 A 的被切换瞬间的 CPU 寄存器值保存在某个地方;
恢复进程 B 之前保存的 CPU 寄存器值,这样就可以运行进程 B 了。
所以,在中断处理的过程中,伴存着进程的保存现场、恢复现场。
进程的调度也是使用栈来保存、恢复现场:
在这里插入图片描述
进程,线程的实例

音乐播放器实例

int main(int argc, char **argv)
{
int key;
while (1)
{
key = read_key();
if (key != -1)
{
switch (key)
{
case NEXT:
select_next_music(); // 在 GUI 选中下一首歌
break;
}
}
else
{
send_music();
}
}
return 0;
}

这个程序只有一条主线,读按键、播放音乐都是顺序执行。
无论按键是否被按下, read_key 函数必须马上返回,否则会使得后续的 send_music 受到阻滞导致音乐
播放不流畅。
读取按键、播放音乐能否分为两个程序进行?可以,但是开销太大:读按键的程序,要把按键通知播放
音乐的程序,进程间通信的效率没那么高。
这时可以用多线程之编程,读取按键是一个线程,播放音乐是另一个线程,它们之间可以通过全局变量
传递数据
,示意代码如下:

多线程音乐播放器

int g_key;
void key_thread_fn()
{
while (1)
{
g_key = read_key();
if (g_key != -1)
{
switch (g_key)
{
case NEXT:
select_next_music(); // 在 GUI 选中下一首歌
break;
}
}
}
}
void music_fn()
{
while (1)
{
if (g_key == STOP)
stop_music();
else
{
send_music();
}
}
}
int main(int argc, char **argv)
{
int key;
create_thread(key_thread_fn);
create_thread(music_fn);
while (1) 
{
sleep(10);
}
return 0;
} 

主函数中开启两个线程,然后两个线程中一直while循环判断,如果发生按键中断,全局变量 g_key就会发生改变,然后就可以分别去执行相关的业务。

这样,按键的读取及 GUI 显示、音乐的播放,可以分开来,不必混杂在一起。
按键线程可以使用阻塞方式读取按键,无按键时是休眠的,这可以节省 CPU 资源。
音乐线程专注于音乐的播放和控制,不用理会按键的具体读取工作。
并且这 2 个线程通过全局变量 g_key 传递数据,高效而简单。
在 Linux 中:资源分配的单位是进程,调度的单位是线程。
也就是说,在一个进程里,可能有多个线程,这些线程共用打开的文件句柄、全局变量等等。
而这些线程,之间是互相独立的,“同时运行”,也就是说:每一个线程,都有自己的栈

硬件中断、软件中断
在这里插入图片描述

Linux 系统把中断的意义扩展了,对于按键中断等硬件产生的中断,称之为“硬件中断”(hard irq)。
每个硬件中断都有对应的处理函数,比如按键中断、网卡中断的处理函数肯定不一样,相对的,还可以人为地制造中断:软件中断(soft irq),如下图所示:
在这里插入图片描述
硬件中断处理完才去处理软件中断,Linux 系统中,各种硬件中断频繁发生,至少定时器中断每 10ms 发生一次。

中断处理两大原则:不能嵌套、越快越好。

当一个中断要耗费很多时间来处理时,它的坏处是:在这段时间内,其他中断无法被处理。换句话说,
在这段时间内,系统是关中断的。如果某个中断就是要做那么多事,我们能不能把它拆分成两部分:紧急的、不紧急的?上半部处理紧急的,在此期间关中断不能被其他中断打断,下半部处理非紧急的,开中断,可以被打断。

中断下半部的实现有很多种方法:tasklet(小任务)、work queue(工作队列)

1.下半部要做的事情耗时不是太长:tasklet
当下半部比较耗时但是能忍受,并且它的处理比较简单时,可以用 tasklet 来处理下半部。tasklet 是
使用软件中断来实现,
在这里插入图片描述
总结:
a. 中断的处理可以分为上半部,下半部
b. 中断上半部,用来处理紧急的事,它是在关中断的状态下执行的
c. 中断下半部,用来处理耗时的、不那么紧急的事,它是在开中断的状态下执行的
d. 中断下半部执行时,有可能会被多次打断,有可能会再次发生同一个中断
e. 中断上半部执行完后,触发中断下半部的处理
f. 中断上半部、下半部的执行过程中,不能休眠:中断休眠的话,以后谁来调度进程啊?

2.下半部要做的事情太多并且很复杂:工作队列
如果中断要做的事情实在太耗时,那就不能用软件中断来做,而应该用内核线程来做:在中断上
半部唤醒内核线程。内核线程和 APP 都一样竞争执行,APP 有机会执行,系统不会卡顿。
这个内核线程是系统帮我们创建的,一般是 kworker 线程,内核中有很多这样的线程:
总结:
a. 很耗时的中断处理,应该放到线程里去
b. 可以使用 work、work queue
c. 在中断上半部调用 schedule_work 函数,触发 work 的处理
d. 既然是在线程中运行,那对应的函数可以休眠。

3。新技术:threaded irq

在这里插入图片描述
你可以只提供 thread_fn,系统会为这个函数创建一个内核线程。发生中断时,内核线程就会执行这个
函数。新技术 threaded irq,为每一个中断都创建一个内核线程;多个中断的内核线程可以分配到多个 CPU上执行,这提高了效率。

Linux 中断系统中的重要数据结构

最核心的结构体是 irq_desc,之前为了易于理解,我们说在 Linux 内核中有一个中断数组,对于每一
个硬件中断,都有一个数组项,这个数组就是 irq_desc 数组。
在这里插入图片描述

外部设备 1、外部设备 n 共享一个 GPIO 中断 B,多个 GPIO 中断汇聚到 GIC(通用中断控制器)的 A 号中断,GIC 再去中断 CPU。那么软件处理时就是反过来,先读取 GIC 获得中断号 A,再细分出 GPIO 中断 B,最后判断是哪一个外部芯片发生了中断
在这里插入图片描述

irqaction 结构体 -------外部设备处理函数

在这里插入图片描述
当调用 request_irq、request_threaded_irq 注册中断处理函数时,内核就会构造一个 irqaction 结
构体。在里面保存 name、dev_id 等,最重要的是 handler、thread_fn、thread。
handler 是中断处理的上半部函数,用来处理紧急的事情。
thread_fn 对应一个内核线程 thread,当 handler 执行完毕,Linux 内核会唤醒对应的内核线程。在内
核线程里,会调用 thread_fn 函数。
可以提供 handler 而不提供 thread_fn,就退化为一般的 request_irq 函数。
可以不提供 handler 只提供 thread_fn,完全由内核线程来处理中断。
也可以既提供 handler 也提供 thread_fn,这就是中断上半部、下半部。

irq_data 结构体
在这里插入图片描述

它就是个中转站,里面有 irq_chip 指针 irq_domain 指针,都是指向别的结构体。
比较有意思的是 irq、 hwirq, irq 是软件中断号, hwirq 是硬件中断号。比如上面我们举的例子,在 GPIO
中断 B 是软件中断号,可以找到 irq_desc[B]这个数组项;GPIO 里的第 x 号中断,这就是 hwirq。
谁来建立 irq、hwirq 之间的联系呢?由 irq_domain 来建立。irq_domain 会把本地的 hwirq 映射为全
局的 irq,什么意思?比如 GPIO 控制器里有第 1 号中断,UART 模块里也有第 1 号中断,这两个“第 1 号中
断”是不一样的,它们属于不同的“域”──irq_domain。
irq_domain 结构体
在这里插入图片描述

irq_domain 结构体中有一个 irq_domain_ops 结构体,里面有各种操作函数,主要是:
① xlate
用来解析设备树的中断属性,提取出 hwirq、type 等信息。
② map
把 hwirq 转换为 irq。
irq_chip 结构体
在这里插入图片描述

我们在 request_irq 后,并不需要手工去使能中断,原因就是系统调用对应的 irq_chip 里的函数帮我
们使能了中断。
我们提供的中断处理函数中,也不需要执行主芯片相关的清中断操作,也是系统帮我们调用 irq_chip
中的相关函数。
但是对于外部设备相关的清中断操作,还是需要我们自己做的。
就像上面图里的“外部设备 1 “、“外部设备 n”,外设备千变万化,内核里可没有对应的清除中断操作

在这里插入图片描述
下一篇写具体的中断代码

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值