首页 > 系统相关 >嵌入式Linux中的LED驱动控制(使用Pinctrl和GPIO子系统)

嵌入式Linux中的LED驱动控制(使用Pinctrl和GPIO子系统)

时间:2024-08-02 22:52:14浏览次数:9  
标签:led pinctrl Pinctrl LED rgb gpio GPIO 节点

在前面驱动LED的所有案例中,都是在驱动程序中去设置每个引脚的复用功能,这会导致所编写的驱动程序移植困难,可重用性差,缺乏对引脚的统一管理,容易出现引脚的重复定义等等弊病。为此,Linux内核引入了pinctrl子系统和GPIO子系统的概念。pinctrl子系统主要用于芯片引脚功能的管理,它基本上是由芯片厂商来实现的(详见官网https://wiki.stmicroelectronics.cn/stm32mpu/wiki/Pinctrl_device_tree_configuration),而GPIO子系统则是负责IO引脚的控制动作。他们都依赖设备树来实现。

下面先来看pinctrl子系统。简单来说它用于管理芯片引脚并自动完成引脚的初始化,开发者只需要在设备树中按照规定的格式写出相应的配置参数即可。

本例中用到的pinctrl子系统文件位于源码arch/arm/boot/dts/目录下,名称为stm32mp157-pinctrl.dtsi。这个文件是芯片厂商官方将芯片的通用部分单独提出来而形成的一些设备树配置。在该文件中,可以看到有如下定义。

soc {
  pinctrl: pin-controller@50002000 {
  #address-cells = <1>;
  #size-cells = <1>;
  compatible = "st,stm32mp157-pinctrl";
  ranges = <0 0x50002000 0xa400>;
  interrupt-parent = <&exti>;
  st,syscfg = <&exti 0x60 0xff>;
  hwlocks = <&hsem 0>;
  pins-are-numbered;
  }
  ……
};

在soc节点中汇总了所需引脚的配置信息,pinctrl子系统存储使用到的节点信息。

在stm32mp157a-basic.dts设备树文件中,通过&pinctrl”(追加)方式在“pinctrl”节点下追加内容,其描述形式如下。

&pinctrl {
  xxx: xxx {
  pins {
      pinmux = <STM32_PINMUX('A', 10, ANALOG)>;
     };
  };
};

上述描述了一个外设xxx,其使用的引脚为GPIOA10,复用功能为ANALOG,电气特性没有指定,使用默认值。

每个芯片厂商的pinctrl子节点的编写格式并不相同,它们由芯片厂商自定义,这不属于设备树的规范。如果要添加自己的pinctrl节点,只要按照上面的格式编写即可。关于pinctrl节点如何去描述,可以在内核文档目录中查找芯片产商给出的文档。如ST官方的pinctrl文档为Documentation/devicetree/bindings/pinctrl/st,stm32-pinctrl.txt。

添加子节点时,只需要将引脚信息以一定格式,写入到stm32mp157a-basic.dts设备树文件中的pinctrl子节点即可,比如本例中添加RGB的pinctrl子节点,如下所示。

&pinctrl {
  pinctrl_rgb_led: rgb_led{
  pins {
      pinmux = <STM32_PINMUX('A', 13, GPIO)>,
            <STM32_PINMUX('G', 2, GPIO)>,
            <STM32_PINMUX('B', 5, GPIO)>;
      drive-push-pull;
     };
  };
};

新增的节点名为“rgb_led”,名字任意选取,长度不要超过32 个字符,最好能表达出节点的信息。“pinctrl_rgb_led”节点标签,“pinctrl_”是固定的格式,后面的内容自己定义,会在后面通过这个标签引用这个节点。在添加完pinctrl子节点后,系统就会根据添加的配置信息将引脚初始化为GPIO功能,并设置为推挽模式。

接下来看GPIO子系统。在没有使用GPIO子系统之前,如果要点亮一个LED,首先需要得到led相关的配置寄存器,再手动地去读、改、写这些配置寄存器,以实现控制LED的目的。有了GPIO子系统之后这部分工作由GPIO子系统来完成,开发者只需要调用GPIO子系统提供的API函数即可完成对GPIO的控制动作。

在stm32mp157-pinctrl.dtsi文件中的pinctrl 子节点已经记录了GPIO控制器的寄存器地址,下面给出的是GPIOA节点部分的内容。

soc {
        pinctrl: pin-controller@50002000 {
            #address-cells = <1>;
            #size-cells = <1>;
            compatible = "st,stm32mp157-pinctrl";
            ranges = <0 0x50002000 0xa400>;
            interrupt-parent = <&exti>;
            st,syscfg = <&exti 0x60 0xff>;
            hwlocks = <&hsem 0>;
            pins-are-numbered;

            gpioa: gpio@50002000 {
                gpio-controller;
                #gpio-cells = <2>;
                interrupt-controller;
                #interrupt-cells = <2>;
                reg = <0x0 0x400>;
                clocks = <&rcc GPIOA>;
                st,bank-name = "GPIOA";
                status = "disabled";
            };
        …………
        };
};

compatible :与GPIO子系统的平台驱动做匹配。
ranges :GPIO外设寄存器的基地址,在gpioa的reg属性中GPIOA的寄存器组的映射地址为50002000,范围为0x400。
interrupt-parent :表示中断控制器是exti外设。
clocks :初始化GPIO外设时钟信息。
gpio-controller :表示gpioa是一个GPIO控制器。
#gpio-cells :表示有多少个cells来描述GPIO引脚。
#interrupt-controller : 是中断控制器。
#interrupt-cells : 表示用多少个cells来描述一个中断。

对以上部分只有大致有个了解就可以了,一般芯片产商会将这部分信息完善好。GPIOA这个节点对整个GPIOA进行了描述。使用GPIO子系统时只需要往设备树中添加设备节点即可。在驱动程序中使用GPIO子系统提供的API就可实现对GPIO的控制。

在本例中,基于GPIO子系统的rgb_led设备树节点添加到了stm32mp157a-basic.dts设备树文件的根节点内,如下所示。

rgb_led{
        #address-cells = <1>;
        #size-cells = <1>;
        pinctrl-names = "default";
        compatible = "fire,rgb-led";
        pinctrl-0 = <&pinctrl_rgb_led>;
        rgb_leds = <&gpioa 13 GPIO_ACTIVE_LOW
                    &gpiog 2 GPIO_ACTIVE_LOW
                    &gpiob 5 GPIO_ACTIVE_LOW>;
        status = "okay";
};

compatible属性值用于与led的平台驱动做匹配。pinctrl-0用于指定前面已经定义好的pinctrl子节点。宏定义GPIO_ACTIVE_LOW指定高电平有效,GPIO_ACTIVE_HIGH指定低电平有效。(注意:在驱动程序中,1表示有效,0表示无效。)

小结一下,先在设备树文件stm32mp157a-basic.dts中包含STM官方的stm32mp157-pinctrl.dtsi文件,然后编写一个pinctrl子节点(见上面的pinctrl_rgb_led),最后编写一个设备树子节点(见上面的rgb_led子节点)来调用它就可以了,余下的事情就是在驱动中去获取这个设备子节点的内容了。pinctrl子系统负责芯片引脚复用功能的选定,GPIO子系统负责引脚输入输了功能的相关配置。

以上完成后,就可以编译设备树了,编译完成后替换开发板上的设备树文件,具体操作可参见“嵌入式Linux中的LED驱动控制(设备树方式)”一文中的相关部分。

接下来看平台驱动程序部分,下面是驱动的全部代码。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/gpio/consumer.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <asm/mach/map.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/io.h>
#include <linux/device.h>
#include <linux/platform_device.h>
static dev_t devid;    //设备号
static struct cdev led_cdev;    //定义字符型结构体
static int rgb_led_red, rgb_led_green, rgb_led_blue;//定义红绿蓝3个变量
struct class *led_class;    //类结构体
struct device_node *rgb_led_device_node; //rgb_led的设备树节点结构体
//实现open函数,为file_oprations结构体成员函数
static int led_open(struct inode *inode, struct file *filp)
{
    return 0;
}
//实现write函数,为file_oprations结构体成员函数
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    unsigned char value; //用于保存接收到的数据
    unsigned long n;
    n = copy_from_user(&value, buf, cnt);
    switch(value)            //根据应用空间的值判断具体操作
  {
        case 0:                        //全部点亮三个LED
            gpio_set_value(rgb_led_red, 0);  
            gpio_set_value(rgb_led_blue, 0);
            gpio_set_value(rgb_led_green, 0); 
            break;
        case 1:                        //点亮红色LED
            gpio_set_value(rgb_led_red, 0);
            break;
        case 2:                        //点亮绿色LED
            gpio_set_value(rgb_led_green, 0); 
            break;
        case 3:                        //点亮蓝色LED
            gpio_set_value(rgb_led_blue, 0);
            break;
        case 4:                        //熄灭红色LED
            gpio_set_value(rgb_led_red, 1);
            break;
        case 5:                        //熄灭绿色LED
            gpio_set_value(rgb_led_green, 1); 
            break;
        case 6:                        //熄灭蓝色LED
            gpio_set_value(rgb_led_blue, 1); 
            break;
        case 7:                        //全部熄灭三个LED
            gpio_set_value(rgb_led_red, 1); 
            gpio_set_value(rgb_led_blue, 1); 
            gpio_set_value(rgb_led_green, 1); 
            break;
        default:                        //全部熄灭
            gpio_set_value(rgb_led_red, 1); 
            gpio_set_value(rgb_led_blue, 1); 
            gpio_set_value(rgb_led_green, 1); 
            break;
    }
       return cnt;
}
//实现release函数,为file_oprations结构体函数
static int led_release(struct inode *inode, struct file *filp)
{
    return 0;
}
//填充一个file_oprations类型的结构体,名为led_dev_fops,包含上述声明的成员函数
static struct file_operations  led_dev_fops = 
{
    .owner = THIS_MODULE,
    .open = led_open,
    .write = led_write,
    .release = led_release,
};
/*----------------平台驱动函数集-----------------*/
static int led_pdrv_probe(struct platform_device *pdv)
{
    //获取rgb_led的设备树节点
    rgb_led_device_node = of_find_node_by_path("/rgb_led");
    if(rgb_led_device_node == NULL)
    {
        printk(KERN_ERR "\t  get rgb_led failed!  \n");
        return -1;
    }
    //获取rgb_led节点的红绿蓝灯子节点
    rgb_led_red = of_get_named_gpio(rgb_led_device_node, "rgb-gpios", 0);
    rgb_led_green = of_get_named_gpio(rgb_led_device_node, "rgb-gpios", 1);
    rgb_led_blue = of_get_named_gpio(rgb_led_device_node, "rgb-gpios", 2);
    //申请红绿蓝灯子节点
    if(gpio_request(rgb_led_red, "red"))
    {
        printk("get gpio_red failed!  \n");
    }
    if(gpio_request(rgb_led_green, "green"))
    {
        printk("get gpio_green failed!  \n");
    }
    if(gpio_request(rgb_led_blue, "blue"))
    {
        printk("get gpio_blue failed!  \n");
    }
    //设置红绿蓝引脚为输出方向,并输出高电平
    gpio_direction_output(rgb_led_red, 1);
    gpio_direction_output(rgb_led_green, 1);
    gpio_direction_output(rgb_led_blue, 1);
    //申请主设备号
    if (alloc_chrdev_region(&devid, 0, 1, "led") < 0)
    {
        printk("fail to alloc devid\n");
        return -EFAULT;
    }
    led_cdev.owner = THIS_MODULE;
    //绑定前面声明的file_oprations类型的结构体到字符设备
    cdev_init(&led_cdev, &led_dev_fops);
    //填充上面申请到的主设备号到字符设备
    if (cdev_add(&led_cdev, devid, 1) < 0)
    {
        printk("fail to add cdev\n");
        return -EFAULT;
    }
    //创建一个类
    led_class = class_create(THIS_MODULE, "my_leds");
    //创建一个设备节点
    device_create(led_class, NULL, devid, NULL, "led");
    printk("platform driver probed!\n");
    return 0;
}
//remove函数中,删除设备并释放设备号
static int led_pdrv_remove(struct platform_device *pdev)
{
    //释放红绿蓝子节点
    gpio_free(rgb_led_red);
    gpio_free(rgb_led_green);
    gpio_free(rgb_led_blue);
    unregister_chrdev_region(devid, 1); //释放主设备号
    cdev_del(&led_cdev);                          //删除字符设备
    device_destroy(led_class, devid);          //销毁设备节点
    class_destroy(led_class);                     //销毁类
    printk("platform driver removed!\n");
    return 0;
}
//填充of_device_id结构体,名为rgb_led,用于指明匹配表
static const struct of_device_id rgb_led[] = {
    {.compatible = "fire,rgb-led"},        //匹配内容
    {/* sentinel */}
};
//以下填充一个platform_driver结构体
struct platform_driver led_platform_driver = {
    .probe = led_pdrv_probe,        //指定probe函数成员
    .remove = led_pdrv_remove,    //指定remove函数成员
    .driver = {
        .name = "rgb-leds-platform",    //指定设备名称
        .owner = THIS_MODULE,
        .of_match_table = rgb_led,        //指定匹配表名称
    }
};
//以下定义模块的入口函数
static int __init led_pdrv_init(void)
{
    platform_driver_register(&led_platform_driver);//注册一个platform驱动
    printk("led platform driver initted!\n");
    return 0;
}
//以下定义模块的出口函数
static void __exit led_pdrv_exit(void)
{
    platform_driver_unregister(&led_platform_driver); //释放一个platform驱动
    printk("led platform driver exited!\n");
}
module_init(led_pdrv_init);
module_exit(led_pdrv_exit);
MODULE_LICENSE("GPL");

