您的位置:首页 > 运维架构 > Linux

lwn拾遗:一个sn3218 led driver引发的驱动范例

2016-02-17 20:07 2236 查看

前言:

注意到lwn上,device drviers中发表了一个led chev 的patch:“leds: add SN3218 LED driver”(http://lwn.net/Articles/673991/)

根据这个我们可以学习一下该patch包含的led chev思想。

patch-0

Si-En SN3218是一款18路呼吸灯驱动芯片,每路单独256级细腻亮度可控,采用i2c接口。sn3218并不支持read操作,所以寄存器的值都会被cached。

其内部寄存器如下:

具体每个寄存器含义可以查看相关文档(sn3218 datasheet.pdf)。

 http://www.si-en.com/uploadpdf/s2011517171720.pdf

patch-1

/Documentation/devicetree/bindings/vendor-prefixes.txt

中添加vid的描述

+si-en	Si-En Technology Ltd.


patch-2

添加dts对sn3218的描述,主要是修改/Documentation/devicetree/bindings/leds/leds-sn3218.txt的文件。该bindings文件描述了dts该如何支持sn3218.

+* Si-En Technology - SN3218 18-Channel LED Driver
+
+Required properties:
+- compatible :
+	"si-en,sn3218"
+- address-cells : must be 1
+- size-cells : must be 0
+- reg : I2C slave address, typically 0x54
+
+There must be at least 1 LED which is represented as a sub-node
+of the sn3218 device.
+
+LED sub-node properties:
+- label : (optional) see Documentation/devicetree/bindings/leds/common.txt
+- reg : number of LED line (could be from 0 to 17)
+- linux,default-trigger : (optional)
+   see Documentation/devicetree/bindings/leds/common.txt
+
+Example:
+
+sn3218: led-controller@54 {           //    i2c的slave偏移地址是0x54,典型值
+	compatible = "si-en,sn3218";
+	#address-cells = <1>;          //   它的address是1个字节,size为0
+	#size-cells = <0>;
+	reg = <0x54>;
+
+	led@0 {
+		label = "led1";        //   label描述名字
+		reg = <0x0>;           //   reg描述了该led的第几个
+	};
+
+	led@1 {
+		label = "led2";
+		reg = <0x1>;
+	};
+
+	led@2 {
+		label = "led3";
+		reg = <0x2>;
+	};
+};
+


patch-3 实际led chev的内容,kconfig,makefile以及code

1,kconfig中添加描述

/drivers/leds/Kconfig
+config LEDS_SN3218
+	tristate "LED support for Si-En SN3218 I2C chip"
+	depends on LEDS_CLASS && I2C
+	depends on OF
+	select REGMAP_I2C
+	help
+	  This option enables support for the Si-EN SN3218 LED driver
+	  connected through I2C. Say Y to enable support for the SN3218 LED.
+
+	  This driver can also be built as a module. If so the module
+	  will be called leds-sn3218.
+

2,makefile中添加kconfig的选项

drivers/leds/Makefile b/drivers/leds/Makefile
+obj-$(CONFIG_LEDS_SN3218)		+= leds-sn3218.o

3,添加c code

/drivers/leds/leds-sn3218.c

3-1,一些基本的头文件

当我们要写i2c的led时需要加上的头文件

+#include <linux/err.h>
+#include <linux/i2c.h>
+#include <linux/leds.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/regmap.h>
+#include <linux/slab.h>

3-2,描述sn3218的寄存器分布的宏

+#define SN3218_MODE		0x00   //  设置shutdown模式
+#define SN3218_PWM_1		0x01   //  1~18 leds的pwm控制
+#define SN3218_PWM_2		0x02
+#define SN3218_PWM_3		0x03
+#define SN3218_PWM_4		0x04
+#define SN3218_PWM_5		0x05
+#define SN3218_PWM_6		0x06
+#define SN3218_PWM_7		0x07
+#define SN3218_PWM_8		0x08
+#define SN3218_PWM_9		0x09
+#define SN3218_PWM_10		0x0a
+#define SN3218_PWM_11		0x0b
+#define SN3218_PWM_12		0x0c
+#define SN3218_PWM_13		0x0d
+#define SN3218_PWM_14		0x0e
+#define SN3218_PWM_15		0x0f
+#define SN3218_PWM_16		0x10
+#define SN3218_PWM_17		0x11
+#define SN3218_PWM_18		0x12
+#define SN3218_LED_1_6		0x13    //   led1~6的使能控制寄存器
+#define SN3218_LED_7_12		0x14
+#define SN3218_LED_13_18	0x15
+#define SN3218_UPDATE		0x16	/* applies to reg 0x01 .. 0x15 */  //  更新响应pwm寄存器和控制寄存器的值
+#define SN3218_RESET		0x17   //   复位寄存器,让所有寄存器的值恢复到default
+
+#define SN3218_LED_MASK		0x3f
+#define SN3218_LED_ON		0x01
+#define SN3218_LED_OFF		0x00

3-3,关键数据结构

+struct sn3218_led;
+
+/**
+ * struct sn3218 -
+ * @client - Pointer to the I2C client
+ * @leds - Pointer to the individual LEDs
+ * @num_leds - Actual number of LEDs
+**/
+struct sn3218 {
+	struct i2c_client *client;    //  i2c client结构体指针
+	struct regmap *regmap;        //  采用regmap协议
+	struct sn3218_led *leds;
+	int num_leds;                 //  led个数
+};
+
+/**
+ * struct sn3218_led -
+ * @chip - Pointer to the container
+ * @led_cdev - led class device pointer
+ * @led_num - LED index ( 0 .. 17 )
+**/
+struct sn3218_led {
+	struct sn3218 *chip;
+	struct led_classdev led_cdev;      //   led chev结构体
+	int led_num;
+};

3-4,驱动的思路

首先要有匹配入口点

+static const struct of_device_id of_sn3218_match[] = {
+	{ .compatible = "si-en,sn3218", },                        //   和dts的node的compatible属性匹配字符串
+	{},
+};
+MODULE_DEVICE_TABLE(of, of_sn3218_match);
+
+static struct i2c_driver sn3218_driver = {
+	.driver = {
+		.name	= "leds-sn3218",
+		.of_match_table = of_match_ptr(of_sn3218_match),   //  和dts中匹配时,调用probe函数
+	},
+	.probe	= sn3218_probe,
+	.remove	= sn3218_remove,
+	.id_table = sn3218_id,
+};
+
+module_i2c_driver(sn3218_driver);                                  //  属于i2c驱动的一部分,i2cdevice
+
+MODULE_DESCRIPTION("Si-En SN3218 LED Driver");
+MODULE_AUTHOR("Stefan Wahren <stefan.wahren@xxxxxxxx>");
+MODULE_LICENSE("GPL v2");

of_match_table的机制是在dts引入之后,之前的是根据name来匹配的。其实原理是一样的,在MACHINE_INIT中会根据dts中node来建立device。这里的device只有几个很简单的结构,包括名字等等。等kernel初始化快结束前,调用__initcall,会对module_init修饰的所有函数调用到。在此期间,bus中会匹配,调用of_match用platform_driver结构体的of_match_table和device的node的compatile字段匹配。如果匹配了就调用platform_driver的probe函数。具体的流程可以以后系统列一下。

假如此时匹配,调用probe函数

+static int sn3218_probe(struct i2c_client *client,
+			const struct i2c_device_id *id)
+{
+	struct sn3218 *sn3218;
+	struct sn3218_led *leds;
+	struct device *dev = &client->dev;   //   几个必要的函数指针
+	int i, ret;
+
+	sn3218 = devm_kzalloc(dev, sizeof(*sn3218), GFP_KERNEL);   //   分配空间
+	if (!sn3218)
+		return -ENOMEM;
+
+	ret = sn3218_init(client, sn3218);                        //  初始化
+	if (ret)
+		return ret;
+
+	i2c_set_clientdata(client, sn3218);
+	leds = sn3218->leds;
+
+	/*
+	 * Since the chip is write-only we need to reset him into
+	 * a defined state (all LEDs off).
+	 */
+	ret = regmap_write(sn3218->regmap, SN3218_RESET, 0xff);
+	if (ret)
+		return ret;
+
+	for (i = 0; i < sn3218->num_leds; i++) {
+		ret = devm_led_classdev_register(dev, &leds[i].led_cdev);
+		if (ret < 0)
+			return ret;
+	}
+
+	/* Set normal mode */
+	return regmap_write(sn3218->regmap, SN3218_MODE, 0x01);
+}
+

1,需要devm_kzalloc给sn3218结构体分配空间,这里的kevm_kzallloc和传统的kzalloc的区别是,以前用kzalloc,驱动工程师必须要记得kfree,然而一单驱动结构复杂,很容易就会忘记kfree,造成很繁琐的bug。现在用kevm_kzalloc后,就像c++的类一样,当这个device给release时,它会自动把其用kevm_kzalloc分配的空间给release掉。

2,调用sn3218_init来给sn3218结构体初始化。

3,调用i2c_set_clientdata,把sn3218结构体和i2c挂钩。

4,调用regmap_write向SN3218_RESET描述的reset寄存器写入0xff,含义是把所有寄存器都复位。因为sn3218是只写寄存器,所以最开始不能读,需要把他们设定成一个已知状态。

5,对于18个led,轮询调用devm_led_classdev_register给每个led描述的led chevshe

6,调用regmap_write向SN3218_MODE描述的normal寄存器写入1,让其进入normal模式。

normal寄存器的含义如下:

我们知道probe函数中会调用sn3218_init,该函数会对sn3218寄存器进行初始配置。

+static int sn3218_init(struct i2c_client *client, struct sn3218 *sn3218)
+{
+	struct device_node *np = client->dev.of_node, *child;
+	struct sn3218_led *leds;
+	int ret, count;
+
+	count = of_get_child_count(np);
+	if (!count)
+		return -ENODEV;
+
+	if (count > NUM_LEDS) {
+		dev_err(&client->dev, "Invalid LED count %d\n", count);
+		return -EINVAL;
+	}
+
+	leds = devm_kzalloc(&client->dev, count * sizeof(*leds), GFP_KERNEL);
+	if (!leds)
+		return -ENOMEM;
+
+	sn3218->leds = leds;
+	sn3218->num_leds = count;
+	sn3218->client = client;
+
+	sn3218->regmap = devm_regmap_init_i2c(client, &sn3218_regmap_config);
+	if (IS_ERR(sn3218->regmap)) {
+		ret = PTR_ERR(sn3218->regmap);
+		dev_err(&client->dev, "Failed to allocate register map: %d\n",
+			ret);
+		return ret;
+	}
+
+	for_each_child_of_node(np, child) {
+		u32 reg;
+
+		ret = of_property_read_u32(child, "reg", &reg);
+		if (ret)
+			goto fail;
+
+		if (reg >= count) {
+			dev_err(&client->dev, "Invalid LED (%u >= %d)\n", reg,
+				count);
+			ret = -EINVAL;
+			goto fail;
+		}
+
+		sn3218_led_init(sn3218, child, reg);
+	}
+
+	return 0;
+
+fail:
+	of_node_put(np);
+	return ret;
+}

1,调用of_get_child_count计算出dtb中的led的node个数。

2,调用devm_kzalloc分配count个sn3218_led 结构体。每个led对应一个该结构体。然后完善sn3218结构体。

3,调用devm_regmap_init_i2c ,传入sn3218_regmap_config 描述的regmap配置。得到一个描述i2c的regmap。

4,对每一个led都调用sn3218_led_init初始化sn3218结构体。

其中的regmap config描述了sn3218的regmap的特点。

+static const struct regmap_config sn3218_regmap_config = {
+	.reg_bits = 8,
+	.val_bits = 8,
+
+	.max_register = SN3218_RESET,
+	.reg_defaults = sn3218_reg_defs,
+	.num_reg_defaults = ARRAY_SIZE(sn3218_reg_defs),
+	.cache_type = REGCACHE_RBTREE,     //   红黑树作为cache的方法,特点查找特别快。
+};


sn3218_init 中调用的sn3218_led_init对每一个led chev设备进行初始化。

void sn3218_led_init(struct sn3218 *sn3218, struct device_node *node, u32 reg)
+{
+	struct sn3218_led *leds = sn3218->leds;
+	struct led_classdev *cdev = &leds[reg].led_cdev;
+
+	leds[reg].led_num = reg;
+	leds[reg].chip = sn3218;
+
+	if (of_property_read_string(node, "label", &cdev->name))
+		cdev->name = node->name;
+
+	of_property_read_string(node, "linux,default-trigger",
+				&cdev->default_trigger);
+
+	cdev->brightness_set_blocking = sn3218_led_set;
+	cdev->max_brightness = LED_FULL;

这里的reg描述的是leds数组的index。leds描述的是sn3218_led结构体,之前分配了count个该结构体。这里用reg来找到第reg个led。接着配置leds[reg]的各项参数。

cdev->brightness_set_blocking = sn3218_led_set;

该语句设定了,当控制该reg亮度时的函数点。

接下来分析空reg亮度的函数。

+static int sn3218_led_set(struct led_classdev *led_cdev,
+			  enum led_brightness brightness)
+{
+	struct sn3218_led *led =
+			container_of(led_cdev, struct sn3218_led, led_cdev);
+	struct regmap *regmap = led->chip->regmap;
+	u8 bank = led->led_num / 6;
+	u8 mask = 0x1 << (led->led_num % 6);
+	u8 val;
+	int ret;
+
+	if (brightness == LED_OFF)
+		val = 0;
+	else
+		val = mask;
+
+	ret = regmap_update_bits(regmap, SN3218_LED_1_6 + bank, mask, val);
+	if (ret < 0)
+		return ret;
+
+	if (brightness > LED_OFF) {
+		ret = regmap_write(regmap, SN3218_PWM_1 + led->led_num,
+				   brightness);
+		if (ret < 0)
+			return ret;
+	}
+
+	ret = regmap_write(regmap, SN3218_UPDATE, 0xff);
+
+	return ret;
+}
+

1,拿到必要的数据结构。regmap等。

+	u8 bank = led->led_num / 6;
+	u8 mask = 0x1 << (led->led_num % 6);

这里要/6和%6在于0x13~0x15描述的led control寄存器,每个寄存器控制6个led。

2,亮度控制有两方面,一个是亮灭,一个是pwm的可编程亮度。

首先,先更新一个亮灭:

ret = regmap_update_bits(regmap, SN3218_LED_1_6 + bank, mask, val);

如果有pwm控制的话,更新pwm亮度:

if (brightness > LED_OFF) {
+		ret = regmap_write(regmap, SN3218_PWM_1 + led->led_num,
+				   brightness);
+		if (ret < 0)
+			return ret;
+	}

最后,对所有寄存器更新一下。

ret = regmap_write(regmap, SN3218_UPDATE, 0xff);

至此,sn3218 led chev驱动基本思路分析完了。里边会用到dts、regmap和i2c的一些api函数,下一节分析一下这些api函数。核心就是如何利用regmap来最后实现i2c的对sn3218的写操作。当然read操作是cache的。

(adsbygoogle = window.adsbygoogle || []).push({});
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息