From 0ba3c3570e62386eba5cb04b54735d3203b38c2e Mon Sep 17 00:00:00 2001
From: Haavard Skinnemoen <hskinnemoen@atmel.com>
Date: Tue, 12 Jun 2007 17:03:10 +0200
Subject: [PATCH] [AVR32] GPIO /dev interface

This adds a simple /dev interface for GPIO, configured using configfs.
To use:
	echo 1 > gpio_id
	echo 0xff00 > pin_mask
	echo 0xff00 > oe_mask
	echo 1 > enabled
	echo -ne '\x00\x00\xff\x00' > /dev/gpio0

and watch the LEDs.

Signed-off-by: Haavard Skinnemoen <hskinnemoen@atmel.com>
---
 arch/avr32/mach-at32ap/Kconfig          |    7 +
 arch/avr32/mach-at32ap/Makefile         |    1 +
 arch/avr32/mach-at32ap/gpio-dev.c       |  570 +++++++++++++++++++++++++++++++
 arch/avr32/mach-at32ap/pio.c            |   76 ++++
 include/asm-avr32/arch-at32ap/portmux.h |   12 +
 5 files changed, 666 insertions(+), 0 deletions(-)
 create mode 100644 arch/avr32/mach-at32ap/gpio-dev.c

diff --git a/arch/avr32/mach-at32ap/Kconfig b/arch/avr32/mach-at32ap/Kconfig
index eb30783..43c5b9f 100644
--- a/arch/avr32/mach-at32ap/Kconfig
+++ b/arch/avr32/mach-at32ap/Kconfig
@@ -26,6 +26,13 @@ config AP7000_8_BIT_SMC
 
 endchoice
 