在驱动中,主要是利用GPIO子系统提供的API函数进行操作,下面就来看一下这些常用的API函数。

1、 获取GPIO编号函数:static inline int of_get_named_gpio(struct device_node *np, const char *propname, int index),参数np指定设备节点,即通过of_find_node_by_path函数查找节点的返回值。参数propname指定GPIO属性名,要与设备树中定义的属性名一致。参数index为索引值,当设备树中的一条引脚属性包含有多个引脚时,该参数用于指定获取那个引脚。如果获取成功则返回获取的GPIO编号,失败则返回一个负数。
2、GPIO申请函数:static inline int gpio_request(unsigned gpio, const char *label),参数gpio为要申请的GPIO编号,该值是函数of_get_named_gpio的返回值。参数label为引脚名字,相当于为申请得到的引脚取了个别名。如果申请成功则返回0,失败返回一个负数。该函数主要用来确认内核没有使用过要申请的引脚,一般在获取到引脚后都应该申请一下。
3、GPIO释放函数:static inline void gpio_free(unsigned gpio),参数gpio为要释放的GPIO编号。该函数与gpio_request互为反函数,一个申请,一个释放。一个GPIO只能被申请一次,当不再使用某一个引脚时应该将其释放。
4、GPIO输出设置函数:static inline int gpio_direction_output(unsigned gpio , int value),参数gpio为要设置的GPIO的编号,参数value指定输出值,1为高电平,0为低电平。如果设置成功则返回0,失败返回一个负数。
5、GPIO输入设置函数:static inline int gpio_direction_input(unsigned gpio),参数gpio为要设置的GPIO的编号,如果设置成功则返回0,失败返回一个负数。
6、获取GPIO引脚值函数:static inline int gpio_get_value(unsigned gpio),参数gpio为要设置的GPIO的编号,返回值为获取得到的引脚状态,失败返回一个负数。
7、设置GPIO输出值:static inline int gpio_direction_output(unsigned gpio, int value),参数gpio为要设置的GPIO的编号,参数value为输出值,1为高电平,0为低电平。如果设置成功则返回0,失败返回一个负数。

