欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED

程序员文章站 2024-02-23 23:13:50
...

   爱,就能使一个人到如此的地步。一次邂逅,一次目光的交融,就是永远的合二为一,就是与上帝的契约;纵使风暴雷电,也无法分解这种心灵的粘合。


  前面的几节,直接使用操作寄存器的方法编写驱动。这只是为了更好掌握驱动程序的本质,在实际开发过程中可不这样做,太低效了!如果驱动开发都是这样去查找寄存器,那我们就变成“寄存器工程师”了,即使是做单片机的都不执着于裸写寄存器了。

一、Pinctrl 子系统

1.1 引入

  无论是哪种芯片,都有类似下图的结构:

【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED
  要想让 pinA、 B 用于 GPIO,需要设置 IOMUX 让它们连接到 GPIO 模块;
  要想让 pinA、 B 用于 I2C,需要设置 IOMUX 让它们连接到 I2C 模块。
  所以 GPIO、 I2C 应该是并列的关系,它们能够使用之前,需要设置 IOMUX。 有时候并不仅仅是设置 IOMUX,还要配置引脚,比如上拉、下拉、开漏等等。
  现在的芯片动辄几百个引脚,在使用到 GPIO 功能时,让你一个引脚一个引脚去找对应的寄存器,这要疯掉。术业有专攻,这些累活就让芯片厂家做吧──他们是 BSP 工程师。我们在他们的基础上开发,我们是驱动工程师。开玩笑的, BSP 工程师是更懂他自家的芯片。

  所以,要把引脚的复用、配置抽出来,做成 Pinctrl 子系统,给 GPIO、 I2C 等模块使用。BSP 工程师要做什么? 看下图:

【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED
  等 BSP 工程师在 GPIO 子系统、 Pinctrl 子系统中把自家芯片的支持加进去后,我们就可以非常方便地使用这些引脚了:点灯简直太简单了。

  等等, GPIO 模块在图中跟 I2C 不是并列的吗?干嘛在讲 Pinctrl 时还把 GPIO 子系统拉进来?大多数的芯片,没有单独的 IOMUX 模块,引脚的复用、配置等等,就是在 GPIO 模块内部实现的。在硬件上 GPIO 和 Pinctrl 是如此密切相关,在软件上它们的关系也非常密切。所以这 2 个子系统要一起讲解。

1.2 重要概念

  从设备树开始学习 Pintrl 会比较容易,主要参考文档是:内核 Documentation\devicetree\bindings\pinctrl\pinctrl-bindings.txt

  这会涉及 2 个对象: pin controller、 client device。前者提供服务:可以用它来复用引脚、配置引脚。后者使用服务声明自己要使用哪些引脚的哪些功能,怎么配置它们。

  • A) pin controller

    • 在芯片手册里你找不到 pin controller,它是一个软件上的概念,你可以认为它对应IOMUX──用来复用引脚,还可以配置引脚(比如上下拉电阻等)。
    • 注意, Pin controller 和 GPIO Controller 不是一回事,前者控制的引脚可用于 GPIO 功能、 I2C 功能;后者只是把引脚配置为输出、输入等简单的功能。
  • B) client device

    • “客户设备”,谁的客户? Pinctrl 系统的客户,那就是使用 Pinctrl 系统的设备,使用引脚的设备。它在设备树里会被定义为一个节点,在节点里声明要用哪些引脚。

下图就可以把几个重要概念理清楚:

