易托管建站工具,做的比较漂亮的中国网站,东莞信科做网站,网址注册平台#xff1c;Linux开发#xff1e;驱动开发 -之-内核定时器与中断
交叉编译环境搭建#xff1a; #xff1c;Linux开发#xff1e; linux开发工具-之-交叉编译环境搭建
uboot移植可参考以下#xff1a; #xff1c;Linux开发#xff1e; -之-系统移植 uboot移植过程详…Linux开发驱动开发 -之-内核定时器与中断
交叉编译环境搭建 Linux开发 linux开发工具-之-交叉编译环境搭建
uboot移植可参考以下 Linux开发 -之-系统移植 uboot移植过程详细记录第一部分 Linux开发 -之-系统移植 uboot移植过程详细记录第二部分 Linux开发 -之-系统移植 uboot移植过程详细记录第三部分(uboot移植完结)
Linux内核及设备树移植可参考以下 Linux开发系统移植 -之- linux内核移植过程详细记录第一部分 Linux开发系统移植 -之- linux内核移植过程详细记录第二部分完结
Linux文件系统构建移植参考以下 Linux开发系统移植 -之- linux构建BusyBox根文件系统及移植过程详细记录 Linux开发系统移植 -之-使用buildroot构建BusyBox根文件系统
Linux驱动开发参考以下 Linux开发驱动开发 -之-pinctrl子系统 Linux开发驱动开发 -之-gpio子系统 Linux开发驱动开发 -之-基于pinctrl/gpio子系统的LED驱动 Linux开发驱动开发 -之-基于pinctrl/gpio子系统的beep驱动 Linux开发驱动开发 -之-资源的并发与竞争处理
一 前言
定时器是我们最常用到的功能一般用来完成定时功能熟悉一下 Linux 内核提供的定时器 API 函数通过这些定时器 API 函数我们可以完成很多要求定时的应用。Linux内核也提供了短延时函数比如微秒、纳秒、毫秒延时函数本章我们就来学习一下这些和时间有关的功能。中断也是频繁使用的功能Linux 内核提供了完善的中断框架我们只需要申请中断然后注册中断处理函数即可使用非常方便不需要一系列复杂的寄存器配置。下面我们就来一一分析。
二 内核定时器
2.1 内核时间管理-系统时钟
如果读者之前接触过UCOS或FreeRTOS就会知道UCOS或FreeRTOS是需要一个硬件定时器提供系统时钟的一般情况下使用Systick作为系统时钟源但也有使用普通定时器 作为时钟源的。相同的Linux要能正常运行也是需要有一个系统时钟的至于所使用的的哪个定时器作为系统时钟源这个笔者也不知道哈哈毕竟linux的东西太多了有兴趣的读者可以自己查阅资料了解一下(如果有熟悉的读者也可以分享一下哈)有一份文档中有描述一个通用定时器在《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf》的“chapter B8 The Generic Timer”章节至于实际linux是否是不是使用这个定时器有兴趣的可以研究下。 在Linux内核有很多需要时间管理来参与的场景或程序比如在周期性的调度程序或者延时程序中其实对于开发驱动程序来说我们不用太关注具体的系统时钟是什么因为这是系统/liunx内核工程师关注的内容对于驱动编写来说只需关注如何使用内核定时器完成相关时间管理即可。硬件定时器提供时钟源时钟源的频率可以设置 设置好以后就周期性的产生定时中断系统使用定时中断来计时。中断周期性产生的频率就是系统频率也叫做节拍率(tick rate)(有的资料也叫系统频率)比如1000Hz, 500Hz100Hz 等等这些说的就是系统节拍率。系统节拍率是可以设置的单位是 Hz我们在编译 Linux 内核的时候可以通过图形化界面设置系统节拍率编译Linux内核前使用‘make menuconfig 命令即可打开配置界面按照如下路径设置配置
- Kernel Features - Timer frequency (choice [y]) 在上图配置中有多个配置值配置自己想要的频率即可保存到“arch/arm/configs/imx_v7_water_emmc_defconfig”后可在该文件内查看配置如下 该定义会被linux内核使用来配置时钟频率。相关引用如下
路径include/asm-generic/param.h
#ifndef __ASM_GENERIC_PARAM_H
#define __ASM_GENERIC_PARAM_H#include uapi/asm-generic/param.h# undef HZ
# define HZ CONFIG_HZ /* Internal kernel timer frequency */
# define USER_HZ 100 /* some user interfaces are */
# define CLOCKS_PER_SEC (USER_HZ) /* in ticks like times() */
#endif /* __ASM_GENERIC_PARAM_H */ 从第7行的注释也能看出其作用是作为linux内部定时器频率。
2.2 内核时间管理-节拍率
在2.1小节我们知道了配置linux内部定时器频率在上述定义的宏HZ表示的是1秒的节拍数也就是我们常说的频率了。 大家看到设置的值是100Hz相对是一个比较小的值按我们学过STM32等单片机的了解怎么也得MHz起步吧我们知道频率越高需要的中断次数也就越频繁频率的高低会有其优缺点之处主要如下 (1) 优点较高的频率/节拍率能够提高系统时间精度如采用100Hz频率时间精度为10ms而采用1000Hz频率时间精度是1ms后者相对前者提升了10倍。高精度时钟的好处有很多对于那些对时间要求严格的函数来说能够以更高的精度运行时间测量也更加准确。 (2)缺点高节拍率会导致中断的产生更加频繁频繁的中断会加剧系统的负担1000Hz 和 100Hz的系统节拍率相比系统要花费 10 倍的“精力”去处理中断。中断服务函数占用处理器的时间增加但是现在的处理器性能都很强大所以采用 1000Hz 的系统节拍率并不会增加太大的负载压力。根据自己的实际情况选择合适的系统节拍率作者所做实验全部采用默认的100Hz 系统节拍率。
讲到节拍率如果读者以前在开发过程中有涉及到开机时间等要求的相比接触过全局变量jiffiesLinux 内核使用全局变量 jiffies 来记录系统从启动以来的系统节拍数系统启动的时候会将 jiffies 初始化为 0jiffies 定义定义如下
路径include/linux/jiffies.h
/** The 64-bit value is not atomic - you MUST NOT read it* without sampling the sequence number in jiffies_lock.* get_jiffies_64() will do this for you as appropriate.*/
extern u64 __jiffy_data jiffies_64;
extern unsigned long volatile __jiffy_data jiffies;第 76 行定义了一个 64 位的 jiffies_64。 第 77 行定义了一个 unsigned long 类型的 32 位的 jiffies。 jiffies_64 和 jiffies 其实是同一个东西jiffies_64 用于 64 位系统而 jiffies 用于 32 位系统。为了兼容不同的硬件jiffies 其实就是 jiffies_64 的低 32 位jiffies_64 和 jiffies 的结构如下图 当我们访问 jiffies 的时候其实访问的是 jiffies_64 的低 32 位使用 get_jiffies_64 这个函数可以获取 jiffies_64 的值。在 32 位的系统上读取 jiffies 的值在 64 位的系统上 jiffes 和 jiffies_64表示同一个变量因此也可以直接读取 jiffies 的值。所以不管是 32 位的系统还是 64 位系统都可以使用 jiffies。 在2.1小节说了 HZ 表示每秒的节拍数jiffies 表示系统运行的 jiffies 节拍数所以 jiffies/HZ 就是系统运行时间单位为秒。 time jiffies/HZ,以100hz为例则time jiffies/100 (单位秒) 不管是 32 位还是 64 位的 jiffies都有溢出的风险溢出以后会重新从 0 开始计数相当于绕回来了因此有些资料也将这个现象也叫做绕回。假如 HZ 为最大值 1000 的时候32 位的 jiffies 大约只需要 49.7(0xffffffff / 1000/3600/24) 天就发生了绕回对于 64 位的 jiffies 来说大概需要5.8 亿年才能绕回因此 jiffies_64 的绕回忽略不计。处理 32 位 jiffies 的绕回显得尤为重要Linux 内核提供了如下表所示的几个 API 函数来处理绕回。 函数路径include/linux/jiffies.h
函数描述time_after(unkown, known)unkown 通常为 jiffiesknown 通常是需要对比的值。time_before(unkown, known)unkown 通常为 jiffiesknown 通常是需要对比的值。time_after_eq(unkown, known)unkown 通常为 jiffiesknown 通常是需要对比的值。time_before_eq(unkown, known)unkown 通常为 jiffiesknown 通常是需要对比的值。
函数使用描述结果time_after(unkown, known)如果 unkown 超过 known 的话返回true否则返回falsetime_before(unkown, known)如果 unkown 没有超过 known 的话返回true否则返回falsetime_after_eq(unkown, known)如果 unkown 超过或等于 known 的话返回true否则返回falsetime_before_eq(unkown, known)如果 unkown 没有超过或等于 known 的话返回true否则返回false
使用示例如下
unsigned long timeout;
timeout jiffies (1 * HZ); /* 超时的时间点 *//*************************************具体功能实现代码************************************//* 判断有没有超时 */if(time_before(jiffies, timeout)) {/* 超时未发生 */} else {/* 超时发生 */}unsigned long timeout;
timeout jiffies (1 * HZ); /* 超时的时间点 *//*************************************具体功能实现代码************************************//* 判断有没有超时 */if(time_after(jiffies, timeout)) {/* 超时发生 */} else {/* 超时未发生 */}在上述两个示例中变量timeout表示超时时间点上述中设置判断代码执行时间不超过1s那么超时时间点就是开始运行代码的时间(当前jiffies)超时时间1s(1*HZ)然后在代码运行后判断当前时间(jiffies) 与设置的超时时间点(timeout)关系。 在time_before示例中表示jiffies 比timeout大则不超时反之超时 在time_after示例中表示jiffies 比timeout小则超时反之不超时
前面我们都是以jiffies 节拍率来计算时间为了开发方便些Linux内核提供了一些转换函数方便jiffies 与ms\us\ns之间的转换如下表 函数路径include/linux/jiffies.h
函数描述int jiffies_to_msecs(const unsigned long j)将 jiffies 类型的参数 j 分别转换为对应的毫秒int jiffies_to_usecs(const unsigned long j)将 jiffies 类型的参数 j 分别转换为对应的微秒int jiffies_to_nsecs(const unsigned long j)将 jiffies 类型的参数 j 分别转换为对应的纳秒long msecs_to_jiffies(const unsigned int m将毫秒转换为 jiffies 类型。long usecs_to_jiffies(const unsigned int u)将微秒转换为 jiffies 类型。unsigned long nsecs_to_jiffies(u64 n)将纳秒转换为 jiffies 类型。
2.3 内核定时器介绍
在学习单片机的时候定时器是比较常用的功能可以做周期性定时工作也可以做捕获计时等功能。Linux 内核定时器采用系统时钟来实现并不是imx6ull中的的 PIT 等硬件定时器。Linux 内核定时器使用很简单只需要提供超时时间(相当于定时值)和定时处理函数即可当超时时间到了以后设置的定时处理函数就会执行和我们使用硬件定时器的套路一样只是使用内核定时器不需要做一大堆的寄存器初始化工作。 值得注意的内核定时器并不是周期性运行的超时以后就会自动关闭因此如果想要实现周期性定时那么就需要在定时处理函数中重新开启定时器。 Linux 内核使用 timer_list 结构体表示内核定时器内容如下
路径include/linux/timer.h
struct timer_list {/** All fields that change during normal runtime grouped to the* same cacheline*/struct list_head entry;unsigned long expires; /* 定时器超时时间单位是节拍数 */struct tvec_base *base;void (*function)(unsigned long); /* 定时处理函数 */unsigned long data; /* 要传递给 function 函数的参数 */int slack;#ifdef CONFIG_TIMER_STATSint start_pid;void *start_site;char start_comm[16];
#endif
#ifdef CONFIG_LOCKDEPstruct lockdep_map lockdep_map;
#endif
};当要使用内核定时器时要先定义一个 timer_list 变量表示定时器tiemr_list 结构体的expires 成员变量表示超时时间单位为节拍数。比如我们现在需要定义一个周期为 4 秒的定时器那么这个定时器的超时时间就是 jiffies(4HZ)因此 expiresjiffies(2HZ)。function 就是定时器超时以后的定时处理函数我们要做的工作就放到这个函数里面这个定时处理函数是需要我们自己编写的。
2.4 内核定时器API
在2.3小节介绍定时器中定义好定时器之后就需要通过linux内核提供的相关API来操作初始化这个定时器了。相关API函数如下表 函数路径include/linux/timer.h
函数原型描述参数返回值void init_timer(struct timer_list *timer)init_timer 函数负责初始化 timer_list 类型变量当我们定义了一个 timer_list 变量以后一定要先用 init_timer 初始化一下timer要初始化定时器返回值没有返回值void add_timer(struct timer_list *timer)add_timer 函数用于向 Linux 内核注册定时器使用 add_timer 函数向内核注册定时器以后定时器就会开始运行timer要注册的定时器返回值没有返回值void del_timer(struct timer_list *timer)del_timer 函数用于删除一个定时器不管定时器有没有被激活都可以使用此函数删除。在多处理器系统上定时器可能会在其他的处理器上运行因此在调用 del_timer 函数删除定时器之前要先等待其他处理器的定时处理器函数退出。timer要删除的定时器返回值0定时器还没被激活1定时器已经激活。void del_timer_sync(struct timer_list *timer)del_timer_sync 函数是 del_timer 函数的同步版会等待其他处理器使用完定时器再删除del_timer_sync 不能使用在中断上下文中。timer要初始化定时器返回值0定时器还没被激活1定时器已经激活。int mod_timer(struct timer_list *timer, unsigned long expires)mod_timer 函数用于修改定时值如果定时器还没有激活的话mod_timer 函数会激活定时器。timer要修改超时时间(定时值)的定时器。expires修改后的超时时间。返回值0调用 mod_timer 函数前定时器未被激活1调用 mod_timer 函数前定时器已被激活。
了解了内核定时器相关API后面我们将使用这些API为我们开发一些功能代码。
2.5 Linux 内核短延时函数
其实在linux内核中提供了一些较短的延时函数有时候我们需要在内核中实现短延时尤其是在 Linux 驱动中。Linux 内核提供了毫秒、微秒和纳秒延时函数如下表 路径include/linux/delay.h
函数描述void ndelay(unsigned long nsecs)延时纳秒函数void udelay(unsigned long usecs)延时微秒函数void mdelay(unsigned long mseces)延时毫秒函数
使用示例 路径drivers/i2c/busses/i2c-au1550.c 在该驱动中有如下内容
路径drivers/i2c/busses/i2c-au1550.c
...
#include linux/delay.h
.....static int wait_xfer_done(struct i2c_au1550_data *adap)
{int i;/* Wait for Tx Buffer Empty */for (i 0; i adap-xfer_timeout; i) {if (RD(adap, PSC_SMBSTAT) PSC_SMBSTAT_TE)return 0;udelay(1);}return -ETIMEDOUT;
}
其中头文件就是引用linux内核延时函数的头文件 其次代码中的udelay(1); 就是调用1微秒延时。该函数的功能就是一直循环等待发送完成每1微秒判断一次是否发送完成。
2.6 内核定时器使用模板
使用内核定时器模板如下
#include linux/timer.h
#include linux/delay.h
#include linux/semaphore.h/* XXX 设备结构体 */
struct XXX_dev{dev_t devid; /* 设备号 */struct cdev cdev; /* cdev */struct class *class; /* 类 */struct device *device; /* 设备 */int major; /* 主设备号 */int minor; /* 次设备号 */struct device_node *nd; /* 设备节点 */int timeperiod; /* 定时周期,单位为 ms */struct timer_list timer; /* 定义一个定时器 */spinlock_t lock; /* 定义自旋锁 *//*所开发设备的其它参数*/....
};struct XXX_dev XXXdev; /* XXX设备 *//*
驱动中的其它相关函数
.......
*//* 定时器回调函数 */
void timer_function(unsigned long arg)
{struct XXX_dev *dev (struct XXX_dev *)arg;int timerperiod;unsigned long flags;/* 定时器中需要做的内容.....*//* 重启定时器 */spin_lock_irqsave(dev-lock, flags);timerperiod dev-timeperiod;spin_unlock_irqrestore(dev-lock, flags);mod_timer(dev-timer, jiffies msecs_to_jiffies(dev-timeperiod));
}static int __init XXX_init(void)
{/* 初始化自旋锁 */spin_lock_init(XXXdev.lock);/*驱动初始化的 其它操作........*//* 初始化 timer设置定时器处理函数,还未设置周期所有不会激活定时器 */init_timer(timerdev.timer);timerdev.timer.function timer_function;timerdev.timer.data (unsigned long)timerdev;return 0;
}/*驱动的其它函数内容........*/
三 单片机STM32中的中断
相信在接触Linux之前大家都接触过STM32单片机我们先来看下STM32的中断然后在分析IMX6ULL的中断这样有个对比能更好的理解。
在STM32中的中断系统主要的关键内容有如下所列 (1) 中断向量表 (2) NVIC-内嵌向量中断控制器 (3) 中断设置使能 (4) 中断服务函数
3.1 中断向量表
中断向量表是一个表这个表里面存放的是中断向量。中断服务程序的入口地址或存放中断服务程序的首地址成为中断向量因此中断向量表是一系列中断服务程序入口地址组成的表。这些中断服务程序(函数)在中断向量表中的位置是由半导体厂商定好的当某个中断被触发以后就会自动跳转到中断向量表中对应的中断服务程序(函数)入口地址处。中断向量表在整个程序的最前面比如 STM32F103 的中断向量表如下所示 在STM32的汇编文件startup_stm32f10x_hd.s中
............AREA RESET, DATA, READONLYEXPORT __VectorsEXPORT __Vectors_EndEXPORT __Vectors_Size__Vectors DCD __initial_sp ; Top of StackDCD Reset_Handler ; Reset HandlerDCD NMI_Handler ; NMI HandlerDCD HardFault_Handler ; Hard Fault HandlerDCD MemManage_Handler ; MPU Fault HandlerDCD BusFault_Handler ; Bus Fault HandlerDCD UsageFault_Handler ; Usage Fault HandlerDCD 0 ; ReservedDCD 0 ; ReservedDCD 0 ; ReservedDCD 0 ; ReservedDCD SVC_Handler ; SVCall HandlerDCD DebugMon_Handler ; Debug Monitor HandlerDCD 0 ; ReservedDCD PendSV_Handler ; PendSV HandlerDCD SysTick_Handler ; SysTick Handler; External InterruptsDCD WWDG_IRQHandler ; Window WatchdogDCD PVD_IRQHandler ; PVD through EXTI Line detectDCD TAMPER_IRQHandler ; TamperDCD RTC_IRQHandler ; RTCDCD FLASH_IRQHandler ; FlashDCD RCC_IRQHandler ; RCCDCD EXTI0_IRQHandler ; EXTI Line 0DCD EXTI1_IRQHandler ; EXTI Line 1DCD EXTI2_IRQHandler ; EXTI Line 2DCD EXTI3_IRQHandler ; EXTI Line 3DCD EXTI4_IRQHandler ; EXTI Line 4DCD DMA1_Channel1_IRQHandler ; DMA1 Channel 1DCD DMA1_Channel2_IRQHandler ; DMA1 Channel 2DCD DMA1_Channel3_IRQHandler ; DMA1 Channel 3DCD DMA1_Channel4_IRQHandler ; DMA1 Channel 4DCD DMA1_Channel5_IRQHandler ; DMA1 Channel 5DCD DMA1_Channel6_IRQHandler ; DMA1 Channel 6DCD DMA1_Channel7_IRQHandler ; DMA1 Channel 7DCD ADC1_2_IRQHandler ; ADC1 ADC2DCD USB_HP_CAN1_TX_IRQHandler ; USB High Priority or CAN1 TXDCD USB_LP_CAN1_RX0_IRQHandler ; USB Low Priority or CAN1 RX0DCD CAN1_RX1_IRQHandler ; CAN1 RX1DCD CAN1_SCE_IRQHandler ; CAN1 SCEDCD EXTI9_5_IRQHandler ; EXTI Line 9..5DCD TIM1_BRK_IRQHandler ; TIM1 BreakDCD TIM1_UP_IRQHandler ; TIM1 UpdateDCD TIM1_TRG_COM_IRQHandler ; TIM1 Trigger and CommutationDCD TIM1_CC_IRQHandler ; TIM1 Capture CompareDCD TIM2_IRQHandler ; TIM2DCD TIM3_IRQHandler ; TIM3DCD TIM4_IRQHandler ; TIM4DCD I2C1_EV_IRQHandler ; I2C1 EventDCD I2C1_ER_IRQHandler ; I2C1 ErrorDCD I2C2_EV_IRQHandler ; I2C2 EventDCD I2C2_ER_IRQHandler ; I2C2 ErrorDCD SPI1_IRQHandler ; SPI1DCD SPI2_IRQHandler ; SPI2DCD USART1_IRQHandler ; USART1DCD USART2_IRQHandler ; USART2DCD USART3_IRQHandler ; USART3DCD EXTI15_10_IRQHandler ; EXTI Line 15..10DCD RTCAlarm_IRQHandler ; RTC Alarm through EXTI LineDCD USBWakeUp_IRQHandler ; USB Wakeup from suspendDCD TIM8_BRK_IRQHandler ; TIM8 BreakDCD TIM8_UP_IRQHandler ; TIM8 UpdateDCD TIM8_TRG_COM_IRQHandler ; TIM8 Trigger and CommutationDCD TIM8_CC_IRQHandler ; TIM8 Capture CompareDCD ADC3_IRQHandler ; ADC3DCD FSMC_IRQHandler ; FSMCDCD SDIO_IRQHandler ; SDIODCD TIM5_IRQHandler ; TIM5DCD SPI3_IRQHandler ; SPI3DCD UART4_IRQHandler ; UART4DCD UART5_IRQHandler ; UART5DCD TIM6_IRQHandler ; TIM6DCD TIM7_IRQHandler ; TIM7DCD DMA2_Channel1_IRQHandler ; DMA2 Channel1DCD DMA2_Channel2_IRQHandler ; DMA2 Channel2DCD DMA2_Channel3_IRQHandler ; DMA2 Channel3DCD DMA2_Channel4_5_IRQHandler ; DMA2 Channel4 Channel5
__Vectors_End__Vectors_Size EQU __Vectors_End - __VectorsAREA |.text|, CODE, READONLY
............上述代码内容就是 STM32F103 的中断向量表中断向量表都是链接到代码的最前面在比如一般 ARM 处理器都是从地址 0X00000000 开始执行指令的那么中断向量表就是从 0X00000000 开始存放的。上述代码中第 7 行的“__initial_sp”就是第一条中断向量存放的是栈顶指针接下来是第 8 行复位中断复位函数 Reset_Handler 的入口地址依次类推直到第84 行的最后一个中断服务函数 DMA2_Channel4_5_IRQHandler 的入口地址这样 STM32F103 的中断向量表就建好了。
我们说 ARM 处理器都是从地址 0X00000000 开始运行的但是我们学习 STM32 的时候代码是下载到 0X8000000 开始的存储区域中。因此中断向量表是存放到 0X8000000 地址处的而不是 0X00000000这样不是就出错了吗为了解决这个问题Cortex-M 架构引入了一个新的概念——中断向量表偏移通过中断向量表偏移就可以将中断向量表存放到任意地址处中断向量表偏移配置在函数 SystemInit 中完成通过向 SCB_VTOR 寄存器写入新的中断向量表首地址即可在文件“system_stm32f10x.c”中代码如下所示
void SystemInit (void)
{/* Reset the RCC clock configuration to the default reset state(for debug purpose) *//* Set HSION bit */RCC-CR | (uint32_t)0x00000001;/* Reset SW, HPRE, PPRE1, PPRE2, ADCPRE and MCO bits */
#ifndef STM32F10X_CLRCC-CFGR (uint32_t)0xF8FF0000;
#elseRCC-CFGR (uint32_t)0xF0FF0000;
#endif /* STM32F10X_CL */ /* Reset HSEON, CSSON and PLLON bits */RCC-CR (uint32_t)0xFEF6FFFF;/* Reset HSEBYP bit */RCC-CR (uint32_t)0xFFFBFFFF;/* Reset PLLSRC, PLLXTPRE, PLLMUL and USBPRE/OTGFSPRE bits */RCC-CFGR (uint32_t)0xFF80FFFF;#ifdef STM32F10X_CL/* Reset PLL2ON and PLL3ON bits */RCC-CR (uint32_t)0xEBFFFFFF;/* Disable all interrupts and clear pending bits */RCC-CIR 0x00FF0000;/* Reset CFGR2 register */RCC-CFGR2 0x00000000;
#elif defined (STM32F10X_LD_VL) || defined (STM32F10X_MD_VL) || (defined STM32F10X_HD_VL)/* Disable all interrupts and clear pending bits */RCC-CIR 0x009F0000;/* Reset CFGR2 register */RCC-CFGR2 0x00000000;
#else/* Disable all interrupts and clear pending bits */RCC-CIR 0x009F0000;
#endif /* STM32F10X_CL */#if defined (STM32F10X_HD) || (defined STM32F10X_XL) || (defined STM32F10X_HD_VL)#ifdef DATA_IN_ExtSRAMSystemInit_ExtMemCtl(); #endif /* DATA_IN_ExtSRAM */
#endif /* Configure the System clock frequency, HCLK, PCLK2 and PCLK1 prescalers *//* Configure the Flash Latency cycles and enable prefetch buffer */SetSysClock();#ifdef VECT_TAB_SRAMSCB-VTOR SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM. */
#elseSCB-VTOR FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH. */
#endif
}第 265 行和第 267 行就是设置中断向量表偏移第 265 行是将中断向量表设置到 RAM 中第267 行是将中断向量表设置到 ROM 中基本都是将中断向量表设置到 ROM 中也就是地址0X8000000 处。第 267 行用到了FALSH_BASE 和 VECT_TAB_OFFSET这两个都是宏定义如下所示
文件system_stm32f10x.c
/* #define VECT_TAB_SRAM */
#define VECT_TAB_OFFSET 0x0 /*! Vector Table base offset field. This value must be a multiple of 0x200. */因此第 267 行的代码就是SCB-VTOR0X080000000中断向量表偏移设置完成。
通过上面的讲解我们了解了两个跟 STM32 中断有关的概念中断向量表和中断向量表偏移那么这个跟 I.MX6U 有什么关系呢因为 I.MX6U 所使用的 Cortex-A7 内核也有中断向量表和中断向量表偏移而且其含义和 STM32 是一模一样的只是用到的寄存器不同而已概念完全相同
3.2 中断控制器
有那么多中断那中断系统得有个管理机构对于 STM32 这种 Cortex-M 内核的单片机来说这个管理机构叫做 NVIC全称叫做 Nested Vectored Interrupt Controller。这里不作详细的讲解既然 Cortex-M 内核有个中断系统的管理机构—NVIC那么 I.MX6U 所使用的 Cortex-A7 内核是不是也有个中断系统管理机构答案是肯定的不过 Cortex-A 内核的中断管理机构不叫做NVIC而是叫做 GIC全称是 general interrupt controller。
3.3 中断设置使能
要使用某个外设的中断肯定要先使能这个外设的中断以 STM32F103 的 PA0 这个 IO 为例假如我们要使用 PA0 的输入中断肯定要使用如下代码来使能对应的中断
NVIC_InitStructure.NVIC_IRQChannel EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 0x02; //抢占优先级 2
NVIC_InitStructure.NVIC_IRQChannelSubPriority 0x02; //子优先级 2
NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; //使能外部中断通道
NVIC_Init(NVIC_InitStructure);上述代码就是使能 PA0 对应的 EXTI0 中断同理如果要使用 I.MX6U 的某个中断的话也需要使能其对应的中断。
3.4 中断服务函数
我们使用中断的目的就是为了使用中断服务函数当中断发生以后中断服务函数就会被调用我们要处理的工作就可以放到中断服务函数中去完成。同样以 STM32F103 的 PA0 为例其中断服务函数如下所示
/* 外部中断 0 服务程序 */
void EXTI0_IRQHandler(void)
{
/* 中断处理代码 */
}在3.1小节的代码31行中就已经设置好EXTI Line 0的中断函数入口为EXTI0_IRQHandler 当 PA0 引脚的中断触发以后就会调用其对应的中断处理函数 EXTI0_IRQHandler我们可以在函数 EXTI0_IRQHandler 中添加中断处理代码。同理I.MX6U 也有中断服务函数当某个外设中断发生以后就会调用其对应的中断服务函数。通过对 STM32 中断系统的回顾我们知道了 Cortex-M 内核的中断处理过程那么 Cortex-A 内核的中断处理过程是否是一样的有什么异同呢接下来我们看看Cortex-A7 内核的中断系统。
四 IMX6ULL中的中断
4.1 中断向量表
与Cortex-M一样Cortex-A7 也有中断向量表中断向量表也是在代码的最前面。Cortex-A7 内核有 8 个异常中断这 8 个异常中断的中断向量表如下表 所示
向量地址中断类型中断模式0x00复位中断(Rest)特权模式(SVC)0x04未定义指令中断(Undefined Instruction)未定义指令中止模式(Undef)0x08软中断(Software Interrupt,SWI)特权模式(SVC)0x0C指令预取中止中断(Prefetch Abort)中止模式0x10数据访问中止中断(Data Abort)中止模式0x14未使用(Not Used)未使用0x18IRQ 中断(IRQ Interrupt)外部中断模式(IRQ)0x1CFIQ 中断(FIQ Interrupt)快速中断模式(FIQ)
中断向量表里面都是中断服务函数的入口地址因此一款芯片有什么中断都是可以从中断向量表看出来的。从上表中可以看出Cortex-A7 一共有 8 个中断而且还有一个中断向量未使用实际只有 7 个中断。
与前面第三章介绍的STM32中断向量表相比Cortex-A7的中断少很多按理解Linux系统应该有很多中断才对STM32拥有的EXTI0_IRQHandler、TIM3_IRQHandler等中断在Cortex-A7中应该也有类似的才对还有SPI、IIC等中断。
Cortex-A 和 Cotex-M 在中断向量表的区别对于 Cortex-M 内核来说中断向量表列举出了一款芯片所有的中断向量包括芯片外设的所有中断。对于 Cotex-A 内核来说并没有这么做在上表中有个 IRQ 中断 Cortex-A 内核 CPU 的所有外部中断都属于这个 IRQ 中断当任意一个外部中断发生的时候都会触发 IRQ 中断。在 IRQ 中断服务函数里面就可以读取指定的寄存器来判断发生的具体是什么中断进而根据具体的中断做出相应的处理。这些外部中断和 IRQ 中断的关系如下图所示 上图左侧的 Software0_IRQn~PMU_IRQ2_IRQ 这些都是 I.MX6U 的中断他们都属于 IRQ 中断。当上图左侧这些中断中任意一个发生的时候 IRQ 中断都会被触发所以我们需要在 IRQ 中断服务函数中判断究竟是左侧的哪个中断发生了然后再做出具体的处理。 在上表中一共有 7 个中断类型简单介绍一下这 7 个中断 ①、复位中断(Rest)CPU 复位以后就会进入复位中断我们可以在复位中断服务函数里面做一些初始化工作比如初始化 SP 指针、DDR 等等。 ②、未定义指令中断(Undefined Instruction)如果指令不能识别的话就会产生此中断。 ③、软中断(Software Interrupt,SWI)由 SWI 指令引起的中断Linux 的系统调用会用 SWI指令来引起软中断通过软中断来陷入到内核空间。 ④、指令预取中止中断(Prefetch Abort)预取指令出错的时候会产生此中断。 ⑤、数据访问中止中断(Data Abort)访问数据出错的时候会产生此中断。 ⑥、IRQ 中断(IRQ Interrupt)外部中断前面已经说了芯片内部的外设中断都会引起此 中断的发生。 ⑦、FIQ 中断(FIQ Interrupt)快速中断如果需要快速处理中断的话就可以使用此中断。 常用的就是复位中断和 IRQ 中断。
4.2 中断控制器
4.2.1 GIC 控制器总览
GIC 是 ARM 公司给 Cortex-A/R 内核提供的一个中断控制器类似 Cortex-M 内核中的NVIC。目前 GIC 有 4 个版本:V1~V4V1 是最老的版本已经被废弃了。V2-V4 目前正在大量的使用。GIC V2 是给 ARMv7-A 架构使用的比如 Cortex-A7、Cortex-A9、Cortex-A15 等V3 和 V4 是给 ARMv8-A/R 架构使用的也就是 64 位芯片使用的。I.MX6U 是 Cortex-A 内核的因此我们主要讲解 GIC V2。GIC V2 最多支持 8 个核。ARM 会根据 GIC 版本的不同研发出不同的 IP 核那些半导体厂商直接购买对应的 IP 核即可比如 ARM 针对 GIC V2 就开发出了 GIC400 这个中断控制器 IP 核。当 GIC 接收到外部中断信号以后就会报给 ARM 内核但是ARM 内核只提供了四个信号给 GIC 来汇报中断情况VFIQ、VIRQ、FIQ 和 IRQ他们之间的关系如下图所示 在上图中GIC 接收众多的外部中断然后对其进行处理最终就只通过四个信号报给 ARM 内核这四个信号的含义如下 VFIQ:虚拟快速 FIQ。 VIRQ:虚拟外部 IRQ。 FIQ:快速中断 IRQ。 IRQ:外部中断 IRQ。 VFIQ 和 VIRQ 是针对虚拟化的我们不讨论虚拟化剩下的就是 FIQ 和 IRQ 了我们前面都讲了很多次了。本次我们只使用 IRQ所以相当于 GIC 最终向 ARM 内核就上报一个 IRQ信号。那么 GIC 是如何完成这个工作的呢在文档《ARM Generic Interrupt Controller(ARM GIC控制器)V2.0.pdf》的第23页有GICV2 总体框图: 上图中左侧部分就是中断源中间部分就是 GIC 控制器最右侧就是中断控制器向处理器内核发送中断信息。我们重点要看的肯定是中间的 GIC 部分GIC 将众多的中断源分为 分为三类 ①、SPI(Shared Peripheral Interrupt),共享中断顾名思义所有 Core 共享的中断这个是最常见的那些外部中断都属于 SPI 中断(注意不是 SPI 总线那个中断) 。比如按键中断、串口中断等等这些中断所有的 Core 都可以处理不限定特定 Core。 ②、PPI(Private Peripheral Interrupt)私有中断我们说了 GIC 是支持多核的每个核肯定有自己独有的中断。这些独有的中断肯定是要指定的核心处理因此这些中断就叫做私有中断。 ③、SGI(Software-generated Interrupt)软件中断由软件触发引起的中断通过向寄存器GICD_SGIR 写入数据来触发系统会使用 SGI 中断来完成多核之间的通信。
4.2.2 GIC 控制器中断 ID
中断源有很多为了区分这些不同的中断源肯定要给他们分配一个唯一 ID这些 ID 就是中断 ID。每一个 CPU 最多支持 1020 个中断 ID中断 ID 号为 ID0~ID1019。这 1020 个 ID 包含了 PPI、SPI 和 SGI那么这三类中断是如何分配这 1020 个中断 ID 的呢这 1020 个 ID 分配如下 ID0~ID15这 16 个 ID 分配给 SGI。 ID16~ID31这 16 个 ID 分配给 PPI。 ID32~ID1019这 988 个 ID 分配给 SPI像 GPIO 中断、串口中断等这些外部中断 至于具体到某个 ID 对应哪个中断那就由半导体厂商根据实际情况去定义了。
比如 I.MX6U 的总共使用了 128 个中断 ID加上前面属于 PPI 和 SGI 的 32 个 IDI.MX6U 的中断源共有 12832160个这 128 个中断 ID 对应的中断在《I.MX6UL 参考手册》的“3.2 CortexA7 interrupts”小节部分内容如下
IRQ ID中断源中断描述32IOMUXCIOMUXC的通用寄存器1。用于在启动时通知核心出现异常情况。33DAP调试访问端口中断请求。34SDMA来自所有通道的SDMA中断请求。35TSCTSC((触摸)中断。36SNVS_LP /SNVS_HPSNVS 中断37LCDIF………88EPIT1EPIT1输出比较中断。89EPIT2EPIT2输出比较中断。90GPIO1INT7中断请求91GPIO1INT6中断请求92GPIO1INT5中断请求93GPIO1INT4中断请求………149PWM8累计中断线路。包括比较中断边沿中断FIFO中断等150ENET1ENET1中断151ENET1ENET1 1588定时器中断[同步]请求。152ENET2ENET2中断153ENET2MAC 0 1588定时器中断[同步]请求。154ReservedReserved155ReservedReserved156ReservedReserved157ReservedReserved158ReservedReserved159PMUcore、gpu或soc调节器上的Brown out事件。
更多中断源请查阅《I.MX6UL 参考手册》的“3.2 CortexA7 interrupts”小节。
NXP 官方 SDK中的文件 MCIMX6Y2C.h在此文件中定义了一个枚举类型 IRQn_Type此枚举类型就枚举出了 I.MX6U 的所有中断代码如下所示
/** Interrupt Number Definitions */
#define NUMBER_OF_INT_VECTORS 160 /** Number of interrupts in the Vector table */typedef enum IRQn {/* Auxiliary constants */NotAvail_IRQn -128, /** Not available device specific interrupt *//* Core interrupts */Software0_IRQn 0, /** Cortex-A7 Software Generated Interrupt 0 */Software1_IRQn 1, /** Cortex-A7 Software Generated Interrupt 1 */Software2_IRQn 2, /** Cortex-A7 Software Generated Interrupt 2 */Software3_IRQn 3, /** Cortex-A7 Software Generated Interrupt 3 */Software4_IRQn 4, /** Cortex-A7 Software Generated Interrupt 4 */Software5_IRQn 5, /** Cortex-A7 Software Generated Interrupt 5 */Software6_IRQn 6, /** Cortex-A7 Software Generated Interrupt 6 */Software7_IRQn 7, /** Cortex-A7 Software Generated Interrupt 7 */Software8_IRQn 8, /** Cortex-A7 Software Generated Interrupt 8 */Software9_IRQn 9, /** Cortex-A7 Software Generated Interrupt 9 */Software10_IRQn 10, /** Cortex-A7 Software Generated Interrupt 10 */Software11_IRQn 11, /** Cortex-A7 Software Generated Interrupt 11 */Software12_IRQn 12, /** Cortex-A7 Software Generated Interrupt 12 */Software13_IRQn 13, /** Cortex-A7 Software Generated Interrupt 13 */Software14_IRQn 14, /** Cortex-A7 Software Generated Interrupt 14 */Software15_IRQn 15, /** Cortex-A7 Software Generated Interrupt 15 */VirtualMaintenance_IRQn 25, /** Cortex-A7 Virtual Maintenance Interrupt */HypervisorTimer_IRQn 26, /** Cortex-A7 Hypervisor Timer Interrupt */VirtualTimer_IRQn 27, /** Cortex-A7 Virtual Timer Interrupt */LegacyFastInt_IRQn 28, /** Cortex-A7 Legacy nFIQ signal Interrupt */SecurePhyTimer_IRQn 29, /** Cortex-A7 Secure Physical Timer Interrupt */NonSecurePhyTimer_IRQn 30, /** Cortex-A7 Non-secure Physical Timer Interrupt */LegacyIRQ_IRQn 31, /** Cortex-A7 Legacy nIRQ Interrupt *//* Device specific interrupts */IOMUXC_IRQn 32, /** General Purpose Register 1 from IOMUXC. Used to notify cores on exception condition while boot. */DAP_IRQn 33, /** Debug Access Port interrupt request. */SDMA_IRQn 34, /** SDMA interrupt request from all channels. */TSC_IRQn 35, /** TSC interrupt. */SNVS_IRQn 36, /** Logic OR of SNVS_LP and SNVS_HP interrupts. */LCDIF_IRQn 37, /** LCDIF sync interrupt. */RNGB_IRQn 38, /** RNGB interrupt. */CSI_IRQn 39, /** CMOS Sensor Interface interrupt request. */PXP_IRQ0_IRQn 40, /** PXP interrupt pxp_irq_0. */SCTR_IRQ0_IRQn 41, /** SCTR compare interrupt ipi_int[0]. */SCTR_IRQ1_IRQn 42, /** SCTR compare interrupt ipi_int[1]. */WDOG3_IRQn 43, /** WDOG3 timer reset interrupt request. */Reserved44_IRQn 44, /** Reserved */APBH_IRQn 45, /** DMA Logical OR of APBH DMA channels 0-3 completion and error interrupts. */WEIM_IRQn 46, /** WEIM interrupt request. */RAWNAND_BCH_IRQn 47, /** BCH operation complete interrupt. */RAWNAND_GPMI_IRQn 48, /** GPMI operation timeout error interrupt. */UART6_IRQn 49, /** UART6 interrupt request. */PXP_IRQ1_IRQn 50, /** PXP interrupt pxp_irq_1. */SNVS_Consolidated_IRQn 51, /** SNVS consolidated interrupt. */SNVS_Security_IRQn 52, /** SNVS security interrupt. */CSU_IRQn 53, /** CSU interrupt request 1. Indicates to the processor that one or more alarm inputs were asserted. */USDHC1_IRQn 54, /** USDHC1 (Enhanced SDHC) interrupt request. */USDHC2_IRQn 55, /** USDHC2 (Enhanced SDHC) interrupt request. */SAI3_RX_IRQn 56, /** SAI3 interrupt ipi_int_sai_rx. */SAI3_TX_IRQn 57, /** SAI3 interrupt ipi_int_sai_tx. */UART1_IRQn 58, /** UART1 interrupt request. */UART2_IRQn 59, /** UART2 interrupt request. */UART3_IRQn 60, /** UART3 interrupt request. */UART4_IRQn 61, /** UART4 interrupt request. */UART5_IRQn 62, /** UART5 interrupt request. */eCSPI1_IRQn 63, /** eCSPI1 interrupt request. */eCSPI2_IRQn 64, /** eCSPI2 interrupt request. */eCSPI3_IRQn 65, /** eCSPI3 interrupt request. */eCSPI4_IRQn 66, /** eCSPI4 interrupt request. */I2C4_IRQn 67, /** I2C4 interrupt request. */I2C1_IRQn 68, /** I2C1 interrupt request. */I2C2_IRQn 69, /** I2C2 interrupt request. */I2C3_IRQn 70, /** I2C3 interrupt request. */UART7_IRQn 71, /** UART-7 ORed interrupt. */UART8_IRQn 72, /** UART-8 ORed interrupt. */Reserved73_IRQn 73, /** Reserved */USB_OTG2_IRQn 74, /** USBO2 USB OTG2 */USB_OTG1_IRQn 75, /** USBO2 USB OTG1 */USB_PHY1_IRQn 76, /** UTMI0 interrupt request. */USB_PHY2_IRQn 77, /** UTMI1 interrupt request. */DCP_IRQ_IRQn 78, /** DCP interrupt request dcp_irq. */DCP_VMI_IRQ_IRQn 79, /** DCP interrupt request dcp_vmi_irq. */DCP_SEC_IRQ_IRQn 80, /** DCP interrupt request secure_irq. */TEMPMON_IRQn 81, /** Temperature Monitor Temperature Sensor (temperature greater than threshold) interrupt request. */ASRC_IRQn 82, /** ASRC interrupt request. */ESAI_IRQn 83, /** ESAI interrupt request. */SPDIF_IRQn 84, /** SPDIF interrupt. */Reserved85_IRQn 85, /** Reserved */PMU_IRQ1_IRQn 86, /** Brown-out event on either the 1.1, 2.5 or 3.0 regulators. */GPT1_IRQn 87, /** Logical OR of GPT1 rollover interrupt line, input capture 1 and 2 lines, output compare 1, 2, and 3 interrupt lines. */EPIT1_IRQn 88, /** EPIT1 output compare interrupt. */EPIT2_IRQn 89, /** EPIT2 output compare interrupt. */GPIO1_INT7_IRQn 90, /** INT7 interrupt request. */GPIO1_INT6_IRQn 91, /** INT6 interrupt request. */GPIO1_INT5_IRQn 92, /** INT5 interrupt request. */GPIO1_INT4_IRQn 93, /** INT4 interrupt request. */GPIO1_INT3_IRQn 94, /** INT3 interrupt request. */GPIO1_INT2_IRQn 95, /** INT2 interrupt request. */GPIO1_INT1_IRQn 96, /** INT1 interrupt request. */GPIO1_INT0_IRQn 97, /** INT0 interrupt request. */GPIO1_Combined_0_15_IRQn 98, /** Combined interrupt indication for GPIO1 signals 0 - 15. */GPIO1_Combined_16_31_IRQn 99, /** Combined interrupt indication for GPIO1 signals 16 - 31. */GPIO2_Combined_0_15_IRQn 100, /** Combined interrupt indication for GPIO2 signals 0 - 15. */GPIO2_Combined_16_31_IRQn 101, /** Combined interrupt indication for GPIO2 signals 16 - 31. */GPIO3_Combined_0_15_IRQn 102, /** Combined interrupt indication for GPIO3 signals 0 - 15. */GPIO3_Combined_16_31_IRQn 103, /** Combined interrupt indication for GPIO3 signals 16 - 31. */GPIO4_Combined_0_15_IRQn 104, /** Combined interrupt indication for GPIO4 signals 0 - 15. */GPIO4_Combined_16_31_IRQn 105, /** Combined interrupt indication for GPIO4 signals 16 - 31. */GPIO5_Combined_0_15_IRQn 106, /** Combined interrupt indication for GPIO5 signals 0 - 15. */GPIO5_Combined_16_31_IRQn 107, /** Combined interrupt indication for GPIO5 signals 16 - 31. */Reserved108_IRQn 108, /** Reserved */Reserved109_IRQn 109, /** Reserved */Reserved110_IRQn 110, /** Reserved */Reserved111_IRQn 111, /** Reserved */WDOG1_IRQn 112, /** WDOG1 timer reset interrupt request. */WDOG2_IRQn 113, /** WDOG2 timer reset interrupt request. */KPP_IRQn 114, /** Key Pad interrupt request. */PWM1_IRQn 115, /** hasRegInstance(PWM1)?Cumulative interrupt line for PWM1. Logical OR of rollover, compare, and FIFO waterlevel crossing interrupts.:Reserved) */PWM2_IRQn 116, /** hasRegInstance(PWM2)?Cumulative interrupt line for PWM2. Logical OR of rollover, compare, and FIFO waterlevel crossing interrupts.:Reserved) */PWM3_IRQn 117, /** hasRegInstance(PWM3)?Cumulative interrupt line for PWM3. Logical OR of rollover, compare, and FIFO waterlevel crossing interrupts.:Reserved) */PWM4_IRQn 118, /** hasRegInstance(PWM4)?Cumulative interrupt line for PWM4. Logical OR of rollover, compare, and FIFO waterlevel crossing interrupts.:Reserved) */CCM_IRQ1_IRQn 119, /** CCM interrupt request ipi_int_1. */CCM_IRQ2_IRQn 120, /** CCM interrupt request ipi_int_2. */GPC_IRQn 121, /** GPC interrupt request 1. */Reserved122_IRQn 122, /** Reserved */SRC_IRQn 123, /** SRC interrupt request src_ipi_int_1. */Reserved124_IRQn 124, /** Reserved */Reserved125_IRQn 125, /** Reserved */CPU_PerformanceUnit_IRQn 126, /** Performance Unit interrupt ~ipi_pmu_irq_b. */CPU_CTI_Trigger_IRQn 127, /** CTI trigger outputs interrupt ~ipi_cti_irq_b. */SRC_Combined_IRQn 128, /** Combined CPU wdog interrupts (4x) out of SRC. */SAI1_IRQn 129, /** SAI1 interrupt request. */SAI2_IRQn 130, /** SAI2 interrupt request. */Reserved131_IRQn 131, /** Reserved */ADC1_IRQn 132, /** ADC1 interrupt request. */ADC_5HC_IRQn 133, /** ADC_5HC interrupt request. */Reserved134_IRQn 134, /** Reserved */Reserved135_IRQn 135, /** Reserved */SJC_IRQn 136, /** SJC interrupt from General Purpose register. */CAAM_Job_Ring0_IRQn 137, /** CAAM job ring 0 interrupt ipi_caam_irq0. */CAAM_Job_Ring1_IRQn 138, /** CAAM job ring 1 interrupt ipi_caam_irq1. */QSPI_IRQn 139, /** QSPI1 interrupt request ipi_int_ored. */TZASC_IRQn 140, /** TZASC (PL380) interrupt request. */GPT2_IRQn 141, /** Logical OR of GPT2 rollover interrupt line, input capture 1 and 2 lines, output compare 1, 2 and 3 interrupt lines. */CAN1_IRQn 142, /** Combined interrupt of ini_int_busoff,ini_int_error,ipi_int_mbor,ipi_int_txwarning and ipi_int_waken */CAN2_IRQn 143, /** Combined interrupt of ini_int_busoff,ini_int_error,ipi_int_mbor,ipi_int_txwarning and ipi_int_waken */Reserved144_IRQn 144, /** Reserved */Reserved145_IRQn 145, /** Reserved */PWM5_IRQn 146, /** Cumulative interrupt line. OR of Rollover Interrupt line, Compare Interrupt line and FIFO Waterlevel crossing interrupt line */PWM6_IRQn 147, /** Cumulative interrupt line. OR of Rollover Interrupt line, Compare Interrupt line and FIFO Waterlevel crossing interrupt line */PWM7_IRQn 148, /** Cumulative interrupt line. OR of Rollover Interrupt line, Compare Interrupt line and FIFO Waterlevel crossing interrupt line */PWM8_IRQn 149, /** Cumulative interrupt line. OR of Rollover Interrupt line, Compare Interrupt line and FIFO Waterlevel crossing interrupt line */ENET1_IRQn 150, /** ENET1 interrupt */ENET1_1588_IRQn 151, /** ENET1 1588 Timer interrupt [synchronous] request. */ENET2_IRQn 152, /** ENET2 interrupt */ENET2_1588_IRQn 153, /** MAC 0 1588 Timer interrupt [synchronous] request. */Reserved154_IRQn 154, /** Reserved */Reserved155_IRQn 155, /** Reserved */Reserved156_IRQn 156, /** Reserved */Reserved157_IRQn 157, /** Reserved */Reserved158_IRQn 158, /** Reserved */PMU_IRQ2_IRQn 159 /** Brown-out event on either core, gpu or soc regulators. */
} IRQn_Type;4.2.3 GIC 控制器逻辑分块
GIC 架构分为了两个逻辑块Distributor 和 CPU Interface也就是分发器端和 CPU 接口端。这两个逻辑块的含义如下
Distributor(分发器端)从4.2.1小节总图可以看出此逻辑块负责处理各个中断事件的分发问题也就是中断事件应该发送到哪个 CPU Interface 上去。分发器收集所有的中断源可以控制每个中断的优先级它总是将优先级最高的中断事件发送到 CPU 接口端。分发器端要做的主要工作如下 ①、全局中断使能控制。 ②、控制每一个中断的使能或者关闭。 ③、设置每个中断的优先级。 ④、设置每个中断的目标处理器列表。 ⑤、设置每个外部中断的触发模式电平触发或边沿触发。 ⑥、设置每个中断属于组 0 还是组 1。
CPU Interface(CPU 接口端)CPU 接口端听名字就知道是和 CPU Core 相连接的因此在4.2.1小节总图 中每个 CPU Core 都可以在 GIC 中找到一个与之对应的 CPU Interface。CPU 接口端就是分发器和 CPU Core 之间的桥梁CPU 接口端主要工作如下 ①、使能或者关闭发送到 CPU Core 的中断请求信号。 ②、应答中断。 ③、通知中断处理完成。 ④、设置优先级掩码通过掩码来设置哪些中断不需要上报给 CPU Core。 ⑤、定义抢占策略。 ⑥、当多个中断到来的时候选择优先级最高的中断通知给 CPU Core。
在IMX6UL官方配套的SDK包里有 core_ca7.h这个头文件定义一些内容如下
/******************************************************************************** GIC相关内容*有关GIC的内容参考ARM Generic Interrupt Controller(ARM GIC控制器)V2.0.pdf******************************************************************************//** GIC寄存器描述结构体* GIC分为分发器端和CPU接口端*/
typedef struct
{uint32_t RESERVED0[1024];__IOM uint32_t D_CTLR; /*! Offset: 0x1000 (R/W) Distributor Control Register */__IM uint32_t D_TYPER; /*! Offset: 0x1004 (R/ ) Interrupt Controller Type Register */__IM uint32_t D_IIDR; /*! Offset: 0x1008 (R/ ) Distributor Implementer Identification Register */uint32_t RESERVED1[29];__IOM uint32_t D_IGROUPR[16]; /*! Offset: 0x1080 - 0x0BC (R/W) Interrupt Group Registers */uint32_t RESERVED2[16];__IOM uint32_t D_ISENABLER[16]; /*! Offset: 0x1100 - 0x13C (R/W) Interrupt Set-Enable Registers */uint32_t RESERVED3[16];__IOM uint32_t D_ICENABLER[16]; /*! Offset: 0x1180 - 0x1BC (R/W) Interrupt Clear-Enable Registers */uint32_t RESERVED4[16];__IOM uint32_t D_ISPENDR[16]; /*! Offset: 0x1200 - 0x23C (R/W) Interrupt Set-Pending Registers */uint32_t RESERVED5[16];__IOM uint32_t D_ICPENDR[16]; /*! Offset: 0x1280 - 0x2BC (R/W) Interrupt Clear-Pending Registers */uint32_t RESERVED6[16];__IOM uint32_t D_ISACTIVER[16]; /*! Offset: 0x1300 - 0x33C (R/W) Interrupt Set-Active Registers */uint32_t RESERVED7[16];__IOM uint32_t D_ICACTIVER[16]; /*! Offset: 0x1380 - 0x3BC (R/W) Interrupt Clear-Active Registers */uint32_t RESERVED8[16];__IOM uint8_t D_IPRIORITYR[512]; /*! Offset: 0x1400 - 0x5FC (R/W) Interrupt Priority Registers */uint32_t RESERVED9[128];__IOM uint8_t D_ITARGETSR[512]; /*! Offset: 0x1800 - 0x9FC (R/W) Interrupt Targets Registers */uint32_t RESERVED10[128];__IOM uint32_t D_ICFGR[32]; /*! Offset: 0x1C00 - 0xC7C (R/W) Interrupt configuration registers */uint32_t RESERVED11[32];__IM uint32_t D_PPISR; /*! Offset: 0x1D00 (R/ ) Private Peripheral Interrupt Status Register */__IM uint32_t D_SPISR[15]; /*! Offset: 0x1D04 - 0xD3C (R/ ) Shared Peripheral Interrupt Status Registers */uint32_t RESERVED12[112];__OM uint32_t D_SGIR; /*! Offset: 0x1F00 ( /W) Software Generated Interrupt Register */uint32_t RESERVED13[3];__IOM uint8_t D_CPENDSGIR[16]; /*! Offset: 0x1F10 - 0xF1C (R/W) SGI Clear-Pending Registers */__IOM uint8_t D_SPENDSGIR[16]; /*! Offset: 0x1F20 - 0xF2C (R/W) SGI Set-Pending Registers */uint32_t RESERVED14[40];__IM uint32_t D_PIDR4; /*! Offset: 0x1FD0 (R/ ) Peripheral ID4 Register */__IM uint32_t D_PIDR5; /*! Offset: 0x1FD4 (R/ ) Peripheral ID5 Register */__IM uint32_t D_PIDR6; /*! Offset: 0x1FD8 (R/ ) Peripheral ID6 Register */__IM uint32_t D_PIDR7; /*! Offset: 0x1FDC (R/ ) Peripheral ID7 Register */__IM uint32_t D_PIDR0; /*! Offset: 0x1FE0 (R/ ) Peripheral ID0 Register */__IM uint32_t D_PIDR1; /*! Offset: 0x1FE4 (R/ ) Peripheral ID1 Register */__IM uint32_t D_PIDR2; /*! Offset: 0x1FE8 (R/ ) Peripheral ID2 Register */__IM uint32_t D_PIDR3; /*! Offset: 0x1FEC (R/ ) Peripheral ID3 Register */__IM uint32_t D_CIDR0; /*! Offset: 0x1FF0 (R/ ) Component ID0 Register */__IM uint32_t D_CIDR1; /*! Offset: 0x1FF4 (R/ ) Component ID1 Register */__IM uint32_t D_CIDR2; /*! Offset: 0x1FF8 (R/ ) Component ID2 Register */__IM uint32_t D_CIDR3; /*! Offset: 0x1FFC (R/ ) Component ID3 Register */__IOM uint32_t C_CTLR; /*! Offset: 0x2000 (R/W) CPU Interface Control Register */__IOM uint32_t C_PMR; /*! Offset: 0x2004 (R/W) Interrupt Priority Mask Register */__IOM uint32_t C_BPR; /*! Offset: 0x2008 (R/W) Binary Point Register */__IM uint32_t C_IAR; /*! Offset: 0x200C (R/ ) Interrupt Acknowledge Register */__OM uint32_t C_EOIR; /*! Offset: 0x2010 ( /W) End Of Interrupt Register */__IM uint32_t C_RPR; /*! Offset: 0x2014 (R/ ) Running Priority Register */__IM uint32_t C_HPPIR; /*! Offset: 0x2018 (R/ ) Highest Priority Pending Interrupt Register */__IOM uint32_t C_ABPR; /*! Offset: 0x201C (R/W) Aliased Binary Point Register */__IM uint32_t C_AIAR; /*! Offset: 0x2020 (R/ ) Aliased Interrupt Acknowledge Register */__OM uint32_t C_AEOIR; /*! Offset: 0x2024 ( /W) Aliased End Of Interrupt Register */__IM uint32_t C_AHPPIR; /*! Offset: 0x2028 (R/ ) Aliased Highest Priority Pending Interrupt Register */uint32_t RESERVED15[41];__IOM uint32_t C_APR0; /*! Offset: 0x20D0 (R/W) Active Priority Register */uint32_t RESERVED16[3];__IOM uint32_t C_NSAPR0; /*! Offset: 0x20E0 (R/W) Non-secure Active Priority Register */uint32_t RESERVED17[6];__IM uint32_t C_IIDR; /*! Offset: 0x20FC (R/ ) CPU Interface Identification Register */uint32_t RESERVED18[960];__OM uint32_t C_DIR; /*! Offset: 0x3000 ( /W) Deactivate Interrupt Register */
} GIC_Type;/* * GIC初始化* 为了简单使用GIC的group0*/
FORCEDINLINE __STATIC_INLINE void GIC_Init(void)
{uint32_t i;uint32_t irqRegs;GIC_Type *gic (GIC_Type *)(__get_CBAR() 0xFFFF0000UL);irqRegs (gic-D_TYPER 0x1FUL) 1;/* On POR, all SPI is in group 0, level-sensitive and using 1-N model *//* Disable all PPI, SGI and SPI */for (i 0; i irqRegs; i)gic-D_ICENABLER[i] 0xFFFFFFFFUL;/* Make all interrupts have higher priority */gic-C_PMR (0xFFUL (8 - __GIC_PRIO_BITS)) 0xFFUL;/* No subpriority, all priority level allows preemption */gic-C_BPR 7 - __GIC_PRIO_BITS;/* Enable group0 distribution */gic-D_CTLR 1UL;/* Enable group0 signaling */gic-C_CTLR 1UL;
}/* * 使能指定的中断*/
FORCEDINLINE __STATIC_INLINE void GIC_EnableIRQ(IRQn_Type IRQn)
{GIC_Type *gic (GIC_Type *)(__get_CBAR() 0xFFFF0000UL);gic-D_ISENABLER[((uint32_t)(int32_t)IRQn) 5] (uint32_t)(1UL (((uint32_t)(int32_t)IRQn) 0x1FUL));
}/* * 关闭指定的中断*/FORCEDINLINE __STATIC_INLINE void GIC_DisableIRQ(IRQn_Type IRQn)
{GIC_Type *gic (GIC_Type *)(__get_CBAR() 0xFFFF0000UL);gic-D_ICENABLER[((uint32_t)(int32_t)IRQn) 5] (uint32_t)(1UL (((uint32_t)(int32_t)IRQn) 0x1FUL));
}/* * 返回中断号 */
FORCEDINLINE __STATIC_INLINE uint32_t GIC_AcknowledgeIRQ(void)
{GIC_Type *gic (GIC_Type *)(__get_CBAR() 0xFFFF0000UL);return gic-C_IAR 0x1FFFUL;
}/* * 向EOIR写入发送中断的中断号来释放中断*/
FORCEDINLINE __STATIC_INLINE void GIC_DeactivateIRQ(uint32_t value)
{GIC_Type *gic (GIC_Type *)(__get_CBAR() 0xFFFF0000UL);gic-C_EOIR value;
}/** 获取运行优先级*/
FORCEDINLINE __STATIC_INLINE uint32_t GIC_GetRunningPriority(void)
{GIC_Type *gic (GIC_Type *)(__get_CBAR() 0xFFFF0000UL);return gic-C_RPR 0xFFUL;
}/** 设置组优先级*/
FORCEDINLINE __STATIC_INLINE void GIC_SetPriorityGrouping(uint32_t PriorityGroup)
{GIC_Type *gic (GIC_Type *)(__get_CBAR() 0xFFFF0000UL);gic-C_BPR PriorityGroup 0x7UL;
}/** 获取组优先级*/
FORCEDINLINE __STATIC_INLINE uint32_t GIC_GetPriorityGrouping(void)
{GIC_Type *gic (GIC_Type *)(__get_CBAR() 0xFFFF0000UL);return gic-C_BPR 0x7UL;
}/** 设置优先级*/
FORCEDINLINE __STATIC_INLINE void GIC_SetPriority(IRQn_Type IRQn, uint32_t priority)
{GIC_Type *gic (GIC_Type *)(__get_CBAR() 0xFFFF0000UL);gic-D_IPRIORITYR[((uint32_t)(int32_t)IRQn)] (uint8_t)((priority (8UL - __GIC_PRIO_BITS)) (uint32_t)0xFFUL);
}/** 获取优先级*/
FORCEDINLINE __STATIC_INLINE uint32_t GIC_GetPriority(IRQn_Type IRQn)
{GIC_Type *gic (GIC_Type *)(__get_CBAR() 0xFFFF0000UL);return(((uint32_t)gic-D_IPRIORITYR[((uint32_t)(int32_t)IRQn)] (8UL - __GIC_PRIO_BITS)));
}第 58 行是 GIC 的 CPU 接口端相关寄存器其相对于 GIC 基地址的偏移为 0X2000同样的获取到 GIC 基地址以后只需要加上 0X2000 即可访问 GIC 的 CPU 接口段寄存器。 那么问题来了GIC 控制器的寄存器基地址在哪里呢这个就需要用到 Cortex-A 的 CP15 协处理器了接着了解一下 CP15 协处理器。
4.2.4 CP15 协处理器
关于 CP15 协处理器和其相关寄存器的详细内容请参考下面两份文档《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf》第 1469 页“B3.17 Oranization of the CP15 registers in a VMSA implementation”。《Cortex-A7 Technical ReferenceManua.pdf》第55 页“Capter 4 System Control”。
CP15 协处理器一般用于存储系统管理但是在中断中也会使用到CP15 协处理器一共有16 个 32 位寄存器。CP15 协处理器的访问通过如下另个指令完成
MRC: 将 CP15 协处理器中的寄存器数据读到 ARM 寄存器中。 MCR: 将 ARM 寄存器的数据写入到 CP15 协处理器寄存器中。
MRC 就是读 CP15 寄存器MCR 就是写 CP15 寄存器MCR 指令格式如下
MCR{cond} p15, opc1, Rt, CRn, CRm, opc2cond:指令执行的条件码如果忽略的话就表示无条件执行。opc1协处理器要执行的操作码。RtARM 源寄存器要写入到 CP15 寄存器的数据就保存在此寄存器中。CRnCP15 协处理器的目标寄存器。CRm协处理器中附加的目标寄存器或者源操作数寄存器如果不需要附加信息就将 CRm 设置为 C0否则结果不可预测。opc2可选的协处理器特定操作码当不需要的时候要设置为 0。MRC 的指令格式和 MCR 一样只不过在 MRC 指令中 Rt 就是目标寄存器也就是从CP15 指定寄存器读出来的数据会保存在 Rt 中。而 CRn 就是源寄存器也就是要读取的写处理器寄存器。
假如我们要将 CP15 中 C0 寄存器的值读取到 R0 寄存器中那么就可以使用如下命令MRC p15, 0, r0, c0, c0, 0
CP15 协处理器有 16 个 32 位寄存器c0~c15本章来看一下 c0、c1、c12 和 c15 这四个寄存器主要涉及GIC和重要信息四个寄存器其他的寄存器大家参考《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf》和《Cortex-A7 Technical ReferenceManua.pdf》”。两个文档即可。
c0 寄存器 CP15 协处理器有 16 个 32 位寄存器c0~c15在使用 MRC 或者 MCR 指令访问这 16 个寄存器的时候指令中的 CRn、opc1、CRm 和 opc2 通过不同的搭配其得到的寄存器含义是不同的。比如 c0 在不同的搭配情况下含义如下图所示(《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf -1471页) 《Cortex-A7 Technical ReferenceManua.pdf》-58页 在上图中当 MRC/MCR 指令中的 CRnc0opc10CRmc0opc20 的时候就表示 此时的 c0 就是 MIDR 寄存器也就是主 ID 寄存器这个也是 c0 的基本作用。对于 Cortex-A7内核来说c0 作为 MDIR 寄存器的时候其含义如下图所示 各bit位所代表的含义如下 bit31:24厂商编号0X41ARM。 bit23:20内核架构的主版本号ARM 内核版本一般使用 rnpn 来表示比如 r0p1其中 r0后面的 0 就是内核架构主版本号。 bit19:16架构代码0XFARMv7 架构。 bit15:4内核版本号0XC07Cortex-A7 MPCore 内核。 bit3:0内核架构的次版本号rnpn 中的 pn比如 r0p1 中 p1 后面的 1 就是次版本号。
c1 寄存器 c1 寄存器同样通过不同的配置其代表的含义也不同如下图所示( 《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf -1472页) 《Cortex-A7 Technical ReferenceManua.pdf》-59页 在上图中当 MRC/MCR 指令中的 CRnc1opc10CRmc0opc20 的时候就表示 此时的 c1 就是 SCTLR 寄存器也就是系统控制寄存器这个是 c1 的基本作用。SCTLR 寄存器主要是完成控制功能的比如使能或者禁止 MMU、I/D Cache 等c1 作为 SCTLR 寄存器的时候其含义如下图所示 SCTLR 的位比较多我们就只看主要用到的几个位 bit13V , 中断向量表基地址选择位为 0 的话中断向量表基地址为 0X00000000软件可以使用 VBAR 来重映射此基地址也就是中断向量表重定位。为 1 的话中断向量表基地址为0XFFFF0000此基地址不能被重映射。 bit12II Cache 使能位为 0 的话关闭 I Cache为 1 的话使能 I Cache。 bit11Z分支预测使能位如果开启 MMU 的话此位也会使能。 bit10SWSWP 和 SWPB 使能位当为 0 的话关闭 SWP 和 SWPB 指令当为 1 的时候就使能 SWP 和 SWPB 指令。 bit9~3未使用保留。 bit2CD Cache 和缓存一致性使能位为 0 的时候禁止 D Cache 和缓存一致性为 1 时使能。 bit1A内存对齐检查使能位为 0 的时候关闭内存对齐检查为 1 的时候使能内存对齐检查。 bit0MMMU 使能位为 0 的时候禁止 MMU为 1 的时候使能 MMU。
如果要读写 SCTLR 的话就可以使用如下命令
MRC p15, 0, Rt, c1, c0, 0 ;读取 SCTLR 寄存器数据保存到 Rt 中。
MCR p15, 0, Rt, c1, c0, 0 ;将 Rt 中的数据写到 SCTLR(c1)寄存器中。c12 寄存器 c12 寄存器通过不同的配置其代表的含义也不同如下图所示( 《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf -1479页) 《Cortex-A7 Technical ReferenceManua.pdf》-67页 在上图中当 MRC/MCR 指令中的 CRnc12opc10CRmc0opc20 的时候就表示此时 c12 为 VBAR 寄存器也就是向量表基地址寄存器。设置中断向量表偏移的时候就需要将新的中断向量表基地址写入 VBAR 中比如在前面的uboot启动kernel中代码链接的起始地址为0X87800000而中断向量表肯定要放到最前面也就是 0X87800000 这个地址处。所以就需要设置 VBAR 为 0X87800000设置命令如下
ldr r0, 0X87800000 ; r00X87800000
MCR p15, 0, r0, c12, c0, 0 ;将 r0 里面的数据写入到 c12 中即 c120X87800000c15 寄存器 c15 寄存器也可以通过不同的配置得到不同的含义如下图所示( 《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf 无描述《Cortex-A7 Technical ReferenceManua.pdf》-68页) 在上图中我们需要 c15 作为 CBAR 寄存器因为 GIC 的基地址就保存在 CBAR中我们可以通过如下命令获取到 GIC 基地址
MRC p15, 4, r1, c15, c0, 0 ; 获取 GIC 基础地址基地址保存在 r1 中。获取到 GIC 基地址以后就可以设置 GIC 相关寄存器了比如我们可以读取当前中断 ID当前中断 ID 保存在 GICC_IAR 中寄存器 GICC_IAR 属于 CPU 接口端寄存器寄存器地址相对于 CPU 接口端起始地址的偏移为 0XC因此获取当前中断 ID 的代码如下
MRC p15, 4, r1, c15, c0, 0 ;获取 GIC 基地址
ADD r1, r1, #0X2000 ;GIC 基地址加 0X2000 得到 CPU 接口端寄存器起始地址
LDR r0, [r1, #0XC] ;读取 CPU 接口端起始地址0XC 处的寄存器值也就是寄存器GIC_IAR 的值简单总结一下通过 c0 寄存器可以获取到处理器内核信息通过 c1 寄存器可以使能或禁止 MMU、I/D Cache 等通过 c12 寄存器可以设置中断向量偏移通过 c15 寄存器可以获取 GIC 基地址。关于 CP15 的其他寄存器大家自行查阅本节前面列举的 2 份 ARM 官方资料。
4.3 中断使能
中断使能包括两部分一个是 IRQ 或者 FIQ 总中断使能另一个就是 ID0~ID1019 这 1020个中断源的使能。 IRQ 和 FIQ 总中断使能 IRQ 和 FIQ 分别是外部中断和快速中断的总开关就类似家里买的进户总电闸然后ID0~ID1019 这 1020 个中断源就类似家里面的各个电器开关。要想开电视那肯定要保证进户总电闸是打开的因此要想使用 I.MX6U 上的外设中断就必须先打开 IRQ 中断(本教程不使用FIQ)。在程序状态寄存器中寄存器 CPSR 的 I1 禁止 IRQ当 I0 使能 IRQF1 禁止 FIQF0 使能 FIQ。我们还有更简单的指令来完成 IRQ 或者 FIQ 的使能和禁止如下表所示
指令描述cpsid i禁止 IRQ 中断。cpsie i使能 IRQ 中断。cpsid f禁止 FIQ 中断cpsie f使能 FIQ 中断
ID0~ID1019 中断使能和禁止 GIC 寄存器 GICD_ISENABLERn 和 GICD_ ICENABLERn 用来完成外部中断的使能和禁止对于 Cortex-A7 内核来说中断 ID 只使用了 512 个。一个 bit 控制一个中断 ID 的使能那么就需要 512/3216 个 GICD_ISENABLER 寄存器来完成中断的使能。同理也需要 16 个GICD_ICENABLER 寄存器来完成中断的禁止。其中: GICD_ISENABLER0 的 bit[15:0]对应ID15-0的 SGI 中断 GICD_ISENABLER0 的 bit[31:16]对应 ID31-16 的 PPI 中断。GICD_ISENABLER1~GICD_ISENABLER15 就是控制 SPI 中断的。
4.4 中断优先级设置
优先级数配置 了解STM32或从3.3节内容可知Cortex-M 的中断优先级分为抢占优先级和子优先级两者是可以配置的。同样的 Cortex-A7 的中断优先级也可以分为抢占优先级和子优先级两者同样是可以配置的。GIC 控制器最多可以支持 256 个优先级数字越小优先级越高Cortex-A7 选择了 32 个优先级。在使用中断的时候需要初始化 GICC_PMR 寄存器此寄存器用来决定使用几级优先级寄存器结构如下图所示 GICC_PMR 寄存器只有低 8 位有效这 8 位最多可以设置 256 个优先级其他优先级数设置如下表所示
bit7:0优先级数11111111256 个优先级11111110128 个优先级1111110064 个优先级1111100032 个优先级1111000016 个优先级
I.MX6U 是 Cortex-A7内核所以支持 32 个优先级因此 GICC_PMR 要设置为 0b11111000。
抢占优先级和子优先级位数设置 抢占优先级和子优先级各占多少位是由寄存器 GICC_BPR 来决定的GICC_BPR 寄存器结构如下图所示 寄存器 GICC_BPR 只有低 3 位有效其值不同抢占优先级和子优先级占用的位数也不同配置如下表所示
Binary Point抢占优先级域子优先级域描述0[7:1][0]7 级抢占优先级1 级子优先级1[7:2][1:0]6 级抢占优先级2 级子优先级2[7:3][2:0]5 级抢占优先级3 级子优先级3[7:4][3:0]4 级抢占优先级4 级子优先级4[7:5][4:0]3 级抢占优先级5 级子优先级5[7:6][5:0]2 级抢占优先级6 级子优先级6[7][6:0]1 级抢占优先级7 级子优先级7无[7:0]0 级抢占优先级8 级子优先级
为了简单起见一般将所有的中断优先级位都配置为抢占优先级比如 I.MX6U 的优先级位数为 5(32 个优先级)所以可以设置 Binary point 为 2表示 5 个优先级位全部为抢占优先级。
优先级设置 前面已经设置好了 I.MX6U 一共有 32 个抢占优先级数字越小优先级越高。具体要使用某个中断的时候就可以设置其优先级为 0~31。某个中断 ID 的中断优先级设置由寄存器D_IPRIORITYR 来完成前面说了 Cortex-A7 使用了 512 个中断 ID每个中断 ID 配有一个优先级寄存器所以一共有 512 个 D_IPRIORITYR 寄存器。如果优先级个数为 32 的话使用寄存器 D_IPRIORITYR 的 bit7:4 来设置优先级也就是说实际的优先级要左移 3 位。比如要设置ID40 中断的优先级为 5示例代码如下
GICD_IPRIORITYR[40] 5 3;优先级设置主要有三部分 ①、设置寄存器 GICC_PMR配置优先级个数比如 I.MX6U 支持 32 级优先级。 ②、设置抢占优先级和子优先级位数一般为了简单起见会将所有的位数都设置为抢占优先级。 ③、设置指定中断 ID 的优先级也就是设置外设优先级。
五 Linux kernel 中断
前面第四章分析那么多具体的硬件GIC相关内容是想读者对gic有个全面的了解。实际开发中断相关驱动并不需要我们太多操作比如配置寄存器使能 IRQ 等等我们所使用的kernel是基于NXP发布的基线开发的所以最底层的gic寄存器相关配置根本不用我们去关注。Linux 内核提供了完善的中断框架我们只需要申请中断然后注册中断处理函数即可使用非常方便不需要一系列复杂的寄存器配置。
硬件中断的处理方法 ①、使能中断初始化相应的寄存器。 ②、注册中断服务函数也就是向 irqTable 数组的指定标号处写入中断服务函数 ②、中断发生以后进入 IRQ 中断服务函数在 IRQ 中断服务函数在数组 irqTable 里面查找具体的中断处理函数找到以后执行相应的中断处理函数。
在 Linux 内核中也提供了大量的中断相关的 API 函数我们来看一下这些跟中断有关的一些内容。
5.1 中断号
每个中断都有一个中断号通过中断号即可区分不同的中断有的资料也把中断号叫做中 断线。在 Linux 内核中使用一个 int 变量表示中断号关于中断号我们已经在前面4.2.2节讲解过了。
5.2 kernel提供的中断API
5.2.1 request_irq 函数
在 Linux 内核中要想使用某个中断是需要申请的request_irq 函数用于申请中断request_irq函数可能会导致睡眠因此不能在中断上下文或者其他禁止睡眠的代码段中使用 request_irq 函数。request_irq 函数会激活(使能)中断所以不需要我们手动去使能中断request_irq 函数原型如下 路径include/linux/interrupt.h
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,const char *name,void *dev)函数参数和返回值含义如下 irq要申请中断的中断号。 handler中断处理函数当中断发生以后就会执行此中断处理函数。 flags中断标志可以在文件 include/linux/interrupt.h 里面查看所有的中断标志这里我们介绍几个常用的中断标志如下表所示
标志描述IRQF_SHARED多个设备共享一个中断线共享的所有中断都必须指定此标志。如果使用共享中断的话request_irq 函数的 dev 参数就是唯一区分他们的标志。IRQF_ONESHOT单次中断中断执行一次就结束。IRQF_TRIGGER_NONE无触发IRQF_TRIGGER_RISING上升沿触发IRQF_TRIGGER_FALLING下降沿触发IRQF_TRIGGER_HIGH高电平触发IRQF_TRIGGER_LOW低电平触发
标志可以通过“|”来实现多种组合. name中断名字设置以后可以在设备的/proc/interrupts 文件中看到对应的中断名字。 dev如果将 flags 设置为 IRQF_SHARED 的话dev 用来区分不同的中断一般情况下将dev 设置为设备结构体dev 会传递给中断处理函数 irq_handler_t 的第二个参数。 返回值0 中断申请成功其他负值 中断申请失败如果返回-EBUSY 的话表示中断已经被申请了。
5.2.2 free_irq函数
使用中断的时候需要通过 request_irq 函数申请使用完成以后就要通过 free_irq 函数释放掉相应的中断。如果中断不是共享的那么 free_irq 会删除中断处理函数并且禁止中断。free_irq函数原型如下所示
void free_irq(unsigned int irq, void *dev)函数参数和返回值含义如下 irq要释放的中断。 dev如果中断设置为共享(IRQF_SHARED)的话此参数用来区分具体的中断。共享中断只有在释放最后中断处理函数的时候才会被禁止掉。 返回值无。
5.2.3 中断处理函数
使用 request_irq 函数申请中断的时候需要设置中断处理函数中断处理函数格式如下所示
irqreturn_t (*irq_handler_t) (int, void *)第一个参数是要中断处理函数要相应的中断号。第二个参数是一个指向 void 的指针也就是个通用指针需要与 request_irq 函数的 dev 参数保持一致。用于区分共享中断的不同设备dev 也可以指向设备数据结构。中断处理函数的返回值为 irqreturn_t 类型irqreturn_t 类型定义如下所示
路径include/linux/irqreturn.h
#ifndef _LINUX_IRQRETURN_H
#define _LINUX_IRQRETURN_H/*** enum irqreturn* IRQ_NONE interrupt was not from this device* IRQ_HANDLED interrupt was handled by this device* IRQ_WAKE_THREAD handler requests to wake the handler thread*/
enum irqreturn {IRQ_NONE (0 0),IRQ_HANDLED (1 0),IRQ_WAKE_THREAD (1 1),
};typedef enum irqreturn irqreturn_t;
#define IRQ_RETVAL(x) ((x) ? IRQ_HANDLED : IRQ_NONE)#endif
可以看出 irqreturn_t 是个枚举类型一共有三种返回值。一般中断服务函数返回值使用如下形式
return IRQ_RETVAL(IRQ_HANDLED)5.2.4 中断使能与禁止函数
常用的中断使用和禁止函数如下所示
路径include/linux/interrupt.h
void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq)enable_irq 和 disable_irq 用于使能和禁止指定的中断irq 就是要禁止的中断号。disable_irq函数要等到当前正在执行的中断处理函数执行完才返回因此使用者需要保证不会产生新的中断并且确保所有已经开始执行的中断处理程序已经全部退出。
在这种情况下可以使用另外一个中断禁止函数
void disable_irq_nosync(unsigned int irq)disable_irq_nosync 函数调用以后立即返回不会等待当前中断处理程序执行完毕。
上面三个函数都是使能或者禁止某一个中断有时候我们需要关闭当前处理器的整个中断系统也就是在学习 STM32 的时候常说的关闭全局中断这个时候可以使用如下两个函数
路径include/linux/interrupt.h
local_irq_enable()
local_irq_disable()local_irq_enable 用于使能当前处理器中断系统local_irq_disable 用于禁止当前处理器中断系统。
多个任务操作local_irq_enable 和local_irq_disable时存在区间重叠的可能。可能导致任务崩溃。所以需要如下两个函数
路径include/linux/irqflags.h
local_irq_save(flags)
local_irq_restore(flags)这两个函数是一对local_irq_save 函数用于禁止中断并且将中断状态保存在 flags 中。local_irq_restore 用于恢复中断将中断状态保存到 flags 状态。
5.3 中断的上半部和下半部
我们在使用request_irq 申请中断的时候注册的中断服务函数属于中断处理的上半部只要中断触发那么中断处理函数就会执行。我们都知道中断处理函数一定要快点执行完毕越短越好但是现实往往是残酷的有些中断处理过程就是比较费时间我们必须要对其进行处理缩小中断处理函数的执行时间。比如电容触摸屏通过中断通知 SOC 有触摸事件发生SOC 响应中断然后通过 IIC 接口读取触摸坐标值并将其上报给系统。但是我们都知道 IIC 的速度最高也只有400Kbit/S所以在中断中通过 IIC 读取数据就会浪费时间。我们可以将通过 IIC 读取触摸数据的操作暂后执行中断处理函数仅仅相应中断然后清除中断标志位即可。这个时候中断处理过程就分为了两部分
上半部上半部就是中断处理函数那些处理过程比较快不会占用很长时间的处理就可以放在上半部完成。
下半部如果中断处理过程比较耗时那么就将这些比较耗时的代码提出来交给下半部去执行这样中断处理函数就会快进快出。
因此Linux 内核将中断分为上半部和下半部的主要目的就是实现中断处理函数的快进快出那些对时间敏感、执行速度快的操作可以放到中断处理函数中也就是上半部。剩下的所有工作都可以放到下半部去执行比如在上半部将数据拷贝到内存中关于数据的具体处理就可以放到下半部去执行。至于哪些代码属于上半部哪些代码属于下半部并没有明确的规定一切根据实际使用情况去判断这个就很考验驱动编写人员的功底了。
这里提供一些可以借鉴的参考点 ①、如果要处理的内容不希望被其他中断打断那么可以放到上半部。 ②、如果要处理的任务对时间敏感可以放到上半部。 ③、如果要处理的任务与硬件有关可以放到上半部 ④、除了上述三点以外的其他任务优先考虑放到下半部。
上半部处理很简单直接编写中断处理函数就行了关键是下半部该怎么做呢Linux 内核提供了多种下半部机制接下来我们来学习一下这些下半部机制。
5.3.1 软中断
一开始 Linux 内核提供了“bottom half”机制来实现下半部简称“BH”。后面引入了软中断和 tasklet 来替代“BH”机制完全可以使用软中断和 tasklet 来替代 BH从 2.5 版本的 Linux内核开始 “bottom half”机制 已经被抛弃了。Linux 内核使用结构体 softirq_action 表示软中断 softirq_action结构体定义在文件 include/linux/interrupt.h 中内容如下
路径include/linux/interrupt.h
/* softirq mask and active fields moved to irq_cpustat_t in* asm/hardirq.h to get better cache usage. KAO*/struct softirq_action
{void (*action)(struct softirq_action *);
};
在 kernel/softirq.c 文件中一共定义了 10 个软中断如下所示
路径kernel/softirq.c
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;路径include/linux/interrupt.h
enum
{HI_SOFTIRQ0, /* 高优先级软中断 */TIMER_SOFTIRQ, /* 定时器软中断 */NET_TX_SOFTIRQ, /* 网络数据发送软中断 */NET_RX_SOFTIRQ, /* 网络数据接收软中断 */BLOCK_SOFTIRQ,BLOCK_IOPOLL_SOFTIRQ,TASKLET_SOFTIRQ, /* tasklet 软中断 */SCHED_SOFTIRQ, /* 调度软中断 */HRTIMER_SOFTIRQ, /* 高精度定时器软中断 */RCU_SOFTIRQ, /* RCU 软中断 */NR_SOFTIRQS
};
可以看出一共有 10 个软中断因此 NR_SOFTIRQS 为 10因此数组 softirq_vec 有 10 个元素。
softirq_action 结构体中的 action 成员变量就是软中断的服务函数数组 softirq_vec 是个全局数组因此所有的 CPU(对于 SMP 系统而言)都可以访问到每个 CPU 都有自己的触发和控制机制并且只执行自己所触发的软中断。但是各个 CPU 所执行的软中断服务函数确是相同的都是数组 softirq_vec 中定义的 action 函数。要使用软中断必须先使用 open_softirq 函数注册对应的软中断处理函数open_softirq 函数原型如下
void open_softirq(int nr, void (*action)(struct softirq_action *))函数参数和返回值含义如下 nr要开启的软中断在示例代码 51.1.2.3 中选择一个。 action软中断对应的处理函数。 返回值没有返回值。
注册好软中断以后需要通过 raise_softirq 函数触发raise_softirq 函数原型如下
void raise_softirq(unsigned int nr)函数参数和返回值含义如下 nr要触发的软中断在示例代码 51.1.2.3 中选择一个。 返回值没有返回值。
软中断必须在编译的时候静态注册Linux 内核使用 softirq_init 函数初始化软中断softirq_init 函数定义在 kernel/softirq.c 文件里面函数内容如下
路径kernel/softirq.c
void __init softirq_init(void)
{int cpu;for_each_possible_cpu(cpu) {per_cpu(tasklet_vec, cpu).tail per_cpu(tasklet_vec, cpu).head;per_cpu(tasklet_hi_vec, cpu).tail per_cpu(tasklet_hi_vec, cpu).head;}open_softirq(TASKLET_SOFTIRQ, tasklet_action);open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}从上诉代码 可以看出softirq_init 函数默认会打开 TASKLET_SOFTIRQ 和HI_SOFTIRQ。
5.3.2 tasklet
tasklet 是利用软中断来实现的另外一种下半部机制在软中断和 tasklet 之间建议大家使用 tasklet。Linux 内核使用 tasklet_struct 结构体来表示 tasklet
路径include/linux/interrupt.h
struct tasklet_struct
{struct tasklet_struct *next;unsigned long state;atomic_t count;void (*func)(unsigned long);unsigned long data;
}; 第 489 行的 func 函数就是 tasklet 要执行的处理函数用户定义函数内容相当于中断处理函数。如果要使用 tasklet必须先定义一个 tasklet然后使用 tasklet_init 函数初始化 tasklettaskled_init 函数原型如下
void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), unsigned long data);
函数参数和返回值含义如下 t要初始化的 tasklet functasklet 的处理函数。 data要传递给 func 函数的参数 返回值没有返回值。
也可以使用宏 DECLARE_TASKLET 来一次性完成 tasklet 的定义和初始化DECLARE_TASKLET 定义在 include/linux/interrupt.h 文件中定义如下:
DECLARE_TASKLET(name, func, data)其中 name 为要定义的 tasklet 名字这个名字就是一个 tasklet_struct 类型的时候变量func就是 tasklet 的处理函数data 是传递给 func 函数的参数。
在上半部也就是中断处理函数中调用 tasklet_schedule 函数就能使 tasklet 在合适的时间运行tasklet_schedule 函数原型如下
void tasklet_schedule(struct tasklet_struct *t)函数参数和返回值含义如下 t要调度的 tasklet也就是 DECLARE_TASKLET 宏里面的 name。 返回值没有返回值。
关于 tasklet 的参考使用示例如下所示
/* 定义 taselet */
struct tasklet_struct testtasklet;/* tasklet 处理函数 */
void testtasklet_func(unsigned long data)
{/* tasklet 具体处理内容 */
}
/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{....../* 调度 tasklet */tasklet_schedule(testtasklet);......
}
/* 驱动入口函数 */
static int __init xxxx_init(void)
{....../* 初始化 tasklet */tasklet_init(testtasklet, testtasklet_func, data);/* 注册中断处理函数 */request_irq(xxx_irq, test_handler, 0, xxx, xxx_dev);......
}5.3.3 工作队列
工作队列是另外一种下半部执行方式工作队列在进程上下文执行工作队列将要推后的工作交给一个内核线程去执行因为工作队列工作在进程上下文因此工作队列允许睡眠或重新调度。因此如果你要推后的工作可以睡眠那么就可以选择工作队列否则的话就只能选择软中断或 tasklet。 Linux 内核使用 work_struct 结构体表示一个工作内容如下
路径include/linux/workqueue.h
struct work_struct {atomic_long_t data;struct list_head entry;work_func_t func; /* 工作队列处理函数 */
#ifdef CONFIG_LOCKDEPstruct lockdep_map lockdep_map;
#endif
};这些工作组织成工作队列工作队列使用 workqueue_struct 结构体表示内容如下
路径kernel/workqueue.c
/** The externally visible workqueue. It relays the issued work items to* the appropriate worker_pool through its pool_workqueues.*/
struct workqueue_struct {struct list_head pwqs; /* WR: all pwqs of this wq */struct list_head list; /* PR: list of all workqueues */struct mutex mutex; /* protects this wq */int work_color; /* WQ: current work color */int flush_color; /* WQ: current flush color */atomic_t nr_pwqs_to_flush; /* flush in progress */struct wq_flusher *first_flusher; /* WQ: first flusher */struct list_head flusher_queue; /* WQ: flush waiters */struct list_head flusher_overflow; /* WQ: flush overflow list */struct list_head maydays; /* MD: pwqs requesting rescue */struct worker *rescuer; /* I: rescue worker */int nr_drainers; /* WQ: drain in progress */int saved_max_active; /* WQ: saved pwq max_active */struct workqueue_attrs *unbound_attrs; /* WQ: only for unbound wqs */struct pool_workqueue *dfl_pwq; /* WQ: only for unbound wqs */#ifdef CONFIG_SYSFSstruct wq_device *wq_dev; /* I: for sysfs interface */
#endif
#ifdef CONFIG_LOCKDEPstruct lockdep_map lockdep_map;
#endifchar name[WQ_NAME_LEN]; /* I: workqueue name *//** Destruction of workqueue_struct is sched-RCU protected to allow* walking the workqueues list without grabbing wq_pool_mutex.* This is used to dump all workqueues from sysrq.*/struct rcu_head rcu;/* hot fields used during command issue, aligned to cacheline */unsigned int flags ____cacheline_aligned; /* WQ: WQ_* flags */struct pool_workqueue __percpu *cpu_pwqs; /* I: per-cpu pwqs */struct pool_workqueue __rcu *numa_pwq_tbl[]; /* FR: unbound pwqs indexed by node */
};Linux 内核使用工作者线程(worker thread)来处理工作队列中的各个工作Linux 内核使用worker 结构体表示工作者线程worker 结构体内容如下
路径kernel/workqueue_internal.h
/** The poor guys doing the actual heavy lifting. All on-duty workers are* either serving the manager role, on idle list or on busy hash. For* details on the locking annotation (L, I, X...), refer to workqueue.c.** Only to be used in workqueue and async.*/
struct worker {/* on idle list while idle, on busy hash table while busy */union {struct list_head entry; /* L: while idle */struct hlist_node hentry; /* L: while busy */};struct work_struct *current_work; /* L: work being processed */work_func_t current_func; /* L: current_works fn */struct pool_workqueue *current_pwq; /* L: current_works pwq */bool desc_valid; /* -desc is valid */struct list_head scheduled; /* L: scheduled works *//* 64 bytes boundary on 64bit, 32 on 32bit */struct task_struct *task; /* I: worker task */struct worker_pool *pool; /* I: the associated pool *//* L: for rescuers */struct list_head node; /* A: anchored at pool-workers *//* A: runs through worker-node */unsigned long last_active; /* L: last active timestamp */unsigned int flags; /* X: flags */int id; /* I: worker id *//** Opaque string set with work_set_desc(). Printed out with task* dump for debugging - WARN, BUG, panic or sysrq.*/char desc[WORKER_DESC_LEN];/* used only by rescuers to point to the target workqueue */struct workqueue_struct *rescue_wq; /* I: the workqueue to rescue */
};
从worker结构体最后一个成员可以看出每个 worker 都有一个工作队列工作者线程处理自己工作队列中的所有工作。在实际的驱动开发中我们只需要定义工作(work_struct)即可关于工作队列和工作者线程我们基本不用去管。简单创建工作很简单直接定义一个 work_struct 结构体变量即可然后使用 INIT_WORK 宏来初始化工作INIT_WORK 宏定义如下
#define INIT_WORK(_work, _func)_work 表示要初始化的工作_func 是工作对应的处理函数。
也可以使用 DECLARE_WORK 宏一次性完成工作的创建和初始化宏定义如下
#define DECLARE_WORK(n, f)n 表示定义的工作(work_struct)f 表示工作对应的处理函数。
和 tasklet 一样工作也是需要调度才能运行的工作的调度函数为 schedule_work函数原型如下所示
bool schedule_work(struct work_struct *work)函数参数和返回值含义如下 work要调度的工作。 返回值0 成功其他值 失败。
关于工作队列的参考使用示例如下所示
/* 定义工作(work) */
struct work_struct testwork;/* work 处理函数 */
void testwork_func_t(struct work_struct *work);
{/* work 具体处理内容 */
}
/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{....../* 调度 work */schedule_work(testwork);......
}
/* 驱动入口函数 */
static int __init xxxx_init(void)
{....../* 初始化 work */INIT_WORK(testwork, testwork_func_t);/* 注册中断处理函数 */request_irq(xxx_irq, test_handler, 0, xxx, xxx_dev);......
}5.4 设备树中断信息节点
目前基本都是使用设备树所以就需要在设备树中设置好中断属性信息Linux 内核通过读取设备树中的中断属性信息来配置中断。对于中断 控制器而言 设备树绑定信息参考文档Documentation/devicetree/bindings/arm/gic.txt。打开 imx6ull.dtsi 文件其中的 intc 节点就是I.MX6ULL 的中断控制器节点节点内容如下所示
路径arch/arm/boot/dts/imx6ull.dtsiintc: interrupt-controller00a01000 {compatible arm,cortex-a7-gic;#interrupt-cells 3;interrupt-controller;reg 0x00a01000 0x1000,0x00a02000 0x100;};第 4 行compatible 属性值为“arm,cortex-a7-gic”在 Linux 内核源码中搜索“arm,cortex-a7-gic”即可找到 GIC 中断控制器驱动文件drivers/irqchip/irq-gic.c。
第 5 行#interrupt-cells 和#address-cells、#size-cells 一样。表示此中断控制器下设备的 cells大小对于设备而言会使用 interrupts 属性描述中断信息#interrupt-cells 描述了 interrupts 属性的 cells 大小也就是一条信息有几个 cells。每个 cells 都是 32 位整形值对于 ARM 处理的GIC 来说一共有 3 个 cells这三个 cells 的含义如下 第一个 cells中断类型0 表示 SPI 中断1 表示 PPI 中断。 第二个 cells中断号对于 SPI 中断来说中断号的范围为 0-987对于 PPI 中断来说中断号的范围为 0~15。 第三个 cells标志bit[3:0]表示中断触发类型为 1 的时候表示上升沿触发为 2 的时候表示下降沿触发为 4 的时候表示高电平触发为 8 的时候表示低电平触发。bit[15:8]为 PPI 中断的 CPU 掩码。
第 6 行interrupt-controller 节点为空表示当前节点是中断控制器。
对于 gpio 来说gpio 节点也可以作为中断控制器比如 imx6ull.dtsi 文件中的 gpio5 节点内容如下所示
路径arch/arm/boot/dts/imx6ull.dtsigpio5: gpio020ac000 {compatible fsl,imx6ul-gpio, fsl,imx35-gpio;reg 0x020ac000 0x4000;interrupts GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH,GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH;gpio-controller;#gpio-cells 2;interrupt-controller;#interrupt-cells 2;};第 5 行interrupts 描述中断源信息对于 gpio5 来说一共有两条信息中断类型都是 SPI触发电平都是 IRQ_TYPE_LEVEL_HIGH。不同之处在于中断源一个是 74一个是 75打开可以打开《IMX6ULL 参考手册》的“Chapter 3 Interrupts and DMA Events”章节找到表 3-1有如下图所示的内容 从上图可以看出GPIO5 一共用了 2 个中断号一个是 74一个是 75。其中 74 对应 GPIO5_IO00-GPIO5_IO15 这低 16 个 IO75 对应 GPIO5_IO16~GPIOI5_IO31 这高 16 位 IO。
第 9 行interrupt-controller 表明了 gpio5 节点也是个中断控制器用于控制 gpio5 所有 IO的中断。
第 10 行将#interrupt-cells 修改为 2。
飞思卡尔使用例子 打开arch/arm/boot/dts/imx6ull-water-emmc.dts文件找到如下所示内容
fxls84711e {compatible fsl,fxls8471;reg 0x1e;position 0;interrupt-parent gpio5;interrupts 0 8;};fxls8471 是 NXP 官方的 6ULL 开发板上的一个磁力计芯片fxls8471 有一个中断引脚链接到了 I.MX6ULL 的 SNVS_TAMPER0 因脚上这个引脚可以复用为GPIO5_IO00。
第 5 行interrupt-parent 属性设置中断控制器这里使用 gpio5 作为中断控制器。 第 6 行interrupts 设置中断信息0 表示 GPIO5_IO008 表示低电平触发。
简单总结一下与中断有关的设备树属性信息 ①、#interrupt-cells指定中断源的信息 cells 个数。 ②、interrupt-controller表示当前节点为中断控制器。 ③、interrupts指定中断号触发方式等。 ④、interrupt-parent指定父中断也就是中断控制器。
5.5 获取中断号
在编写中断有关的驱动时我们需要在驱动里获取中断号前面第5.4小节w欧美讲了中断号和中断相关信息已经写入设备树中了那么我们在写驱动时需要用到则可以去设备树中获取中断号和中断相关信息。 kernel中提供了函数 irq_of_parse_and_map 函数从 interupts 属性中提取到对应的设备号函数原型如下
路径drivers\of\irq.c
unsigned int irq_of_parse_and_map(struct device_node *dev,int index)函数参数和返回值含义如下 dev设备节点。 index索引号interrupts 属性可能包含多条中断信息通过 index 指定要获取的信息。 返回值中断号。 如果使用 GPIO 的话可以使用 gpio_to_irq 函数来获取 gpio 对应的中断号函数原型如 下
路径 include\linux\gpio.hDocumentation\gpio\gpio-legacy.txt
int gpio_to_irq(unsigned int gpio)函数参数和返回值含义如下 gpio要获取的 GPIO 编号。 返回值GPIO 对应的中断号。
上面将了那么多那么该如何使用中断呢
六 Linux kernel 中断 实验
前面我们只分析了内核定时器当然还有系统硬件定时器(但是我们暂时没用到)还分析了Cortex-a7架构下的中断控制器GIC及其工作流程。如果不是基于Linux系统开发想要参考STM32单片机一样当裸机开发的话我们需要配置的东西很多。如 硬件中断的处理方法 ①、使能中断初始化相应的寄存器。 ②、注册中断服务函数也就是向 irqTable 数组的指定标号处写入中断服务函数 ②、中断发生以后进入 IRQ 中断服务函数在 IRQ 中断服务函数在数组 irqTable 里面查找具体的中断处理函数找到以后执行相应的中断处理函数。
如果使用linux开发中断相关功能代码就方便很多了大体上就是申请中断注册一下中断函数即可。其实并不是说我们完全不需要按照逻辑的流程来处理中断首先我们使用的是NXP官方开发板配套的kernel代码里面的相关板级代原厂都移植好了。NXP半导厂会借助Linux kernel的框架代码将硬件相关的配置操作隐藏起来一般驱动开发人员无需关注这类工作内容是系统工程师做bring up时的工作内容。
第四节的内容主要是想让读者对中断的流程有一个全面的了解。
接下来我们就以一个按键来编写一个中断验证一下对应的功能。
6.1 新增key设备树节点
笔者所用的开发板上有一个key该按键链接 I.MX6U 的 UART1_CTS 这个 IO。
结合前面我们学习过的pinctrl子系统首先新增pinctrl节点添加内容如下
路径arch\arm\boot\dts\imx6ull-water-emmc.dts
pinctrl_key: keygrp {fsl,pins MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 0xF080 /* KEY0 */;};继续在dts文件新增key设备树节点内容如下
路径arch\arm\boot\dts\imx6ull-water-emmc.dts
key { #address-cells 1;#size-cells 1;compatible water-key;pinctrl-names default;pinctrl-0 pinctrl_key;key-gpio gpio1 18 GPIO_ACTIVE_LOW; /* KEY0 */interrupt-parent gpio1;interrupts 18 IRQ_TYPE_EDGE_BOTH; /* FALLING RISING */status okay;};上述两个设备树节点新增完毕后可通过“make dts” 或全编 kernel然后用新的DTB文件启动在设备中可看到新增的设备树节点信息如下
sudo cp arch/arm/boot/dts/imx6ull-water-emmc.dtb ../../../tftp/6.2 编写key驱动代码-手动挂载方式
编译单独的ko文件然后动态挂载设备。
6.2.1 新建key目录及c文件
新建key目录并以vscode工程打开然后新建key.c文件如下
6.2.2 新建key目录
配置kernel头文件路径同时按下ctrlshitfP选择“c/c:编辑配置JSON”添加linux的头文件注意是绝对路径内容如下
{configurations: [{name: Linux,includePath: [${workspaceFolder}/**,/home/water/imax/NXP/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga/include,/home/water/imax/NXP/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include,/home/water/imax/NXP/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include/generated/],defines: [],compilerPath: /usr/bin/gcc,cStandard: c17,cppStandard: gnu14,intelliSenseMode: linux-gcc-x64}],version: 4
}6.2.3 编写驱动
在key.c中输入以下内容
/***************************************************************
Copyright © OneFu Co., Ltd. 2018-2023. All rights reserved.
文件名 : key.c
作者 : water
版本 : V1.0
描述 : Linux 中断驱动实验
其他 : 无
日志 : 初版 V1.0 2023/06/12 water创建
***************************************************************/
#include linux/types.h
#include linux/kernel.h
#include linux/delay.h
#include linux/ide.h
#include linux/init.h
#include linux/module.h
#include linux/errno.h
#include linux/gpio.h
#include linux/cdev.h
#include linux/device.h
#include linux/of.h
#include linux/of_address.h
#include linux/of_gpio.h
#include linux/semaphore.h
#include linux/timer.h
#include linux/of_irq.h
#include linux/irq.h
#include asm/mach/map.h
#include asm/uaccess.h
#include asm/io.h#define KEYIRQ_CNT 1 /* 设备号个数 */
#define KEYIRQ_NAME KEYIRQ /*设备名*/
#define KEY0VALUE 0X01 /* key0按键值 */
#define INVAKEY 0XFF /* 无效按键值 */
#define KEY_NUM 1 /* 按键数量 *//* IO中断描述结构体 */
struct irq_keydesc {int gpio; /* 使用的GPIO */int irqnum; /* GPIO中断号 */unsigned char value; /* 按键键值 */char name[10]; /* 设备名 */irqreturn_t (*handler)(int, void *); /* 中断处理函数 */
};/* keyirq设备结构体 */
struct keyirq_dev{dev_t devid; /* 设备号 */struct cdev cdev; /* cdev */struct class *class; /* 类 */struct device *device; /* 设备 */int major; /* 主设备号 */int minor; /* 次设备号 */struct device_node *nd; /* 设备节点 */atomic_t keyvalue; /* 有效的按键键值 */atomic_t releasekey; /* 标记是否完成一次完成的按键*/struct timer_list timer; /* 定义一个定时器*/struct irq_keydesc irqkeydesc[KEY_NUM]; /* 按键描述数组 */unsigned char curkeynum; /* 当前的按键号 */
};struct keyirq_dev keyirq; /* irq 设备 *//* description : 中断服务函数开启定时器延时10ms* 定时器用于按键消抖。* param - irq : 中断号 * param - dev_id : 设备结构。* return : 中断执行结果*/
static irqreturn_t key0_handler(int irq, void *dev_id)
{struct keyirq_dev *dev (struct keyirq_dev *)dev_id;dev-curkeynum 0;dev-timer.data (volatile long)dev_id;mod_timer(dev-timer, jiffies msecs_to_jiffies(10)); /* 10ms定时 */return IRQ_RETVAL(IRQ_HANDLED);
}/* description : 定时器服务函数用于按键消抖定时器到了以后* 再次读取按键值如果按键还是处于按下状态就表示按键有效。* param - arg : 设备结构变量* return : 无*/
void timer_function(unsigned long arg)
{unsigned char value;unsigned char num;struct irq_keydesc *keydesc;struct keyirq_dev *dev (struct keyirq_dev *)arg;num dev-curkeynum;keydesc dev-irqkeydesc[num];value gpio_get_value(keydesc-gpio); /* 读取IO值 */if(value 0){ /* 按下按键 */atomic_set(dev-keyvalue, keydesc-value);}else{ /* 按键松开 */atomic_set(dev-keyvalue, 0x80 | keydesc-value);atomic_set(dev-releasekey, 1); /* 标记松开按键即完成一次完整的按键过程 */ }
}/** description : 按键IO初始化* param : 无* return : 无*/
static int keyio_init(void)
{unsigned char i 0;int ret 0;keyirq.nd of_find_node_by_path(/key);if (keyirq.nd NULL){printk(key node not find!\r\n);return -EINVAL;} /* 提取GPIO */for (i 0; i KEY_NUM; i) {keyirq.irqkeydesc[i].gpio of_get_named_gpio(keyirq.nd ,key-gpio, i);if (keyirq.irqkeydesc[i].gpio 0) {printk(cant get key%d\r\n, i);}}/* 初始化key所使用的IO并且设置成中断模式 */for (i 0; i KEY_NUM; i) {memset(keyirq.irqkeydesc[i].name, 0, sizeof(keyirq.irqkeydesc[i].name)); /* 缓冲区清零 */sprintf(keyirq.irqkeydesc[i].name, KEY%d, i); /* 组合名字 */gpio_request(keyirq.irqkeydesc[i].gpio, keyirq.irqkeydesc[i].name);gpio_direction_input(keyirq.irqkeydesc[i].gpio); keyirq.irqkeydesc[i].irqnum irq_of_parse_and_map(keyirq.nd, i);
#if 0keyirq.irqkeydesc[i].irqnum gpio_to_irq(keyirq.irqkeydesc[i].gpio);
#endifprintk(key%d:gpio%d, irqnum%d\r\n,i, keyirq.irqkeydesc[i].gpio, keyirq.irqkeydesc[i].irqnum);}/* 申请中断 */keyirq.irqkeydesc[0].handler key0_handler;keyirq.irqkeydesc[0].value KEY0VALUE;for (i 0; i KEY_NUM; i) {ret request_irq(keyirq.irqkeydesc[i].irqnum, keyirq.irqkeydesc[i].handler, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, keyirq.irqkeydesc[i].name, keyirq);if(ret 0){printk(irq %d request failed!\r\n, keyirq.irqkeydesc[i].irqnum);return -EFAULT;}}/* 创建定时器 */init_timer(keyirq.timer);keyirq.timer.function timer_function;return 0;
}/** description : 打开设备* param - inode : 传递给驱动的inode* param - filp : 设备文件file结构体有个叫做private_data的成员变量* 一般在open的时候将private_data指向设备结构体。* return : 0 成功;其他 失败*/
static int keyirq_open(struct inode *inode, struct file *filp)
{filp-private_data keyirq; /* 设置私有数据 */return 0;
}/** description : 从设备读取数据 * param - filp : 要打开的设备文件(文件描述符)* param - buf : 返回给用户空间的数据缓冲区* param - cnt : 要读取的数据长度* param - offt : 相对于文件首地址的偏移* return : 读取的字节数如果为负值表示读取失败*/
static ssize_t keyirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{int ret 0;unsigned char keyvalue 0;unsigned char releasekey 0;struct keyirq_dev *dev (struct keyirq_dev *)filp-private_data;keyvalue atomic_read(dev-keyvalue);releasekey atomic_read(dev-releasekey);if (releasekey) { /* 有按键按下 */ if (keyvalue 0x80) {keyvalue ~0x80;ret copy_to_user(buf, keyvalue, sizeof(keyvalue));} else {goto data_error;}atomic_set(dev-releasekey, 0);/* 按下标志清零 */} else {goto data_error;}return 0;data_error:return -EINVAL;
}/* 设备操作函数 */
static struct file_operations keyirq_fops {.owner THIS_MODULE,.open keyirq_open,.read keyirq_read,
};/** description : 驱动入口函数* param : 无* return : 无*/
static int __init keyirq_init(void)
{/* 1、构建设备号 */if (keyirq.major) {keyirq.devid MKDEV(keyirq.major, 0);register_chrdev_region(keyirq.devid, KEYIRQ_CNT, KEYIRQ_NAME);} else {alloc_chrdev_region(keyirq.devid, 0, KEYIRQ_CNT, KEYIRQ_NAME);keyirq.major MAJOR(keyirq.devid);keyirq.minor MINOR(keyirq.devid);}/* 2、注册字符设备 */cdev_init(keyirq.cdev, keyirq_fops);cdev_add(keyirq.cdev, keyirq.devid, KEYIRQ_CNT);/* 3、创建类 */keyirq.class class_create(THIS_MODULE, KEYIRQ_NAME);if (IS_ERR(keyirq.class)) {return PTR_ERR(keyirq.class);}/* 4、创建设备 */keyirq.device device_create(keyirq.class, NULL, keyirq.devid, NULL, KEYIRQ_NAME);if (IS_ERR(keyirq.device)) {return PTR_ERR(keyirq.device);}/* 5、初始化按键 */atomic_set(keyirq.keyvalue, INVAKEY);atomic_set(keyirq.releasekey, 0);keyio_init();return 0;
}/** description : 驱动出口函数* param : 无* return : 无*/
static void __exit keyirq_exit(void)
{unsigned int i 0;/* 删除定时器 */del_timer_sync(keyirq.timer); /* 删除定时器 *//* 释放中断 */for (i 0; i KEY_NUM; i) {free_irq(keyirq.irqkeydesc[i].irqnum, keyirq);gpio_free(keyirq.irqkeydesc[i].gpio);}cdev_del(keyirq.cdev);unregister_chrdev_region(keyirq.devid, KEYIRQ_CNT);device_destroy(keyirq.class, keyirq.devid);class_destroy(keyirq.class);
}module_init(keyirq_init);
module_exit(keyirq_exit);
MODULE_LICENSE(GPL);
MODULE_AUTHOR(water);6.2.4 编写Makefile
在key.c同级目录下新建Makefile并输入以下内容
ARCHarm
CROSS_COMPILEarm-linux-gnueabihf-KERNELDIR : /home/water/imax/NXP/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga/
CURRENT_PATH : $(shell pwd)
obj-m : key.oKBUILD_CFLAGS -fno-piebuild: kernel_moduleskernel_modules:$(MAKE) -C $(KERNELDIR) M$(CURRENT_PATH) modules
clean:$(MAKE) -C $(KERNELDIR) M$(CURRENT_PATH) clean6.2.5 编译验证
在Makefile同级目录下执行make命令完成编译如下 将编译生成的key.ko文件放到设备的“/lib/modules/4.1.15”目录下如下
命令行进入到“/lib/modules/4.1.15”目录下然后执行以下命令进行挂在驱动
depmod //第一次加载驱动的时候需要运行此命令
modprobe key.ko //加载驱动驱动加载成功以后可以通过查看/proc/interrupts 文件来检查一下对应的中断有没有被注册上输入如下命令
cat /proc/interrupts如下图 其中47表示中断号18表示GPIO序号与驱动挂载时输出的信息一致。
后续编写测试APP验证可直接跳到6.4节进行APP验证。
6.3 编写key驱动代码-并入kernel方式
当一个硬件设计固定后为了方便使用外设一般都是直接编译进内核这样就不需要每次手动挂载了接下来就开看下如下编写编进内核的key驱动。
6.3.1 新建 驱动文件
首先我们要了解一下键盘也是属于按键键盘在linux中作为input设备使用在kernel有单独的目录管理所以我们把key也当成input设备放到其对应的目录下。对于键盘功能使用后续文章再进行讲解。
找到drivers\input\keyboard目录并新建“gpio_waterkeys.c”,并输入6.2.3小节的代码内容。
6.3.2 增加编译条件
找到同级目录下的Makefile文件“drivers\input\keyboard\Makefile”再末尾添加
obj-$(CONFIG_KEYBOARD_WATERKEY) gpio_waterkeys.o6.3.3 增加配置条件
主要时使用“make menuconfig”时可以配置是否使用这个驱动模块。 找到同级目录下的Kconfig文件“drivers\input\keyboard\Kconfig”再末尾添加
config KEYBOARD_WATERKEYtristate WATER KEY GPIO Buttonsdepends on GPIOLIBhelpThis driver implements support for buttons connectedto GPIO pins of various CPUs (and some other chips).Say Y here if your device has buttons connecteddirectly to such GPIO pins. Your board-specificsetup logic must also provide a platform device,with configuration data saying which GPIOs are used.To compile this driver as a module, choose M here: themodule will be called gpio_keys.6.3.4 编译
上述内容添加完成后我们就可以编译了 首先运行“make menuconfig”进行配置使用我们添加的key模块如下 然后保存
然后再进行整体kernel编译 命令make
上述驱动准备完成后我们就来编写key测试app验证。
6.4 app验证
6.4.1 编写app代码
编写key测试app新建文件key_app.c并输入一下内容
#include stdio.h
#include unistd.h
#include sys/types.h
#include sys/stat.h
#include fcntl.h
#include stdlib.h
#include string.h
#include linux/ioctl.h
/***************************************************************
Copyright © OneFu Co., Ltd. 2018-2023. All rights reserved.
文件名 : key_app.c
作者 : water
版本 : V1.0
描述 : 中断测试应用程序
其他 : 无
使用方法 ./key_app /dev/keyirq 打开测试App
日志 : 初版V1.0 2023/06/12 water创建
***************************************************************//** description : main主程序* param - argc : argv数组元素个数* param - argv : 具体参数* return : 0 成功;其他 失败*/
int main(int argc, char *argv[])
{int keyfd;int ret 0;char *filename;unsigned char data;int ledstatu 0;char ledfilename[]/dev/gpioled;int ledfd,retvalue;unsigned char databuf[1]; //定义的buf用来读写数据用if (argc ! 2) {printf(Error Usage!\r\n);return -1;}filename argv[1];keyfd open(filename, O_RDWR);if (keyfd 0) {printf(Cant open file %s\r\n, filename);return -1;}ledfd open(ledfilename,O_RDWR); /*打开驱动文件*/if(ledfd 0){printf(Cant open file %s\r\n,ledfilename); /*打开失败输出提示*/return -1;}while (1) {ret read(keyfd, data, sizeof(data));if (ret 0) { /* 数据读取错误或者无效 */} else { /* 数据读取正确 */if (data) /* 读取到数据 */{printf(key value %#X\r\n, data);ledstatu !ledstatu;databuf[0] ledstatu;retvalue write(ledfd, databuf, sizeof(databuf)); /*向设备驱动写入数据*/if(retvalue 0){printf(LED Control Failed!\r\n,ledfilename); /*写入错误输出提示*/}}}}retvalue close(keyfd);if(retvalue 0){printf(Cant close file %s\r\n,filename); /*关闭错误输出提示*/ret -1;goto OUT1;}
OUT1:retvalue close(ledfd);if(retvalue 0){printf(Cant close file %s\r\n,ledfilename); /*关闭错误输出提示*/ret -1;goto OUT2;}
OUT2:return ret;
}//编译arm-linux-gnueabihf-gcc key_app.c -o key_app6.4.2 编译验证
编译key_app.c,使用以下命令
arm-linux-gnueabihf-gcc key_app.c -o key_app将编译生成的key_app放到设备中并运行命令如下
cp key_app ../../../nfs/buildrootfs/root/water_soft/key/运行命令 ./key_app /dev/KEYIRQ根据按键按下控制LED灯的开关状态。 实际现象与设计现象相符。
不管是动态挂载还是编译进内核都会有 “/dev/KEYIRQ”这个设备那么app就可以通过操作这个设备控制获取key的输入了。
七 总结
主要体现掌握以下几点 1、熟悉内核定时器的使用方法步骤 2、了解cortex-a7的中断流程原理 3、熟悉掌握linux kernel中断驱动的编写方法步骤。
内核定时器和中断是开发功能的常用技术手段只有掌握更多的技术才能更好的为我们的开发服务。
额外的使用 ./key_app /dev/KEYIRQ 然app运行在后台然后通过top命令看一下cpu占用结果如下 可看到cpu占用很高这是因为app是一直循环read设备的这样驱动就会一直去获取按键值 并返回给app如此反复所以cpu占用率就特别高这种情况是不能满足实际使用的。 下一篇文章将讲解如何处理这样现象。
由于文章参考文档资源上传异常文章所参考文档可联系作者获取QQ759521350