2018-08-10 13:26:49 +03:00
|
|
|
// SPDX-License-Identifier: GPL-2.0
|
2017-06-22 11:17:32 +01:00
|
|
|
/*
|
|
|
|
* Synopsys DesignWare I2C adapter driver (slave only).
|
|
|
|
*
|
|
|
|
* Based on the Synopsys DesignWare I2C adapter driver (master).
|
|
|
|
*
|
|
|
|
* Copyright (C) 2016 Synopsys Inc.
|
|
|
|
*/
|
2024-12-30 16:59:49 +01:00
|
|
|
|
|
|
|
#define DEFAULT_SYMBOL_NAMESPACE "I2C_DW"
|
|
|
|
|
2017-06-22 11:17:32 +01:00
|
|
|
#include <linux/delay.h>
|
|
|
|
#include <linux/err.h>
|
|
|
|
#include <linux/errno.h>
|
|
|
|
#include <linux/i2c.h>
|
|
|
|
#include <linux/interrupt.h>
|
|
|
|
#include <linux/io.h>
|
|
|
|
#include <linux/module.h>
|
|
|
|
#include <linux/pm_runtime.h>
|
2020-05-28 12:33:18 +03:00
|
|
|
#include <linux/regmap.h>
|
2017-06-22 11:17:32 +01:00
|
|
|
|
|
|
|
#include "i2c-designware-core.h"
|
|
|
|
|
|
|
|
static void i2c_dw_configure_fifo_slave(struct dw_i2c_dev *dev)
|
|
|
|
{
|
|
|
|
/* Configure Tx/Rx FIFO threshold levels. */
|
2020-05-28 12:33:18 +03:00
|
|
|
regmap_write(dev->map, DW_IC_TX_TL, 0);
|
|
|
|
regmap_write(dev->map, DW_IC_RX_TL, 0);
|
2017-06-22 11:17:32 +01:00
|
|
|
|
|
|
|
/* Configure the I2C slave. */
|
2020-05-28 12:33:18 +03:00
|
|
|
regmap_write(dev->map, DW_IC_CON, dev->slave_cfg);
|
|
|
|
regmap_write(dev->map, DW_IC_INTR_MASK, DW_IC_INTR_SLAVE_MASK);
|
2017-06-22 11:17:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2024-09-25 15:44:23 +03:00
|
|
|
* i2c_dw_init_slave() - Initialize the DesignWare i2c slave hardware
|
2017-06-22 11:17:32 +01:00
|
|
|
* @dev: device private data
|
|
|
|
*
|
|
|
|
* This function configures and enables the I2C in slave mode.
|
|
|
|
* This function is called during I2C init function, and in case of timeout at
|
|
|
|
* run time.
|
2024-09-25 15:44:23 +03:00
|
|
|
*
|
|
|
|
* Return: 0 on success, or negative errno otherwise.
|
2017-06-22 11:17:32 +01:00
|
|
|
*/
|
2017-06-28 17:23:28 +03:00
|
|
|
static int i2c_dw_init_slave(struct dw_i2c_dev *dev)
|
2017-06-22 11:17:32 +01:00
|
|
|
{
|
|
|
|
int ret;
|
|
|
|
|
|
|
|
ret = i2c_dw_acquire_lock(dev);
|
|
|
|
if (ret)
|
|
|
|
return ret;
|
|
|
|
|
|
|
|
/* Disable the adapter. */
|
2018-04-28 16:56:07 +03:00
|
|
|
__i2c_dw_disable(dev);
|
2017-06-22 11:17:32 +01:00
|
|
|
|
2018-06-19 14:23:22 +03:00
|
|
|
/* Write SDA hold time if supported */
|
|
|
|
if (dev->sda_hold_time)
|
2020-05-28 12:33:18 +03:00
|
|
|
regmap_write(dev->map, DW_IC_SDA_HOLD, dev->sda_hold_time);
|
2017-06-22 11:17:32 +01:00
|
|
|
|
|
|
|
i2c_dw_configure_fifo_slave(dev);
|
|
|
|
i2c_dw_release_lock(dev);
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int i2c_dw_reg_slave(struct i2c_client *slave)
|
|
|
|
{
|
|
|
|
struct dw_i2c_dev *dev = i2c_get_adapdata(slave->adapter);
|
|
|
|
|
|
|
|
if (dev->slave)
|
|
|
|
return -EBUSY;
|
|
|
|
if (slave->flags & I2C_CLIENT_TEN)
|
|
|
|
return -EAFNOSUPPORT;
|
2017-08-15 17:34:45 +03:00
|
|
|
pm_runtime_get_sync(dev->dev);
|
|
|
|
|
2017-06-22 11:17:32 +01:00
|
|
|
/*
|
|
|
|
* Set slave address in the IC_SAR register,
|
|
|
|
* the address to which the DW_apb_i2c responds.
|
|
|
|
*/
|
2018-04-28 16:56:07 +03:00
|
|
|
__i2c_dw_disable_nowait(dev);
|
2020-05-28 12:33:18 +03:00
|
|
|
regmap_write(dev->map, DW_IC_SAR, slave->addr);
|
2017-06-22 11:17:32 +01:00
|
|
|
dev->slave = slave;
|
|
|
|
|
2018-04-28 16:56:07 +03:00
|
|
|
__i2c_dw_enable(dev);
|
2017-06-22 11:17:32 +01:00
|
|
|
|
2022-11-07 15:42:39 +02:00
|
|
|
dev->status = 0;
|
2017-06-22 11:17:32 +01:00
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int i2c_dw_unreg_slave(struct i2c_client *slave)
|
|
|
|
{
|
|
|
|
struct dw_i2c_dev *dev = i2c_get_adapdata(slave->adapter);
|
|
|
|
|
2022-11-07 15:42:46 +02:00
|
|
|
regmap_write(dev->map, DW_IC_INTR_MASK, 0);
|
2024-08-22 20:58:41 +03:00
|
|
|
i2c_dw_disable(dev);
|
2019-08-15 16:52:11 +03:00
|
|
|
synchronize_irq(dev->irq);
|
2017-06-22 11:17:32 +01:00
|
|
|
dev->slave = NULL;
|
i2c: designware: Invoke runtime suspend on quick slave re-registration
Replaced pm_runtime_put() with pm_runtime_put_sync_suspend() to ensure
the runtime suspend is invoked immediately when unregistering a slave.
This prevents a race condition where suspend was skipped when
unregistering and registering slave in quick succession.
For example, consider the rapid sequence of
`delete_device -> new_device -> delete_device -> new_device`.
In this sequence, it is observed that the dw_i2c_plat_runtime_suspend()
might not be invoked after `delete_device` operation.
This is because after `delete_device` operation, when the
pm_runtime_put() is about to trigger suspend, the following `new_device`
operation might race and cancel the suspend.
If that happens, during the `new_device` operation,
dw_i2c_plat_runtime_resume() is skipped (since there was no suspend), which
means `i_dev->init()`, i.e. i2c_dw_init_slave(), is skipped.
Since i2c_dw_init_slave() is skipped, i2c_dw_configure_fifo_slave() is
skipped too, which leaves `DW_IC_INTR_MASK` unconfigured. If we inspect
the interrupt mask register using devmem, it will show as zero.
Example shell script to reproduce the issue:
```
#!/bin/sh
SLAVE_LADDR=0x1010
SLAVE_BUS=13
NEW_DEVICE=/sys/bus/i2c/devices/i2c-$SLAVE_BUS/new_device
DELETE_DEVICE=/sys/bus/i2c/devices/i2c-$SLAVE_BUS/delete_device
# Create initial device
echo slave-24c02 $SLAVE_LADDR > $NEW_DEVICE
sleep 2
# Rapid sequence of
# delete_device -> new_device -> delete_device -> new_device
echo $SLAVE_LADDR > $DELETE_DEVICE
echo slave-24c02 $SLAVE_LADDR > $NEW_DEVICE
echo $SLAVE_LADDR > $DELETE_DEVICE
echo slave-24c02 $SLAVE_LADDR > $NEW_DEVICE
# Using devmem to inspect IC_INTR_MASK will show as zero
```
Signed-off-by: Tan En De <ende.tan@starfivetech.com>
Acked-by: Jarkko Nikula <jarkko.nikula@linux.intel.com>
Link: https://lore.kernel.org/r/20250412023303.378600-1-ende.tan@starfivetech.com
Signed-off-by: Andi Shyti <andi.shyti@kernel.org>
2025-04-12 10:33:03 +08:00
|
|
|
pm_runtime_put_sync_suspend(dev->dev);
|
2017-06-22 11:17:32 +01:00
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static u32 i2c_dw_read_clear_intrbits_slave(struct dw_i2c_dev *dev)
|
|
|
|
{
|
2023-01-24 17:17:32 +05:30
|
|
|
unsigned int stat, dummy;
|
2017-06-22 11:17:32 +01:00
|
|
|
|
|
|
|
/*
|
|
|
|
* The IC_INTR_STAT register just indicates "enabled" interrupts.
|
2020-03-19 17:30:12 +02:00
|
|
|
* The unmasked raw version of interrupt status bits is available
|
2017-06-22 11:17:32 +01:00
|
|
|
* in the IC_RAW_INTR_STAT register.
|
|
|
|
*
|
|
|
|
* That is,
|
2020-05-28 12:33:18 +03:00
|
|
|
* stat = readl(IC_INTR_STAT);
|
2017-06-22 11:17:32 +01:00
|
|
|
* equals to,
|
2020-05-28 12:33:18 +03:00
|
|
|
* stat = readl(IC_RAW_INTR_STAT) & readl(IC_INTR_MASK);
|
2017-06-22 11:17:32 +01:00
|
|
|
*
|
|
|
|
* The raw version might be useful for debugging purposes.
|
|
|
|
*/
|
2020-05-28 12:33:18 +03:00
|
|
|
regmap_read(dev->map, DW_IC_INTR_STAT, &stat);
|
2017-06-22 11:17:32 +01:00
|
|
|
|
|
|
|
/*
|
|
|
|
* Do not use the IC_CLR_INTR register to clear interrupts, or
|
|
|
|
* you'll miss some interrupts, triggered during the period from
|
2020-05-28 12:33:18 +03:00
|
|
|
* readl(IC_INTR_STAT) to readl(IC_CLR_INTR).
|
2017-06-22 11:17:32 +01:00
|
|
|
*
|
|
|
|
* Instead, use the separately-prepared IC_CLR_* registers.
|
|
|
|
*/
|
|
|
|
if (stat & DW_IC_INTR_TX_ABRT)
|
2020-05-28 12:33:18 +03:00
|
|
|
regmap_read(dev->map, DW_IC_CLR_TX_ABRT, &dummy);
|
2017-06-22 11:17:32 +01:00
|
|
|
if (stat & DW_IC_INTR_RX_UNDER)
|
2020-05-28 12:33:18 +03:00
|
|
|
regmap_read(dev->map, DW_IC_CLR_RX_UNDER, &dummy);
|
2017-06-22 11:17:32 +01:00
|
|
|
if (stat & DW_IC_INTR_RX_OVER)
|
2020-05-28 12:33:18 +03:00
|
|
|
regmap_read(dev->map, DW_IC_CLR_RX_OVER, &dummy);
|
2017-06-22 11:17:32 +01:00
|
|
|
if (stat & DW_IC_INTR_TX_OVER)
|
2020-05-28 12:33:18 +03:00
|
|
|
regmap_read(dev->map, DW_IC_CLR_TX_OVER, &dummy);
|
2017-06-22 11:17:32 +01:00
|
|
|
if (stat & DW_IC_INTR_RX_DONE)
|
2020-05-28 12:33:18 +03:00
|
|
|
regmap_read(dev->map, DW_IC_CLR_RX_DONE, &dummy);
|
2017-06-22 11:17:32 +01:00
|
|
|
if (stat & DW_IC_INTR_ACTIVITY)
|
2020-05-28 12:33:18 +03:00
|
|
|
regmap_read(dev->map, DW_IC_CLR_ACTIVITY, &dummy);
|
2017-06-22 11:17:32 +01:00
|
|
|
if (stat & DW_IC_INTR_STOP_DET)
|
2020-05-28 12:33:18 +03:00
|
|
|
regmap_read(dev->map, DW_IC_CLR_STOP_DET, &dummy);
|
2017-06-22 11:17:32 +01:00
|
|
|
if (stat & DW_IC_INTR_START_DET)
|
2020-05-28 12:33:18 +03:00
|
|
|
regmap_read(dev->map, DW_IC_CLR_START_DET, &dummy);
|
2017-06-22 11:17:32 +01:00
|
|
|
if (stat & DW_IC_INTR_GEN_CALL)
|
2020-05-28 12:33:18 +03:00
|
|
|
regmap_read(dev->map, DW_IC_CLR_GEN_CALL, &dummy);
|
2017-06-22 11:17:32 +01:00
|
|
|
|
|
|
|
return stat;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Interrupt service routine. This gets called whenever an I2C slave interrupt
|
|
|
|
* occurs.
|
|
|
|
*/
|
2022-11-07 15:42:42 +02:00
|
|
|
static irqreturn_t i2c_dw_isr_slave(int this_irq, void *dev_id)
|
2017-06-22 11:17:32 +01:00
|
|
|
{
|
2022-11-07 15:42:42 +02:00
|
|
|
struct dw_i2c_dev *dev = dev_id;
|
2023-01-24 17:17:32 +05:30
|
|
|
unsigned int raw_stat, stat, enabled, tmp;
|
2020-05-28 12:33:18 +03:00
|
|
|
u8 val = 0, slave_activity;
|
2017-06-22 11:17:32 +01:00
|
|
|
|
2020-05-28 12:33:18 +03:00
|
|
|
regmap_read(dev->map, DW_IC_ENABLE, &enabled);
|
|
|
|
regmap_read(dev->map, DW_IC_RAW_INTR_STAT, &raw_stat);
|
|
|
|
regmap_read(dev->map, DW_IC_STATUS, &tmp);
|
|
|
|
slave_activity = ((tmp & DW_IC_STATUS_SLAVE_ACTIVITY) >> 6);
|
2017-06-22 11:17:32 +01:00
|
|
|
|
2017-08-11 14:44:55 +03:00
|
|
|
if (!enabled || !(raw_stat & ~DW_IC_INTR_ACTIVITY) || !dev->slave)
|
2022-11-07 15:42:42 +02:00
|
|
|
return IRQ_NONE;
|
2017-06-22 11:17:32 +01:00
|
|
|
|
2020-10-30 16:04:19 +08:00
|
|
|
stat = i2c_dw_read_clear_intrbits_slave(dev);
|
2017-06-22 11:17:32 +01:00
|
|
|
dev_dbg(dev->dev,
|
2017-06-29 09:22:15 +01:00
|
|
|
"%#x STATUS SLAVE_ACTIVITY=%#x : RAW_INTR_STAT=%#x : INTR_STAT=%#x\n",
|
2017-06-22 11:17:32 +01:00
|
|
|
enabled, slave_activity, raw_stat, stat);
|
|
|
|
|
2020-10-30 16:04:20 +08:00
|
|
|
if (stat & DW_IC_INTR_RX_FULL) {
|
2022-11-07 15:42:37 +02:00
|
|
|
if (!(dev->status & STATUS_WRITE_IN_PROGRESS)) {
|
|
|
|
dev->status |= STATUS_WRITE_IN_PROGRESS;
|
|
|
|
dev->status &= ~STATUS_READ_IN_PROGRESS;
|
2020-10-30 16:04:20 +08:00
|
|
|
i2c_slave_event(dev->slave, I2C_SLAVE_WRITE_REQUESTED,
|
|
|
|
&val);
|
|
|
|
}
|
|
|
|
|
2022-11-07 15:42:38 +02:00
|
|
|
do {
|
|
|
|
regmap_read(dev->map, DW_IC_DATA_CMD, &tmp);
|
2023-05-24 11:14:59 -07:00
|
|
|
if (tmp & DW_IC_DATA_CMD_FIRST_DATA_BYTE)
|
|
|
|
i2c_slave_event(dev->slave,
|
|
|
|
I2C_SLAVE_WRITE_REQUESTED,
|
|
|
|
&val);
|
2022-11-07 15:42:38 +02:00
|
|
|
val = tmp;
|
|
|
|
i2c_slave_event(dev->slave, I2C_SLAVE_WRITE_RECEIVED,
|
|
|
|
&val);
|
|
|
|
regmap_read(dev->map, DW_IC_STATUS, &tmp);
|
|
|
|
} while (tmp & DW_IC_STATUS_RFNE);
|
2020-10-30 16:04:20 +08:00
|
|
|
}
|
2017-06-22 11:17:32 +01:00
|
|
|
|
|
|
|
if (stat & DW_IC_INTR_RD_REQ) {
|
|
|
|
if (slave_activity) {
|
2020-10-30 16:04:20 +08:00
|
|
|
regmap_read(dev->map, DW_IC_CLR_RD_REQ, &tmp);
|
|
|
|
|
2022-11-07 15:42:37 +02:00
|
|
|
if (!(dev->status & STATUS_READ_IN_PROGRESS)) {
|
|
|
|
i2c_slave_event(dev->slave,
|
|
|
|
I2C_SLAVE_READ_REQUESTED,
|
|
|
|
&val);
|
|
|
|
dev->status |= STATUS_READ_IN_PROGRESS;
|
|
|
|
dev->status &= ~STATUS_WRITE_IN_PROGRESS;
|
|
|
|
} else {
|
|
|
|
i2c_slave_event(dev->slave,
|
|
|
|
I2C_SLAVE_READ_PROCESSED,
|
|
|
|
&val);
|
|
|
|
}
|
|
|
|
regmap_write(dev->map, DW_IC_DATA_CMD, val);
|
2017-06-22 11:17:32 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-07 15:42:37 +02:00
|
|
|
if (stat & DW_IC_INTR_STOP_DET)
|
2017-06-22 11:17:32 +01:00
|
|
|
i2c_slave_event(dev->slave, I2C_SLAVE_STOP, &val);
|
|
|
|
|
2022-11-07 15:42:42 +02:00
|
|
|
return IRQ_HANDLED;
|
2017-06-22 11:17:32 +01:00
|
|
|
}
|
|
|
|
|
2017-07-09 15:57:44 -05:00
|
|
|
static const struct i2c_algorithm i2c_dw_algo = {
|
2017-06-22 11:17:32 +01:00
|
|
|
.functionality = i2c_dw_func,
|
|
|
|
.reg_slave = i2c_dw_reg_slave,
|
|
|
|
.unreg_slave = i2c_dw_unreg_slave,
|
|
|
|
};
|
|
|
|
|
2020-04-25 16:44:45 +03:00
|
|
|
void i2c_dw_configure_slave(struct dw_i2c_dev *dev)
|
|
|
|
{
|
2024-05-31 11:17:48 +02:00
|
|
|
dev->functionality = I2C_FUNC_SLAVE;
|
2020-04-25 16:44:45 +03:00
|
|
|
|
|
|
|
dev->slave_cfg = DW_IC_CON_RX_FIFO_FULL_HLD_CTRL |
|
|
|
|
DW_IC_CON_RESTART_EN | DW_IC_CON_STOP_DET_IFADDRESSED;
|
|
|
|
|
|
|
|
dev->mode = DW_IC_SLAVE;
|
|
|
|
}
|
|
|
|
EXPORT_SYMBOL_GPL(i2c_dw_configure_slave);
|
|
|
|
|
2017-06-22 11:17:32 +01:00
|
|
|
int i2c_dw_probe_slave(struct dw_i2c_dev *dev)
|
|
|
|
{
|
|
|
|
struct i2c_adapter *adap = &dev->adapter;
|
|
|
|
int ret;
|
|
|
|
|
|
|
|
dev->init = i2c_dw_init_slave;
|
|
|
|
|
2020-05-28 12:33:18 +03:00
|
|
|
ret = i2c_dw_init_regmap(dev);
|
2018-06-19 14:23:19 +03:00
|
|
|
if (ret)
|
|
|
|
return ret;
|
|
|
|
|
2018-06-19 14:23:22 +03:00
|
|
|
ret = i2c_dw_set_sda_hold(dev);
|
|
|
|
if (ret)
|
|
|
|
return ret;
|
|
|
|
|
2020-05-28 12:33:18 +03:00
|
|
|
ret = i2c_dw_set_fifo_size(dev);
|
|
|
|
if (ret)
|
|
|
|
return ret;
|
2020-03-06 16:19:54 +03:00
|
|
|
|
2017-06-22 11:17:32 +01:00
|
|
|
ret = dev->init(dev);
|
|
|
|
if (ret)
|
|
|
|
return ret;
|
|
|
|
|
|
|
|
snprintf(adap->name, sizeof(adap->name),
|
|
|
|
"Synopsys DesignWare I2C Slave adapter");
|
|
|
|
adap->retries = 3;
|
|
|
|
adap->algo = &i2c_dw_algo;
|
|
|
|
adap->dev.parent = dev->dev;
|
|
|
|
i2c_set_adapdata(adap, dev);
|
|
|
|
|
|
|
|
ret = devm_request_irq(dev->dev, dev->irq, i2c_dw_isr_slave,
|
|
|
|
IRQF_SHARED, dev_name(dev->dev), dev);
|
|
|
|
if (ret) {
|
2024-09-25 15:44:23 +03:00
|
|
|
dev_err(dev->dev, "failure requesting IRQ %i: %d\n",
|
2017-06-22 11:17:32 +01:00
|
|
|
dev->irq, ret);
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
ret = i2c_add_numbered_adapter(adap);
|
|
|
|
if (ret)
|
|
|
|
dev_err(dev->dev, "failure adding adapter: %d\n", ret);
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
EXPORT_SYMBOL_GPL(i2c_dw_probe_slave);
|
|
|
|
|
|
|
|
MODULE_AUTHOR("Luis Oliveira <lolivei@synopsys.com>");
|
|
|
|
MODULE_DESCRIPTION("Synopsys DesignWare I2C bus slave adapter");
|
|
|
|
MODULE_LICENSE("GPL v2");
|
module: Convert symbol namespace to string literal
Clean up the existing export namespace code along the same lines of
commit 33def8498fdd ("treewide: Convert macro and uses of __section(foo)
to __section("foo")") and for the same reason, it is not desired for the
namespace argument to be a macro expansion itself.
Scripted using
git grep -l -e MODULE_IMPORT_NS -e EXPORT_SYMBOL_NS | while read file;
do
awk -i inplace '
/^#define EXPORT_SYMBOL_NS/ {
gsub(/__stringify\(ns\)/, "ns");
print;
next;
}
/^#define MODULE_IMPORT_NS/ {
gsub(/__stringify\(ns\)/, "ns");
print;
next;
}
/MODULE_IMPORT_NS/ {
$0 = gensub(/MODULE_IMPORT_NS\(([^)]*)\)/, "MODULE_IMPORT_NS(\"\\1\")", "g");
}
/EXPORT_SYMBOL_NS/ {
if ($0 ~ /(EXPORT_SYMBOL_NS[^(]*)\(([^,]+),/) {
if ($0 !~ /(EXPORT_SYMBOL_NS[^(]*)\(([^,]+), ([^)]+)\)/ &&
$0 !~ /(EXPORT_SYMBOL_NS[^(]*)\(\)/ &&
$0 !~ /^my/) {
getline line;
gsub(/[[:space:]]*\\$/, "");
gsub(/[[:space:]]/, "", line);
$0 = $0 " " line;
}
$0 = gensub(/(EXPORT_SYMBOL_NS[^(]*)\(([^,]+), ([^)]+)\)/,
"\\1(\\2, \"\\3\")", "g");
}
}
{ print }' $file;
done
Requested-by: Masahiro Yamada <masahiroy@kernel.org>
Signed-off-by: Peter Zijlstra (Intel) <peterz@infradead.org>
Link: https://mail.google.com/mail/u/2/#inbox/FMfcgzQXKWgMmjdFwwdsfgxzKpVHWPlc
Acked-by: Greg KH <gregkh@linuxfoundation.org>
Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
2024-12-02 15:59:47 +01:00
|
|
|
MODULE_IMPORT_NS("I2C_DW_COMMON");
|