图一:状态0和状态1均称为《复用节点》
【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED
图二:状态0称为《复用节点》,状态1称为《配置节点》
【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED
图二中,左边是 pincontroller 节点,右边是 client device 节点:

  • a) pin state

    • 对于一个“client device”来说,比如对于一个 UART 设备,它有多个“状态”: default、 sleep等,那对应的引脚也有这些状态。
      • 比如默认状态下, UART 设备是工作的,那么所用的引脚就要复用为 UART 功能。
      • 在休眠状态下, 为了省电,可以把这些引脚复用为 GPIO 功能;或者直接把它们配置输出高电平。
    • 上图中, pinctrl-names 里定义了 2 种状态: default、 sleep。
      • 第 0 种状态用到的引脚在 pinctrl-0 中定义,它是 state_0_node_a,位于pincontroller 节点中。
      • 第 1 种状态用到的引脚在 pinctrl-1 中定义,它是 state_1_node_a,位于 pincontroller 节点中。
      • 当这个设备处于 default 状态时, pinctrl 子系统会自动根据上述信息把所用引脚复用为uart0 功能。
      • 当这这个设备处于 sleep 状态时, pinctrl 子系统会自动根据上述信息把所用引脚配置为高电平。
  • b) groups 和 function:

    • 一个设备会用到一个或多个引脚,这些引脚就可以归为一组(group);这些引脚可以复用为某个功能: function。
    • 当然:一个设备可以用到多组多能引脚,比如 A1、 A2 两组引脚, A1 组复用为 F1 功能, A2组复用为 F2 功能。
  • c) Generic pin multiplexing node 和 Generic pin configuration node

    • 在上图左边的 pin controller 节点中,有子节点或孙节点,它们是给 client device 使用的。
    • 可以用来描述复用信息:哪组(group)引脚复用为哪个功能(function);
    • 可以用来描述配置信息:哪组(group)引脚配置为哪个设置功能(setting),比如上拉、下拉等.
1.3 使用实例

  注意: pin controller 节点的格式, 没有统一的标准!(但是《复用节点》和《配置节点》这种概念还是有的)每家芯片都不一样。甚至上面的 group、 function 关键字也不一定有,但是概念是有的。

  client device是有固定格式的!(它是写到设备树中的!!)

使用实例如下图。

【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED

1.4 代码中怎么引用 pinctrl

  关于代码中怎么引用 pinctrl,这是透明的,我们的驱动基本不用管。当设备切换状态时,对应的 pinctrl 就会被调用。比如在 platform_device 和 platform_driver 的枚举过程中,流程如下:

【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED
  同样,当系统休眠时,也会去设置该设备 sleep 状态对应的引脚,不需要我们自己去调用代码。

  非要自己调用,也有函数:

devm_pinctrl_get_select_default(struct device *dev); // 使用"default"状态的引脚
pinctrl_get_select(struct device *dev, const char *name); // 根据 name 选择某种状态的引脚
pinctrl_put(struct pinctrl *p); // 不再使用, 退出时调用

二、GPIO 子系统重要概念

2.1 引入

  要操作 GPIO 引脚,先把所用引脚配置为 GPIO 功能,这通过 Pinctrl 子系统来实现。然后就可以根据设置引脚方向(输入还是输出)、读值─获得电平状态,写值─输出高低电平。以前我们通过寄存器来操作 GPIO 引脚,即使 LED 驱动程序,对于不同的板子它的代码也完全不同。

  当 BSP 工程师实现了 GPIO 子系统后,我们就可以:

  • 在设备树里指定 GPIO 引脚(哪一组的哪一个引脚)
  • 在驱动代码中:
    • 使用 GPIO 子系统的标准函数获得 GPIO、设置 GPIO 方向、读取/设置 GPIO 值。这样的驱动代码,将是单板无关的。
2.2 在设备树中指定引脚

  在几乎所有 ARM 芯片中, GPIO 都分为几组,每组中有若干个引脚。 所以在使用 GPIO子系统之前,就要先确定:它是哪组的?组里的哪一个?

  在设备树中,“GPIO 组”就是一个 GPIO Controller,这通常都由芯片厂家设置好。我们要做的是找到它名字,比如“gpio1”,然后指定要用它里面的哪个引脚,比如<&gpio1 0>。

有代码更直观,下图是一些芯片的 GPIO 控制器节点,它们一般都是厂家定义好,在
xxx.dtsi 文件中:

【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED

  我们暂时只需要关心里面的这 2 个属性:

  • gpio-controller;
    • 表示这个节点是一个 GPIO Controller,它下面有很多引脚
  • #gpio-cells = <2>;
    • 表示这个控制器下每一个引脚要用 2 个 32 位的数(cell)来描述(除了第一个参数之外,还需要两个参数描述这个GPIO)
    • 使用多少个 cell 来描述一个引脚,这是 GPIO Controller 自己决定的。比如可以用其中一个 cell 来表示那是哪一个引脚,用另一个 cell 来表示它是高电平有效还是低电平有效,甚至还可以用更多的 cell 来示其他特性。
    • 普遍的用法是,用第 1 个 cell 来表示哪一个引脚,用第 2 个 cell 来表示有效电平:
