内核中断子系统介绍
很多人在学习中断子系统的过程中,在对基本概念与整体不太了解的情况下,过早的陷入了各种架构的实现细节,如同盲人摸象。这里主要给大家明确中断的各个基本概念,希望从这个角度能让大家更好的理解中断子系统。
什么是中断
在计算机科学中,中断(英语:Interrupt)是指处理器接收到来自硬件或软件的信号,提示发生了某个事件,应该被注意,这种情况就称为中断。中断子系统中的中断指的是其中硬件的一方,后续中断均按此理解。
中断处理的参与对象和流程
中断处理中有着多个对象的参与,理解每个对象在其中是如何参与是很重要的。以下列举了中断处理的参与对象。
中断事件:指中断事件本身的抽象。中断号:用于硬件和软件识别并区分中断事件,需要注意同一个中断事件在中断处理的不同阶段未必是同一个中断号。中断源:有中断事件需要 cpu 处理的硬件。中断控制器:非必须,用于解决系统拥有多中断源场景的硬件;从中断源接收中断事件并传递到 cpu;可以级联。cpu:收到中断,cpu 跳转到特定的地址——中断向量。由中断向量开始软件对中断的处理。中断事件在硬件中的流程如下,上一行是中断事件的体现形式,下一行是所在的硬件:
再把软件处理结合起来,形成一个硬件软件切换的过程:
相邻的中断事件体现形式的映射方式可以在所在的对象的连接的实现中找到。
中断子系统
现在把之前的流程具有的部分对比内核中断子系统,可以发现还多出了一个通用中断处理层。因为内核需要支持各种不同的架构与外设,需要解耦架构硬件相关部分(cpu 与中断控制器)与非架构相关(外设),使得开发外设驱动并不需要了解架构相关部分。另一方面,系统硬件拓扑结构的信息一般由设备树源码 DTS 体现。
硬件封装层
硬件封装层包括 cpu 和中断控制器两部分。区分开 cpu 和中断控制器相当重要,希望大家能更明确 cpu 和中断控制器的概念。软件在 cpu 方面主要需要按架构实现中断向量的处理,可以看 arch/「/kernel/entry」 .S 的汇编实现。另外还需要为通用的开关中断方法提供架构实现:
通用中断开关方法具体架构中断开关实现local_irq_enablearch_local_irq_enablelocal_irq_disablearch_local_irq_disable这个 local 指的是 local_cpu,表示的是当前 cpu 是否响应中断:当前 cpu 关中断的情况下,中断控制器不管怎么玩都是徒劳的。事实上 cpu 对中断开关的实现还包含着很多条件,类似特权态、非屏蔽中断 NMI 之类的,可以在后文找下具体分析。软件对中断控制器的抽象是 struct irq_chip,体现的是中断控制器所具体的行为。这里列举部分重要成员讲解:
起因struct irq_chip 成员说明怎么控制中断控制器是否屏蔽某个中断事件?irq_enable/ irq_disable中断控制器如何配置中断事件的触发方式irq_set_type控制各个中断的电气触发条件,例如边沿触发或者是电平触发。中断控制器如何得知中断事件被 cpu 响应?irq_ack中断控制器在实现中会根据中断事件被 cpu 开始响应或完成响应来决定该中断事件类型是否会再度通知 cpu 处理。中断控制器如何得知 cpu 完成处理中断事件?irq_eoi中断控制器在实现中会根据中断事件被 cpu 开始响应或完成响应来决定该中断事件类型是否会再度通知 cpu 处理。在 smp 系统中,中断事件应该通知哪个 cpu?irq_set_affinityaffinity 表示了中断事件在中断控制器中配置的目标 cpu,根据具体实现可以是 1 个或多个。此外,当多个中断事件同时发生,中断控制器会根据其优先级的实现来决定中断事件通知给 cpu 的顺序,某些实现是可配置的。另一方面,考虑到系统中可能存在多个中断控制器,使得单一中断控制器的中断号不足以区分中断事件,所以引入了软件中断号的概念。加上硬件中断号映射中断号的软件抽象 struct irq_domain,再看中断控制器软件抽象到中断源软件抽象的流程:
##中断流控处理层 这一层主要是隐藏了中断控制器在具体中断事件处理函数调用前后的一些处理逻辑,包括:
何时对中断控制器发出 ack 回应?mask_irq 和 unmask_irq 的处理;中断控制器是否需要 eoi 回应?何时打开 cpu 的本地 irq 中断?以便允许 irq 的嵌套;类似于在用洗衣机洗衣服的时候,我们不关心衣服可能要经历过的洗涤多久、脱水多久、漂洗多久诸如此类的步骤细节,只需要按衣服类型选择流程;内核引入中断流程的抽象类型 irq_flow_handler_t 屏蔽了中断事件相关的 cpu、中断控制器和中断源的属性的不同带来的处理流程差异。这里举例部分内核实现:
handle_simple_irq:用于简易流控处理。handle_level_irq:用于电平触发中断的流控处理。handle_edge_irq:用于边沿触发中断的流控处理。handle_fasteoi_irq:用于需要响应 eoi 的中断控制器handle_percpu_irq:用于只在单一 cpu 响应的中断。驱动程序 API 与中断通用逻辑
对于中断事件本身,内核使用 struct irq_desc 进行描述,它包含着所有的信息。而对于中断控制器与中断源的驱动来说,关注的信息都只是其中的一部分。中断事件从中断源到中断控制器的映射的描述一般事先会静态定义好并存放在设备树源码里,即中断源的设备树节点包含着相连的中断控制器和中断事件对应在中断控制器中断号的信息;而作为驱动程序需要对软件中断号 irq 和中断事件处理函数建立映射。那么要把设备树节点中的中断控制器和中断控制器中断号转换成软件中断号 irq,内核给驱动程序提供了接口:
irq_of_parse_and_map:驱动由设备树节点获得 irq。当中断控制器和中断控制器中断号转换成软件中断号 irq 映射不存在时,这个接口会申请 irq_desc 并建立映射,根据连接着的中断控制器的驱动提供的硬件中断号映射中断号的软件抽象 irq_domain 完成映射。在映射过程中会包括对 irq_desc 的一些属性的设置,如:
irq_set_handler:驱动选择 irq_flow_handler。irq_set_chip:驱动选择 irq 连接的中断控制器。irq_alloc_desc 系列:驱动直接申请 irq_desc。中断源驱动获取到 irq,还需要将 irq 与中断处理函数建立映射:
request_irq/request_threaded_irq:驱动将中断处理函数注册到 irq。enable_irq & disable_irq:驱动开关 irq。接下来将对一些具体的架构实现做介绍。这里介绍两个处理器 armv8 和 x86,以及两个中断控制器 arm-gicv3 和 x86-APIC 的实现。希望帮助大家得出诸如“arm 内核有中断嵌套吗”“arm cpu eoi 是做什么”这类问题的答案。
armv8
arm 核心拥有 2 个外部中断线,IRQ 和 FIQ;这两根中断线连接到中断控制器上,中断控制器通过拉高和拉低这两根中断线触发中断。一个中断应该触发 IRQ 还是 FIQ 中断线,由其 GROUP 属性和当前的特权级和安全域决定。
GROUP 的定义:
arm 核,软件可以写 SCR、HCR 和 PSTATE.DAIF 寄存器以决定响应中断的特权级和屏蔽中断;arm 不支持 NMI。arm 核由于中断控制器的实现,同时只会有一个需要被响应的中断,因此不限制 IRQ/FIQ 响应顺序的实现。arm 核上处于触发状态的中断线需要结合 SCR、HCR 和 PSTATE.DAIF 寄存器判断是否触发中断,不论当前是否处于中断。在中断触发时,arm 核心根据 VBAR 系列寄存器的基地址,会按具体情况选择偏移跳转到对应的地址。
x86
Intel x86 架构提供 INTR 和 NMI 两个中断引脚,他们通常与 Local APIC 相连, 用于接收 Local APIC 传递的中断信号。一个中断应该触发 INTR 还是 NMI 中断线由 Local APIC 实现。
x86,中断都在 ring0 响应。x86 上软件使用 CLI 指令将本 CPU 的 EFLAGS 寄存器的 IF 位清 0,阻止接收中断;STI 指令将 IF 位置为 1,允许接收中断。这两条指令都只对当前 CPU 起作用,而不影响平台上的其他 CPU。x86 中断线的实现原生支持 NMI。x86 核上同时只会有一个需要被响应的中断,它由 Local APIC 从 IRR 中选择;当 Local APIC 不使能时,优先响应 NMI 中断线。不论当前是否处于中断,x86 核上若 INTR 处于触发且未屏蔽中断即会触发中断;NMI 处于触发则直接触发中断。中断触发时,x86 核根据寄存器 IDTR 记录的基址和中断控制器的寄存器 ISR 提供的中断向量号找到 IDT 表中对应的 Interrupt Gate 表项,跳转到相应的地址。
arm-gicv3
从逻辑视图上看,gicv3 的核外部分统称 IRI,由 Distributor、ITS、Redistributor 这 3 种组件组成;gicv3 核内部分是 CPU interface,PE 可以理解为 cpu;IRI 与 CPU interface 通过 GIC Stream Protocol interface 交互。
不同的中断在 gic 上对应着不同的 INTID;gic 把中断类型分为 LPI、PPI、SPI、SGI,约束 INTID 取值对应的中断类型。SGI 指由 CPU 直接写对应的寄存器触发中断;PPI 指中断为特定一个 CPU 私有/专用,同一中断号的 PPI 在不同 CPU 可以指不同的中断源;SPI 对应 PPI,是所有 CPU 全局共享的,同一中断号的 SPI 在不同 CPU 均指相同的中断源;LPI 的区分是中断路由上的不同,主要是在 IRI 中由 ITS 路由的中断,其余 3 种中断均不经过 ITS;某些实现下还有直接在 Redistributor 触发的 LPI 中断。
一个外部中断从在外设上产生,依次经过 IRI、CPU interface 并最终通过中断线到达 PE;PE 产生的中断需要先经过 CPU interface 到 IRI,再到目标的 CPU interface 和 PE。逻辑上,IRI 可以对应多个 PE,因此对于需要被一个特定目标 PE 响应的中断,gicv3 通过引入 affinity routing 机制解决这种路由问题。同一时间,CPU interface 上只能存在一个待处理的中断,对于多个中断被发送到 CPU interface 上,gic 引入优先级的机制来决定如何选择保留的中断;这个优先级的机制还被运用在 IRI 上,优先级更高的中断会被优先发送到 CPU interface。另外,CPU interface 还负责将这个待处理的中断按照 GROUP 属性和当前的特权级和安全域决定触发 IRQ 还是 FIQ 中断线;并且当 PE 当前处于中断时,CPU interface 还需要通过中断优先级分组的机制判断待处理的中断是否需要被通知给 PE,即抢占。
x86-APIC
从逻辑视图上看,APIC 的核外部分是 I/O APIC,核内部分是 Local APIC。I/O APIC 根据内部 PRT table 中的 RTE 发送中断消息给 Local APIC。I/O APIC 中 PRT table 由 24 个 RTE 项组成,每一项对应一个 IRQ 引脚。I/O APIC 可以有多个,当多个 I/O APIC 存在时,使用 GSI 代表每个 I/O APIC 管脚的编号:例如 I/O APIC1 有 24 个 IRQ,I/O APIC2 也有 24 个 IRQ,则 I/O APIC2 的 GSI 是从 24 开始,GSI = 24 + IRQ(I/O APIC2)。I/O APIC 的 24 个管脚没有优先级之分。一个外部中断经过 I/O APIC 再到 Local APIC,最后由 Local APIC 控制中断线在 CPU 上触发中断;CPU 内部的中断源由 Local APIC 管理,不需要经过 I/O APIC;IPI 也由 Local APIC 管理,同样不需要经过 I/O APIC。
Local APIC 支持 0-255 的中断向量号,它们可以同时存在于寄存器 IRR 上,引入中断优先级进行选择:优先级 = 中断向量号 / 16 因为 32 以下的中断向量号是保留的,所以可用中断优先级范围为 2-15,数字越大优先级越高;当优先级高于寄存器 PPR 的情况下会操作 INTR 中断线,若当前已经处于中断则可能出现抢占。中断向量号的低 4 位会在当 PPR 改变的情况下,ISR 从 IRR 上选择中断向量号的比较中使用,同样也是数字越大优先级越高。
参考资料
[1]arm generic interrupt controller architecture specification
[2] ARM Architecture Reference Manual ARMv8
[3]multiprocessor specification
[4]intel® 64 and ia-32 architectures software developer's manual volume 3a: system programming guide, part 1
「正点原子Linux连载」第四十五章 pinctrl和gpio子系统实验二
1)实验平台:正点原子Linux开发板
2)摘自《正点原子 I.MX6U嵌入式Linux驱动开发指南》
关注官方微信号公众号,获取更多资料:正点原子
大家将imx35_gpio_hwdata中的各个成员变量和图45.2.2.1中的GPIO寄存器表对比就会发现,imx35_gpio_hwdata结构体就是GPIO寄存器组结构。这样我们后面就可以通过mxc_gpio_hwdata这个全局变量来访问GPIO的相应寄存器了。
继续回到示例代码45.2.2.5的mxc_gpio_probe函数中,第417行,调用函数platform_get_resource获取设备树中内存资源信息,也就是reg属性值。前面说了reg属性指定了GPIO1控制器的寄存器基地址为0X0209C000,在配合前面已经得到的mxc_gpio_hwdata,这样Linux内核就可以访问gpio1的所有寄存器了。
第418行,调用devm_ioremap_resource函数进行内存映射,得到0x0209C000在Linux内核中的虚拟地址。
第422、423行,通过platform_get_irq函数获取中断号,第422行获取高16位GPIO的中断号,第423行获取底16位GPIO中断号。
第428、429行,操作GPIO1的IMR和ISR这两个寄存器,关闭GPIO1所有IO中断,并且清除状态寄存器。
第438~448行,设置对应GPIO的中断服务函数,不管是高16位还是低16位,中断服务函数都是mx3_gpio_irq_handler。
第450~453行,bgpio_init函数第一个参数为bgc,是bgpio_chip结构体指针。bgpio_chip结构体有个gc成员变量,gc是个gpio_chip结构体类型的变量。gpio_chip结构体是抽象出来的GPIO控制器,gpio_chip结构体如下所示(有缩减):
示例代码45.2.2.9 gpio_chip结构体
74struct gpio_chip {
75constchar * label;
76struct device * dev;
77struct module * owner;
78struct list_head list;
79
80int (* request)( struct gpio_chip * chip,
81 unsigned offset);
82void (* free)( struct gpio_chip * chip,
83 unsigned offset);
84int (* get_direction)( struct gpio_chip * chip,
85 unsigned offset);
86int (* direction_input)( struct gpio_chip * chip,
87 unsigned offset);
88int (* direction_output)( struct gpio_chip * chip,
89 unsigned offset, int value);
90int (* get)( struct gpio_chip * chip,
91 unsigned offset);
92void (* set)( struct gpio_chip * chip,
93 unsigned offset, int value);
......
145};
可以看出,gpio_chip大量的成员都是函数,这些函数就是GPIO操作函数。bgpio_init函数主要任务就是初始化bgc->gc。bgpio_init里面有三个setup函数:bgpio_setup_io、bgpio_setup_accessors和bgpio_setup_direction。这三个函数就是初始化bgc->gc中的各种有关GPIO的操作,比如输出,输入等等。第451~453行的GPIO_PSR、GPIO_DR和GPIO_GDIR都是I.MX6ULL的GPIO寄存器。这些寄存器地址会赋值给bgc参数的reg_dat、reg_set、reg_clr和reg_dir这些成员变量。至此,bgc既有了对GPIO的操作函数,又有了I.MX6ULL有关GPIO的寄存器,那么只要得到bgc就可以对I.MX6ULL的GPIO进行操作。
继续回到mxc_gpio_probe函数,第461行调用函数gpiochip_add向Linux内核注册gpio_chip,也就是port->bgc.gc。注册完成以后我们就可以在驱动中使用gpiolib提供的各个API函数。
45.2.3 gpio子系统API函数
对于驱动开发人员,设置好设备树以后就可以使用gpio子系统提供的API函数来操作指定的GPIO,gpio子系统向驱动开发人员屏蔽了具体的读写寄存器过程。这就是驱动分层与分离的好处,大家各司其职,做好自己的本职工作即可。gpio子系统提供的常用的API函数有下面几个:
1、gpio_request函数
gpio_request函数用于申请一个GPIO管脚,在使用一个GPIO之前一定要使用gpio_request进行申请,函数原型如下:
int gpio_request(unsigned gpio, const char *label)
函数参数和返回值含义如下:
gpio :要申请的gpio标号,使用of_get_named_gpio函数从设备树获取指定GPIO属性信息,此函数会返回这个GPIO的标号。
label :给gpio设置个名字。
返回值: 0,申请成功;其他值,申请失败。
2、gpio_free函数
如果不使用某个GPIO了,那么就可以调用gpio_free函数进行释放。函数原型如下:
void gpio_free(unsigned gpio)
函数参数和返回值含义如下:
gpio :要释放的gpio标号。
返回值: 无。
3、gpio_direction_input函数
此函数用于设置某个GPIO为输入,函数原型如下所示:
int gpio_direction_input(unsigned gpio)
函数参数和返回值含义如下:
gpio :要设置为输入的GPIO标号。
返回值: 0,设置成功;负值,设置失败。
4、gpio_direction_output函数
此函数用于设置某个GPIO为输出,并且设置默认输出值,函数原型如下:
int gpio_direction_output(unsigned gpio, int value)
函数参数和返回值含义如下:
gpio :要设置为输出的GPIO标号。
value: GPIO默认输出值。
返回值: 0,设置成功;负值,设置失败。
5、gpio_get_value函数
此函数用于获取某个GPIO的值(0或1),此函数是个宏,定义所示:
#define gpio_get_value __gpio_get_value
int __gpio_get_value(unsigned gpio)
函数参数和返回值含义如下:
gpio :要获取的GPIO标号。
返回值: 非负值,得到的GPIO值;负值,获取失败。
6、gpio_set_value函数
此函数用于设置某个GPIO的值,此函数是个宏,定义如下
#define gpio_set_value __gpio_set_value
void __gpio_set_value(unsigned gpio, int value)
函数参数和返回值含义如下:
gpio :要设置的GPIO标号。
value: 要设置的值。
返回值: 无
关于gpio子系统常用的API函数就讲这些,这些是我们用的最多的。
45.2.4 设备树中添加gpio节点模板
继续完成45.1.3中的test设备,在45.1.3中我们已经讲解了如何创建test设备的pinctrl节点。本节我们来学习一下如何创建test设备的GPIO节点。
1、创建test设备节点
在根节点“/”下创建test设备子节点,如下所示:
示例代码45.2.4.1 test设备节点
1 test {
2/* 节点内容 */
3};
2、添加pinctrl信息
在45.1.3中我们创建了pinctrl_test节点,此节点描述了test设备所使用的GPIO_IO00这个PIN的信息,我们要将这节点添加到test设备节点中,如下所示:
示例代码45.2.4.2 向test节点添加pinctrl信息
1 test {
2 pinctrl- names = "default";
3 pinctrl- 0=<& pinctrl_test>;
4/* 其他节点内容 */
5};
第2行,添加pinctrl-names属性,此熟悉描述pinctrl名字为“default”。
第3行,添加pinctrl-0节点,此节点引用45.1.3中创建的pinctrl_test节点,表示tset设备的所使用的PIN信息保存在pinctrl_test节点中。
3、添加GPIO属性信息
我们最后需要在test节点中添加GPIO属性信息,表明test所使用的GPIO是哪个引脚,添加完成以后如下所示:
示例代码45.2.4.3 向test节点添加gpio属性
1 test {
2 pinctrl- names = "default";
3 pinctrl- 0=<& pinctrl_test>;
4 gpio =<&gpio1 0 GPIO_ACTIVE_LOW>
5};
第4行,test设备所使用的gpio。
关于pinctrl子系统和gpio子系统就讲解到这里,接下来就使用pinctrl和gpio子系统来驱动I.MX6ULL-ALPHA开发板上的LED灯。
45.2.5 与gpio相关的OF函数
在示例代码45.2.4.3中,我们定义了一个名为“gpio”的属性,gpio属性描述了test这个设备所使用的GPIO。在驱动程序中需要读取gpio属性内容,Linux内核提供了几个与GPIO有关的OF函数,常用的几个OF函数如下所示:
1、of_gpio_named_count函数
of_gpio_named_count函数用于获取设备树某个属性里面定义了几个GPIO信息,要注意的是空的GPIO信息也会被统计到,比如:
gpios = <0
&gpio1 1 2
0
&gpio2 3 4>;
上述代码的“gpios”节点一共定义了4个GPIO,但是有2个是空的,没有实际的含义。通过of_gpio_named_count函数统计出来的GPIO数量就是4个,此函数原型如下:
int of_gpio_named_count(struct device_node *np,const char *propname)
函数参数和返回值含义如下:
nd :设备节点。
propname :要统计的GPIO属性。
返回值: 正值,统计到的GPIO数量;负值,失败。
2、of_gpio_count函数
和of_gpio_named_count函数一样,但是不同的地方在于,此函数统计的是“gpios”这个属性的GPIO数量,而of_gpio_named_count函数可以统计任意属性的GPIO信息,函数原型如下所示:
int of_gpio_count(struct device_node *np)
函数参数和返回值含义如下:
nd :设备节点。
返回值: 正值,统计到的GPIO数量;负值,失败。
3、of_get_named_gpio函数
此函数获取GPIO编号,因为Linux内核中关于GPIO的API函数都要使用GPIO编号,此函数会将设备树中类似<&gpio5 7 GPIO_ACTIVE_LOW>的属性信息转换为对应的GPIO编号,此函数在驱动中使用很频繁!函数原型如下:
int of_get_named_gpio(struct device_node *np,
const char *propname,
int index)
函数参数和返回值含义如下:
nd :设备节点。
propname :包含要获取GPIO信息的属性名。
index: GPIO索引,因为一个属性里面可能包含多个GPIO,此参数指定要获取哪个GPIO的编号,如果只有一个GPIO信息的话此参数为0。
返回值: 正值,获取到的GPIO编号;负值,失败。
45.3硬件原理图分析
本章实验硬件原理图参考8.3小节即可。
45.4 实验程序编写
本实验对应的例程路径为:开发板光盘->2、Linux驱动例程->5_gpioled。
本章实验我们继续研究LED灯,在第四十四章实验中我们通过设备树向dtsled.c文件传递相应的寄存器物理地址,然后在驱动文件中配置寄存器。本章实验我们使用pinctrl和gpio子系统来完成LED灯驱动。
45.4.1 修改设备树文件
1、添加pinctrl节点
I.MX6U-ALPHA开发板上的LED灯使用了GPIO1_IO03这个PIN,打开imx6ull-alientek-emmc.dts,在iomuxc节点的imx6ul-evk子节点下创建一个名为“pinctrl_led”的子节点,节点内容如下所示:
示例代码45.4.1.1 GPIO1_IO03 pinctrl节点
1 pinctrl_led: ledgrp {
2 fsl, pins =<
3 MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10B0/* LED0 */
4>;
5};
第3行,将GPIO1_IO03这个PIN复用为GPIO1_IO03,也就是GPIO,GPI1O_IO03这个PIN的电气属性值为0X10B0,也就是设置IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03寄存器的值为0X10B0。
2、添加LED设备节点
在根节点“/”下创建LED灯节点,节点名为“gpioled”,节点内容如下:
示例代码45.4.1.2 创建LED灯节点
1 gpioled {
2 #address- cells =< 1>;
3 #size- cells =< 1>;
4 compatible = "atkalpha-gpioled";
5 pinctrl- names = "default";
6 pinctrl-0=<&pinctrl_led>;
7 led-gpio =<&gpio1 3 GPIO_ACTIVE_LOW>;
8 status = "okay";
9}
第7行,pinctrl-0属性设置LED灯所使用的PIN对应的pinctrl节点。
第8行,led-gpio属性指定了LED灯所使用的GPIO,在这里就是GPIO1的IO03,低电平有效。稍后编写驱动程序的时候会获取led-gpio属性的内容来得到GPIO编号,因为gpio子系统的API操作函数需要GPIO编号。
3、检查PIN是否被其他外设使用
这一点非常重要!!!
很多初次接触设备树的驱动开发人员很容易因为这个小问题栽了大跟头!因为我们所使用的设备树基本都是在半导体厂商提供的设备树文件基础上修改而来的,而半导体厂商提供的设备树是根据自己官方开发板编写的,很多PIN的配置和我们所使用的开发板不一样。比如A这个引脚在官方开发板接的是I2C的SDA,而我们所使用的硬件可能将A这个引脚接到了其他的外设,比如LED灯上,接不同的外设,A这个引脚的配置就不同。一个引脚一次只能实现一个功能,如果A引脚在设备树中配置为了I2C的SDA信号,那么A引脚就不能再配置为GPIO,否则的话驱动程序在申请GPIO的时候就会失败。检查PIN有没有被其他外设使用包括两个方面:
①、检查pinctrl设置。
②、如果这个PIN配置为GPIO的话,检查这个GPIO有没有被别的外设使用。
在本章实验中LED灯使用的PIN为GPIO1_IO03,因此先检查GPIO_IO03这个PIN有没有被其他的pinctrl节点使用,在imx6ull-alientek-emmc.dts中找到如下内容:
示例代码45.4.1.3 pinctrl_tsc节点
480 pinctrl_tsc: tscgrp {
481 fsl, pins =<
482 MX6UL_PAD_GPIO1_IO01__GPIO1_IO01 0xb0
483 MX6UL_PAD_GPIO1_IO02__GPIO1_IO02 0xb0
484 MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0xb0
485 MX6UL_PAD_GPIO1_IO04__GPIO1_IO04 0xb0
486>;
487};
pinctrl_tsc节点是TSC(电阻触摸屏接口)的pinctrl节点,从第484行可以看出,默认情况下GPIO1_IO03作为了TSC外设的PIN。所以我们需要将第484行屏蔽掉!和C语言一样,在要屏蔽的内容前后加上“/*”和“*/”符号即可。其实在I.MX6U-ALPHA开发板上并没有用到TSC接口,所以第482~485行的内容可以全部屏蔽掉。
因为本章实验我们将GPIO1_IO03这个PIN配置为了GPIO,所以还需要查找一下有没有其他的外设使用了GPIO1_IO03,在imx6ull-alientek-emmc.dts中搜索“gpio1 3”,找到如下内容:
示例代码45.4.1.4 tsc节点
723& tsc {
724 pinctrl- names = "default";
725 pinctrl- 0=<& pinctrl_tsc>;
726 xnur-gpio =<&gpio1 3 GPIO_ACTIVE_LOW>;
727 measure- delay- time =< 0xffff>;
728 pre- charge- time =< 0xfff>;
729 status = "okay";
730};
tsc是TSC的外设节点,从726行可以看出,tsc外设也使用了GPIO1_IO03,同样我们需要将这一行屏蔽掉。然后在继续搜索“gpio1 3”,看看除了本章的LED灯以外还有没有其他的地方也使用了GPIO1_IO03,找到一个屏蔽一个。
设备树编写完成以后使用“makedtbs”命令重新编译设备树,然后使用新编译出来的imx6ull-alientek-emmc.dtb文件启动Linux系统。启动成功以后进入“/proc/device-tree”目录中查看“gpioled”节点是否存在,如果存在的话就说明设备树基本修改成功(具体还要驱动验证),结果如图45.4.1.1所示:
图45.4.1.1 gpio子节点
45.4.2 LED灯驱动程序编写
设备树准备好以后就可以编写驱动程序了,本章实验在第四十四章实验驱动文件dtsled.c的基础上修改而来。新建名为“5_gpioled”文件夹,然后在5_gpioled文件夹里面创建vscode工程,工作区命名为“gpioled”。工程创建好以后新建gpioled.c文件,在gpioled.c里面输入如下内容:
示例代码45.4.2.1 gpioled.c驱动文件代码
1 #include < linux/ types. h>
2 #include < linux/ kernel. h>
3 #include < linux/ delay. h>
4 #include < linux/ ide. h>
5 #include < linux/ init. h>
6 #include < linux/ module. h>
7 #include < linux/ errno. h>
8 #include < linux/ gpio. h>
9 #include < linux/ cdev. h>
10 #include < linux/ device. h>
11 #include < linux/ of. h>
12 #include < linux/ of_address. h>
13 #include < linux/ of_gpio. h>
14 #include < asm/ mach/ map. h>
15 #include < asm/ uaccess. h>
16 #include < asm/ io. h>
17/***************************************************************
18 Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
19文件名 : gpioled.c
20作者 : 左忠凯
21版本 : V1.0
22描述 : 采用pinctrl和gpio子系统驱动LED灯。
23其他 : 无
24论坛 : www.openedv.com
25日志 : 初版V1.0 2019/7/13 左忠凯创建
26 ***************************************************************/
27 #define GPIOLED_CNT 1 /* 设备号个数 */
28 #define GPIOLED_NAME "gpioled" /* 名字 */
29 #define LEDOFF 0 /* 关灯 */
30 #define LEDON 1 /* 开灯 */
31
32/* gpioled设备结构体 */
33struct gpioled_dev{
34 dev_t devid; /* 设备号 */
35struct cdev cdev; /* cdev */
36struct class * class; /* 类 */
37struct device * device; /* 设备 */
38int major; /* 主设备号 */
39int minor; /* 次设备号 */
40struct device_node * nd; /* 设备节点 */
41int led_gpio; /* led所使用的GPIO编号 */
42};
43
44struct gpioled_dev gpioled; /* led设备 */
45
46/*
47 * @description : 打开设备
48 * @param – inode : 传递给驱动的inode
49 * @param – filp : 设备文件,file结构体有个叫做private_data的成员变量
50 * 一般在open的时候将private_data指向设备结构体。
51 * @return : 0 成功;其他失败
52 */
53staticint led_open( struct inode * inode, struct file * filp)
54{
55 filp->private_data =&gpioled;/* 设置私有数据 */
56return 0;
57}
58
59/*
60 * @description : 从设备读取数据
61 * @param – filp : 要打开的设备文件(文件描述符)
62 * @param - buf : 返回给用户空间的数据缓冲区
63 * @param - cnt : 要读取的数据长度
64 * @param – offt : 相对于文件首地址的偏移
65 * @return : 读取的字节数,如果为负值,表示读取失败
66 */
67static ssize_t led_read( struct file * filp, char __user * buf,
size_t cnt, loff_t * offt)
68{
69return 0;
70}
71
72/*
73 * @description : 向设备写数据
74 * @param - filp : 设备文件,表示打开的文件描述符
75 * @param - buf : 要写给设备写入的数据
76 * @param - cnt : 要写入的数据长度
77 * @param – offt : 相对于文件首地址的偏移
78 * @return : 写入的字节数,如果为负值,表示写入失败
79 */
80static ssize_t led_write( struct file * filp, constchar __user * buf,
size_t cnt, loff_t * offt)
81{
82int retvalue;
83unsignedchar databuf[ 1];
84unsignedchar ledstat;
85struct gpioled_dev *dev = filp->private_data;
86
87 retvalue = copy_from_user( databuf, buf, cnt);
88if( retvalue < 0){
89 printk( "kernel write failed!\r\n");
90return- EFAULT;
91}
92
93 ledstat = databuf[ 0]; /* 获取状态值 */
94
95if( ledstat == LEDON){
96 gpio_set_value(dev->led_gpio,0);/* 打开LED灯 */
97}elseif( ledstat == LEDOFF){
98 gpio_set_value(dev->led_gpio,1);/* 关闭LED灯 */
99}
100return 0;
101}
102
103/*
104 * @description : 关闭/释放设备
105 * @param – filp : 要关闭的设备文件(文件描述符)
106 * @return : 0 成功;其他失败
107 */
108staticint led_release( struct inode * inode, struct file * filp)
109{
110return 0;
111}
112
113/* 设备操作函数 */
114staticstruct file_operations gpioled_fops ={
115. owner = THIS_MODULE,
116. open = led_open,
117. read = led_read,
118. write = led_write,
119. release = led_release,
120};
121
122/*
123 * @description : 驱动入口函数
124 * @param : 无
125 * @return : 无
126 */
127staticint __init led_init( void)
128{
129int ret = 0;
130
131/* 设置LED所使用的GPIO */
132/* 1、获取设备节点:gpioled */
133 gpioled.nd = of_find_node_by_path("/gpioled");
134if( gpioled. nd ==NULL){
135 printk( "gpioled node not find!\r\n");
136return- EINVAL;
137}else{
138 printk( "gpioled node find!\r\n");
139}
140
141/* 2、获取设备树中的gpio属性,得到LED所使用的LED编号 */
142 gpioled.led_gpio = of_get_named_gpio(gpioled.nd,"led-gpio",0);
143if( gpioled. led_gpio < 0){
144 printk( "can't get led-gpio");
145return- EINVAL;
146}
147 printk( "led-gpio num = %d\r\n", gpioled. led_gpio);
148
149/* 3、设置GPIO1_IO03为输出,并且输出高电平,默认关闭LED灯 */
150 ret = gpio_direction_output(gpioled.led_gpio,1);
151if( ret < 0){
152 printk( "can't set gpio!\r\n");
153}
154
155/* 注册字符设备驱动 */
156/* 1、创建设备号 */
157if( gpioled. major){ /* 定义了设备号 */
158 gpioled. devid = MKDEV( gpioled. major, 0);
159 register_chrdev_region( gpioled. devid, GPIOLED_CNT, GPIOLED_NAME);
160}else{ /* 没有定义设备号 */
161 alloc_chrdev_region(& gpioled. devid, 0, GPIOLED_CNT, GPIOLED_NAME); /* 申请设备号 */
162 gpioled. major = MAJOR( gpioled. devid); /* 获取分配号的主设备号 */
163 gpioled. minor = MINOR( gpioled. devid); /* 获取分配号的次设备号 */
164}
165 printk( "gpioled major=%d,minor=%d\r\n", gpioled. major, gpioled. minor);
166
167/* 2、初始化cdev */
168 gpioled. cdev. owner = THIS_MODULE;
169 cdev_init(& gpioled. cdev,& gpioled_fops);
170
171/* 3、添加一个cdev */
172 cdev_add(& gpioled. cdev, gpioled. devid, GPIOLED_CNT);
173
174/* 4、创建类 */
175 gpioled. class = class_create( THIS_MODULE, GPIOLED_NAME);
176if( IS_ERR( gpioled. class)){
177return PTR_ERR( gpioled. class);
178}
179
180/* 5、创建设备 */
181 gpioled. device = device_create( gpioled. class,NULL, gpioled. devid,NULL, GPIOLED_NAME);
182if( IS_ERR( gpioled. device)){
183return PTR_ERR( gpioled. device);
184}
185return 0;
186}
187
188/*
189 * @description : 驱动出口函数
190 * @param : 无
191 * @return : 无
192 */
193staticvoid __exit led_exit( void)
194{
195/* 注销字符设备驱动 */
196 cdev_del(& gpioled. cdev); /* 删除cdev */
197 unregister_chrdev_region( gpioled. devid, GPIOLED_CNT); /* 注销 */
198
199 device_destroy( gpioled. class, gpioled. devid);
200 class_destroy( gpioled. class);
201}
202
203 module_init( led_init);
204 module_exit( led_exit);
205 MODULE_LICENSE( "GPL");
206 MODULE_AUTHOR( "zuozhongkai");
第41行,在设备结构体gpioled_dev中加入led_gpio这个成员变量,此成员变量保存LED等所使用的GPIO编号。
第55行,将设备结构体变量gpioled设置为filp的私有数据private_data。
第85行,通过读取filp的private_data成员变量来得到设备结构体变量,也就是gpioled。这种将设备结构体设置为filp私有数据的方法在Linux内核驱动里面非常常见。
第96、97行,直接调用gpio_set_value函数来向GPIO写入数据,实现开/关LED的效果。不需要我们直接操作相应的寄存器。
第133行,获取节点“/gpioled”。
第142行,通过函数of_get_named_gpio函数获取LED所使用的LED编号。相当于将gpioled节点中的“led-gpio”属性值转换为对应的LED编号。
第150行,调用函数gpio_direction_output设置GPIO1_IO03这个GPIO为输出,并且默认高电平,这样默认就会关闭LED灯。
可以看出gpioled.c文件中的内容和第四十四章的dtsled.c差不多,只是取消掉了配置寄存器的过程,改为使用Linux内核提供的API函数。在GPIO操作上更加的规范化,符合Linux代码框架,而且也简化了GPIO驱动开发的难度,以后我们所有例程用到GPIO的地方都采用此方法。
44.4.3 编写测试APP
本章直接使用第四十二章的测试APP,将上一章的ledApp.c文件复制到本章实验工程下即可。
45.5 运行测试
45.5.1 编译驱动程序和测试APP
1、编译驱动程序
编写Makefile文件,本章实验的Makefile文件和第四十章实验基本一样,只是将obj-m变量的值改为gpioled.o,Makefile内容如下所示:
示例代码45.5.1.1 Makefile文件
1 KERNELDIR:= /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
......
4 obj-m := gpioled.o.o
......
11 clean:
12$(MAKE) -C $(KERNELDIR) M= $(CURRENT_PATH) clean
第4行,设置obj-m变量的值为gpioled.o。
输入如下命令编译出驱动模块文件:
make-j32
编译成功以后就会生成一个名为“gpioled.ko”的驱动模块文件。
2、编译测试APP
输入如下命令编译测试ledApp.c这个测试程序:
arm-linux-gnueabihf-gcc ledApp.c -o ledApp
编译成功以后就会生成ledApp这个应用程序。
45.5.2 运行测试
将上一小节编译出来的gpioled.ko和ledApp这两个文件拷贝到rootfs/lib/modules/4.1.15目录中,重启开发板,进入到目录lib/modules/4.1.15中,输入如下命令加载gpioled.ko驱动模块:
depmod //第一次加载驱动的时候需要运行此命令
modprobe gpioled.ko //加载驱动
驱动加载成功以后会在终端中输出一些信息,如图45.5.2.1所示:
图45.5.2.1 驱动加载成功以后输出的信息
从图45.5.2.1可以看出,gpioled这个节点找到了,并且GPIO1_IO03这个GPIO的编号为3。驱动加载成功以后就可以使用ledApp软件来测试驱动是否工作正常,输入如下命令打开LED灯:
./ledApp /dev/gpioled 1 //打开LED灯
输入上述命令以后观察I.MX6U-ALPHA开发板上的红色LED灯是否点亮,如果点亮的话说明驱动工作正常。在输入如下命令关闭LED灯:
./ledApp /dev/gpioled 0 //关闭LED灯
输入上述命令以后观察I.MX6U-ALPHA开发板上的红色LED灯是否熄灭。如果要卸载驱动的话输入如下命令即可:
rmmodgpioled.ko
相关问答
外企软件开发,所说的bing up是什么意思 - GxaGdNSR 的回答 -...bringUP需要做的工作1、kernelspacesensordriver:驱动和设备树2、userspacecamera对应的chromatixlibrary和sensorlibr...