494 lines
16 KiB
C
494 lines
16 KiB
C
/*
|
||
* Copyright : (C) 2022 Phytium Information Technology, Inc.
|
||
* All Rights Reserved.
|
||
*
|
||
* This program is OPEN SOURCE software: you can redistribute it and/or modify it
|
||
* under the terms of the Phytium Public License as published by the Phytium Technology Co.,Ltd,
|
||
* either version 1.0 of the License, or (at your option) any later version.
|
||
*
|
||
* This program is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY;
|
||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||
* See the Phytium Public License for more details.
|
||
*
|
||
*
|
||
* FilePath: fusb_hub.c
|
||
* Date: 2022-02-11 13:33:07
|
||
* LastEditTime: 2022-02-17 17:48:52
|
||
* Description: This files is for implmentation of USB hub function
|
||
* you may refer to chapter 11 Hub specification for details
|
||
*
|
||
* Modify History:
|
||
* Ver Who Date Changes
|
||
* ----- ------ -------- --------------------------------------
|
||
* 1.0 Zhugengyu 2022/2/7 init commit
|
||
*/
|
||
|
||
#include "fkernel.h"
|
||
#include "fdebug.h"
|
||
#include "fassert.h"
|
||
|
||
#include "fusb_private.h"
|
||
#include "fusb_generic_hub.h"
|
||
|
||
#define FUSB_DEBUG_TAG "FUSB_HUB"
|
||
#define FUSB_ERROR(format, ...) FT_DEBUG_PRINT_E(FUSB_DEBUG_TAG, format, ##__VA_ARGS__)
|
||
#define FUSB_WARN(format, ...) FT_DEBUG_PRINT_W(FUSB_DEBUG_TAG, format, ##__VA_ARGS__)
|
||
#define FUSB_INFO(format, ...) FT_DEBUG_PRINT_I(FUSB_DEBUG_TAG, format, ##__VA_ARGS__)
|
||
#define FUSB_DEBUG(format, ...) FT_DEBUG_PRINT_D(FUSB_DEBUG_TAG, format, ##__VA_ARGS__)
|
||
|
||
/* assume that FUSB_REQ_HOST_TO_DEVICE is overwritten if necessary */
|
||
#define DR_PORT FUsbGenerateReqType(FUSB_REQ_HOST_TO_DEVICE, FUSB_REQ_TYPE_CLASS, FUSB_REQ_RECP_OTHER) /* 10100011B */
|
||
#define DR_HUB FUsbGenerateReqType(FUSB_REQ_HOST_TO_DEVICE, FUSB_REQ_TYPE_CLASS, FUSB_REQ_RECP_DEV) /* 10100000B */
|
||
|
||
#define FUSB_HUB_PORT_STATUS 0
|
||
#define FUSB_HUB_PORT_CHANGE 1
|
||
|
||
#define FUSB_HUB_PORT_BUF_LEN 2
|
||
|
||
/* status (and status change) bits, refer to Table 10-10, Port Status Field in USB spec */
|
||
#define FUSB_HUB_STATUS_PORT_CONNECTION BIT(0) /* reflects if device is currently connected to this port */
|
||
#define FUSB_HUB_STATUS_PORT_ENABLE BIT(1) /* reflects if this port is enabled */
|
||
#define FUSB_HUB_STATUS_PORT_SUSPEND BIT(2) /* reflects if this port is suspend, only for USB2 */
|
||
#define FUSB_HUB_STATUS_PORT_OVER_CURRENT BIT(3) /* reports over-current conditions in this port */
|
||
#define FUSB_HUB_STATUS_PORT_RESET BIT(4) /* reset signaling asserted */
|
||
#define FUSB_HUB_STATUS_BH_PORT_RESET BIT(5) /* warm reset completed */
|
||
#define FUSB_HUB_STATUS_PORT_LINK_STATE BIT(6) /* link state changed */
|
||
#define FUSB_HUB_STATUS_PORT_PORT_CONFIG_ERROR BIT(7) /* port fails to config */
|
||
|
||
/* feature selectors (for setting / clearing features), refer to USB spec. Table 10-17. Hub Class Feature Selectors for details */
|
||
#define FUSB_HUB_SEL_PORT_RESET 4
|
||
#define FUSB_HUB_SEL_PORT_POWER 8
|
||
#define FUSB_HUB_SEL_C_PORT_CONNECTION 16
|
||
#define FUSB_HUB_SEL_C_PORT_ENABLE 17 /* USB2 only */
|
||
#define FUSB_HUB_SEL_C_PORT_SUSPEND 18 /* USB2 only */
|
||
#define FUSB_HUB_SEL_C_PORT_OVER_CURRENT 19
|
||
#define FUSB_HUB_SEL_C_PORT_RESET 20
|
||
#define FUSB_HUB_SEL_C_PORT_LINK_STATE 25
|
||
#define FUSB_HUB_SEL_C_PORT_CONFIG_ERROR 26
|
||
#define FUSB_HUB_SEL_C_BH_PORT_RESET 27
|
||
|
||
/* request type (USB 3.0 hubs only) */
|
||
#define SET_HUB_DEPTH 12
|
||
|
||
/**
|
||
* @name: FUsbHubIntrEp
|
||
* @msg: 获取USB Hub的中断端点
|
||
* @return {FUsbEndpoint *} 中断类型的功能端点
|
||
* @param {FUsbDev} *dev, Hub实例
|
||
*/
|
||
static FUsbEndpoint *FUsbHubIntrEp(FUsbDev *const dev)
|
||
{
|
||
FASSERT(dev);
|
||
int i;
|
||
|
||
for (i = 0; i < dev->num_endp; ++i)
|
||
{
|
||
if (dev->endpoints[i].type == FUSB_INTERRUPT_EP &&
|
||
dev->endpoints[i].direction == FUSB_IN)
|
||
return &dev->endpoints[i];
|
||
}
|
||
|
||
return NULL;
|
||
}
|
||
|
||
/**
|
||
* @name: FUsbHubPortStatusChange
|
||
* @msg: Usb Hub的Port状态变化回调函数
|
||
* @return {FUsbTransCode} USB请求返回值
|
||
* @param {FUsbDev} *dev, Hub实例
|
||
* @param {int} port, Port号
|
||
*/
|
||
static FUsbTransCode FUsbHubPortStatusChange(FUsbDev *const dev, const int port)
|
||
{
|
||
FASSERT(dev);
|
||
unsigned short buf[FUSB_HUB_PORT_BUF_LEN]; /* Hub Status and Change Status */
|
||
FUsbTransCode ret = FUsbGetStatus(dev, port, DR_PORT, sizeof(buf), buf);
|
||
|
||
if (ret >= FUSB_CC_ZERO_BYTES)
|
||
{
|
||
ret = buf[FUSB_HUB_PORT_CHANGE] & FUSB_HUB_STATUS_PORT_CONNECTION;
|
||
if (ret)
|
||
{
|
||
ret = FUsbClearFeature(dev, port, FUSB_HUB_SEL_C_PORT_CONNECTION, DR_PORT);
|
||
}
|
||
}
|
||
|
||
return ret;
|
||
}
|
||
|
||
/**
|
||
* @name: FUsbHubPortConnected
|
||
* @msg: Usb Hub的Port连接回调函数
|
||
* @return {FUsbTransCode} USB请求返回值
|
||
* @param {FUsbDev} *dev, Hub实例
|
||
* @param {int} port, Port号
|
||
*/
|
||
static FUsbTransCode FUsbHubPortConnected(FUsbDev *const dev, const int port)
|
||
{
|
||
FASSERT(dev);
|
||
unsigned short buf[FUSB_HUB_PORT_BUF_LEN]; /* Hub Status and Change Status */
|
||
FUsbTransCode ret = FUsbGetStatus(dev, port, DR_PORT, sizeof(buf), buf);
|
||
|
||
if (ret >= FUSB_CC_ZERO_BYTES)
|
||
{
|
||
ret = buf[FUSB_HUB_PORT_STATUS] & FUSB_HUB_STATUS_PORT_CONNECTION;
|
||
}
|
||
|
||
return ret;
|
||
}
|
||
|
||
/**
|
||
* @name: FUsbHubPortInReset
|
||
* @msg: 检查Hub port是否处于Reset状态
|
||
* @return {FUsbTransCode} USB请求返回值
|
||
* @param {FUsbDev} *dev, Hub实例
|
||
* @param {int} port, Port号
|
||
*/
|
||
static FUsbTransCode FUsbHubPortInReset(FUsbDev *const dev, const int port)
|
||
{
|
||
FASSERT(dev);
|
||
unsigned short buf[FUSB_HUB_PORT_BUF_LEN]; /* Hub Status and Change Status */
|
||
FUsbTransCode ret = FUsbGetStatus(dev, port, DR_PORT, sizeof(buf), buf);
|
||
|
||
if (ret >= FUSB_CC_ZERO_BYTES)
|
||
ret = buf[FUSB_HUB_PORT_STATUS] & FUSB_HUB_STATUS_PORT_RESET;
|
||
|
||
return ret;
|
||
}
|
||
|
||
/**
|
||
* @name: FUsbHubPortEnabled
|
||
* @msg: 检查Hub port是否已使能
|
||
* @return {FUsbTransCode} USB请求返回值
|
||
* @param {FUsbDev} *dev, Hub实例
|
||
* @param {int} port, Port号
|
||
*/
|
||
static FUsbTransCode FUsbHubPortEnabled(FUsbDev *const dev, const int port)
|
||
{
|
||
FASSERT(dev);
|
||
unsigned short buf[FUSB_HUB_PORT_BUF_LEN]; /* Hub Status and Change Status */
|
||
FUsbTransCode ret = FUsbGetStatus(dev, port, DR_PORT, sizeof(buf), buf);
|
||
|
||
if (ret >= FUSB_CC_ZERO_BYTES)
|
||
ret = buf[FUSB_HUB_PORT_STATUS] & FUSB_HUB_STATUS_PORT_ENABLE;
|
||
|
||
return ret;
|
||
}
|
||
|
||
/**
|
||
* @name: FUsbHubPortSpeed
|
||
* @msg: 获取Hub port的速度类型
|
||
* @return {FUsbSpeed} Port的速度类型,支持SuperSpeed和HighSpeed
|
||
* @param {FUsbDev} *dev, Hub实例
|
||
* @param {int} port, Port号
|
||
*/
|
||
static FUsbSpeed FUsbHubPortSpeed(FUsbDev *const dev, const int port)
|
||
{
|
||
FASSERT(dev);
|
||
unsigned short buf[FUSB_HUB_PORT_BUF_LEN]; /* Hub Status and Change Status */
|
||
FUsbTransCode ret = FUsbGetStatus(dev, port, DR_PORT, sizeof(buf), buf);
|
||
int speed;
|
||
|
||
if (ret >= FUSB_CC_ZERO_BYTES && (buf[FUSB_HUB_PORT_STATUS] & FUSB_HUB_STATUS_PORT_ENABLE))
|
||
{
|
||
/* SuperSpeed hubs can only have SuperSpeed devices. */
|
||
if (FUsbIsSuperSpeed(dev->speed))
|
||
return dev->speed;
|
||
|
||
/*[bit] 10 9 (USB 2.0 port status word)
|
||
* 0 0 full speed
|
||
* 0 1 low speed
|
||
* 1 0 high speed
|
||
* 1 1 invalid
|
||
*/
|
||
speed = (buf[FUSB_HUB_PORT_STATUS] >> 9) & 0x3;
|
||
if (speed != 0x3) /* high-speed device */
|
||
return speed;
|
||
}
|
||
|
||
return FUSB_UNKNOWN_SPEED;
|
||
}
|
||
|
||
/**
|
||
* @name: FUsbHubEnablePort
|
||
* @msg: 使能Hub port
|
||
* @return {FUsbTransCode} USB请求返回值
|
||
* @param {FUsbDev} *dev, Hub实例
|
||
* @param {int} port, port号
|
||
*/
|
||
static FUsbTransCode FUsbHubEnablePort(FUsbDev *const dev, const int port)
|
||
{
|
||
FASSERT(dev);
|
||
return FUsbSetFeature(dev, port, FUSB_HUB_SEL_PORT_POWER, DR_PORT);
|
||
}
|
||
|
||
/**
|
||
* @name: FUsbHubStartPortReset
|
||
* @msg: 开始Reset Hub port
|
||
* @return {FUsbTransCode} USB请求返回值
|
||
* @param {FUsbDev} *dev, Hub实例
|
||
* @param {int} port, port号
|
||
*/
|
||
static FUsbTransCode FUsbHubStartPortReset(FUsbDev *const dev, const int port)
|
||
{
|
||
FASSERT(dev);
|
||
return FUsbSetFeature(dev, port, FUSB_HUB_SEL_PORT_RESET, DR_PORT);
|
||
}
|
||
|
||
static void FUsbHubSetHubDepth(FUsbDev *const dev)
|
||
{
|
||
FASSERT(dev);
|
||
FUsbDevReq dr =
|
||
{
|
||
.bmRequestType = FUsbGenerateReqType(FUSB_REQ_HOST_TO_DEVICE,
|
||
FUSB_REQ_TYPE_CLASS, FUSB_REQ_RECP_DEV),
|
||
.bRequest = SET_HUB_DEPTH,
|
||
.wValue = 0,
|
||
.wIndex = 0,
|
||
.wLength = 0,
|
||
};
|
||
FUsbDev *parent = dev;
|
||
|
||
FASSERT(dev->controller);
|
||
while (parent->hub > 0)
|
||
{
|
||
FASSERT(dev->controller->devices[parent->hub]);
|
||
parent = dev->controller->devices[parent->hub];
|
||
dr.wValue++;
|
||
}
|
||
|
||
FUsbTransCode ret = dev->controller->control(dev, FUSB_OUT, sizeof(dr), &dr, 0, NULL);
|
||
if (ret < FUSB_CC_ZERO_BYTES)
|
||
{
|
||
FUSB_ERROR("Failed SET_HUB_DEPTH(%d) on hub %d: %d ",
|
||
dr.wValue, dev->address, ret);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
static const FUsbGenericHubOps FUSB_HUB_OPS =
|
||
{
|
||
.hub_status_changed = NULL,
|
||
.port_status_changed = FUsbHubPortStatusChange,
|
||
.port_connected = FUsbHubPortConnected,
|
||
.port_in_reset = FUsbHubPortInReset,
|
||
.port_enabled = FUsbHubPortEnabled,
|
||
.port_speed = FUsbHubPortSpeed,
|
||
.enable_port = FUsbHubEnablePort,
|
||
.disable_port = NULL,
|
||
.start_port_reset = FUsbHubStartPortReset,
|
||
.reset_port = FUsbGenericHubResetPort,
|
||
};
|
||
|
||
/* Clear CSC if set and enumerate port if it's connected regardless of change
|
||
bits. Some broken hubs don't set CSC if already connected during reset. */
|
||
static void FUsbHubPortInit(FUsbDev *const dev, const int port)
|
||
{
|
||
FASSERT(dev);
|
||
unsigned short buf[FUSB_HUB_PORT_BUF_LEN]; /* Hub Status and Change Status */
|
||
FUsbTransCode ret = FUsbGetStatus(dev, port, DR_PORT, sizeof(buf), buf);
|
||
|
||
if (ret < FUSB_CC_ZERO_BYTES)
|
||
return;
|
||
|
||
if (buf[FUSB_HUB_PORT_CHANGE] & FUSB_HUB_STATUS_PORT_CONNECTION)
|
||
FUsbClearFeature(dev, port, FUSB_HUB_SEL_C_PORT_CONNECTION, DR_PORT);
|
||
|
||
if (buf[FUSB_HUB_PORT_STATUS] & FUSB_HUB_STATUS_PORT_CONNECTION)
|
||
{
|
||
FUSB_INFO("usbhub: Port coldplug at %d ", port);
|
||
FUsbGenericHubScanPort(dev, port);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
/**
|
||
* @name: FUsbHubHandlePortChange
|
||
* @msg: Hub端口状态变化的处理回调函数
|
||
* @return {*}
|
||
* @param {FUsbDev} *dev
|
||
* @param {int} port
|
||
*/
|
||
static FUsbTransCode FUsbHubHandlePortChange(FUsbDev *const dev, const int port)
|
||
{
|
||
FASSERT(dev);
|
||
static const struct
|
||
{
|
||
unsigned short change_bit;
|
||
unsigned short clear_sel;
|
||
} change_bits[] =
|
||
{
|
||
{FUSB_HUB_STATUS_PORT_CONNECTION, FUSB_HUB_SEL_C_PORT_CONNECTION},
|
||
{FUSB_HUB_STATUS_PORT_ENABLE, FUSB_HUB_SEL_C_PORT_ENABLE},
|
||
{FUSB_HUB_STATUS_PORT_SUSPEND, FUSB_HUB_SEL_C_PORT_SUSPEND},
|
||
{FUSB_HUB_STATUS_PORT_OVER_CURRENT, FUSB_HUB_SEL_C_PORT_OVER_CURRENT},
|
||
{FUSB_HUB_STATUS_PORT_RESET, FUSB_HUB_SEL_C_PORT_RESET},
|
||
{FUSB_HUB_STATUS_BH_PORT_RESET, FUSB_HUB_SEL_C_BH_PORT_RESET},
|
||
{FUSB_HUB_STATUS_PORT_LINK_STATE, FUSB_HUB_SEL_C_PORT_LINK_STATE},
|
||
{FUSB_HUB_STATUS_PORT_PORT_CONFIG_ERROR, FUSB_HUB_SEL_C_PORT_CONFIG_ERROR},
|
||
};
|
||
|
||
FUsbTransCode ret = 0;
|
||
unsigned int i;
|
||
unsigned short checked_bits = 0;
|
||
unsigned short buf[FUSB_HUB_PORT_BUF_LEN] = {0, 0}; /* Hub Status and Change Status */
|
||
|
||
ret = FUsbGetStatus(dev, port, DR_PORT, sizeof(buf), buf);
|
||
if (ret < FUSB_CC_ZERO_BYTES)
|
||
return ret;
|
||
|
||
/*
|
||
* Second word holds the change bits. The interrupt transfer shows
|
||
* a logical or of these bits, so we have to clear them all.
|
||
*/
|
||
for (i = 0; i < ARRAY_SIZE(change_bits); ++i)
|
||
{
|
||
if (buf[FUSB_HUB_PORT_CHANGE] & change_bits[i].change_bit)
|
||
{
|
||
/* clear feature if specific change bit = 1 */
|
||
FUsbClearFeature(dev, port, change_bits[i].clear_sel, DR_PORT);
|
||
}
|
||
|
||
checked_bits |= change_bits[i].change_bit;
|
||
}
|
||
|
||
if (buf[FUSB_HUB_PORT_CHANGE] & ~checked_bits)
|
||
FUSB_DEBUG("Spurious change bit at port %d ", port);
|
||
|
||
/* Now, handle connection changes. */
|
||
if (buf[FUSB_HUB_PORT_CHANGE] & FUSB_HUB_STATUS_PORT_CONNECTION)
|
||
{
|
||
FUSB_DEBUG("Port change at %d ", port);
|
||
ret = FUsbGenericHubScanPort(dev, port);
|
||
}
|
||
|
||
return ret;
|
||
}
|
||
|
||
/**
|
||
* @name: FUsbHubPoll
|
||
* @msg: 轮询Hub的所有端口,检查端口状态变化
|
||
* @return {*}
|
||
* @param {FUsbDev} *dev, Hub设备实例
|
||
*/
|
||
static void FUsbHubPoll(FUsbDev *const dev)
|
||
{
|
||
FASSERT(dev);
|
||
int port, i;
|
||
u8 buf[32] = {0};
|
||
const u8 *ibuf;
|
||
|
||
/* First, gather all change bits from finished interrupt transfers. */
|
||
const size_t port_bytes = min(ARRAY_SIZE(buf),
|
||
(size_t)DIV_ROUND_UP(FUSB_GEN_HUB_GET(dev)->num_ports + 1, 8));
|
||
|
||
while (NULL != (ibuf = dev->controller->poll_intr_queue(FUSB_GEN_HUB_GET(dev)->data)))
|
||
{
|
||
for (i = 0; (size_t)i < port_bytes; ++i)
|
||
buf[i] |= ibuf[i];
|
||
}
|
||
|
||
for (port = 1; port <= FUSB_GEN_HUB_GET(dev)->num_ports; ++port)
|
||
{
|
||
/* ports start at bit1; bit0 is hub status change */
|
||
if (buf[port / 8] & (1 << (port % 8)))
|
||
{
|
||
if (FUsbHubHandlePortChange(dev, port) < 0)
|
||
return;
|
||
}
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
/**
|
||
* @name: FUsbHubDestory
|
||
* @msg: USB Hub的去初始化函数
|
||
* @return {*}
|
||
* @param {FUsbDev} *dev, Hub设备实例
|
||
*/
|
||
static void FUsbHubDestory(FUsbDev *const dev)
|
||
{
|
||
FASSERT(dev);
|
||
FUsbEndpoint *const intr_ep = FUsbHubIntrEp(dev);
|
||
FASSERT(intr_ep); /* interrupt ep must exists */
|
||
dev->controller->destroy_intr_queue(intr_ep, FUSB_GEN_HUB_GET(dev)->data);
|
||
FUsbGenericHubDestory(dev);
|
||
}
|
||
|
||
/**
|
||
* @name: FUsbHubInit
|
||
* @msg: USB Hub的初始化函数,由应用程序注册到FUSB框架中
|
||
* @return {*}
|
||
* @param {FUsbDev} *dev, Hub设备实例
|
||
*/
|
||
void FUsbHubInit(FUsbDev *dev)
|
||
{
|
||
FASSERT(dev);
|
||
FUsbEndpoint *const intr_ep = FUsbHubIntrEp(dev); /* get the first intrrupt ep found */
|
||
if (NULL == intr_ep)
|
||
{
|
||
FUSB_ERROR("No interrupt-in endpoint found ");
|
||
return;
|
||
}
|
||
|
||
FASSERT(dev->controller);
|
||
|
||
/* Get number of ports from hub descriptor */
|
||
int type = FUsbIsSuperSpeed(dev->speed) ? FUSB_DESC_TYPE_SUPER_SPEED_HUB : FUSB_DESC_TYPE_HUB; /* similar enough */
|
||
FUsbHubDescriptor desc; /* won't fit the whole thing, we don't care */
|
||
if (FUsbGetDescriptor(dev, FUsbGenerateReqType(FUSB_REQ_DEVICE_TO_HOST, FUSB_REQ_TYPE_CLASS, FUSB_REQ_RECP_DEV), type, 0, &desc, sizeof(desc)) != sizeof(desc))
|
||
{
|
||
FUSB_ERROR("FUsbGetDescriptor(HUB) failed ");
|
||
FUsbDetachDev(dev->controller, dev->address);
|
||
return;
|
||
}
|
||
|
||
if (FUsbIsSuperSpeed(dev->speed))
|
||
{
|
||
FUsbHubSetHubDepth(dev);
|
||
}
|
||
|
||
/*
|
||
* Register interrupt transfer:
|
||
* one bit per port + one bit for the hub,
|
||
* 20 transfers in the queue, like our HID driver,
|
||
* one transfer per 256ms
|
||
*/
|
||
void *const intrq = dev->controller->create_intr_queue(
|
||
intr_ep, intr_ep->maxpacketsize, 20, 256);
|
||
if (NULL == intrq)
|
||
{
|
||
FUsbDetachDev(dev->controller, dev->address);
|
||
return;
|
||
}
|
||
|
||
/*
|
||
* Limit the number of ports by the max packet size of
|
||
* the interrupt endpoint. This shouldn't be necessary
|
||
* but prevents a potential overflow in FUsbHubPoll().
|
||
*/
|
||
const unsigned int num_ports =
|
||
min((int)desc.bNbrPorts, intr_ep->maxpacketsize * 8 - 1);
|
||
if (FUsbGenericHubInit(dev, num_ports, &FUSB_HUB_OPS))
|
||
{
|
||
dev->controller->destroy_intr_queue(intr_ep, intrq);
|
||
FUsbDetachDev(dev->controller, dev->address);
|
||
return;
|
||
}
|
||
|
||
unsigned int port;
|
||
for (port = 1; port <= num_ports; ++port)
|
||
{
|
||
FUsbHubPortInit(dev, port);
|
||
}
|
||
|
||
FUSB_GEN_HUB_GET(dev)->data = intrq;
|
||
dev->poll = FUsbHubPoll;
|
||
dev->destroy = FUsbHubDestory;
|
||
|
||
return;
|
||
}
|