GPIO_ACTIVE_HIGH : 高电平有效
GPIO_ACTIVE_LOW  :  低电平有效

  定义 GPIO Controller 是芯片厂家的事,我们怎么引用某个引脚呢?在自己的设备节点中使用属性"[<name>-]gpios",示例如下:

【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED
  上图中,可以使用 gpios 属性,也可以使用 name-gpios 属性。(注意,必须是gpios)

2.3、 在驱动代码中调用 GPIO 子系统

  在设备树中指定了 GPIO 引脚,在驱动代码中如何使用?也就是 GPIO 子系统的接口函数是什么?

  GPIO 子系统有两套接口: 基于描述符的(descriptor-based)、老的(legacy)。前者的函数都有前缀“gpiod_”,它使用 gpio_desc 结构体来表示一个引脚;后者的函数都有前缀“gpio_”,它使用一个整数来表示一个引脚。

  要操作一个引脚,首先要 get 引脚,然后设置方向,读值、写值。

  驱动程序中使用不同的GPIO子系统接口要包含不同的头文件:

#include <linux/gpio/consumer.h> // descriptor-based#include <linux/gpio.h> // legacy

下表列出常用的函数:

descriptor-based legacy
获得 GPIO 获得 GPIO
gpiod_get gpio_request
gpiod_get_index
gpiod_get_array gpio_request_array
devm_gpiod_get
devm_gpiod_get_index
devm_gpiod_get_array
设置方向 设置方向
gpiod_direction_input gpio_direction_input
gpiod_direction_output gpio_direction_output
读值、写值 读值、写值
gpiod_get_value gpio_get_value
gpiod_set_value gpio_set_value
释放 GPIO 释放 GPIO
gpio_free gpio_free
gpiod_put gpio_free_array
gpiod_put_array
devm_gpiod_put
devm_gpiod_put_array

  有前缀“devm_”的含义是“设备资源管理”(Managed Device Resource),这是一种自动释放资源的机制。它的思想是“资源是属于设备的,设备不存在时资源就可以自动释放”。

  比如在 Linux 开发过程中,先申请了 GPIO,再申请内存;如果内存申请失败,那么在返回之前就需要先释放 GPIO 资源。如果使用 devm 的相关函数,在内存申请失败时可以直接返回:设备的销毁函数会自动地释放已经申请了的 GPIO 资源。

  故,建议使用“devm_”版本的相关函数。

举例,假设备在设备树中有如下节点:

foo_device {
	compatible = "acme,foo";
	...
	led-gpios = <&gpio 15 GPIO_ACTIVE_HIGH>, /* red */
				<&gpio 16 GPIO_ACTIVE_HIGH>, /* green */
				<&gpio 17 GPIO_ACTIVE_HIGH>; /* blue */
	power-gpios = <&gpio 1 GPIO_ACTIVE_LOW>;
};

那么可以使用下面的函数获得引脚:

struct gpio_desc *red, *green, *blue, *power;

red   = gpiod_get_index(dev, "led", 0, GPIOD_OUT_HIGH);
green = gpiod_get_index(dev, "led", 1, GPIOD_OUT_HIGH);
blue  = gpiod_get_index(dev, "led", 2, GPIOD_OUT_HIGH);
power = gpiod_get(dev, "power", GPIOD_OUT_HIGH);

   要注意的是, gpiod_set_value 设置的值是“逻辑值”,不一定等于物理值。

  什么意思?看下图:

【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED

  旧的“gpio_”函数没办法根据设备树信息获得引脚,它需要先知道引脚号。引脚号怎么确定?
  在 GPIO 子系统中,每注册一个 GPIO Controller 时会确定它的“base number”,那么这个控制器里的第 n 号引脚的号码就是: base number + n。但是如果硬件有变化、设备树有变化,这个 base number 并不能保证是固定的,应该查看 sysfs 来确定 base number。