对比上面的平台驱动与前面“嵌入式Linux中的LED驱动控制(设备树方式)”一文中的平台驱动可以看到,在引入了GPIO子系统之后,由于不再涉及寄存器操作,程序变得简单多了,也更有利于移植。

配套的Makefile文件内容如下。

KERNEL_DIR=/opt/ebf_linux_kernel_mp157_depth1/build_image/build
ARCH=arm
CROSS_COMPILE=arm-linux-gnueabihf-
export ARCH CROSS_COMPILE
obj-m := led.o
all:
    $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
clean:
    $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean

接下来是应用程序,文件名为app.c。

#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
   int fd;
   unsigned char val = 0;
   fd = open("/dev/led", O_RDWR);        //打开设备节点
   if( fd < 0 )
      printf("can`t open\n");
   if( argc != 3 )                        //命令参数不对时提示
    {
       printf("Usage :\n");
       printf("%s <all|red|green|blue> <on|off>\n", argv[0]);
       return 0;
    }
   if(strcmp(argv[1], "all") == 0)
    {
      if(strcmp(argv[2], "on") == 0)
         val = 0;                        //值为0时全部点亮
      else
         val = 7;                        //值为7时全部熄灭
    }
   else if(strcmp(argv[1], "red") == 0)
    {
      if(strcmp(argv[2], "on") == 0)
        val = 1;                        //值为1时红色点亮
      else
        val = 4;                        //值为4时红色熄灭
    }
   else if(strcmp(argv[1], "green") == 0)
    {
      if(strcmp(argv[2], "on") == 0)
        val = 2;                        //值为2时绿色点亮
      else
        val = 5;                        //值为5时绿色熄灭
    }
   else if(strcmp(argv[1], "blue") == 0)
    {
      if(strcmp(argv[2], "on") == 0)
        val = 3;                        //值为3时蓝色点亮
      else
        val = 6;                        //值为6时蓝色熄灭
    }
   write(fd, &val, 1);            //把值写入设备节点
   close(fd);                     //关闭设备节点
   return 0;
}

