STM32MP157驱动开发——Linux自带的LED灯驱动


0.前言

  学习了一个月的驱动开发了,今天仍然要开发一个 LED 灯设备驱动。和以往的字符型、新字符型、设备树下的字符驱动、gpio 子系统的开发方式都不同,今天使用 platform 驱动框架进行开发,这也是现在 Linux 驱动开发的最常用的方式。并且本次开发中,将一次性对开发板上的两个 LED 灯进行开发。

一、Linux 内核自带 LED 驱动使能

  在上一节中编写过基于设备树的 platform LED 灯驱动,但其实在 Linux 内核中已经自带了相关的 LED 驱动。不过要使用的话需要先配置 Linux 内核。在内核源码目录下使用make menuconfig命令打开 Linux 配置菜单,在Device Drivers --> LED Support(NEW_LEDS [=y]) --> LED Support for GPIO connected LEDs路径下,将该选项使能并编译进内核。按下"?"按键并输入"LED_GPIO"则可以查看相关的帮助。
在这里插入图片描述
在这里插入图片描述
重新编译内核,将编译出来的 uImage 镜像用于启动开发板。

二、驱动简介

1.LED灯驱动框架分析

  LED 灯驱动文件为/drivers/leds/leds-gpio.c,在/drivers/leds/Makefile 这个文件中包含了相关驱动的编译信息:

	# SPDX-License-Identifier: GPL-2.0
	
	# LED Core
	obj-$(CONFIG_NEW_LEDS) += led-core.o
......
	obj-$(CONFIG_LEDS_PCA9532) += leds-pca9532.o
	obj-$(CONFIG_LEDS_GPIO_REGISTER) += leds-gpio-register.o
	obj-$(CONFIG_LEDS_GPIO) += leds-gpio.o
	obj-$(CONFIG_LEDS_LP3944) += leds-lp3944.o
......

如果定义了 CONFIG_LEDS_GPIO 就会编译 leds-gpio.c 这个文件,在.config 文件中就会有“CONFIG_LEDS_GPIO=y”这一行,leds-gpio.c 驱动文件也会被编译。
   在leds-gpio.c 这个驱动文件中:

static const struct of_device_id of_gpio_leds_match[] = {
	{ .compatible = "gpio-leds", },
	{},
};
......
static struct platform_driver gpio_led_driver = {
	.probe = gpio_led_probe,
	.shutdown = gpio_led_shutdown,
	.driver = {
		.name = "leds-gpio",
		.of_match_table = of_gpio_leds_match,
	},
};

module_platform_driver(gpio_led_driver);

of_gpio_leds_match 为 LED 驱动的匹配表,此表只有一个匹配项, compatible 内容为“gpio-leds”,因此设备树中的 LED 灯设备节点的 compatible 属性值也要为“gpio-leds”,否则设备和驱动匹配不成功,驱动就没法工作。
platform_driver 驱动结构体变量为 Linux 内核自带的 LED 驱动,采用了 platform 框架。probe 函数为 gpio_led_probe,因此当驱动和设备匹配成功后 gpio_led_probe 函数会执行。驱动名字为“leds-gpio”,因此匹配成功后会在 /sys/bus/platform/drivers 目录下存在一个名为“leds-gpio”的文件。
module_platform_driver 函数向 Linux 内核注册 gpio_led_driver 这个 platform 驱动。

2.module_platform_driver 函数简析

  LED 驱动会采用 module_platform_driver 函数向 Linux 内核注册 platform 驱动,其实在 Linux 内核中会大量采用 module_platform_driver 来完成向 Linux 内核注册 platform 驱动的操作。module_platform_driver 定义在 include/linux/platform_device.h 文件中,为一个宏:

#define module_platform_driver(__platform_driver) \
		module_driver(__platform_driver, platform_driver_register, platform_driver_unregister)

module_platform_driver 依赖 module_driver,module_driver 也是一个宏,定义在 include/linux/device.h 文件中:

#define module_driver(__driver, __register, __unregister, ...) \
static int __init __driver##_init(void) \
{ \
	return __register(&(__driver) , ##__VA_ARGS__); \
} \
module_init(__driver##_init); \
static void __exit __driver##_exit(void) \
{ \
 	__unregister(&(__driver) , ##__VA_ARGS__); \
} \
module_exit(__driver##_exit);

所以,将module_platform_driver(gpio_led_driver)函数展开,就等同于以下代码:

static int __init gpio_led_driver_init(void)
{
	return platform_driver_register (&(gpio_led_driver));
}
module_init(gpio_led_driver_init);
static void __exit gpio_led_driver_exit(void)
{
	platform_driver_unregister (&(gpio_led_driver) );
}
module_exit(gpio_led_driver_exit);

因此 module_platform_driver 函数的功能就是完成 platform 驱动的注册和删除。

3.gpio_led_probe 函数简析

  当驱动和设备匹配以后 gpio_led_probe 函数就会执行,此函数主要是从设备树中获取 LED 灯的 GPIO 信息:

static int gpio_led_probe(struct platform_device *pdev)
{
	struct gpio_led_platform_data *pdata = dev_get_platdata(&pdev->dev);
	struct gpio_leds_priv *priv;
	int i, ret = 0;
	
	if (pdata && pdata->num_leds) { 	/* 非设备树方式 */
		/* 获取 platform_device 信息 */
		.....
	} else { 							/* 采用设备树 */
		priv = gpio_leds_create(pdev);
		if (IS_ERR(priv))
		return PTR_ERR(priv);
	}
	
	platform_set_drvdata(pdev, priv);
	return 0;
}

其中有个判断设备树的函数段,如果使用设备树的话,使用 gpio_leds_create 函数从设备树中提取设备信息,获取到的 LED 灯 GPIO 信息保存在返回值中,gpio_leds_create 函数内容如下:

static struct gpio_leds_priv *gpio_leds_create(struct platform_device *pdev)
{
	struct device *dev = &pdev->dev;
	struct fwnode_handle *child;
	struct gpio_leds_priv *priv;
	int count, ret;

	count = device_get_child_node_count(dev);
	if (!count)
		return ERR_PTR(-ENODEV);

	priv = devm_kzalloc(dev, sizeof_gpio_leds_priv(count), GFP_KERNEL);
	if (!priv)
		return ERR_PTR(-ENOMEM);

	device_for_each_child_node(dev, child) {
		struct gpio_led_data *led_dat = &priv->leds[priv->num_leds];
		struct gpio_led led = {};
		const char *state = NULL;

		/*
		* Acquire gpiod from DT with uninitialized label, which
		* will be updated after LED class device is registered,
		* Only then the final LED name is known.
		*/
		led.gpiod = devm_fwnode_get_gpiod_from_child(dev, NULL, child, GPIOD_ASIS, NULL);
		if (IS_ERR(led.gpiod)) {
			fwnode_handle_put(child);
			return ERR_CAST(led.gpiod);
		}
		led_dat->gpiod = led.gpiod;

		fwnode_property_read_string(child, "linux,default-trigger", &led.default_trigger);

		if (!fwnode_property_read_string(child, "default-state", &state)) {
			if (!strcmp(state, "keep"))
				led.default_state = LEDS_GPIO_DEFSTATE_KEEP;
			else if (!strcmp(state, "on"))
				led.default_state = LEDS_GPIO_DEFSTATE_ON;
			else
				led.default_state = LEDS_GPIO_DEFSTATE_OFF;
		}

		if (fwnode_property_present(child, "retain-state-suspended"))
			led.retain_state_suspended = 1;
		if (fwnode_property_present(child, "retain-state-shutdown"))
			led.retain_state_shutdown = 1;
		if (fwnode_property_present(child, "panic-indicator"))
			led.panic_indicator = 1;

		ret = create_gpio_led(&led, led_dat, dev, child, NULL);
		if (ret < 0) {
			fwnode_handle_put(child);
			return ERR_PTR(ret);
		}
		/* Set gpiod label to match the corresponding LED name. */
		gpiod_set_consumer_name(led_dat->gpiod, led_dat->cdev.dev->kobj.name);
		priv->num_leds++;
	}

	return priv;
}

①调用 device_get_child_node_count 函数统计子节点数量,一般在设备树中创建一个节点表示 LED 灯,然后在这个节点下面为每个 LED 灯创建一个子节点。因此子节点数量也是 LED 灯的数量
②遍历每个子节点,获取每个子节点的信息
③获取 LED 灯所使用的 GPIO 信息
④使用 fwnode_property_read_string 函数获取“linux,default-trigger”属性值,可以通过此属性设置某个 LED 灯在Linux 系统中的默认功能,比如作为系统心跳指示灯等等
⑤获取“default-state”属性值,也就是 LED 灯的默认状态属性
⑥调用 create_gpio_led 函数创建 LED 相关的 io,其实就是设置 LED 所使用的 io 为输出之类的。create_gpio_led 函数主要是初始化 led_dat 这个 gpio_led_data 结构体类型变量,led_dat 保存了 LED 的操作函数等内容
⑦使用 label 属性作为 LED 的名字,led_dat->cdev.dev->kobj.name 指向设备树里的 LED 灯节点下的 label 属性
总结:gpio_led_probe 函数主要功能就是获取 LED 灯的设备信息,然后根据这些信息来初始化对应的 IO,设置为输出

三、设备树节点编写

  在 Documentation/devicetree/bindings/leds/leds-gpio.txt 文档中,讲解了Linux 自带驱动对应的设备树节点该如何编写。主要有以下五点:
①创建一个节点表示 LED 灯设备,比如 dtsleds,如果板子上有多个 LED 灯的话每个 LED 灯都作为 dtsleds 的子节点
②dtsleds 节点的 compatible 属性值一定要为“gpio-leds”
③设置 label 属性,此属性为可选,每个子节点都有一个 label 属性,label 属性一般表示LED 灯的名字,比如以颜色区分的话就是 red、 green 等
④每个子节点必须要设置 gpios 属性值,表示此 LED 所使用的 GPIO 引脚
⑤可以设置“ linux,default-trigger”属性值,也就是设置 LED 灯的默认功能,查阅 Documentation/devicetree/bindings/leds/common.txt 这个文档来查看可选功能:

属性值功能
backlightLED 灯作为背光
default-onLED 灯打开
heartbeatLED 灯作为心跳指示灯,可以作为系统运行提示灯
disk-activityLED 灯作为磁盘活动指示灯
ide-diskLED 灯作为硬盘活动指示灯
timerLED 灯周期性闪烁,由定时器驱动,闪烁频率可以修改

⑥可以设置“default-state”属性值,可以设置为 on、off 或 keep,为 on 时 LED 灯默认打开,为 off 时 LED 灯默认关闭,为 keep 时 LED 灯保持当前模式
⑦另外还有一些其他的可选属性,比如 led-sources、color、function 等属性,可以查阅相关文档自行设置

在正点原子的STM32MP157开发板上,两个 LED 分别连接在 PI0 和 PF3 引脚上,所以首先修改设备树文件stm32mp15-pinctrl.dtsi,在之前创建的 led_pins_a 节点下添加一个 LED 子节点:

&pinctrl {
	led_pins_a: gpioled-0 {
		pins {
			pinmux = <STM32_PINMUX('I', 0, GPIO)>,	/*LED0*/
				 <STM32_PINMUX('F', 3, GPIO)>;	/*LED1*/
			driver-push-pull;
			bias-pull-up;
			output-high;
			slew-rate = <0>;
		};
	};
......
};

然后在设备树文件 stm32mp157d-atk.dts 中按照上文的文档要求,在根节点/下添加设备子节点:

dtsleds {
		compatible = "gpio-leds";
		pinctrl-0 = <&led_pins_a>;
		
		led0 {
			label = "red";
			gpios = <&gpioi 0 GPIO_ACTIVE_LOW>;
			default-state = "off";
		};
		
		led1 {
			label = "green";
			gpios = <&gpiof 3 GPIO_ACTIVE_LOW>;
			default-state = "off";
		};
};

①设置 LED 的 pinctrl 节点为 led_pins_a,compatible 值为"gpio-leds",内核的LED驱动属性匹配值
②设置 led0 和 led1 的名称、引脚和默认状态
修改完成后,使用make dtbs命令编译出设备树文件,用于启动开发板。

四、运行测试

  使用编译出的新 uImage 和设备树 .dtb 文件启动开发板,查看 /sys/bus/platform/devices/dtsleds 这个目录是否存在:
在这里插入图片描述
在该目录下存在一个 leds 目录,里面就有新创建的两个 led 设备:
在这里插入图片描述
再对两个 LED 设备进行测试,在 sys/class/leds 目录下,存在 red/brightness 和 green/brightness 两个文件,通过操作这两个文件即可控制两个 LED 的亮灭:
在这里插入图片描述

echo 1 > /sys/class/leds/red/brightness //打开 LED0
echo 1 > /sys/class/leds/green/brightness //打开 LED1
echo 0 > /sys/class/leds/red/brightness //关闭 LED0
echo 0 > /sys/class/leds/green/brightness //关闭 LED1

如果能正常的打开和关闭两个 LED 灯就说明 Linux 内核自带的 LED 灯驱动工作正常。一般会将一个 LED 灯作为系统指示灯,系统运行正常的话这个 LED 指示灯就会闪烁。这里设置 LED0 作为系统指示灯,在 dtsleds/led0 这个设备节点中加入“linux,defaulttrigger”属性信息即可,属性值为“heartbeat”,修改完以后的 dtsleds 节点内容如下:

dtsleds {
	compatible = "gpio-leds";
	pinctrl-0 = <&led_pins_a>;

	led0 {
		label = "red";
		gpios = <&gpioi 0 GPIO_ACTIVE_LOW>;
		linux,default-trigger = "heartbeat";
		default-state = "on";
	};

	led1 {
		label = "green";
		gpios = <&gpiof 3 GPIO_ACTIVE_LOW>;
		default-state = "off";
	};
};

设置 LED0 为 heartbeat,默认打开 LED0。重新编译设备树并且使用新的设备树启动 Linux 系统,启动以后 LED0 就会闪烁,作为系统心跳指示灯,表示系统正在运行

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值