2.4 sysfs 中的访问方法(老的legacy操作方法,不看也罢)

  在 sysfs 中访问 GPIO,实际上用的就是引脚号,老的方法(legacy )

  • a) 先确定某个 GPIO Controller 的基准引脚号(base number),再计算出某个引脚的号码。方法如下:
    • ① 先在开发板的/sys/class/gpio 目录下,找到各个 gpiochipXXX 目录:
    • 【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED
    • ② 然后进入某个 gpiochip 目录,查看文件 label 的内容
    • ③ 根据 label 的内容对比设备树
      • label 内容来自设备树,比如它的寄存器基地址。用来跟设备树(dtsi 文件)比较,就可以知道这对应哪一个 GPIO Controller。
      • 下图是在 100ask_imx6ull 上运行的结果,通过对比设备树可知 gpiochip96 对应 gpio4:
        【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED

所以 gpio4 这组引脚的基准引脚号就是 96,这也可以“cat base”来再次确认。

  • b) 基于 sysfs 操作引脚
  • 对于输入引脚,假设引脚号为 N,可以通过如下方法读取引脚值
echo N> /sys/class/gpio/export
echo in > /sys/class/gpio/gpio110/direction
cat /sys/class/gpio/gpio110/value
echo N> /sys/class/gpio/unexport
  • 对于输出引脚,假设引脚号为 N,可以通过如下方法设置它的值为1:
echo N > /sys/class/gpio/export
echo out > /sys/class/gpio/gpioN/direction
echo 1 > /sys/class/gpio/gpioN/value
echo N > /sys/class/gpio/unexport
  • 注意:如果驱动程序已经使用了该引脚,那么将会 export 失败,会提示下面的错误:

【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED

三、基于 GPIO 子系统的 LED 驱动程序

3.1 编写思路

  GPIO 的地位跟其他模块,比如 I2C、 UART 的地方是一样的,要使用某个引脚,需要先把引脚配置为 GPIO 功能,这要使用 Pinctrl 子系统, 只需要在设备树里指定就可以。在驱动代码上不需要我们做任何事情。

  GPIO 本身需要确定引脚,这也需要在设备树里指定。设备树节点会被内核转换为 platform_device。对应的, 驱动代码中要注册一个 platform_driver,在 probe 函数中:获得引脚、 注册file_operations。在 file_operations 中: 设置方向、读值/写值。

下图就是一个设备树的例子:

【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED

3.2 在设备树中添加 Pinctrl 信息

  有些芯片提供了设备树生成工具,在 GUI 界面中选择引脚功能和配置信息,就可以自动生成 Pinctrl 子结点。把它复制到你的设备树文件中,再在 client device 结点中引用就可以。

  有些芯片只提供文档,那就去阅读文档,一般在内核源码目录:Documentation\devicetree\bindings\pinctrl 下面,保存有该厂家的文档。

  如果连文档都没有,那只能参考内核源码中的设备树文件,在内核源码目录arch/arm/boot/dts 目录下。

  最后一步, 网络搜索。Pinctrl 子节点的样式如下:

【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED

3.3 在设备树中添加 GPIO 信息

  先查看电路原理图确定所用引脚,再在设备树中指定:添加”[name]-gpios”属性, 指定使 用 的 是 哪 一 个 GPIO Controller 里 的 哪 一 个 引 脚 , 还 有 其 他 Flag 信 息 , 比 如GPIO_ACTIVE_LOW 等。 具体需要多少个 cell 来描述一个引脚,需要查看设备树中这个 GPIO Controller 节点里的“#gpio-cells”属性值,也可以查看内核文档。

示例如下:
【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED

3.4 编程示例
  • a. 定义、注册一个 platform_driver
  • b. 在它的 probe 函数里:
    • b.1 根据 platform_device 的设备树信息确定 GPIO: gpiod_get
    • b.2 定义、注册一个 file_operations 结构体
    • b.3 在 file_operarions 中使用 GPIO 子系统的函数操作 GPIO:
      gpiod_direction_output、 gpiod_set_value
①、打开I.MX引脚配置工具生成pinctrl

选择I.MX6ULL的配置文件(第一次使用需要下载一些东西,最好挂上*!)
【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED

这一次我们还是以GPIO5_3为例,操作如图所示

【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED
需要说明的:

  • 图形化操作修改,对应右边的imx-board.dtsi会有添加相加代码,并且添加的代码用绿色标注出来!
  • 【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED
  • 路由置×××,,,就是复用成×××功能,前面有红色标志的标识选不了
  • 【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED
②、修改设备树文件
  • 放置软件生成的pinctrl配置到设备树下
  • 打开设备树文件,我的在/home/clay/linux/IMX6ULL/Linux_Drivers/linux-4.9.88/arch/arm/boot/dts/100ask_imx6ull_qemu.dts
  • 搜索GPIO5_IO3对应的引脚名字,即iomuxc_snvs
  • 【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED
  • 【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED

复制软件生成的部分代码到该节点下,即

BOARD_InitPinsSnvs: BOARD_InitPinsSnvsGrp {        /*!< Function assigned for the core: Cortex-A7[ca7] */
            fsl,pins = <
                MX6ULL_PAD_SNVS_TAMPER3__GPIO5_IO03        0x000110A0
            >;
        };

【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED

BOARD_InitPinsSnvs和BOARD_InitPinsSnvsGrp名字可以改成我们想要的(后面要用),比如改成:myled_for_gpio_subsys
【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED

接着在根目录下创建一个子节点!依次加入:

	myled{
		compatible = "100ask,leddrv";
		pinctrl-names = "default";
		pinctrl-0 = <&myled_for_gpio_subsys>;
		led-gpios = <&gpio5 3 GPIO_ACTIVE_LOW>;
	};

【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED

需要说明的是:

  • compatible 要和驱动中的compatible 名字一样
  • 【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED
  • pinctrl-0取值正式我们上一步从工具复制过来重命名的!
  • led-gpios要和驱动中的一致!
  • 【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED
  • GPIO_ACTIVE_LOW 这个有效电平需要根据原理图确定,我们的小灯是低电平点亮,所以有效电平是低!

最后排查一下系统中还有没有其他在使用GPIO_IO3,在设备树中利用工具生成的MX6ULL_PAD_SNVS_TAMPER3__GPIO5_IO03进行搜索即可。

若找到,比如如图所示

【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED

我们可以再搜索pinctrl_leds找到对应的节点,比如如图所示

【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED

我们可以在该节点中加入status="disabled";,禁止它起作用!
【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED

这一些完成后,编译设备树!

进入Linux源码目录,我的是/home/clay/linux/IMX6ULL/Linux_Drivers/linux-4.9.88,然后输入make dtbs

【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED

没有问题后,复制100ask_imx6ull_qemu.dtb文件到/home/clay/linux/qemu/new/ubuntu-16.04_imx6ul_qemu_system-release/imx6ull-system-image进行覆盖!

②、编写驱动程序

这个还是老套路了!!!

leddrv.c

#include <linux/module.h>

#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>


/* 1. 确定主设备号                                                                 */
static int major = 0;
static struct class *led_class;
static struct gpio_desc *led_gpio;


/* 3. 实现对应的open/read/write等函数,填入file_operations结构体                   */
static ssize_t led_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}

/* write(fd, &val, 1); */
static ssize_t led_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
	int err;
	char status;//这个表示逻辑值
	//struct inode *inode = file_inode(file);
	//int minor = iminor(inode);
	
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	err = copy_from_user(&status, buf, 1);

	/* 根据次设备号和status控制LED */
	gpiod_set_value(led_gpio, status);
	
	return 1;
}

static int led_drv_open (struct inode *node, struct file *file)
{
	//int minor = iminor(node);
	
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	/* 根据次设备号初始化LED */
	gpiod_direction_output(led_gpio, 0);//这里的1 0 表示逻辑值,因为DTS里面设置了GPIO_ACTIVE_HIGH
	
	return 0;
}

static int led_drv_close (struct inode *node, struct file *file)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}

/* 定义自己的file_operations结构体                                              */
static struct file_operations led_drv = {
	.owner	 = THIS_MODULE,
	.open    = led_drv_open,
	.read    = led_drv_read,
	.write   = led_drv_write,
	.release = led_drv_close,
};

/* 4. 从platform_device获得GPIO
 *    把file_operations结构体告诉内核:注册驱动程序
 */
