嵌入式Linux设备驱动程序开发指南11(I2C从端驱动)——读书笔记

十一、I2C驱动

11.1 概述

I2C是飞利浦开发的一种协议,使用SMBus命令。
struct i2c_msg //定义了I2C每条消息的从端地址、消息传送字节数和数据本身。
在Documentation/i2c/smbus-protocol查看SMBus函数详细。
Linux I2C子系统基于Linux设备模型,组成部分是:

I2C总线核心即,drivers/i2c/ic-core.c
I2C控制器驱动即,drivers/i2c/busses/i2c-imx.c

probe()函数探测每个I2C控制器初始化适配器,并通过i2_add_numbered_adapter()函数——位于drivers/i2c/i2c-core.c
of_platform_populate()函数将I2C控制器设备注册到平台总线核心。
函数调用流程:

I2C从端驱动 -> I2C总线核心驱动 -> I2C控制器驱动。

请添加图片描述

11.2 I2C从端驱动

I2C从端驱动,它们用于控制I/O扩展器、DAC、加速度计和多显LED控制器。

I2C设备驱动实例化到I2C总线核心。

注册/销毁驱动:

i2c_add_driver()		//注册驱动
i2c_del_driver()		//销毁驱动

在设备树中申明I2C设备:
通过of_platform_populate()注册到I2C总线核心。

			i2c1: i2c@fc028000 {
				dmas = <0>, <0>;
				pinctrl-names = "default";
				pinctrl-0 = <&pinctrl_i2c1_default>;
				status = "okay";

				at24@54 {
					compatible = "atmel,24c02";
					reg = <0x54>;
					pagesize = <16>;
				};
		
				ltc3206: ltc3206@1b {
					compatible = "arrow,ltc3206";
					reg = <0x1b>;
					pinctrl-0 = <&pinctrl_cs_default>;
					gpios = <&pioA 57 GPIO_ACTIVE_LOW>;
					
					led1r {
						label = "red";
					};

					led1b {
						label = "blue";
					};
					
					led1g {
						label = "green";
					};
					
					ledmain {
						label = "main";
					};
					
					ledsub {
						label = "sub";
					};
				};	

11.3 I2C I/O扩展设备

一个驱动控制I2C设备,驱动将管理连接到I2C总线的几个PCF8574 I/O扩展设备。The PCF8574 IO Expansion Board is used as a remote 8-bit I/O expander for I2C-bus. Up to 8 PCF8574, IO Expansion Board can be connected to the I2C-bus, providing up to 64 I/O ports.
The PCF8574 IO Expansion Board features an I2C pinheader on one side and an I2C connector on the opposite side. Hence, it’s more flexible to connect the board to your development system. The board also supports I2C cascading, allowing the use of multi modules connected to the I2C bus at the same time by connecting the pinheader and connector.
参考连接:
https://www.waveshare.com/wiki/PCF8574_IO_Expansion_Board

io_sam_expander.c驱动代码:

#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/i2c.h>
#include <linux/fs.h>
#include <linux/of.h>
#include <linux/uaccess.h>

/* This structure will represent single device */
struct ioexp_dev {
	struct i2c_client * client;
	struct miscdevice ioexp_miscdevice;
	char name[8]; /* ioexpXX */
};

/* User is reading data from /dev/ioexpXX */
static ssize_t ioexp_read_file(struct file *file, char __user *userbuf,
                               size_t count, loff_t *ppos)
{
	int expval, size;
	char buf[3];
	struct ioexp_dev * ioexp;

	ioexp = container_of(file->private_data,
			     struct ioexp_dev, 
			     ioexp_miscdevice);

	/* read IO expander input to expval */
	expval = i2c_smbus_read_byte(ioexp->client);
	if (expval < 0)
		return -EFAULT;

	/* 
     * converts expval in 2 characters (2bytes) + null value (1byte)
	 * The values converted are char values (FF) that match with the hex
	 * int(s32) value of the expval variable.
	 * if we want to get the int value again, we have to
	 * do Kstrtoul(). We convert 1 byte int value to
	 * 2 bytes char values. For instance 255 (1 int byte) = FF (2 char bytes).
	 */
	size = sprintf(buf, "%02x", expval);

	/* 
     * replace NULL by \n. It is not needed to have the char array
	 * ended with \0 character.
	 */
	buf[size] = '\n';

	/* send size+1 to include the \n character */
	if(*ppos == 0){
		if(copy_to_user(userbuf, buf, size+1)){
			pr_info("Failed to return led_value to user space\n");
			return -EFAULT;
		}
		*ppos+=1;
		return size+1;
	}

	return 0;
}

/* Writing from the terminal command line, \n is added */
static ssize_t ioexp_write_file(struct file *file, const char __user *userbuf,
                                   size_t count, loff_t *ppos)
{
	int ret;
	unsigned long val;
	char buf[4];
	struct ioexp_dev * ioexp;

	ioexp = container_of(file->private_data,
			     struct ioexp_dev, 
			     ioexp_miscdevice);

	dev_info(&ioexp->client->dev, 
		 "ioexp_write_file entered on %s\n", ioexp->name);

	dev_info(&ioexp->client->dev,
		 "we have written %zu characters\n", count); 

	if(copy_from_user(buf, userbuf, count)) {
		dev_err(&ioexp->client->dev, "Bad copied value\n");
		return -EFAULT;
	}

	buf[count-1] = '\0';

	/* convert the string to an unsigned long */
	ret = kstrtoul(buf, 0, &val);
	if (ret)
		return -EINVAL;

	dev_info(&ioexp->client->dev, "the value is %lu\n", val);

	ret = i2c_smbus_write_byte(ioexp->client, val);
	if (ret < 0)
		dev_err(&ioexp->client->dev, "the device is not found\n");

	dev_info(&ioexp->client->dev, 
		 "ioexp_write_file exited on %s\n", ioexp->name);

	return count;
}

static const struct file_operations ioexp_fops = {
	.owner = THIS_MODULE,
	.read = ioexp_read_file,
	.write = ioexp_write_file,
};

static int ioexp_probe(struct i2c_client * client,
		const struct i2c_device_id * id)
{
	static int counter = 0;

	struct ioexp_dev * ioexp;

	/* Allocate new structure representing device */
	ioexp = devm_kzalloc(&client->dev, sizeof(struct ioexp_dev), GFP_KERNEL);

	/* Store pointer to the device-structure in bus device context */
	i2c_set_clientdata(client,ioexp);

	/* Store pointer to I2C device/client */
	ioexp->client = client;

	/* Initialize the misc device, ioexp incremented after each probe call */
	sprintf(ioexp->name, "ioexp%02d", counter++); 
	dev_info(&client->dev, 
		 "ioexp_probe is entered on %s\n", ioexp->name);

	ioexp->ioexp_miscdevice.name = ioexp->name;
	ioexp->ioexp_miscdevice.minor = MISC_DYNAMIC_MINOR;
	ioexp->ioexp_miscdevice.fops = &ioexp_fops;

	/* Register misc device */
	return misc_register(&ioexp->ioexp_miscdevice);

	dev_info(&client->dev, 
		 "ioexp_probe is exited on %s\n", ioexp->name);

	return 0;
}

static int ioexp_remove(struct i2c_client * client)
{
	struct ioexp_dev * ioexp;

	/* Get device structure from bus device context */	
	ioexp = i2c_get_clientdata(client);

	dev_info(&client->dev, 
		 "ioexp_remove is entered on %s\n", ioexp->name);

	/* Deregister misc device */
	misc_deregister(&ioexp->ioexp_miscdevice);

	dev_info(&client->dev, 
		 "ioexp_remove is exited on %s\n", ioexp->name);

	return 0;
}

static const struct of_device_id ioexp_dt_ids[] = {
	{ .compatible = "arrow,ioexp", },
	{ }
};
MODULE_DEVICE_TABLE(of, ioexp_dt_ids);

static const struct i2c_device_id i2c_ids[] = {
	{ .name = "ioexp", },
	{ }
};
MODULE_DEVICE_TABLE(i2c, i2c_ids);

static struct i2c_driver ioexp_driver = {
	.driver = {
		.name = "ioexp",
		.owner = THIS_MODULE,
		.of_match_table = ioexp_dt_ids,
	},
	.probe = ioexp_probe,
	.remove = ioexp_remove,
	.id_table = i2c_ids,
};

module_i2c_driver(ioexp_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR(" ");
MODULE_DESCRIPTION("This is a driver that controls several i2c IO expanders");

测试调试:

insmod io_imx_empander.ko
ls -l /dev/ioexp*
echo 0 > /dev/ioexp00 //set all out to 0
echo 255 > /dev/ioexp01 //set all out to 1
rmmod io_imx_empander.ko

11.4 sysfs文件系统

sysfs是一个虚拟文件系统,sysfs将从内核设备模型中导出设备和驱动信息到用户态。
通过查看/sys/可看到系统的内核视图,

/sys/bus/ 包含总线列表
/sys/device 包含设备列表
/sys/bus/<bus>device/ 特定总线上的设备
/sys/bus/<bus>drivers/ 特定总线上的驱动
/sys/class/ 将/sys/device目录中的每个,都是该设备的符号链接
/sys/bus/<bus>/devices/<device>/driver/ 符号链接到管理设备的驱动

kobjeect基础结构:
sysfs本质是与kobject基础结构相关。kobject是设备模型的基本结构,

struct kobject obj1;
struct attribuate attr1;
struct device_attribuate dev_attr1;

static DEVICE_ATTR()


sysfs_create_file()
sysfs_remove_file()

device_create_file()
device_remove_file()

sysfs_create_group()
sysfs_remove_group()

11.5 I2C多显LED模块(ltc3206)

实现一个驱动LTC3206 I2C多显LED控制。
PD4、PD5焊点接I2C信号。

11.5.1 设备树

			pinctrl@fc038000 {
							/*
							 * There is no real pinmux for ADC, if the pin
							 * is not requested by another peripheral then
							 * the muxing is done when channel is enabled.
							 * Requesting pins for ADC is GPIO is
							 * encouraged to prevent conflicts and to
							 * disable bias in order to be in the same
							 * state when the pin is not muxed to the adc.
							 */
							pinctrl_i2c1_default: i2c1_default {
								pinmux = <PIN_PD4__TWD1>,
										 <PIN_PD5__TWCK1>;
								bias-disable;
							};
							pinctrl_cs_default: cs_gpio_default {
								 pinmux = <PIN_PB25__GPIO>;
								bias-disable;
							};
			};

			i2c1: i2c@fc028000 {
				dmas = <0>, <0>;
				pinctrl-names = "default";
				pinctrl-0 = <&pinctrl_i2c1_default>;
				status = "okay";

				at24@54 {
					compatible = "atmel,24c02";
					reg = <0x54>;
					pagesize = <16>;
				};
		
				ltc3206: ltc3206@1b {
					compatible = "arrow,ltc3206";
					reg = <0x1b>;
					pinctrl-0 = <&pinctrl_cs_default>;
					gpios = <&pioA 57 GPIO_ACTIVE_LOW>;
					
					led1r {
						label = "red";
					};

					led1b {
						label = "blue";
					};
					
					led1g {
						label = "green";
					};
					
					ledmain {
						label = "main";
					};
					
					ledsub {
						label = "sub";
					};
				};	

11.5.2 模块代码分析

包含头文件:

#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/leds.h>
#include <linux/gpio/consumer.h>
#include <linux/delay.h>

定义设备命令掩码;

#define LED_NAME_LEN	32
#define CMD_RED_SHIFT	4
#define CMD_BLUE_SHIFT	4
#define CMD_GREEN_SHIFT	0
#define CMD_MAIN_SHIFT	4
#define CMD_SUB_SHIFT	0
#define EN_CS_SHIFT	(1 << 2)

定义私有数据结构:

/* set a led_device struct for each 5 led device */
struct led_device {
	u8 brightness;
	struct led_classdev cdev;
	struct led_priv *private;
};

/* 
 * store the global parameters shared for the 5 led devices
 * the parameters are updated after each led_control() call
 */
struct led_priv {
	u32 num_leds;
	u8 command[3];
	struct gpio_desc *display_cs;
	struct i2c_client *client;
};

驱动函数摘要:

device_get_child_node_count()		//函数获取LED数量。
devm_kzallow()						//分配全局私有数据结构
gpiod_get()							//获取GPIO电平值
gpiod_direction_out()				//
gpiod_set_value()
led_control()						//LED亮度控制
module_i2c_driver(ltc3206_driver);	//在I2C总线上注册驱动

11.5.3 ltc3206_sam_led_class.c代码

#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/leds.h>
#include <linux/gpio/consumer.h>
#include <linux/delay.h>

#define LED_NAME_LEN	32
#define CMD_RED_SHIFT	4
#define CMD_BLUE_SHIFT	4
#define CMD_GREEN_SHIFT	0
#define CMD_MAIN_SHIFT	4
#define CMD_SUB_SHIFT	0
#define EN_CS_SHIFT	(1 << 2)

/* set a led_device struct for each 5 led device */
struct led_device {
	u8 brightness;
	struct led_classdev cdev;
	struct led_priv *private;
};

/* 
 * store the global parameters shared for the 5 led devices
 * the parameters are updated after each led_control() call
 */
struct led_priv {
	u32 num_leds;
	u8 command[3];
	struct gpio_desc *display_cs;
	struct i2c_client *client;
};

/* function that writes to the I2C device */
static int ltc3206_led_write(struct i2c_client *client, u8 *command)
{
	int ret = i2c_master_send(client, command, 3);
	if (ret >= 0)
		return 0;
	return ret;
}

/* the sysfs functions */
static ssize_t sub_select(struct device *dev, struct device_attribute *attr,
			  const char *buf, size_t count)
{
	char *buffer;
	struct i2c_client *client;
	struct led_priv *private;

	buffer = buf;

	/* replace \n added from terminal with \0 */
	*(buffer+(count-1)) = '\0';

	client = to_i2c_client(dev);
	private = i2c_get_clientdata(client);

	private->command[0] |= EN_CS_SHIFT; /* set the 3d bit A2 */
	ltc3206_led_write(private->client, private->command);
	
	if(!strcmp(buffer, "on")) {
		gpiod_set_value(private->display_cs, 1); /* low */
		usleep_range(100, 200);
		gpiod_set_value(private->display_cs, 0); /* high */
	}
	else if (!strcmp(buffer, "off")) {
		gpiod_set_value(private->display_cs, 0); /* high */
		usleep_range(100, 200);
		gpiod_set_value(private->display_cs, 1); /* low */
	}
	else {
		dev_err(&client->dev, "Bad led value.\n");
		return -EINVAL;
	}

	return count;
}
static DEVICE_ATTR(sub, S_IWUSR, NULL, sub_select);

static ssize_t rgb_select(struct device *dev, struct device_attribute *attr,
			  const char *buf, size_t count)
{
	char *buffer;
	struct i2c_client *client = to_i2c_client(dev);
	struct led_priv *private = i2c_get_clientdata(client);
	buffer = buf;

	*(buffer+(count-1)) = '\0';

	private->command[0] &= ~(EN_CS_SHIFT); /* clear the 3d bit */

	ltc3206_led_write(private->client, private->command);

	if(!strcmp(buffer, "on")) {
		gpiod_set_value(private->display_cs, 1); /* low */
		usleep_range(100, 200);
		gpiod_set_value(private->display_cs, 0); /* high */
	}
	else if (!strcmp(buffer, "off")) {
		gpiod_set_value(private->display_cs, 0); /* high */
		usleep_range(100, 200);
		gpiod_set_value(private->display_cs, 1); /* low */
	}
	else {
		dev_err(&client->dev, "Bad led value.\n");
		return -EINVAL;
	}

	return count;
}
static DEVICE_ATTR(rgb, S_IWUSR, NULL, rgb_select);

static struct attribute *display_cs_attrs[] = {
	&dev_attr_rgb.attr,
	&dev_attr_sub.attr,
	NULL,
};

static struct attribute_group display_cs_group = {
	.name = "display_cs",
	.attrs = display_cs_attrs,
};

/* 
 * this is the function that is called for each led device
 * when writing the brightness file under each device
 * the global parameters are kept in the led_priv struct
 * that is pointed inside each led_device struct
 */
static int led_control(struct led_classdev *led_cdev,
		       enum led_brightness value)
{
	struct led_classdev *cdev;
	struct led_device *led;
	led = container_of(led_cdev, struct led_device, cdev);
	cdev = &led->cdev;
	led->brightness = value;

	dev_info(cdev->dev, "the subsystem is %s\n", cdev->name);

	if (value > 15 || value < 0)
		return -EINVAL;

	if (strcmp(cdev->name,"red") == 0) {
		led->private->command[0] &= 0x0F; /* clear the upper nibble */
		led->private->command[0] |= ((led->brightness << CMD_RED_SHIFT) & 0xF0);
	}
	else if (strcmp(cdev->name,"blue") == 0) {
		led->private->command[1] &= 0x0F; /* clear the upper nibble */
		led->private->command[1] |= ((led->brightness << CMD_BLUE_SHIFT) & 0xF0);
	}
	else if (strcmp(cdev->name,"green") == 0) {
		led->private->command[1] &= 0xF0; /* clear the lower nibble */
		led->private->command[1] |= ((led->brightness << CMD_GREEN_SHIFT) & 0x0F);
	}
	else if (strcmp(cdev->name,"main") == 0) {
		led->private->command[2] &= 0x0F; /* clear the upper nibble */
		led->private->command[2] |= ((led->brightness << CMD_MAIN_SHIFT) & 0xF0);
	}
	else if (strcmp(cdev->name,"sub") == 0) {
		led->private->command[2] &= 0xF0; //clear the lower nibble
		led->private->command[2] |= ((led->brightness << CMD_SUB_SHIFT) & 0x0F);
	}
	else
		dev_info(cdev->dev, "No display found\n");

	return ltc3206_led_write(led->private->client, led->private->command);
}

static int __init ltc3206_probe(struct i2c_client *client,
				const struct i2c_device_id *id)
{
	int count, ret;
	u8 value[3];
	struct fwnode_handle *child;
	struct device *dev = &client->dev;
	struct led_priv *private;

	dev_info(dev, "platform_probe enter\n");

	/* 
         * set blue led maximum value for i2c testing
	 * ENRGB must be set to VCC 
	 */
	value[0] = 0x00;
	value[1] = 0xF0;
	value[2] = 0x00;

	i2c_master_send(client, value, 3);

	dev_info(dev, "led BLUE is ON\n");

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

	dev_info(dev, "there are %d nodes\n", count);

	private = devm_kzalloc(dev, sizeof(*private), GFP_KERNEL);
	if (!private)
		return -ENOMEM;

	private->client = client;
	i2c_set_clientdata(client, private);

	private->display_cs = devm_gpiod_get(dev, NULL, GPIOD_ASIS);
	if (IS_ERR(private->display_cs)) {
		ret = PTR_ERR(private->display_cs);
		dev_err(dev, "Unable to claim gpio\n");
		return ret;
	}

	gpiod_direction_output(private->display_cs, 1);

	/* Register sysfs hooks */
	ret = sysfs_create_group(&client->dev.kobj, &display_cs_group);
	if (ret < 0) {
		dev_err(&client->dev, "couldn't register sysfs group\n");
		return ret;
	}
	
	/* parse all the child nodes */
	device_for_each_child_node(dev, child){

		struct led_device *led_device;
		struct led_classdev *cdev;

		led_device = devm_kzalloc(dev, sizeof(*led_device), GFP_KERNEL);
		if (!led_device)
			return -ENOMEM;

		cdev = &led_device->cdev;
		led_device->private = private;
		
		fwnode_property_read_string(child, "label", &cdev->name);

		if (strcmp(cdev->name,"main") == 0) {
			led_device->cdev.brightness_set_blocking = led_control;
			ret = devm_led_classdev_register(dev, &led_device->cdev);
			if (ret)
				goto err;
			dev_info(cdev->dev, "the subsystem is %s and num is %d\n", 
				 cdev->name, private->num_leds);
		}
		else if (strcmp(cdev->name,"sub") == 0) {
			led_device->cdev.brightness_set_blocking = led_control;
			ret = devm_led_classdev_register(dev, &led_device->cdev);
			if (ret)
				goto err;
			dev_info(cdev->dev, "the subsystem is %s and num is %d\n", 
				 cdev->name, private->num_leds);
		}
		else if (strcmp(cdev->name,"red") == 0) {
			led_device->cdev.brightness_set_blocking = led_control;
			ret = devm_led_classdev_register(dev, &led_device->cdev);
			if (ret)
				goto err;
			dev_info(cdev->dev, "the subsystem is %s and num is %d\n", 
				 cdev->name, private->num_leds);
		}
		else if (strcmp(cdev->name,"green") == 0) {
			led_device->cdev.brightness_set_blocking = led_control;
			ret = devm_led_classdev_register(dev, &led_device->cdev);
			if (ret)
				goto err;
			dev_info(cdev->dev, "the subsystem is %s and num is %d\n", 
				 cdev->name, private->num_leds);
		}
		else if (strcmp(cdev->name,"blue") == 0) {
			led_device->cdev.brightness_set_blocking = led_control;
			ret = devm_led_classdev_register(dev, &led_device->cdev);
			if (ret)
				goto err;
			dev_info(cdev->dev, "the subsystem is %s and num is %d\n", 
				 cdev->name, private->num_leds);
		}
		else {
			dev_err(dev, "Bad device tree value\n");
			return -EINVAL;
		}

		private->num_leds++;
	}
	
	dev_info(dev, "i am out of the device tree\n");
	dev_info(dev, "my_probe() function is exited.\n");
	return 0;

err:
	fwnode_handle_put(child);
	sysfs_remove_group(&client->dev.kobj, &display_cs_group);

	return ret;
}

static int ltc3206_remove(struct i2c_client *client)
{
	dev_info(&client->dev, "leds_remove enter\n");
	sysfs_remove_group(&client->dev.kobj, &display_cs_group);
	dev_info(&client->dev, "leds_remove exit\n");

	return 0;
}

static const struct of_device_id my_of_ids[] = {
	{ .compatible = "arrow,ltc3206"},
	{},
};
MODULE_DEVICE_TABLE(of, my_of_ids);

static const struct i2c_device_id ltc3206_id[] = {
	{ "ltc3206", 0 },
	{ }
};
MODULE_DEVICE_TABLE(i2c, ltc3206_id);

static struct i2c_driver ltc3206_driver = {
	.probe = ltc3206_probe,
	.remove = ltc3206_remove,
	.id_table	= ltc3206_id,
	.driver = {
		.name = "ltc3206",
		.of_match_table = my_of_ids,
		.owner = THIS_MODULE,
	}
};

module_i2c_driver(ltc3206_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR(" ");
MODULE_DESCRIPTION("This is a driver that controls the \
                   ltc3206 I2C multidisplay device");

11.5.4 ltc3206测试调试

insmod ltc3206_imx_led_class.ko
echo 10 > /sys/class/leds/red/brightness
echo 10 > /sys/class/leds/blue/brightness
echo 10 > /sys/class/leds/main/brightness   //设置最大值
echo 10 > /sys/class/leds/sub/brightness   //切换SUB
echo off > /sys/class/i2c-dev/i2c-2/device/2-001b/display_cs/sub    //关闭

rmmod ltc3206_imx_led_class.ko

感谢阅读,祝君成功!
-by aiziyou

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jack.Jia

感谢打赏!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值