可以看出应用程序与“嵌入式Linux中的LED驱动控制(设备树方式)”中的一样,并没有改动过。完成后,先执行make命令编译驱动程序,若成功会生成名为led.ko的驱动模块文件。然后对应用程序进行交叉编译,执行“arm-linux-gnueabihf-gcc app.c -o app”即可。实验结果与“嵌入式Linux中的LED驱动控制”一文中的完全一样。

对于在嵌入式Linux下控制LED,虽然前面给出过很多种方式,但无疑使用Pinctl和GPIO子系统来实现,才是其最终方式,也是最为推荐的方式。

标签:led,pinctrl,Pinctrl,LED,rgb,gpio,GPIO,节点
From: https://www.cnblogs.com/fxzq/p/18295226

相关文章

  • [paper阅读笔记][2023]CorpusLM: Towards a Unified Language Model on Corpusfor Kno
    文章链接:https://arxiv.org/pdf/2402.01176v2Paper的任务处理各种知识密集型任务任务的科学问题本文任务虽然是:提出一个统一的语言模型来处理各种知识密集型任务,但其实其本质科学问题是:如何提高LLMs在知识密集型任务中的检索效率。原因是:LLMs在生成文本时容易出现错误信......
  • ESP32-S3+1.3寸OLED+SH1106
    简介:事情就是为了看大一点的屏幕,买了1.3寸的OLED屏幕(4pin),结果发现是SH1106驱动。试了很多方法,终于点亮了这个oled。资料下载:1.首先就是下载普中资料:普中科技-各型号产品资料下载链接_公司新闻_新闻资讯_深圳市普中科技有限公司(prechin.cn)2.根据型号,我直接是ESP32-S3,下......
  • .net 8 应用在docker容器中创建失败 Failed to create CoreCLR, HRESULT: 0x80070008
    在UAT环境中docker容器里部署.net8应用没问题,在dev环境dockerrun启动失败,并报错FailedtocreateCoreCLR,HRESULT:0x80070008以下文章解决了我遇到的问题,感谢https://www.cnblogs.com/cyq1162/p/17981333处理办法增加--security-optseccomp=unconfineddockerrun-......
  • centos7 解决docker 拉取镜像错误 error pulling image configuration: download fai
    为什么会出现i/otimeout错误?i/otimeout错误主要是由于网络连接不稳定或者服务器响应慢导致的。当Docker尝试从镜像仓库拉取镜像时,如果在规定时间内没有得到响应,就会出现i/otimeout错误。“错误的根源在于网络连接和镜像仓库的响应速度” 解决方案:换源为了解决这个......
  • 51单片机之LED篇(二)独立按键
    一、独立按键的介绍1.1独立按键的基本原理相当于一种电子开关,按下时开关接通,松开时开关断开。开关功能:独立按键内部通常包含一个有弹性的金属片,当按键被按下时,金属片与触点接触,电路连通;当按键松开时,金属片恢复原状,电路断开。电平变化:在51单片机系统中,独立按键通常一端接......
  • 51单片机之LED篇(一)
    本文所用的单片机是普中51A2套件一、LED介绍1.1结构LED灯,即发光二极管,是一种固态的半导体器件,其核心是一个PN结。LED灯的结构相对简单,主要由PN结芯片、电极和光学系统组成。1.2发光原理LED灯的发光原理基于半导体材料的特性。当给LED的PN结加上正向电压时,由于PN结的内......
  • STM32学习三GPIO操作
    点灯:二极管利用的是两边的电压差点亮,如图外接有3.3v电压,所以只需要引脚给低电平即可驱动。当外接地时,需要芯片给出高电平驱动,在推挽输出的模式下输入输出都有很强的驱动能里。但一般都是led外接高电压,因为很多芯片都采用高电平弱驱动,低电平强驱动的规则。1.芯片数据手册......
  • 【STM32】GPIO口以及EXTI外部中断
    个人主页~有关结构体的知识在这~有关枚举的知识在这~GPIO口以及EXTI外部中断GPIO一、简介二、基本结构三、输入输出模式1、输入模式(1)上拉输入(2)下拉输入(3)浮空输入(4)模拟输入2、输出模式(1)推挽输出(2)开漏输出(3)复用推挽输出(4)复用开漏输出EXIT外部中断一、中断系统二、......
  • STM32—GPIO
    1.GPIO介绍GPIO可配置8种输入输出模式模式名称性质特征浮空输入数字输入可读取引脚电平,若引脚悬空,则电平不确定上拉输入数字输入可读取引脚电平,内部连接上拉电阻,悬空时默认高电平下拉输入数字输入可读取引脚电平,内部链接下拉电阻,悬空时默认低电平模拟输入模拟输入GPIO无效,......
  • 独“数”一帜 双证加冕!TeleDB亮相可信数据库发展大会
    近日,2024可信数据库发展大会在北京召开,主题为“自主、创新、引领”。大会重磅发布多项中国信通院及中国通信标准化协会大数据技术标准推进委员会(CCSATC601)在数据库领域最新研究和实践成果。一众数据库领域的专家、学者、创业者汇聚一堂,围绕金融、电信、能源与政务领域的数据库应......