static int chip_demo_gpio_probe(struct platform_device *pdev)
{
	//int err;
	
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

	/* 4.1 设备树中定义有: led-gpios=<...>;	*/
    led_gpio = gpiod_get(&pdev->dev, "led", 0);//第三个参数为0,表示先不设置引脚!
	if (IS_ERR(led_gpio)) {
		dev_err(&pdev->dev, "Failed to get GPIO for led\n");
		return PTR_ERR(led_gpio);
	}
    
	/* 4.2 注册file_operations 	*/
	major = register_chrdev(0, "100ask_led", &led_drv);  /* /dev/led */

	led_class = class_create(THIS_MODULE, "100ask_led_class");
	if (IS_ERR(led_class)) {
		printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
		unregister_chrdev(major, "led");
		gpiod_put(led_gpio);
		return PTR_ERR(led_class);
	}

	device_create(led_class, NULL, MKDEV(major, 0), NULL, "100ask_led%d", 0); /* /dev/100ask_led0 */
        
    return 0;
    
}

static int chip_demo_gpio_remove(struct platform_device *pdev)
{
	device_destroy(led_class, MKDEV(major, 0));
	class_destroy(led_class);
	unregister_chrdev(major, "100ask_led");
	gpiod_put(led_gpio);
    
    return 0;
}


static const struct of_device_id ask100_leds[] = {
    { .compatible = "100ask,leddrv" },
    { },
};

/* 1. 定义platform_driver */
static struct platform_driver chip_demo_gpio_driver = {
    .probe      = chip_demo_gpio_probe,
    .remove     = chip_demo_gpio_remove,
    .driver     = {
        .name   = "100ask_led",
        .of_match_table = ask100_leds,
    },
};

/* 2. 在入口函数注册platform_driver */
static int __init led_init(void)
{
    int err;
    
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	
    err = platform_driver_register(&chip_demo_gpio_driver); 
	
	return err;
}

/* 3. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数
 *     卸载platform_driver
 */
static void __exit led_exit(void)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

    platform_driver_unregister(&chip_demo_gpio_driver);
}


/* 7. 其他完善:提供设备信息,自动创建设备节点                                     */

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

ledtest.c


#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

/*
 * ./ledtest /dev/100ask_led0 on
 * ./ledtest /dev/100ask_led0 off
 */
int main(int argc, char **argv)
{
	int fd;
	char status;
	
	/* 1. 判断参数 */
	if (argc != 3) 
	{
		printf("Usage: %s <dev> <on | off>\n", argv[0]);
		return -1;
	}

	/* 2. 打开文件 */
	fd = open(argv[1], O_RDWR);
	if (fd == -1)
	{
		printf("can not open file %s\n", argv[1]);
		return -1;
	}

	/* 3. 写文件 */
	if (0 == strcmp(argv[2], "on"))
	{
		status = 1;
		write(fd, &status, 1);
	}
	else
	{
		status = 0;
		write(fd, &status, 1);
	}
	
	close(fd);
	
	return 0;
}

Makefile

KERN_DIR = /home/clay/linux/IMX6ULL/Linux_Drivers/linux-4.9.88# 板子所用内核源码的目录

all:
	make -C $(KERN_DIR) M=`pwd` modules 
	$(CROSS_COMPILE)gcc -o ledtest ledtest.c 

clean:
	make -C $(KERN_DIR) M=`pwd` modules clean
	rm -rf modules.order
	rm -f ledtest
	
obj-m += leddrv.o

四、运行程序

编译程序没有问题后,运行qemu虚拟开发板,并做好准备工作!将

  • 拷贝led.ko和ledtest到NFS中
cp *.ko ledtest ~/linux/qemu/NFS/
  • 在qemu终端,加载led.ko文件
insmod led.ko

在qemu中加载最后一个模块时,会出现下面的提示信息,但是ctrl+c之后,似乎测试还是可以用的,不知道是怎么回事。知道的朋友,可以在下面留言一起探讨!
【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED

  • 在qemu终端,运行应用程序打开LED0
./ledtest /dev/100ask_led0 on

【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED

  • 在qemu终端,运行应用程序关闭LED0
./ledtest /dev/100ask_led0 off

【嵌入式Linux驱动开发】九、了解重要Pinctrl和GPIO子系统的使用,点亮一盏真的LED

大功告成,点亮一个LED终于完结!哈哈哈~