+config GPIO_DEV
+	bool "GPIO /dev interface"
+	select CONFIGFS_FS
+	default n
+	help
+	  Say `Y' to enable a /dev interface to the GPIO pins.
+
 endmenu
 
 endif # PLATFORM_AT32AP
diff --git a/arch/avr32/mach-at32ap/Makefile b/arch/avr32/mach-at32ap/Makefile
index a8b4450..250372a 100644
--- a/arch/avr32/mach-at32ap/Makefile
+++ b/arch/avr32/mach-at32ap/Makefile
@@ -2,3 +2,4 @@ obj-y				+= at32ap.o clock.o intc.o extint.o pio.o hsmc.o
 obj-$(CONFIG_CPU_AT32AP7000)	+= at32ap7000.o
 obj-$(CONFIG_CPU_AT32AP7000)	+= time-tc.o
 obj-$(CONFIG_CPU_FREQ_AT32AP)	+= cpufreq.o
+obj-$(CONFIG_GPIO_DEV)		+= gpio-dev.o
diff --git a/arch/avr32/mach-at32ap/gpio-dev.c b/arch/avr32/mach-at32ap/gpio-dev.c
new file mode 100644
index 0000000..3d4810d
--- /dev/null
+++ b/arch/avr32/mach-at32ap/gpio-dev.c
@@ -0,0 +1,570 @@
+/*
+ * GPIO /dev and configfs interface
+ *
+ * Copyright (C) 2006-2007 Atmel Corporation
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ */
+#include <linux/configfs.h>
+#include <linux/cdev.h>
+#include <linux/fs.h>
+#include <linux/interrupt.h>
+#include <linux/poll.h>
+#include <linux/uaccess.h>
+#include <linux/wait.h>
+
+#include <asm/gpio.h>
+#include <asm/arch/portmux.h>
+
+#define GPIO_DEV_MAX			8
+
+static struct class *gpio_dev_class;
+static dev_t gpio_devt;
+
+struct gpio_item {
+	spinlock_t lock;
+
+	int enabled;
+	int initialized;
+	int port;
+	u32 pin_mask;
+	u32 oe_mask;
+
+	/* Pin state last time we read it (for blocking reads) */
+	u32 pin_state;
+	int changed;
+
+	wait_queue_head_t change_wq;
+	struct fasync_struct *async_queue;
+
+	int id;
+	struct class_device *gpio_dev;
+	struct cdev char_dev;
+	struct config_item item;
+};
+
+struct gpio_attribute {
+	struct configfs_attribute attr;
+	ssize_t (*show)(struct gpio_item *, char *);
+	ssize_t (*store)(struct gpio_item *, const char *, size_t);
+};
+
+static irqreturn_t gpio_dev_interrupt(int irq, void *dev_id)
+{
+	struct gpio_item *gpio = dev_id;
+	u32 old_state, new_state;
+
+	old_state = gpio->pin_state;
+	new_state = at32_gpio_get_value_multiple(gpio->port, gpio->pin_mask);
+	gpio->pin_state = new_state;
+
+	if (new_state != old_state) {
+		gpio->changed = 1;
+		wake_up_interruptible(&gpio->change_wq);
+
+		if (gpio->async_queue)
+			kill_fasync(&gpio->async_queue, SIGIO, POLL_IN);
+	}
+
+	return IRQ_HANDLED;
+}
+
+static int gpio_dev_open(struct inode *inode, struct file *file)
+{
+	struct gpio_item *gpio = container_of(inode->i_cdev,
+					      struct gpio_item,
+					      char_dev);
+	unsigned int irq;
+	unsigned int i;
+	int ret;
+
+	nonseekable_open(inode, file);
+	config_item_get(&gpio->item);
+	file->private_data = gpio;
+
+	gpio->pin_state = at32_gpio_get_value_multiple(gpio->port,
+						       gpio->pin_mask);
+	gpio->changed = 1;
+
+	for (i = 0; i < 32; i++) {
+		if (gpio->pin_mask & (1 << i)) {
+			irq = gpio_to_irq(32 * gpio->port + i);
+			ret = request_irq(irq, gpio_dev_interrupt, 0,
+					  "gpio-dev", gpio);
+			if (ret)
+				goto err_irq;
+		}
+	}
+
+	return 0;
+
+err_irq:
+	while (i--) {
+		if (gpio->pin_mask & (1 << i)) {
+			irq = gpio_to_irq(32 * gpio->port + i);
+			free_irq(irq, gpio);
+		}
+	}
+
+	config_item_put(&gpio->item);
+
+	return ret;
+}
+
+static int gpio_dev_fasync(int fd, struct file *file, int mode)
+{
+	struct gpio_item *gpio = file->private_data;
+
+	return fasync_helper(fd, file, mode, &gpio->async_queue);
+}
+
+static int gpio_dev_release(struct inode *inode, struct file *file)
+{
+	struct gpio_item *gpio = file->private_data;
+	unsigned int irq;
+	unsigned int i;
+
+	gpio_dev_fasync(-1, file, 0);
+
+	for (i = 0; i < 32; i++) {
+		if (gpio->pin_mask & (1 << i)) {
+			irq = gpio_to_irq(32 * gpio->port + i);
+			free_irq(irq, gpio);
+		}
+	}
+
+	config_item_put(&gpio->item);
+
+	return 0;
+}
+
+static unsigned int gpio_dev_poll(struct file *file, poll_table *wait)
+{
+	struct gpio_item *gpio = file->private_data;
+	unsigned int mask = 0;
+
+	poll_wait(file, &gpio->change_wq, wait);
+	if (gpio->changed)
+		mask |= POLLIN | POLLRDNORM;
+
+	return mask;
+}
+
+static ssize_t gpio_dev_read(struct file *file, char __user *buf,
+			     size_t count, loff_t *offset)
+{
+	struct gpio_item *gpio = file->private_data;
+	u32 value;
+
+	spin_lock_irq(&gpio->lock);
+	while (!gpio->changed) {
+		spin_unlock_irq(&gpio->lock);
+
+		if (file->f_flags & O_NONBLOCK)
+			return -EAGAIN;
+
+		if (wait_event_interruptible(gpio->change_wq, gpio->changed))
+			return -ERESTARTSYS;
+
+		spin_lock_irq(&gpio->lock);
+	}
+
+	gpio->changed = 0;
+	value = at32_gpio_get_value_multiple(gpio->port, gpio->pin_mask);
+
+	spin_unlock_irq(&gpio->lock);
+
+	count = min(count, (size_t)4);
+	if (copy_to_user(buf, &value, count))
+		return -EFAULT;
+
+	return count;
+}
+
+static ssize_t gpio_dev_write(struct file *file, const char __user *buf,
+			      size_t count, loff_t *offset)
+{
+	struct gpio_item *gpio = file->private_data;
+	u32 value = 0;
+	u32 mask = ~0UL;
+
+	count = min(count, (size_t)4);
+	if (copy_from_user(&value, buf, count))
+		return -EFAULT;
+
+	/* Assuming big endian */
+	mask <<= (4 - count) * 8;
+	mask &= gpio->pin_mask;
+
+	at32_gpio_set_value_multiple(gpio->port, value, mask);
+
+	return count;
+}
+
+static struct file_operations gpio_dev_fops = {
+	.owner		= THIS_MODULE,
+	.llseek		= no_llseek,
+	.open		= gpio_dev_open,
+	.release	= gpio_dev_release,
+	.fasync		= gpio_dev_fasync,
+	.poll		= gpio_dev_poll,
+	.read		= gpio_dev_read,
+	.write		= gpio_dev_write,
+};
+
+static struct gpio_item *to_gpio_item(struct config_item *item)
+{
+	return item ? container_of(item, struct gpio_item, item) : NULL;
+}
+
+static ssize_t gpio_show_gpio_id(struct gpio_item *gpio, char *page)
+{
+	return sprintf(page, "%d\n", gpio->port);
+}
+
+static ssize_t gpio_store_gpio_id(struct gpio_item *gpio,
+				  const char *page, size_t count)
+{
+	unsigned long id;
+	char *p = (char *)page;
+	ssize_t ret = -EINVAL;
+
+	id = simple_strtoul(p, &p, 0);
+	if (!p || (*p && (*p != '\n')))
+		return -EINVAL;
+
+	/* Switching PIO is not allowed when live... */
+	spin_lock(&gpio->lock);
+	if (!gpio->enabled) {
+		ret = -ENXIO;
+		if (at32_gpio_port_is_valid(id)) {
+			gpio->port = id;
+			ret = count;
+		}
+	}
+	spin_unlock(&gpio->lock);
+
+	return ret;
+}
+
+static ssize_t gpio_show_pin_mask(struct gpio_item *gpio, char *page)
+{
+	return sprintf(page, "0x%08x\n", gpio->pin_mask);
+}
+
+static ssize_t gpio_store_pin_mask(struct gpio_item *gpio,
+				   const char *page, size_t count)
+{
+	u32 new_mask;
+	char *p = (char *)page;
+	ssize_t ret = -EINVAL;
+
+	new_mask = simple_strtoul(p, &p, 0);
+	if (!p || (*p && (*p != '\n')))
+		return -EINVAL;
+
+	/* Can't update the pin mask while live. */
+	spin_lock(&gpio->lock);
+	if (!gpio->enabled) {
+		gpio->oe_mask &= new_mask;
+		gpio->pin_mask = new_mask;
+		ret = count;
+	}
+	spin_unlock(&gpio->lock);
+
+	return ret;
+}
+
+static ssize_t gpio_show_oe_mask(struct gpio_item *gpio, char *page)
+{
+	return sprintf(page, "0x%08x\n", gpio->oe_mask);
+}
+
+static ssize_t gpio_store_oe_mask(struct gpio_item *gpio,
+				  const char *page, size_t count)
+{
+	u32 mask;
+	char *p = (char *)page;
+	ssize_t ret = -EINVAL;
+
+	mask = simple_strtoul(p, &p, 0);
+	if (!p || (*p && (*p != '\n')))
+		return -EINVAL;
+
+	spin_lock(&gpio->lock);
+	if (!gpio->enabled) {
+		gpio->oe_mask = mask & gpio->pin_mask;
+		ret = count;
+	}
+	spin_unlock(&gpio->lock);
+
+	return ret;
+}
+
+static ssize_t gpio_show_enabled(struct gpio_item *gpio, char *page)
+{
+	return sprintf(page, "%d\n", gpio->enabled);
+}
+
+static ssize_t gpio_store_enabled(struct gpio_item *gpio,
+				  const char *page, size_t count)
+{
+	char *p = (char *)page;
+	int enabled;
+	int ret;
+
+	enabled = simple_strtoul(p, &p, 0);
+	if (!p || (*p && (*p != '\n')))
+		return -EINVAL;
+
+	/* make it a boolean value */
+	enabled = !!enabled;
+
+	if (gpio->enabled == enabled)
+		/* No change; do nothing. */
+		return count;
+
+	BUG_ON(gpio->id >= GPIO_DEV_MAX);
+
+	if (!enabled) {
+		class_device_unregister(gpio->gpio_dev);
+		cdev_del(&gpio->char_dev);
+		at32_deselect_pins(gpio->port, gpio->pin_mask);
+		gpio->initialized = 0;
+	} else {
+		if (gpio->port < 0 || !gpio->pin_mask)
+			return -ENODEV;
+	}
+
+	/* Disallow any updates to gpio_id or pin_mask */
+	spin_lock(&gpio->lock);
+	gpio->enabled = enabled;
+	spin_unlock(&gpio->lock);
+
+	if (!enabled)
+		return count;
+
+	/* Now, try to allocate the pins */
+	ret = at32_select_gpio_pins(gpio->port, gpio->pin_mask, gpio->oe_mask);
+	if (ret)
+		goto err_alloc_pins;
+
+	gpio->initialized = 1;
+
+	cdev_init(&gpio->char_dev, &gpio_dev_fops);
+	gpio->char_dev.owner = THIS_MODULE;
+	ret = cdev_add(&gpio->char_dev, MKDEV(MAJOR(gpio_devt), gpio->id), 1);
+	if (ret < 0)
+		goto err_cdev_add;
+	gpio->gpio_dev = class_device_create(gpio_dev_class, NULL,
+					     MKDEV(MAJOR(gpio_devt), gpio->id),
+					     NULL,
+					     "gpio%d", gpio->id);
+	if (IS_ERR(gpio->gpio_dev)) {
+		printk(KERN_ERR "failed to create gpio%d\n", gpio->id);
+		ret = PTR_ERR(gpio->gpio_dev);
+		goto err_class_dev;
+	}
+
+	printk(KERN_INFO "created gpio%d (port%d/0x%08x) as (%d:%d)\n",
+	       gpio->id, gpio->port, gpio->pin_mask,
+	       MAJOR(gpio->gpio_dev->devt), MINOR(gpio->gpio_dev->devt));
+
+	return 0;
+
+err_class_dev:
+	cdev_del(&gpio->char_dev);
+err_cdev_add:
+	at32_deselect_pins(gpio->port, gpio->pin_mask);
+	gpio->initialized = 0;
+err_alloc_pins:
+	spin_lock(&gpio->lock);
+	gpio->enabled = 0;
+	spin_unlock(&gpio->lock);
+
+	return ret;
+}
+
+static struct gpio_attribute gpio_item_attr_gpio_id = {
+	.attr = {
+		.ca_owner = THIS_MODULE,
+		.ca_name = "gpio_id",
+		.ca_mode = S_IRUGO | S_IWUSR,
+	},
+	.show = gpio_show_gpio_id,
+	.store = gpio_store_gpio_id,
+};
+static struct gpio_attribute gpio_item_attr_pin_mask = {
+	.attr = {
+		.ca_owner = THIS_MODULE,
+		.ca_name = "pin_mask",
+		.ca_mode = S_IRUGO | S_IWUSR,
+	},
+	.show = gpio_show_pin_mask,
+	.store = gpio_store_pin_mask,
+};
+static struct gpio_attribute gpio_item_attr_oe_mask = {
+	.attr = {
+		.ca_owner = THIS_MODULE,
+		.ca_name = "oe_mask",
+		.ca_mode = S_IRUGO | S_IWUSR,
+	},
+	.show = gpio_show_oe_mask,
+	.store = gpio_store_oe_mask,
+};
+static struct gpio_attribute gpio_item_attr_enabled = {
+	.attr = {
+		.ca_owner = THIS_MODULE,
+		.ca_name = "enabled",
+		.ca_mode = S_IRUGO | S_IWUSR,
+	},
+	.show = gpio_show_enabled,
+	.store = gpio_store_enabled,
+};
+
+static struct configfs_attribute *gpio_item_attrs[] = {
+	&gpio_item_attr_gpio_id.attr,
+	&gpio_item_attr_pin_mask.attr,
+	&gpio_item_attr_oe_mask.attr,
+	&gpio_item_attr_enabled.attr,
+	NULL,
+};
+
+static ssize_t gpio_show_attr(struct config_item *item,
+			      struct configfs_attribute *attr,
+			      char *page)
+{
+	struct gpio_item *gpio_item = to_gpio_item(item);
+	struct gpio_attribute *gpio_attr
+		= container_of(attr, struct gpio_attribute, attr);
+	ssize_t ret = 0;
+
+	if (gpio_attr->show)
+		ret = gpio_attr->show(gpio_item, page);
+	return ret;
+}
+
+static ssize_t gpio_store_attr(struct config_item *item,
+			       struct configfs_attribute *attr,
+			       const char *page, size_t count)
+{
+	struct gpio_item *gpio_item = to_gpio_item(item);
+	struct gpio_attribute *gpio_attr
+		= container_of(attr, struct gpio_attribute, attr);
+	ssize_t ret = -EINVAL;
+
+	if (gpio_attr->store)
+		ret = gpio_attr->store(gpio_item, page, count);
+	return ret;
+}
+
+static void gpio_release(struct config_item *item)
+{
+	kfree(to_gpio_item(item));
+}
+
+static struct configfs_item_operations gpio_item_ops = {
+	.release		= gpio_release,
+	.show_attribute		= gpio_show_attr,
+	.store_attribute	= gpio_store_attr,
+};
+
+static struct config_item_type gpio_item_type = {
+	.ct_item_ops	= &gpio_item_ops,
+	.ct_attrs	= gpio_item_attrs,
+	.ct_owner	= THIS_MODULE,
+};
+
+static struct config_item *gpio_make_item(struct config_group *group,
+					  const char *name)
+{
+	static int next_id;
+	struct gpio_item *gpio;
+
+	if (next_id >= GPIO_DEV_MAX)
+		return NULL;
+
+	gpio = kzalloc(sizeof(struct gpio_item), GFP_KERNEL);
+	if (!gpio)
+		return NULL;
+
+	gpio->id = next_id++;
+	config_item_init_type_name(&gpio->item, name, &gpio_item_type);
+	spin_lock_init(&gpio->lock);
+	init_waitqueue_head(&gpio->change_wq);
+
+	return &gpio->item;
+}
+
+static void gpio_drop_item(struct config_group *group,
+			   struct config_item *item)
+{
+	struct gpio_item *gpio = to_gpio_item(item);
+
+	spin_lock(&gpio->lock);
+	if (gpio->enabled) {
+		class_device_unregister(gpio->gpio_dev);
+		cdev_del(&gpio->char_dev);
+	}
+
+	if (gpio->initialized) {
+		at32_deselect_pins(gpio->port, gpio->pin_mask);
+		gpio->initialized = 0;
+		gpio->enabled = 0;
+	}
+	spin_unlock(&gpio->lock);
+}
+
+static struct configfs_group_operations gpio_group_ops = {
+	.make_item	= gpio_make_item,
+	.drop_item	= gpio_drop_item,
+};
+
+static struct config_item_type gpio_group_type = {
+	.ct_group_ops	= &gpio_group_ops,
+	.ct_owner	= THIS_MODULE,
+};
+
+static struct configfs_subsystem gpio_subsys = {
+	.su_group = {
+		.cg_item = {
+			 .ci_namebuf = "gpio",
+			 .ci_type = &gpio_group_type,
+		 },
+	},
+};
+
+static int __init gpio_dev_init(void)
+{
+	int err;
+
+	gpio_dev_class = class_create(THIS_MODULE, "gpio-dev");
+	if (IS_ERR(gpio_dev_class)) {
+		err = PTR_ERR(gpio_dev_class);
+		goto err_class_create;
+	}
+
+	err = alloc_chrdev_region(&gpio_devt, 0, GPIO_DEV_MAX, "gpio");
+	if (err < 0)
+		goto err_alloc_chrdev;
+
+	/* Configfs initialization */
+	config_group_init(&gpio_subsys.su_group);
+	init_MUTEX(&gpio_subsys.su_sem);
+	err = configfs_register_subsystem(&gpio_subsys);
+	if (err)
+		goto err_register_subsys;
+
+	return 0;
+
+err_register_subsys:
+	unregister_chrdev_region(gpio_devt, GPIO_DEV_MAX);
+err_alloc_chrdev:
+	class_destroy(gpio_dev_class);
+err_class_create:
+	printk(KERN_WARNING "Failed to initialize gpio /dev interface\n");
+	return err;
+}
+late_initcall(gpio_dev_init);
diff --git a/arch/avr32/mach-at32ap/pio.c b/arch/avr32/mach-at32ap/pio.c
index 1eb99b8..153bb9a 100644
--- a/arch/avr32/mach-at32ap/pio.c
+++ b/arch/avr32/mach-at32ap/pio.c
@@ -158,6 +158,82 @@ fail:
 	dump_stack();
 }
 
+#ifdef CONFIG_GPIO_DEV
+
+/* Gang allocators and accessors; used by the GPIO /dev driver */
+int at32_gpio_port_is_valid(unsigned int port)
+{
+	return port < MAX_NR_PIO_DEVICES && pio_dev[port].regs != NULL;
+}
+
+int at32_select_gpio_pins(unsigned int port, u32 pins, u32 oe_mask)
+{
+	struct pio_device *pio;
+	u32 old, new;
+
+	pio = &pio_dev[port];
+	BUG_ON(port > ARRAY_SIZE(pio_dev) || !pio->regs || (oe_mask & ~pins));
+
+	/* Try to allocate the pins */
+	do {
+		old = pio->pinmux_mask;
+		if (old & pins)
+			return -EBUSY;
+
+		new = old | pins;
+	} while (cmpxchg(&pio->pinmux_mask, old, new) != old);
+
+	/* That went well, now configure the port */
+	pio_writel(pio, OER, oe_mask);
+	pio_writel(pio, PER, pins);
+
+	return 0;
+}
+
+void at32_deselect_pins(unsigned int port, u32 pins)
+{
+	struct pio_device *pio;
+	u32 old, new;
+
+	pio = &pio_dev[port];
+	BUG_ON(port > ARRAY_SIZE(pio_dev) || !pio->regs);
+
+	/* Return to a "safe" mux configuration */
+	pio_writel(pio, PUER, pins);
+	pio_writel(pio, ODR, pins);
+
+	/* Deallocate the pins */
+	do {
+		old = pio->pinmux_mask;
+		new = old & ~pins;
+	} while (cmpxchg(&pio->pinmux_mask, old, new) != old);
+}
+
+u32 at32_gpio_get_value_multiple(unsigned int port, u32 pins)
+{
+	struct pio_device *pio;
+
+	pio = &pio_dev[port];
+	BUG_ON(port > ARRAY_SIZE(pio_dev) || !pio->regs);
+
+	return pio_readl(pio, PDSR) & pins;
+}
+
+void at32_gpio_set_value_multiple(unsigned int port, u32 value, u32 mask)
+{
+	struct pio_device *pio;
+
+	pio = &pio_dev[port];
+	BUG_ON(port > ARRAY_SIZE(pio_dev) || !pio->regs);
+
+	/* No atomic updates for now... */
+	pio_writel(pio, CODR, ~value & mask);
+	pio_writel(pio, SODR, value & mask);
+}
+
+#endif /* CONFIG_GPIO_DEV */
+
+
 /*--------------------------------------------------------------------------*/
 
 /* GPIO API */
diff --git a/include/asm-avr32/arch-at32ap/portmux.h b/include/asm-avr32/arch-at32ap/portmux.h
index 9930871..0f95567 100644
--- a/include/asm-avr32/arch-at32ap/portmux.h
+++ b/include/asm-avr32/arch-at32ap/portmux.h
@@ -25,4 +25,16 @@ void at32_select_periph(unsigned int pin, unsigned int periph,
 void at32_select_gpio(unsigned int pin, unsigned long flags);
 void at32_reserve_pin(unsigned int pin);
 
+#ifdef CONFIG_GPIO_DEV
+
+/* Gang allocators and accessors; used by the GPIO /dev driver */
+int at32_gpio_port_is_valid(unsigned int port);
+int at32_select_gpio_pins(unsigned int port, u32 pins, u32 oe_mask);
+void at32_deselect_pins(unsigned int port, u32 pins);
+
+u32 at32_gpio_get_value_multiple(unsigned int port, u32 pins);
+void at32_gpio_set_value_multiple(unsigned int port, u32 value, u32 mask);
+
+#endif /* CONFIG_GPIO_DEV */
+
 #endif /* __ASM_ARCH_PORTMUX_H__ */
-- 
1.5.2.2

