lwn拾遗:一个sn3218 led driver引发的驱动范例
前言:
注意到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", ®); + 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({});- 一个函数引发的MySQL驱动问题
- 安卓项目中使用JSON引发的一个小错误
- Spring之LoadTimeWeaver——一个需求引发的思考---转
- 张浩:一个小鸟引发的血案——社会化分享的价值
- 一个嵌入式初学者引发的思考(jesse谈自己的经验体会) 【转】
- 一个事故引发的思考
- 一个参数所引发的悲剧
- 原创:一个由计算平均采购单价引发的学案
- 一个空格引发的血案~
- wince6.0 下的流驱动开发范例
- 求Sn=a+aa+aaa+aaaa+aaaaa的前5项之和,其中a是一个数字,例如:2+22+222+2222+22222
- 转:一个Sqrt函数引发的血案
- 如何搭建一个Linux驱动编写环境(centos)
- 一个“Spring轮子”引发的“血案”(2)
- 由一个简单的客户端间TCP/UDP通信程序引发的关于设计模式的思考
- 由一个问题引发的思考——关于数据库的外键约束
- 一个detect问题引发的一系列思考
- leakcanary作者发现的一个Dialog的各种listener容易引发的内存泄露问题
- Vert.x,一个异步、可伸缩、并发应用框架引发的思考
- 一个“Spring轮子”引发的“血案”(2)